BigPickle

This commit is contained in:
Andrew Hurley 2026-06-27 17:28:44 +08:00
parent 46a9a7e144
commit 5864a69f1b
4 changed files with 480 additions and 42 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
__pycache__/
*.pyc
*.pyo
*.egg-info/
dist/
build/

65
lxcd.completion Normal file
View File

@ -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

421
lxcd.py
View File

@ -1,8 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import argparse
import grp
import os import os
import re
import subprocess import subprocess
import sys import sys
import time
from pathlib import Path from pathlib import Path
def get_real_user_info(): def get_real_user_info():
@ -24,6 +27,39 @@ def get_real_user_info():
Path.home() 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): def run_cmd(cmd, check=True):
"""Helper to run shell commands.""" """Helper to run shell commands."""
try: try:
@ -32,32 +68,132 @@ def run_cmd(cmd, check=True):
print(f"Error executing command: {' '.join(cmd)}", file=sys.stderr) print(f"Error executing command: {' '.join(cmd)}", file=sys.stderr)
sys.exit(e.returncode) 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): def handle_list(args):
# Pass the user.lxcd=true filter directly to LXD's native listing engine print("Fetching lxcd container environments...")
run_cmd(["lxc", "list", "user.lxcd=true"])
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): def handle_clone(args):
print(f"Cloning container '{args.source}' to '{args.name}'...") print(f"Cloning container '{args.source}' to '{args.name}'...")
# 1. Native LXD copy (duplicates the filesystem, mounts, and tags) # 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 # 2. Start the new cloned container
print(f"Starting cloned container '{args.name}'...") 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 # 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}'!") print(f"Container '{args.name}' cloned successfully from '{args.source}'!")
def handle_create(args): def handle_create(args):
host_username, uid, gid, host_home = get_real_user_info() host_username, uid, gid, host_home = get_real_user_info()
host_timezone = detect_timezone()
host_locale = detect_locale()
# Paths for targeted workspace sharing # Paths for targeted workspace sharing
host_coder_dir = host_home / "Coder" host_coder_dir = host_home / "Coder"
container_coder_path = f"/home/{host_username}/Coder" container_coder_path = f"/home/{host_username}/Coder"
# Ensure the 'Coder' directory exists on the host machine # Ensure the 'Coder' directory exists on the host machine
if not host_coder_dir.exists(): if not host_coder_dir.exists():
print(f"Creating missing directory on host: {host_coder_dir}") 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) 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 '{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 cloud_config = f"""#cloud-config
timezone: {host_timezone}
locale: {host_locale}
users: users:
- name: {host_username} - name: {host_username}
gecos: lxcd user gecos: lxcd user
@ -81,31 +236,34 @@ users:
groups: groups:
- {host_username} - {host_username}
{ssh_client_packages}bootcmd:
- mkdir -p /home/{host_username}
- chown {uid}:{gid} /home/{host_username}
runcmd: runcmd:
# Copy missing skeleton files (without overwriting existing mounts) and fix ownership
- cp -rpn /etc/skel/. /home/{host_username}/ - 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 # 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 # Tag the container so lxcd knows it owns it
print("Tagging container as managed by lxcd...") 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 # Inject Cloud-init settings
print(f"Injecting cloud-init config for user '{host_username}'...") 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 # Mount host's Coder directory using shift=true
print(f"Mounting host {host_coder_dir} -> {container_coder_path} (shift=true)...") print(f"Mounting host {host_coder_dir} -> {container_coder_path} (shift=true)...")
run_cmd([ lxc_run(["config", "device", "add", args.name, "host-coder", "disk",
"lxc", "config", "device", "add", args.name, "host-coder", "disk", f"source={host_coder_dir}",
f"source={host_coder_dir}", f"path={container_coder_path}",
f"path={container_coder_path}", "shift=true"])
"shift=true"
])
# Handle --git option # Handle --git option
if args.git: if args.git:
@ -113,25 +271,43 @@ runcmd:
if gitconfig_path.exists(): if gitconfig_path.exists():
container_git_path = f"/home/{host_username}/.gitconfig" container_git_path = f"/home/{host_username}/.gitconfig"
print(f"Mapping host git configurations to {container_git_path} (shift=true)...") print(f"Mapping host git configurations to {container_git_path} (shift=true)...")
run_cmd([ lxc_run(["config", "device", "add", args.name, "gitconfig", "disk",
"lxc", "config", "device", "add", args.name, "gitconfig", "disk", f"source={gitconfig_path}",
f"source={gitconfig_path}", f"path={container_git_path}",
f"path={container_git_path}", "shift=true"])
"shift=true"
])
else: else:
print(f"Warning: --git requested, but {gitconfig_path} was not found on host.", file=sys.stderr) print(f"Warning: --git requested, but {gitconfig_path} was not found on host.", file=sys.stderr)
# Handle --nested option # Handle --nested option
if args.nested: if args.nested:
print("Enabling nesting capabilities...") print("Enabling nesting capabilities...")
run_cmd(["lxc", "config", "set", args.name, "security.nesting", "true"]) lxc_run([ "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.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 # Start container and await cloud-init provisioning
print("Starting container and executing cloud-init user provisioning...") print("Starting container and executing cloud-init user provisioning...")
run_cmd(["lxc", "start", args.name]) lxc_run([ "start", args.name])
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}' is ready! Workspace mounted at {container_coder_path}") 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 # Dynamically find the user matching your UID inside the container
try: try:
result = subprocess.run( result = lxc_subprocess(
["lxc", "exec", args.name, "--", "id", "-nu", str(uid)], ["exec", args.name, "--", "id", "-nu", str(uid)],
capture_output=True, text=True, check=True capture_output=True, text=True, check=True
) )
container_username = result.stdout.strip() container_username = result.stdout.strip()
@ -172,46 +348,171 @@ def handle_enter(args):
if display: if display:
env_prefix += f"DISPLAY={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 # 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 = [ exec_cmd = [
"lxc", "exec", args.name, "lxc", "exec", args.name,
"--cwd", target_cwd, "--cwd", target_cwd,
"--user", str(uid), "--user", str(uid),
"--group", str(gid), "--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): def handle_delete(args):
print(f"Stopping container '{args.name}'...") 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}'...") print(f"Deleting container '{args.name}'...")
run_cmd(["lxc", "delete", args.name]) lxc_run([ "delete", args.name])
print(f"Container '{args.name}' deleted successfully.") 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 <name> --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): def handle_rename(args):
print(f"Preparing to rename '{args.old_name}' to '{args.new_name}'...") print(f"Preparing to rename '{args.old_name}' to '{args.new_name}'...")
# 1. LXD requires containers to be stopped before renaming # 1. LXD requires containers to be stopped before renaming
print(f"Stopping '{args.old_name}'...") print(f"Stopping '{args.old_name}'...")
# Using stdout/stderr redirection internally to hide the error if it's already stopped # Using stdout/stderr redirection internally to hide the error if it's already stopped
subprocess.run( lxc_subprocess(
["lxc", "stop", args.old_name], ["stop", args.old_name],
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL stderr=subprocess.DEVNULL
) )
# 2. Execute the rename # 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 # 3. Restart the container under its new name
print(f"Starting renamed container '{args.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 # 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}'!") 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("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("-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("--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("--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 # Clone command
parser_clone = subparsers.add_parser("clone", help="Clone an existing LXD container") parser_clone = subparsers.add_parser("clone", help="Clone an existing LXD container")
@ -243,10 +549,33 @@ def main():
# List command # List command
parser_list = subparsers.add_parser("list", aliases=["ls"], help="List all managed LXD containers") 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 # Delete command
parser_delete = subparsers.add_parser("delete", help="Delete an LXD container") parser_delete = subparsers.add_parser("delete", help="Delete an LXD container")
parser_delete.add_argument("name", help="Name of the container to delete") 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() args = parser.parse_args()
if args.command == "create": if args.command == "create":
@ -259,8 +588,16 @@ def main():
handle_enter(args) handle_enter(args)
elif args.command in ["list", "ls"]: elif args.command in ["list", "ls"]:
handle_list(args) 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": elif args.command == "delete":
handle_delete(args) handle_delete(args)
elif args.command == "add":
handle_add(args)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

30
setup.sh Executable file
View File

@ -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"