Skip to content

Commit

Permalink
Added Full ZFS Dataset Support (#118)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
templehasfallen and Jip-Hop authored Apr 14, 2024
1 parent f046dd3 commit a7c4b9d
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 7 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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).

Expand Down
60 changes: 60 additions & 0 deletions docs/zfsmigration.md
Original file line number Diff line number Diff line change
@@ -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.
68 changes: 62 additions & 6 deletions jlmkr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit a7c4b9d

Please sign in to comment.