first
This commit is contained in:
commit
46a9a7e144
|
|
@ -0,0 +1,268 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def get_real_user_info():
|
||||||
|
"""
|
||||||
|
Detects the real user when running under sudo.
|
||||||
|
Falls back to normal environment variables if not running under sudo.
|
||||||
|
"""
|
||||||
|
sudo_user = os.environ.get("SUDO_USER")
|
||||||
|
sudo_uid = os.environ.get("SUDO_UID")
|
||||||
|
sudo_gid = os.environ.get("SUDO_GID")
|
||||||
|
|
||||||
|
if sudo_user and sudo_uid and sudo_gid:
|
||||||
|
return sudo_user, int(sudo_uid), int(sudo_gid), Path(f"/home/{sudo_user}")
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
os.environ.get("USER", "root"),
|
||||||
|
os.getuid(),
|
||||||
|
os.getgid(),
|
||||||
|
Path.home()
|
||||||
|
)
|
||||||
|
|
||||||
|
def run_cmd(cmd, check=True):
|
||||||
|
"""Helper to run shell commands."""
|
||||||
|
try:
|
||||||
|
return subprocess.run(cmd, check=check, text=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"Error executing command: {' '.join(cmd)}", file=sys.stderr)
|
||||||
|
sys.exit(e.returncode)
|
||||||
|
|
||||||
|
def handle_list(args):
|
||||||
|
# Pass the user.lxcd=true filter directly to LXD's native listing engine
|
||||||
|
run_cmd(["lxc", "list", "user.lxcd=true"])
|
||||||
|
|
||||||
|
def handle_clone(args):
|
||||||
|
print(f"Cloning container '{args.source}' to '{args.name}'...")
|
||||||
|
|
||||||
|
# 1. Native LXD copy (duplicates the filesystem, mounts, and tags)
|
||||||
|
run_cmd(["lxc", "copy", args.source, args.name])
|
||||||
|
|
||||||
|
# 2. Start the new cloned container
|
||||||
|
print(f"Starting cloned container '{args.name}'...")
|
||||||
|
run_cmd(["lxc", "start", args.name])
|
||||||
|
|
||||||
|
# 3. Wait for network/cloud-init stabilization
|
||||||
|
run_cmd(["lxc", "exec", args.name, "--", "cloud-init", "status", "--wait"], check=False)
|
||||||
|
|
||||||
|
print(f"Container '{args.name}' cloned successfully from '{args.source}'!")
|
||||||
|
|
||||||
|
def handle_create(args):
|
||||||
|
host_username, uid, gid, host_home = get_real_user_info()
|
||||||
|
|
||||||
|
# Paths for targeted workspace sharing
|
||||||
|
host_coder_dir = host_home / "Coder"
|
||||||
|
container_coder_path = f"/home/{host_username}/Coder"
|
||||||
|
|
||||||
|
# Ensure the 'Coder' directory exists on the host machine
|
||||||
|
if not host_coder_dir.exists():
|
||||||
|
print(f"Creating missing directory on host: {host_coder_dir}")
|
||||||
|
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}'...")
|
||||||
|
|
||||||
|
# Cloud-init configuration that drops stock 'ubuntu' user and seeds defaults
|
||||||
|
cloud_config = f"""#cloud-config
|
||||||
|
users:
|
||||||
|
- name: {host_username}
|
||||||
|
gecos: lxcd user
|
||||||
|
primary_group: {host_username}
|
||||||
|
groups: [adm, sudo, plugdev]
|
||||||
|
shell: /bin/bash
|
||||||
|
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||||
|
lock_passwd: true
|
||||||
|
homedir: /home/{host_username}
|
||||||
|
|
||||||
|
groups:
|
||||||
|
- {host_username}
|
||||||
|
|
||||||
|
runcmd:
|
||||||
|
# Copy missing skeleton files (without overwriting existing mounts) and fix ownership
|
||||||
|
- cp -rpn /etc/skel/. /home/{host_username}/
|
||||||
|
- chown -R {host_username}:{host_username} /home/{host_username}/
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Initialize container
|
||||||
|
run_cmd(["lxc", "init", args.image, args.name])
|
||||||
|
|
||||||
|
# Tag the container so lxcd knows it owns it
|
||||||
|
print("Tagging container as managed by lxcd...")
|
||||||
|
run_cmd(["lxc", "config", "set", args.name, "user.lxcd", "true"])
|
||||||
|
|
||||||
|
# Inject Cloud-init settings
|
||||||
|
print(f"Injecting cloud-init config for user '{host_username}'...")
|
||||||
|
run_cmd(["lxc", "config", "set", args.name, "cloud-init.user-data", cloud_config])
|
||||||
|
|
||||||
|
# Mount host's Coder directory using shift=true
|
||||||
|
print(f"Mounting host {host_coder_dir} -> {container_coder_path} (shift=true)...")
|
||||||
|
run_cmd([
|
||||||
|
"lxc", "config", "device", "add", args.name, "host-coder", "disk",
|
||||||
|
f"source={host_coder_dir}",
|
||||||
|
f"path={container_coder_path}",
|
||||||
|
"shift=true"
|
||||||
|
])
|
||||||
|
|
||||||
|
# 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)...")
|
||||||
|
run_cmd([
|
||||||
|
"lxc", "config", "device", "add", args.name, "gitconfig", "disk",
|
||||||
|
f"source={gitconfig_path}",
|
||||||
|
f"path={container_git_path}",
|
||||||
|
"shift=true"
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
print(f"Warning: --git requested, but {gitconfig_path} was not found on host.", file=sys.stderr)
|
||||||
|
|
||||||
|
# Handle --nested option
|
||||||
|
if args.nested:
|
||||||
|
print("Enabling nesting capabilities...")
|
||||||
|
run_cmd(["lxc", "config", "set", args.name, "security.nesting", "true"])
|
||||||
|
run_cmd(["lxc", "config", "set", args.name, "security.syscalls.intercept.mknod", "true"])
|
||||||
|
|
||||||
|
# Start container and await cloud-init provisioning
|
||||||
|
print("Starting container and executing cloud-init user provisioning...")
|
||||||
|
run_cmd(["lxc", "start", args.name])
|
||||||
|
run_cmd(["lxc", "exec", args.name, "--", "cloud-init", "status", "--wait"], check=False)
|
||||||
|
|
||||||
|
print(f"Container '{args.name}' is ready! Workspace mounted at {container_coder_path}")
|
||||||
|
|
||||||
|
def handle_enter(args):
|
||||||
|
_, uid, gid, host_home = get_real_user_info()
|
||||||
|
host_cwd = os.getcwd()
|
||||||
|
|
||||||
|
# Dynamically find the user matching your UID inside the container
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["lxc", "exec", args.name, "--", "id", "-nu", str(uid)],
|
||||||
|
capture_output=True, text=True, check=True
|
||||||
|
)
|
||||||
|
container_username = result.stdout.strip()
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
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
|
||||||
|
|
||||||
|
env_prefix = (
|
||||||
|
f"HOME={container_home} "
|
||||||
|
f"TERM={os.environ.get('TERM', 'xterm-256color')} "
|
||||||
|
f"LANG={os.environ.get('LANG', 'C.UTF-8')} "
|
||||||
|
)
|
||||||
|
|
||||||
|
display = os.environ.get("DISPLAY")
|
||||||
|
if display:
|
||||||
|
env_prefix += f"DISPLAY={display} "
|
||||||
|
|
||||||
|
# Execute using native LXD flags for user, group, and working directory
|
||||||
|
exec_cmd = [
|
||||||
|
"lxc", "exec", args.name,
|
||||||
|
"--cwd", target_cwd,
|
||||||
|
"--user", str(uid),
|
||||||
|
"--group", str(gid),
|
||||||
|
"--",
|
||||||
|
"sh", "-c", f"env {env_prefix} /bin/bash --login"
|
||||||
|
]
|
||||||
|
|
||||||
|
os.execvp("lxc", exec_cmd)
|
||||||
|
|
||||||
|
def handle_delete(args):
|
||||||
|
print(f"Stopping container '{args.name}'...")
|
||||||
|
run_cmd(["lxc", "stop", args.name], check=False)
|
||||||
|
print(f"Deleting container '{args.name}'...")
|
||||||
|
run_cmd(["lxc", "delete", args.name])
|
||||||
|
print(f"Container '{args.name}' deleted successfully.")
|
||||||
|
|
||||||
|
def handle_rename(args):
|
||||||
|
print(f"Preparing to rename '{args.old_name}' to '{args.new_name}'...")
|
||||||
|
|
||||||
|
# 1. LXD requires containers to be stopped before renaming
|
||||||
|
print(f"Stopping '{args.old_name}'...")
|
||||||
|
# Using stdout/stderr redirection internally to hide the error if it's already stopped
|
||||||
|
subprocess.run(
|
||||||
|
["lxc", "stop", args.old_name],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Execute the rename
|
||||||
|
run_cmd(["lxc", "rename", args.old_name, args.new_name])
|
||||||
|
|
||||||
|
# 3. Restart the container under its new name
|
||||||
|
print(f"Starting renamed container '{args.new_name}'...")
|
||||||
|
run_cmd(["lxc", "start", args.new_name])
|
||||||
|
|
||||||
|
# 4. Wait for it to stabilize
|
||||||
|
run_cmd(["lxc", "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)")
|
||||||
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
# 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("--nested", action="store_true", help="Enable nesting")
|
||||||
|
parser_create.add_argument("--git", action="store_true", help="Map host ~/.gitconfig into the container")
|
||||||
|
|
||||||
|
# Clone command
|
||||||
|
parser_clone = subparsers.add_parser("clone", help="Clone an existing LXD container")
|
||||||
|
parser_clone.add_argument("source", help="Name of the source container")
|
||||||
|
parser_clone.add_argument("name", help="Name of the new cloned container")
|
||||||
|
|
||||||
|
# Rename command
|
||||||
|
parser_rename = subparsers.add_parser("rename", aliases=["mv"], help="Rename an existing LXD container")
|
||||||
|
parser_rename.add_argument("old_name", help="Current name of the container")
|
||||||
|
parser_rename.add_argument("new_name", help="New name for the container")
|
||||||
|
|
||||||
|
# Enter command
|
||||||
|
parser_enter = subparsers.add_parser("enter", help="Enter an existing LXD container")
|
||||||
|
parser_enter.add_argument("name", help="Name of the container to enter")
|
||||||
|
|
||||||
|
# List command
|
||||||
|
parser_list = subparsers.add_parser("list", aliases=["ls"], help="List all managed LXD 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")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.command == "create":
|
||||||
|
handle_create(args)
|
||||||
|
elif args.command == "clone":
|
||||||
|
handle_clone(args)
|
||||||
|
elif args.command in ["rename", "mv"]:
|
||||||
|
handle_rename(args)
|
||||||
|
elif args.command == "enter":
|
||||||
|
handle_enter(args)
|
||||||
|
elif args.command in ["list", "ls"]:
|
||||||
|
handle_list(args)
|
||||||
|
elif args.command == "delete":
|
||||||
|
handle_delete(args)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue