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>
This commit is contained in:
TempleHasFallen 2024-04-14 17:07:47 +03:00 committed by GitHub
parent f046dd3d32
commit a7c4b9dbad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 126 additions and 7 deletions

View File

@ -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).

60
docs/zfsmigration.md Normal file
View File

@ -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.

View File

@ -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)