Extract create action
This commit is contained in:
parent
fbf6e4d9a5
commit
1c0a7a46d4
|
@ -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
|
|
@ -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():
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue