Extract create action

This commit is contained in:
jonct 2024-07-15 03:39:06 -04:00
parent fbf6e4d9a5
commit 1c0a7a46d4
No known key found for this signature in database
6 changed files with 456 additions and 436 deletions

303
src/jlmkr/actions/create.py Normal file
View File

@ -0,0 +1,303 @@
# SPDX-FileCopyrightText: © 2024 Jip-Hop and the Jailmakers <https://github.com/Jip-Hop/jailmaker>
#
# SPDX-License-Identifier: LGPL-3.0-only
import contextlib
import os
import re
from inspect import cleandoc
from pathlib import Path, PurePath
from textwrap import dedent
from donor.jlmkr import DISCLAIMER
from utils.chroot import Chroot
from utils.config_parser import KeyValueParser, DEFAULT_CONFIG
from utils.console import YELLOW, BOLD, NORMAL, eprint
from utils.download import run_lxc_download_script
from utils.files import stat_chmod, get_mount_point
from utils.jail_dataset import check_jail_name_valid, check_jail_name_available
from utils.jail_dataset import get_jail_config_path, get_jail_rootfs_path
from utils.jail_dataset import get_jail_path, get_zfs_dataset, create_zfs_dataset, cleanup
from utils.paths import COMMAND_NAME, JAILS_DIR_PATH, SCRIPT_NAME, SCRIPT_DIR_PATH
def create_jail(**kwargs):
print(DISCLAIMER)
if os.path.basename(SCRIPT_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}
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"):
print(
dedent(
f"""
{YELLOW}{BOLD}WARNING: BEWARE OF DATA LOSS{NORMAL}
{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}
"""
)
)
jail_name = kwargs.pop("jail_name")
start_now = False
if not check_jail_name_valid(jail_name):
return 1
if not check_jail_name_available(jail_name):
return 1
start_now = kwargs.pop("start", start_now)
jail_config_path = kwargs.pop("config")
config = KeyValueParser()
if jail_config_path:
# TODO: fallback to default values for e.g. distro and release if they are not in the config file
if jail_config_path == "-":
print(f"Creating jail {jail_name} from config template passed via stdin.")
config.read_string(sys.stdin.read())
else:
print(f"Creating jail {jail_name} from config template {jail_config_path}.")
if jail_config_path not in config.read(jail_config_path):
eprint(f"Failed to read config template {jail_config_path}.")
return 1
else:
print(f"Creating jail {jail_name} with default config.")
config.read_string(DEFAULT_CONFIG)
for option in [
"distro",
"gpu_passthrough_intel",
"gpu_passthrough_nvidia",
"release",
"seccomp",
"startup",
"systemd_nspawn_user_args",
]:
value = kwargs.pop(option)
if (
value is not None
# String, non-empty list of args or int
and (isinstance(value, int) or len(value))
and value is not config.my_get(option, None)
):
# TODO: this will wipe all systemd_nspawn_user_args from the template...
# Should there be an option to append them instead?
print(f"Overriding {option} config value with {value}.")
config.my_set(option, value)
jail_path = get_jail_path(jail_name)
distro = config.my_get("distro")
release = config.my_get("release")
# Cleanup in except, but only once the jail_path is final
# Otherwise we may cleanup the wrong directory
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):
# 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)
# Create directory for rootfs
os.makedirs(jail_rootfs_path, exist_ok=True)
# LXC download script needs to write to this file during install
# but we don't need it so we will remove it later
open(jail_config_path, "a").close()
if (
returncode := run_lxc_download_script(
jail_name, jail_path, jail_rootfs_path, distro, release
)
!= 0
):
cleanup(jail_path)
return returncode
# Assuming the name of your jail is "myjail"
# and "machinectl shell myjail" doesn't work
# Try:
#
# Stop the jail with:
# machinectl stop myjail
# And start a shell inside the jail without the --boot option:
# systemd-nspawn -q -D jails/myjail/rootfs /bin/sh
# Then set a root password with:
# In case of amazonlinux you may need to run:
# yum update -y && yum install -y passwd
# passwd
# exit
# Then you may login from the host via:
# machinectl login myjail
#
# You could also enable SSH inside the jail to login
#
# Or if that doesn't work (e.g. for alpine) get a shell via:
# nsenter -t $(machinectl show myjail -p Leader --value) -a /bin/sh -l
# But alpine jails made with jailmaker have other issues
# They don't shutdown cleanly via systemctl and machinectl...
with Chroot(jail_rootfs_path):
# Use chroot to correctly resolve absolute /sbin/init symlink
init_system_name = os.path.basename(os.path.realpath("/sbin/init"))
if (
init_system_name != "systemd"
and parse_os_release(jail_rootfs_path).get("ID") != "nixos"
):
print(
dedent(
f"""
{YELLOW}{BOLD}WARNING: DISTRO NOT SUPPORTED{NORMAL}
Chosen distro appears not to use systemd...
You probably will not get a shell with:
machinectl shell {jail_name}
You may get a shell with this command:
nsenter -t $(machinectl show {jail_name} -p Leader --value) -a /bin/sh -l
Read about the downsides of nsenter:
https://github.com/systemd/systemd/issues/12785#issuecomment-503019081
{BOLD}Using this distro with {COMMAND_NAME} is NOT recommended.{NORMAL}
"""
)
)
print("Autostart has been disabled.")
print("You need to start this jail manually.")
config.my_set("startup", 0)
start_now = False
# Remove config which systemd handles for us
with contextlib.suppress(FileNotFoundError):
os.remove(os.path.join(jail_rootfs_path, "etc/machine-id"))
with contextlib.suppress(FileNotFoundError):
os.remove(os.path.join(jail_rootfs_path, "etc/resolv.conf"))
# https://github.com/systemd/systemd/issues/852
print(
"\n".join([f"pts/{i}" for i in range(0, 11)]),
file=open(os.path.join(jail_rootfs_path, "etc/securetty"), "w"),
)
network_dir_path = os.path.join(jail_rootfs_path, "etc/systemd/network")
# Modify default network settings, if network_dir_path exists
if os.path.isdir(network_dir_path):
default_host0_network_file = os.path.join(
jail_rootfs_path, "lib/systemd/network/80-container-host0.network"
)
# Check if default host0 network file exists
if os.path.isfile(default_host0_network_file):
override_network_file = os.path.join(
network_dir_path, "80-container-host0.network"
)
# Override the default 80-container-host0.network file (by using the same name)
# This config applies when using the --network-bridge option of systemd-nspawn
# Disable LinkLocalAddressing on IPv4, or else the container won't get IP address via DHCP
# But keep it enabled on IPv6, as SLAAC and DHCPv6 both require a local-link address to function
print(
Path(default_host0_network_file)
.read_text()
.replace("LinkLocalAddressing=yes", "LinkLocalAddressing=ipv6"),
file=open(override_network_file, "w"),
)
# Setup DHCP for macvlan network interfaces
# This config applies when using the --network-macvlan option of systemd-nspawn
# https://www.debian.org/doc/manuals/debian-reference/ch05.en.html#_the_modern_network_configuration_without_gui
print(
cleandoc(
"""
[Match]
Virtualization=container
Name=mv-*
[Network]
DHCP=yes
LinkLocalAddressing=ipv6
[DHCPv4]
UseDNS=true
UseTimezone=true
"""
),
file=open(os.path.join(network_dir_path, "mv-dhcp.network"), "w"),
)
# Setup DHCP for veth-extra network interfaces
# This config applies when using the --network-veth-extra option of systemd-nspawn
# https://www.debian.org/doc/manuals/debian-reference/ch05.en.html#_the_modern_network_configuration_without_gui
print(
cleandoc(
"""
[Match]
Virtualization=container
Name=vee-*
[Network]
DHCP=yes
LinkLocalAddressing=ipv6
[DHCPv4]
UseDNS=true
UseTimezone=true
"""
),
file=open(os.path.join(network_dir_path, "vee-dhcp.network"), "w"),
)
# Override preset which caused systemd-networkd to be disabled (e.g. fedora 39)
# https://www.freedesktop.org/software/systemd/man/latest/systemd.preset.html
# https://github.com/lxc/lxc-ci/blob/f632823ecd9b258ed42df40449ec54ed7ef8e77d/images/fedora.yaml#L312C5-L312C38
preset_path = os.path.join(jail_rootfs_path, "etc/systemd/system-preset")
os.makedirs(preset_path, exist_ok=True)
print(
"enable systemd-networkd.service",
file=open(os.path.join(preset_path, "00-jailmaker.preset"), "w"),
)
with open(jail_config_path, "w") as fp:
config.write(fp)
os.chmod(jail_config_path, 0o600)
# Cleanup on any exception and rethrow
except BaseException as error:
cleanup(jail_path)
raise error
if start_now:
return start_jail(jail_name)
return 0

