diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0163807 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +dist/ +build/ diff --git a/lxcd.completion b/lxcd.completion new file mode 100644 index 0000000..668e022 --- /dev/null +++ b/lxcd.completion @@ -0,0 +1,65 @@ +#!/bin/bash + +_lxcd_completion() { + local cur prev words cword + _init_completion || return + + local commands="create clone rename enter list delete add stop start restart" + + if [ $cword -eq 1 ]; then + COMPREPLY=($(compgen -W "${commands}" -- "$cur")) + return 0 + fi + + local subcommand="${words[1]}" + + # Use sudo for lxc if the user isn't in the lxd group + if id -nG | grep -qw lxd 2>/dev/null; then + local LXC="lxc" + else + local LXC="sudo lxc" + fi + + # Fetch lxcd-managed container names + local containers=$($LXC list user.lxcd=true -c n --format csv 2>/dev/null | tr '\n' ' ') + + case "${subcommand}" in + create) + if [[ "$cur" == -* ]]; then + COMPREPLY=($(compgen -W "--nested --sshd --git --certs --aptcache --ssh-client" -- "$cur")) + else + COMPREPLY=($(compgen -W "ubuntu: ubuntu/24.04 ubuntu/22.04 debian/12 debian/11" -- "$cur")) + fi + ;; + + clone) + if [ $cword -eq 2 ]; then + COMPREPLY=($(compgen -W "${containers}" -- "$cur")) + fi + ;; + + rename|mv) + if [ $cword -eq 2 ]; then + COMPREPLY=($(compgen -W "${containers}" -- "$cur")) + fi + ;; + + enter|delete|stop|start|restart) + COMPREPLY=($(compgen -W "${containers}" -- "$cur")) + ;; + + add) + if [ $cword -eq 2 ]; then + COMPREPLY=($(compgen -W "${containers}" -- "$cur")) + elif [[ "$cur" == -* ]]; then + COMPREPLY=($(compgen -W "--nested --sshd --git --certs --aptcache --ssh-client" -- "$cur")) + fi + ;; + + *) + COMPREPLY=() + ;; + esac +} + +complete -F _lxcd_completion lxcd diff --git a/lxcd.py b/lxcd.py index 52db575..d8a7363 100755 --- a/lxcd.py +++ b/lxcd.py @@ -1,8 +1,11 @@ #!/usr/bin/env python3 import argparse +import grp import os +import re import subprocess import sys +import time from pathlib import Path def get_real_user_info(): @@ -24,6 +27,39 @@ def get_real_user_info(): Path.home() ) +def detect_timezone(): + tz = "UTC" + tz_file = Path("/etc/timezone") + if tz_file.exists(): + tz = tz_file.read_text().strip() + else: + try: + result = subprocess.run( + ["timedatectl"], capture_output=True, text=True, check=False + ) + m = re.search(r"Time zone:\s+(\S+)", result.stdout) + if m: + tz = m.group(1) + except FileNotFoundError: + pass + return tz + + +def detect_locale(): + return os.environ.get("LANG", "en_US.UTF-8") + + +def lxc_prefix(): + """Return ['sudo'] if the user isn't root and isn't in the lxd group.""" + if os.getuid() == 0: + return [] + try: + if grp.getgrnam("lxd").gr_gid in os.getgroups(): + return [] + except KeyError: + pass + return ["sudo"] + def run_cmd(cmd, check=True): """Helper to run shell commands.""" try: @@ -32,32 +68,132 @@ def run_cmd(cmd, check=True): print(f"Error executing command: {' '.join(cmd)}", file=sys.stderr) sys.exit(e.returncode) +def lxc_run(args, check=True): + """Run an lxc command via run_cmd, auto-adding sudo if needed.""" + return run_cmd([*lxc_prefix(), "lxc", *args], check=check) + +def lxc_subprocess(args, **kwargs): + """Run an lxc command via subprocess.run, auto-adding sudo if needed.""" + return subprocess.run([*lxc_prefix(), "lxc", *args], **kwargs) + def handle_list(args): - # Pass the user.lxcd=true filter directly to LXD's native listing engine - run_cmd(["lxc", "list", "user.lxcd=true"]) + print("Fetching lxcd container environments...") + + result = lxc_subprocess( + ["list", "user.lxcd=true", "--format", "json"], + capture_output=True, text=True, check=False + ) + if result.returncode != 0: + print("Error communicating with LXD.", file=sys.stderr) + return + + import json + containers = json.loads(result.stdout) + + print(f"\n{'CONTAINER NAME':<25} {'STATUS':<12} {'IPV4 ADDRESS':<25} {'ACTIVE OPTIONS':<25}") + print("-" * 87) + + count = 0 + for c in containers: + name = c["name"] + status = c["status"] + ipv4 = "N/A" + state = c.get("state") + if state: + networks = state.get("network", {}) + eth0 = networks.get("eth0", {}) + addrs = [a["address"] for a in eth0.get("addresses", []) + if a.get("family") == "inet"] + if addrs: + ipv4 = addrs[0] + + active_opts = [] + + # Check nesting (works whether running or stopped) + nesting_check = lxc_subprocess( + ["config", "get", name, "security.nesting"], + capture_output=True, text=True, check=False + ) + if nesting_check.stdout.strip() == "true": + active_opts.append("nesting") + + # Check certs (works whether running or stopped) + dev_check = lxc_subprocess( + ["config", "device", "show", name], + capture_output=True, text=True, check=False + ) + if "host-certs" in dev_check.stdout: + active_opts.append("certs") + + # Check git (works whether running or stopped) + if "gitconfig" in dev_check.stdout: + active_opts.append("git") + + # Running-only checks + if status.upper() == "RUNNING": + ssh_check = lxc_subprocess( + ["exec", name, "--", "systemctl", "is-active", "ssh.socket"], + capture_output=True, text=True, check=False + ) + if ssh_check.stdout.strip() == "active": + active_opts.append("sshd") + + apt_cache_check = lxc_subprocess( + ["exec", name, "--", "test", "-f", "/etc/apt/apt.conf.d/00lxcd-proxy"], + capture_output=True, text=True, check=False + ) + if apt_cache_check.returncode == 0: + active_opts.append("aptcache") + + ssh_client_check = lxc_subprocess( + ["exec", name, "--", "which", "ssh"], + capture_output=True, text=True, check=False + ) + if ssh_client_check.returncode == 0: + active_opts.append("ssh-client") + else: + # Offline guess: check cloud-init config for apt proxy + ci_check = lxc_subprocess( + ["config", "get", name, "cloud-init.user-data"], + capture_output=True, text=True, check=False + ) + if "Acquire::http::Proxy" in ci_check.stdout: + active_opts.append("aptcache(?)") + if "openssh-client" in ci_check.stdout: + active_opts.append("ssh-client(?)") + + opts_str = ", ".join(active_opts) if active_opts else "none" + print(f"{name:<25} {status:<12} {ipv4:<25} {opts_str:<25}") + count += 1 + + if count == 0: + print("No active lxcd environments found.") + print() def handle_clone(args): print(f"Cloning container '{args.source}' to '{args.name}'...") # 1. Native LXD copy (duplicates the filesystem, mounts, and tags) - run_cmd(["lxc", "copy", args.source, args.name]) + lxc_run([ "copy", args.source, args.name]) # 2. Start the new cloned container print(f"Starting cloned container '{args.name}'...") - run_cmd(["lxc", "start", args.name]) + lxc_run([ "start", args.name]) # 3. Wait for network/cloud-init stabilization - run_cmd(["lxc", "exec", args.name, "--", "cloud-init", "status", "--wait"], check=False) + lxc_run([ "exec", args.name, "--", "cloud-init", "status", "--wait"], check=False) print(f"Container '{args.name}' cloned successfully from '{args.source}'!") def handle_create(args): host_username, uid, gid, host_home = get_real_user_info() - + host_timezone = detect_timezone() + host_locale = detect_locale() + # Paths for targeted workspace sharing host_coder_dir = host_home / "Coder" container_coder_path = f"/home/{host_username}/Coder" - + # Ensure the 'Coder' directory exists on the host machine if not host_coder_dir.exists(): print(f"Creating missing directory on host: {host_coder_dir}") @@ -65,9 +201,28 @@ def handle_create(args): os.chown(host_coder_dir, uid, gid) print(f"Creating LXD container '{args.name}' from image '{args.image}'...") - - # Cloud-init configuration that drops stock 'ubuntu' user and seeds defaults + + # Determine SSH action based on --sshd flag + ssh_action = "systemctl disable --now ssh.service ssh.socket" + if args.sshd: + ssh_action = "systemctl enable --now ssh.service ssh.socket" + print("Security profiles adjusted: SSH daemon and socket explicitly requested.") + + # Cloud-init configuration + apt_proxy_cmd = ( + f" - mkdir -p /etc/apt/apt.conf.d\n" + f" - echo 'Acquire::http::Proxy \"{args.aptcache}\";' > /etc/apt/apt.conf.d/00lxcd-proxy\n" + ) if args.aptcache else "" + + ssh_client_packages = "packages:\n - openssh-client\n" if args.ssh_client else "" + ssh_client_cmd = ( + f" - mkdir -p /home/{host_username}/.ssh\n" + f" - chmod 700 /home/{host_username}/.ssh\n" + ) if args.ssh_client else "" + cloud_config = f"""#cloud-config +timezone: {host_timezone} +locale: {host_locale} users: - name: {host_username} gecos: lxcd user @@ -81,31 +236,34 @@ users: groups: - {host_username} +{ssh_client_packages}bootcmd: + - mkdir -p /home/{host_username} + - chown {uid}:{gid} /home/{host_username} + runcmd: - # Copy missing skeleton files (without overwriting existing mounts) and fix ownership - cp -rpn /etc/skel/. /home/{host_username}/ - - chown -R {host_username}:{host_username} /home/{host_username}/ -""" + - mkdir -p {container_coder_path} +{ssh_client_cmd} - chown -R {host_username}:{host_username} /home/{host_username} + - {ssh_action} +{apt_proxy_cmd}""" # Initialize container - run_cmd(["lxc", "init", args.image, args.name]) + lxc_run([ "init", args.image, args.name]) # Tag the container so lxcd knows it owns it print("Tagging container as managed by lxcd...") - run_cmd(["lxc", "config", "set", args.name, "user.lxcd", "true"]) + lxc_run([ "config", "set", args.name, "user.lxcd", "true"]) # Inject Cloud-init settings print(f"Injecting cloud-init config for user '{host_username}'...") - run_cmd(["lxc", "config", "set", args.name, "cloud-init.user-data", cloud_config]) + lxc_run([ "config", "set", args.name, "cloud-init.user-data", cloud_config]) # Mount host's Coder directory using shift=true print(f"Mounting host {host_coder_dir} -> {container_coder_path} (shift=true)...") - run_cmd([ - "lxc", "config", "device", "add", args.name, "host-coder", "disk", - f"source={host_coder_dir}", - f"path={container_coder_path}", - "shift=true" - ]) + lxc_run(["config", "device", "add", args.name, "host-coder", "disk", + f"source={host_coder_dir}", + f"path={container_coder_path}", + "shift=true"]) # Handle --git option if args.git: @@ -113,25 +271,43 @@ runcmd: if gitconfig_path.exists(): container_git_path = f"/home/{host_username}/.gitconfig" print(f"Mapping host git configurations to {container_git_path} (shift=true)...") - run_cmd([ - "lxc", "config", "device", "add", args.name, "gitconfig", "disk", - f"source={gitconfig_path}", - f"path={container_git_path}", - "shift=true" - ]) + lxc_run(["config", "device", "add", args.name, "gitconfig", "disk", + f"source={gitconfig_path}", + f"path={container_git_path}", + "shift=true"]) else: print(f"Warning: --git requested, but {gitconfig_path} was not found on host.", file=sys.stderr) # Handle --nested option if args.nested: print("Enabling nesting capabilities...") - run_cmd(["lxc", "config", "set", args.name, "security.nesting", "true"]) - run_cmd(["lxc", "config", "set", args.name, "security.syscalls.intercept.mknod", "true"]) + lxc_run([ "config", "set", args.name, "security.nesting", "true"]) + lxc_run([ "config", "set", args.name, "security.syscalls.intercept.mknod", "true"]) + + # Handle --aptcache option + if args.aptcache: + print(f"APT cache/proxy will be configured to {args.aptcache} via cloud-init...") + + # Handle --ssh-client option + if args.ssh_client: + print(f"SSH client and ~/.ssh directory will be set up for '{host_username}' via cloud-init...") + + # Handle --certs option + if args.certs: + host_certs = Path("/etc/ssl/certs/ca-certificates.crt") + if host_certs.exists(): + print("Mounting host ca-certificates bundle securely into container...") + lxc_run(["config", "device", "add", args.name, "host-certs", "disk", + f"source={host_certs}", + f"path={host_certs}", + "shift=true"]) + else: + print("Warning: Host cert bundle path not found. Skipping certificate mount configuration.") # Start container and await cloud-init provisioning print("Starting container and executing cloud-init user provisioning...") - run_cmd(["lxc", "start", args.name]) - run_cmd(["lxc", "exec", args.name, "--", "cloud-init", "status", "--wait"], check=False) + lxc_run([ "start", args.name]) + lxc_run([ "exec", args.name, "--", "cloud-init", "status", "--wait"], check=False) print(f"Container '{args.name}' is ready! Workspace mounted at {container_coder_path}") @@ -141,8 +317,8 @@ def handle_enter(args): # Dynamically find the user matching your UID inside the container try: - result = subprocess.run( - ["lxc", "exec", args.name, "--", "id", "-nu", str(uid)], + result = lxc_subprocess( + ["exec", args.name, "--", "id", "-nu", str(uid)], capture_output=True, text=True, check=True ) container_username = result.stdout.strip() @@ -172,46 +348,171 @@ def handle_enter(args): if display: env_prefix += f"DISPLAY={display} " + # Check if container has ~/.ssh (set up by --ssh-client) and start ssh-agent if so + has_ssh_client = lxc_subprocess( + ["exec", args.name, "--", "test", "-d", f"{container_home}/.ssh"], + capture_output=True, text=True, check=False + ).returncode == 0 + # Execute using native LXD flags for user, group, and working directory + prefix = lxc_prefix() + shell_cmd = "exec /bin/bash --login" + if has_ssh_client: + shell_cmd = "eval $(ssh-agent) && ssh-add && " + shell_cmd exec_cmd = [ "lxc", "exec", args.name, "--cwd", target_cwd, "--user", str(uid), "--group", str(gid), "--", - "sh", "-c", f"env {env_prefix} /bin/bash --login" + "sh", "-c", f"env {env_prefix} sh -c '{shell_cmd}'" ] - os.execvp("lxc", exec_cmd) + if prefix: + os.execvp("sudo", ["sudo", *exec_cmd]) + else: + os.execvp("lxc", exec_cmd) + +def handle_stop(args): + print(f"Stopping container '{args.name}'...") + lxc_run([ "stop", args.name], check=False) + print(f"Container '{args.name}' stopped.") + +def handle_start(args): + print(f"Starting container '{args.name}'...") + lxc_run([ "start", args.name]) + lxc_run([ "exec", args.name, "--", "cloud-init", "status", "--wait"], check=False) + print(f"Container '{args.name}' started.") + +def handle_restart(args): + print(f"Restarting container '{args.name}'...") + lxc_run([ "restart", args.name], check=False) + lxc_run([ "exec", args.name, "--", "cloud-init", "status", "--wait"], check=False) + print(f"Container '{args.name}' restarted.") def handle_delete(args): print(f"Stopping container '{args.name}'...") - run_cmd(["lxc", "stop", args.name], check=False) + lxc_run([ "stop", args.name], check=False) print(f"Deleting container '{args.name}'...") - run_cmd(["lxc", "delete", args.name]) + lxc_run([ "delete", args.name]) print(f"Container '{args.name}' deleted successfully.") +def container_exists(name): + result = lxc_subprocess( + ["info", name], + capture_output=True, text=True, check=False + ) + return result.returncode == 0 + + +def handle_add(args): + host_username, uid, gid, host_home = get_real_user_info() + + if not container_exists(args.name): + print(f"Error: Container '{args.name}' does not exist.", file=sys.stderr) + sys.exit(1) + + if not args.nested and not args.sshd and not args.git and not args.certs and not args.aptcache and not args.ssh_client: + print("Error: Specify at least one option to add.\n" + "Example: lxcd add --git --nested", file=sys.stderr) + sys.exit(1) + + state = lxc_subprocess( + ["info", args.name], + capture_output=True, text=True, check=False + ) + was_stopped = "Status: RUNNING" not in state.stdout + if was_stopped: + print(f"Starting container '{args.name}' temporarily to apply system operations...") + lxc_subprocess(["start", args.name], check=False) + time.sleep(2) + + if args.nested: + print(f"Enabling nesting support for '{args.name}'...") + lxc_run([ "config", "set", args.name, "security.nesting", "true"]) + lxc_run([ "config", "set", args.name, "security.syscalls.intercept.mknod", "true"]) + + if args.sshd: + print(f"Activating SSH daemon and socket inside '{args.name}'...") + lxc_run([ "exec", args.name, "--", "systemctl", "enable", "--now", "ssh.service", "ssh.socket"]) + + if args.git: + was_running = not was_stopped + print(f"Installing git inside '{args.name}'...") + lxc_run([ "exec", args.name, "--", "env", "DEBIAN_FRONTEND=noninteractive", "apt-get", "update", "-y"]) + lxc_run([ "exec", args.name, "--", "env", "DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y", "git"]) + + host_gitconfig = host_home / ".gitconfig" + if host_gitconfig.exists(): + print(f"Mapping host .gitconfig into '{args.name}' as a device mount...") + lxc_subprocess(["stop", args.name], check=False) + lxc_subprocess(["config", "device", "remove", args.name, "gitconfig"], + capture_output=True, text=True, check=False) + lxc_run(["config", "device", "add", args.name, "gitconfig", "disk", + f"source={host_gitconfig}", + f"path=/home/{host_username}/.gitconfig", + "shift=true"]) + if was_running: + lxc_run([ "start", args.name]) + lxc_run([ "exec", args.name, "--", "cloud-init", "status", "--wait"], check=False) + else: + print(f"Warning: Host ~/.gitconfig not found. Git was installed, but file copy skipped.") + + if args.certs: + host_certs = Path("/etc/ssl/certs/ca-certificates.crt") + if host_certs.exists(): + print(f"Mounting host ca-certificates bundle into '{args.name}'...") + lxc_subprocess(["config", "device", "remove", args.name, "host-certs"], + capture_output=True, text=True, check=False) + lxc_run(["config", "device", "add", args.name, "host-certs", "disk", + f"source={host_certs}", + f"path={host_certs}", + "shift=true"]) + else: + print("Warning: Host cert bundle path not found. Skipping certificate configuration.") + + if args.aptcache: + print(f"Configuring APT proxy to {args.aptcache} inside '{args.name}'...") + lxc_run([ "exec", args.name, "--", "mkdir", "-p", "/etc/apt/apt.conf.d"]) + lxc_run([ "exec", args.name, "--", "sh", "-c", + f"echo 'Acquire::http::Proxy \"{args.aptcache}\";' > /etc/apt/apt.conf.d/00lxcd-proxy"]) + + if args.ssh_client: + print(f"Installing SSH client and setting up ~/.ssh inside '{args.name}'...") + lxc_run([ "exec", args.name, "--", "env", "DEBIAN_FRONTEND=noninteractive", "apt-get", "update", "-y"]) + lxc_run([ "exec", args.name, "--", "env", "DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y", "openssh-client"]) + lxc_run([ "exec", args.name, "--", "mkdir", "-p", f"/home/{host_username}/.ssh"]) + lxc_run([ "exec", args.name, "--", "chmod", "700", f"/home/{host_username}/.ssh"]) + lxc_run([ "exec", args.name, "--", "chown", f"{host_username}:{host_username}", f"/home/{host_username}/.ssh"]) + + if was_stopped: + print(f"Returning '{args.name}' to its original stopped state...") + lxc_subprocess(["stop", args.name], check=False) + + print(f"\nSuccessfully updated container '{args.name}' with requested features!") + + def handle_rename(args): print(f"Preparing to rename '{args.old_name}' to '{args.new_name}'...") # 1. LXD requires containers to be stopped before renaming print(f"Stopping '{args.old_name}'...") # Using stdout/stderr redirection internally to hide the error if it's already stopped - subprocess.run( - ["lxc", "stop", args.old_name], + lxc_subprocess( + ["stop", args.old_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) # 2. Execute the rename - run_cmd(["lxc", "rename", args.old_name, args.new_name]) + lxc_run([ "rename", args.old_name, args.new_name]) # 3. Restart the container under its new name print(f"Starting renamed container '{args.new_name}'...") - run_cmd(["lxc", "start", args.new_name]) + lxc_run([ "start", args.new_name]) # 4. Wait for it to stabilize - run_cmd(["lxc", "exec", args.new_name, "--", "cloud-init", "status", "--wait"], check=False) + lxc_run([ "exec", args.new_name, "--", "cloud-init", "status", "--wait"], check=False) print(f"Container successfully renamed to '{args.new_name}'!") @@ -224,7 +525,12 @@ def main(): parser_create.add_argument("name", help="Name of the container") parser_create.add_argument("-i", "--image", default="ubuntu:", help="LXD image to use (defaults to 'ubuntu:')") parser_create.add_argument("--nested", action="store_true", help="Enable nesting") + parser_create.add_argument("--sshd", action="store_true", help="Enable SSH daemon") parser_create.add_argument("--git", action="store_true", help="Map host ~/.gitconfig into the container") + parser_create.add_argument("--certs", action="store_true", help="Mount host CA certificates bundle") + parser_create.add_argument("--aptcache", help="URL for an APT cache/proxy (e.g. http://10.0.0.1:3142)") + parser_create.add_argument("--ssh-client", action="store_true", + help="Install SSH client and set up ~/.ssh for remote access from within the container") # Clone command parser_clone = subparsers.add_parser("clone", help="Clone an existing LXD container") @@ -243,10 +549,33 @@ def main(): # List command parser_list = subparsers.add_parser("list", aliases=["ls"], help="List all managed LXD containers") + # Stop command + parser_stop = subparsers.add_parser("stop", help="Stop a running container") + parser_stop.add_argument("name", help="Name of the container to stop") + + # Start command + parser_start = subparsers.add_parser("start", help="Start a stopped container") + parser_start.add_argument("name", help="Name of the container to start") + + # Restart command + parser_restart = subparsers.add_parser("restart", help="Restart a container") + parser_restart.add_argument("name", help="Name of the container to restart") + # Delete command parser_delete = subparsers.add_parser("delete", help="Delete an LXD container") parser_delete.add_argument("name", help="Name of the container to delete") + # Add command + parser_add = subparsers.add_parser("add", help="Add features to an existing container") + parser_add.add_argument("name", help="Name of the container") + parser_add.add_argument("--nested", action="store_true", help="Enable nesting") + parser_add.add_argument("--sshd", action="store_true", help="Enable SSH daemon") + parser_add.add_argument("--git", action="store_true", help="Install git and copy ~/.gitconfig") + parser_add.add_argument("--certs", action="store_true", help="Mount host CA certificates bundle") + parser_add.add_argument("--aptcache", help="URL for an APT cache/proxy (e.g. http://10.0.0.1:3142)") + parser_add.add_argument("--ssh-client", action="store_true", + help="Install SSH client and set up ~/.ssh for remote access from within the container") + args = parser.parse_args() if args.command == "create": @@ -259,8 +588,16 @@ def main(): handle_enter(args) elif args.command in ["list", "ls"]: handle_list(args) + elif args.command == "stop": + handle_stop(args) + elif args.command == "start": + handle_start(args) + elif args.command == "restart": + handle_restart(args) elif args.command == "delete": handle_delete(args) + elif args.command == "add": + handle_add(args) if __name__ == "__main__": main() diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..90ebd60 --- /dev/null +++ b/setup.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -euo pipefail + +BIN_DIR="${BIN_DIR:-/usr/local/bin}" +COMPLETION_DIR="${COMPLETION_DIR:-/etc/bash_completion.d}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +if [ $EUID -ne 0 ]; then + echo "This script must be run with root privileges (sudo)." >&2 + echo "Usage: sudo $0" >&2 + echo "" >&2 + echo "To install to a user-writable location instead:" >&2 + echo " BIN_DIR=~/.local/bin COMPLETION_DIR=~/.bash_completion.d $0" >&2 + exit 1 +fi + +mkdir -p "$BIN_DIR" "$COMPLETION_DIR" + +echo "Installing lxcd to ${BIN_DIR}/lxcd..." +cp "$SCRIPT_DIR/lxcd.py" "${BIN_DIR}/lxcd" +chmod 755 "${BIN_DIR}/lxcd" + +if [ -f "$SCRIPT_DIR/lxcd.completion" ]; then + echo "Installing bash completion to ${COMPLETION_DIR}/lxcd..." + cp "$SCRIPT_DIR/lxcd.completion" "${COMPLETION_DIR}/lxcd" +fi + +echo "" +echo "lxcd installed successfully!" +echo "Usage: lxcd --help"