From eddf850c4f7b69b6e51d5f8add8bfaddbe27c976 Mon Sep 17 00:00:00 2001 From: Andrew Hurley Date: Sun, 20 Sep 2026 18:40:56 +0800 Subject: [PATCH] massive amazing changes --- lxcd.completion | 28 ++- lxcd.py | 503 +++++++++++++++++++++++++++++++----------------- 2 files changed, 342 insertions(+), 189 deletions(-) diff --git a/lxcd.completion b/lxcd.completion index 8ae04ed..98c60a9 100644 --- a/lxcd.completion +++ b/lxcd.completion @@ -4,7 +4,9 @@ _lxcd_completion() { local cur prev words cword _init_completion || return - local commands="create clone rename enter list delete add stop start restart" + local commands="create clone rename enter list delete stop start restart" + local init_opts="-f --force -w --workspace -e --edit -p --packages -n --nested -i --image --aptcache --ssh" + local create_opts="--nested --aptcache -i --image -w --workspace -p --packages --ssh" if [ $cword -eq 1 ]; then COMPREPLY=($(compgen -W "-v --verbose ${commands}" -- "$cur")) @@ -24,9 +26,15 @@ _lxcd_completion() { local containers=$($LXC list user.lxcd=true -c n --format csv 2>/dev/null | tr '\n' ' ') case "${subcommand}" in + init) + if [[ "$cur" == -* ]]; then + COMPREPLY=($(compgen -W "${init_opts}" -- "$cur")) + fi + ;; + create) if [[ "$cur" == -* ]]; then - COMPREPLY=($(compgen -W "--nested --aptcache -i --image -m --minimal -w --workspace" -- "$cur")) + COMPREPLY=($(compgen -W "${create_opts}" -- "$cur")) else COMPREPLY=($(compgen -W "ubuntu: ubuntu/24.04 ubuntu/22.04 debian/12 debian/11" -- "$cur")) fi @@ -44,15 +52,17 @@ _lxcd_completion() { fi ;; - enter|delete|stop|start|restart) - COMPREPLY=($(compgen -W "${containers}" -- "$cur")) - ;; - - add) + enter) if [ $cword -eq 2 ]; then COMPREPLY=($(compgen -W "${containers}" -- "$cur")) - elif [[ "$cur" == -* ]]; then - COMPREPLY=($(compgen -W "--nested --aptcache --map --unmap" -- "$cur")) + fi + ;; + + delete|stop|start|restart) + if [[ "$cur" == -* ]]; then + COMPREPLY=($(compgen -W "-a --all" -- "$cur")) + else + COMPREPLY=($(compgen -W "${containers}" -- "$cur")) fi ;; diff --git a/lxcd.py b/lxcd.py index 4fb2b9e..6411a7c 100755 --- a/lxcd.py +++ b/lxcd.py @@ -4,7 +4,6 @@ import os import re import subprocess import sys -import time import json from pathlib import Path @@ -15,27 +14,46 @@ def config_path(): home = Path(f"/home/{sudo_user}") if sudo_user else Path.home() return home / ".lxcdrc" -def ensure_config(): +def ensure_config(force=False, workspace="Coder", packages=None, nested=False, + image=None, aptcache=None, ssh=None): path = config_path() - if path.exists(): + if path.exists() and not force: return - config_content = """# lxcd configuration + packages = packages or [] + packages_block = "\n".join(packages) if packages else "" + nested_value = "true" if nested else "" + image_value = image or "ubuntu:" + aptcache_value = aptcache or "" + ssh_value = ssh or "" + + config_content = f"""# lxcd configuration # -# [workspace] sets the default workspace directory name used by 'lxcd create' -# and 'lxcd enter'. -# Additional sections (e.g. [git], [opencode]) define paths to mount -# into containers via 'lxcd add --map=
'. +# [options] holds key=value defaults applied by 'lxcd create'. +# workspace: default workspace directory name +# image: LXD image to use +# nested: enable nesting (e.g. true) or leave blank +# aptcache: APT cache/proxy URL or leave blank +# ssh: SSH public key to authorize for the container user; blank +# disables the SSH server +# +# [packages] lists packages installed by default in new containers via +# 'lxcd create'. +# +# [mappings] lists paths (relative to ~/) mounted into containers when they +# are entered via 'lxcd enter'. -[workspace] -Code +[options] +workspace={workspace} +image={image_value} +nested={nested_value} +aptcache={aptcache_value} +ssh={ssh_value} -[git] -.gitconfig +[packages] +{packages_block} -[opencode] -.config/opencode -.local/share/opencode +[mappings] """ try: path.write_text(config_content) @@ -43,6 +61,12 @@ Code except Exception as e: print(f"Warning: Could not create config at {path}: {e}", file=sys.stderr) +def require_config(): + path = config_path() + if not path.exists(): + print(f"Error: Config file {path} not found. Run 'lxcd init' to create it.", file=sys.stderr) + sys.exit(1) + def load_config(): path = config_path() if not path.exists(): @@ -50,8 +74,25 @@ def load_config(): config = {} section = None - values = [] - + entries = [] + + def flush(): + nonlocal entries + if section is None: + return + kv = {} + is_dict = False + for e in entries: + if "=" in e: + key, _, value = e.partition("=") + kv[key.strip()] = value.strip() + is_dict = True + if is_dict: + config[section] = kv + else: + config[section] = list(entries) + entries = [] + try: for line in path.read_text().splitlines(): line = line.strip() @@ -60,15 +101,13 @@ def load_config(): m = re.match(r"^\[(.+)\]$", line) if m: - if section is not None: - config[section] = values + flush() section = m.group(1) - values = [] + entries = [] elif section is not None: - values.append(line) - - if section is not None: - config[section] = values + entries.append(line) + + flush() except Exception as e: print(f"Warning: Could not read config from {path}: {e}", file=sys.stderr) @@ -162,47 +201,82 @@ def handle_list(args): containers = json.loads(result.stdout) - print(f"\n{'CONTAINER NAME':<25} {'STATUS':<12} {'IPV4 ADDRESS':<25} {'ACTIVE OPTIONS':<25}") - print("-" * 87) + COLUMNS = { + "name": ("CONTAINER NAME", 25), + "status": ("STATUS", 12), + "ipv4": ("IPV4 ADDRESS", 22), + "image": ("IMAGE", 18), + "packages": ("PACKAGES", 25), + "opts": ("ACTIVE OPTIONS", 25), + } + + if args.brief is not None: + cols = [c.strip() for c in args.brief.split(",")] if args.brief.strip() else ["ipv4"] + else: + cols = ["name", "status", "ipv4", "image", "packages", "opts"] + + if "name" not in cols: + cols = ["name", *cols] + + seen = set() + cols = [c for c in cols if not (c in seen or seen.add(c))] + + chosen = {c: COLUMNS[c] for c in cols if c in COLUMNS} + + header = " ".join(f"{label:<{w}}" for label, w in chosen.values()) + print(f"\n{header}") + print("-" * sum(w for _, w in chosen.values())) + + def get_config(name, key): + return lxc_subprocess( + ["config", "get", name, key], + capture_output=True, text=True, check=False + ).stdout.strip() count = 0 cfg = load_config() + map_sections = [k for k in cfg.keys() if k not in ("options", "packages")] - for c in containers: - name = c["name"] - status = c["status"] + for container in containers: + name = container["name"] + status = container["status"] ipv4 = "N/A" - state = c.get("state") - if state: - networks = state.get("network") or {} + container_state = container.get("state") + if container_state: + networks = container_state.get("network") or {} eth0 = networks.get("eth0", {}) addrs = [a["address"] for a in eth0.get("addresses", []) if a.get("family") == "inet"] if addrs: ipv4 = addrs[0] - def get_config(key): - return lxc_subprocess( - ["config", "get", name, key], - capture_output=True, text=True, check=False - ).stdout.strip() + cells = { + "name": name, + "status": status, + "ipv4": ipv4, + } - active_opts = [] + if "opts" in cols: + active = [] + if get_config(name, "security.nesting") == "true": + active.append("nesting") + if get_config(name, "user.lxcd.aptcache") == "true": + active.append("aptcache") + if get_config(name, "user.lxcd.ssh") == "true": + active.append("ssh") + for section in map_sections: + if get_config(name, f"user.lxcd.map.{section}") == "true": + active.append(section) + cells["opts"] = ", ".join(active) if active else "none" - if get_config("security.nesting") == "true": - active_opts.append("nesting") + if "image" in cols: + cells["image"] = get_config(name, "user.lxcd.image") or "N/A" - if get_config("user.lxcd.aptcache") == "true": - active_opts.append("aptcache") + if "packages" in cols: + cells["packages"] = get_config(name, "user.lxcd.packages") or "none" - for section in cfg.keys(): - if section == "workspace": - continue - if get_config(f"user.lxcd.map.{section}") == "true": - active_opts.append(section) - - opts_str = ", ".join(active_opts) if active_opts else "none" - print(f"{name:<25} {status:<12} {ipv4:<25} {opts_str:<25}") + row = " ".join(f"{cells[c]:<{w}}" for c, (label, w) in chosen.items()) + print(row) count += 1 if count == 0: @@ -220,14 +294,38 @@ def handle_clone(args): print(f"Container '{args.name}' cloned successfully from '{args.source}'!") +def handle_init(args): + if args.edit: + path = config_path() + if not path.exists(): + packages = [p.strip() for p in args.packages.split(",")] if args.packages else None + ensure_config(force=True, workspace=args.workspace, packages=packages, + nested=args.nested, image=args.image, + aptcache=args.aptcache, ssh=args.ssh) + editor = os.environ.get("EDITOR", os.environ.get("VISUAL", "vi")) + subprocess.run([editor, str(path)]) + return + if config_path().exists(): + if args.force: + print(f"Overwriting existing config at {config_path()}...") + else: + print(f"Error: Config file {config_path()} already exists. Use --force to overwrite.", file=sys.stderr) + sys.exit(1) + packages = [p.strip() for p in args.packages.split(",")] if args.packages else None + ensure_config(force=args.force, workspace=args.workspace, packages=packages, + nested=args.nested, image=args.image, + aptcache=args.aptcache, ssh=args.ssh) + def handle_create(args): - ensure_config() cfg = load_config() - workspace = args.workspace or (cfg.get("workspace")[0] if cfg.get("workspace") else "Coder") + options = cfg.get("options") if isinstance(cfg.get("options"), dict) else {} + workspace = args.workspace or options.get("workspace") or "Coder" + nested = args.nested or bool(options.get("nested")) + aptcache = args.aptcache or options.get("aptcache") or None + image = args.image or options.get("image") or "ubuntu:" + ssh_key = args.ssh or options.get("ssh") or None host_username, uid, gid, host_home = get_real_user_info() - if args.minimal: - args.image = "ubuntu-minimal:" host_timezone = detect_timezone() host_locale = detect_locale() @@ -239,17 +337,37 @@ def handle_create(args): host_coder_dir.mkdir(parents=True, exist_ok=True) os.chown(host_coder_dir, uid, gid) - print(f"Creating LXD container '{args.name}' from image '{args.image}'...") + print(f"Creating LXD container '{args.name}' from image '{image}'...") 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 "" + f" - echo 'Acquire::http::Proxy \"{aptcache}\";' > /etc/apt/apt.conf.d/00lxcd-proxy\n" + ) if aptcache else "" + + if args.packages: + packages = [p.strip() for p in args.packages.split(",") if p.strip()] + else: + packages = cfg.get("packages") or [] + if ssh_key and not any("openssh-server" in p for p in packages): + packages = [*packages, "openssh-server"] + + packages_cmd = "" + if packages: + pkg_list = "\n".join(f" - {pkg}" for pkg in packages) + packages_cmd = f"packages:\n{pkg_list}\n" + + if ssh_key: + ssh_auth_cmd = f" ssh_authorized_keys:\n - {ssh_key}\n" + ssh_runcmd = " - systemctl enable --now sshd || systemctl enable --now ssh || true" + print("Enabling SSH server and authorizing provided public key...") + else: + ssh_auth_cmd = "" + ssh_runcmd = " - systemctl disable --now sshd 2>/dev/null || systemctl disable --now ssh 2>/dev/null || true" cloud_config = f"""#cloud-config timezone: {host_timezone} locale: {host_locale} -users: +{packages_cmd}users: - name: {host_username} gecos: lxcd user primary_group: {host_username} @@ -258,7 +376,7 @@ users: sudo: ALL=(ALL) NOPASSWD:ALL lock_passwd: true homedir: /home/{host_username} - +{ssh_auth_cmd} groups: - {host_username} @@ -272,13 +390,15 @@ runcmd: - cp -rpn /etc/skel/. /home/{host_username}/ - mkdir -p {container_coder_path} - chown -R {host_username}:{host_username} /home/{host_username} - - echo "SSH left at default image state" +{ssh_runcmd} {apt_proxy_cmd}""" - lxc_run(["init", args.image, args.name]) + lxc_run(["init", image, args.name]) print("Tagging container as managed by lxcd...") lxc_run(["config", "set", args.name, "user.lxcd", "true"]) + lxc_run(["config", "set", args.name, "user.lxcd.image", image]) + lxc_run(["config", "set", args.name, "user.lxcd.packages", ",".join(packages)]) print(f"Injecting cloud-init config for user '{host_username}'..." ) lxc_run(["config", "set", args.name, "cloud-init.user-data", cloud_config]) @@ -289,26 +409,50 @@ runcmd: f"path={container_coder_path}", "shift=true"]) - if args.nested: + if nested: print("Enabling nesting capabilities...") lxc_run(["config", "set", args.name, "security.nesting", "true"]) lxc_run(["config", "set", args.name, "security.syscalls.intercept.mknod", "true"]) set_label(args.name, "nested") - if args.aptcache: - print(f"APT cache/proxy will be configured to {args.aptcache} via cloud-init...") + if aptcache: + print(f"APT cache/proxy will be configured to {aptcache} via cloud-init...") set_label(args.name, "aptcache") + if ssh_key: + set_label(args.name, "ssh") + print("Starting container and executing cloud-init user provisioning...") 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}") +def apply_mappings(name, host_username, host_home): + cfg = load_config() + paths = cfg.get("mappings") + if not isinstance(paths, list) or not paths: + return + for idx, path in enumerate(paths): + host_path = host_home / path + container_path = f"/home/{host_username}/{path}" + lxc_subprocess(["config", "device", "remove", name, f"map-{idx}"], + capture_output=True, text=True, check=False) + if host_path.exists(): + lxc_run(["config", "device", "add", name, f"map-{idx}", "disk", + f"source={host_path}", + f"path={container_path}", + "shift=true"]) + print(f"Mapped {host_path} -> {container_path}") + else: + print(f"Warning: {host_path} not found, skipping", file=sys.stderr) + set_label(name, "map.mappings") + def handle_enter(args): cfg = load_config() - workspace = (cfg.get("workspace")[0] if cfg.get("workspace") else "Coder") - _, uid, gid, host_home = get_real_user_info() + options = cfg.get("options") if isinstance(cfg.get("options"), dict) else {} + workspace = options.get("workspace") or "Coder" + host_username, uid, gid, host_home = get_real_user_info() state = lxc_subprocess( ["info", args.name], @@ -319,6 +463,8 @@ def handle_enter(args): lxc_run(["start", args.name]) lxc_run(["exec", args.name, "--", "cloud-init", "status", "--wait"], check=False) print(f"Container '{args.name}' started.") + + apply_mappings(args.name, host_username, host_home) try: result = lxc_subprocess( @@ -343,114 +489,71 @@ def handle_enter(args): if display: env_prefix += f"DISPLAY={display} " - shell_cmd = "exec /bin/bash --login" exec_cmd = [ "lxc", "exec", args.name, "--cwd", target_cwd, "--user", str(uid), "--group", str(gid), "--", - "sh", "-c", f"env {env_prefix} sh -c '{shell_cmd}'" + "sh", "-c", f"env {env_prefix} sh -c 'exec /bin/bash --login'" ] os.execvp("lxc", exec_cmd) +def resolve_targets(args): + try: + names = list(args.names) + except AttributeError: + names = [args.name] + if args.all: + result = lxc_subprocess( + ["list", "user.lxcd=true", "-c", "n", "--format", "csv"], + capture_output=True, text=True, check=False + ) + if result.returncode != 0: + print("Error communicating with LXD.", file=sys.stderr) + sys.exit(1) + names += [n.strip() for n in result.stdout.splitlines() if n.strip()] + + seen = set() + unique = [] + for n in names: + if n not in seen: + seen.add(n) + unique.append(n) + + if not unique: + print("Error: Specify at least one container name or use --all.", file=sys.stderr) + sys.exit(1) + return unique + def handle_stop(args): - print(f"Stopping container '{args.name}'...") - lxc_run(["stop", args.name], check=False) - print(f"Container '{args.name}' stopped.") + for name in resolve_targets(args): + print(f"Stopping container '{name}'...") + lxc_run(["stop", name], check=False) + print(f"Container '{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.") + for name in resolve_targets(args): + print(f"Starting container '{name}'...") + lxc_run(["start", name]) + lxc_run(["exec", name, "--", "cloud-init", "status", "--wait"], check=False) + print(f"Container '{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.") + for name in resolve_targets(args): + print(f"Restarting container '{name}'...") + lxc_run(["restart", name], check=False) + lxc_run(["exec", name, "--", "cloud-init", "status", "--wait"], check=False) + print(f"Container '{name}' restarted.") def handle_delete(args): - print(f"Stopping container '{args.name}'...") - lxc_run(["stop", args.name], check=False) - print(f"Deleting container '{args.name}'...") - lxc_run(["delete", args.name]) - print(f"Container '{args.name}' deleted successfully.") - -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.aptcache and not args.map and not args.unmap: - print("Error: Specify at least one option to add.\n" - "Example: lxcd add --nested --aptcache", 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"]) - set_label(args.name, "nested") - - 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"]) - set_label(args.name, "aptcache") - - if args.map: - cfg = load_config() - paths = cfg.get(args.map) - if paths: - for idx, path in enumerate(paths): - host_path = host_home / path - if host_path.exists(): - container_path = f"/home/{host_username}/{path}" - lxc_subprocess(["config", "device", "remove", args.name, f"map-{args.map}-{idx}"], - capture_output=True, text=True, check=False) - lxc_run(["config", "device", "add", args.name, f"map-{args.map}-{idx}", "disk", - f"source={host_path}", - f"path={container_path}", - "shift=true"]) - print(f"Mapped {host_path} -> {container_path}") - else: - print(f"Warning: {host_path} not found, skipping", file=sys.stderr) - set_label(args.name, f"map.{args.map}") - else: - print(f"Warning: Section '{args.map}' not found in ~/.lxcdrc", file=sys.stderr) - - if args.unmap: - idx = 0 - while True: - result = lxc_subprocess(["config", "device", "remove", args.name, f"map-{args.unmap}-{idx}"], - capture_output=True, text=True, check=False) - if result.returncode != 0: - break - idx += 1 - set_label(args.name, f"map.{args.unmap}", "") - print(f"Unmapped section '{args.unmap}'") - - 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!") + for name in resolve_targets(args): + print(f"Stopping container '{name}'...") + lxc_run(["stop", name], check=False) + print(f"Deleting container '{name}'...") + lxc_run(["delete", name]) + print(f"Container '{name}' deleted successfully.") def handle_rename(args): print(f"Preparing to rename '{args.old_name}' to '{args.new_name}'...") @@ -471,18 +574,57 @@ def handle_rename(args): print(f"Container successfully renamed to '{args.new_name}'!") def main(): - parser = argparse.ArgumentParser(description="lxcd: A Distrobox-like wrapper for LXD containers") + description = """lxcd: A Distrobox-like wrapper for LXD containers. + +This tool simplifies managing LXD-based development environments. +It supports configuration-driven workspace names and dynamic path mapping +controlled by a ~/.lxcdrc configuration file. + +Configuration file (~/.lxcdrc) format: + [options] + workspace=Coder + nested= + aptcache= + + [packages] + vim + git + + [mappings] + .gitconfig + +Usage Workflow: + 1. Create a container: lxcd create myenv + 2. Enter container: lxcd enter myenv + (paths in [mappings] are mounted automatically on enter)""" + + parser = argparse.ArgumentParser( + description=description, + formatter_class=argparse.RawDescriptionHelpFormatter + ) parser.add_argument("-v", "--verbose", action="store_true", help="Show verbose command output") subparsers = parser.add_subparsers(dest="command", required=True) + # Init command + parser_init = subparsers.add_parser("init", help="Write the ~/.lxcdrc config file") + parser_init.add_argument("-f", "--force", action="store_true", help="Overwrite an existing config file") + parser_init.add_argument("-w", "--workspace", default="Coder", help="Default workspace directory name (default: Coder)") + parser_init.add_argument("-e", "--edit", action="store_true", help="Edit the config file with $EDITOR (creates it first if missing)") + parser_init.add_argument("-p", "--packages", help="Comma-separated list of packages installed by default in new containers") + parser_init.add_argument("-n", "--nested", action="store_true", help="Enable nesting by default in new containers") + parser_init.add_argument("-i", "--image", help="LXD image to use by default in new containers (default: ubuntu:)") + parser_init.add_argument("--aptcache", help="URL for an APT cache/proxy stored as a default (e.g. http://10.0.0.1:3142)") + parser_init.add_argument("--ssh", help="SSH public key authorized for the container user by default") + # Create command parser_create = subparsers.add_parser("create", help="Create a new LXD container") 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("-m", "--minimal", action="store_true", help="Use ubuntu-minimal: image instead") - parser_create.add_argument("-w", "--workspace", help="Workspace directory name (default: Coder or ~/.lxcdrc [workspace])") + parser_create.add_argument("-i", "--image", help="LXD image to use (default: ubuntu: or ~/.lxcdrc [options] image=)") + parser_create.add_argument("-w", "--workspace", help="Workspace directory name (default: ~/.lxcdrc [options] workspace=)") parser_create.add_argument("--nested", action="store_true", help="Enable nesting") parser_create.add_argument("--aptcache", help="URL for an APT cache/proxy (e.g. http://10.0.0.1:3142)") + parser_create.add_argument("-p", "--packages", help="Comma-separated packages to install (overrides ~/.lxcdrc [packages])") + parser_create.add_argument("--ssh", help="SSH public key to authorize (default: ~/.lxcdrc [options] ssh=; blank disables SSH)") # Clone command parser_clone = subparsers.add_parser("clone", help="Clone an existing LXD container") @@ -500,37 +642,40 @@ def main(): # List command parser_list = subparsers.add_parser("list", aliases=["ls"], help="List all managed LXD containers") + parser_list.add_argument("-b", "--brief", nargs="?", const="", metavar="COLS", + help="Only show name plus a comma-separated subset of status,ipv4,image,packages,opts (default: ipv4)") # 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") + parser_stop = subparsers.add_parser("stop", help="Stop one or more running containers") + parser_stop.add_argument("names", nargs="*", help="Names of containers to stop") + parser_stop.add_argument("-a", "--all", action="store_true", help="Stop all lxcd-managed containers") # 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") + parser_start = subparsers.add_parser("start", help="Start one or more stopped containers") + parser_start.add_argument("names", nargs="*", help="Names of containers to start") + parser_start.add_argument("-a", "--all", action="store_true", help="Start all lxcd-managed containers") # Restart command - parser_restart = subparsers.add_parser("restart", help="Restart a container") - parser_restart.add_argument("name", help="Name of the container to restart") + parser_restart = subparsers.add_parser("restart", help="Restart one or more containers") + parser_restart.add_argument("names", nargs="*", help="Names of containers to restart") + parser_restart.add_argument("-a", "--all", action="store_true", help="Restart all lxcd-managed containers") # 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("--aptcache", help="URL for an APT cache/proxy (e.g. http://10.0.0.1:3142)") - parser_add.add_argument("--map", help="Map paths from a ~/.lxcdrc section into the container") - parser_add.add_argument("--unmap", help="Remove a previously mapped section") + parser_delete = subparsers.add_parser("delete", help="Delete one or more LXD containers") + parser_delete.add_argument("names", nargs="*", help="Names of containers to delete") + parser_delete.add_argument("-a", "--all", action="store_true", help="Delete all lxcd-managed containers") args = parser.parse_args() global VERBOSE VERBOSE = args.verbose - if args.command == "create": + if args.command != "init": + require_config() + + if args.command == "init": + handle_init(args) + elif args.command == "create": handle_create(args) elif args.command == "clone": handle_clone(args) @@ -548,8 +693,6 @@ def main(): handle_restart(args) elif args.command == "delete": handle_delete(args) - elif args.command == "add": - handle_add(args) if __name__ == "__main__": main()