Add startup command

This commit is contained in:
Jip-Hop 2023-08-15 19:28:43 +02:00
parent dd9d610178
commit 645c758e50
2 changed files with 107 additions and 57 deletions

View File

@ -26,21 +26,12 @@ Create a new dataset called `jailmaker` with the default settings (from TrueNAS
cd /mnt/mypool/jailmaker cd /mnt/mypool/jailmaker
curl --location --remote-name https://raw.githubusercontent.com/Jip-Hop/jailmaker/main/jlmkr.py curl --location --remote-name https://raw.githubusercontent.com/Jip-Hop/jailmaker/main/jlmkr.py
chmod +x jlmkr.py chmod +x jlmkr.py
```
The `jlmkr.py` script (and the jails + config it creates) are now stored on the `jailmaker` dataset and will survive updates of TrueNAS SCALE.
### Install Jailmaker Dependencies
Unfortunately since version 22.12.3 TrueNAS SCALE no longer includes systemd-nspawn. In order to use jailmaker, we need to first install systemd-nspawn using the command below.
```shell
./jlmkr.py install ./jlmkr.py install
``` ```
We need to do this again after each update of TrueNAS SCALE. So it is recommended to schedule this command as Post Init Script (see [Autostart Jail on Boot](#autostart-jail-on-boot)). The `jlmkr.py` script (and the jails + config it creates) are now stored on the `jailmaker` dataset and will survive updates of TrueNAS SCALE. Additionally a symlink has been created so you can call `jlmkr` from anywhere.
Additionally the install command will create a symlink from `/usr/local/sbin/jlmkr` to `jlmkr.py`. Thanks this this you can now run the `jlmkr` command from anywhere (instead of having to run `./jlmkr.py` from inside the directory where you've placed it). After an update of TrueNAS SCALE the symlink will be lost and `systemd-nspawn` (the core package which makes `jailmaker` work) may be gone too. Not to worry, just run `./jlmkr.py install` again or use [the `./jlmkr.py startup` command](#startup-jails-on-boot).
## Usage ## Usage
@ -54,9 +45,18 @@ jlmkr create myjail
After answering a few questions you should have your first jail up and running! After answering a few questions you should have your first jail up and running!
#### Autostart Jail on Boot ### Startup Jails on Boot
In order to start a jail automatically after TrueNAS boots, run `jlmkr start myjail` as Post Init Script with Type `Command` from the TrueNAS web interface. If you want to automatically install systemd-nspawn if it's not already installed (recommended to keep working after a TrueNAS SCALE update) then you may use a command such as this instead: `/mnt/mypool/jailmaker/jlmkr.py install && jlmkr start myjail`. ```shell
# Best to call startup directly (not through the jlmkr symlink)
/mnt/mypool/jailmaker/jlmkr.py startup
# Can be called from the symlink too...
# But this may not be available after a TrueNAS SCALE update
jlmkr startup
```
In order to start jails automatically after TrueNAS boots, run `/mnt/mypool/jailmaker/jlmkr.py startup` as Post Init Script with Type `Command` from the TrueNAS web interface. This will automatically fix the installation of `systemd-nspawn` and setup the `jlmkr` symlink, as well as start all the jails with `startup=1` in the config file. Running the `startup` command Post Init is recommended to keep `jailmaker` working after a TrueNAS SCALE update.
### Start Jail ### Start Jail
@ -134,11 +134,11 @@ See [Advanced Networking](./NETWORKING.md) for more.
## Docker ## Docker
Jailmaker won't install Docker for you, but it can setup the jail with the capabilities required to run docker. You can manually install Docker inside the jail using the [official installation guide](https://docs.docker.com/engine/install/#server) or use [convenience script](https://get.docker.com). The `jailmaker` script won't install Docker for you, but it can setup the jail with the capabilities required to run docker. You can manually install Docker inside the jail using the [official installation guide](https://docs.docker.com/engine/install/#server) or use [convenience script](https://get.docker.com).
## Nvidia GPU ## Nvidia GPU
To make passthrough of the nvidia GPU work, you need to schedule a Pre Init command. The reason is that TrueNAS SCALE by default doesn't load the nvidia kernel modules (and jailmaker doesn't do that either). [This screenshot](https://user-images.githubusercontent.com/1704047/222915803-d6dd51b0-c4dd-4189-84be-a04d38cca0b3.png) shows what the configuration should look like. To make passthrough of the nvidia GPU work, you need to schedule a Pre Init command. The reason is that TrueNAS SCALE by default doesn't load the nvidia kernel modules (and `jailmaker` doesn't do that either). [This screenshot](https://user-images.githubusercontent.com/1704047/222915803-d6dd51b0-c4dd-4189-84be-a04d38cca0b3.png) shows what the configuration should look like.
``` ```
[ ! -f /dev/nvidia-uvm ] && modprobe nvidia-current-uvm && /usr/bin/nvidia-modprobe -c0 -u [ ! -f /dev/nvidia-uvm ] && modprobe nvidia-current-uvm && /usr/bin/nvidia-modprobe -c0 -u
@ -146,7 +146,7 @@ To make passthrough of the nvidia GPU work, you need to schedule a Pre Init comm
## Comparison ## Comparison
TODO: write comparison between systemd-nspawn (without jailmaker), LXC, VMs, Docker (on the host). TODO: write comparison between systemd-nspawn (without `jailmaker`), LXC, VMs, Docker (on the host).
### Incompatible Distros ### Incompatible Distros

130
jlmkr.py
View File

@ -210,28 +210,48 @@ def stop_jail(jail_name):
subprocess.run(["machinectl", "poweroff", jail_name]) subprocess.run(["machinectl", "poweroff", jail_name])
def start_jail(jail_name): def parse_config(jail_config_path):
"""
Start jail with given name.
"""
if jail_is_running(jail_name):
fail(
f"Skipped starting jail {jail_name}. It appears to be running already...")
jail_path = get_jail_path(jail_name)
jail_config_path = get_jail_config_path(jail_name)
config = configparser.ConfigParser() config = configparser.ConfigParser()
try: try:
# Workaround to read config file without section headers # Workaround to read config file without section headers
config.read_string('[DEFAULT]\n'+Path(jail_config_path).read_text()) config.read_string('[DEFAULT]\n'+Path(jail_config_path).read_text())
except FileNotFoundError: except FileNotFoundError:
fail(f'Unable to find: {jail_config_path}.') eprint(f'Unable to find config file: {jail_config_path}.')
return
config = dict(config['DEFAULT']) config = dict(config['DEFAULT'])
print("Config loaded!") return config
def start_jail(jail_name, check_startup_enabled=False):
"""
Start jail with given name.
"""
skip_start_message = f"Skipped starting jail {jail_name}. It appears to be running already..."
if not check_startup_enabled and jail_is_running(jail_name):
fail(skip_start_message)
jail_path = get_jail_path(jail_name)
jail_config_path = get_jail_config_path(jail_name)
config = parse_config(jail_config_path)
if not config:
fail(f'Aborting...')
# Only start if the startup setting is enabled in the config
if check_startup_enabled:
if config.get('startup') == '1':
# We should start this jail based on the startup config...
if jail_is_running(jail_name):
# ...but we can skip if it's already running
eprint(skip_start_message)
return
else:
# Skip starting this jail since the startup config setting isnot enabled
return
systemd_run_additional_args = [ systemd_run_additional_args = [
f"--unit={SYMLINK_NAME}-{jail_name}", f"--unit={SYMLINK_NAME}-{jail_name}",
@ -327,27 +347,20 @@ def start_jail(jail_name):
] ]
print(dedent(f""" print(dedent(f"""
Starting jail with the following command: Starting jail {jail_name} with the following command:
{shlex.join(cmd)} {shlex.join(cmd)}
Starting jail with name: {jail_name}
""")) """))
try: try:
subprocess.run(cmd, check=True) subprocess.run(cmd, check=True)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
fail(dedent(f""" fail(dedent(f"""
Failed to start the jail... Failed to start jail {jail_name}...
In case of a config error, you may fix it with: In case of a config error, you may fix it with:
{SYMLINK_NAME} edit {jail_name} {SYMLINK_NAME} edit {jail_name}
""")) """))
print(dedent(f"""
Get a shell:
{COMMAND_NAME} shell {jail_name}
"""))
def cleanup(jail_path): def cleanup(jail_path):
""" """
@ -651,6 +664,15 @@ def create_jail(jail_name, distro='debian', release='bullseye'):
systemd_nspawn_user_args = input("Additional flags: ") or "" systemd_nspawn_user_args = input("Additional flags: ") or ""
# Disable tab auto completion # Disable tab auto completion
readline.parse_and_bind('tab: self-insert') readline.parse_and_bind('tab: self-insert')
print(dedent(f"""
The `{COMMAND_NAME} startup` command can automatically ensure {COMMAND_NAME} is installed properly and start a selection of jails.
This comes in handy when you want to automatically start multiple jails after booting TrueNAS SCALE (e.g. from a Post Init Script).
"""))
startup = int(agree(
f"Do you want to start this jail when running: {COMMAND_NAME} startup?", 'n'))
print() print()
jail_config_path = get_jail_config_path(jail_name) jail_config_path = get_jail_config_path(jail_name)
@ -786,6 +808,7 @@ def create_jail(jail_name, distro='debian', release='bullseye'):
] ]
config = cleandoc(f""" config = cleandoc(f"""
startup={startup}
docker_compatible={docker_compatible} docker_compatible={docker_compatible}
gpu_passthrough_intel={gpu_passthrough_intel} gpu_passthrough_intel={gpu_passthrough_intel}
gpu_passthrough_nvidia={gpu_passthrough_nvidia} gpu_passthrough_nvidia={gpu_passthrough_nvidia}
@ -805,7 +828,7 @@ def create_jail(jail_name, distro='debian', release='bullseye'):
raise error raise error
print() print()
if agree("Do you want to start the jail?", 'y'): if agree(f"Do you want to start jail {jail_name} right now?", 'y'):
start_jail(jail_name) start_jail(jail_name)
@ -890,6 +913,15 @@ def run_command_and_parse_json(command):
return None return None
def get_all_jail_names():
try:
jail_names = os.listdir(JAILS_DIR_PATH)
except FileNotFoundError:
jail_names = []
return jail_names
def list_jails(): def list_jails():
""" """
List all available and running jails. List all available and running jails.
@ -898,16 +930,13 @@ def list_jails():
jails = {} jails = {}
empty_value_indicator = '-' empty_value_indicator = '-'
try: jail_names = get_all_jail_names()
jail_dirs = os.listdir(JAILS_DIR_PATH)
except FileNotFoundError:
jail_dirs = []
if not jail_dirs: if not jail_names:
print('No jails.') print('No jails.')
return return
for jail in jail_dirs: for jail in jail_names:
jails[jail] = {"name": jail, "running": False} jails[jail] = {"name": jail, "running": False}
# Get running jails from machinectl # Get running jails from machinectl
@ -937,7 +966,19 @@ def list_jails():
# TODO: add additional properties from the jails config file # TODO: add additional properties from the jails config file
print_table(["name", "running", "os", "version", "addresses"], for jail_name in jails:
config = parse_config(get_jail_config_path(jail_name))
startup = False
if config:
startup = bool(int(config.get('startup', '0')))
# TODO: in case config is missing or parsing fails,
# should an error message be thrown here?
jails[jail_name]['startup'] = startup
print_table(["name", "running", "startup", "os", "version", "addresses"],
sorted(jails.values(), key=lambda x: x['name']), empty_value_indicator) sorted(jails.values(), key=lambda x: x['name']), empty_value_indicator)
@ -983,6 +1024,12 @@ def install_jailmaker():
print("Done installing jailmaker.") print("Done installing jailmaker.")
def startup_jails():
install_jailmaker()
for jail_name in get_all_jail_names():
start_jail(jail_name, True)
def main(): def main():
if os.stat(SCRIPT_PATH).st_uid != 0: if os.stat(SCRIPT_PATH).st_uid != 0:
fail( fail(
@ -1042,6 +1089,9 @@ def main():
subparsers.add_parser(name='images', epilog=DISCLAIMER, subparsers.add_parser(name='images', epilog=DISCLAIMER,
help='list available images to create jails from') help='list available images to create jails from')
subparsers.add_parser(name='startup', epilog=DISCLAIMER,
help=f'install {SYMLINK_NAME} and startup selected jails')
if os.getuid() != 0: if os.getuid() != 0:
parser.print_usage() parser.print_usage()
fail("Run this script as root...") fail("Run this script as root...")
@ -1054,7 +1104,13 @@ def main():
args, additional_args = parser.parse_known_args() args, additional_args = parser.parse_known_args()
if args.subcommand == 'start': if args.subcommand == 'install':
install_jailmaker()
elif args.subcommand == 'create':
create_jail(args.name)
elif args.subcommand == 'start':
start_jail(args.name) start_jail(args.name)
elif args.subcommand == 'shell': elif args.subcommand == 'shell':
@ -1072,9 +1128,6 @@ def main():
elif args.subcommand == 'stop': elif args.subcommand == 'stop':
stop_jail(args.name) stop_jail(args.name)
elif args.subcommand == 'create':
create_jail(args.name)
elif args.subcommand == 'edit': elif args.subcommand == 'edit':
edit_jail(args.name) edit_jail(args.name)
@ -1084,14 +1137,11 @@ def main():
elif args.subcommand == 'list': elif args.subcommand == 'list':
list_jails() list_jails()
elif args.subcommand == 'install':
install_jailmaker()
elif args.subcommand == 'images': elif args.subcommand == 'images':
run_lxc_download_script() run_lxc_download_script()
elif args.subcommand: elif args.subcommand == 'startup':
parser.print_usage() startup_jails()
else: else:
if agree("Create a new jail?", 'y'): if agree("Create a new jail?", 'y'):