diff --git a/README.md b/README.md index a694511..f49668e 100644 --- a/README.md +++ b/README.md @@ -21,65 +21,72 @@ TrueNAS SCALE can create persistent Linux 'jails' with systemd-nspawn. This app - Optional: GPU passthrough (including nvidia GPU with the drivers bind mounted from the host) - Starting the jail with your config applied +## Requirements + +Beginning with 24.04 (Dragonfish), TrueNAS SCALE officially includes the systemd-nspawn containerization program in the base system. Technically there's nothing to install. You can run the `jlmkr` tool directly, or put it somewhere convenient in your search path. + +Your user account must have administrative access (i.e. the ability to use `sudo`), and the `jlmkr` tool must be owned by the root user. + ## Installation -Beginning with 24.04 (Dragonfish), TrueNAS SCALE officially includes the systemd-nspawn containerization program in the base system. Technically there's nothing to install. You only need the `jlmkr` app in the right place. [Instructions 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`. +TL/DR: [Instructions with screenshots](https://www.truenas.com/docs/scale/scaletutorials/apps/sandboxes/) are provided on the TrueNAS website. -TODO: update install instructions. For now one may clone or download the repo and run the below commands to create the `jlmkr` zipapp. +> A note on installing *any* command-line tool or script: TrueNAS SCALE is a sealed storage appliance. It does [not allow](https://www.truenas.com/docs/scale/scaletutorials/systemsettings/advanced/developermode/) installing systemwide packages, nor making any other changes to system directories. Not even to `/usr/local/bin`. *\[You probably know this; creating your own customizable user environment is one of the top reasons to create a jail!]* +> +> The platform [standard location](https://www.freedesktop.org/software/systemd/man/latest/file-hierarchy.html#Home%20Directory) for your own tools and scripts is inside your home directory at `~/.local/bin`. This directory is not included in your shell's search path by default, so by default your shell will not find things there. Also it may not yet exist, so first create it if necessary. +> +> mkdir -p ~/.local/bin +> +> Add this directory to your search path with the following command. Consider appending the same command to the end of your `~/.bashrc` and/or `~/.zshrc` files, so that the same change will load and apply to your *future* login sessions. +> +> export PATH=~/.local/bin:"$PATH" -```shell -rm -rf /tmp/jlmkr-build -mkdir -p /tmp/jlmkr-build -cd /tmp/jlmkr-build -curl -L https://github.com/Jip-Hop/jailmaker/archive/refs/heads/v3.0.0.tar.gz | tar xvz --strip-components=1 -python3 -m zipapp src/jlmkr -p "/usr/bin/env python3" -o jlmkr -cp jlmkr /mnt/mypool/jailmaker/ -``` +*Until stable release builds are available, you may need to build `jlmkr` from source code using the developer instructions below. As an alternative: you could download and extract `jlmkr` from the latest experimental [build artifacts](https://github.com/Jip-Hop/jailmaker/actions). But then soon…* -Alternatively one may download and extract `jlmkr` from the build artifacts of the [GitHub Actions](https://github.com/Jip-Hop/jailmaker/actions). +Download the latest `jlmkr` tool from the project [release page](https://github.com/Jip-Hop/jailmaker/releases) *\[coming soon\]* and extract its `jlmkr` file from the archive. The following command will copy it into `~/.local/bin` with the necessary root ownership and permissions. -The `jlmkr` app (and the jails + config it creates) are now stored on the `jailmaker` dataset and will survive updates of TrueNAS SCALE. If the automatically created `jails` directory is also a ZFS dataset (which is true for new users), then the `jlmkr` app will automatically create a new dataset for every jail created. This allows you to snapshot individual jails. For legacy users (where the `jails` directory is not a dataset) each jail will be stored in a plain directory. + sudo install ./jlmkr ~/.local/bin/ -### Alias +## First-time setup -TODO: explain how to run `jlmkr` without using the absolute path. This probably involves building and releasing the `zipapp` on GitHub, downloading it into a directory added to the `PATH`. But this also requires the `jailmaker` directory to be configurable (instead of using the directory the `jlmkr` app itself is in) by using the `JAILMAKER_DIR` env variable. +Create a single common ZFS dataset in which to store your jails. You can use the TrueNAS web interface, and accept its suggested defaults. We will refer to this as the **jailmaker directory** throughout documentation. -```bash -mkdir /root/bin -cd /root/bin -curl -o jlmkr --location --remote-name https://some_url -chmod +x jlmkr -cd ../ -echo 'export PATH="/root/bin:$PATH"' | tee -a .bashrc .zshrc -echo 'export JAILMAKER_DIR=/mnt/tank/path/to/desired/jailmaker/dir' | tee -a .bashrc .zshrc -``` +> A note on datasets and directories: The jailmaker directory is *not required* to be a ZFS dataset, but is recommended. Jails being created inside a jailmaker *dataset* will themselves be created as datasets. This gives them independent snapshot histories, and the opportunity for rollback. + +The `jlmkr` tool needs to know where to find its jailmaker directory. For now, pass that setting through an environment variable named `JAILMAKER_DIR`. For example: if your jailmaker directory is at `/mnt/pool/jailmaker` in the filesystem, you should enter the following command. + + export JAILMAKER_DIR=/mnt/pool/jailmaker + +Consider also appending this command to your `~/.bashrc` and/or `~/.zshrc` files, so that the same change will load and apply to your *future* login sessions. ## Usage +If you have not yet done so, set the `JAILMAKER_DIR` environment variable as described above. The following commands will rely on that setting, to know where to find the *jailmaker directory*. + ### Create Jail Creating a jail with the default settings is as simple as: ```shell -./jlmkr create --start myjail +jlmkr create --start myjail ``` You may also specify a path to a config template, for a quick and consistent jail creation process. ```shell -./jlmkr create --start --config /path/to/config/template myjail +jlmkr create --start --config /path/to/config/template myjail ``` -Or you can override the default config by using flags. See `./jlmkr create --help` for the available options. Anything passed after the jail name will be passed to `systemd-nspawn` when starting the jail. See the `systemd-nspawn` manual for available options, specifically [Mount Options](https://manpages.debian.org/bookworm/systemd-container/systemd-nspawn.1.en.html#Mount_Options) and [Networking Options](https://manpages.debian.org/bookworm/systemd-container/systemd-nspawn.1.en.html#Networking_Options) are frequently used. +Or you can override the default config by using flags. See `jlmkr create --help` for the available options. Anything passed after the jail name will be passed to `systemd-nspawn` when starting the jail. See the `systemd-nspawn` manual for available options, specifically [Mount Options](https://manpages.debian.org/bookworm/systemd-container/systemd-nspawn.1.en.html#Mount_Options) and [Networking Options](https://manpages.debian.org/bookworm/systemd-container/systemd-nspawn.1.en.html#Networking_Options) are frequently used. ```shell -./jlmkr create --start --distro=ubuntu --release=jammy myjail --bind-ro=/mnt +jlmkr create --start --distro=ubuntu --release=jammy myjail --bind-ro=/mnt ``` If you omit the jail name, the create process is interactive. You'll be presented with questions which guide you through the process. ```shell -./jlmkr create +jlmkr create ``` After answering some questions you should have created your first jail (and it should be running if you chose to start it after creating)! @@ -96,7 +103,7 @@ In order to start jails automatically after TrueNAS boots, run `/mnt/mypool/jail ### Start Jail ```shell -./jlmkr start myjail +jlmkr start myjail ``` ### List Jails @@ -104,7 +111,7 @@ In order to start jails automatically after TrueNAS boots, run `/mnt/mypool/jail See list of jails (including running, startup state, GPU passthrough, distro, and IP). ```shell -./jlmkr list +jlmkr list ``` ### Execute Command in Jail @@ -112,41 +119,41 @@ See list of jails (including running, startup state, GPU passthrough, distro, an You may want to execute a command inside a jail, for example manually from the TrueNAS shell, a shell script or a CRON job. The example below executes the `env` command inside the jail. ```shell -./jlmkr exec myjail env +jlmkr exec myjail env ``` This example executes bash inside the jail with a command as additional argument. ```shell -./jlmkr exec myjail bash -c 'echo test; echo $RANDOM;' +jlmkr exec myjail bash -c 'echo test; echo $RANDOM;' ``` ### Edit Jail Config ```shell -./jlmkr edit myjail +jlmkr edit myjail ``` -Once you've created a jail, it will exist in a directory inside the `jails` dir next to `jlmkr`. For example `/mnt/mypool/jailmaker/jails/myjail` if you've named your jail `myjail`. You may edit the jail configuration file using the `./jlmkr edit myjail` command. This opens the config file in your favorite editor, as determined by following [Debian's guidelines](https://www.debian.org/doc/debian-policy/ch-customized-programs.html#editors-and-pagers) on the matter. You'll have to stop the jail and start it again with `jlmkr` for these changes to take effect. +Once you've created a jail, it will exist in a directory inside the `jails` dir next to `jlmkr`. For example `/mnt/mypool/jailmaker/jails/myjail` if you've named your jail `myjail`. You may edit the jail configuration file using the `jlmkr edit myjail` command. This opens the config file in your favorite editor, as determined by following [Debian's guidelines](https://www.debian.org/doc/debian-policy/ch-customized-programs.html#editors-and-pagers) on the matter. You'll have to stop the jail and start it again with `jlmkr` for these changes to take effect. ### Remove Jail Delete a jail and remove it's files (requires confirmation). ```shell -./jlmkr remove myjail +jlmkr remove myjail ``` ### Stop Jail ```shell -./jlmkr stop myjail +jlmkr stop myjail ``` ### Restart Jail ```shell -./jlmkr restart myjail +jlmkr restart myjail ``` ### Jail Shell @@ -154,13 +161,13 @@ Delete a jail and remove it's files (requires confirmation). Switch into the jail's shell. ```shell -./jlmkr shell myjail +jlmkr shell myjail ``` ### Jail Status ```shell -./jlmkr status myjail +jlmkr status myjail ``` ### Jail Logs @@ -168,7 +175,7 @@ Switch into the jail's shell. View a jail's logs. ```shell -./jlmkr log myjail +jlmkr log myjail ``` ### Additional Commands @@ -221,6 +228,23 @@ TODO: write comparison between systemd-nspawn (without `jailmaker`), LXC, VMs, D The rootfs image `jlmkr` downloads comes from the [Linux Containers Image server](https://images.linuxcontainers.org). These images are made for LXC. We can use them with systemd-nspawn too, although not all of them work properly. For example, the `alpine` image doesn't work well. If you stick with common systemd based distros (Debian, Ubuntu, Arch Linux...) you should be fine. +## Development + +This is really all it takes at the moment to get started. + +```shell +git clone -b v3.0.0 https://github.com/Jip-Hop/jailmaker jailmaker-src +python3 -m zipapp -o jlmkr -p /usr/bin/python3 jailmaker-src/src/jlmkr +``` + +You can take the resulting `jlmkr` file and install it as described more thoroughly under Installation, above. + +```shell +sudo install ./jlmkr ~/.local/bin/ +``` + +We hope you'll join us on [the project](https://github.com/Jip-Hop/jailmaker) and look forward to working with you on any future pull requests. + ## Filing Issues and Community Support When in need of help or when you think you've found a bug in `jailmaker`, [please start with reading this](https://github.com/Jip-Hop/jailmaker/discussions/135). diff --git a/src/jlmkr/actions/create.py b/src/jlmkr/actions/create.py index 635bbda..fc72981 100644 --- a/src/jlmkr/actions/create.py +++ b/src/jlmkr/actions/create.py @@ -10,7 +10,7 @@ from textwrap import dedent from data import DISCLAIMER -from paths import COMMAND_NAME, JAILS_DIR_PATH, SCRIPT_DIR_PATH, SCRIPT_NAME +from paths import COMMAND_NAME, JAILS_DIR_PATH, JAILMAKER_DIR_PATH, SCRIPT_NAME from utils.chroot import Chroot from utils.config_parser import DEFAULT_CONFIG, KeyValueParser from utils.console import BOLD, NORMAL, YELLOW, eprint @@ -33,19 +33,19 @@ def create_jail(**kwargs): print(DISCLAIMER) - if os.path.basename(SCRIPT_DIR_PATH) != "jailmaker": + if os.path.basename(JAILMAKER_DIR_PATH) != "jailmaker": eprint( dedent( f""" {COMMAND_NAME} needs to create files. Currently it can not decide if it is safe to create files in: - {SCRIPT_DIR_PATH} + {JAILMAKER_DIR_PATH} Please create a dedicated dataset called "jailmaker", store {SCRIPT_NAME} there and try again.""" ) ) return 1 - if not PurePath(get_mount_point(SCRIPT_DIR_PATH)).is_relative_to("/mnt"): + if not PurePath(get_mount_point(JAILMAKER_DIR_PATH)).is_relative_to("/mnt"): print( dedent( f""" @@ -54,7 +54,7 @@ def create_jail(**kwargs): {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. Jails will be stored under: - {SCRIPT_DIR_PATH} + {JAILMAKER_DIR_PATH} """ ) ) @@ -118,7 +118,7 @@ def create_jail(**kwargs): try: # Create the dir or dataset where to store the jails if not os.path.exists(JAILS_DIR_PATH): - if get_zfs_dataset(SCRIPT_DIR_PATH): + if get_zfs_dataset(JAILMAKER_DIR_PATH): # Creating "jails" dataset if "jailmaker" is a ZFS Dataset create_zfs_dataset(JAILS_DIR_PATH) else: diff --git a/src/jlmkr/paths.py b/src/jlmkr/paths.py index fbe98ad..34596a9 100755 --- a/src/jlmkr/paths.py +++ b/src/jlmkr/paths.py @@ -2,20 +2,63 @@ # # SPDX-License-Identifier: LGPL-3.0-only -import os.path +import os -# When running as a zipapp, the script file is a parent -ZIPAPP_PATH = os.path.realpath(__file__) -while not os.path.exists(ZIPAPP_PATH): - ZIPAPP_PATH = os.path.dirname(ZIPAPP_PATH) +from configparser import ConfigParser +from pathlib import Path +from utils.console import fail -SCRIPT_PATH = os.path.realpath(ZIPAPP_PATH) -SCRIPT_NAME = os.path.basename(SCRIPT_PATH) -SCRIPT_DIR_PATH = os.path.dirname(SCRIPT_PATH) -COMMAND_NAME = os.path.basename(ZIPAPP_PATH) -JAILS_DIR_PATH = os.path.join(SCRIPT_DIR_PATH, "jails") -JAIL_CONFIG_NAME = "config" -JAIL_ROOTFS_NAME = "rootfs" +def _get_selected_jailmaker_directory() -> Path: + ''' + Determine the user's affirmative choice of parent jailmaker directory + ''' + # first choice: global --dir/-D argument + #TODO + + # next: JAILMAKER_DIR environment variable + envname = 'JAILMAKER_DIR' + if envname in os.environ: + return Path(os.environ[envname]) + + # next: ~/.local/share/jailmaker.conf + secname = 'DEFAULT' + cfgname = 'jailmaker_dir' + username = '' + if os.getuid() == 0 and 'SUDO_USER' in os.environ: + username = os.environ['SUDO_USER'] + cfgpath = Path(f'~{username}/.local/share/jailmaker.conf').expanduser() + cfg = ConfigParser() + cfg.read(cfgpath) + if cfgname in cfg[secname]: + return Path(cfg[secname][cfgname]) + + # reluctantly: current directory + # ... if it's named jailmaker + # ... seems to be structured correctly + #TODO: ... iff + + fail("Please specify a jailmaker directory path (JAILMAKER_DIR)") + + +def get_tool_path_on_disk() -> Path: + ''' + Determine the script's location on disk + ''' + # When running as a zipapp, the script file is an ancestor + path = Path(__file__).resolve(strict=False) + while path and not path.is_file(): + path = path.parent + return path + +SCRIPT_PATH = get_tool_path_on_disk() +SCRIPT_NAME = SCRIPT_PATH.name +COMMAND_NAME = SCRIPT_NAME SHORTNAME = "jlmkr" + +JAILMAKER_DIR_PATH = _get_selected_jailmaker_directory() + +JAILS_DIR_PATH = JAILMAKER_DIR_PATH.joinpath("jails") +JAIL_CONFIG_NAME = "config" +JAIL_ROOTFS_NAME = "rootfs" diff --git a/src/jlmkr/utils/dataset.py b/src/jlmkr/utils/dataset.py index 5afee9c..8a50bef 100644 --- a/src/jlmkr/utils/dataset.py +++ b/src/jlmkr/utils/dataset.py @@ -6,13 +6,13 @@ import subprocess from pathlib import PurePath -from paths import SCRIPT_DIR_PATH +from paths import JAILMAKER_DIR_PATH from utils.console import eprint, fail def _get_relative_path_in_jailmaker_dir(absolute_path): - return PurePath(absolute_path).relative_to(SCRIPT_DIR_PATH) + return PurePath(absolute_path).relative_to(JAILMAKER_DIR_PATH) def get_zfs_dataset(path): @@ -37,7 +37,7 @@ def get_zfs_base_path(): """ Get ZFS dataset path for jailmaker directory. """ - zfs_base_path = get_zfs_dataset(SCRIPT_DIR_PATH) + zfs_base_path = get_zfs_dataset(JAILMAKER_DIR_PATH) if not zfs_base_path: fail("Failed to get dataset path for jailmaker directory.")