jailmaker/jlmkr.py

1360 lines
43 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
import argparse
import configparser
import contextlib
import ctypes
2023-08-12 16:14:09 +00:00
import glob
import hashlib
2023-08-15 11:03:43 +00:00
import json
import os
2023-08-15 06:07:36 +00:00
import platform
import re
import readline
import shlex
import shutil
import stat
import subprocess
import sys
2023-02-28 19:25:11 +00:00
import time
import urllib.request
2023-08-15 13:21:53 +00:00
from collections import defaultdict
from inspect import cleandoc
from pathlib import Path, PurePath
from textwrap import dedent
# Only set a color if we have an interactive tty
if sys.stdout.isatty():
2024-01-21 17:21:24 +00:00
BOLD = "\033[1m"
RED = "\033[91m"
YELLOW = "\033[93m"
UNDERLINE = "\033[4m"
NORMAL = "\033[0m"
else:
2024-01-21 17:21:24 +00:00
BOLD = RED = YELLOW = UNDERLINE = NORMAL = ""
DISCLAIMER = f"""{YELLOW}{BOLD}USE THIS SCRIPT AT YOUR OWN RISK!
IT COMES WITHOUT WARRANTY AND IS NOT SUPPORTED BY IXSYSTEMS.{NORMAL}"""
2024-01-21 17:21:24 +00:00
DESCRIPTION = (
"Create persistent Linux 'jails' on TrueNAS SCALE, with full access to all files \
2023-08-12 16:14:09 +00:00
via bind mounts, thanks to systemd-nspawn!"
2024-01-21 17:21:24 +00:00
)
2024-01-21 17:21:24 +00:00
VERSION = "1.0.1"
2024-01-21 17:21:24 +00:00
JAILS_DIR_PATH = "jails"
JAIL_CONFIG_NAME = "config"
JAIL_ROOTFS_NAME = "rootfs"
DOWNLOAD_SCRIPT_DIGEST = (
"6cca2eda73c7358c232fecb4e750b3bf0afa9636efb5de6a9517b7df78be12a4"
)
2023-08-14 13:19:25 +00:00
SCRIPT_PATH = os.path.realpath(__file__)
SCRIPT_NAME = os.path.basename(SCRIPT_PATH)
SCRIPT_DIR_PATH = os.path.dirname(SCRIPT_PATH)
2023-08-15 15:52:25 +00:00
COMMAND_NAME = os.path.basename(__file__)
2024-01-21 17:21:24 +00:00
SYMLINK_NAME = "jlmkr"
TEXT_EDITOR = "nano"
def eprint(*args, **kwargs):
"""
Print to stderr.
"""
print(*args, file=sys.stderr, **kwargs)
def fail(*args, **kwargs):
"""
Print to stderr and exit.
"""
eprint(*args, **kwargs)
sys.exit(1)
2023-02-28 19:25:11 +00:00
def get_jail_path(jail_name):
return os.path.join(JAILS_DIR_PATH, jail_name)
2023-08-14 14:12:33 +00:00
def get_jail_config_path(jail_name):
return os.path.join(get_jail_path(jail_name), JAIL_CONFIG_NAME)
def get_jail_rootfs_path(jail_name):
return os.path.join(get_jail_path(jail_name), JAIL_ROOTFS_NAME)
2023-03-03 21:33:41 +00:00
def passthrough_intel(gpu_passthrough_intel, systemd_nspawn_additional_args):
2024-01-21 17:21:24 +00:00
if gpu_passthrough_intel != "1":
2023-03-03 21:33:41 +00:00
return
2024-01-21 17:21:24 +00:00
if not os.path.exists("/dev/dri"):
eprint(
dedent(
"""
2023-03-03 21:33:41 +00:00
No intel GPU seems to be present...
2024-01-21 17:21:24 +00:00
Skip passthrough of intel GPU."""
)
)
2023-03-03 21:33:41 +00:00
return
2024-01-21 17:21:24 +00:00
systemd_nspawn_additional_args.append("--bind=/dev/dri")
2023-03-03 21:33:41 +00:00
2024-01-21 17:21:24 +00:00
def passthrough_nvidia(
gpu_passthrough_nvidia, systemd_nspawn_additional_args, jail_name
):
2024-01-27 04:44:05 +00:00
jail_rootfs_path = get_jail_rootfs_path(jail_name)
ld_so_conf_path = Path(
os.path.join(jail_rootfs_path), f"etc/ld.so.conf.d/{SYMLINK_NAME}-nvidia.conf"
)
if gpu_passthrough_nvidia != "1":
# Cleanup the config file we made when passthrough was enabled
ld_so_conf_path.unlink(missing_ok=True)
return
2024-01-26 21:33:33 +00:00
# Load the nvidia kernel module
if subprocess.run(["modprobe", "nvidia-current-uvm"]).returncode != 0:
eprint(
dedent(
"""
Failed to load nvidia-current-uvm kernel module.
Skip passthrough of nvidia GPU."""
)
)
return
2024-01-27 04:44:05 +00:00
# Run nvidia-smi to initialize the nvidia driver
# If we can't run nvidia-smi successfully,
# then nvidia-container-cli list will fail too:
# we shouldn't continue with gpu passthrough
2024-01-26 21:33:33 +00:00
if subprocess.run(["nvidia-smi", "-f", "/dev/null"]).returncode != 0:
2023-03-04 17:31:56 +00:00
eprint("Skip passthrough of nvidia GPU.")
return
2023-03-04 10:36:43 +00:00
2023-03-03 21:33:41 +00:00
try:
2024-01-21 17:21:24 +00:00
nvidia_files = set(
(
[
x
for x in subprocess.check_output(["nvidia-container-cli", "list"])
.decode()
.split("\n")
if x
]
)
)
2023-03-03 21:33:41 +00:00
except:
2024-01-21 17:21:24 +00:00
eprint(
dedent(
"""
2023-03-03 21:33:41 +00:00
Unable to detect which nvidia driver files to mount.
2024-01-21 17:21:24 +00:00
Skip passthrough of nvidia GPU."""
)
)
2023-03-04 17:31:56 +00:00
return
2023-03-03 21:33:41 +00:00
# Also make nvidia-smi available inside the path,
2023-03-04 17:31:56 +00:00
# while mounting the symlink will be resolved and nvidia-smi will appear as a regular file
2024-01-21 17:21:24 +00:00
nvidia_files.add("/usr/bin/nvidia-smi")
2023-03-03 21:33:41 +00:00
nvidia_mounts = []
for file_path in nvidia_files:
if not os.path.exists(file_path):
# Don't try to mount files not present on the host
2024-01-21 17:21:24 +00:00
print(f"Skipped mounting {file_path}, it doesn't exist on the host...")
2023-03-03 21:33:41 +00:00
continue
2024-01-21 17:21:24 +00:00
if file_path.startswith("/dev/"):
2023-03-03 21:33:41 +00:00
nvidia_mounts.append(f"--bind={file_path}")
else:
nvidia_mounts.append(f"--bind-ro={file_path}")
# Check if the parent dir exists where we want to write our conf file
if ld_so_conf_path.parent.exists():
2024-01-21 17:21:24 +00:00
nvidia_libraries = set(
Path(x)
for x in subprocess.check_output(
["nvidia-container-cli", "list", "--libraries"]
)
.decode()
.split("\n")
if x
)
library_folders = set(str(x.parent) for x in nvidia_libraries)
2023-03-03 21:33:41 +00:00
# Only write if the conf file doesn't yet exist or has different contents
existing_conf_libraries = set()
if ld_so_conf_path.exists():
2023-08-14 14:12:33 +00:00
existing_conf_libraries.update(
2024-01-21 17:21:24 +00:00
x for x in ld_so_conf_path.read_text().splitlines() if x
)
if library_folders != existing_conf_libraries:
2024-01-21 17:21:24 +00:00
print("\n".join(x for x in library_folders), file=ld_so_conf_path.open("w"))
# Run ldconfig inside systemd-nspawn jail with nvidia mounts...
subprocess.run(
2024-01-21 17:21:24 +00:00
[
"systemd-nspawn",
"--quiet",
f"--machine={jail_name}",
2023-08-14 14:12:33 +00:00
f"--directory={jail_rootfs_path}",
*nvidia_mounts,
2024-01-21 17:21:24 +00:00
"ldconfig",
]
)
2023-03-03 21:33:41 +00:00
else:
2024-01-21 17:21:24 +00:00
eprint(
dedent(
"""
2023-03-03 21:33:41 +00:00
Unable to write the ld.so.conf.d directory inside the jail (it doesn't exist).
Skipping call to ldconfig.
2024-01-21 17:21:24 +00:00
The nvidia drivers will probably not be detected..."""
)
)
2023-03-03 21:33:41 +00:00
systemd_nspawn_additional_args += nvidia_mounts
2023-08-15 12:45:52 +00:00
def exec_jail(jail_name, cmd, args):
"""
Execute a command in the jail with given name.
"""
2024-01-21 17:21:24 +00:00
subprocess.run(
[
"systemd-run",
"--machine",
jail_name,
"--quiet",
"--pipe",
"--wait",
"--collect",
"--service-type=exec",
cmd,
]
+ args,
check=True,
)
2023-08-15 12:45:52 +00:00
2023-08-15 11:49:52 +00:00
def status_jail(jail_name):
"""
2023-08-15 12:45:52 +00:00
Show the status of the systemd service wrapping the jail with given name.
2023-08-15 11:49:52 +00:00
"""
# Alternatively `machinectl status jail_name` could be used
subprocess.run(["systemctl", "status", f"{SYMLINK_NAME}-{jail_name}"])
def log_jail(jail_name):
"""
Show the log file of the jail with given name.
"""
subprocess.run(["journalctl", "-u", f"{SYMLINK_NAME}-{jail_name}"])
2023-08-15 11:21:28 +00:00
def shell_jail(jail_name):
"""
Open a shell in the jail with given name.
"""
subprocess.run(["machinectl", "shell", jail_name])
2023-08-15 11:13:51 +00:00
def stop_jail(jail_name):
"""
Stop jail with given name.
"""
subprocess.run(["machinectl", "poweroff", jail_name])
2023-08-15 17:28:43 +00:00
def parse_config(jail_config_path):
config = configparser.ConfigParser()
try:
# Workaround to read config file without section headers
2024-01-21 17:21:24 +00:00
config.read_string("[DEFAULT]\n" + Path(jail_config_path).read_text())
2023-08-15 17:28:43 +00:00
except FileNotFoundError:
2024-01-21 17:21:24 +00:00
eprint(f"Unable to find config file: {jail_config_path}.")
2023-08-15 17:28:43 +00:00
return
2024-01-21 17:21:24 +00:00
config = dict(config["DEFAULT"])
2023-08-15 17:28:43 +00:00
return config
def start_jail(jail_name, check_startup_enabled=False):
"""
Start jail with given name.
"""
2024-01-21 17:21:24 +00:00
skip_start_message = (
f"Skipped starting jail {jail_name}. It appears to be running already..."
)
2023-08-15 11:49:52 +00:00
2023-08-15 17:28:43 +00:00
if not check_startup_enabled and jail_is_running(jail_name):
fail(skip_start_message)
2023-02-28 19:25:11 +00:00
jail_path = get_jail_path(jail_name)
2023-08-14 14:12:33 +00:00
jail_config_path = get_jail_config_path(jail_name)
2023-08-15 17:28:43 +00:00
config = parse_config(jail_config_path)
2023-08-15 17:28:43 +00:00
if not config:
2024-01-20 15:47:51 +00:00
fail("Aborting...")
2023-08-15 17:28:43 +00:00
# Only start if the startup setting is enabled in the config
if check_startup_enabled:
2024-01-21 17:21:24 +00:00
if config.get("startup") == "1":
2023-08-15 17:28:43 +00:00
# We should start this jail based on the startup config...
if jail_is_running(jail_name):
# ...but we can skip if it's already running
eprint(skip_start_message)
return
else:
# Skip starting this jail since the startup config setting isnot enabled
return
systemd_run_additional_args = [
2023-08-15 05:41:54 +00:00
f"--unit={SYMLINK_NAME}-{jail_name}",
f"--working-directory=./{jail_path}",
f"--description=My nspawn jail {jail_name} [created with jailmaker]",
]
# Always add --bind-ro=/sys/module to make lsmod happy
# https://manpages.debian.org/bookworm/manpages/sysfs.5.en.html
systemd_nspawn_additional_args = [
f"--machine={jail_name}",
f"--directory={JAIL_ROOTFS_NAME}",
]
2024-01-26 21:33:33 +00:00
# TODO: split the docker_compatible option into separate options
# - privileged (to disable seccomp, set DevicePolicy=auto and add all capabilities)
# - how to call the option to enable ip_forward and bridge-nf-call?
# - add CSV value for preloading kernel modules like linux.kernel_modules in LXC
2024-01-27 03:23:46 +00:00
2024-01-21 17:21:24 +00:00
if config.get("docker_compatible") == "1":
# Enable ip forwarding on the host (docker needs it)
2024-01-21 17:21:24 +00:00
print(1, file=open("/proc/sys/net/ipv4/ip_forward", "w"))
# Load br_netfilter kernel module and enable bridge-nf-call to fix warning when running docker info:
# WARNING: bridge-nf-call-iptables is disabled
# WARNING: bridge-nf-call-ip6tables is disabled
#
# If we are using Apps then this should already be enabled
# May cause "guest container traffic to be blocked by iptables rules that are intended for the host"
# https://unix.stackexchange.com/q/720105/477308
# https://github.com/moby/moby/issues/24809
# https://docs.oracle.com/en/operating-systems/oracle-linux/docker/docker-KnownIssues.html#docker-issues
# https://wiki.libvirt.org/page/Net.bridge.bridge-nf-call_and_sysctl.conf
# https://serverfault.com/questions/963759/docker-breaks-libvirt-bridge-network
2024-01-21 17:21:24 +00:00
if subprocess.run(["modprobe", "br_netfilter"]).returncode == 0:
print(1, file=open("/proc/sys/net/bridge/bridge-nf-call-iptables", "w"))
print(1, file=open("/proc/sys/net/bridge/bridge-nf-call-ip6tables", "w"))
else:
2024-01-21 17:21:24 +00:00
eprint(
dedent(
"""
Failed to load br_netfilter kernel module."""
)
)
# To properly run docker inside the jail, we need to lift restrictions
# Without DevicePolicy=auto images with device nodes may not be pulled
# For example docker pull ljishen/sysbench would fail
# Fortunately I didn't encounter many images with device nodes...
#
# Issue: https://github.com/moby/moby/issues/35245
#
# The systemd-nspawn manual explicitly mentions:
# Device nodes may not be created
# https://www.freedesktop.org/software/systemd/man/systemd-nspawn.html
#
# Workaround: https://github.com/kinvolk/kube-spawn/pull/328
#
2024-01-26 19:56:25 +00:00
# As of 26-3-2024 on TrueNAS-SCALE-23.10.1.1 it seems to no longer be
# required to use DevicePolicy=auto
# Docker can successfully pull the ljishen/sysbench test image
# Running mknod /dev/port c 1 4 manually works too...
# Unknown why this suddenly started working...
# https://github.com/systemd/systemd/issues/21987
#
# Use SYSTEMD_SECCOMP=0: https://github.com/systemd/systemd/issues/18370
systemd_run_additional_args += [
2024-01-21 17:21:24 +00:00
"--setenv=SYSTEMD_SECCOMP=0",
"--property=DevicePolicy=auto",
]
# Add additional flags required for docker
systemd_nspawn_additional_args += [
2024-01-21 17:21:24 +00:00
"--capability=all",
]
# Legacy gpu_passthrough config setting
2024-01-21 17:21:24 +00:00
if config.get("gpu_passthrough") == "1":
gpu_passthrough_intel = "1"
gpu_passthrough_nvidia = "1"
2023-03-02 17:23:34 +00:00
else:
2024-01-21 17:21:24 +00:00
gpu_passthrough_intel = config.get("gpu_passthrough_intel")
gpu_passthrough_nvidia = config.get("gpu_passthrough_nvidia")
2024-01-21 17:21:24 +00:00
if gpu_passthrough_intel == "1" or gpu_passthrough_nvidia == "1":
systemd_nspawn_additional_args.append("--property=DeviceAllow=char-drm rw")
2023-03-03 21:33:41 +00:00
passthrough_intel(gpu_passthrough_intel, systemd_nspawn_additional_args)
2024-01-21 17:21:24 +00:00
passthrough_nvidia(
gpu_passthrough_nvidia, systemd_nspawn_additional_args, jail_name
)
cmd = [
"systemd-run",
*shlex.split(config.get("systemd_run_default_args", "")),
*systemd_run_additional_args,
"--",
"systemd-nspawn",
*shlex.split(config.get("systemd_nspawn_default_args", "")),
*systemd_nspawn_additional_args,
*shlex.split(config.get("systemd_nspawn_user_args", "")),
]
print(
dedent(
f"""
2023-08-15 17:28:43 +00:00
Starting jail {jail_name} with the following command:
2023-02-27 19:49:25 +00:00
{shlex.join(cmd)}
2024-01-21 17:21:24 +00:00
"""
)
)
try:
subprocess.run(cmd, check=True)
except subprocess.CalledProcessError:
2024-01-21 17:21:24 +00:00
fail(
dedent(
f"""
2023-08-15 17:28:43 +00:00
Failed to start jail {jail_name}...
In case of a config error, you may fix it with:
2023-08-15 05:41:54 +00:00
{SYMLINK_NAME} edit {jail_name}
2024-01-21 17:21:24 +00:00
"""
)
)
def cleanup(jail_path):
"""
Cleanup after aborted jail creation.
"""
if os.path.isdir(jail_path):
eprint(f"Cleaning up: {jail_path}")
shutil.rmtree(jail_path)
def input_with_default(prompt, default):
"""
Ask user for input with a default value already provided.
"""
readline.set_startup_hook(lambda: readline.insert_text(default))
try:
return input(prompt)
finally:
readline.set_startup_hook()
def validate_sha256(file_path, digest):
"""
Validates if a file matches a sha256 digest.
"""
try:
2024-01-21 17:21:24 +00:00
with open(file_path, "rb") as f:
file_hash = hashlib.sha256(f.read()).hexdigest()
return file_hash == digest
except FileNotFoundError:
return False
2024-01-21 17:21:24 +00:00
def run_lxc_download_script(
jail_name=None, jail_path=None, jail_rootfs_path=None, distro=None, release=None
):
arch = "amd64"
lxc_dir = ".lxc"
lxc_cache = os.path.join(lxc_dir, "cache")
lxc_download_script = os.path.join(lxc_dir, "lxc-download.sh")
2023-02-05 16:22:46 +00:00
# Create the lxc dirs if nonexistent
os.makedirs(lxc_dir, exist_ok=True)
stat_chmod(lxc_dir, 0o700)
os.makedirs(lxc_cache, exist_ok=True)
stat_chmod(lxc_cache, 0o700)
2023-08-15 11:03:43 +00:00
try:
if os.stat(lxc_download_script).st_uid != 0:
os.remove(lxc_download_script)
except FileNotFoundError:
pass
2023-02-05 16:22:46 +00:00
# Fetch the lxc download script if not present locally (or hash doesn't match)
if not validate_sha256(lxc_download_script, DOWNLOAD_SCRIPT_DIGEST):
urllib.request.urlretrieve(
2024-01-21 17:21:24 +00:00
"https://raw.githubusercontent.com/Jip-Hop/lxc/58520263041b6864cadad96278848f9b8ce78ee9/templates/lxc-download.in",
lxc_download_script,
)
2023-02-05 16:22:46 +00:00
if not validate_sha256(lxc_download_script, DOWNLOAD_SCRIPT_DIGEST):
fail("Abort! Downloaded script has unexpected contents.")
stat_chmod(lxc_download_script, 0o700)
2023-08-15 15:40:10 +00:00
2023-08-15 15:39:28 +00:00
check_exit_code = False
2023-02-05 16:22:46 +00:00
if None not in [jail_name, jail_path, jail_rootfs_path, distro, release]:
2023-08-15 15:39:28 +00:00
check_exit_code = True
2024-01-21 17:21:24 +00:00
cmd = [
lxc_download_script,
f"--name={jail_name}",
f"--path={jail_path}",
f"--rootfs={jail_rootfs_path}",
f"--arch={arch}",
f"--dist={distro}",
f"--release={release}",
]
2023-02-05 16:22:46 +00:00
else:
2024-01-21 17:21:24 +00:00
cmd = [lxc_download_script, "--list", f"--arch={arch}"]
2023-08-15 15:39:28 +00:00
2024-01-21 17:21:24 +00:00
p1 = subprocess.Popen(
cmd, stdout=subprocess.PIPE, env={"LXC_CACHE_PATH": lxc_cache}
)
2023-08-15 15:39:28 +00:00
2024-01-21 17:21:24 +00:00
for line in iter(p1.stdout.readline, b""):
2023-08-15 15:39:28 +00:00
line = line.decode().strip()
2023-08-15 15:40:10 +00:00
# Filter out the known incompatible distros
2024-01-21 17:21:24 +00:00
if not re.match(
r"^(alpine|amazonlinux|busybox|devuan|funtoo|openwrt|plamo|voidlinux)\s",
line,
):
2023-08-15 15:39:28 +00:00
print(line)
p1.wait()
2023-08-15 15:40:10 +00:00
2023-08-15 15:39:28 +00:00
if check_exit_code and p1.returncode != 0:
fail("Aborting...")
2023-02-05 16:22:46 +00:00
def stat_chmod(file_path, mode):
"""
Change mode if file doesn't already have this mode.
"""
if mode != stat.S_IMODE(os.stat(file_path).st_mode):
os.chmod(file_path, mode)
def agree(question, default=None):
"""
Ask user a yes/no question.
"""
2024-01-21 17:21:24 +00:00
hint = "[Y/n]" if default == "y" else ("[y/N]" if default == "n" else "[y/n]")
while True:
user_input = input(f"{question} {hint} ") or default
2024-01-21 17:21:24 +00:00
if user_input.lower() in ["y", "n"]:
return user_input.lower() == "y"
eprint("Invalid input. Please type 'y' for yes or 'n' for no and press enter.")
def get_mount_point(path):
"""
Return the mount point on which the given path resides.
"""
path = os.path.abspath(path)
while not os.path.ismount(path):
path = os.path.dirname(path)
return path
2023-02-28 19:25:11 +00:00
def check_jail_name_valid(jail_name, warn=True):
"""
Return True if jail name matches the required format.
"""
2024-01-21 17:21:24 +00:00
if (
re.match(r"^[.a-zA-Z0-9-]{1,64}$", jail_name)
and not jail_name.startswith(".")
and ".." not in jail_name
):
2023-02-28 19:25:11 +00:00
return True
if warn:
2024-01-21 17:21:24 +00:00
eprint(
dedent(
f"""
2023-02-28 19:25:11 +00:00
{YELLOW}{BOLD}WARNING: INVALID NAME{NORMAL}
A valid name consists of:
- allowed characters (alphanumeric, dash, dot)
- no leading or trailing dots
- no sequences of multiple dots
2024-01-21 17:21:24 +00:00
- max 64 characters"""
)
)
2023-02-28 19:25:11 +00:00
return False
def check_jail_name_available(jail_name, warn=True):
"""
Return True if jail name is not yet taken.
"""
if not os.path.exists(get_jail_path(jail_name)):
return True
if warn:
print()
eprint("A jail with this name already exists.")
return False
2024-01-21 17:21:24 +00:00
def create_jail(jail_name, distro="debian", release="bookworm"):
"""
Create jail with given name.
"""
print(DISCLAIMER)
2024-01-21 17:21:24 +00:00
if os.path.basename(os.getcwd()) != "jailmaker":
fail(
dedent(
f"""
2023-08-15 15:52:25 +00:00
{COMMAND_NAME} needs to create files.
Currently it can not decide if it is safe to create files in:
{SCRIPT_DIR_PATH}
2024-01-21 17:21:24 +00:00
Please create a dedicated directory called 'jailmaker', store {SCRIPT_NAME} there and try again."""
)
)
2024-01-21 17:21:24 +00:00
if not PurePath(get_mount_point(os.getcwd())).is_relative_to("/mnt"):
print(
dedent(
f"""
{YELLOW}{BOLD}WARNING: BEWARE OF DATA LOSS{NORMAL}
{SCRIPT_NAME} should be on a dataset mounted under /mnt (it currently is not).
Storing it on the boot-pool means losing all jails when updating TrueNAS.
If you continue, jails will be stored under:
{SCRIPT_DIR_PATH}
2024-01-21 17:21:24 +00:00
"""
)
)
if not agree("Do you wish to ignore this warning and continue?", "n"):
fail("Aborting...")
# Create the dir where to store the jails
os.makedirs(JAILS_DIR_PATH, exist_ok=True)
stat_chmod(JAILS_DIR_PATH, 0o700)
print()
2024-01-21 17:21:24 +00:00
if not agree(f"Install the recommended image ({distro} {release})?", "y"):
print(
dedent(
f"""
{YELLOW}{BOLD}WARNING: ADVANCED USAGE{NORMAL}
You may now choose from a list which distro to install.
2023-08-15 15:52:25 +00:00
But not all of them may work with {COMMAND_NAME} since these images are made for LXC.
Distros based on systemd probably work (e.g. Ubuntu, Arch Linux and Rocky Linux).
2024-01-21 17:21:24 +00:00
"""
)
)
input("Press Enter to continue...")
print()
2023-02-05 16:22:46 +00:00
run_lxc_download_script()
2024-01-21 17:21:24 +00:00
print(
dedent(
"""
Choose from the DIST column.
2024-01-21 17:21:24 +00:00
"""
)
)
distro = input("Distro: ")
2024-01-21 17:21:24 +00:00
print(
dedent(
"""
Choose from the RELEASE column (or ARCH if RELEASE is empty).
2024-01-21 17:21:24 +00:00
"""
)
)
release = input("Release: ")
2023-02-28 19:25:11 +00:00
while True:
print()
jail_name = input_with_default("Enter jail name: ", jail_name).strip()
2023-02-28 19:25:11 +00:00
if check_jail_name_valid(jail_name):
if check_jail_name_available(jail_name):
break
2023-02-28 19:25:11 +00:00
jail_path = get_jail_path(jail_name)
# Cleanup in except, but only once the jail_path is final
# Otherwise we may cleanup the wrong directory
try:
2024-01-21 17:21:24 +00:00
print(
dedent(
f"""
2023-08-15 15:52:25 +00:00
Docker won't be installed by {COMMAND_NAME}.
But it can setup the jail with the capabilities required to run docker.
You can turn DOCKER_COMPATIBLE mode on/off post-install.
2024-01-21 17:21:24 +00:00
"""
)
)
docker_compatible = 0
2024-01-21 17:21:24 +00:00
if agree("Make jail docker compatible right now?", "n"):
docker_compatible = 1
print()
gpu_passthrough_intel = 0
2024-01-27 03:23:46 +00:00
if agree("Passthrough the intel GPU (if present)?", "n"):
gpu_passthrough_intel = 1
2024-01-27 03:23:46 +00:00
print()
2024-01-27 03:23:46 +00:00
gpu_passthrough_nvidia = 0
2023-03-04 10:36:43 +00:00
2024-01-27 03:23:46 +00:00
if agree("Passthrough the nvidia GPU (if present)?", "n"):
gpu_passthrough_nvidia = 1
2024-01-21 17:21:24 +00:00
print(
dedent(
f"""
{YELLOW}{BOLD}WARNING: CHECK SYNTAX{NORMAL}
2023-02-27 19:49:25 +00:00
You may pass additional flags to systemd-nspawn.
With incorrect flags the jail may not start.
It is possible to correct/add/remove flags post-install.
2024-01-21 17:21:24 +00:00
"""
)
)
2024-01-21 17:21:24 +00:00
if agree("Show the man page for systemd-nspawn?", "n"):
subprocess.run(["man", "systemd-nspawn"])
else:
2023-08-24 08:03:48 +00:00
try:
2024-01-21 17:21:24 +00:00
base_os_version = platform.freedesktop_os_release().get(
"VERSION_CODENAME", release
)
2023-08-24 08:03:48 +00:00
except AttributeError:
base_os_version = release
2024-01-21 17:21:24 +00:00
print(
dedent(
f"""
You may read the systemd-nspawn manual online:
2024-01-21 17:21:24 +00:00
https://manpages.debian.org/{base_os_version}/systemd-container/systemd-nspawn.1.en.html"""
)
)
# Backslashes and colons need to be escaped in bind mount options:
# e.g. to bind mount a file called:
# weird chars :?\"
# the corresponding command would be:
# --bind-ro='/mnt/data/weird chars \:?\\"'
2024-01-21 17:21:24 +00:00
print(
dedent(
"""
Would you like to add additional systemd-nspawn flags?
For example to mount directories inside the jail you may:
Mount the TrueNAS location /mnt/pool/dataset to the /home directory of the jail with:
--bind='/mnt/pool/dataset:/home'
Or the same, but readonly, with:
--bind-ro='/mnt/pool/dataset:/home'
Or create MACVLAN interface for static IP, with:
--network-macvlan=eno1 --resolv-conf=bind-host
2024-01-21 17:21:24 +00:00
"""
)
)
# Enable tab auto completion of file paths after the = symbol
2024-01-21 17:21:24 +00:00
readline.set_completer_delims("=")
readline.parse_and_bind("tab: complete")
readline_lib = ctypes.CDLL(readline.__file__)
rl_completer_quote_characters = ctypes.c_char_p.in_dll(
2024-01-21 17:21:24 +00:00
readline_lib, "rl_completer_quote_characters"
)
# Let the readline library know about quote characters for completion
2024-01-21 17:21:24 +00:00
rl_completer_quote_characters.value = "\"'".encode("utf-8")
# TODO: more robust tab completion of file paths with space or = character
# Currently completing these only works when the path is quoted
# https://thoughtbot.com/blog/tab-completion-in-gnu-readline
# https://stackoverflow.com/a/67118744
# https://github.com/python-cmd2/cmd2/blob/ee7599f9ac0dbb6ce3793f6b665ba1200d3ef9a3/cmd2/cmd2.py
# https://stackoverflow.com/a/40152927
systemd_nspawn_user_args = input("Additional flags: ") or ""
# Disable tab auto completion
2024-01-21 17:21:24 +00:00
readline.parse_and_bind("tab: self-insert")
2024-01-21 17:21:24 +00:00
print(
dedent(
f"""
2023-08-15 17:28:43 +00:00
The `{COMMAND_NAME} startup` command can automatically ensure {COMMAND_NAME} is installed properly and start a selection of jails.
This comes in handy when you want to automatically start multiple jails after booting TrueNAS SCALE (e.g. from a Post Init Script).
2024-01-21 17:21:24 +00:00
"""
)
)
2023-08-15 17:28:43 +00:00
2024-01-21 17:21:24 +00:00
startup = int(
agree(
f"Do you want to start this jail when running: {COMMAND_NAME} startup?",
"n",
)
)
2023-08-15 17:28:43 +00:00
print()
2024-01-21 17:21:24 +00:00
2023-08-14 14:12:33 +00:00
jail_config_path = get_jail_config_path(jail_name)
jail_rootfs_path = get_jail_rootfs_path(jail_name)
# Create directory for rootfs
os.makedirs(jail_rootfs_path, exist_ok=True)
# LXC download script needs to write to this file during install
# but we don't need it so we will remove it later
open(jail_config_path, "a").close()
2024-01-21 17:21:24 +00:00
run_lxc_download_script(jail_name, jail_path, jail_rootfs_path, distro, release)
# Assuming the name of your jail is "myjail"
# and "machinectl shell myjail" doesn't work
# Try:
#
# Stop the jail with:
# machinectl stop myjail
# And start a shell inside the jail without the --boot option:
# systemd-nspawn -q -D jails/myjail/rootfs /bin/sh
# Then set a root password with:
# In case of amazonlinux you may need to run:
# yum update -y && yum install -y passwd
# passwd
# exit
# Then you may login from the host via:
# machinectl login myjail
#
# You could also enable SSH inside the jail to login
#
# Or if that doesn't work (e.g. for alpine) get a shell via:
# nsenter -t $(machinectl show myjail -p Leader --value) -a /bin/sh -l
# But alpine jails made with jailmaker have other issues
# They don't shutdown cleanly via systemctl and machinectl...
2024-01-21 17:21:24 +00:00
if (
os.path.basename(
os.path.realpath(os.path.join(jail_rootfs_path, "sbin/init"))
)
!= "systemd"
):
print(
dedent(
f"""
{YELLOW}{BOLD}WARNING: DISTRO NOT SUPPORTED{NORMAL}
2023-02-27 19:49:25 +00:00
Chosen distro appears not to use systemd...
You probably will not get a shell with:
machinectl shell {jail_name}
You may get a shell with this command:
nsenter -t $(machinectl show {jail_name} -p Leader --value) -a /bin/sh -l
Read about the downsides of nsenter:
https://github.com/systemd/systemd/issues/12785#issuecomment-503019081
2023-08-15 15:52:25 +00:00
{BOLD}Using this distro with {COMMAND_NAME} is NOT recommended.{NORMAL}
2024-01-21 17:21:24 +00:00
"""
)
)
2024-01-21 17:21:24 +00:00
if agree("Abort creating jail?", "y"):
exit(1)
with contextlib.suppress(FileNotFoundError):
# Remove config which systemd handles for us
2024-01-21 17:21:24 +00:00
os.remove(os.path.join(jail_rootfs_path, "etc/machine-id"))
os.remove(os.path.join(jail_rootfs_path, "etc/resolv.conf"))
# https://github.com/systemd/systemd/issues/852
2024-01-21 17:21:24 +00:00
print(
"\n".join([f"pts/{i}" for i in range(0, 11)]),
file=open(os.path.join(jail_rootfs_path, "etc/securetty"), "w"),
)
2024-01-21 17:21:24 +00:00
network_dir_path = os.path.join(jail_rootfs_path, "etc/systemd/network")
# Modify default network settings, if network_dir_path exists
if os.path.isdir(network_dir_path):
default_host0_network_file = os.path.join(
2024-01-21 17:21:24 +00:00
jail_rootfs_path, "lib/systemd/network/80-container-host0.network"
)
# Check if default host0 network file exists
if os.path.isfile(default_host0_network_file):
override_network_file = os.path.join(
2024-01-21 17:21:24 +00:00
network_dir_path, "80-container-host0.network"
)
# Override the default 80-container-host0.network file (by using the same name)
# This config applies when using the --network-bridge option of systemd-nspawn
# Disable LinkLocalAddressing on IPv4, or else the container won't get IP address via DHCP
# But keep it enabled on IPv6, as SLAAC and DHCPv6 both require a local-link address to function
2024-01-21 17:21:24 +00:00
print(
Path(default_host0_network_file)
.read_text()
.replace("LinkLocalAddressing=yes", "LinkLocalAddressing=ipv6"),
file=open(override_network_file, "w"),
)
# Setup DHCP for macvlan network interfaces
# This config applies when using the --network-macvlan option of systemd-nspawn
# https://www.debian.org/doc/manuals/debian-reference/ch05.en.html#_the_modern_network_configuration_without_gui
2024-01-21 17:21:24 +00:00
print(
cleandoc(
"""
[Match]
Virtualization=container
Name=mv-*
[Network]
DHCP=yes
LinkLocalAddressing=ipv6
[DHCPv4]
UseDNS=true
UseTimezone=true
2024-01-21 17:21:24 +00:00
"""
),
file=open(os.path.join(network_dir_path, "mv-dhcp.network"), "w"),
)
2024-01-21 17:10:09 +00:00
# Override preset which caused systemd-networkd to be disabled (e.g. fedora 39)
# https://www.freedesktop.org/software/systemd/man/latest/systemd.preset.html
# https://github.com/lxc/lxc-ci/blob/f632823ecd9b258ed42df40449ec54ed7ef8e77d/images/fedora.yaml#L312C5-L312C38
2024-01-21 17:21:24 +00:00
preset_path = os.path.join(jail_rootfs_path, "etc/systemd/system-preset")
2024-01-21 17:10:09 +00:00
os.makedirs(preset_path, exist_ok=True)
2024-01-21 17:21:24 +00:00
print(
"enable systemd-networkd.service",
file=open(os.path.join(preset_path, "00-jailmaker.preset"), "w"),
)
2024-01-21 17:10:09 +00:00
# Use mostly default settings for systemd-nspawn but with systemd-run instead of a service file:
# https://github.com/systemd/systemd/blob/main/units/systemd-nspawn%40.service.in
# Use TasksMax=infinity since this is what docker does:
# https://github.com/docker/engine/blob/master/contrib/init/systemd/docker.service
# Use SYSTEMD_NSPAWN_LOCK=0: otherwise jail won't start jail after a shutdown (but why?)
# Would give "directory tree currently busy" error and I'd have to run
# `rm /run/systemd/nspawn/locks/*` and remove the .lck file from jail_path
# Disabling locking isn't a big deal as systemd-nspawn will prevent starting a container
# with the same name anyway: as long as we're starting jails using this script,
# it won't be possible to start the same jail twice
systemd_run_default_args = [
2024-01-21 17:21:24 +00:00
"--property=KillMode=mixed",
"--property=Type=notify",
"--property=RestartForceExitStatus=133",
"--property=SuccessExitStatus=133",
"--property=Delegate=yes",
"--property=TasksMax=infinity",
"--collect",
"--setenv=SYSTEMD_NSPAWN_LOCK=0",
]
systemd_nspawn_default_args = [
"--keep-unit",
"--quiet",
"--boot",
"--bind-ro=/sys/module",
]
2024-01-21 17:21:24 +00:00
config = cleandoc(
f"""
2023-08-15 17:28:43 +00:00
startup={startup}
docker_compatible={docker_compatible}
gpu_passthrough_intel={gpu_passthrough_intel}
gpu_passthrough_nvidia={gpu_passthrough_nvidia}
systemd_nspawn_user_args={systemd_nspawn_user_args}
# You generally will not need to change the options below
systemd_run_default_args={' '.join(systemd_run_default_args)}
systemd_nspawn_default_args={' '.join(systemd_nspawn_default_args)}
2024-01-21 17:21:24 +00:00
"""
)
2024-01-21 17:21:24 +00:00
print(config, file=open(jail_config_path, "w"))
os.chmod(jail_config_path, 0o600)
# Cleanup on any exception and rethrow
except BaseException as error:
cleanup(jail_path)
raise error
print()
2024-01-21 17:21:24 +00:00
if agree(f"Do you want to start jail {jail_name} right now?", "y"):
start_jail(jail_name)
2023-08-14 15:31:59 +00:00
def jail_is_running(jail_name):
2024-01-21 17:21:24 +00:00
return (
subprocess.run(
["machinectl", "show", jail_name],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
).returncode
== 0
)
2023-08-14 15:31:59 +00:00
2023-08-14 14:12:33 +00:00
def edit_jail(jail_name):
"""
Edit jail with given name.
"""
if check_jail_name_valid(jail_name):
if check_jail_name_available(jail_name, False):
eprint(f"A jail with name {jail_name} does not exist.")
else:
2023-08-15 05:41:54 +00:00
jail_config_path = get_jail_config_path(jail_name)
if not shutil.which(TEXT_EDITOR):
2023-08-15 11:34:37 +00:00
eprint(f"Unable to edit config file: {jail_config_path}.")
eprint(f"The {TEXT_EDITOR} text editor is not available.")
2023-08-15 05:41:54 +00:00
else:
2023-08-15 12:01:05 +00:00
subprocess.run([TEXT_EDITOR, get_jail_config_path(jail_name)])
2023-08-15 05:41:54 +00:00
if jail_is_running(jail_name):
print("\nRestart the jail for edits to apply (if you made any).")
2023-08-14 14:12:33 +00:00
2023-03-10 22:13:23 +00:00
def remove_jail(jail_name):
2023-02-27 19:25:15 +00:00
"""
2023-03-10 22:13:23 +00:00
Remove jail with given name.
2023-02-27 19:25:15 +00:00
"""
2023-02-28 19:25:11 +00:00
if check_jail_name_valid(jail_name):
if check_jail_name_available(jail_name, False):
2023-02-27 21:01:36 +00:00
eprint(f"A jail with name {jail_name} does not exist.")
2023-02-28 19:25:11 +00:00
else:
2024-01-21 17:21:24 +00:00
check = (
input(f'\nCAUTION: Type "{jail_name}" to confirm jail deletion!\n\n')
or ""
)
2023-02-28 19:25:11 +00:00
if check == jail_name:
jail_path = get_jail_path(jail_name)
2023-08-14 15:31:59 +00:00
if jail_is_running(jail_name):
print(f"\nWait for {jail_name} to stop...", end="")
2023-08-15 11:13:51 +00:00
stop_jail(jail_name)
2023-08-14 15:31:59 +00:00
# Need to sleep since deleting immediately after stop causes problems...
while jail_is_running(jail_name):
time.sleep(1)
print(".", end="", flush=True)
print(f"\nCleaning up: {jail_path}")
2023-02-28 19:25:11 +00:00
shutil.rmtree(jail_path)
else:
eprint("Wrong name, nothing happened.")
2023-02-27 19:25:15 +00:00
2023-02-28 21:17:20 +00:00
2023-08-15 11:03:43 +00:00
def print_table(header, list_of_objects, empty_value_indicator):
# Find max width for each column
widths = defaultdict(int)
for obj in list_of_objects:
for hdr in header:
2024-01-21 17:21:24 +00:00
widths[hdr] = max(widths[hdr], len(str(obj.get(hdr))), len(str(hdr)))
2023-08-15 11:03:43 +00:00
# Print header
2024-01-21 17:21:24 +00:00
print(
UNDERLINE + " ".join(hdr.upper().ljust(widths[hdr]) for hdr in header) + NORMAL
)
2023-08-15 11:03:43 +00:00
# Print rows
for obj in list_of_objects:
2024-01-21 17:21:24 +00:00
print(
" ".join(
str(obj.get(hdr, empty_value_indicator)).ljust(widths[hdr])
for hdr in header
)
)
2023-08-15 11:03:43 +00:00
def run_command_and_parse_json(command):
result = subprocess.run(command, capture_output=True, text=True)
output = result.stdout.strip()
try:
parsed_output = json.loads(output)
return parsed_output
except json.JSONDecodeError as e:
print(f"Error parsing JSON: {e}")
return None
2023-08-15 17:28:43 +00:00
def get_all_jail_names():
try:
jail_names = os.listdir(JAILS_DIR_PATH)
except FileNotFoundError:
jail_names = []
return jail_names
def list_jails():
"""
2023-02-28 19:49:44 +00:00
List all available and running jails.
"""
2023-08-15 11:03:43 +00:00
jails = {}
2024-01-21 17:21:24 +00:00
empty_value_indicator = "-"
2023-02-28 19:49:44 +00:00
2023-08-15 17:28:43 +00:00
jail_names = get_all_jail_names()
2023-08-15 11:03:43 +00:00
2023-08-15 17:28:43 +00:00
if not jail_names:
2024-01-21 17:21:24 +00:00
print("No jails.")
2023-08-15 11:03:43 +00:00
return
2023-08-15 17:28:43 +00:00
for jail in jail_names:
2023-08-15 11:03:43 +00:00
jails[jail] = {"name": jail, "running": False}
# Get running jails from machinectl
2024-01-21 17:21:24 +00:00
running_machines = run_command_and_parse_json(["machinectl", "list", "-o", "json"])
2023-08-15 11:03:43 +00:00
# Augment the jails dict with output from machinectl
for machine in running_machines:
2024-01-21 17:21:24 +00:00
machine_name = machine["machine"]
2023-08-15 11:03:43 +00:00
# We're only interested in the list of jails made with jailmaker
2024-01-21 17:21:24 +00:00
if machine["service"] == "systemd-nspawn" and machine_name in jails:
addresses = (machine.get("addresses") or empty_value_indicator).split("\n")
2023-08-15 11:03:43 +00:00
if len(addresses) > 1:
2024-01-21 17:21:24 +00:00
addresses = addresses[0] + ""
2023-08-15 11:03:43 +00:00
else:
addresses = addresses[0]
jails[machine_name] = {
"name": machine_name,
"running": True,
2024-01-21 17:21:24 +00:00
"os": machine["os"],
"version": machine["version"],
"addresses": addresses,
2023-08-15 11:03:43 +00:00
}
# TODO: add additional properties from the jails config file
2023-02-28 19:49:44 +00:00
2023-08-15 17:28:43 +00:00
for jail_name in jails:
config = parse_config(get_jail_config_path(jail_name))
startup = False
if config:
2024-01-21 17:21:24 +00:00
startup = bool(int(config.get("startup", "0")))
2023-08-15 17:28:43 +00:00
# TODO: in case config is missing or parsing fails,
# should an error message be thrown here?
2024-01-21 17:21:24 +00:00
jails[jail_name]["startup"] = startup
2023-08-15 17:28:43 +00:00
2024-01-21 17:21:24 +00:00
print_table(
["name", "running", "startup", "os", "version", "addresses"],
sorted(jails.values(), key=lambda x: x["name"]),
empty_value_indicator,
)
2023-02-27 19:25:15 +00:00
2023-08-14 14:12:33 +00:00
2023-08-14 13:42:20 +00:00
def install_jailmaker():
2023-08-12 16:14:09 +00:00
# Check if command exists in path
2024-01-21 17:21:24 +00:00
if shutil.which("systemd-nspawn"):
2023-08-12 16:14:09 +00:00
print("systemd-nspawn is already installed.")
2023-08-14 14:12:33 +00:00
else:
2023-08-14 13:42:20 +00:00
print("Installing jailmaker dependencies...")
original_permissions = {}
2023-08-12 16:14:09 +00:00
2024-01-21 17:21:24 +00:00
print(
"Temporarily enable apt and dpkg (if not already enabled) to install systemd-nspawn."
)
2023-08-12 16:14:09 +00:00
2023-08-14 13:42:20 +00:00
# Make /bin/apt* and /bin/dpkg* files executable
2024-01-21 17:21:24 +00:00
for file in glob.glob("/bin/apt*") + (glob.glob("/bin/dpkg*")):
2023-08-14 13:42:20 +00:00
original_permissions[file] = os.stat(file).st_mode
stat_chmod(file, 0o755)
2023-08-12 16:14:09 +00:00
2024-01-21 17:21:24 +00:00
subprocess.run(["apt-get", "update"], check=True)
subprocess.run(["apt-get", "install", "-y", "systemd-container"], check=True)
2023-08-12 16:14:09 +00:00
2023-08-14 13:42:20 +00:00
# Restore original permissions
print("Restore permissions of apt and dpkg.")
2023-08-12 16:14:09 +00:00
2023-08-14 13:42:20 +00:00
for file, original_permission in original_permissions.items():
stat_chmod(file, original_permission)
2023-08-14 14:12:33 +00:00
2024-01-21 17:21:24 +00:00
target = f"/usr/local/sbin/{SYMLINK_NAME}"
2023-08-12 16:14:09 +00:00
2023-08-14 13:42:20 +00:00
# Check if command exists in path
2023-08-15 05:41:54 +00:00
if shutil.which(SYMLINK_NAME):
print(f"The {SYMLINK_NAME} command is available.")
2023-08-14 13:42:20 +00:00
elif not os.path.lexists(target):
print(f"Creating symlink {target} to {SCRIPT_PATH}.")
os.symlink(SCRIPT_PATH, target)
else:
2023-08-14 14:12:33 +00:00
print(
2024-01-21 17:21:24 +00:00
f"File {target} already exists... Maybe it's a broken symlink from a previous install attempt?"
)
2023-08-14 13:42:20 +00:00
print(f"Skipped creating new symlink {target} to {SCRIPT_PATH}.")
2023-08-12 16:14:09 +00:00
2023-08-14 13:42:20 +00:00
print("Done installing jailmaker.")
2023-02-28 21:17:20 +00:00
2023-08-14 14:12:33 +00:00
2023-08-15 17:28:43 +00:00
def startup_jails():
install_jailmaker()
for jail_name in get_all_jail_names():
start_jail(jail_name, True)
def main():
2023-08-14 13:19:25 +00:00
if os.stat(SCRIPT_PATH).st_uid != 0:
2023-08-14 14:12:33 +00:00
fail(
2024-01-21 17:21:24 +00:00
f"This script should be owned by the root user... Fix it manually with: `chown root {SCRIPT_PATH}`."
)
2024-01-21 17:21:24 +00:00
parser = argparse.ArgumentParser(description=DESCRIPTION, epilog=DISCLAIMER)
2023-03-01 20:06:04 +00:00
2024-01-21 17:21:24 +00:00
parser.add_argument("--version", action="version", version=VERSION)
2024-01-21 17:21:24 +00:00
subparsers = parser.add_subparsers(title="commands", dest="subcommand", metavar="")
2024-01-21 17:21:24 +00:00
subparsers.add_parser(
name="install",
epilog=DISCLAIMER,
help="install jailmaker dependencies and create symlink",
)
2023-08-15 05:31:27 +00:00
2024-01-21 17:21:24 +00:00
subparsers.add_parser(
name="create", epilog=DISCLAIMER, help="create a new jail"
).add_argument("name", nargs="?", help="name of the jail")
2024-01-21 17:21:24 +00:00
subparsers.add_parser(
name="start", epilog=DISCLAIMER, help="start a previously created jail"
).add_argument("name", help="name of the jail")
2023-02-27 19:25:15 +00:00
2024-01-21 17:21:24 +00:00
subparsers.add_parser(
name="shell", epilog=DISCLAIMER, help="open shell in running jail"
).add_argument("name", help="name of the jail")
2023-08-15 11:21:28 +00:00
2024-01-21 17:21:24 +00:00
exec_parser = subparsers.add_parser(
name="exec", epilog=DISCLAIMER, help="execute a command in the jail"
)
exec_parser.add_argument("name", help="name of the jail")
exec_parser.add_argument("cmd", help="command to execute")
2023-08-15 12:45:52 +00:00
2024-01-21 17:21:24 +00:00
subparsers.add_parser(
name="status", epilog=DISCLAIMER, help="show jail status"
).add_argument("name", help="name of the jail")
2023-08-15 11:49:52 +00:00
2024-01-21 17:21:24 +00:00
subparsers.add_parser(
name="log", epilog=DISCLAIMER, help="show jail log"
).add_argument("name", help="name of the jail")
2023-08-15 11:49:52 +00:00
2024-01-21 17:21:24 +00:00
subparsers.add_parser(
name="stop", epilog=DISCLAIMER, help="stop a running jail"
).add_argument("name", help="name of the jail")
2023-08-15 11:13:51 +00:00
2024-01-21 17:21:24 +00:00
subparsers.add_parser(
name="edit",
epilog=DISCLAIMER,
help=f"edit jail config with {TEXT_EDITOR} text editor",
).add_argument("name", help="name of the jail to edit")
2023-08-14 14:12:33 +00:00
2024-01-21 17:21:24 +00:00
subparsers.add_parser(
name="remove", epilog=DISCLAIMER, help="remove a previously created jail"
).add_argument("name", help="name of the jail to remove")
2024-01-21 17:21:24 +00:00
subparsers.add_parser(name="list", epilog=DISCLAIMER, help="list jails")
2023-02-05 16:22:46 +00:00
2024-01-21 17:21:24 +00:00
subparsers.add_parser(
name="images",
epilog=DISCLAIMER,
help="list available images to create jails from",
)
2024-01-21 17:21:24 +00:00
subparsers.add_parser(
name="startup",
epilog=DISCLAIMER,
help=f"install {SYMLINK_NAME} and startup selected jails",
)
2023-08-15 17:28:43 +00:00
if os.getuid() != 0:
parser.print_usage()
fail("Run this script as root...")
# Set appropriate permissions (if not already set) for this file, since it's executed as root
2023-08-15 06:07:19 +00:00
stat_chmod(SCRIPT_PATH, 0o700)
2023-02-05 16:22:46 +00:00
# Work relative to this script
os.chdir(SCRIPT_DIR_PATH)
2023-08-15 12:45:52 +00:00
args, additional_args = parser.parse_known_args()
2024-01-21 17:21:24 +00:00
if args.subcommand == "install":
2023-08-15 17:28:43 +00:00
install_jailmaker()
2024-01-21 17:21:24 +00:00
elif args.subcommand == "create":
2023-08-15 17:28:43 +00:00
create_jail(args.name)
2024-01-21 17:21:24 +00:00
elif args.subcommand == "start":
2023-08-15 11:13:51 +00:00
start_jail(args.name)
2024-01-21 17:21:24 +00:00
elif args.subcommand == "shell":
2023-08-15 11:21:28 +00:00
shell_jail(args.name)
2024-01-21 17:21:24 +00:00
elif args.subcommand == "exec":
2023-08-15 12:45:52 +00:00
exec_jail(args.name, args.cmd, additional_args)
2024-01-21 17:21:24 +00:00
elif args.subcommand == "status":
2023-08-15 11:49:52 +00:00
status_jail(args.name)
2024-01-21 17:21:24 +00:00
elif args.subcommand == "log":
2023-08-15 11:49:52 +00:00
log_jail(args.name)
2024-01-21 17:21:24 +00:00
elif args.subcommand == "stop":
2023-08-15 11:13:51 +00:00
stop_jail(args.name)
2024-01-21 17:21:24 +00:00
elif args.subcommand == "edit":
2023-08-14 14:12:33 +00:00
edit_jail(args.name)
2024-01-21 17:21:24 +00:00
elif args.subcommand == "remove":
2023-03-10 22:13:23 +00:00
remove_jail(args.name)
2023-02-27 19:25:15 +00:00
2024-01-21 17:21:24 +00:00
elif args.subcommand == "list":
list_jails()
2024-01-21 17:21:24 +00:00
elif args.subcommand == "images":
2023-02-05 16:22:46 +00:00
run_lxc_download_script()
2024-01-21 17:21:24 +00:00
elif args.subcommand == "startup":
2023-08-15 17:28:43 +00:00
startup_jails()
else:
2024-01-21 17:21:24 +00:00
if agree("Create a new jail?", "y"):
print()
create_jail("")
else:
parser.print_usage()
2024-01-21 17:21:24 +00:00
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
sys.exit(130)