BigPickle
This commit is contained in:
parent
46a9a7e144
commit
5864a69f1b
|
|
@ -0,0 +1,6 @@
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
@ -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
|
||||||
405
lxcd.py
405
lxcd.py
|
|
@ -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,27 +68,127 @@ 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"
|
||||||
|
|
@ -66,8 +202,27 @@ def handle_create(args):
|
||||||
|
|
||||||
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}'"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if prefix:
|
||||||
|
os.execvp("sudo", ["sudo", *exec_cmd])
|
||||||
|
else:
|
||||||
os.execvp("lxc", exec_cmd)
|
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()
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
Loading…
Reference in New Issue