From d7b30011b01bc4eb873eba16e7a26c29a4f85572 Mon Sep 17 00:00:00 2001 From: Jip-Hop <2871973+Jip-Hop@users.noreply.github.com> Date: Sat, 10 Feb 2024 13:04:18 +0100 Subject: [PATCH] Create jlmkr shell aliases --- README.md | 16 ++++----- jlmkr.py | 99 ++++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 94 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index c10ef13..cf7a078 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ Persistent Linux 'jails' on TrueNAS SCALE to install software (docker-compose, p TrueNAS SCALE can create persistent Linux 'jails' with systemd-nspawn. This script helps with the following: -- Installing the systemd-container package (which includes systemd-nspawn) - Setting up the jail so it won't be lost when you update SCALE - Choosing a distro (Debian 12 strongly recommended, but Ubuntu, Arch Linux or Rocky Linux seem good choices too) - Optional: configuring the jail so you can run Docker inside it @@ -23,7 +22,7 @@ Despite what the word 'jail' implies, jailmaker's intended use case is to create ## Installation -Create a new dataset called `jailmaker` with the default settings (from TrueNAS web interface). Then login as the root user and download `jlmkr.py`. +[Installation steps with screenshots](https://www.truenas.com/docs/scale/scaletutorials/apps/sandboxes/) are provided on the TrueNAS website. Start by creating a new dataset called `jailmaker` with the default settings (from TrueNAS web interface). Then login as the root user and download `jlmkr.py`. ```shell cd /mnt/mypool/jailmaker @@ -32,9 +31,9 @@ chmod +x jlmkr.py ./jlmkr.py install ``` -The `jlmkr.py` script (and the jails + config it creates) are now stored on the `jailmaker` dataset and will survive updates of TrueNAS SCALE. Additionally a symlink has been created so you can call `jlmkr` from anywhere. +The `jlmkr.py` script (and the jails + config it creates) are now stored on the `jailmaker` dataset and will survive updates of TrueNAS SCALE. Additionally a symlink has been created (if the boot pool is not readonly) so you can call `jlmkr` from anywhere. -After an update of TrueNAS SCALE the symlink will be lost and `systemd-nspawn` (the core package which makes `jailmaker` work) may be gone too. Not to worry, just run `./jlmkr.py install` again or use [the `./jlmkr.py startup` command](#startup-jails-on-boot). +After an update of TrueNAS SCALE the symlink will be lost (but the shell aliases will remain). To restore the symlink, just run `./jlmkr.py install` again or use [the `./jlmkr.py startup` command](#startup-jails-on-boot). ## Usage @@ -51,15 +50,12 @@ After answering a few questions you should have your first jail up and running! ### Startup Jails on Boot ```shell -# Best to call startup directly (not through the jlmkr symlink) +# Call startup using the absolute path to jlmkr.py +# The jlmkr shell alias doesn't work in Init/Shutdown Scripts /mnt/mypool/jailmaker/jlmkr.py startup - -# Can be called from the symlink too... -# But this may not be available after a TrueNAS SCALE update -jlmkr startup ``` -In order to start jails automatically after TrueNAS boots, run `/mnt/mypool/jailmaker/jlmkr.py startup` as Post Init Script with Type `Command` from the TrueNAS web interface. This will automatically fix the installation of `systemd-nspawn` and setup the `jlmkr` symlink, as well as start all the jails with `startup=1` in the config file. Running the `startup` command Post Init is recommended to keep `jailmaker` working after a TrueNAS SCALE update. +In order to start jails automatically after TrueNAS boots, run `/mnt/mypool/jailmaker/jlmkr.py startup` as Post Init Script with Type `Command` from the TrueNAS web interface. This creates the `jlmkr` symlink (if possible), as well as start all the jails with `startup=1` in the config file. ### Start Jail diff --git a/jlmkr.py b/jlmkr.py index 3007d86..1d53c3e 100755 --- a/jlmkr.py +++ b/jlmkr.py @@ -4,6 +4,7 @@ import argparse import configparser import contextlib import ctypes +import errno import glob import hashlib import json @@ -1073,7 +1074,9 @@ def create_jail(jail_name, distro="debian", release="bookworm"): """ ) - config += f"\n\nsystemd_nspawn_user_args={systemd_nspawn_user_args_multiline}\n\n" + config += ( + f"\n\nsystemd_nspawn_user_args={systemd_nspawn_user_args_multiline}\n\n" + ) config += cleandoc( """ @@ -1314,6 +1317,58 @@ def list_jails(): return 0 +def replace_or_add_string(file_path, regex, replacement_string): + """ + Replace all occurrences of a regular expression in a file with a given string. + Add the string to the end of the file if regex doesn't match. + + Args: + file_path (str): The path to the file. + regex (str): The regular expression to search for. + replacement_string (str): The string to replace the matches with. + """ + + with open(file_path, "a+") as f: + f.seek(0) + + updated = False + found = False + new_text = "" + replacement_line = f"{replacement_string}\n" + + for line in f: + if not re.match(regex, line): + new_text += line + continue + + found = True + new_text += replacement_line + + if replacement_line != line: + updated = True + + if not new_text.strip(): + # In case of an empty file just write the replacement_string + new_text = replacement_line + updated = True + elif not found: + # Add a newline to the end of the file in case it's not there + if not new_text.endswith("\n"): + new_text += "\n" + # Then add our replacement_string to the end of the file + new_text += replacement_line + updated = True + + # Only overwrite in case there are change to the file + if updated: + f.seek(0) + f.truncate() + f.write(new_text) + return True + + return False + + def install_jailmaker(): # Check if command exists in path if shutil.which("systemd-nspawn"): @@ -1341,19 +1396,41 @@ def install_jailmaker(): for file, original_permission in original_permissions.items(): stat_chmod(file, original_permission) - target = f"/usr/local/sbin/{SYMLINK_NAME}" + symlink = f"/usr/local/sbin/{SYMLINK_NAME}" - # Check if command exists in path - if shutil.which(SYMLINK_NAME): - print(f"The {SYMLINK_NAME} command is available.") - elif not os.path.lexists(target): - print(f"Creating symlink {target} to {SCRIPT_PATH}.") - os.symlink(SCRIPT_PATH, target) - else: + if os.path.lexists(symlink) and not os.path.islink(symlink): print( - f"File {target} already exists... Maybe it's a broken symlink from a previous install attempt?" + f"Unable to create symlink at {symlink}. File already exists but is not a symlink." ) - print(f"Skipped creating new symlink {target} to {SCRIPT_PATH}.") + # Check if the symlink is already pointing to the desired destination + elif os.path.realpath(symlink) != SCRIPT_PATH: + try: + Path(symlink).unlink(missing_ok=True) + os.symlink(SCRIPT_PATH, symlink) + print(f"Created symlink {symlink} to {SCRIPT_PATH}.") + except OSError as e: + if e.errno != errno.EROFS: + raise e + + print( + f"Cannot create symlink because {symlink} is on a readonly filesystem." + ) + + alias = f"alias jlmkr={shlex.quote(SCRIPT_PATH)} # managed by jailmaker" + alias_regex = re.compile(r"^\s*alias jlmkr=.*# managed by jailmaker\s*") + shell_env = os.getenv("SHELL") + + for shell_type in ["bash", "zsh"]: + file = "/root/.bashrc" if shell_type == "bash" else "/root/.zshrc" + + if replace_or_add_string(file, alias_regex, alias): + print(f"Created {shell_type} alias {SYMLINK_NAME}.") + if shell_env.endswith(shell_type): + print( + f"Please source {file} manually for the {SYMLINK_NAME} alias to become effective immediately." + ) + else: + print(f"The {shell_type} alias {SYMLINK_NAME} is already present.") print("Done installing jailmaker.")