diff --git a/.gitignore b/.gitignore index 0163807..56da79e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,10 @@ __pycache__/ *.egg-info/ dist/ build/ + +# Snapcraft build artifacts +*.snap +parts/ +stage/ +prime/ +.snapcraft/ diff --git a/lxcd.completion b/lxcd.completion index 7c54884..8ae04ed 100644 --- a/lxcd.completion +++ b/lxcd.completion @@ -26,7 +26,7 @@ _lxcd_completion() { case "${subcommand}" in create) if [[ "$cur" == -* ]]; then - COMPREPLY=($(compgen -W "--nested --git --certs --aptcache --ssh-client -i --image -m --minimal" -- "$cur")) + COMPREPLY=($(compgen -W "--nested --aptcache -i --image -m --minimal -w --workspace" -- "$cur")) else COMPREPLY=($(compgen -W "ubuntu: ubuntu/24.04 ubuntu/22.04 debian/12 debian/11" -- "$cur")) fi @@ -52,7 +52,7 @@ _lxcd_completion() { if [ $cword -eq 2 ]; then COMPREPLY=($(compgen -W "${containers}" -- "$cur")) elif [[ "$cur" == -* ]]; then - COMPREPLY=($(compgen -W "--nested --git --certs --aptcache --ssh-client" -- "$cur")) + COMPREPLY=($(compgen -W "--nested --aptcache --map --unmap" -- "$cur")) fi ;; diff --git a/lxcd.py b/lxcd.py index 8de9d46..14d28ff 100755 --- a/lxcd.py +++ b/lxcd.py @@ -6,8 +6,75 @@ import re import subprocess import sys import time +import json from pathlib import Path +VERBOSE = False + +def config_path(): + sudo_user = os.environ.get("SUDO_USER") + home = Path(f"/home/{sudo_user}") if sudo_user else Path.home() + return home / ".lxcdrc" + +def ensure_config(): + path = config_path() + if path.exists(): + return + + config_content = """# 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=
'. + +[workspace] +Code + +[git] +.gitconfig + +[opencode] +.config/opencode +.local/share/opencode +""" + try: + path.write_text(config_content) + print(f"Created {path}") + except Exception as e: + print(f"Warning: Could not create config at {path}: {e}", file=sys.stderr) + +def load_config(): + path = config_path() + if not path.exists(): + return {} + + config = {} + section = None + values = [] + + try: + for line in path.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + + m = re.match(r"^\[(.+)\]$", line) + if m: + if section is not None: + config[section] = values + section = m.group(1) + values = [] + elif section is not None: + values.append(line) + + if section is not None: + config[section] = values + except Exception as e: + print(f"Warning: Could not read config from {path}: {e}", file=sys.stderr) + + return config + def get_real_user_info(): """ Detects the real user when running under sudo. @@ -28,6 +95,9 @@ def get_real_user_info(): ) def detect_timezone(): + if os.environ.get("SNAP"): + return "UTC" + tz = "UTC" tz_file = Path("/etc/timezone") if tz_file.exists(): @@ -40,19 +110,17 @@ def detect_timezone(): m = re.search(r"Time zone:\s+(\S+)", result.stdout) if m: tz = m.group(1) - except FileNotFoundError: + except Exception: pass return tz - def detect_locale(): return os.environ.get("LANG", "en_US.UTF-8") - -VERBOSE = False - def lxc_prefix(): - """Return ['sudo'] if the user isn't root and isn't in the lxd group.""" + """Return [] if running inside a Snap, or if the user is root or in the lxd group.""" + if os.environ.get("SNAP"): + return [] if os.getuid() == 0: return [] try: @@ -84,6 +152,17 @@ def lxc_subprocess(args, **kwargs): print(f"+ {' '.join(cmd)}", file=sys.stderr) return subprocess.run(cmd, **kwargs) +def set_label(name, feature, value="true"): + """Set a user.lxcd. config key on a container.""" + lxc_run(["config", "set", name, f"user.lxcd.{feature}", value], check=False) + +def container_exists(name): + result = lxc_subprocess( + ["info", name], + capture_output=True, text=True, check=False + ) + return result.returncode == 0 + def handle_list(args): print("Fetching lxcd container environments...") @@ -95,13 +174,14 @@ def handle_list(args): 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 + cfg = load_config() + for c in containers: name = c["name"] status = c["status"] @@ -115,7 +195,6 @@ def handle_list(args): if addrs: ipv4 = addrs[0] - # Read lxcd feature labels via individual config get def get_config(key): return lxc_subprocess( ["config", "get", name, key], @@ -124,14 +203,17 @@ def handle_list(args): active_opts = [] - # Check nesting via security.nesting (native LXD config) if get_config("security.nesting") == "true": active_opts.append("nesting") - # Check labels for other features - for feature in ("certs", "git", "aptcache", "ssh-client"): - if get_config(f"user.lxcd.{feature}") == "true": - active_opts.append(feature) + if get_config("user.lxcd.aptcache") == "true": + active_opts.append("aptcache") + + 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}") @@ -144,30 +226,28 @@ def handle_list(args): def handle_clone(args): print(f"Cloning container '{args.source}' to '{args.name}'...") - # 1. Native LXD copy (duplicates the filesystem, mounts, and tags) - lxc_run([ "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}'...") - lxc_run([ "start", args.name]) - - # 3. Wait for network/cloud-init stabilization - lxc_run([ "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}' cloned successfully from '{args.source}'!") def handle_create(args): + ensure_config() + cfg = load_config() + workspace = args.workspace or (cfg.get("workspace")[0] if cfg.get("workspace") else "Coder") + 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() - # Paths for targeted workspace sharing - host_coder_dir = host_home / "Coder" - container_coder_path = f"/home/{host_username}/Coder" + host_coder_dir = host_home / workspace + container_coder_path = f"/home/{host_username}/{workspace}" - # 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}") host_coder_dir.mkdir(parents=True, exist_ok=True) @@ -175,18 +255,11 @@ def handle_create(args): print(f"Creating LXD container '{args.name}' from image '{args.image}'...") - # 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} @@ -203,91 +276,64 @@ users: groups: - {host_username} -{ssh_client_packages}bootcmd: +bootcmd: - mkdir -p /home/{host_username} + - mkdir -p /home/{host_username}/.config /home/{host_username}/.local /home/{host_username}/.local/share + - chown {uid}:{gid} /home/{host_username}/.config /home/{host_username}/.local /home/{host_username}/.local/share || true - chown {uid}:{gid} /home/{host_username} runcmd: - cp -rpn /etc/skel/. /home/{host_username}/ - mkdir -p {container_coder_path} -{ssh_client_cmd} - chown -R {host_username}:{host_username} /home/{host_username} + - chown -R {host_username}:{host_username} /home/{host_username} - echo "SSH left at default image state" {apt_proxy_cmd}""" - # Initialize container - lxc_run([ "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...") - lxc_run([ "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}'...") - lxc_run([ "config", "set", args.name, "cloud-init.user-data", cloud_config]) + print(f"Injecting cloud-init config for user '{host_username}'..." ) + 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)...") 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: - gitconfig_path = host_home / ".gitconfig" - if gitconfig_path.exists(): - container_git_path = f"/home/{host_username}/.gitconfig" - print(f"Mapping host git configurations to {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"]) - set_label(args.name, "git") - 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...") - lxc_run([ "config", "set", args.name, "security.nesting", "true"]) - lxc_run([ "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"]) set_label(args.name, "nested") - # Handle --aptcache option if args.aptcache: print(f"APT cache/proxy will be configured to {args.aptcache} via cloud-init...") set_label(args.name, "aptcache") - # 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...") - set_label(args.name, "ssh-client") - - # 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"]) - set_label(args.name, "certs") - 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...") - lxc_run([ "start", args.name]) - lxc_run([ "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}") 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() - host_cwd = os.getcwd() + + state = lxc_subprocess( + ["info", args.name], + capture_output=True, text=True, check=False + ) + if "Status: RUNNING" not in state.stdout: + print(f"Container '{args.name}' is not running. Starting it...") + lxc_run(["start", args.name]) + lxc_run(["exec", args.name, "--", "cloud-init", "status", "--wait"], check=False) + print(f"Container '{args.name}' started.") - # Dynamically find the user matching your UID inside the container try: result = lxc_subprocess( ["exec", args.name, "--", "id", "-nu", str(uid)], @@ -298,17 +344,8 @@ def handle_enter(args): container_username = "root" container_home = f"/home/{container_username}" - - # Smart CWD Mapping: Ensure the host directory actually exists in the container - host_coder_dir = str(host_home / "Coder") - container_coder_dir = f"{container_home}/Coder" - - # If the user is inside the mapped Coder directory on the host, translate the path - if host_cwd.startswith(host_coder_dir): - target_cwd = host_cwd.replace(host_coder_dir, container_coder_dir, 1) - else: - # Fallback to the container's home directory if executed from an unmapped host path - target_cwd = container_home + container_coder_dir = f"{container_home}/{workspace}" + target_cwd = container_coder_dir env_prefix = ( f"HOME={container_home} " @@ -320,17 +357,8 @@ def handle_enter(args): if display: env_prefix += f"DISPLAY={display} " - # Check if container has the ssh-client feature (set by --ssh-client) - has_ssh_client = lxc_subprocess( - ["config", "get", args.name, "user.lxcd.ssh-client"], - capture_output=True, text=True, check=False - ).stdout.strip() == "true" - - # 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, @@ -347,40 +375,28 @@ def handle_enter(args): def handle_stop(args): print(f"Stopping container '{args.name}'...") - lxc_run([ "stop", args.name], check=False) + 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) + 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) + 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}'...") - lxc_run([ "stop", args.name], check=False) + lxc_run(["stop", args.name], check=False) print(f"Deleting container '{args.name}'...") - lxc_run([ "delete", args.name]) + lxc_run(["delete", args.name]) print(f"Container '{args.name}' deleted successfully.") -def set_label(name, feature, value="true"): - """Set a user.lxcd. config key on a container.""" - lxc_run([ "config", "set", name, f"user.lxcd.{feature}", value], check=False) - -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() @@ -388,9 +404,9 @@ def handle_add(args): print(f"Error: Container '{args.name}' does not exist.", file=sys.stderr) sys.exit(1) - if not args.nested and not args.git and not args.certs and not args.aptcache and not args.ssh_client: + 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 --git --nested", file=sys.stderr) + "Example: lxcd add --nested --aptcache", file=sys.stderr) sys.exit(1) state = lxc_subprocess( @@ -405,58 +421,48 @@ def handle_add(args): 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"]) + 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.git: - was_running = not was_stopped - 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"]) - set_label(args.name, "git") - 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. Skipping git config mapping.") - - 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"]) - set_label(args.name, "certs") - 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", + 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.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"]) - set_label(args.name, "ssh-client") + 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...") @@ -464,33 +470,26 @@ def handle_add(args): 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 lxc_subprocess( ["stop", args.old_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) - # 2. Execute the rename - lxc_run([ "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}'...") - lxc_run([ "start", args.new_name]) - - # 4. Wait for it to stabilize - lxc_run([ "exec", args.new_name, "--", "cloud-init", "status", "--wait"], check=False) + lxc_run(["start", args.new_name]) + lxc_run(["exec", args.new_name, "--", "cloud-init", "status", "--wait"], check=False) print(f"Container successfully renamed to '{args.new_name}'!") def main(): - parser = argparse.ArgumentParser(description="lxcd: A Distrobox-like wrapper for LXD (Sudo Optimized)") + parser = argparse.ArgumentParser(description="lxcd: A Distrobox-like wrapper for LXD containers") parser.add_argument("-v", "--verbose", action="store_true", help="Show verbose command output") subparsers = parser.add_subparsers(dest="command", required=True) @@ -499,12 +498,9 @@ 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("-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("--nested", action="store_true", help="Enable nesting") - 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") @@ -543,11 +539,9 @@ def main(): 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("--git", action="store_true", help="Map host ~/.gitconfig into the container") - 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") + 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") args = parser.parse_args() @@ -577,5 +571,3 @@ def main(): if __name__ == "__main__": main() - - diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 0000000..471ed8f --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,27 @@ +name: lxcd +base: core22 +version: git +summary: A Distrobox-like wrapper for LXD +description: | + lxcd provides a Distrobox-like experience using LXD containers. + It creates, manages, and enters LXD containers with features + like nesting, host CA certificate mounts, APT caching, and + SSH client setup. + +grade: stable +confinement: classic + +apps: + lxcd: + command: bin/lxcd + completer: lxcd.completion + +parts: + lxcd: + plugin: dump + source: . + organize: + lxcd.py: bin/lxcd + prime: + - bin/lxcd + - lxcd.completion