From 474faf6ede70a04a18779c8d8ff10af16e1e15c2 Mon Sep 17 00:00:00 2001 From: Jip-Hop <2871973+Jip-Hop@users.noreply.github.com> Date: Sun, 21 Jan 2024 18:21:24 +0100 Subject: [PATCH] Use ruff formatter --- jlmkr.py | 739 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 458 insertions(+), 281 deletions(-) diff --git a/jlmkr.py b/jlmkr.py index e374f38..54bc067 100755 --- a/jlmkr.py +++ b/jlmkr.py @@ -25,32 +25,36 @@ from textwrap import dedent # Only set a color if we have an interactive tty if sys.stdout.isatty(): - BOLD = '\033[1m' - RED = '\033[91m' - YELLOW = '\033[93m' - UNDERLINE = '\033[4m' - NORMAL = '\033[0m' + BOLD = "\033[1m" + RED = "\033[91m" + YELLOW = "\033[93m" + UNDERLINE = "\033[4m" + NORMAL = "\033[0m" else: - BOLD = RED = YELLOW = UNDERLINE = NORMAL = '' + 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}""" -DESCRIPTION = "Create persistent Linux 'jails' on TrueNAS SCALE, with full access to all files \ +DESCRIPTION = ( + "Create persistent Linux 'jails' on TrueNAS SCALE, with full access to all files \ via bind mounts, thanks to systemd-nspawn!" +) -VERSION = '1.0.1' +VERSION = "1.0.1" -JAILS_DIR_PATH = 'jails' -JAIL_CONFIG_NAME = 'config' -JAIL_ROOTFS_NAME = 'rootfs' -DOWNLOAD_SCRIPT_DIGEST = '6cca2eda73c7358c232fecb4e750b3bf0afa9636efb5de6a9517b7df78be12a4' +JAILS_DIR_PATH = "jails" +JAIL_CONFIG_NAME = "config" +JAIL_ROOTFS_NAME = "rootfs" +DOWNLOAD_SCRIPT_DIGEST = ( + "6cca2eda73c7358c232fecb4e750b3bf0afa9636efb5de6a9517b7df78be12a4" +) SCRIPT_PATH = os.path.realpath(__file__) SCRIPT_NAME = os.path.basename(SCRIPT_PATH) SCRIPT_DIR_PATH = os.path.dirname(SCRIPT_PATH) COMMAND_NAME = os.path.basename(__file__) -SYMLINK_NAME = 'jlmkr' -TEXT_EDITOR = 'nano' +SYMLINK_NAME = "jlmkr" +TEXT_EDITOR = "nano" def eprint(*args, **kwargs): @@ -81,24 +85,31 @@ def get_jail_rootfs_path(jail_name): def passthrough_intel(gpu_passthrough_intel, systemd_nspawn_additional_args): - if gpu_passthrough_intel != '1': + if gpu_passthrough_intel != "1": return - if not os.path.exists('/dev/dri'): - eprint(dedent(""" + if not os.path.exists("/dev/dri"): + eprint( + dedent( + """ No intel GPU seems to be present... - Skip passthrough of intel GPU.""")) + Skip passthrough of intel GPU.""" + ) + ) return - systemd_nspawn_additional_args.append('--bind=/dev/dri') + systemd_nspawn_additional_args.append("--bind=/dev/dri") -def passthrough_nvidia(gpu_passthrough_nvidia, systemd_nspawn_additional_args, jail_name): +def passthrough_nvidia( + gpu_passthrough_nvidia, systemd_nspawn_additional_args, jail_name +): 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') + 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': + if gpu_passthrough_nvidia != "1": # Cleanup the config file we made when passthrough was enabled ld_so_conf_path.unlink(missing_ok=True) return @@ -108,67 +119,93 @@ def passthrough_nvidia(gpu_passthrough_nvidia, systemd_nspawn_additional_args, j # If we can't run nvidia-smi successfully, # then nvidia-container-cli list will fail too: # we shouldn't continue with gpu passthrough - subprocess.run(['nvidia-smi', '-f', '/dev/null'], check=True) + subprocess.run(["nvidia-smi", "-f", "/dev/null"], check=True) except: eprint("Skip passthrough of nvidia GPU.") return try: - nvidia_files = set(([x for x in subprocess.check_output( - ['nvidia-container-cli', 'list']).decode().split('\n') if x])) + nvidia_files = set( + ( + [ + x + for x in subprocess.check_output(["nvidia-container-cli", "list"]) + .decode() + .split("\n") + if x + ] + ) + ) except: - eprint(dedent(""" + eprint( + dedent( + """ Unable to detect which nvidia driver files to mount. - Skip passthrough of nvidia GPU.""")) + Skip passthrough of nvidia GPU.""" + ) + ) return # Also make nvidia-smi available inside the path, # while mounting the symlink will be resolved and nvidia-smi will appear as a regular file - nvidia_files.add('/usr/bin/nvidia-smi') + nvidia_files.add("/usr/bin/nvidia-smi") 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 - print( - f"Skipped mounting {file_path}, it doesn't exist on the host...") + print(f"Skipped mounting {file_path}, it doesn't exist on the host...") continue - if file_path.startswith('/dev/'): + if file_path.startswith("/dev/"): 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(): - nvidia_libraries = set(Path(x) for x in subprocess.check_output( - ['nvidia-container-cli', 'list', '--libraries']).decode().split('\n') if x) + 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) # Only write if the conf file doesn't yet exist or has different contents existing_conf_libraries = set() if ld_so_conf_path.exists(): existing_conf_libraries.update( - x for x in ld_so_conf_path.read_text().splitlines() if x) + x for x in ld_so_conf_path.read_text().splitlines() if x + ) if library_folders != existing_conf_libraries: - print("\n".join(x for x in library_folders), - file=ld_so_conf_path.open('w')) + 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( - ['systemd-nspawn', - '--quiet', + [ + "systemd-nspawn", + "--quiet", f"--machine={jail_name}", f"--directory={jail_rootfs_path}", *nvidia_mounts, - "ldconfig"]) + "ldconfig", + ] + ) else: - eprint(dedent(""" + eprint( + dedent( + """ Unable to write the ld.so.conf.d directory inside the jail (it doesn't exist). Skipping call to ldconfig. - The nvidia drivers will probably not be detected...""")) + The nvidia drivers will probably not be detected...""" + ) + ) systemd_nspawn_additional_args += nvidia_mounts @@ -177,8 +214,21 @@ def exec_jail(jail_name, cmd, args): """ Execute a command in the jail with given name. """ - subprocess.run(['systemd-run', '--machine', jail_name, '--quiet', '--pipe', - '--wait', '--collect', '--service-type=exec', cmd] + args, check=True) + subprocess.run( + [ + "systemd-run", + "--machine", + jail_name, + "--quiet", + "--pipe", + "--wait", + "--collect", + "--service-type=exec", + cmd, + ] + + args, + check=True, + ) def status_jail(jail_name): @@ -214,12 +264,12 @@ def parse_config(jail_config_path): config = configparser.ConfigParser() try: # Workaround to read config file without section headers - config.read_string('[DEFAULT]\n'+Path(jail_config_path).read_text()) + config.read_string("[DEFAULT]\n" + Path(jail_config_path).read_text()) except FileNotFoundError: - eprint(f'Unable to find config file: {jail_config_path}.') + eprint(f"Unable to find config file: {jail_config_path}.") return - config = dict(config['DEFAULT']) + config = dict(config["DEFAULT"]) return config @@ -228,7 +278,9 @@ def start_jail(jail_name, check_startup_enabled=False): """ Start jail with given name. """ - skip_start_message = f"Skipped starting jail {jail_name}. It appears to be running already..." + skip_start_message = ( + f"Skipped starting jail {jail_name}. It appears to be running already..." + ) if not check_startup_enabled and jail_is_running(jail_name): fail(skip_start_message) @@ -243,7 +295,7 @@ def start_jail(jail_name, check_startup_enabled=False): # Only start if the startup setting is enabled in the config if check_startup_enabled: - if config.get('startup') == '1': + if config.get("startup") == "1": # 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 @@ -264,9 +316,9 @@ def start_jail(jail_name, check_startup_enabled=False): f"--directory={JAIL_ROOTFS_NAME}", ] - if config.get('docker_compatible') == '1': + if config.get("docker_compatible") == "1": # Enable ip forwarding on the host (docker needs it) - print(1, file=open('/proc/sys/net/ipv4/ip_forward', 'w')) + 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 @@ -280,12 +332,16 @@ def start_jail(jail_name, check_startup_enabled=False): # https://wiki.libvirt.org/page/Net.bridge.bridge-nf-call_and_sysctl.conf # https://serverfault.com/questions/963759/docker-breaks-libvirt-bridge-network - 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')) + 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: - eprint(dedent(""" - Failed to load br_netfilter kernel module.""")) + 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 @@ -310,56 +366,65 @@ def start_jail(jail_name, check_startup_enabled=False): # Use SYSTEMD_SECCOMP=0: https://github.com/systemd/systemd/issues/18370 systemd_run_additional_args += [ - '--setenv=SYSTEMD_SECCOMP=0', - '--property=DevicePolicy=auto', + "--setenv=SYSTEMD_SECCOMP=0", + "--property=DevicePolicy=auto", ] # Add additional flags required for docker systemd_nspawn_additional_args += [ - '--capability=all', - '--system-call-filter=add_key keyctl bpf', + "--capability=all", + "--system-call-filter=add_key keyctl bpf", ] # Legacy gpu_passthrough config setting - if config.get('gpu_passthrough') == '1': - gpu_passthrough_intel = '1' - gpu_passthrough_nvidia = '1' + if config.get("gpu_passthrough") == "1": + gpu_passthrough_intel = "1" + gpu_passthrough_nvidia = "1" else: - gpu_passthrough_intel = config.get('gpu_passthrough_intel') - gpu_passthrough_nvidia = config.get('gpu_passthrough_nvidia') + gpu_passthrough_intel = config.get("gpu_passthrough_intel") + gpu_passthrough_nvidia = config.get("gpu_passthrough_nvidia") - if gpu_passthrough_intel == '1' or gpu_passthrough_nvidia == '1': - systemd_nspawn_additional_args.append( - '--property=DeviceAllow=char-drm rw') + if gpu_passthrough_intel == "1" or gpu_passthrough_nvidia == "1": + systemd_nspawn_additional_args.append("--property=DeviceAllow=char-drm rw") passthrough_intel(gpu_passthrough_intel, systemd_nspawn_additional_args) - passthrough_nvidia(gpu_passthrough_nvidia, - systemd_nspawn_additional_args, jail_name) + 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', '')) - ] + 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""" + print( + dedent( + f""" Starting jail {jail_name} with the following command: {shlex.join(cmd)} - """)) + """ + ) + ) try: subprocess.run(cmd, check=True) except subprocess.CalledProcessError: - fail(dedent(f""" + fail( + dedent( + f""" Failed to start jail {jail_name}... In case of a config error, you may fix it with: {SYMLINK_NAME} edit {jail_name} - """)) + """ + ) + ) def cleanup(jail_path): @@ -387,19 +452,20 @@ def validate_sha256(file_path, digest): Validates if a file matches a sha256 digest. """ try: - with open(file_path, 'rb') as f: + with open(file_path, "rb") as f: file_hash = hashlib.sha256(f.read()).hexdigest() return file_hash == digest except FileNotFoundError: return False -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') +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") # Create the lxc dirs if nonexistent os.makedirs(lxc_dir, exist_ok=True) @@ -416,7 +482,9 @@ def run_lxc_download_script(jail_name=None, jail_path=None, jail_rootfs_path=Non # 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( - "https://raw.githubusercontent.com/Jip-Hop/lxc/58520263041b6864cadad96278848f9b8ce78ee9/templates/lxc-download.in", lxc_download_script) + "https://raw.githubusercontent.com/Jip-Hop/lxc/58520263041b6864cadad96278848f9b8ce78ee9/templates/lxc-download.in", + lxc_download_script, + ) if not validate_sha256(lxc_download_script, DOWNLOAD_SCRIPT_DIGEST): fail("Abort! Downloaded script has unexpected contents.") @@ -426,18 +494,29 @@ def run_lxc_download_script(jail_name=None, jail_path=None, jail_rootfs_path=Non if None not in [jail_name, jail_path, jail_rootfs_path, distro, release]: check_exit_code = True - 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}'] + 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}", + ] else: - cmd = [lxc_download_script, '--list', f'--arch={arch}'] + cmd = [lxc_download_script, "--list", f"--arch={arch}"] - p1 = subprocess.Popen(cmd, stdout=subprocess.PIPE, env={ - "LXC_CACHE_PATH": lxc_cache}) + p1 = subprocess.Popen( + cmd, stdout=subprocess.PIPE, env={"LXC_CACHE_PATH": lxc_cache} + ) - for line in iter(p1.stdout.readline, b''): + for line in iter(p1.stdout.readline, b""): line = line.decode().strip() # Filter out the known incompatible distros - if not re.match(r"^(alpine|amazonlinux|busybox|devuan|funtoo|openwrt|plamo|voidlinux)\s", line): + if not re.match( + r"^(alpine|amazonlinux|busybox|devuan|funtoo|openwrt|plamo|voidlinux)\s", + line, + ): print(line) p1.wait() @@ -458,14 +537,13 @@ def agree(question, default=None): """ Ask user a yes/no question. """ - hint = '[Y/n]' if default == 'y' else ( - '[y/N]' if default == 'n' else '[y/n]') + 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 - if user_input.lower() in ['y', 'n']: - return user_input.lower() == 'y' + 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.") @@ -484,18 +562,26 @@ def check_jail_name_valid(jail_name, warn=True): """ Return True if jail name matches the required format. """ - if re.match(r"^[.a-zA-Z0-9-]{1,64}$", jail_name) and not jail_name.startswith(".") and ".." not in jail_name: + if ( + re.match(r"^[.a-zA-Z0-9-]{1,64}$", jail_name) + and not jail_name.startswith(".") + and ".." not in jail_name + ): return True if warn: - eprint(dedent(f""" + eprint( + dedent( + f""" {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 - - max 64 characters""")) + - max 64 characters""" + ) + ) return False @@ -512,30 +598,38 @@ def check_jail_name_available(jail_name, warn=True): return False -def create_jail(jail_name, distro='debian', release='bookworm'): +def create_jail(jail_name, distro="debian", release="bookworm"): """ Create jail with given name. """ print(DISCLAIMER) - if os.path.basename(os.getcwd()) != 'jailmaker': - fail(dedent(f""" + if os.path.basename(os.getcwd()) != "jailmaker": + fail( + dedent( + f""" {COMMAND_NAME} needs to create files. Currently it can not decide if it is safe to create files in: {SCRIPT_DIR_PATH} - Please create a dedicated directory called 'jailmaker', store {SCRIPT_NAME} there and try again.""")) + Please create a dedicated directory called 'jailmaker', store {SCRIPT_NAME} there and try again.""" + ) + ) - if not PurePath(get_mount_point(os.getcwd())).is_relative_to('/mnt'): - print(dedent(f""" + 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} - """)) - if not agree("Do you wish to ignore this warning and continue?", 'n'): + """ + ) + ) + if not agree("Do you wish to ignore this warning and continue?", "n"): fail("Aborting...") # Create the dir where to store the jails @@ -543,28 +637,40 @@ def create_jail(jail_name, distro='debian', release='bookworm'): stat_chmod(JAILS_DIR_PATH, 0o700) print() - if not agree(f"Install the recommended image ({distro} {release})?", 'y'): - print(dedent(f""" + 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. 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). - """)) + """ + ) + ) input("Press Enter to continue...") print() run_lxc_download_script() - print(dedent(""" + print( + dedent( + """ Choose from the DIST column. - """)) + """ + ) + ) distro = input("Distro: ") - print(dedent(""" + print( + dedent( + """ Choose from the RELEASE column (or ARCH if RELEASE is empty). - """)) + """ + ) + ) release = input("Release: ") @@ -580,58 +686,76 @@ def create_jail(jail_name, distro='debian', release='bookworm'): # Cleanup in except, but only once the jail_path is final # Otherwise we may cleanup the wrong directory try: - print(dedent(f""" + print( + dedent( + f""" 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. - """)) + """ + ) + ) docker_compatible = 0 - if agree('Make jail docker compatible right now?', 'n'): + if agree("Make jail docker compatible right now?", "n"): docker_compatible = 1 print() gpu_passthrough_intel = 0 - if os.path.exists('/dev/dri'): + if os.path.exists("/dev/dri"): print("Detected the presence of an intel GPU.\n") - if agree('Passthrough the intel GPU?', 'n'): + if agree("Passthrough the intel GPU?", "n"): gpu_passthrough_intel = 1 gpu_passthrough_nvidia = 0 try: - subprocess.run(['nvidia-smi'], check=True, - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.run( + ["nvidia-smi"], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) nvidia_detected = True except: nvidia_detected = False if nvidia_detected: print("Detected the presence of an nvidia GPU.\n") - if agree('Passthrough the nvidia GPU?', 'n'): + if agree("Passthrough the nvidia GPU?", "n"): gpu_passthrough_nvidia = 1 - print(dedent(f""" + print( + dedent( + f""" {YELLOW}{BOLD}WARNING: CHECK SYNTAX{NORMAL} 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. - """)) + """ + ) + ) - if agree('Show the man page for systemd-nspawn?', 'n'): - subprocess.run(['man', 'systemd-nspawn']) + if agree("Show the man page for systemd-nspawn?", "n"): + subprocess.run(["man", "systemd-nspawn"]) else: try: - base_os_version = platform.freedesktop_os_release().get('VERSION_CODENAME', release) + base_os_version = platform.freedesktop_os_release().get( + "VERSION_CODENAME", release + ) except AttributeError: base_os_version = release - print(dedent(f""" + print( + dedent( + f""" You may read the systemd-nspawn manual online: - https://manpages.debian.org/{base_os_version}/systemd-container/systemd-nspawn.1.en.html""")) + 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: @@ -639,7 +763,9 @@ def create_jail(jail_name, distro='debian', release='bookworm'): # the corresponding command would be: # --bind-ro='/mnt/data/weird chars \:?\\"' - print(dedent(""" + 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: @@ -648,20 +774,21 @@ def create_jail(jail_name, distro='debian', release='bookworm'): --bind-ro='/mnt/pool/dataset:/home' Or create MACVLAN interface for static IP, with: --network-macvlan=eno1 --resolv-conf=bind-host - """)) + """ + ) + ) # Enable tab auto completion of file paths after the = symbol - readline.set_completer_delims('=') - readline.parse_and_bind('tab: complete') + 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( - readline_lib, - "rl_completer_quote_characters" + readline_lib, "rl_completer_quote_characters" ) # Let the readline library know about quote characters for completion - rl_completer_quote_characters.value = "\"'".encode('utf-8') + 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 @@ -672,18 +799,26 @@ def create_jail(jail_name, distro='debian', release='bookworm'): systemd_nspawn_user_args = input("Additional flags: ") or "" # Disable tab auto completion - readline.parse_and_bind('tab: self-insert') + readline.parse_and_bind("tab: self-insert") - print(dedent(f""" + print( + dedent( + f""" 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). - """)) + """ + ) + ) - startup = int(agree( - f"Do you want to start this jail when running: {COMMAND_NAME} startup?", 'n')) + startup = int( + agree( + f"Do you want to start this jail when running: {COMMAND_NAME} startup?", + "n", + ) + ) print() - + jail_config_path = get_jail_config_path(jail_name) jail_rootfs_path = get_jail_rootfs_path(jail_name) @@ -693,8 +828,7 @@ def create_jail(jail_name, distro='debian', release='bookworm'): # but we don't need it so we will remove it later open(jail_config_path, "a").close() - run_lxc_download_script(jail_name, jail_path, - jail_rootfs_path, distro, release) + 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 @@ -719,9 +853,15 @@ def create_jail(jail_name, distro='debian', release='bookworm'): # But alpine jails made with jailmaker have other issues # They don't shutdown cleanly via systemctl and machinectl... - if os.path.basename(os.path.realpath( - os.path.join(jail_rootfs_path, 'sbin/init'))) != "systemd": - print(dedent(f""" + 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} Chosen distro appears not to use systemd... @@ -736,44 +876,55 @@ def create_jail(jail_name, distro='debian', release='bookworm'): https://github.com/systemd/systemd/issues/12785#issuecomment-503019081 {BOLD}Using this distro with {COMMAND_NAME} is NOT recommended.{NORMAL} - """)) + """ + ) + ) - if agree("Abort creating jail?", 'y'): + if agree("Abort creating jail?", "y"): exit(1) with contextlib.suppress(FileNotFoundError): # Remove config which systemd handles for us - os.remove(os.path.join(jail_rootfs_path, 'etc/machine-id')) - os.remove(os.path.join(jail_rootfs_path, 'etc/resolv.conf')) + 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 - print('\n'.join([f"pts/{i}" for i in range(0, 11)]), - file=open(os.path.join(jail_rootfs_path, 'etc/securetty'), 'w')) + print( + "\n".join([f"pts/{i}" for i in range(0, 11)]), + file=open(os.path.join(jail_rootfs_path, "etc/securetty"), "w"), + ) - network_dir_path = os.path.join( - jail_rootfs_path, "etc/systemd/network") + 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( - jail_rootfs_path, "lib/systemd/network/80-container-host0.network") + 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( - network_dir_path, "80-container-host0.network") + 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 - print(Path(default_host0_network_file).read_text().replace("LinkLocalAddressing=yes", - "LinkLocalAddressing=ipv6"), file=open(override_network_file, 'w')) + 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 - print(cleandoc(""" + print( + cleandoc( + """ [Match] Virtualization=container Name=mv-* @@ -785,15 +936,21 @@ def create_jail(jail_name, distro='debian', release='bookworm'): [DHCPv4] UseDNS=true UseTimezone=true - """), file=open(os.path.join(network_dir_path, "mv-dhcp.network"), "w")) + """ + ), + file=open(os.path.join(network_dir_path, "mv-dhcp.network"), "w"), + ) # 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 - - preset_path = os.path.join(jail_rootfs_path, 'etc/systemd/system-preset') + + preset_path = os.path.join(jail_rootfs_path, "etc/systemd/system-preset") os.makedirs(preset_path, exist_ok=True) - print('enable systemd-networkd.service', file=open(os.path.join(preset_path, '00-jailmaker.preset'), "w")) + print( + "enable systemd-networkd.service", + file=open(os.path.join(preset_path, "00-jailmaker.preset"), "w"), + ) # 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 @@ -808,23 +965,20 @@ def create_jail(jail_name, distro='debian', release='bookworm'): # it won't be possible to start the same jail twice systemd_run_default_args = [ - '--property=KillMode=mixed', - '--property=Type=notify', - '--property=RestartForceExitStatus=133', - '--property=SuccessExitStatus=133', - '--property=Delegate=yes', - '--property=TasksMax=infinity', - '--collect', - '--setenv=SYSTEMD_NSPAWN_LOCK=0' + "--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' - ] + systemd_nspawn_default_args = ["--keep-unit", "--quiet", "--boot"] - config = cleandoc(f""" + config = cleandoc( + f""" startup={startup} docker_compatible={docker_compatible} gpu_passthrough_intel={gpu_passthrough_intel} @@ -833,9 +987,10 @@ def create_jail(jail_name, distro='debian', release='bookworm'): # 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)} - """) + """ + ) - print(config, file=open(jail_config_path, 'w')) + print(config, file=open(jail_config_path, "w")) os.chmod(jail_config_path, 0o600) @@ -845,13 +1000,19 @@ def create_jail(jail_name, distro='debian', release='bookworm'): raise error print() - if agree(f"Do you want to start jail {jail_name} right now?", 'y'): + if agree(f"Do you want to start jail {jail_name} right now?", "y"): start_jail(jail_name) def jail_is_running(jail_name): - return subprocess.run(["machinectl", "show", jail_name], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL).returncode == 0 + return ( + subprocess.run( + ["machinectl", "show", jail_name], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ).returncode + == 0 + ) def edit_jail(jail_name): @@ -881,8 +1042,10 @@ def remove_jail(jail_name): if check_jail_name_available(jail_name, False): eprint(f"A jail with name {jail_name} does not exist.") else: - check = input( - f'\nCAUTION: Type "{jail_name}" to confirm jail deletion!\n\n') or "" + check = ( + input(f'\nCAUTION: Type "{jail_name}" to confirm jail deletion!\n\n') + or "" + ) if check == jail_name: jail_path = get_jail_path(jail_name) if jail_is_running(jail_name): @@ -900,22 +1063,25 @@ def remove_jail(jail_name): 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: - widths[hdr] = max(widths[hdr], len( - str(obj.get(hdr))), len(str(hdr))) + widths[hdr] = max(widths[hdr], len(str(obj.get(hdr))), len(str(hdr))) # Print header - print(UNDERLINE + ' '.join(hdr.upper().ljust(widths[hdr]) - for hdr in header) + NORMAL) + print( + UNDERLINE + " ".join(hdr.upper().ljust(widths[hdr]) for hdr in header) + NORMAL + ) # Print rows for obj in list_of_objects: - print(' '.join(str(obj.get(hdr, empty_value_indicator)).ljust( - widths[hdr]) for hdr in header)) + print( + " ".join( + str(obj.get(hdr, empty_value_indicator)).ljust(widths[hdr]) + for hdr in header + ) + ) def run_command_and_parse_json(command): @@ -945,79 +1111,79 @@ def list_jails(): """ jails = {} - empty_value_indicator = '-' + empty_value_indicator = "-" jail_names = get_all_jail_names() if not jail_names: - print('No jails.') + print("No jails.") return for jail in jail_names: jails[jail] = {"name": jail, "running": False} # Get running jails from machinectl - running_machines = run_command_and_parse_json( - ['machinectl', 'list', '-o', 'json']) + running_machines = run_command_and_parse_json(["machinectl", "list", "-o", "json"]) # Augment the jails dict with output from machinectl for machine in running_machines: - machine_name = machine['machine'] + machine_name = machine["machine"] # We're only interested in the list of jails made with jailmaker - if machine['service'] == 'systemd-nspawn' and machine_name in jails: - - addresses = (machine.get('addresses') - or empty_value_indicator).split('\n') + if machine["service"] == "systemd-nspawn" and machine_name in jails: + addresses = (machine.get("addresses") or empty_value_indicator).split("\n") if len(addresses) > 1: - addresses = addresses[0] + '…' + addresses = addresses[0] + "…" else: addresses = addresses[0] jails[machine_name] = { "name": machine_name, "running": True, - "os": machine['os'], - "version": machine['version'], - "addresses": addresses + "os": machine["os"], + "version": machine["version"], + "addresses": addresses, } # TODO: add additional properties from the jails config file for jail_name in jails: - config = parse_config(get_jail_config_path(jail_name)) startup = False if config: - startup = bool(int(config.get('startup', '0'))) + startup = bool(int(config.get("startup", "0"))) # TODO: in case config is missing or parsing fails, # should an error message be thrown here? - jails[jail_name]['startup'] = startup + jails[jail_name]["startup"] = startup - print_table(["name", "running", "startup", "os", "version", "addresses"], - sorted(jails.values(), key=lambda x: x['name']), empty_value_indicator) + print_table( + ["name", "running", "startup", "os", "version", "addresses"], + sorted(jails.values(), key=lambda x: x["name"]), + empty_value_indicator, + ) def install_jailmaker(): # Check if command exists in path - if shutil.which('systemd-nspawn'): + if shutil.which("systemd-nspawn"): print("systemd-nspawn is already installed.") else: print("Installing jailmaker dependencies...") original_permissions = {} - print("Temporarily enable apt and dpkg (if not already enabled) to install systemd-nspawn.") + print( + "Temporarily enable apt and dpkg (if not already enabled) to install systemd-nspawn." + ) # Make /bin/apt* and /bin/dpkg* files executable - for file in (glob.glob('/bin/apt*') + (glob.glob('/bin/dpkg*'))): + for file in glob.glob("/bin/apt*") + (glob.glob("/bin/dpkg*")): original_permissions[file] = os.stat(file).st_mode stat_chmod(file, 0o755) - subprocess.run(['apt-get', 'update'], check=True) - subprocess.run(['apt-get', 'install', '-y', - 'systemd-container'], check=True) + subprocess.run(["apt-get", "update"], check=True) + subprocess.run(["apt-get", "install", "-y", "systemd-container"], check=True) # Restore original permissions print("Restore permissions of apt and dpkg.") @@ -1025,7 +1191,7 @@ def install_jailmaker(): for file, original_permission in original_permissions.items(): stat_chmod(file, original_permission) - target = f'/usr/local/sbin/{SYMLINK_NAME}' + target = f"/usr/local/sbin/{SYMLINK_NAME}" # Check if command exists in path if shutil.which(SYMLINK_NAME): @@ -1035,7 +1201,8 @@ def install_jailmaker(): os.symlink(SCRIPT_PATH, target) else: print( - f"File {target} already exists... Maybe it's a broken symlink from a previous install attempt?") + f"File {target} already exists... Maybe it's a broken symlink from a previous install attempt?" + ) print(f"Skipped creating new symlink {target} to {SCRIPT_PATH}.") print("Done installing jailmaker.") @@ -1050,64 +1217,74 @@ def startup_jails(): def main(): if os.stat(SCRIPT_PATH).st_uid != 0: fail( - f"This script should be owned by the root user... Fix it manually with: `chown root {SCRIPT_PATH}`.") + f"This script should be owned by the root user... Fix it manually with: `chown root {SCRIPT_PATH}`." + ) - parser = argparse.ArgumentParser( - description=DESCRIPTION, epilog=DISCLAIMER) + parser = argparse.ArgumentParser(description=DESCRIPTION, epilog=DISCLAIMER) - parser.add_argument('--version', action='version', version=VERSION) + parser.add_argument("--version", action="version", version=VERSION) - subparsers = parser.add_subparsers( - title='commands', dest='subcommand', metavar="") + subparsers = parser.add_subparsers(title="commands", dest="subcommand", metavar="") - subparsers.add_parser(name='install', epilog=DISCLAIMER, - help='install jailmaker dependencies and create symlink') + subparsers.add_parser( + name="install", + epilog=DISCLAIMER, + help="install jailmaker dependencies and create symlink", + ) - subparsers.add_parser(name='create', epilog=DISCLAIMER, - help='create a new jail').add_argument( - 'name', nargs='?', help='name of the jail') + subparsers.add_parser( + name="create", epilog=DISCLAIMER, help="create a new jail" + ).add_argument("name", nargs="?", help="name of the jail") - subparsers.add_parser(name='start', epilog=DISCLAIMER, - help='start a previously created jail').add_argument( - 'name', help='name of the jail') + subparsers.add_parser( + name="start", epilog=DISCLAIMER, help="start a previously created jail" + ).add_argument("name", help="name of the jail") - subparsers.add_parser(name='shell', epilog=DISCLAIMER, - help='open shell in running jail').add_argument( - 'name', help='name of the jail') + subparsers.add_parser( + name="shell", epilog=DISCLAIMER, help="open shell in running jail" + ).add_argument("name", help="name of the jail") - 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') + 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") - subparsers.add_parser(name='status', epilog=DISCLAIMER, - help='show jail status').add_argument( - 'name', help='name of the jail') + subparsers.add_parser( + name="status", epilog=DISCLAIMER, help="show jail status" + ).add_argument("name", help="name of the jail") - subparsers.add_parser(name='log', epilog=DISCLAIMER, - help='show jail log').add_argument( - 'name', help='name of the jail') + subparsers.add_parser( + name="log", epilog=DISCLAIMER, help="show jail log" + ).add_argument("name", help="name of the jail") - subparsers.add_parser(name='stop', epilog=DISCLAIMER, - help='stop a running jail').add_argument( - 'name', help='name of the jail') + subparsers.add_parser( + name="stop", epilog=DISCLAIMER, help="stop a running jail" + ).add_argument("name", help="name of the jail") - 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') + 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") - subparsers.add_parser(name='remove', epilog=DISCLAIMER, - help='remove a previously created jail').add_argument( - 'name', help='name of the jail to remove') + subparsers.add_parser( + name="remove", epilog=DISCLAIMER, help="remove a previously created jail" + ).add_argument("name", help="name of the jail to remove") - subparsers.add_parser(name='list', epilog=DISCLAIMER, - help='list jails') + subparsers.add_parser(name="list", epilog=DISCLAIMER, help="list jails") - subparsers.add_parser(name='images', epilog=DISCLAIMER, - help='list available images to create jails from') + subparsers.add_parser( + name="images", + epilog=DISCLAIMER, + help="list available images to create jails from", + ) - subparsers.add_parser(name='startup', epilog=DISCLAIMER, - help=f'install {SYMLINK_NAME} and startup selected jails') + subparsers.add_parser( + name="startup", + epilog=DISCLAIMER, + help=f"install {SYMLINK_NAME} and startup selected jails", + ) if os.getuid() != 0: parser.print_usage() @@ -1121,54 +1298,54 @@ def main(): args, additional_args = parser.parse_known_args() - if args.subcommand == 'install': + if args.subcommand == "install": install_jailmaker() - elif args.subcommand == 'create': + elif args.subcommand == "create": create_jail(args.name) - elif args.subcommand == 'start': + elif args.subcommand == "start": start_jail(args.name) - elif args.subcommand == 'shell': + elif args.subcommand == "shell": shell_jail(args.name) - elif args.subcommand == 'exec': + elif args.subcommand == "exec": exec_jail(args.name, args.cmd, additional_args) - elif args.subcommand == 'status': + elif args.subcommand == "status": status_jail(args.name) - elif args.subcommand == 'log': + elif args.subcommand == "log": log_jail(args.name) - elif args.subcommand == 'stop': + elif args.subcommand == "stop": stop_jail(args.name) - elif args.subcommand == 'edit': + elif args.subcommand == "edit": edit_jail(args.name) - elif args.subcommand == 'remove': + elif args.subcommand == "remove": remove_jail(args.name) - elif args.subcommand == 'list': + elif args.subcommand == "list": list_jails() - elif args.subcommand == 'images': + elif args.subcommand == "images": run_lxc_download_script() - elif args.subcommand == 'startup': + elif args.subcommand == "startup": startup_jails() else: - if agree("Create a new jail?", 'y'): + if agree("Create a new jail?", "y"): print() create_jail("") else: parser.print_usage() -if __name__ == '__main__': +if __name__ == "__main__": try: main() except KeyboardInterrupt: