From a7c4b9dbad636f30046cd5d112e482250892f83f Mon Sep 17 00:00:00 2001 From: TempleHasFallen <84207477+templehasfallen@users.noreply.github.com> Date: Sun, 14 Apr 2024 17:07:47 +0300 Subject: [PATCH] Added Full ZFS Dataset Support (#118) Added Full ZFS Dataset Support: - The script will now create a ZFS dataset for each jail if the 'jailmaker' directory is a ZFS dataset - The script will create the 'jails' directory as a dataset if the 'jailmaker' directory is a ZFS dataset - The script will now remove the ZFS dataset (including snapshots) when deleting the jail - Dual mode: For legacy use without datasets, it will continue to work as previously Added a guide to migrate from using directories to using ZFS datasets. Closes #80. --------- Co-authored-by: Jip-Hop <2871973+Jip-Hop@users.noreply.github.com> --- README.md | 5 +++- docs/zfsmigration.md | 60 ++++++++++++++++++++++++++++++++++++++ jlmkr.py | 68 ++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 docs/zfsmigration.md diff --git a/README.md b/README.md index f0113a0..6245220 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ TrueNAS SCALE can create persistent Linux 'jails' with systemd-nspawn. This scri - 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) +- Will create a ZFS Dataset for each jail if the `jailmaker` directory is a dataset (easy snapshotting) - Optional: configuring the jail so you can run Docker inside it - Optional: GPU passthrough (including [nvidia GPU](README.md#nvidia-gpu) with the drivers bind mounted from the host) - Starting the jail with your config applied @@ -31,7 +32,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. A symlink has been created so you can call `jlmkr` from anywhere (unless the boot pool is readonly, which is the default since SCALE 24.04). Additionally shell aliases have been setup, so you can still call `jlmkr` in an interactive shell (even if the symlink couldn't be created). +The `jlmkr.py` script (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.py` script 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. + +A symlink has been created so you can call `jlmkr` from anywhere (unless the boot pool is readonly, which is the default since SCALE 24.04). Additionally shell aliases have been setup, so you can still call `jlmkr` in an interactive shell (even if the symlink couldn't be created). 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). diff --git a/docs/zfsmigration.md b/docs/zfsmigration.md new file mode 100644 index 0000000..1c02419 --- /dev/null +++ b/docs/zfsmigration.md @@ -0,0 +1,60 @@ +# ZFS Datasets Migration + +From version 1.1.4 ZFS Datasets support was added to jailmaker. +By default starting in v1.1.4, jailmaker will create a separate dataset for each jail if possible. This allows the user to configure snapshots, rollbacks, replications etc. + +Jailmaker operates in dual-mode: it supports using both directories and datasets. If the 'jailmaker' directory is a dataset, it will use datasets, if it is a directory, it will use directories. +___ +## Procedure to migrate from directories to ZFS Datasets + +### Stop all jails + +`jlmkr stop jail1` + +`jlmkr stop jail2` +etc.. + +### Move/rename the 'jailmaker' directory + +`mv jailmaker orig_jailmaker` + +### Create the ZFS datasets for jailmaker + +Create all the required datasets via GUI or CLI. + +You need to create the following datasets: + +`jailmaker` + +`jailmaker/jails` + +And one for each existing jail: + +`jailmaker/jails/jail1` + +`jailmaker/jails/jail2` +etc. + + +Via CLI: +``` +zfs create mypool/jailmaker +zfs create mypool/jailmaker/jails +zfs create mypool/jailmaker/jails/jail1 +zfs create mypool/jailmaker/jails/jail2 +``` + + +### Move the existing jail data into the newly created datasets + +Now move all the jail data: + +`rsync -av orig_jailmaker/ jailmaker/` + +Warning! It's important that both directories have the `/` at the end to make sure contents are copied correctly. Otherwise you may end up with `jailmaker/jailmaker` + +### Test everything works + +If everything works, you should be able to use the `jlmkr` command directly. Try doing a `jlmkr list` to check if the jails are correctly recognized + +You can also try creating a new jail and see that the dataset is created automatically. \ No newline at end of file diff --git a/jlmkr.py b/jlmkr.py index a2d4ec1..f2fc612 100755 --- a/jlmkr.py +++ b/jlmkr.py @@ -4,7 +4,7 @@ with full access to all files via bind mounts, \ thanks to systemd-nspawn!""" -__version__ = "1.1.3" +__version__ = "1.1.4" __disclaimer__ = """USE THIS SCRIPT AT YOUR OWN RISK! IT COMES WITHOUT WARRANTY AND IS NOT SUPPORTED BY IXSYSTEMS.""" @@ -752,7 +752,11 @@ def cleanup(jail_path): """ Cleanup jail. """ - if os.path.isdir(jail_path): + if get_zfs_dataset(jail_path): + eprint(f"Cleaning up: {jail_path}.") + remove_zfs_dataset(jail_path) + + elif os.path.isdir(jail_path): # Workaround for https://github.com/python/cpython/issues/73885 # Should be fixed in Python 3.13 https://stackoverflow.com/a/70549000 def _onerror(func, path, exc_info): @@ -891,6 +895,49 @@ def get_mount_point(path): return path +def get_zfs_dataset(path): + """ + Get ZFS dataset path. + """ + path = os.path.realpath(path) + with open("/proc/mounts", "r") as f: + for line in f: + fields = line.split() + if fields[1] == path and fields[2] == "zfs": + return fields[0] + + +def get_zfs_base_path(): + """ + Get ZFS dataset path for jailmaker directory. + """ + zfs_base_path = get_zfs_dataset(SCRIPT_DIR_PATH) + if not zfs_base_path: + fail("Failed to get dataset path for jailmaker directory.") + + return zfs_base_path + + +def create_zfs_dataset(relative_path): + """ + Create a ZFS Dataset. + Receives the dataset to be created relative to the jailmaker script (e.g. "jails" or "jails/newjail"). + """ + dataset_to_create = os.path.join(get_zfs_base_path(), relative_path) + eprint(f"Creating ZFS Dataset {dataset_to_create}") + subprocess.run(["zfs", "create", dataset_to_create], check=True) + + +def remove_zfs_dataset(relative_path): + """ + Remove a ZFS Dataset. + Receives the dataset to be created relative to the jailmaker script (e.g. "jails/oldjail"). + """ + dataset_to_remove = os.path.join((get_zfs_base_path()), relative_path) + eprint(f"Removing ZFS Dataset {dataset_to_remove}") + subprocess.run(["zfs", "destroy", "-r", dataset_to_remove], check=True) + + def check_jail_name_valid(jail_name, warn=True): """ Return True if jail name matches the required format. @@ -1161,7 +1208,7 @@ def create_jail(**kwargs): {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 dataset called "jailmaker", store {SCRIPT_NAME} there and try again.""" ) ) return 1 @@ -1246,9 +1293,18 @@ def create_jail(**kwargs): # Cleanup in except, but only once the jail_path is final # Otherwise we may cleanup the wrong directory try: - # Create the dir where to store the jails - os.makedirs(JAILS_DIR_PATH, exist_ok=True) - stat_chmod(JAILS_DIR_PATH, 0o700) + # 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): + # Creating "jails" dataset if "jailmaker" is a ZFS Dataset + create_zfs_dataset(JAILS_DIR_PATH) + else: + os.makedirs(JAILS_DIR_PATH, exist_ok=True) + stat_chmod(JAILS_DIR_PATH, 0o700) + + # Creating a dataset for the jail if the jails dir is a dataset + if get_zfs_dataset(JAILS_DIR_PATH): + create_zfs_dataset(jail_path) jail_config_path = get_jail_config_path(jail_name) jail_rootfs_path = get_jail_rootfs_path(jail_name)