From ee8483775e0309c64d1fa390a11a8a94d1285368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 7 Apr 2026 15:54:54 +0200 Subject: [PATCH 1/5] =?UTF-8?q?shared/options:=20add=20equivalent=20of=20"?= =?UTF-8?q?+=E2=80=A6"=20for=20nested=20commandline=20parsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/options.c | 5 ++ src/shared/options.h | 23 ++++--- src/test/test-options.c | 146 +++++++++++++++++++++++++++++----------- 3 files changed, 123 insertions(+), 51 deletions(-) diff --git a/src/shared/options.c b/src/shared/options.c index e6b82fe34d..f00d9674d7 100644 --- a/src/shared/options.c +++ b/src/shared/options.c @@ -108,6 +108,11 @@ int option_parse( /* Looks like we found an option parameter */ break; + if (state->stop_at_first_nonoption) { + state->parsing_stopped = true; + return 0; + } + state->optind++; } diff --git a/src/shared/options.h b/src/shared/options.h index c1ffa595ce..1b0488579f 100644 --- a/src/shared/options.h +++ b/src/shared/options.h @@ -79,17 +79,20 @@ extern const Option __start_SYSTEMD_OPTIONS[]; extern const Option __stop_SYSTEMD_OPTIONS[]; typedef struct OptionParser { - /* Those two should stay first so that it's possible to initialize the struct as { argc, argv }. */ - int argc; /* The original argc. */ - char **argv; /* The argv array, possibly reordered. */ + /* Those three should stay first so that it's possible to initialize the struct as { argc, argv } + * or { argc, argv, true/false }. */ + int argc; /* The original argc. */ + char **argv; /* The argv array, possibly reordered. */ + bool stop_at_first_nonoption; /* Same as "+…" for getopt_long — only parse options before the first + * positional argument. */ - int optind; /* Position of the parameter being handled. - * 0 → option parsing hasn't been started yet. */ - int short_option_offset; /* Set when we're parsing an argument with one or more short options. - * 0 → we're not parsing short options. */ - int positional_offset; /* Offset to where positional parameters are. After processing has been - * finished, all options and their args are to the left of this offset. */ - bool parsing_stopped; /* We processed "--" or an option that terminates option parsing. */ + bool parsing_stopped; /* We processed "--" or an option that terminates option parsing. */ + int optind; /* Position of the parameter being handled. + * 0 → option parsing hasn't been started yet. */ + int short_option_offset; /* Set when we're parsing an argument with one or more short options. + * 0 → we're not parsing short options. */ + int positional_offset; /* Offset to where positional parameters are. After processing has been + * finished, all options and their args are to the left of this offset. */ } OptionParser; int option_parse( diff --git a/src/test/test-options.c b/src/test/test-options.c index fb1f61f358..a9cafbdd73 100644 --- a/src/test/test-options.c +++ b/src/test/test-options.c @@ -14,7 +14,8 @@ static void test_option_parse_one( char **argv, const Option options[], const Entry *entries, - char **remaining) { + char **remaining, + bool stop_at_first_nonoption) { _cleanup_free_ char *joined = strv_join(argv, ", "); log_debug("/* %s(%s) */", __func__, joined); @@ -31,7 +32,7 @@ static void test_option_parse_one( for (const Entry *e = entries; e && (e->long_code || e->short_code != 0); e++) n_entries++; - OptionParser state = { argc, argv }; + OptionParser state = { argc, argv, stop_at_first_nonoption }; const Option *opt; const char *arg; for (int c; (c = option_parse(options, options + n_options, &state, &opt, &arg)) != 0; ) { @@ -99,7 +100,8 @@ TEST(option_parse) { test_option_parse_one(STRV_MAKE("arg0"), options, NULL, - NULL); + NULL, + false); test_option_parse_one(STRV_MAKE("arg0", "string1", @@ -111,7 +113,8 @@ TEST(option_parse) { STRV_MAKE("string1", "string2", "string3", - "string4")); + "string4"), + false); test_option_parse_one(STRV_MAKE("arg0", "--", @@ -124,7 +127,8 @@ TEST(option_parse) { STRV_MAKE("string1", "--help", "-h", - "string4")); + "string4"), + false); test_option_parse_one(STRV_MAKE("arg0", "string1", @@ -137,7 +141,8 @@ TEST(option_parse) { STRV_MAKE("string1", "string2", "--", - "string4")); + "string4"), + false); test_option_parse_one(STRV_MAKE("arg0", "string1", @@ -150,7 +155,8 @@ TEST(option_parse) { STRV_MAKE("string1", "string2", "string3", - "string4")); + "string4"), + false); test_option_parse_one(STRV_MAKE("arg0", "--help"), @@ -159,7 +165,21 @@ TEST(option_parse) { { "help" }, {} }, - NULL); + NULL, + false); + + test_option_parse_one(STRV_MAKE("arg0", + "--help", + "string1", + "--help"), + options, + (Entry[]) { + { "help" }, + {} + }, + STRV_MAKE("string1", + "--help"), + true); test_option_parse_one(STRV_MAKE("arg0", "-h"), @@ -168,7 +188,8 @@ TEST(option_parse) { { "help" }, {} }, - NULL); + NULL, + false); test_option_parse_one(STRV_MAKE("arg0", "--help", @@ -184,7 +205,8 @@ TEST(option_parse) { STRV_MAKE("string1", "string2", "string3", - "string4")); + "string4"), + false); test_option_parse_one(STRV_MAKE("arg0", "-h", @@ -200,7 +222,8 @@ TEST(option_parse) { STRV_MAKE("string1", "string2", "string3", - "string4")); + "string4"), + false); test_option_parse_one(STRV_MAKE("arg0", "string1", @@ -216,7 +239,8 @@ TEST(option_parse) { STRV_MAKE("string1", "string2", "string3", - "string4")); + "string4"), + false); test_option_parse_one(STRV_MAKE("arg0", "string1", @@ -232,7 +256,8 @@ TEST(option_parse) { STRV_MAKE("string1", "string2", "string3", - "string4")); + "string4"), + false); test_option_parse_one(STRV_MAKE("arg0", "string1", @@ -248,7 +273,8 @@ TEST(option_parse) { STRV_MAKE("string1", "string2", "string3", - "string4")); + "string4"), + false); test_option_parse_one(STRV_MAKE("arg0", "string1", @@ -264,7 +290,8 @@ TEST(option_parse) { STRV_MAKE("string1", "string2", "string3", - "string4")); + "string4"), + false); test_option_parse_one(STRV_MAKE("arg0", "--required1", "reqarg1"), @@ -273,7 +300,8 @@ TEST(option_parse) { { "required1", "reqarg1" }, {} }, - NULL); + NULL, + false); test_option_parse_one(STRV_MAKE("arg0", "-r", "reqarg1"), @@ -282,7 +310,8 @@ TEST(option_parse) { { "required1", "reqarg1" }, {} }, - NULL); + NULL, + false); test_option_parse_one(STRV_MAKE("arg0", "string1", @@ -294,7 +323,19 @@ TEST(option_parse) { {} }, STRV_MAKE("string1", - "string2")); + "string2"), + false); + + test_option_parse_one(STRV_MAKE("arg0", + "string1", + "string2", + "-r", "reqarg1"), + options, + NULL, + STRV_MAKE("string1", + "string2", + "-r", "reqarg1"), + true); test_option_parse_one(STRV_MAKE("arg0", "--optional1=optarg1"), @@ -303,7 +344,8 @@ TEST(option_parse) { { "optional1", "optarg1" }, {} }, - NULL); + NULL, + true); test_option_parse_one(STRV_MAKE("arg0", "--optional1", "string1"), @@ -312,7 +354,8 @@ TEST(option_parse) { { "optional1", NULL }, {} }, - STRV_MAKE("string1")); + STRV_MAKE("string1"), + false); test_option_parse_one(STRV_MAKE("arg0", "-ooptarg1"), @@ -321,7 +364,8 @@ TEST(option_parse) { { "optional1", "optarg1" }, {} }, - NULL); + NULL, + false); test_option_parse_one(STRV_MAKE("arg0", "-o", "string1"), @@ -330,7 +374,8 @@ TEST(option_parse) { { "optional1", NULL }, {} }, - STRV_MAKE("string1")); + STRV_MAKE("string1"), + false); test_option_parse_one(STRV_MAKE("arg0", "string1", @@ -399,7 +444,8 @@ TEST(option_parse) { "string7", "--help", "--required1", - "--optional1")); + "--optional1"), + false); } TEST(option_stops_parsing) { @@ -422,7 +468,8 @@ TEST(option_stops_parsing) { {} }, STRV_MAKE("--help", - "foo")); + "foo"), + false); /* Options before --exec are still parsed */ test_option_parse_one(STRV_MAKE("arg0", @@ -437,7 +484,8 @@ TEST(option_stops_parsing) { {} }, STRV_MAKE("--version", - "bar")); + "bar"), + false); /* --exec with no trailing args */ test_option_parse_one(STRV_MAKE("arg0", @@ -447,7 +495,8 @@ TEST(option_stops_parsing) { { "exec" }, {} }, - NULL); + NULL, + false); /* --exec after positional args */ test_option_parse_one(STRV_MAKE("arg0", @@ -463,7 +512,8 @@ TEST(option_stops_parsing) { STRV_MAKE("pos1", "--help", "--required", - "val")); + "val"), + false); /* "--" after --exec: "--" is still consumed as end-of-options marker. This is needed for * backwards compatibility, systemd-dissect implemented this behaviour. But also, it makes @@ -478,7 +528,8 @@ TEST(option_stops_parsing) { { "exec" }, {} }, - STRV_MAKE("--help")); + STRV_MAKE("--help"), + false); /* "--" before --exec: "--" terminates first, --exec is positional */ test_option_parse_one(STRV_MAKE("arg0", @@ -488,7 +539,8 @@ TEST(option_stops_parsing) { options, NULL, STRV_MAKE("--exec", - "--help")); + "--help"), + false); /* Multiple options then --exec then more option-like args */ test_option_parse_one(STRV_MAKE("arg0", @@ -506,7 +558,8 @@ TEST(option_stops_parsing) { }, STRV_MAKE("-h", "--required", - "val2")); + "val2"), + false); } TEST(option_group_marker) { @@ -530,7 +583,8 @@ TEST(option_group_marker) { { "debug" }, {} }, - NULL); + NULL, + false); /* Check that group marker name is ignored */ test_option_parse_one(STRV_MAKE("arg0", @@ -542,7 +596,8 @@ TEST(option_group_marker) { { "version" }, {} }, - NULL); + NULL, + false); /* Verify that the group marker is not mistaken for an option */ test_option_invalid_one(STRV_MAKE("arg0", @@ -569,7 +624,8 @@ TEST(option_group_marker) { { "Advance" }, {} }, - NULL); + NULL, + false); /* Partial match with multiple candidates */ test_option_invalid_one(STRV_MAKE("arg0", @@ -592,7 +648,8 @@ TEST(option_optional_arg) { { "output", "foo.txt" }, {} }, - NULL); + NULL, + false); /* Long option without = does NOT consume the next arg */ test_option_parse_one(STRV_MAKE("arg0", @@ -602,7 +659,8 @@ TEST(option_optional_arg) { { "output", NULL }, {} }, - STRV_MAKE("foo.txt")); + STRV_MAKE("foo.txt"), + false); /* Short option with inline arg */ test_option_parse_one(STRV_MAKE("arg0", @@ -612,7 +670,8 @@ TEST(option_optional_arg) { { "output", "foo.txt" }, {} }, - NULL); + NULL, + false); /* Short option without inline arg does NOT consume the next arg */ test_option_parse_one(STRV_MAKE("arg0", @@ -622,7 +681,8 @@ TEST(option_optional_arg) { { "output", NULL }, {} }, - STRV_MAKE("foo.txt")); + STRV_MAKE("foo.txt"), + false); /* Optional arg option at end of argv */ test_option_parse_one(STRV_MAKE("arg0", @@ -632,7 +692,8 @@ TEST(option_optional_arg) { { "output", NULL }, {} }, - NULL); + NULL, + false); /* Mixed: optional arg with other options */ test_option_parse_one(STRV_MAKE("arg0", @@ -646,7 +707,8 @@ TEST(option_optional_arg) { { "help" }, {} }, - NULL); + NULL, + false); /* Short combo: -ho (h then o with no arg) */ test_option_parse_one(STRV_MAKE("arg0", @@ -657,7 +719,8 @@ TEST(option_optional_arg) { { "output", NULL }, {} }, - STRV_MAKE("pos1")); + STRV_MAKE("pos1"), + false); /* Short combo: -hobar (h then o with inline arg "bar") */ test_option_parse_one(STRV_MAKE("arg0", @@ -668,7 +731,8 @@ TEST(option_optional_arg) { { "output", "bar" }, {} }, - NULL); + NULL, + false); } /* Test the OPTION, OPTION_LONG, OPTION_SHORT, OPTION_FULL, OPTION_GROUP macros From 4c778c51c07db3eb0e07dd875f10eb85f086f096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 7 Apr 2026 16:22:21 +0200 Subject: [PATCH 2/5] vmspawn: convert to the new option parser Uses stop_at_first_nonoption for POSIX-style option parsing. --help output is the same, apart from whitespace differences and common strings. The error message for --ssh-key-type= is fixed. Co-developed-by: Claude Opus 4.6 --- src/vmspawn/vmspawn.c | 1051 ++++++++++++++++++----------------------- 1 file changed, 453 insertions(+), 598 deletions(-) diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 1b3558b47e..93c8fbbbbd 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include #include @@ -39,6 +38,7 @@ #include "fd-util.h" #include "fileio.h" #include "fork-notify.h" +#include "format-table.h" #include "format-util.h" #include "fs-util.h" #include "gpt.h" @@ -58,6 +58,7 @@ #include "netif-util.h" #include "nsresource.h" #include "osc-context.h" +#include "options.h" #include "pager.h" #include "parse-argument.h" #include "parse-util.h" @@ -202,6 +203,11 @@ STATIC_DESTRUCTOR_REGISTER(arg_bind_user, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_bind_user_shell, freep); STATIC_DESTRUCTOR_REGISTER(arg_bind_user_groups, strv_freep); +static void unref_many_tables(Table* (*tablesp)[]) { + for (Table **t = *ASSERT_PTR(tablesp); *t; t++) + *t = table_unref(*t); +} + static int help(void) { _cleanup_free_ char *link = NULL; int r; @@ -212,107 +218,46 @@ static int help(void) { if (r < 0) return log_oom(); - printf("%1$s [OPTIONS...] [ARGUMENTS...]\n\n" - "%5$sSpawn a command or OS in a virtual machine.%6$s\n\n" - " -h --help Show this help\n" - " --version Print version string\n" - " -q --quiet Do not show status information\n" - " --no-pager Do not pipe output into a pager\n" - " --no-ask-password Do not prompt for password\n" - " --system Run in the system service manager scope\n" - " --user Run in the user service manager scope\n" - "\n%3$sImage:%4$s\n" - " -D --directory=PATH Root directory for the VM\n" - " -x --ephemeral Run VM with snapshot of the disk or directory\n" - " -i --image=FILE|DEVICE Root file system disk image or device for the VM\n" - " --image-format=FORMAT Specify disk image format (raw, qcow2; default: raw)\n" - " --image-disk-type=TYPE\n" - " Specify disk type (virtio-blk, virtio-scsi, nvme,\n" - " scsi-cd; default: virtio-blk)\n" - "\n%3$sHost Configuration:%4$s\n" - " --cpus=CPUS Configure number of CPUs in guest\n" - " --ram=BYTES[:MAXBYTES[:SLOTS]]\n" - " Configure guest's RAM size (and max/slots for\n" - " hotplug)\n" - " --kvm=BOOL Enable use of KVM\n" - " --vsock=BOOL Override autodetection of VSOCK support\n" - " --vsock-cid=CID Specify the CID to use for the guest's VSOCK support\n" - " --tpm=BOOL Enable use of a virtual TPM\n" - " --tpm-state=off|auto|PATH\n" - " Where to store TPM state\n" - " --efi-nvram-template=PATH\n" - " Set the path to the EFI NVRAM template file to use\n" - " --efi-nvram-state=off|auto|PATH\n" - " Where to store EFI Variable NVRAM state\n" - " --linux=PATH Specify the linux kernel for direct kernel boot\n" - " --initrd=PATH Specify the initrd for direct kernel boot\n" - " -n --network-tap Create a TAP device for networking\n" - " --network-user-mode Use user mode networking\n" - " --secure-boot=BOOL|auto\n" - " Enable searching for firmware supporting SecureBoot\n" - " --firmware=PATH|list|describe\n" - " Select firmware definition file (or list/describe\n" - " available)\n" - " --firmware-features=FEATURE[,FEATURE...]|list\n" - " Require/exclude specific firmware features\n" - " --discard-disk=BOOL Control processing of discard requests\n" - " -G --grow-image=BYTES Grow image file to specified size in bytes\n" - "\n%3$sExecution:%4$s\n" - " -s --smbios11=STRING Pass an arbitrary SMBIOS Type #11 string to the VM\n" - " --notify-ready=BOOL Wait for ready notification from the VM\n" - "\n%3$sSystem Identity:%4$s\n" - " -M --machine=NAME Set the machine name for the VM\n" - " --uuid=UUID Set a specific machine UUID for the VM\n" - "\n%3$sProperties:%4$s\n" - " -S --slice=SLICE Place the VM in the specified slice\n" - " --property=NAME=VALUE Set scope unit property\n" - " --register=BOOLEAN Register VM as machine\n" - " --keep-unit Do not register a scope for the machine, reuse\n" - " the service unit vmspawn is running in\n" - "\n%3$sUser Namespacing:%4$s\n" - " --private-users=UIDBASE[:NUIDS]\n" - " Configure the UID/GID range to map into the\n" - " virtiofsd namespace\n" - "\n%3$sMounts:%4$s\n" - " --bind=SOURCE[:TARGET]\n" - " Mount a file or directory from the host into the VM\n" - " --bind-ro=SOURCE[:TARGET]\n" - " Mount a file or directory, but read-only\n" - " --extra-drive=[FORMAT:][DISKTYPE:]PATH\n" - " Adds an additional disk to the VM\n" - " FORMAT: raw, qcow2\n" - " DISKTYPE: virtio-blk, virtio-scsi, nvme,\n" - " scsi-cd\n" - " --bind-user=NAME Bind user from host to virtual machine\n" - " --bind-user-shell=BOOL|PATH\n" - " Configure the shell to use for --bind-user= users\n" - " --bind-user-group=GROUP\n" - " Add an auxiliary group to --bind-user= users\n" - "\n%3$sIntegration:%4$s\n" - " --forward-journal=FILE|DIR\n" - " Forward the VM's journal to the host\n" - " --pass-ssh-key=BOOL Create an SSH key to access the VM\n" - " --ssh-key-type=TYPE Choose what type of SSH key to pass\n" - "\n%3$sInput/Output:%4$s\n" - " --console=MODE Console mode (interactive, native, gui, read-only\n" - " or headless)\n" - " --console-transport=TRANSPORT\n" - " Console transport (virtio or serial)\n" - " --background=COLOR Set ANSI color for background\n" - "\n%3$sCredentials:%4$s\n" - " --set-credential=ID:VALUE\n" - " Pass a credential with literal value to the VM\n" - " --load-credential=ID:PATH\n" - " Load credential for the VM from file or AF_UNIX\n" - " stream socket.\n" - "\nSee the %2$s for details.\n", + static const char *groups[] = { + NULL, + "Image", + "Host Configuration", + "Execution", + "System Identity", + "Properties", + "User Namespacing", + "Mounts", + "Integration", + "Input/Output", + "Credentials", + }; + + _cleanup_(unref_many_tables) Table* tables[ELEMENTSOF(groups) + 1] = {}; + + for (size_t i = 0; i < ELEMENTSOF(groups); i++) { + r = option_parser_get_help_table_group(groups[i], &tables[i]); + if (r < 0) + return r; + } + + (void) table_sync_column_widths(0, tables[0], tables[1], tables[2], tables[3], tables[4], + tables[5], tables[6], tables[7], tables[8], tables[9], tables[10]); + + printf("%s [OPTIONS...] [ARGUMENTS...]\n\n" + "%sSpawn a command or OS in a virtual machine.%s\n", program_invocation_short_name, - link, - ansi_underline(), - ansi_normal(), ansi_highlight(), ansi_normal()); + for (size_t i = 0; i < ELEMENTSOF(groups); i++) { + printf("\n%s%s:%s\n", ansi_underline(), groups[i] ?: "Options", ansi_normal()); + + r = table_print_or_warn(tables[i]); + if (r < 0) + return r; + } + + printf("\nSee the %s for details.\n", link); return 0; } @@ -374,216 +319,117 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_oom(); - enum { - ARG_VERSION = 0x100, - ARG_NO_PAGER, - ARG_CPUS, - ARG_RAM, - ARG_KVM, - ARG_VSOCK, - ARG_VSOCK_CID, - ARG_TPM, - ARG_LINUX, - ARG_INITRD, - ARG_QEMU_GUI, - ARG_NETWORK_USER_MODE, - ARG_UUID, - ARG_REGISTER, - ARG_KEEP_UNIT, - ARG_BIND, - ARG_BIND_RO, - ARG_EXTRA_DRIVE, - ARG_SECURE_BOOT, - ARG_PRIVATE_USERS, - ARG_FORWARD_JOURNAL, - ARG_PASS_SSH_KEY, - ARG_SSH_KEY_TYPE, - ARG_SET_CREDENTIAL, - ARG_LOAD_CREDENTIAL, - ARG_FIRMWARE, - ARG_FIRMWARE_FEATURES, - ARG_DISCARD_DISK, - ARG_CONSOLE, - ARG_BACKGROUND, - ARG_TPM_STATE, - ARG_EFI_NVRAM_TEMPLATE, - ARG_EFI_NVRAM_STATE, - ARG_NO_ASK_PASSWORD, - ARG_PROPERTY, - ARG_NOTIFY_READY, - ARG_BIND_USER, - ARG_BIND_USER_SHELL, - ARG_BIND_USER_GROUP, - ARG_SYSTEM, - ARG_USER, - ARG_IMAGE_FORMAT, - ARG_IMAGE_DISK_TYPE, - ARG_CONSOLE_TRANSPORT, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "quiet", no_argument, NULL, 'q' }, - { "no-pager", no_argument, NULL, ARG_NO_PAGER }, - { "image", required_argument, NULL, 'i' }, - { "image-format", required_argument, NULL, ARG_IMAGE_FORMAT }, - { "image-disk-type", required_argument, NULL, ARG_IMAGE_DISK_TYPE }, - { "ephemeral", no_argument, NULL, 'x' }, - { "directory", required_argument, NULL, 'D' }, - { "machine", required_argument, NULL, 'M' }, - { "slice", required_argument, NULL, 'S' }, - { "cpus", required_argument, NULL, ARG_CPUS }, - { "qemu-smp", required_argument, NULL, ARG_CPUS }, /* Compat alias */ - { "ram", required_argument, NULL, ARG_RAM }, - { "qemu-mem", required_argument, NULL, ARG_RAM }, /* Compat alias */ - { "kvm", required_argument, NULL, ARG_KVM }, - { "qemu-kvm", required_argument, NULL, ARG_KVM }, /* Compat alias */ - { "vsock", required_argument, NULL, ARG_VSOCK }, - { "qemu-vsock", required_argument, NULL, ARG_VSOCK }, /* Compat alias */ - { "vsock-cid", required_argument, NULL, ARG_VSOCK_CID }, - { "tpm", required_argument, NULL, ARG_TPM }, - { "linux", required_argument, NULL, ARG_LINUX }, - { "initrd", required_argument, NULL, ARG_INITRD }, - { "console", required_argument, NULL, ARG_CONSOLE }, - { "console-transport", required_argument, NULL, ARG_CONSOLE_TRANSPORT }, - { "qemu-gui", no_argument, NULL, ARG_QEMU_GUI }, /* compat option */ - { "network-tap", no_argument, NULL, 'n' }, - { "network-user-mode", no_argument, NULL, ARG_NETWORK_USER_MODE }, - { "uuid", required_argument, NULL, ARG_UUID }, - { "register", required_argument, NULL, ARG_REGISTER }, - { "keep-unit", no_argument, NULL, ARG_KEEP_UNIT }, - { "bind", required_argument, NULL, ARG_BIND }, - { "bind-ro", required_argument, NULL, ARG_BIND_RO }, - { "extra-drive", required_argument, NULL, ARG_EXTRA_DRIVE }, - { "secure-boot", required_argument, NULL, ARG_SECURE_BOOT }, - { "private-users", required_argument, NULL, ARG_PRIVATE_USERS }, - { "forward-journal", required_argument, NULL, ARG_FORWARD_JOURNAL }, - { "pass-ssh-key", required_argument, NULL, ARG_PASS_SSH_KEY }, - { "ssh-key-type", required_argument, NULL, ARG_SSH_KEY_TYPE }, - { "set-credential", required_argument, NULL, ARG_SET_CREDENTIAL }, - { "load-credential", required_argument, NULL, ARG_LOAD_CREDENTIAL }, - { "firmware", required_argument, NULL, ARG_FIRMWARE }, - { "firmware-features", required_argument, NULL, ARG_FIRMWARE_FEATURES }, - { "discard-disk", required_argument, NULL, ARG_DISCARD_DISK }, - { "background", required_argument, NULL, ARG_BACKGROUND }, - { "smbios11", required_argument, NULL, 's' }, - { "grow-image", required_argument, NULL, 'G' }, - { "tpm-state", required_argument, NULL, ARG_TPM_STATE }, - { "efi-nvram-template", required_argument, NULL, ARG_EFI_NVRAM_TEMPLATE }, - { "efi-nvram-state", required_argument, NULL, ARG_EFI_NVRAM_STATE }, - { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD }, - { "property", required_argument, NULL, ARG_PROPERTY }, - { "notify-ready", required_argument, NULL, ARG_NOTIFY_READY }, - { "bind-user", required_argument, NULL, ARG_BIND_USER }, - { "bind-user-shell", required_argument, NULL, ARG_BIND_USER_SHELL }, - { "bind-user-group", required_argument, NULL, ARG_BIND_USER_GROUP }, - { "system", no_argument, NULL, ARG_SYSTEM }, - { "user", no_argument, NULL, ARG_USER }, - {} - }; - - int c; - assert(argc >= 0); assert(argv); - optind = 0; - while ((c = getopt_long(argc, argv, "+hD:i:xM:nqs:G:S:", options, NULL)) >= 0) + OptionParser state = { argc, argv, /* stop_at_first_nonoption= */ true }; + const Option *current; + const char *arg; + + FOREACH_OPTION_FULL(&state, c, ¤t, &arg, /* on_error= */ return c) switch (c) { - case 'h': + + OPTION_COMMON_HELP: return help(); - case ARG_VERSION: + OPTION_COMMON_VERSION: return version(); - case 'q': + OPTION('q', "quiet", NULL, "Do not show status information"): arg_quiet = true; break; - case 'D': - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_directory); - if (r < 0) - return r; - - break; - - case 'i': - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image); - if (r < 0) - return r; - - break; - - case ARG_IMAGE_FORMAT: - arg_image_format = image_format_from_string(optarg); - if (arg_image_format < 0) - return log_error_errno(arg_image_format, - "Invalid image format: %s", optarg); - break; - - case ARG_IMAGE_DISK_TYPE: - arg_image_disk_type = disk_type_from_string(optarg); - if (arg_image_disk_type < 0) - return log_error_errno(arg_image_disk_type, - "Invalid image disk type: %s", optarg); - break; - - case 'M': - if (isempty(optarg)) - arg_machine = mfree(arg_machine); - else { - if (!hostname_is_valid(optarg, 0)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Invalid machine name: %s", optarg); - - r = free_and_strdup(&arg_machine, optarg); - if (r < 0) - return log_oom(); - } - break; - - case 'x': - arg_ephemeral = true; - break; - - case ARG_NO_PAGER: + OPTION_COMMON_NO_PAGER: arg_pager_flags |= PAGER_DISABLE; break; - case ARG_CPUS: - r = free_and_strdup_warn(&arg_cpus, optarg); + OPTION_COMMON_NO_ASK_PASSWORD: + arg_ask_password = false; + break; + + OPTION_LONG("system", NULL, "Run in the system service manager scope"): + arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; + break; + + OPTION_LONG("user", NULL, "Run in the user service manager scope"): + arg_runtime_scope = RUNTIME_SCOPE_USER; + break; + + OPTION_GROUP("Image"): + break; + + OPTION('D', "directory", "PATH", "Root directory for the VM"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_directory); if (r < 0) return r; break; - case ARG_RAM: - r = parse_ram(optarg); + OPTION('x', "ephemeral", NULL, "Run VM with snapshot of the disk or directory"): + arg_ephemeral = true; + break; + + OPTION('i', "image", "FILE|DEVICE", "Root file system disk image or device for the VM"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_image); if (r < 0) return r; break; - case ARG_KVM: - r = parse_tristate_argument_with_auto("--kvm=", optarg, &arg_kvm); + OPTION_LONG("image-format", "FORMAT", "Specify disk image format (raw, qcow2; default: raw)"): + arg_image_format = image_format_from_string(arg); + if (arg_image_format < 0) + return log_error_errno(arg_image_format, + "Invalid image format: %s", arg); + break; + + OPTION_LONG("image-disk-type", "TYPE", + "Specify disk type (virtio-blk, virtio-scsi, nvme, scsi-cd; default: virtio-blk)"): + arg_image_disk_type = disk_type_from_string(arg); + if (arg_image_disk_type < 0) + return log_error_errno(arg_image_disk_type, + "Invalid image disk type: %s", arg); + break; + + OPTION_GROUP("Host Configuration"): + break; + + OPTION_LONG("cpus", "CPUS", "Configure number of CPUs in guest"): {} + OPTION_LONG("qemu-smp", "CPUS", /* help= */ NULL): /* Compat alias */ + r = free_and_strdup_warn(&arg_cpus, arg); if (r < 0) return r; break; - case ARG_VSOCK: - r = parse_tristate_argument_with_auto("--vsock=", optarg, &arg_vsock); + OPTION_LONG("ram", "BYTES[:MAXBYTES[:SLOTS]]", + "Configure guest's RAM size (and max/slots for hotplug)"): {} + OPTION_LONG("qemu-mem", "BYTES", /* help= */ NULL): /* Compat alias */ + r = parse_ram(arg); if (r < 0) return r; break; - case ARG_VSOCK_CID: - if (isempty(optarg)) + OPTION_LONG("kvm", "BOOL", "Enable use of KVM"): {} + OPTION_LONG("qemu-kvm", "BOOL", /* help= */ NULL): /* Compat alias */ + r = parse_tristate_argument_with_auto("--kvm=", arg, &arg_kvm); + if (r < 0) + return r; + break; + + OPTION_LONG("vsock", "BOOL", "Override autodetection of VSOCK support"): {} + OPTION_LONG("qemu-vsock", "BOOL", /* help= */ NULL): /* Compat alias */ + r = parse_tristate_argument_with_auto("--vsock=", arg, &arg_vsock); + if (r < 0) + return r; + break; + + OPTION_LONG("vsock-cid", "CID", "Specify the CID to use for the guest's VSOCK support"): + if (isempty(arg)) arg_vsock_cid = VMADDR_CID_ANY; else { unsigned cid; - r = vsock_parse_cid(optarg, &cid); + r = vsock_parse_cid(arg, &cid); if (r < 0) - return log_error_errno(r, "Failed to parse --vsock-cid: %s", optarg); + return log_error_errno(r, "Failed to parse --vsock-cid: %s", arg); if (!VSOCK_CID_IS_REGULAR(cid)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified CID is not regular, refusing: %u", cid); @@ -591,89 +437,302 @@ static int parse_argv(int argc, char *argv[]) { } break; - case ARG_TPM: - r = parse_tristate_argument_with_auto("--tpm=", optarg, &arg_tpm); + OPTION_LONG("tpm", "BOOL", "Enable use of a virtual TPM"): + r = parse_tristate_argument_with_auto("--tpm=", arg, &arg_tpm); if (r < 0) return r; break; - case ARG_LINUX: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_linux); + OPTION_LONG("tpm-state", "off|auto|PATH", "Where to store TPM state"): + r = isempty(arg) ? false : + streq(arg, "auto") ? true : + parse_boolean(arg); + if (r >= 0) { + arg_tpm_state_mode = r ? STATE_AUTO : STATE_OFF; + arg_tpm_state_path = mfree(arg_tpm_state_path); + break; + } + + if (!path_is_valid(arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid path in --tpm-state= parameter: %s", arg); + + if (!path_is_absolute(arg) && !startswith(arg, "./")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Path in --tpm-state= parameter must be absolute or start with './': %s", arg); + + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_tpm_state_path); + if (r < 0) + return r; + + arg_tpm_state_mode = STATE_PATH; + break; + + OPTION_LONG("efi-nvram-template", "PATH", "Set the path to the EFI NVRAM template file to use"): + if (!isempty(arg) && !path_is_absolute(arg) && !startswith(arg, "./")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Absolute path or path starting with './' required."); + + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_efi_nvram_template); if (r < 0) return r; break; - case ARG_INITRD: { + OPTION_LONG("efi-nvram-state", "off|auto|PATH", "Where to store EFI Variable NVRAM state"): + r = isempty(arg) ? false : + streq(arg, "auto") ? true : + parse_boolean(arg); + if (r >= 0) { + arg_efi_nvram_state_mode = r ? STATE_AUTO : STATE_OFF; + arg_efi_nvram_state_path = mfree(arg_efi_nvram_state_path); + break; + } + + if (!path_is_valid(arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid path in --efi-nvram-state= parameter: %s", arg); + + if (!path_is_absolute(arg) && !startswith(arg, "./")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Path in --efi-nvram-state= parameter must be absolute or start with './': %s", arg); + + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_efi_nvram_state_path); + if (r < 0) + return r; + + arg_efi_nvram_state_mode = STATE_PATH; + break; + + OPTION_LONG("linux", "PATH", "Specify the linux kernel for direct kernel boot"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_linux); + if (r < 0) + return r; + break; + + OPTION_LONG("initrd", "PATH", "Specify the initrd for direct kernel boot"): { _cleanup_free_ char *initrd_path = NULL; - r = parse_path_argument(optarg, /* suppress_root= */ false, &initrd_path); + r = parse_path_argument(arg, /* suppress_root= */ false, &initrd_path); if (r < 0) return r; r = strv_consume(&arg_initrds, TAKE_PTR(initrd_path)); if (r < 0) return log_oom(); - break; } - case ARG_CONSOLE: - arg_console_mode = console_mode_from_string(optarg); - if (arg_console_mode < 0) - return log_error_errno(arg_console_mode, "Failed to parse specified console mode: %s", optarg); - - break; - - case ARG_CONSOLE_TRANSPORT: - arg_console_transport = console_transport_from_string(optarg); - if (arg_console_transport < 0) - return log_error_errno(arg_console_transport, "Failed to parse specified console transport: %s", optarg); - - break; - - case ARG_QEMU_GUI: - arg_console_mode = CONSOLE_GUI; - break; - - case 'n': + OPTION('n', "network-tap", NULL, "Create a TAP device for networking"): arg_network_stack = NETWORK_STACK_TAP; break; - case ARG_NETWORK_USER_MODE: + OPTION_LONG("network-user-mode", NULL, "Use user mode networking"): arg_network_stack = NETWORK_STACK_USER; break; - case ARG_UUID: - r = id128_from_string_nonzero(optarg, &arg_uuid); - if (r == -ENXIO) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Machine UUID may not be all zeroes."); - if (r < 0) - return log_error_errno(r, "Invalid UUID: %s", optarg); + OPTION_LONG("secure-boot", "BOOL|auto", "Enable searching for firmware supporting SecureBoot"): { + int b; - break; - - case ARG_REGISTER: - r = parse_tristate_argument_with_auto("--register=", optarg, &arg_register); + r = parse_tristate_argument_with_auto("--secure-boot=", arg, &b); if (r < 0) return r; + free(set_remove(arg_firmware_features_include, "secure-boot")); + free(set_remove(arg_firmware_features_exclude, "secure-boot")); + + if (b >= 0) { + r = set_put_strdup(b > 0 ? &arg_firmware_features_include : &arg_firmware_features_exclude, "secure-boot"); + if (r < 0) + return log_oom(); + } + break; + } + + OPTION_LONG("firmware", "PATH|list|describe", + "Select firmware definition file (or list/describe available)"): + if (streq(arg, "list")) { + _cleanup_strv_free_ char **l = NULL; + + r = list_ovmf_config(&l); + if (r < 0) + return log_error_errno(r, "Failed to list firmwares: %m"); + + bool nl = false; + fputstrv(stdout, l, "\n", &nl); + if (nl) + putchar('\n'); + + return 0; + } + + if (streq(arg, "describe")) { + /* Handled after argument parsing so that --firmware-features= is + * taken into account. */ + arg_firmware = mfree(arg_firmware); + arg_firmware_describe = true; + break; + } + + arg_firmware_describe = false; + + if (!isempty(arg) && !path_is_absolute(arg) && !startswith(arg, "./")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Absolute path or path starting with './' required."); + + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_firmware); + if (r < 0) + return r; break; - case ARG_KEEP_UNIT: + OPTION_LONG("firmware-features", "FEATURE,...|list", + "Require/exclude specific firmware features"): { + if (isempty(arg)) { + arg_firmware_features_include = set_free(arg_firmware_features_include); + arg_firmware_features_exclude = set_free(arg_firmware_features_exclude); + break; + } + + if (streq(arg, "list")) { + _cleanup_strv_free_ char **l = NULL; + + r = list_ovmf_firmware_features(&l); + if (r < 0) + return log_error_errno(r, "Failed to list firmware features: %m"); + + bool nl = false; + fputstrv(stdout, l, "\n", &nl); + if (nl) + putchar('\n'); + + return 0; + } + + _cleanup_strv_free_ char **features = strv_split(arg, ","); + if (!features) + return log_oom(); + + STRV_FOREACH(feature, features) { + const char *e = startswith(*feature, "~"); + r = set_put_strdup(e ? &arg_firmware_features_exclude : &arg_firmware_features_include, e ?: *feature); + if (r < 0) + return log_oom(); + } + break; + } + + OPTION_LONG("discard-disk", "BOOL", "Control processing of discard requests"): + r = parse_boolean_argument("--discard-disk=", arg, &arg_discard_disk); + if (r < 0) + return r; + break; + + OPTION('G', "grow-image", "BYTES", "Grow image file to specified size in bytes"): + if (isempty(arg)) { + arg_grow_image = 0; + break; + } + + r = parse_size(arg, 1024, &arg_grow_image); + if (r < 0) + return log_error_errno(r, "Failed to parse --grow-image= parameter: %s", arg); + break; + + OPTION_GROUP("Execution"): + break; + + OPTION('s', "smbios11", "STRING", "Pass an arbitrary SMBIOS Type #11 string to the VM"): + if (isempty(arg)) { + arg_smbios11 = strv_free(arg_smbios11); + break; + } + + if (!utf8_is_valid(arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "SMBIOS Type 11 string is not UTF-8 clean, refusing: %s", arg); + + if (strv_extend(&arg_smbios11, arg) < 0) + return log_oom(); + break; + + OPTION_LONG("notify-ready", "BOOL", "Wait for ready notification from the VM"): + r = parse_boolean_argument("--notify-ready=", arg, &arg_notify_ready); + if (r < 0) + return r; + break; + + OPTION_GROUP("System Identity"): + break; + + OPTION('M', "machine", "NAME", "Set the machine name for the VM"): + if (isempty(arg)) + arg_machine = mfree(arg_machine); + else { + if (!hostname_is_valid(arg, 0)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid machine name: %s", arg); + + r = free_and_strdup(&arg_machine, arg); + if (r < 0) + return log_oom(); + } + break; + + OPTION_LONG("uuid", "UUID", "Set a specific machine UUID for the VM"): + r = id128_from_string_nonzero(arg, &arg_uuid); + if (r == -ENXIO) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Machine UUID may not be all zeroes."); + if (r < 0) + return log_error_errno(r, "Invalid UUID: %s", arg); + break; + + OPTION_GROUP("Properties"): + break; + + OPTION('S', "slice", "SLICE", "Place the VM in the specified slice"): { + _cleanup_free_ char *mangled = NULL; + + r = unit_name_mangle_with_suffix(arg, /* operation= */ NULL, UNIT_NAME_MANGLE_WARN, ".slice", &mangled); + if (r < 0) + return log_error_errno(r, "Failed to turn '%s' into unit name: %m", arg); + + free_and_replace(arg_slice, mangled); + break; + } + + OPTION_LONG("property", "NAME=VALUE", "Set scope unit property"): + if (strv_extend(&arg_property, arg) < 0) + return log_oom(); + break; + + OPTION_LONG("register", "BOOLEAN", "Register VM as machine"): + r = parse_tristate_argument_with_auto("--register=", arg, &arg_register); + if (r < 0) + return r; + break; + + OPTION_LONG("keep-unit", NULL, + "Do not register a scope for the machine, reuse the service unit vmspawn is running in"): arg_keep_unit = true; break; - case ARG_BIND: - case ARG_BIND_RO: - r = runtime_mount_parse(&arg_runtime_mounts, optarg, c == ARG_BIND_RO); - if (r < 0) - return log_error_errno(r, "Failed to parse --bind(-ro)= argument %s: %m", optarg); - + OPTION_GROUP("User Namespacing"): break; - case ARG_EXTRA_DRIVE: { + OPTION_LONG("private-users", "UIDBASE[:NUIDS]", + "Configure the UID/GID range to map into the virtiofsd namespace"): + r = parse_userns_uid_range(arg, &arg_uid_shift, &arg_uid_range); + if (r < 0) + return r; + break; + + OPTION_GROUP("Mounts"): + break; + + OPTION_LONG("bind", "SOURCE[:TARGET]", "Mount a file or directory from the host into the VM"): {} + OPTION_LONG("bind-ro", "SOURCE[:TARGET]", "Mount a file or directory, but read-only"): { + bool read_only = streq(current->long_code, "bind-ro"); + r = runtime_mount_parse(&arg_runtime_mounts, arg, read_only); + if (r < 0) + return log_error_errno(r, "Failed to parse --%s= argument %s: %m", current->long_code, arg); + break; + } + + OPTION_LONG("extra-drive", "[FORMAT:][DISKTYPE:]PATH", "Adds an additional disk to the VM"): { ImageFormat format = IMAGE_FORMAT_RAW; DiskType extra_disk_type = _DISK_TYPE_INVALID; - const char *dp = optarg; + const char *dp = arg; /* Parse optional colon-separated prefixes. The format and disk type * value sets don't overlap, so they can appear in any order. */ @@ -717,310 +776,105 @@ static int parse_argv(int argc, char *argv[]) { .format = format, .disk_type = extra_disk_type, }; - break; } - case ARG_SECURE_BOOT: { - int b; + OPTION_LONG("bind-user", "NAME", "Bind user from host to virtual machine"): + if (!valid_user_group_name(arg, /* flags= */ 0)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid user name to bind: %s", arg); - r = parse_tristate_argument_with_auto("--secure-boot=", optarg, &b); - if (r < 0) - return r; - - free(set_remove(arg_firmware_features_include, "secure-boot")); - free(set_remove(arg_firmware_features_exclude, "secure-boot")); - - if (b >= 0) { - r = set_put_strdup(b > 0 ? &arg_firmware_features_include : &arg_firmware_features_exclude, "secure-boot"); - if (r < 0) - return log_oom(); - } - - break; - } - - case ARG_PRIVATE_USERS: - r = parse_userns_uid_range(optarg, &arg_uid_shift, &arg_uid_range); - if (r < 0) - return r; - break; - - case ARG_FORWARD_JOURNAL: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_forward_journal); - if (r < 0) - return r; - break; - - case ARG_PASS_SSH_KEY: - r = parse_boolean_argument("--pass-ssh-key=", optarg, &arg_pass_ssh_key); - if (r < 0) - return r; - break; - - case ARG_SSH_KEY_TYPE: - if (!string_is_safe(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid value for --arg-ssh-key-type=: %s", optarg); - - r = free_and_strdup_warn(&arg_ssh_key_type, optarg); - if (r < 0) - return r; - break; - - case ARG_SET_CREDENTIAL: { - r = machine_credential_set(&arg_credentials, optarg); - if (r < 0) - return r; - break; - } - - case ARG_LOAD_CREDENTIAL: { - r = machine_credential_load(&arg_credentials, optarg); - if (r < 0) - return r; - - break; - } - - case ARG_FIRMWARE: - if (streq(optarg, "list")) { - _cleanup_strv_free_ char **l = NULL; - - r = list_ovmf_config(&l); - if (r < 0) - return log_error_errno(r, "Failed to list firmwares: %m"); - - bool nl = false; - fputstrv(stdout, l, "\n", &nl); - if (nl) - putchar('\n'); - - return 0; - } - - if (streq(optarg, "describe")) { - /* Handled after argument parsing so that --firmware-features= is - * taken into account. */ - arg_firmware = mfree(arg_firmware); - arg_firmware_describe = true; - break; - } - - arg_firmware_describe = false; - - if (!isempty(optarg) && !path_is_absolute(optarg) && !startswith(optarg, "./")) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Absolute path or path starting with './' required."); - - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_firmware); - if (r < 0) - return r; - - break; - - case ARG_FIRMWARE_FEATURES: { - if (isempty(optarg)) { - arg_firmware_features_include = set_free(arg_firmware_features_include); - arg_firmware_features_exclude = set_free(arg_firmware_features_exclude); - break; - } - - if (streq(optarg, "list")) { - _cleanup_strv_free_ char **l = NULL; - - r = list_ovmf_firmware_features(&l); - if (r < 0) - return log_error_errno(r, "Failed to list firmware features: %m"); - - bool nl = false; - fputstrv(stdout, l, "\n", &nl); - if (nl) - putchar('\n'); - - return 0; - } - - _cleanup_strv_free_ char **features = strv_split(optarg, ","); - if (!features) + if (strv_extend(&arg_bind_user, arg) < 0) return log_oom(); - - STRV_FOREACH(feature, features) { - const char *e = startswith(*feature, "~"); - r = set_put_strdup(e ? &arg_firmware_features_exclude : &arg_firmware_features_include, e ?: *feature); - if (r < 0) - return log_oom(); - } - - break; - } - - case ARG_DISCARD_DISK: - r = parse_boolean_argument("--discard-disk=", optarg, &arg_discard_disk); - if (r < 0) - return r; break; - case ARG_BACKGROUND: - r = parse_background_argument(optarg, &arg_background); - if (r < 0) - return r; - break; - - case 's': - if (isempty(optarg)) { - arg_smbios11 = strv_free(arg_smbios11); - break; - } - - if (!utf8_is_valid(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "SMBIOS Type 11 string is not UTF-8 clean, refusing: %s", optarg); - - if (strv_extend(&arg_smbios11, optarg) < 0) - return log_oom(); - - break; - - case 'G': - if (isempty(optarg)) { - arg_grow_image = 0; - break; - } - - r = parse_size(optarg, 1024, &arg_grow_image); - if (r < 0) - return log_error_errno(r, "Failed to parse --grow-image= parameter: %s", optarg); - - break; - - case ARG_TPM_STATE: - r = isempty(optarg) ? false : - streq(optarg, "auto") ? true : - parse_boolean(optarg); - if (r >= 0) { - arg_tpm_state_mode = r ? STATE_AUTO : STATE_OFF; - arg_tpm_state_path = mfree(arg_tpm_state_path); - break; - } - - if (!path_is_valid(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid path in --tpm-state= parameter: %s", optarg); - - if (!path_is_absolute(optarg) && !startswith(optarg, "./")) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Path in --tpm-state= parameter must be absolute or start with './': %s", optarg); - - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm_state_path); - if (r < 0) - return r; - - arg_tpm_state_mode = STATE_PATH; - break; - - case ARG_EFI_NVRAM_TEMPLATE: - if (!isempty(optarg) && !path_is_absolute(optarg) && !startswith(optarg, "./")) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Absolute path or path starting with './' required."); - - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_efi_nvram_template); - if (r < 0) - return r; - - break; - - case ARG_EFI_NVRAM_STATE: - r = isempty(optarg) ? false : - streq(optarg, "auto") ? true : - parse_boolean(optarg); - if (r >= 0) { - arg_efi_nvram_state_mode = r ? STATE_AUTO : STATE_OFF; - arg_efi_nvram_state_path = mfree(arg_efi_nvram_state_path); - break; - } - - if (!path_is_valid(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid path in --efi-nvram-state= parameter: %s", optarg); - - if (!path_is_absolute(optarg) && !startswith(optarg, "./")) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Path in --efi-nvram-state= parameter must be absolute or start with './': %s", optarg); - - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_efi_nvram_state_path); - if (r < 0) - return r; - - arg_efi_nvram_state_mode = STATE_PATH; - break; - - case ARG_NO_ASK_PASSWORD: - arg_ask_password = false; - break; - - case 'S': { - _cleanup_free_ char *mangled = NULL; - - r = unit_name_mangle_with_suffix(optarg, /* operation= */ NULL, UNIT_NAME_MANGLE_WARN, ".slice", &mangled); - if (r < 0) - return log_error_errno(r, "Failed to turn '%s' into unit name: %m", optarg); - - free_and_replace(arg_slice, mangled); - break; - } - - case ARG_PROPERTY: - if (strv_extend(&arg_property, optarg) < 0) - return log_oom(); - - break; - - case ARG_NOTIFY_READY: - r = parse_boolean_argument("--notify-ready=", optarg, &arg_notify_ready); - if (r < 0) - return r; - - break; - - case ARG_BIND_USER: - if (!valid_user_group_name(optarg, /* flags= */ 0)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid user name to bind: %s", optarg); - - if (strv_extend(&arg_bind_user, optarg) < 0) - return log_oom(); - - break; - - case ARG_BIND_USER_SHELL: { + OPTION_LONG("bind-user-shell", "BOOL|PATH", + "Configure the shell to use for --bind-user= users"): { bool copy = false; char *sh = NULL; - r = parse_user_shell(optarg, &sh, ©); + r = parse_user_shell(arg, &sh, ©); if (r == -ENOMEM) return log_oom(); if (r < 0) - return log_error_errno(r, "Invalid user shell to bind: %s", optarg); + return log_error_errno(r, "Invalid user shell to bind: %s", arg); free_and_replace(arg_bind_user_shell, sh); arg_bind_user_shell_copy = copy; - break; } - case ARG_BIND_USER_GROUP: - if (!valid_user_group_name(optarg, /* flags= */ 0)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid bind user auxiliary group name: %s", optarg); + OPTION_LONG("bind-user-group", "GROUP", "Add an auxiliary group to --bind-user= users"): + if (!valid_user_group_name(arg, /* flags= */ 0)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid bind user auxiliary group name: %s", arg); - if (strv_extend(&arg_bind_user_groups, optarg) < 0) + if (strv_extend(&arg_bind_user_groups, arg) < 0) return log_oom(); - break; - case ARG_SYSTEM: - arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; + OPTION_GROUP("Integration"): break; - case ARG_USER: - arg_runtime_scope = RUNTIME_SCOPE_USER; + OPTION_LONG("forward-journal", "FILE|DIR", "Forward the VM's journal to the host"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_forward_journal); + if (r < 0) + return r; break; - case '?': - return -EINVAL; + OPTION_LONG("pass-ssh-key", "BOOL", "Create an SSH key to access the VM"): + r = parse_boolean_argument("--pass-ssh-key=", arg, &arg_pass_ssh_key); + if (r < 0) + return r; + break; - default: - assert_not_reached(); + OPTION_LONG("ssh-key-type", "TYPE", "Choose what type of SSH key to pass"): + if (!string_is_safe(arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid value for --ssh-key-type=: %s", arg); + + r = free_and_strdup_warn(&arg_ssh_key_type, arg); + if (r < 0) + return r; + break; + + OPTION_GROUP("Input/Output"): + break; + + OPTION_LONG("console", "MODE", + "Console mode (interactive, native, gui, read-only or headless)"): + arg_console_mode = console_mode_from_string(arg); + if (arg_console_mode < 0) + return log_error_errno(arg_console_mode, "Failed to parse specified console mode: %s", arg); + break; + + OPTION_LONG("console-transport", "TRANSPORT", "Console transport (virtio or serial)"): + arg_console_transport = console_transport_from_string(arg); + if (arg_console_transport < 0) + return log_error_errno(arg_console_transport, "Failed to parse specified console transport: %s", arg); + break; + + OPTION_LONG("qemu-gui", NULL, /* help= */ NULL): /* Compat alias */ + arg_console_mode = CONSOLE_GUI; + break; + + OPTION_LONG("background", "COLOR", "Set ANSI color for background"): + r = parse_background_argument(arg, &arg_background); + if (r < 0) + return r; + break; + + OPTION_GROUP("Credentials"): + break; + + OPTION_LONG("set-credential", "ID:VALUE", "Pass a credential with literal value to the VM"): + r = machine_credential_set(&arg_credentials, arg); + if (r < 0) + return r; + break; + + OPTION_LONG("load-credential", "ID:PATH", + "Load credential for the VM from file or AF_UNIX stream socket"): + r = machine_credential_load(&arg_credentials, arg); + if (r < 0) + return r; + break; } /* Drop duplicate --bind-user= and --bind-user-group= entries */ @@ -1058,8 +912,9 @@ static int parse_argv(int argc, char *argv[]) { arg_uid_range = 0x10000; } - if (argc > optind) { - arg_kernel_cmdline_extra = strv_copy(argv + optind); + char **args = option_parser_get_args(&state); + if (!strv_isempty(args)) { + arg_kernel_cmdline_extra = strv_copy(args); if (!arg_kernel_cmdline_extra) return log_oom(); } From bf5bc9a7b26191ff4254836bd909cbb92eafe480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 7 Apr 2026 17:13:46 +0200 Subject: [PATCH 3/5] nspawn: convert to the new option parser Uses stop_at_first_nonoption for POSIX-style option parsing. Includes a fixup for b4df0a9ee62d553e21f3b70c28841cfd1b8736f1, where global optarg was used instead of the function param. This made no difference previously because they were always equal. Co-developed-by: Claude Opus 4.6 --- src/nspawn/nspawn.c | 1526 ++++++++++++++++++------------------------- 1 file changed, 637 insertions(+), 889 deletions(-) diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 006e91caa9..153babe67c 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include #include @@ -50,6 +49,7 @@ #include "fd-util.h" #include "fdset.h" #include "fileio.h" +#include "format-table.h" #include "format-util.h" #include "fs-util.h" #include "gpt.h" @@ -90,6 +90,7 @@ #include "nsresource.h" #include "os-util.h" #include "osc-context.h" +#include "options.h" #include "pager.h" #include "parse-argument.h" #include "parse-util.h" @@ -346,7 +347,7 @@ static int parse_private_users( *ret_uid_shift = 0; *ret_uid_range = UINT32_C(0x10000); - } else if (streq(optarg, "managed")) { + } else if (streq(s, "managed")) { /* managed: User namespace on, and acquire it from systemd-nsresourced */ *ret_userns_mode = USER_NAMESPACE_MANAGED; *ret_uid_shift = UID_INVALID; @@ -354,7 +355,7 @@ static int parse_private_users( } else { /* anything else: User namespacing on, UID range is explicitly configured */ - r = parse_userns_uid_range(optarg, ret_uid_shift, ret_uid_range); + r = parse_userns_uid_range(s, ret_uid_shift, ret_uid_range); if (r < 0) return r; *ret_userns_mode = USER_NAMESPACE_FIXED; @@ -363,6 +364,11 @@ static int parse_private_users( return 0; } +static void unref_many_tables(Table* (*tablesp)[]) { + for (Table **t = *ASSERT_PTR(tablesp); *t; t++) + *t = table_unref(*t); +} + static int help(void) { _cleanup_free_ char *link = NULL; int r; @@ -373,166 +379,55 @@ static int help(void) { if (r < 0) return log_oom(); - printf("%1$s [OPTIONS...] [PATH] [ARGUMENTS...]\n\n" - "%5$sSpawn a command or OS in a lightweight container.%6$s\n\n" - " -h --help Show this help\n" - " --version Print version string\n" - " -q --quiet Do not show status information\n" - " --no-pager Do not pipe output into a pager\n" - " --settings=BOOLEAN Load additional settings from .nspawn file\n" - " --cleanup Clean up left-over mounts and underlying mount\n" - " points used by the container\n" - " --no-ask-password Do not prompt for password\n" - "\n%3$sImage:%4$s\n" - " -D --directory=PATH Root directory for the container\n" - " --template=PATH Initialize root directory from template directory,\n" - " if missing\n" - " -x --ephemeral Run container with snapshot of root directory, and\n" - " remove it after exit\n" - " -i --image=PATH Root file system disk image (or device node) for\n" - " the container\n" - " --image-policy=POLICY Specify disk image dissection policy\n" - " --oci-bundle=PATH OCI bundle directory\n" - " --read-only Mount the root directory read-only\n" - " --volatile[=MODE] Run the system in volatile mode\n" - " --root-hash=HASH Specify verity root hash for root disk image\n" - " --root-hash-sig=SIG Specify pkcs7 signature of root hash for verity\n" - " as a DER encoded PKCS7, either as a path to a file\n" - " or as an ASCII base64 encoded string prefixed by\n" - " 'base64:'\n" - " --verity-data=PATH Specify hash device for verity\n" - " --pivot-root=PATH[:PATH]\n" - " Pivot root to given directory in the container\n" - "\n%3$sExecution:%4$s\n" - " -a --as-pid2 Maintain a stub init as PID1, invoke binary as PID2\n" - " -b --boot Boot up full system (i.e. invoke init)\n" - " --chdir=PATH Set working directory in the container\n" - " -E --setenv=NAME[=VALUE] Pass an environment variable to PID 1\n" - " -u --uid=USER Run the command under specified user or UID\n" - " --kill-signal=SIGNAL Select signal to use for shutting down PID 1\n" - " --notify-ready=BOOLEAN Receive notifications from the child init process\n" - " --suppress-sync=BOOLEAN\n" - " Suppress any form of disk data synchronization\n" - "\n%3$sSystem Identity:%4$s\n" - " -M --machine=NAME Set the machine name for the container\n" - " --hostname=NAME Override the hostname for the container\n" - " --uuid=UUID Set a specific machine UUID for the container\n" - "\n%3$sProperties:%4$s\n" - " -S --slice=SLICE Place the container in the specified slice\n" - " --property=NAME=VALUE Set scope unit property\n" - " --register=BOOLEAN Register container as machine\n" - " --keep-unit Do not register a scope for the machine, reuse\n" - " the service unit nspawn is running in\n" - "\n%3$sUser Namespacing:%4$s\n" - " --private-users=no Run without user namespacing\n" - " --private-users=yes|pick|identity|managed\n" - " Run within user namespace, autoselect UID/GID range\n" - " --private-users=UIDBASE[:NUIDS]\n" - " Similar, but with user configured UID/GID range\n" - " --private-users-ownership=MODE\n" - " Adjust ('chown') or map ('map') OS tree ownership\n" - " to private UID/GID range\n" - " --private-users-delegate=N\n" - " Delegate N additional 64K UID/GID ranges for use\n" - " by nested containers (requires managed user\n" - " namespaces)\n" - " -U Equivalent to --private-users=pick and\n" - " --private-users-ownership=auto\n" - "\n%3$sNetworking:%4$s\n" - " --private-network Disable network in container\n" - " --network-interface=HOSTIF[:CONTAINERIF]\n" - " Assign an existing network interface to the\n" - " container\n" - " --network-macvlan=HOSTIF[:CONTAINERIF]\n" - " Create a macvlan network interface based on an\n" - " existing network interface to the container\n" - " --network-ipvlan=HOSTIF[:CONTAINERIF]\n" - " Create an ipvlan network interface based on an\n" - " existing network interface to the container\n" - " -n --network-veth Add a virtual Ethernet connection between host\n" - " and container\n" - " --network-veth-extra=HOSTIF[:CONTAINERIF]\n" - " Add an additional virtual Ethernet link between\n" - " host and container\n" - " --network-bridge=INTERFACE\n" - " Add a virtual Ethernet connection to the container\n" - " and attach it to an existing bridge on the host\n" - " --network-zone=NAME Similar, but attach the new interface to an\n" - " automatically managed bridge interface\n" - " --network-namespace-path=PATH\n" - " Set network namespace to the one represented by\n" - " the specified kernel namespace file node\n" - " -p --port=[PROTOCOL:]HOSTPORT[:CONTAINERPORT]\n" - " Expose a container IP port on the host\n" - "\n%3$sSecurity:%4$s\n" - " --capability=CAP In addition to the default, retain specified\n" - " capability\n" - " --drop-capability=CAP Drop the specified capability from the default set\n" - " --ambient-capability=CAP\n" - " Sets the specified capability for the started\n" - " process. Not useful if booting a machine.\n" - " --no-new-privileges Set PR_SET_NO_NEW_PRIVS flag for container payload\n" - " --system-call-filter=LIST|~LIST\n" - " Permit/prohibit specific system calls\n" - " -Z --selinux-context=SECLABEL\n" - " Set the SELinux security context to be used by\n" - " processes in the container\n" - " -L --selinux-apifs-context=SECLABEL\n" - " Set the SELinux security context to be used by\n" - " API/tmpfs file systems in the container\n" - "\n%3$sResources:%4$s\n" - " --rlimit=NAME=LIMIT Set a resource limit for the payload\n" - " --oom-score-adjust=VALUE\n" - " Adjust the OOM score value for the payload\n" - " --cpu-affinity=CPUS Adjust the CPU affinity of the container\n" - " --personality=ARCH Pick personality for this container\n" - "\n%3$sIntegration:%4$s\n" - " --resolv-conf=MODE Select mode of /etc/resolv.conf initialization\n" - " --timezone=MODE Select mode of /etc/localtime initialization\n" - " --link-journal=MODE Link up guest journal, one of no, auto, guest, \n" - " host, try-guest, try-host\n" - " -j Equivalent to --link-journal=try-guest\n" - "\n%3$sMounts:%4$s\n" - " --bind=PATH[:PATH[:OPTIONS]]\n" - " Bind mount a file or directory from the host into\n" - " the container\n" - " --bind-ro=PATH[:PATH[:OPTIONS]\n" - " Similar, but creates a read-only bind mount\n" - " --inaccessible=PATH Over-mount file node with inaccessible node to mask\n" - " it\n" - " --tmpfs=PATH:[OPTIONS] Mount an empty tmpfs to the specified directory\n" - " --overlay=PATH[:PATH...]:PATH\n" - " Create an overlay mount from the host to \n" - " the container\n" - " --overlay-ro=PATH[:PATH...]:PATH\n" - " Similar, but creates a read-only overlay mount\n" - " --bind-user=NAME Bind user from host to container\n" - " --bind-user-shell=BOOL|PATH\n" - " Configure the shell to use for --bind-user= users\n" - " --bind-user-group=GROUP\n" - " Add an auxiliary group to --bind-user= users\n" - "\n%3$sInput/Output:%4$s\n" - " --console=MODE Select how stdin/stdout/stderr and /dev/console are\n" - " set up for the container.\n" - " -P --pipe Equivalent to --console=pipe\n" - " --background=COLOR Set ANSI color for background\n" - "\n%3$sCredentials:%4$s\n" - " --set-credential=ID:VALUE\n" - " Pass a credential with literal value to container.\n" - " --load-credential=ID:PATH\n" - " Load credential to pass to container from file or\n" - " AF_UNIX stream socket.\n" - "\n%3$sOther:%4$s\n" - " --system Run in the system service manager scope\n" - " --user Run in the user service manager scope\n" - "\nSee the %2$s for details.\n", + static const char *groups[] = { + NULL, + "Image", + "Execution", + "System Identity", + "Properties", + "User Namespacing", + "Networking", + "Security", + "Resources", + "Integration", + "Mounts", + "Input/Output", + "Credentials", + "Other", + }; + + _cleanup_(unref_many_tables) Table* tables[ELEMENTSOF(groups) + 1] = {}; + + for (size_t i = 0; i < ELEMENTSOF(groups); i++) { + r = option_parser_get_help_table_group(groups[i], &tables[i]); + if (r < 0) + return r; + } + + (void) table_sync_column_widths(0, tables[0], tables[1], tables[2], tables[3], + tables[4], tables[5], tables[6], tables[7], + tables[8], tables[9], tables[10], tables[11], + tables[12], tables[13]); + + printf("%s [OPTIONS...] [PATH] [ARGUMENTS...]\n\n" + "%sSpawn a command or OS in a lightweight container.%s\n\n", program_invocation_short_name, - link, - ansi_underline(), - ansi_normal(), ansi_highlight(), ansi_normal()); + r = table_print_or_warn(tables[0]); + if (r < 0) + return r; + + for (size_t i = 1; i < ELEMENTSOF(groups); i++) { + printf("\n%s%s:%s\n", ansi_underline(), groups[i], ansi_normal()); + + r = table_print_or_warn(tables[i]); + if (r < 0) + return r; + } + + printf("\nSee the %s for details.\n", link); return 0; } @@ -689,159 +584,7 @@ static int parse_environment(void) { } static int parse_argv(int argc, char *argv[]) { - enum { - ARG_VERSION = 0x100, - ARG_PRIVATE_NETWORK, - ARG_UUID, - ARG_READ_ONLY, - ARG_CAPABILITY, - ARG_AMBIENT_CAPABILITY, - ARG_DROP_CAPABILITY, - ARG_LINK_JOURNAL, - ARG_BIND, - ARG_BIND_RO, - ARG_TMPFS, - ARG_OVERLAY, - ARG_OVERLAY_RO, - ARG_INACCESSIBLE, - ARG_SHARE_SYSTEM, - ARG_REGISTER, - ARG_KEEP_UNIT, - ARG_NETWORK_INTERFACE, - ARG_NETWORK_MACVLAN, - ARG_NETWORK_IPVLAN, - ARG_NETWORK_BRIDGE, - ARG_NETWORK_ZONE, - ARG_NETWORK_VETH_EXTRA, - ARG_NETWORK_NAMESPACE_PATH, - ARG_PERSONALITY, - ARG_VOLATILE, - ARG_TEMPLATE, - ARG_PROPERTY, - ARG_PRIVATE_USERS, - ARG_PRIVATE_USERS_DELEGATE, - ARG_KILL_SIGNAL, - ARG_SETTINGS, - ARG_CHDIR, - ARG_PIVOT_ROOT, - ARG_PRIVATE_USERS_CHOWN, - ARG_PRIVATE_USERS_OWNERSHIP, - ARG_NOTIFY_READY, - ARG_ROOT_HASH, - ARG_ROOT_HASH_SIG, - ARG_VERITY_DATA, - ARG_SYSTEM_CALL_FILTER, - ARG_RLIMIT, - ARG_HOSTNAME, - ARG_NO_NEW_PRIVILEGES, - ARG_OOM_SCORE_ADJUST, - ARG_CPU_AFFINITY, - ARG_RESOLV_CONF, - ARG_TIMEZONE, - ARG_CONSOLE, - ARG_PIPE, - ARG_OCI_BUNDLE, - ARG_NO_PAGER, - ARG_SET_CREDENTIAL, - ARG_LOAD_CREDENTIAL, - ARG_BIND_USER, - ARG_BIND_USER_SHELL, - ARG_BIND_USER_GROUP, - ARG_SUPPRESS_SYNC, - ARG_IMAGE_POLICY, - ARG_BACKGROUND, - ARG_CLEANUP, - ARG_NO_ASK_PASSWORD, - ARG_MSTACK, - ARG_USER, - ARG_SYSTEM, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "directory", required_argument, NULL, 'D' }, - { "template", required_argument, NULL, ARG_TEMPLATE }, - { "ephemeral", no_argument, NULL, 'x' }, - { "uid", required_argument, NULL, 'u' }, - { "user", optional_argument, NULL, ARG_USER }, - { "private-network", no_argument, NULL, ARG_PRIVATE_NETWORK }, - { "as-pid2", no_argument, NULL, 'a' }, - { "boot", no_argument, NULL, 'b' }, - { "uuid", required_argument, NULL, ARG_UUID }, - { "read-only", no_argument, NULL, ARG_READ_ONLY }, - { "capability", required_argument, NULL, ARG_CAPABILITY }, - { "ambient-capability", required_argument, NULL, ARG_AMBIENT_CAPABILITY }, - { "drop-capability", required_argument, NULL, ARG_DROP_CAPABILITY }, - { "no-new-privileges", required_argument, NULL, ARG_NO_NEW_PRIVILEGES }, - { "link-journal", required_argument, NULL, ARG_LINK_JOURNAL }, - { "bind", required_argument, NULL, ARG_BIND }, - { "bind-ro", required_argument, NULL, ARG_BIND_RO }, - { "tmpfs", required_argument, NULL, ARG_TMPFS }, - { "overlay", required_argument, NULL, ARG_OVERLAY }, - { "overlay-ro", required_argument, NULL, ARG_OVERLAY_RO }, - { "inaccessible", required_argument, NULL, ARG_INACCESSIBLE }, - { "machine", required_argument, NULL, 'M' }, - { "hostname", required_argument, NULL, ARG_HOSTNAME }, - { "slice", required_argument, NULL, 'S' }, - { "setenv", required_argument, NULL, 'E' }, - { "selinux-context", required_argument, NULL, 'Z' }, - { "selinux-apifs-context", required_argument, NULL, 'L' }, - { "quiet", no_argument, NULL, 'q' }, - { "share-system", no_argument, NULL, ARG_SHARE_SYSTEM }, /* not documented */ - { "register", required_argument, NULL, ARG_REGISTER }, - { "keep-unit", no_argument, NULL, ARG_KEEP_UNIT }, - { "network-interface", required_argument, NULL, ARG_NETWORK_INTERFACE }, - { "network-macvlan", required_argument, NULL, ARG_NETWORK_MACVLAN }, - { "network-ipvlan", required_argument, NULL, ARG_NETWORK_IPVLAN }, - { "network-veth", no_argument, NULL, 'n' }, - { "network-veth-extra", required_argument, NULL, ARG_NETWORK_VETH_EXTRA }, - { "network-bridge", required_argument, NULL, ARG_NETWORK_BRIDGE }, - { "network-zone", required_argument, NULL, ARG_NETWORK_ZONE }, - { "network-namespace-path", required_argument, NULL, ARG_NETWORK_NAMESPACE_PATH }, - { "personality", required_argument, NULL, ARG_PERSONALITY }, - { "image", required_argument, NULL, 'i' }, - { "volatile", optional_argument, NULL, ARG_VOLATILE }, - { "port", required_argument, NULL, 'p' }, - { "property", required_argument, NULL, ARG_PROPERTY }, - { "private-users", optional_argument, NULL, ARG_PRIVATE_USERS }, - { "private-users-chown", optional_argument, NULL, ARG_PRIVATE_USERS_CHOWN }, /* obsolete */ - { "private-users-ownership",required_argument, NULL, ARG_PRIVATE_USERS_OWNERSHIP}, - { "private-users-delegate", required_argument, NULL, ARG_PRIVATE_USERS_DELEGATE }, - { "kill-signal", required_argument, NULL, ARG_KILL_SIGNAL }, - { "settings", required_argument, NULL, ARG_SETTINGS }, - { "chdir", required_argument, NULL, ARG_CHDIR }, - { "pivot-root", required_argument, NULL, ARG_PIVOT_ROOT }, - { "notify-ready", required_argument, NULL, ARG_NOTIFY_READY }, - { "root-hash", required_argument, NULL, ARG_ROOT_HASH }, - { "root-hash-sig", required_argument, NULL, ARG_ROOT_HASH_SIG }, - { "verity-data", required_argument, NULL, ARG_VERITY_DATA }, - { "system-call-filter", required_argument, NULL, ARG_SYSTEM_CALL_FILTER }, - { "rlimit", required_argument, NULL, ARG_RLIMIT }, - { "oom-score-adjust", required_argument, NULL, ARG_OOM_SCORE_ADJUST }, - { "cpu-affinity", required_argument, NULL, ARG_CPU_AFFINITY }, - { "resolv-conf", required_argument, NULL, ARG_RESOLV_CONF }, - { "timezone", required_argument, NULL, ARG_TIMEZONE }, - { "console", required_argument, NULL, ARG_CONSOLE }, - { "pipe", no_argument, NULL, ARG_PIPE }, - { "oci-bundle", required_argument, NULL, ARG_OCI_BUNDLE }, - { "no-pager", no_argument, NULL, ARG_NO_PAGER }, - { "set-credential", required_argument, NULL, ARG_SET_CREDENTIAL }, - { "load-credential", required_argument, NULL, ARG_LOAD_CREDENTIAL }, - { "bind-user", required_argument, NULL, ARG_BIND_USER }, - { "bind-user-shell", required_argument, NULL, ARG_BIND_USER_SHELL }, - { "bind-user-group", required_argument, NULL, ARG_BIND_USER_GROUP }, - { "suppress-sync", required_argument, NULL, ARG_SUPPRESS_SYNC }, - { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, - { "background", required_argument, NULL, ARG_BACKGROUND }, - { "cleanup", no_argument, NULL, ARG_CLEANUP }, - { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD }, - { "mstack", required_argument, NULL, ARG_MSTACK }, - { "system", no_argument, NULL, ARG_SYSTEM }, - {} - }; - - int c, r; + int r; uint64_t plus = 0, minus = 0; bool mask_all_settings = false, mask_no_settings = false; @@ -885,465 +628,47 @@ static int parse_argv(int argc, char *argv[]) { argc--; } - /* Resetting to 0 forces the invocation of an internal initialization routine of getopt_long() - * that checks for GNU extensions in optstring ('-' or '+' at the beginning). */ - optind = 0; - while ((c = getopt_long(argc, argv, "+hD:u:abL:M:jS:Z:qi:xp:nUE:P", options, NULL)) >= 0) + OptionParser state = { argc, argv, /* stop_at_first_nonoption= */ true }; + const Option *opt; + const char *arg; + + FOREACH_OPTION_FULL(&state, c, &opt, &arg, /* on_error= */ return c) { switch (c) { - case 'h': + OPTION_COMMON_HELP: return help(); - case ARG_VERSION: + OPTION_COMMON_VERSION: return version(); - case 'D': - r = parse_path_argument(optarg, false, &arg_directory); - if (r < 0) - return r; - - arg_settings_mask |= SETTING_DIRECTORY; - break; - - case ARG_TEMPLATE: - r = parse_path_argument(optarg, false, &arg_template); - if (r < 0) - return r; - - arg_settings_mask |= SETTING_DIRECTORY; - break; - - case 'i': - r = parse_path_argument(optarg, false, &arg_image); - if (r < 0) - return r; - - arg_settings_mask |= SETTING_DIRECTORY; - break; - - case ARG_MSTACK: - r = parse_path_argument(optarg, false, &arg_mstack); - if (r < 0) - return r; - - arg_settings_mask |= SETTING_DIRECTORY; - break; - - case ARG_OCI_BUNDLE: - r = parse_path_argument(optarg, false, &arg_oci_bundle); - if (r < 0) - return r; - - break; - - case 'x': - arg_ephemeral = true; - arg_settings_mask |= SETTING_EPHEMERAL; - break; - - case 'u': - r = free_and_strdup(&arg_user, optarg); - if (r < 0) - return log_oom(); - - arg_settings_mask |= SETTING_USER; - break; - - case ARG_NETWORK_ZONE: { - _cleanup_free_ char *j = NULL; - - j = strjoin("vz-", optarg); - if (!j) - return log_oom(); - - if (!ifname_valid(j)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Network zone name not valid: %s", j); - - free_and_replace(arg_network_zone, j); - - arg_network_veth = true; - arg_private_network = true; - arg_settings_mask |= SETTING_NETWORK; - break; - } - - case ARG_NETWORK_BRIDGE: - - if (!ifname_valid(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Bridge interface name not valid: %s", optarg); - - r = free_and_strdup(&arg_network_bridge, optarg); - if (r < 0) - return log_oom(); - - _fallthrough_; - case 'n': - arg_network_veth = true; - arg_private_network = true; - arg_settings_mask |= SETTING_NETWORK; - break; - - case ARG_NETWORK_VETH_EXTRA: - r = veth_extra_parse(&arg_network_veth_extra, optarg); - if (r < 0) - return log_error_errno(r, "Failed to parse --network-veth-extra= parameter: %s", optarg); - - arg_private_network = true; - arg_settings_mask |= SETTING_NETWORK; - break; - - case ARG_NETWORK_INTERFACE: - r = interface_pair_parse(&arg_network_interfaces, optarg); - if (r < 0) - return r; - - arg_private_network = true; - arg_settings_mask |= SETTING_NETWORK; - break; - - case ARG_NETWORK_MACVLAN: - r = macvlan_pair_parse(&arg_network_macvlan, optarg); - if (r < 0) - return r; - - arg_private_network = true; - arg_settings_mask |= SETTING_NETWORK; - break; - - case ARG_NETWORK_IPVLAN: - r = ipvlan_pair_parse(&arg_network_ipvlan, optarg); - if (r < 0) - return r; - - _fallthrough_; - case ARG_PRIVATE_NETWORK: - arg_private_network = true; - arg_settings_mask |= SETTING_NETWORK; - break; - - case ARG_NETWORK_NAMESPACE_PATH: - r = parse_path_argument(optarg, false, &arg_network_namespace_path); - if (r < 0) - return r; - - arg_settings_mask |= SETTING_NETWORK; - break; - - case 'b': - if (arg_start_mode == START_PID2) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "--boot and --as-pid2 may not be combined."); - - arg_start_mode = START_BOOT; - arg_settings_mask |= SETTING_START_MODE; - break; - - case 'a': - if (arg_start_mode == START_BOOT) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "--boot and --as-pid2 may not be combined."); - - arg_start_mode = START_PID2; - arg_settings_mask |= SETTING_START_MODE; - break; - - case ARG_UUID: - r = id128_from_string_nonzero(optarg, &arg_uuid); - if (r == -ENXIO) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Machine UUID may not be all zeroes."); - if (r < 0) - return log_error_errno(r, "Invalid UUID: %s", optarg); - - arg_settings_mask |= SETTING_MACHINE_ID; - break; - - case 'S': { - _cleanup_free_ char *mangled = NULL; - - r = unit_name_mangle_with_suffix(optarg, NULL, UNIT_NAME_MANGLE_WARN, ".slice", &mangled); - if (r < 0) - return log_oom(); - - free_and_replace(arg_slice, mangled); - arg_settings_mask |= SETTING_SLICE; - break; - } - - case 'M': - if (!isempty(optarg) && !hostname_is_valid(optarg, /* flags= */ 0)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Invalid machine name: %s", optarg); - - r = free_and_strdup_warn(&arg_machine, optarg); - if (r < 0) - return r; - break; - - case ARG_HOSTNAME: - if (!isempty(optarg) && !hostname_is_valid(optarg, /* flags= */ 0)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Invalid hostname: %s", optarg); - - r = free_and_strdup_warn(&arg_hostname, optarg); - if (r < 0) - return r; - - arg_settings_mask |= SETTING_HOSTNAME; - break; - - case 'Z': - arg_selinux_context = optarg; - break; - - case 'L': - arg_selinux_apifs_context = optarg; - break; - - case ARG_READ_ONLY: - arg_read_only = true; - arg_settings_mask |= SETTING_READ_ONLY; - break; - - case ARG_AMBIENT_CAPABILITY: { - uint64_t m; - r = parse_capability_spec(optarg, &m); - if (r <= 0) - return r; - arg_caps_ambient |= m; - arg_settings_mask |= SETTING_CAPABILITY; - break; - } - case ARG_CAPABILITY: - case ARG_DROP_CAPABILITY: { - uint64_t m; - r = parse_capability_spec(optarg, &m); - if (r <= 0) - return r; - - if (c == ARG_CAPABILITY) - plus |= m; - else - minus |= m; - arg_settings_mask |= SETTING_CAPABILITY; - break; - } - case ARG_NO_NEW_PRIVILEGES: - r = parse_boolean_argument("--no-new-privileges=", optarg, &arg_no_new_privileges); - if (r < 0) - return r; - - arg_settings_mask |= SETTING_NO_NEW_PRIVILEGES; - break; - - case 'j': - arg_link_journal = LINK_GUEST; - arg_link_journal_try = true; - arg_settings_mask |= SETTING_LINK_JOURNAL; - break; - - case ARG_LINK_JOURNAL: - r = parse_link_journal(optarg, &arg_link_journal, &arg_link_journal_try); - if (r < 0) - return log_error_errno(r, "Failed to parse link journal mode %s", optarg); - - arg_settings_mask |= SETTING_LINK_JOURNAL; - break; - - case ARG_BIND: - case ARG_BIND_RO: - r = bind_mount_parse(&arg_custom_mounts, &arg_n_custom_mounts, optarg, c == ARG_BIND_RO); - if (r < 0) - return log_error_errno(r, "Failed to parse --bind(-ro)= argument %s: %m", optarg); - - arg_settings_mask |= SETTING_CUSTOM_MOUNTS; - break; - - case ARG_TMPFS: - r = tmpfs_mount_parse(&arg_custom_mounts, &arg_n_custom_mounts, optarg); - if (r < 0) - return log_error_errno(r, "Failed to parse --tmpfs= argument %s: %m", optarg); - - arg_settings_mask |= SETTING_CUSTOM_MOUNTS; - break; - - case ARG_OVERLAY: - case ARG_OVERLAY_RO: - r = overlay_mount_parse(&arg_custom_mounts, &arg_n_custom_mounts, optarg, c == ARG_OVERLAY_RO); - if (r == -EADDRNOTAVAIL) - return log_error_errno(r, "--overlay(-ro)= needs at least two colon-separated directories specified."); - if (r < 0) - return log_error_errno(r, "Failed to parse --overlay(-ro)= argument %s: %m", optarg); - - arg_settings_mask |= SETTING_CUSTOM_MOUNTS; - break; - - case ARG_INACCESSIBLE: - r = inaccessible_mount_parse(&arg_custom_mounts, &arg_n_custom_mounts, optarg); - if (r < 0) - return log_error_errno(r, "Failed to parse --inaccessible= argument %s: %m", optarg); - - arg_settings_mask |= SETTING_CUSTOM_MOUNTS; - break; - - case 'E': - r = strv_env_replace_strdup_passthrough(&arg_setenv, optarg); - if (r < 0) - return log_error_errno(r, "Cannot assign environment variable %s: %m", optarg); - - arg_settings_mask |= SETTING_ENVIRONMENT; - break; - - case 'q': + OPTION('q', "quiet", NULL, "Do not show status information"): arg_quiet = true; break; - case ARG_SHARE_SYSTEM: - /* We don't officially support this anymore, except for compat reasons. People should use the - * $SYSTEMD_NSPAWN_SHARE_* environment variables instead. */ - log_warning("Please do not use --share-system anymore, use $SYSTEMD_NSPAWN_SHARE_* instead."); - arg_clone_ns_flags = 0; + OPTION_COMMON_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; break; - case ARG_REGISTER: - r = parse_tristate_argument_with_auto("--register=", optarg, &arg_register); - if (r < 0) - return r; - - break; - - case ARG_KEEP_UNIT: - arg_keep_unit = true; - break; - - case ARG_PERSONALITY: - - arg_personality = personality_from_string(optarg); - if (arg_personality == PERSONALITY_INVALID) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Unknown or unsupported personality '%s'.", optarg); - - arg_settings_mask |= SETTING_PERSONALITY; - break; - - case ARG_VOLATILE: - - if (!optarg) - arg_volatile_mode = VOLATILE_YES; - else if (streq(optarg, "help")) - return DUMP_STRING_TABLE(volatile_mode, VolatileMode, _VOLATILE_MODE_MAX); - else { - VolatileMode m; - - m = volatile_mode_from_string(optarg); - if (m < 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Failed to parse --volatile= argument: %s", optarg); - else - arg_volatile_mode = m; - } - - arg_settings_mask |= SETTING_VOLATILE_MODE; - break; - - case 'p': - r = expose_port_parse(&arg_expose_ports, optarg); - if (r == -EEXIST) - return log_error_errno(r, "Duplicate port specification: %s", optarg); - if (r < 0) - return log_error_errno(r, "Failed to parse host port %s: %m", optarg); - - arg_settings_mask |= SETTING_EXPOSE_PORTS; - break; - - case ARG_PROPERTY: - if (strv_extend(&arg_property, optarg) < 0) - return log_oom(); - - break; - - case ARG_PRIVATE_USERS: - r = parse_private_users(optarg, &arg_userns_mode, &arg_uid_shift, &arg_uid_range); - if (r < 0) - return r; - - arg_settings_mask |= SETTING_USERNS; - break; - - case 'U': - if (userns_supported()) { - /* Note that arg_userns_ownership is implied by USER_NAMESPACE_PICK further down. - * We use _USER_NAMESPACE_MODE_INVALID as a marker so that the final resolution - * (PICK vs MANAGED) is deferred to after the getopt loop where arg_runtime_scope - * has its final value regardless of option order. */ - arg_userns_mode = _USER_NAMESPACE_MODE_INVALID; - arg_uid_shift = UID_INVALID; - arg_uid_range = UINT32_C(0x10000); - - arg_settings_mask |= SETTING_USERNS; - } - - break; - - case ARG_PRIVATE_USERS_CHOWN: - arg_userns_ownership = USER_NAMESPACE_OWNERSHIP_CHOWN; - - arg_settings_mask |= SETTING_USERNS; - break; - - case ARG_PRIVATE_USERS_OWNERSHIP: - if (streq(optarg, "help")) - return DUMP_STRING_TABLE(user_namespace_ownership, UserNamespaceOwnership, _USER_NAMESPACE_OWNERSHIP_MAX); - - arg_userns_ownership = user_namespace_ownership_from_string(optarg); - if (arg_userns_ownership < 0) - return log_error_errno(arg_userns_ownership, "Cannot parse --private-users-ownership= value: %s", optarg); - - arg_settings_mask |= SETTING_USERNS; - break; - - case ARG_PRIVATE_USERS_DELEGATE: - r = safe_atou(optarg, &arg_delegate_container_ranges); - if (r < 0) - return log_error_errno(r, "Failed to parse --private-users-delegate= parameter: %s", optarg); - - arg_settings_mask |= SETTING_USERNS; - break; - - case ARG_KILL_SIGNAL: - if (streq(optarg, "help")) - return DUMP_STRING_TABLE(signal, int, _NSIG); - - arg_kill_signal = signal_from_string(optarg); - if (arg_kill_signal < 0) - return log_error_errno(arg_kill_signal, "Cannot parse signal: %s", optarg); - - arg_settings_mask |= SETTING_KILL_SIGNAL; - break; - - case ARG_SETTINGS: - + OPTION_LONG("settings", "BOOLEAN", "Load additional settings from .nspawn file"): /* no → do not read files * yes → read files, do not override cmdline, trust only subset * override → read files, override cmdline, trust only subset * trusted → read files, do not override cmdline, trust all */ - r = parse_boolean(optarg); + r = parse_boolean(arg); if (r < 0) { - if (streq(optarg, "trusted")) { + if (streq(arg, "trusted")) { mask_all_settings = false; mask_no_settings = false; arg_settings_trusted = true; - } else if (streq(optarg, "override")) { + } else if (streq(arg, "override")) { mask_all_settings = false; mask_no_settings = true; arg_settings_trusted = -1; } else - return log_error_errno(r, "Failed to parse --settings= argument: %s", optarg); + return log_error_errno(r, "Failed to parse --settings= argument: %s", arg); } else if (r > 0) { /* yes */ mask_all_settings = false; @@ -1355,19 +680,167 @@ static int parse_argv(int argc, char *argv[]) { mask_no_settings = false; arg_settings_trusted = false; } - break; - case ARG_CHDIR: { + OPTION_LONG("cleanup", NULL, + "Clean up left-over mounts and underlying mount points used by the container"): + arg_cleanup = true; + break; + + OPTION_COMMON_NO_ASK_PASSWORD: + arg_ask_password = false; + break; + + OPTION_GROUP("Image"): {} + + OPTION('D', "directory", "PATH", "Root directory for the container"): + r = parse_path_argument(arg, false, &arg_directory); + if (r < 0) + return r; + arg_settings_mask |= SETTING_DIRECTORY; + break; + + OPTION_LONG("template", "PATH", + "Initialize root directory from template directory, if missing"): + r = parse_path_argument(arg, false, &arg_template); + if (r < 0) + return r; + arg_settings_mask |= SETTING_DIRECTORY; + break; + + OPTION('x', "ephemeral", NULL, + "Run container with snapshot of root directory, and remove it after exit"): + arg_ephemeral = true; + arg_settings_mask |= SETTING_EPHEMERAL; + break; + + OPTION('i', "image", "PATH", + "Root file system disk image (or device node) for the container"): + r = parse_path_argument(arg, false, &arg_image); + if (r < 0) + return r; + arg_settings_mask |= SETTING_DIRECTORY; + break; + + OPTION_LONG("image-policy", "POLICY", "Specify disk image dissection policy"): + r = parse_image_policy_argument(arg, &arg_image_policy); + if (r < 0) + return r; + break; + + OPTION_LONG("mstack", "PATH", /* help= */ NULL): + r = parse_path_argument(arg, false, &arg_mstack); + if (r < 0) + return r; + arg_settings_mask |= SETTING_DIRECTORY; + break; + + OPTION_LONG("oci-bundle", "PATH", "OCI bundle directory"): + r = parse_path_argument(arg, false, &arg_oci_bundle); + if (r < 0) + return r; + break; + + OPTION_LONG("read-only", NULL, "Mount the root directory read-only"): + arg_read_only = true; + arg_settings_mask |= SETTING_READ_ONLY; + break; + + OPTION_LONG_FLAGS(OPTION_OPTIONAL_ARG, "volatile", "MODE", "Run the system in volatile mode"): + if (!arg) + arg_volatile_mode = VOLATILE_YES; + else if (streq(arg, "help")) + return DUMP_STRING_TABLE(volatile_mode, VolatileMode, _VOLATILE_MODE_MAX); + else { + VolatileMode m; + + m = volatile_mode_from_string(arg); + if (m < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Failed to parse --volatile= argument: %s", arg); + else + arg_volatile_mode = m; + } + arg_settings_mask |= SETTING_VOLATILE_MODE; + break; + + OPTION_LONG("root-hash", "HASH", "Specify verity root hash for root disk image"): { + _cleanup_(iovec_done) struct iovec k = {}; + + r = unhexmem(arg, &k.iov_base, &k.iov_len); + if (r < 0) + return log_error_errno(r, "Failed to parse root hash: %s", arg); + if (k.iov_len < sizeof(sd_id128_t)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Root hash must be at least 128-bit long: %s", arg); + + iovec_done(&arg_verity_settings.root_hash); + arg_verity_settings.root_hash = TAKE_STRUCT(k); + break; + } + + OPTION_LONG("root-hash-sig", "SIG", + "Specify pkcs7 signature of root hash for verity"): { + _cleanup_(iovec_done) struct iovec p = {}; + const char *value; + + if ((value = startswith(arg, "base64:"))) { + r = unbase64mem(value, &p.iov_base, &p.iov_len); + if (r < 0) + return log_error_errno(r, "Failed to parse root hash signature '%s': %m", arg); + + } else { + r = read_full_file(arg, (char**) &p.iov_base, &p.iov_len); + if (r < 0) + return log_error_errno(r, "Failed to parse root hash signature file '%s': %m", arg); + } + + iovec_done(&arg_verity_settings.root_hash_sig); + arg_verity_settings.root_hash_sig = TAKE_STRUCT(p); + break; + } + + OPTION_LONG("verity-data", "PATH", "Specify hash device for verity"): + r = parse_path_argument(arg, false, &arg_verity_settings.data_path); + if (r < 0) + return r; + break; + + OPTION_LONG("pivot-root", "PATH[:PATH]", + "Pivot root to given directory in the container"): + r = pivot_root_parse(&arg_pivot_root_new, &arg_pivot_root_old, arg); + if (r < 0) + return log_error_errno(r, "Failed to parse --pivot-root= argument %s: %m", arg); + arg_settings_mask |= SETTING_PIVOT_ROOT; + break; + + OPTION_GROUP("Execution"): {} + + OPTION('a', "as-pid2", NULL, "Maintain a stub init as PID1, invoke binary as PID2"): + if (arg_start_mode == START_BOOT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--boot and --as-pid2 may not be combined."); + arg_start_mode = START_PID2; + arg_settings_mask |= SETTING_START_MODE; + break; + + OPTION('b', "boot", NULL, "Boot up full system (i.e. invoke init)"): + if (arg_start_mode == START_PID2) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--boot and --as-pid2 may not be combined."); + arg_start_mode = START_BOOT; + arg_settings_mask |= SETTING_START_MODE; + break; + + OPTION_LONG("chdir", "PATH", "Set working directory in the container"): { _cleanup_free_ char *wd = NULL; - if (!path_is_absolute(optarg)) + if (!path_is_absolute(arg)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Working directory %s is not an absolute path.", optarg); + "Working directory %s is not an absolute path.", arg); - r = path_simplify_alloc(optarg, &wd); + r = path_simplify_alloc(arg, &wd); if (r < 0) - return log_error_errno(r, "Failed to simplify path %s: %m", optarg); + return log_error_errno(r, "Failed to simplify path %s: %m", arg); if (!path_is_normalized(wd)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Working directory path is not normalized: %s", wd); @@ -1380,68 +853,294 @@ static int parse_argv(int argc, char *argv[]) { break; } - case ARG_PIVOT_ROOT: - r = pivot_root_parse(&arg_pivot_root_new, &arg_pivot_root_old, optarg); + OPTION('E', "setenv", "NAME[=VALUE]", "Pass an environment variable to PID 1"): + r = strv_env_replace_strdup_passthrough(&arg_setenv, arg); if (r < 0) - return log_error_errno(r, "Failed to parse --pivot-root= argument %s: %m", optarg); - - arg_settings_mask |= SETTING_PIVOT_ROOT; + return log_error_errno(r, "Cannot assign environment variable %s: %m", arg); + arg_settings_mask |= SETTING_ENVIRONMENT; break; - case ARG_NOTIFY_READY: - r = parse_boolean_argument("--notify-ready=", optarg, &arg_notify_ready); + OPTION('u', "uid", "USER", "Run the command under specified user or UID"): + r = free_and_strdup(&arg_user, arg); + if (r < 0) + return log_oom(); + arg_settings_mask |= SETTING_USER; + break; + + OPTION_LONG("kill-signal", "SIGNAL", "Select signal to use for shutting down PID 1"): + if (streq(arg, "help")) + return DUMP_STRING_TABLE(signal, int, _NSIG); + + arg_kill_signal = signal_from_string(arg); + if (arg_kill_signal < 0) + return log_error_errno(arg_kill_signal, "Cannot parse signal: %s", arg); + arg_settings_mask |= SETTING_KILL_SIGNAL; + break; + + OPTION_LONG("notify-ready", "BOOLEAN", "Receive notifications from the child init process"): + r = parse_boolean_argument("--notify-ready=", arg, &arg_notify_ready); if (r < 0) return r; - arg_settings_mask |= SETTING_NOTIFY_READY; break; - case ARG_ROOT_HASH: { - _cleanup_(iovec_done) struct iovec k = {}; - - r = unhexmem(optarg, &k.iov_base, &k.iov_len); + OPTION_LONG("suppress-sync", "BOOLEAN", "Suppress any form of disk data synchronization"): + r = parse_boolean_argument("--suppress-sync=", arg, &arg_suppress_sync); if (r < 0) - return log_error_errno(r, "Failed to parse root hash: %s", optarg); - if (k.iov_len < sizeof(sd_id128_t)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Root hash must be at least 128-bit long: %s", optarg); - - iovec_done(&arg_verity_settings.root_hash); - arg_verity_settings.root_hash = TAKE_STRUCT(k); + return r; + arg_settings_mask |= SETTING_SUPPRESS_SYNC; break; - } - case ARG_ROOT_HASH_SIG: { - _cleanup_(iovec_done) struct iovec p = {}; - char *value; + OPTION_GROUP("System Identity"): {} - if ((value = startswith(optarg, "base64:"))) { - r = unbase64mem(value, &p.iov_base, &p.iov_len); - if (r < 0) - return log_error_errno(r, "Failed to parse root hash signature '%s': %m", optarg); - - } else { - r = read_full_file(optarg, (char**) &p.iov_base, &p.iov_len); - if (r < 0) - return log_error_errno(r, "Failed to parse root hash signature file '%s': %m", optarg); - } - - iovec_done(&arg_verity_settings.root_hash_sig); - arg_verity_settings.root_hash_sig = TAKE_STRUCT(p); - break; - } - - case ARG_VERITY_DATA: - r = parse_path_argument(optarg, false, &arg_verity_settings.data_path); + OPTION('M', "machine", "NAME", "Set the machine name for the container"): + if (!isempty(arg) && !hostname_is_valid(arg, /* flags= */ 0)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid machine name: %s", arg); + r = free_and_strdup_warn(&arg_machine, arg); if (r < 0) return r; break; - case ARG_SYSTEM_CALL_FILTER: { + OPTION_LONG("hostname", "NAME", "Override the hostname for the container"): + if (!isempty(arg) && !hostname_is_valid(arg, /* flags= */ 0)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid hostname: %s", arg); + r = free_and_strdup_warn(&arg_hostname, arg); + if (r < 0) + return r; + arg_settings_mask |= SETTING_HOSTNAME; + break; + + OPTION_LONG("uuid", "UUID", "Set a specific machine UUID for the container"): + r = id128_from_string_nonzero(arg, &arg_uuid); + if (r == -ENXIO) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Machine UUID may not be all zeroes."); + if (r < 0) + return log_error_errno(r, "Invalid UUID: %s", arg); + arg_settings_mask |= SETTING_MACHINE_ID; + break; + + OPTION_GROUP("Properties"): {} + + OPTION('S', "slice", "SLICE", "Place the container in the specified slice"): { + _cleanup_free_ char *mangled = NULL; + + r = unit_name_mangle_with_suffix(arg, NULL, UNIT_NAME_MANGLE_WARN, ".slice", &mangled); + if (r < 0) + return log_oom(); + + free_and_replace(arg_slice, mangled); + arg_settings_mask |= SETTING_SLICE; + break; + } + + OPTION_LONG("property", "NAME=VALUE", "Set scope unit property"): + if (strv_extend(&arg_property, arg) < 0) + return log_oom(); + break; + + OPTION_LONG("register", "BOOLEAN", "Register container as machine"): + r = parse_tristate_argument_with_auto("--register=", arg, &arg_register); + if (r < 0) + return r; + break; + + OPTION_LONG("keep-unit", NULL, + "Do not register a scope for the machine, reuse the service unit nspawn is running in"): + arg_keep_unit = true; + break; + + OPTION_GROUP("User Namespacing"): {} + + OPTION_LONG_FLAGS(OPTION_OPTIONAL_ARG, "private-users", "MODE", + "Run within user namespace, configure UID/GID range"): + r = parse_private_users(arg, &arg_userns_mode, &arg_uid_shift, &arg_uid_range); + if (r < 0) + return r; + arg_settings_mask |= SETTING_USERNS; + break; + + OPTION_LONG("private-users-ownership", "MODE", + "Adjust ('chown') or map ('map') OS tree ownership to private UID/GID range"): + if (streq(arg, "help")) + return DUMP_STRING_TABLE(user_namespace_ownership, UserNamespaceOwnership, _USER_NAMESPACE_OWNERSHIP_MAX); + + arg_userns_ownership = user_namespace_ownership_from_string(arg); + if (arg_userns_ownership < 0) + return log_error_errno(arg_userns_ownership, "Cannot parse --private-users-ownership= value: %s", arg); + arg_settings_mask |= SETTING_USERNS; + break; + + OPTION_LONG_FLAGS(OPTION_OPTIONAL_ARG, "private-users-chown", "MODE", /* help= */ NULL): /* obsolete */ + arg_userns_ownership = USER_NAMESPACE_OWNERSHIP_CHOWN; + arg_settings_mask |= SETTING_USERNS; + break; + + OPTION_LONG("private-users-delegate", "N", + "Delegate N additional 64K UID/GID ranges for use by nested containers"): + r = safe_atou(arg, &arg_delegate_container_ranges); + if (r < 0) + return log_error_errno(r, "Failed to parse --private-users-delegate= parameter: %s", arg); + arg_settings_mask |= SETTING_USERNS; + break; + + OPTION_SHORT('U', NULL, + "Equivalent to --private-users=pick and --private-users-ownership=auto"): + if (userns_supported()) { + arg_userns_mode = _USER_NAMESPACE_MODE_INVALID; + arg_uid_shift = UID_INVALID; + arg_uid_range = UINT32_C(0x10000); + arg_settings_mask |= SETTING_USERNS; + } + break; + + OPTION_GROUP("Networking"): {} + + OPTION_LONG("private-network", NULL, "Disable network in container"): + arg_private_network = true; + arg_settings_mask |= SETTING_NETWORK; + break; + + OPTION_LONG("network-interface", "HOSTIF[:CONTAINERIF]", + "Assign an existing network interface to the container"): + r = interface_pair_parse(&arg_network_interfaces, arg); + if (r < 0) + return r; + arg_private_network = true; + arg_settings_mask |= SETTING_NETWORK; + break; + + OPTION_LONG("network-macvlan", "HOSTIF[:CONTAINERIF]", + "Create a macvlan network interface based on an existing network interface to the container"): + r = macvlan_pair_parse(&arg_network_macvlan, arg); + if (r < 0) + return r; + arg_private_network = true; + arg_settings_mask |= SETTING_NETWORK; + break; + + OPTION_LONG("network-ipvlan", "HOSTIF[:CONTAINERIF]", + "Create an ipvlan network interface based on an existing network interface to the container"): + r = ipvlan_pair_parse(&arg_network_ipvlan, arg); + if (r < 0) + return r; + arg_private_network = true; + arg_settings_mask |= SETTING_NETWORK; + break; + + OPTION('n', "network-veth", NULL, + "Add a virtual Ethernet connection between host and container"): + arg_network_veth = true; + arg_private_network = true; + arg_settings_mask |= SETTING_NETWORK; + break; + + OPTION_LONG("network-veth-extra", "HOSTIF[:CONTAINERIF]", + "Add an additional virtual Ethernet link between host and container"): + r = veth_extra_parse(&arg_network_veth_extra, arg); + if (r < 0) + return log_error_errno(r, "Failed to parse --network-veth-extra= parameter: %s", arg); + arg_private_network = true; + arg_settings_mask |= SETTING_NETWORK; + break; + + OPTION_LONG("network-bridge", "INTERFACE", + "Add a virtual Ethernet connection to the container and attach it to an existing bridge on the host"): + if (!ifname_valid(arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Bridge interface name not valid: %s", arg); + r = free_and_strdup(&arg_network_bridge, arg); + if (r < 0) + return log_oom(); + arg_network_veth = true; + arg_private_network = true; + arg_settings_mask |= SETTING_NETWORK; + break; + + OPTION_LONG("network-zone", "NAME", + "Similar, but attach the new interface to an automatically managed bridge interface"): { + _cleanup_free_ char *j = NULL; + + j = strjoin("vz-", arg); + if (!j) + return log_oom(); + + if (!ifname_valid(j)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Network zone name not valid: %s", j); + + free_and_replace(arg_network_zone, j); + arg_network_veth = true; + arg_private_network = true; + arg_settings_mask |= SETTING_NETWORK; + break; + } + + OPTION_LONG("network-namespace-path", "PATH", + "Set network namespace to the one represented by the specified kernel namespace file node"): + r = parse_path_argument(arg, false, &arg_network_namespace_path); + if (r < 0) + return r; + arg_settings_mask |= SETTING_NETWORK; + break; + + OPTION('p', "port", "[PROTOCOL:]HOSTPORT[:CONTAINERPORT]", + "Expose a container IP port on the host"): + r = expose_port_parse(&arg_expose_ports, arg); + if (r == -EEXIST) + return log_error_errno(r, "Duplicate port specification: %s", arg); + if (r < 0) + return log_error_errno(r, "Failed to parse host port %s: %m", arg); + arg_settings_mask |= SETTING_EXPOSE_PORTS; + break; + + OPTION_GROUP("Security"): {} + + OPTION_LONG("capability", "CAP", + "In addition to the default, retain specified capability"): {} + OPTION_LONG("drop-capability", "CAP", + "Drop the specified capability from the default set"): { + uint64_t m; + r = parse_capability_spec(arg, &m); + if (r <= 0) + return r; + + if (streq(opt->long_code, "capability")) + plus |= m; + else + minus |= m; + arg_settings_mask |= SETTING_CAPABILITY; + break; + } + + OPTION_LONG("ambient-capability", "CAP", + "Sets the specified capability for the started process"): { + uint64_t m; + r = parse_capability_spec(arg, &m); + if (r <= 0) + return r; + arg_caps_ambient |= m; + arg_settings_mask |= SETTING_CAPABILITY; + break; + } + + OPTION_LONG("no-new-privileges", "BOOL", + "Set PR_SET_NO_NEW_PRIVS flag for container payload"): + r = parse_boolean_argument("--no-new-privileges=", arg, &arg_no_new_privileges); + if (r < 0) + return r; + arg_settings_mask |= SETTING_NO_NEW_PRIVILEGES; + break; + + OPTION_LONG("system-call-filter", "LIST|~LIST", + "Permit/prohibit specific system calls"): { bool negative; const char *items; - negative = optarg[0] == '~'; - items = negative ? optarg + 1 : optarg; + negative = arg[0] == '~'; + items = negative ? arg + 1 : arg; for (;;) { _cleanup_free_ char *word = NULL; @@ -1461,25 +1160,36 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_oom(); } - arg_settings_mask |= SETTING_SYSCALL_FILTER; break; } - case ARG_RLIMIT: { + OPTION('Z', "selinux-context", "SECLABEL", + "Set the SELinux security context to be used by processes in the container"): + arg_selinux_context = arg; + break; + + OPTION('L', "selinux-apifs-context", "SECLABEL", + "Set the SELinux security context to be used by API/tmpfs file systems in the container"): + arg_selinux_apifs_context = arg; + break; + + OPTION_GROUP("Resources"): {} + + OPTION_LONG("rlimit", "NAME=LIMIT", "Set a resource limit for the payload"): { const char *eq; _cleanup_free_ char *name = NULL; int rl; - if (streq(optarg, "help")) + if (streq(arg, "help")) return DUMP_STRING_TABLE(rlimit, int, _RLIMIT_MAX); - eq = strchr(optarg, '='); + eq = strchr(arg, '='); if (!eq) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--rlimit= expects an '=' assignment."); - name = strndup(optarg, eq - optarg); + name = strndup(arg, eq - arg); if (!name) return log_oom(); @@ -1501,180 +1211,218 @@ static int parse_argv(int argc, char *argv[]) { break; } - case ARG_OOM_SCORE_ADJUST: - r = parse_oom_score_adjust(optarg, &arg_oom_score_adjust); + OPTION_LONG("oom-score-adjust", "VALUE", "Adjust the OOM score value for the payload"): + r = parse_oom_score_adjust(arg, &arg_oom_score_adjust); if (r < 0) - return log_error_errno(r, "Failed to parse --oom-score-adjust= parameter: %s", optarg); - + return log_error_errno(r, "Failed to parse --oom-score-adjust= parameter: %s", arg); arg_oom_score_adjust_set = true; arg_settings_mask |= SETTING_OOM_SCORE_ADJUST; break; - case ARG_CPU_AFFINITY: { + OPTION_LONG("cpu-affinity", "CPUS", "Adjust the CPU affinity of the container"): { CPUSet cpuset; - r = parse_cpu_set(optarg, &cpuset); + r = parse_cpu_set(arg, &cpuset); if (r < 0) - return log_error_errno(r, "Failed to parse CPU affinity mask %s: %m", optarg); + return log_error_errno(r, "Failed to parse CPU affinity mask %s: %m", arg); cpu_set_done_and_replace(arg_cpu_set, cpuset); arg_settings_mask |= SETTING_CPU_AFFINITY; break; } - case ARG_RESOLV_CONF: - if (streq(optarg, "help")) + OPTION_LONG("personality", "ARCH", "Pick personality for this container"): + arg_personality = personality_from_string(arg); + if (arg_personality == PERSONALITY_INVALID) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unknown or unsupported personality '%s'.", arg); + arg_settings_mask |= SETTING_PERSONALITY; + break; + + OPTION_GROUP("Integration"): {} + + OPTION_LONG("resolv-conf", "MODE", "Select mode of /etc/resolv.conf initialization"): + if (streq(arg, "help")) return DUMP_STRING_TABLE(resolv_conf_mode, ResolvConfMode, _RESOLV_CONF_MODE_MAX); - arg_resolv_conf = resolv_conf_mode_from_string(optarg); + arg_resolv_conf = resolv_conf_mode_from_string(arg); if (arg_resolv_conf < 0) return log_error_errno(arg_resolv_conf, - "Failed to parse /etc/resolv.conf mode: %s", optarg); - + "Failed to parse /etc/resolv.conf mode: %s", arg); arg_settings_mask |= SETTING_RESOLV_CONF; break; - case ARG_TIMEZONE: - if (streq(optarg, "help")) + OPTION_LONG("timezone", "MODE", "Select mode of /etc/localtime initialization"): + if (streq(arg, "help")) return DUMP_STRING_TABLE(timezone_mode, TimezoneMode, _TIMEZONE_MODE_MAX); - arg_timezone = timezone_mode_from_string(optarg); + arg_timezone = timezone_mode_from_string(arg); if (arg_timezone < 0) return log_error_errno(arg_timezone, - "Failed to parse /etc/localtime mode: %s", optarg); - + "Failed to parse /etc/localtime mode: %s", arg); arg_settings_mask |= SETTING_TIMEZONE; break; - case ARG_CONSOLE: - if (streq(optarg, "help")) - return DUMP_STRING_TABLE(console_mode, ConsoleMode, _CONSOLE_MODE_MAX); - - arg_console_mode = console_mode_from_string(optarg); - if (arg_console_mode < 0) - return log_error_errno(arg_console_mode, "Unknown console mode: %s", optarg); - - arg_settings_mask |= SETTING_CONSOLE_MODE; - - break; - - case 'P': - case ARG_PIPE: - arg_console_mode = CONSOLE_PIPE; - arg_settings_mask |= SETTING_CONSOLE_MODE; - break; - - case ARG_NO_PAGER: - arg_pager_flags |= PAGER_DISABLE; - break; - - case ARG_SET_CREDENTIAL: - r = machine_credential_set(&arg_credentials, optarg); + OPTION_LONG("link-journal", "MODE", + "Link up guest journal, one of no, auto, guest, host, try-guest, try-host"): + r = parse_link_journal(arg, &arg_link_journal, &arg_link_journal_try); if (r < 0) - return r; - - arg_settings_mask |= SETTING_CREDENTIALS; + return log_error_errno(r, "Failed to parse link journal mode %s", arg); + arg_settings_mask |= SETTING_LINK_JOURNAL; break; - case ARG_LOAD_CREDENTIAL: - r = machine_credential_load(&arg_credentials, optarg); + OPTION_SHORT('j', NULL, "Equivalent to --link-journal=try-guest"): + arg_link_journal = LINK_GUEST; + arg_link_journal_try = true; + arg_settings_mask |= SETTING_LINK_JOURNAL; + break; + + OPTION_GROUP("Mounts"): {} + + OPTION_LONG("bind", "PATH[:PATH[:OPTIONS]]", + "Bind mount a file or directory from the host into the container"): {} + OPTION_LONG("bind-ro", "PATH[:PATH[:OPTIONS]]", + "Similar, but creates a read-only bind mount"): + r = bind_mount_parse(&arg_custom_mounts, &arg_n_custom_mounts, arg, + streq(opt->long_code, "bind-ro")); if (r < 0) - return r; - - arg_settings_mask |= SETTING_CREDENTIALS; + return log_error_errno(r, "Failed to parse --bind(-ro)= argument %s: %m", arg); + arg_settings_mask |= SETTING_CUSTOM_MOUNTS; break; - case ARG_BIND_USER: - if (!valid_user_group_name(optarg, 0)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid user name to bind: %s", optarg); + OPTION_LONG("inaccessible", "PATH", + "Over-mount file node with inaccessible node to mask it"): + r = inaccessible_mount_parse(&arg_custom_mounts, &arg_n_custom_mounts, arg); + if (r < 0) + return log_error_errno(r, "Failed to parse --inaccessible= argument %s: %m", arg); + arg_settings_mask |= SETTING_CUSTOM_MOUNTS; + break; - if (strv_extend(&arg_bind_user, optarg) < 0) + OPTION_LONG("tmpfs", "PATH:[OPTIONS]", + "Mount an empty tmpfs to the specified directory"): + r = tmpfs_mount_parse(&arg_custom_mounts, &arg_n_custom_mounts, arg); + if (r < 0) + return log_error_errno(r, "Failed to parse --tmpfs= argument %s: %m", arg); + arg_settings_mask |= SETTING_CUSTOM_MOUNTS; + break; + + OPTION_LONG("overlay", "PATH[:PATH...]:PATH", + "Create an overlay mount from the host to the container"): {} + OPTION_LONG("overlay-ro", "PATH[:PATH...]:PATH", + "Similar, but creates a read-only overlay mount"): + r = overlay_mount_parse(&arg_custom_mounts, &arg_n_custom_mounts, arg, + streq(opt->long_code, "overlay-ro")); + if (r == -EADDRNOTAVAIL) + return log_error_errno(r, "--overlay(-ro)= needs at least two colon-separated directories specified."); + if (r < 0) + return log_error_errno(r, "Failed to parse --overlay(-ro)= argument %s: %m", arg); + arg_settings_mask |= SETTING_CUSTOM_MOUNTS; + break; + + OPTION_LONG("bind-user", "NAME", "Bind user from host to container"): + if (!valid_user_group_name(arg, 0)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid user name to bind: %s", arg); + if (strv_extend(&arg_bind_user, arg) < 0) return log_oom(); - arg_settings_mask |= SETTING_BIND_USER; break; - case ARG_BIND_USER_SHELL: { + OPTION_LONG("bind-user-shell", "BOOL|PATH", + "Configure the shell to use for --bind-user= users"): { bool copy = false; char *sh = NULL; - r = parse_user_shell(optarg, &sh, ©); + r = parse_user_shell(arg, &sh, ©); if (r == -ENOMEM) return log_oom(); if (r < 0) - return log_error_errno(r, "Invalid user shell to bind: %s", optarg); + return log_error_errno(r, "Invalid user shell to bind: %s", arg); free_and_replace(arg_bind_user_shell, sh); arg_bind_user_shell_copy = copy; - arg_settings_mask |= SETTING_BIND_USER_SHELL; break; } - case ARG_BIND_USER_GROUP: - if (!valid_user_group_name(optarg, /* flags= */ 0)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid bind user auxiliary group name: %s", optarg); - - if (strv_extend(&arg_bind_user_groups, optarg) < 0) + OPTION_LONG("bind-user-group", "GROUP", + "Add an auxiliary group to --bind-user= users"): + if (!valid_user_group_name(arg, /* flags= */ 0)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid bind user auxiliary group name: %s", arg); + if (strv_extend(&arg_bind_user_groups, arg) < 0) return log_oom(); - break; - case ARG_SUPPRESS_SYNC: - r = parse_boolean_argument("--suppress-sync=", optarg, &arg_suppress_sync); - if (r < 0) - return r; + OPTION_GROUP("Input/Output"): {} - arg_settings_mask |= SETTING_SUPPRESS_SYNC; + OPTION_LONG("console", "MODE", + "Select how stdin/stdout/stderr and /dev/console are set up for the container"): + if (streq(arg, "help")) + return DUMP_STRING_TABLE(console_mode, ConsoleMode, _CONSOLE_MODE_MAX); + + arg_console_mode = console_mode_from_string(arg); + if (arg_console_mode < 0) + return log_error_errno(arg_console_mode, "Unknown console mode: %s", arg); + arg_settings_mask |= SETTING_CONSOLE_MODE; break; - case ARG_IMAGE_POLICY: - r = parse_image_policy_argument(optarg, &arg_image_policy); + OPTION('P', "pipe", NULL, "Equivalent to --console=pipe"): + arg_console_mode = CONSOLE_PIPE; + arg_settings_mask |= SETTING_CONSOLE_MODE; + break; + + OPTION_LONG("background", "COLOR", "Set ANSI color for background"): + r = parse_background_argument(arg, &arg_background); if (r < 0) return r; break; - case ARG_BACKGROUND: - r = parse_background_argument(optarg, &arg_background); + OPTION_GROUP("Credentials"): {} + + OPTION_LONG("set-credential", "ID:VALUE", + "Pass a credential with literal value to container"): + r = machine_credential_set(&arg_credentials, arg); if (r < 0) return r; + arg_settings_mask |= SETTING_CREDENTIALS; break; - case ARG_CLEANUP: - arg_cleanup = true; + OPTION_LONG("load-credential", "ID:PATH", + "Load credential to pass to container from file or AF_UNIX stream socket"): + r = machine_credential_load(&arg_credentials, arg); + if (r < 0) + return r; + arg_settings_mask |= SETTING_CREDENTIALS; break; - case ARG_NO_ASK_PASSWORD: - arg_ask_password = false; + OPTION_GROUP("Other"): {} + + OPTION_LONG("share-system", NULL, /* help= */ NULL): /* not documented */ + log_warning("Please do not use --share-system anymore, use $SYSTEMD_NSPAWN_SHARE_* instead."); + arg_clone_ns_flags = 0; break; - case ARG_USER: - if (optarg) { + OPTION_LONG_FLAGS(OPTION_OPTIONAL_ARG, "user", "NAME", "Run in the user service manager scope"): + if (arg) { /* --user=NAME is a deprecated alias for --uid=NAME */ log_warning("--user=NAME is deprecated, use --uid=NAME instead."); - r = free_and_strdup(&arg_user, optarg); + r = free_and_strdup(&arg_user, arg); if (r < 0) return log_oom(); - arg_settings_mask |= SETTING_USER; } else arg_runtime_scope = RUNTIME_SCOPE_USER; break; - case ARG_SYSTEM: + OPTION_LONG("system", NULL, "Run in the system service manager scope"): arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } + } - if (argc > optind) { + char **args = option_parser_get_args(&state); + if (!strv_isempty(args)) { strv_free(arg_parameters); - arg_parameters = strv_copy(argv + optind); + arg_parameters = strv_copy(args); if (!arg_parameters) return log_oom(); From f293418a5035ec6ead2e28e80329e768d4b9b500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 7 Apr 2026 17:48:25 +0200 Subject: [PATCH 4/5] shared/options: add helper function to peek at or consume the next arg The test was partially written with Claude Opus 4.6. It's a bit on the verbose side, but does the job. --- src/shared/options.c | 22 +++++++ src/shared/options.h | 3 + src/test/test-options.c | 141 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+) diff --git a/src/shared/options.c b/src/shared/options.c index f00d9674d7..20ca7948e5 100644 --- a/src/shared/options.c +++ b/src/shared/options.c @@ -239,6 +239,28 @@ int option_parse( return option->id; } +char* option_parser_next_arg(const OptionParser *state) { + /* Peek at the next argument, whatever it is (option or position arg). + * May return NULL. */ + + assert(state->optind > 0); + assert(state->positional_offset <= state->argc); + + return state->optind < state->argc ? state->argv[state->optind] : NULL; +} + +char* option_parser_consume_next_arg(OptionParser *state) { + /* "Take" the next argument, whatever it is (option or position arg). + * The argument remains in the array, but the optind pointer is moved + * so we won't try to interpret it as an option. + * May return NULL. */ + + char *t = option_parser_next_arg(state); + if (t) + shift_arg(state->argv, state->positional_offset++, state->optind++); + return t; +} + char** option_parser_get_args(const OptionParser *state) { /* Returns positional args as a strv. * If "--" was found, it has been moved before state->positional_offset. diff --git a/src/shared/options.h b/src/shared/options.h index 1b0488579f..e834cbeded 100644 --- a/src/shared/options.h +++ b/src/shared/options.h @@ -113,6 +113,9 @@ int option_parse( #define FOREACH_OPTION(parser, opt, ret_a, on_error) \ FOREACH_OPTION_FULL(parser, opt, /* ret_o= */ NULL, ret_a, on_error) +char* option_parser_next_arg(const OptionParser *state); +char* option_parser_consume_next_arg(OptionParser *state); + char** option_parser_get_args(const OptionParser *state); size_t option_parser_get_n_args(const OptionParser *state); diff --git a/src/test/test-options.c b/src/test/test-options.c index a9cafbdd73..b91ac54884 100644 --- a/src/test/test-options.c +++ b/src/test/test-options.c @@ -1091,4 +1091,145 @@ TEST(option_macros) { "-h")); } +/* Test the pattern used by nspawn's --user: an optional-arg option that also + * peeks at the next arg to handle legacy "space-separated" form. */ +TEST(option_optional_arg_consume) { + static const Option options[] = { + { 1, .short_code = 'h', .long_code = "help" }, + { 2, .long_code = "user", .metavar = "NAME", .flags = OPTION_OPTIONAL_ARG }, + { 3, .short_code = 'u', .long_code = "uid", .metavar = "USER" }, + {} + }; + + /* --user=NAME: optional arg provided via = */ + test_option_parse_one(STRV_MAKE("arg0", + "--user=root"), + options, + (Entry[]) { + { "user", "root" }, + {} + }, + NULL, + /* stop_at_first_nonoption= */ false); + + /* --user without arg: next arg is an option, so no consumption */ + test_option_parse_one(STRV_MAKE("arg0", + "--user", + "--help"), + options, + (Entry[]) { + { "user", NULL }, + { "help" }, + {} + }, + NULL, + /* stop_at_first_nonoption= */ false); + + /* --user without arg: next arg is positional (doesn't start with -). + * The option parser returns NULL for the arg. The caller would then + * use option_parser_next_arg/consume_next_arg to grab it. */ + { + char **argv = STRV_MAKE("arg0", "--user", "someuser", "pos1"); + int argc = strv_length(argv); + + OptionParser state = { argc, argv }; + const Option *opt; + const char *arg; + + ASSERT_OK_POSITIVE(option_parse(options, options + 3, &state, &opt, &arg)); + ASSERT_STREQ(opt->long_code, "user"); + ASSERT_NULL(arg); + ASSERT_STREQ(option_parser_next_arg(&state), "someuser"); + ASSERT_STREQ(option_parser_consume_next_arg(&state), "someuser"); + + ASSERT_EQ(option_parse(options, options + 3, &state, &opt, &arg), 0); + + ASSERT_TRUE(strv_equal(option_parser_get_args(&state), STRV_MAKE("pos1"))); + } + + /* --user at end of args: no next arg, so scope mode */ + { + char **argv = STRV_MAKE("arg0", "--user"); + int argc = strv_length(argv); + + OptionParser state = { argc, argv }; + const Option *opt; + const char *arg; + + ASSERT_OK_POSITIVE(option_parse(options, options + 3, &state, &opt, &arg)); + ASSERT_STREQ(opt->long_code, "user"); + ASSERT_NULL(arg); + ASSERT_NULL(option_parser_next_arg(&state)); + ASSERT_NULL(option_parser_consume_next_arg(&state)); + + ASSERT_EQ(option_parse(options, options + 3, &state, &opt, &arg), 0); + + ASSERT_TRUE(strv_isempty(option_parser_get_args(&state))); + } + + /* --user followed by -u (option): scope mode, -u gets its own processing */ + { + char **argv = STRV_MAKE("arg0", "--user", "-u", "nobody"); + int argc = strv_length(argv); + + OptionParser state = { argc, argv }; + const Option *opt; + const char *arg; + + ASSERT_OK_POSITIVE(option_parse(options, options + 3, &state, &opt, &arg)); + ASSERT_STREQ(opt->long_code, "user"); + ASSERT_NULL(arg); + ASSERT_STREQ(option_parser_next_arg(&state), "-u"); + + ASSERT_OK_POSITIVE(option_parse(options, options + 3, &state, &opt, &arg)); + ASSERT_STREQ(opt->long_code, "uid"); + ASSERT_STREQ(arg, "nobody"); + ASSERT_NULL(option_parser_next_arg(&state)); + ASSERT_NULL(option_parser_consume_next_arg(&state)); + + ASSERT_EQ(option_parse(options, options + 3, &state, &opt, &arg), 0); + + ASSERT_TRUE(strv_isempty(option_parser_get_args(&state))); + } + + /* "Functional test": --user followed by -u (option): scope mode, -u gets its own processing, + * handled like in a real option parser. */ + { + char **argv = STRV_MAKE("arg0", "--user", "-u", "nobody", "nogroup", "--user=nobody", "--user"); + int argc = strv_length(argv); + + OptionParser state = { argc, argv }; + const Option *opt; + const char *arg; + int scope_seen = 0; + int nobody_seen = 0; + + for (int c; (c = option_parse(options, options + 3, &state, &opt, &arg)) != 0; ) { + ASSERT_OK(c); + + if (streq_ptr(opt->long_code, "user")) { + if (!arg) { + const char *t = option_parser_next_arg(&state); + if (t && t[0] != '-') + arg = option_parser_consume_next_arg(&state); + } + + if (arg) { + ASSERT_STREQ(arg, "nobody"); + nobody_seen ++; + } else + scope_seen ++; + + } else if (streq_ptr(opt->long_code, "uid")) { + ASSERT_STREQ(arg, "nobody"); + nobody_seen ++; + } + } + + ASSERT_EQ(nobody_seen, 2); + ASSERT_EQ(scope_seen, 2); + ASSERT_TRUE(strv_equal(option_parser_get_args(&state), STRV_MAKE("nogroup"))); + } +} + DEFINE_TEST_MAIN(LOG_DEBUG); From e02f66d36fe6a15c036fc86d1e9c1f9b828eb4c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 7 Apr 2026 17:49:54 +0200 Subject: [PATCH 5/5] =?UTF-8?q?nspawn:=20make=20handling=20of=20compat=20-?= =?UTF-8?q?-user=3D=E2=80=A6=20less=20elaborate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up for e7fb7296f56dacc24054cddb2e1f0aa55ee7dc94. --- src/nspawn/nspawn.c | 54 +++++++++++++-------------------------------- 1 file changed, 15 insertions(+), 39 deletions(-) diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 153babe67c..211f124893 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -584,50 +584,13 @@ static int parse_environment(void) { } static int parse_argv(int argc, char *argv[]) { - int r; uint64_t plus = 0, minus = 0; bool mask_all_settings = false, mask_no_settings = false; + int r; assert(argc >= 0); assert(argv); - /* --user= used to require an argument (the container user to run as). It has been repurposed to - * optionally set the runtime scope, with --uid= replacing the old container user functionality. - * To maintain backwards compatibility with the space-separated form (--user NAME), stitch them - * together into --user=NAME before getopt sees them. Without this, getopt's optional_argument - * handling would interpret --user NAME as --user (no arg) followed by a positional argument. - * Operate on a copy to avoid modifying the caller's argv. */ - _cleanup_strv_free_ char **argv_copy = NULL; - for (int i = 1; i < argc - 1; i++) { - if (streq(argv[i], "--")) - break; /* Respect end-of-options sentinel */ - if (!streq(argv[i], "--user")) - continue; - if (argv[i + 1][0] == '-') - continue; /* Next arg is an option, not a username */ - - /* Deep copy so we can freely replace and free entries */ - if (!argv_copy) { - argv_copy = strv_copy(argv); - if (!argv_copy) - return log_oom(); - argv = argv_copy; - } - - log_warning("--user NAME is deprecated, use --uid=NAME instead."); - - /* Stitch "--user" and the following argument into "--user=NAME" */ - free(argv[i]); - argv[i] = strjoin("--user=", argv[i + 1]); - if (!argv[i]) - return log_oom(); - - /* Remove the now-consumed argument and shrink argc accordingly */ - free(argv[i + 1]); - memmove(argv + i + 1, argv + i + 2, (argc - i - 1) * sizeof(char*)); - argc--; - } - OptionParser state = { argc, argv, /* stop_at_first_nonoption= */ true }; const Option *opt; const char *arg; @@ -1401,10 +1364,23 @@ static int parse_argv(int argc, char *argv[]) { break; OPTION_LONG_FLAGS(OPTION_OPTIONAL_ARG, "user", "NAME", "Run in the user service manager scope"): - if (arg) { + if (arg) /* --user=NAME is a deprecated alias for --uid=NAME */ log_warning("--user=NAME is deprecated, use --uid=NAME instead."); + else { + /* --user= used to require an argument (the container user to run as). It has + * been repurposed to optionally set the runtime scope, with --uid= replacing + * the old container user functionality. To maintain backwards compatibility + * with the space-separated form (--user NAME), if the next arg does not look + * like an option, interpret it a user name. */ + const char *t = option_parser_next_arg(&state); + if (t && t[0] != '-') { + arg = option_parser_consume_next_arg(&state); + log_warning("--user NAME is deprecated, use --uid=NAME instead."); + } + } + if (arg) { r = free_and_strdup(&arg_user, arg); if (r < 0) return log_oom();