View File

@ -8,8 +8,7 @@ import subprocess
from collections import defaultdict
from utils.console import NORMAL, UNDERLINE
from utils.config_parser import parse_config_file
from utils.jail_dataset import get_jail_config_path, get_jail_rootfs_path, parse_os_release
from utils.parent_dataset import get_all_jail_names
from utils.jail_dataset import get_all_jail_names, get_jail_config_path, get_jail_rootfs_path, parse_os_release
def list_jails():

View File

@ -26,7 +26,6 @@ import sys
import tempfile
import time
import urllib.request
from inspect import cleandoc
from pathlib import Path, PurePath
from textwrap import dedent
@ -70,6 +69,7 @@ class CustomSubParser(argparse.ArgumentParser):
from utils.chroot import Chroot
from utils.console import eprint, fail
from utils.jail_dataset import check_jail_name_valid, check_jail_name_available
from utils.jail_dataset import get_jail_path, get_jail_config_path, get_jail_rootfs_path
from actions.exec import exec_jail
@ -80,139 +80,10 @@ from actions.start import start_jail
from actions.restart import restart_jail
from actions.images import run_lxc_download_script
def cleanup(jail_path):
"""
Cleanup jail.
"""
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):
exc_type, exc_value, exc_traceback = exc_info
if issubclass(exc_type, PermissionError):
# Update the file permissions with the immutable and append-only bit cleared
subprocess.run(["chattr", "-i", "-a", path])
# Reattempt the removal
func(path)
elif not issubclass(exc_type, FileNotFoundError):
raise exc_value
eprint(f"Cleaning up: {jail_path}.")
shutil.rmtree(jail_path, onerror=_onerror)
from utils.jail_dataset import cleanup, check_jail_name_valid, check_jail_name_available
from utils.download import run_lxc_download_script
from utils.files import stat_chmod
def get_mount_point(path):
"""
Return the mount point on which the given path resides.
"""
path = os.path.abspath(path)
while not os.path.ismount(path):
path = os.path.dirname(path)
return path
def get_relative_path_in_jailmaker_dir(absolute_path):
return PurePath(absolute_path).relative_to(SCRIPT_DIR_PATH)
def get_zfs_dataset(path):
"""
Get ZFS dataset path.
"""
def clean_field(field):
# Put back spaces which were encoded
# https://github.com/openzfs/zfs/issues/11182
return field.replace("\\040", " ")
path = os.path.realpath(path)
with open("/proc/mounts", "r") as f:
for line in f:
fields = line.split()
if "zfs" == fields[2] and path == clean_field(fields[1]):
return clean_field(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(absolute_path):
"""
Create a ZFS Dataset inside the jailmaker directory at the provided absolute path.
E.g. "/mnt/mypool/jailmaker/jails" or "/mnt/mypool/jailmaker/jails/newjail").
"""
relative_path = get_relative_path_in_jailmaker_dir(absolute_path)
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(absolute_path):
"""
Remove a ZFS Dataset inside the jailmaker directory at the provided absolute path.
E.g. "/mnt/mypool/jailmaker/jails/oldjail".
"""
relative_path = get_relative_path_in_jailmaker_dir(absolute_path)
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.
"""
if (
re.match(r"^[.a-zA-Z0-9-]{1,64}$", jail_name)
and not jail_name.startswith(".")
and ".." not in jail_name
):
return True
if warn:
eprint(
dedent(
f"""
{YELLOW}{BOLD}WARNING: INVALID NAME{NORMAL}
A valid name consists of:
- allowed characters (alphanumeric, dash, dot)
- no leading or trailing dots
- no sequences of multiple dots
- max 64 characters"""
)
)
return False
def check_jail_name_available(jail_name, warn=True):
"""
Return True if jail name is not yet taken.
"""
if not os.path.exists(get_jail_path(jail_name)):
return True
if warn:
print()
eprint("A jail with this name already exists.")
return False
from utils.jail_dataset import get_zfs_dataset, create_zfs_dataset, remove_zfs_dataset
def get_text_editor():
@ -229,288 +100,7 @@ def get_text_editor():
)
def create_jail(**kwargs):
print(DISCLAIMER)
if os.path.basename(SCRIPT_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}
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"):
print(
dedent(
f"""
{YELLOW}{BOLD}WARNING: BEWARE OF DATA LOSS{NORMAL}
{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}
"""
)
)
jail_name = kwargs.pop("jail_name")
start_now = False
if not check_jail_name_valid(jail_name):
return 1
if not check_jail_name_available(jail_name):
return 1
start_now = kwargs.pop("start", start_now)
jail_config_path = kwargs.pop("config")
config = KeyValueParser()
if jail_config_path:
# TODO: fallback to default values for e.g. distro and release if they are not in the config file
if jail_config_path == "-":
print(f"Creating jail {jail_name} from config template passed via stdin.")
config.read_string(sys.stdin.read())
else:
print(f"Creating jail {jail_name} from config template {jail_config_path}.")
if jail_config_path not in config.read(jail_config_path):
eprint(f"Failed to read config template {jail_config_path}.")
return 1
else:
print(f"Creating jail {jail_name} with default config.")
config.read_string(DEFAULT_CONFIG)
for option in [
"distro",
"gpu_passthrough_intel",
"gpu_passthrough_nvidia",
"release",
"seccomp",
"startup",
"systemd_nspawn_user_args",
]:
value = kwargs.pop(option)
if (
value is not None
# String, non-empty list of args or int
and (isinstance(value, int) or len(value))
and value is not config.my_get(option, None)
):
# TODO: this will wipe all systemd_nspawn_user_args from the template...
# Should there be an option to append them instead?
print(f"Overriding {option} config value with {value}.")
config.my_set(option, value)
jail_path = get_jail_path(jail_name)
distro = config.my_get("distro")
release = config.my_get("release")
# Cleanup in except, but only once the jail_path is final
# Otherwise we may cleanup the wrong directory
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):
# 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)
# Create directory for rootfs
os.makedirs(jail_rootfs_path, exist_ok=True)
# LXC download script needs to write to this file during install
# but we don't need it so we will remove it later
open(jail_config_path, "a").close()
if (
returncode := run_lxc_download_script(
jail_name, jail_path, jail_rootfs_path, distro, release
)
!= 0
):
cleanup(jail_path)
return returncode
# Assuming the name of your jail is "myjail"
# and "machinectl shell myjail" doesn't work
# Try:
#
# Stop the jail with:
# machinectl stop myjail
# And start a shell inside the jail without the --boot option:
# systemd-nspawn -q -D jails/myjail/rootfs /bin/sh
# Then set a root password with:
# In case of amazonlinux you may need to run:
# yum update -y && yum install -y passwd
# passwd
# exit
# Then you may login from the host via:
# machinectl login myjail
#
# You could also enable SSH inside the jail to login
#
# Or if that doesn't work (e.g. for alpine) get a shell via:
# nsenter -t $(machinectl show myjail -p Leader --value) -a /bin/sh -l
# But alpine jails made with jailmaker have other issues
# They don't shutdown cleanly via systemctl and machinectl...
with Chroot(jail_rootfs_path):
# Use chroot to correctly resolve absolute /sbin/init symlink
init_system_name = os.path.basename(os.path.realpath("/sbin/init"))
if (
init_system_name != "systemd"
and parse_os_release(jail_rootfs_path).get("ID") != "nixos"
):
print(
dedent(
f"""
{YELLOW}{BOLD}WARNING: DISTRO NOT SUPPORTED{NORMAL}
Chosen distro appears not to use systemd...
You probably will not get a shell with:
machinectl shell {jail_name}
You may get a shell with this command:
nsenter -t $(machinectl show {jail_name} -p Leader --value) -a /bin/sh -l
Read about the downsides of nsenter:
https://github.com/systemd/systemd/issues/12785#issuecomment-503019081
{BOLD}Using this distro with {COMMAND_NAME} is NOT recommended.{NORMAL}
"""
)
)
print("Autostart has been disabled.")
print("You need to start this jail manually.")
config.my_set("startup", 0)
start_now = False
# Remove config which systemd handles for us
with contextlib.suppress(FileNotFoundError):
os.remove(os.path.join(jail_rootfs_path, "etc/machine-id"))
with contextlib.suppress(FileNotFoundError):
os.remove(os.path.join(jail_rootfs_path, "etc/resolv.conf"))
# https://github.com/systemd/systemd/issues/852
print(
"\n".join([f"pts/{i}" for i in range(0, 11)]),
file=open(os.path.join(jail_rootfs_path, "etc/securetty"), "w"),
)
network_dir_path = os.path.join(jail_rootfs_path, "etc/systemd/network")
# Modify default network settings, if network_dir_path exists
if os.path.isdir(network_dir_path):
default_host0_network_file = os.path.join(
jail_rootfs_path, "lib/systemd/network/80-container-host0.network"
)
# Check if default host0 network file exists
if os.path.isfile(default_host0_network_file):
override_network_file = os.path.join(
network_dir_path, "80-container-host0.network"
)
# Override the default 80-container-host0.network file (by using the same name)
# This config applies when using the --network-bridge option of systemd-nspawn
# Disable LinkLocalAddressing on IPv4, or else the container won't get IP address via DHCP
# But keep it enabled on IPv6, as SLAAC and DHCPv6 both require a local-link address to function
print(
Path(default_host0_network_file)
.read_text()
.replace("LinkLocalAddressing=yes", "LinkLocalAddressing=ipv6"),
file=open(override_network_file, "w"),
)
# Setup DHCP for macvlan network interfaces
# This config applies when using the --network-macvlan option of systemd-nspawn
# https://www.debian.org/doc/manuals/debian-reference/ch05.en.html#_the_modern_network_configuration_without_gui
print(
cleandoc(
"""
[Match]
Virtualization=container
Name=mv-*
[Network]
DHCP=yes
LinkLocalAddressing=ipv6
[DHCPv4]
UseDNS=true
UseTimezone=true
"""
),
file=open(os.path.join(network_dir_path, "mv-dhcp.network"), "w"),
)
# Setup DHCP for veth-extra network interfaces
# This config applies when using the --network-veth-extra option of systemd-nspawn
# https://www.debian.org/doc/manuals/debian-reference/ch05.en.html#_the_modern_network_configuration_without_gui
print(
cleandoc(
"""
[Match]
Virtualization=container
Name=vee-*
[Network]
DHCP=yes
LinkLocalAddressing=ipv6
[DHCPv4]
UseDNS=true
UseTimezone=true
"""
),
file=open(os.path.join(network_dir_path, "vee-dhcp.network"), "w"),
)
# Override preset which caused systemd-networkd to be disabled (e.g. fedora 39)
# https://www.freedesktop.org/software/systemd/man/latest/systemd.preset.html
# https://github.com/lxc/lxc-ci/blob/f632823ecd9b258ed42df40449ec54ed7ef8e77d/images/fedora.yaml#L312C5-L312C38
preset_path = os.path.join(jail_rootfs_path, "etc/systemd/system-preset")
os.makedirs(preset_path, exist_ok=True)
print(
"enable systemd-networkd.service",
file=open(os.path.join(preset_path, "00-jailmaker.preset"), "w"),
)
with open(jail_config_path, "w") as fp:
config.write(fp)
os.chmod(jail_config_path, 0o600)
# Cleanup on any exception and rethrow
except BaseException as error:
cleanup(jail_path)
raise error
if start_now:
return start_jail(jail_name)
return 0
from actions.create import create_jail
from utils.jail_dataset import jail_is_running
@ -574,8 +164,7 @@ def remove_jail(jail_name):
return 1
from utils.parent_dataset import get_all_jail_names
from utils.jail_dataset import parse_os_release
from utils.jail_dataset import get_all_jail_names, parse_os_release
from actions.list import list_jails

View File

@ -12,3 +12,13 @@ def stat_chmod(file_path, mode):
"""
if mode != stat.S_IMODE(os.stat(file_path).st_mode):
os.chmod(file_path, mode)
def get_mount_point(path):
"""
Return the mount point on which the given path resides.
"""
path = os.path.abspath(path)
while not os.path.ismount(path):
path = os.path.dirname(path)
return path

View File

@ -4,10 +4,15 @@
import os.path
import platform
import re
import shutil
import subprocess
from pathlib import PurePath
from textwrap import dedent
from utils.chroot import Chroot
from utils.paths import JAILS_DIR_PATH, JAIL_CONFIG_NAME, JAIL_ROOTFS_NAME
from utils.console import eprint, YELLOW, BOLD, NORMAL
from utils.paths import JAILS_DIR_PATH, JAIL_CONFIG_NAME, JAIL_ROOTFS_NAME, SCRIPT_DIR_PATH
def get_jail_path(jail_name):
@ -22,6 +27,50 @@ def get_jail_rootfs_path(jail_name):
return os.path.join(get_jail_path(jail_name), JAIL_ROOTFS_NAME)
def get_relative_path_in_jailmaker_dir(absolute_path):
return PurePath(absolute_path).relative_to(SCRIPT_DIR_PATH)
def check_jail_name_valid(jail_name, warn=True):
"""
Return True if jail name matches the required format.
"""
if (
re.match(r"^[.a-zA-Z0-9-]{1,64}$", jail_name)
and not jail_name.startswith(".")
and ".." not in jail_name
):
return True
if warn:
eprint(
dedent(
f"""
{YELLOW}{BOLD}WARNING: INVALID NAME{NORMAL}
A valid name consists of:
- allowed characters (alphanumeric, dash, dot)
- no leading or trailing dots
- no sequences of multiple dots
- max 64 characters"""
)
)
return False
def check_jail_name_available(jail_name, warn=True):
"""
Return True if jail name is not yet taken.
"""
if not os.path.exists(get_jail_path(jail_name)):
return True
if warn:
print()
eprint("A jail with this name already exists.")
return False
def parse_os_release(new_root):
result = {}
with Chroot(new_root):
@ -48,3 +97,89 @@ def jail_is_running(jail_name):
).returncode
== 0
)
def get_zfs_dataset(path):
"""
Get ZFS dataset path.
"""
def clean_field(field):
# Put back spaces which were encoded
# https://github.com/openzfs/zfs/issues/11182
return field.replace("\\040", " ")
path = os.path.realpath(path)
with open("/proc/mounts", "r") as f:
for line in f:
fields = line.split()
if "zfs" == fields[2] and path == clean_field(fields[1]):
return clean_field(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 get_all_jail_names():
try:
jail_names = os.listdir(JAILS_DIR_PATH)
except FileNotFoundError:
jail_names = []
return jail_names
def create_zfs_dataset(absolute_path):
"""
Create a ZFS Dataset inside the jailmaker directory at the provided absolute path.
E.g. "/mnt/mypool/jailmaker/jails" or "/mnt/mypool/jailmaker/jails/newjail").
"""
relative_path = get_relative_path_in_jailmaker_dir(absolute_path)
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(absolute_path):
"""
Remove a ZFS Dataset inside the jailmaker directory at the provided absolute path.
E.g. "/mnt/mypool/jailmaker/jails/oldjail".
"""
relative_path = get_relative_path_in_jailmaker_dir(absolute_path)
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 cleanup(jail_path):
"""
Cleanup jail.
"""
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):
exc_type, exc_value, exc_traceback = exc_info
if issubclass(exc_type, PermissionError):
# Update the file permissions with the immutable and append-only bit cleared
subprocess.run(["chattr", "-i", "-a", path])
# Reattempt the removal
func(path)
elif not issubclass(exc_type, FileNotFoundError):
raise exc_value
eprint(f"Cleaning up: {jail_path}.")
shutil.rmtree(jail_path, onerror=_onerror)

View File

@ -1,16 +0,0 @@
# SPDX-FileCopyrightText: © 2024 Jip-Hop and the Jailmakers <https://github.com/Jip-Hop/jailmaker>
#
# SPDX-License-Identifier: LGPL-3.0-only
import os
from utils.paths import JAILS_DIR_PATH
def get_all_jail_names():
try:
jail_names = os.listdir(JAILS_DIR_PATH)
except FileNotFoundError:
jail_names = []
return jail_names