diff --git a/README.md b/README.md index 3783d38..0276a9d 100644 --- a/README.md +++ b/README.md @@ -26,21 +26,12 @@ Create a new dataset called `jailmaker` with the default settings (from TrueNAS cd /mnt/mypool/jailmaker curl --location --remote-name https://raw.githubusercontent.com/Jip-Hop/jailmaker/main/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 ``` -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 @@ -54,9 +45,18 @@ jlmkr create myjail 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 @@ -134,11 +134,11 @@ See [Advanced Networking](./NETWORKING.md) for more. ## 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 -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 @@ -146,7 +146,7 @@ To make passthrough of the nvidia GPU work, you need to schedule a Pre Init comm ## 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 diff --git a/jlmkr.py b/jlmkr.py index def8f43..5ee660e 100755 --- a/jlmkr.py +++ b/jlmkr.py @@ -210,28 +210,48 @@ def stop_jail(jail_name): subprocess.run(["machinectl", "poweroff", jail_name]) -def start_jail(jail_name): - """ - 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) - +def parse_config(jail_config_path): config = configparser.ConfigParser() try: # Workaround to read config file without section headers config.read_string('[DEFAULT]\n'+Path(jail_config_path).read_text()) 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']) - 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 = [ f"--unit={SYMLINK_NAME}-{jail_name}", @@ -327,27 +347,20 @@ def start_jail(jail_name): ] print(dedent(f""" - Starting jail with the following command: + Starting jail {jail_name} with the following command: {shlex.join(cmd)} - - Starting jail with name: {jail_name} """)) try: subprocess.run(cmd, check=True) except subprocess.CalledProcessError: 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: {SYMLINK_NAME} edit {jail_name} """)) - print(dedent(f""" - Get a shell: - {COMMAND_NAME} shell {jail_name} - """)) - def cleanup(jail_path): """ @@ -651,8 +664,17 @@ def create_jail(jail_name, distro='debian', release='bullseye'): systemd_nspawn_user_args = input("Additional flags: ") or "" # Disable tab auto completion readline.parse_and_bind('tab: self-insert') - print() + 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() + jail_config_path = get_jail_config_path(jail_name) jail_rootfs_path = get_jail_rootfs_path(jail_name) @@ -786,6 +808,7 @@ def create_jail(jail_name, distro='debian', release='bullseye'): ] config = cleandoc(f""" + startup={startup} docker_compatible={docker_compatible} gpu_passthrough_intel={gpu_passthrough_intel} gpu_passthrough_nvidia={gpu_passthrough_nvidia} @@ -805,7 +828,7 @@ def create_jail(jail_name, distro='debian', release='bullseye'): raise error 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) @@ -890,6 +913,15 @@ def run_command_and_parse_json(command): 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(): """ List all available and running jails. @@ -898,16 +930,13 @@ def list_jails(): jails = {} empty_value_indicator = '-' - try: - jail_dirs = os.listdir(JAILS_DIR_PATH) - except FileNotFoundError: - jail_dirs = [] + jail_names = get_all_jail_names() - if not jail_dirs: + if not jail_names: print('No jails.') return - for jail in jail_dirs: + for jail in jail_names: jails[jail] = {"name": jail, "running": False} # Get running jails from machinectl @@ -937,7 +966,19 @@ def list_jails(): # 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) @@ -983,6 +1024,12 @@ def install_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(): if os.stat(SCRIPT_PATH).st_uid != 0: fail( @@ -1042,6 +1089,9 @@ def main(): subparsers.add_parser(name='images', epilog=DISCLAIMER, 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: parser.print_usage() fail("Run this script as root...") @@ -1054,7 +1104,13 @@ def main(): 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) elif args.subcommand == 'shell': @@ -1072,9 +1128,6 @@ def main(): elif args.subcommand == 'stop': stop_jail(args.name) - elif args.subcommand == 'create': - create_jail(args.name) - elif args.subcommand == 'edit': edit_jail(args.name) @@ -1084,14 +1137,11 @@ def main(): elif args.subcommand == 'list': list_jails() - elif args.subcommand == 'install': - install_jailmaker() - elif args.subcommand == 'images': run_lxc_download_script() - elif args.subcommand: - parser.print_usage() + elif args.subcommand == 'startup': + startup_jails() else: if agree("Create a new jail?", 'y'):