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 collections import defaultdict
from utils.console import NORMAL, UNDERLINE from utils.console import NORMAL, UNDERLINE
from utils.config_parser import parse_config_file 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.jail_dataset import get_all_jail_names, get_jail_config_path, get_jail_rootfs_path, parse_os_release
from utils.parent_dataset import get_all_jail_names
def list_jails(): def list_jails():

View File

@ -26,7 +26,6 @@ import sys
import tempfile import tempfile
import time import time
import urllib.request import urllib.request
from inspect import cleandoc
from pathlib import Path, PurePath from pathlib import Path, PurePath
from textwrap import dedent from textwrap import dedent
@ -70,6 +69,7 @@ class CustomSubParser(argparse.ArgumentParser):
from utils.chroot import Chroot from utils.chroot import Chroot
from utils.console import eprint, fail 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 utils.jail_dataset import get_jail_path, get_jail_config_path, get_jail_rootfs_path
from actions.exec import exec_jail from actions.exec import exec_jail
@ -80,139 +80,10 @@ from actions.start import start_jail
from actions.restart import restart_jail from actions.restart import restart_jail
from actions.images import run_lxc_download_script from actions.images import run_lxc_download_script
from utils.jail_dataset import cleanup, check_jail_name_valid, check_jail_name_available
def cleanup(jail_path): from utils.download import run_lxc_download_script
"""
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.files import stat_chmod from utils.files import stat_chmod
from utils.jail_dataset import get_zfs_dataset, create_zfs_dataset, remove_zfs_dataset
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
def get_text_editor(): def get_text_editor():
@ -229,288 +100,7 @@ def get_text_editor():
) )
def create_jail(**kwargs): from actions.create import create_jail
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 utils.jail_dataset import jail_is_running from utils.jail_dataset import jail_is_running
@ -574,8 +164,7 @@ def remove_jail(jail_name):
return 1 return 1
from utils.parent_dataset import get_all_jail_names from utils.jail_dataset import get_all_jail_names, parse_os_release
from utils.jail_dataset import parse_os_release
from actions.list import list_jails 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): if mode != stat.S_IMODE(os.stat(file_path).st_mode):
os.chmod(file_path, 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 os.path
import platform import platform
import re
import shutil
import subprocess import subprocess
from pathlib import PurePath
from textwrap import dedent
from utils.chroot import Chroot 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): 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) 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): def parse_os_release(new_root):
result = {} result = {}
with Chroot(new_root): with Chroot(new_root):
@ -48,3 +97,89 @@ def jail_is_running(jail_name):
).returncode ).returncode
== 0 == 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