From: Zbigniew Jędrzejewski-Szmek Date: Tue, 7 Apr 2026 15:13:46 +0000 (+0200) Subject: nspawn: convert to the new option parser X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=bf5bc9a7b26191ff4254836bd909cbb92eafe480;p=thirdparty%2Fsystemd.git 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 --- diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 006e91caa91..153babe67c0 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,563 +628,519 @@ 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; + OPTION('q', "quiet", NULL, "Do not show status information"): + arg_quiet = true; + break; - arg_settings_mask |= SETTING_DIRECTORY; + OPTION_COMMON_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; break; - case ARG_TEMPLATE: - r = parse_path_argument(optarg, false, &arg_template); - if (r < 0) - return r; + 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 + */ - arg_settings_mask |= SETTING_DIRECTORY; + r = parse_boolean(arg); + if (r < 0) { + if (streq(arg, "trusted")) { + mask_all_settings = false; + mask_no_settings = false; + arg_settings_trusted = true; + + } 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", arg); + } else if (r > 0) { + /* yes */ + mask_all_settings = false; + mask_no_settings = false; + arg_settings_trusted = -1; + } else { + /* no */ + mask_all_settings = true; + mask_no_settings = false; + arg_settings_trusted = false; + } break; - case 'i': - r = parse_path_argument(optarg, false, &arg_image); - if (r < 0) - return r; + OPTION_LONG("cleanup", NULL, + "Clean up left-over mounts and underlying mount points used by the container"): + arg_cleanup = true; + break; - arg_settings_mask |= SETTING_DIRECTORY; + OPTION_COMMON_NO_ASK_PASSWORD: + arg_ask_password = false; break; - case ARG_MSTACK: - r = parse_path_argument(optarg, false, &arg_mstack); + 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; - case ARG_OCI_BUNDLE: - r = parse_path_argument(optarg, false, &arg_oci_bundle); + 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; - case 'x': + 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; - case 'u': - r = free_and_strdup(&arg_user, optarg); + 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 log_oom(); + return r; + arg_settings_mask |= SETTING_DIRECTORY; + break; - arg_settings_mask |= SETTING_USER; + 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; - case ARG_NETWORK_ZONE: { - _cleanup_free_ char *j = NULL; + 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; - j = strjoin("vz-", optarg); - if (!j) - return log_oom(); + OPTION_LONG("oci-bundle", "PATH", "OCI bundle directory"): + r = parse_path_argument(arg, false, &arg_oci_bundle); + if (r < 0) + return r; + break; - if (!ifname_valid(j)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Network zone name not valid: %s", j); + OPTION_LONG("read-only", NULL, "Mount the root directory read-only"): + arg_read_only = true; + arg_settings_mask |= SETTING_READ_ONLY; + break; - free_and_replace(arg_network_zone, j); + 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; - arg_network_veth = true; - arg_private_network = true; - arg_settings_mask |= SETTING_NETWORK; + 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; - } - - case ARG_NETWORK_BRIDGE: - if (!ifname_valid(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Bridge interface name not valid: %s", optarg); + OPTION_LONG("root-hash", "HASH", "Specify verity root hash for root disk image"): { + _cleanup_(iovec_done) struct iovec k = {}; - r = free_and_strdup(&arg_network_bridge, optarg); + r = unhexmem(arg, &k.iov_base, &k.iov_len); if (r < 0) - return log_oom(); + 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); - _fallthrough_; - case 'n': - arg_network_veth = true; - arg_private_network = true; - arg_settings_mask |= SETTING_NETWORK; + iovec_done(&arg_verity_settings.root_hash); + arg_verity_settings.root_hash = TAKE_STRUCT(k); 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); + OPTION_LONG("root-hash-sig", "SIG", + "Specify pkcs7 signature of root hash for verity"): { + _cleanup_(iovec_done) struct iovec p = {}; + const char *value; - arg_private_network = true; - arg_settings_mask |= SETTING_NETWORK; - break; + 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); - case ARG_NETWORK_INTERFACE: - r = interface_pair_parse(&arg_network_interfaces, optarg); - if (r < 0) - return r; + } 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); + } - arg_private_network = true; - arg_settings_mask |= SETTING_NETWORK; + iovec_done(&arg_verity_settings.root_hash_sig); + arg_verity_settings.root_hash_sig = TAKE_STRUCT(p); break; + } - case ARG_NETWORK_MACVLAN: - r = macvlan_pair_parse(&arg_network_macvlan, optarg); + 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; - - arg_private_network = true; - arg_settings_mask |= SETTING_NETWORK; break; - case ARG_NETWORK_IPVLAN: - r = ipvlan_pair_parse(&arg_network_ipvlan, optarg); + 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 r; - - _fallthrough_; - case ARG_PRIVATE_NETWORK: - arg_private_network = true; - arg_settings_mask |= SETTING_NETWORK; + return log_error_errno(r, "Failed to parse --pivot-root= argument %s: %m", arg); + arg_settings_mask |= SETTING_PIVOT_ROOT; break; - case ARG_NETWORK_NAMESPACE_PATH: - r = parse_path_argument(optarg, false, &arg_network_namespace_path); - if (r < 0) - return r; + OPTION_GROUP("Execution"): {} - arg_settings_mask |= SETTING_NETWORK; - break; - - case 'b': - if (arg_start_mode == START_PID2) + 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_BOOT; + arg_start_mode = START_PID2; arg_settings_mask |= SETTING_START_MODE; break; - case 'a': - if (arg_start_mode == START_BOOT) + 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_PID2; + arg_start_mode = START_BOOT; arg_settings_mask |= SETTING_START_MODE; break; - case ARG_UUID: - r = id128_from_string_nonzero(optarg, &arg_uuid); - if (r == -ENXIO) + OPTION_LONG("chdir", "PATH", "Set working directory in the container"): { + _cleanup_free_ char *wd = NULL; + + if (!path_is_absolute(arg)) 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); + "Working directory %s is not an absolute path.", arg); - arg_settings_mask |= SETTING_MACHINE_ID; - break; + r = path_simplify_alloc(arg, &wd); + if (r < 0) + return log_error_errno(r, "Failed to simplify path %s: %m", arg); - case 'S': { - _cleanup_free_ char *mangled = NULL; + if (!path_is_normalized(wd)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Working directory path is not normalized: %s", wd); - r = unit_name_mangle_with_suffix(optarg, NULL, UNIT_NAME_MANGLE_WARN, ".slice", &mangled); - if (r < 0) - return log_oom(); + if (path_below_api_vfs(wd)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Working directory is below API VFS, refusing: %s", wd); - free_and_replace(arg_slice, mangled); - arg_settings_mask |= SETTING_SLICE; + free_and_replace(arg_chdir, wd); + arg_settings_mask |= SETTING_WORKING_DIRECTORY; 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); + 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 r; + return log_error_errno(r, "Cannot assign environment variable %s: %m", arg); + arg_settings_mask |= SETTING_ENVIRONMENT; 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); + OPTION('u', "uid", "USER", "Run the command under specified user or UID"): + r = free_and_strdup(&arg_user, arg); if (r < 0) - return r; - - arg_settings_mask |= SETTING_HOSTNAME; - break; - - case 'Z': - arg_selinux_context = optarg; + return log_oom(); + arg_settings_mask |= SETTING_USER; break; - case 'L': - arg_selinux_apifs_context = optarg; - 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); - case ARG_READ_ONLY: - arg_read_only = true; - arg_settings_mask |= SETTING_READ_ONLY; + 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; - case ARG_AMBIENT_CAPABILITY: { - uint64_t m; - r = parse_capability_spec(optarg, &m); - if (r <= 0) + 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_caps_ambient |= m; - arg_settings_mask |= SETTING_CAPABILITY; + arg_settings_mask |= SETTING_NOTIFY_READY; 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); + 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 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; + arg_settings_mask |= SETTING_SUPPRESS_SYNC; 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); + OPTION_GROUP("System Identity"): {} - 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); + 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 log_error_errno(r, "Failed to parse --bind(-ro)= argument %s: %m", optarg); - - arg_settings_mask |= SETTING_CUSTOM_MOUNTS; + return r; break; - case ARG_TMPFS: - r = tmpfs_mount_parse(&arg_custom_mounts, &arg_n_custom_mounts, optarg); + 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 log_error_errno(r, "Failed to parse --tmpfs= argument %s: %m", optarg); - - arg_settings_mask |= SETTING_CUSTOM_MOUNTS; + return r; + arg_settings_mask |= SETTING_HOSTNAME; 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."); + 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, "Failed to parse --overlay(-ro)= argument %s: %m", optarg); - - arg_settings_mask |= SETTING_CUSTOM_MOUNTS; + return log_error_errno(r, "Invalid UUID: %s", arg); + arg_settings_mask |= SETTING_MACHINE_ID; 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); + OPTION_GROUP("Properties"): {} - arg_settings_mask |= SETTING_CUSTOM_MOUNTS; - break; + OPTION('S', "slice", "SLICE", "Place the container in the specified slice"): { + _cleanup_free_ char *mangled = NULL; - case 'E': - r = strv_env_replace_strdup_passthrough(&arg_setenv, optarg); + r = unit_name_mangle_with_suffix(arg, NULL, UNIT_NAME_MANGLE_WARN, ".slice", &mangled); if (r < 0) - return log_error_errno(r, "Cannot assign environment variable %s: %m", optarg); - - arg_settings_mask |= SETTING_ENVIRONMENT; - break; + return log_oom(); - case 'q': - arg_quiet = true; + free_and_replace(arg_slice, mangled); + arg_settings_mask |= SETTING_SLICE; 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_LONG("property", "NAME=VALUE", "Set scope unit property"): + if (strv_extend(&arg_property, arg) < 0) + return log_oom(); break; - case ARG_REGISTER: - r = parse_tristate_argument_with_auto("--register=", optarg, &arg_register); + OPTION_LONG("register", "BOOLEAN", "Register container as machine"): + r = parse_tristate_argument_with_auto("--register=", arg, &arg_register); if (r < 0) return r; - break; - case ARG_KEEP_UNIT: + 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; - case ARG_PERSONALITY: + OPTION_GROUP("User Namespacing"): {} - 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; + 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; - 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); + 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_settings_mask |= SETTING_EXPOSE_PORTS; + 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; - case ARG_PROPERTY: - if (strv_extend(&arg_property, optarg) < 0) - return log_oom(); - + 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; - case ARG_PRIVATE_USERS: - r = parse_private_users(optarg, &arg_userns_mode, &arg_uid_shift, &arg_uid_range); + 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 r; - + return log_error_errno(r, "Failed to parse --private-users-delegate= parameter: %s", arg); arg_settings_mask |= SETTING_USERNS; break; - case 'U': + OPTION_SHORT('U', NULL, + "Equivalent to --private-users=pick and --private-users-ownership=auto"): 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; + OPTION_GROUP("Networking"): {} - arg_settings_mask |= SETTING_USERNS; + OPTION_LONG("private-network", NULL, "Disable network in container"): + arg_private_network = true; + arg_settings_mask |= SETTING_NETWORK; 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; + 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; - case ARG_PRIVATE_USERS_DELEGATE: - r = safe_atou(optarg, &arg_delegate_container_ranges); + 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 log_error_errno(r, "Failed to parse --private-users-delegate= parameter: %s", optarg); - - arg_settings_mask |= SETTING_USERNS; + return r; + arg_private_network = true; + arg_settings_mask |= SETTING_NETWORK; 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; + 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; - case ARG_SETTINGS: - - /* 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); - if (r < 0) { - if (streq(optarg, "trusted")) { - mask_all_settings = false; - mask_no_settings = false; - arg_settings_trusted = true; - - } else if (streq(optarg, "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); - } else if (r > 0) { - /* yes */ - mask_all_settings = false; - mask_no_settings = false; - arg_settings_trusted = -1; - } else { - /* no */ - mask_all_settings = true; - mask_no_settings = false; - arg_settings_trusted = false; - } - + 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; - case ARG_CHDIR: { - _cleanup_free_ char *wd = NULL; + 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; - if (!path_is_absolute(optarg)) + 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), - "Working directory %s is not an absolute path.", optarg); - - r = path_simplify_alloc(optarg, &wd); + "Bridge interface name not valid: %s", arg); + r = free_and_strdup(&arg_network_bridge, arg); if (r < 0) - return log_error_errno(r, "Failed to simplify path %s: %m", optarg); + return log_oom(); + arg_network_veth = true; + arg_private_network = true; + arg_settings_mask |= SETTING_NETWORK; + break; - if (!path_is_normalized(wd)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Working directory path is not normalized: %s", wd); + OPTION_LONG("network-zone", "NAME", + "Similar, but attach the new interface to an automatically managed bridge interface"): { + _cleanup_free_ char *j = NULL; - if (path_below_api_vfs(wd)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Working directory is below API VFS, refusing: %s", wd); + j = strjoin("vz-", arg); + if (!j) + return log_oom(); - free_and_replace(arg_chdir, wd); - arg_settings_mask |= SETTING_WORKING_DIRECTORY; + 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_PIVOT_ROOT: - r = pivot_root_parse(&arg_pivot_root_new, &arg_pivot_root_old, optarg); + 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 log_error_errno(r, "Failed to parse --pivot-root= argument %s: %m", optarg); - - arg_settings_mask |= SETTING_PIVOT_ROOT; + return r; + arg_settings_mask |= SETTING_NETWORK; break; - case ARG_NOTIFY_READY: - r = parse_boolean_argument("--notify-ready=", optarg, &arg_notify_ready); + 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 r; - - arg_settings_mask |= SETTING_NOTIFY_READY; + return log_error_errno(r, "Failed to parse host port %s: %m", arg); + arg_settings_mask |= SETTING_EXPOSE_PORTS; break; - case ARG_ROOT_HASH: { - _cleanup_(iovec_done) struct iovec k = {}; + OPTION_GROUP("Security"): {} - r = unhexmem(optarg, &k.iov_base, &k.iov_len); - 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); + 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; - iovec_done(&arg_verity_settings.root_hash); - arg_verity_settings.root_hash = TAKE_STRUCT(k); + if (streq(opt->long_code, "capability")) + plus |= m; + else + minus |= m; + arg_settings_mask |= SETTING_CAPABILITY; break; } - case ARG_ROOT_HASH_SIG: { - _cleanup_(iovec_done) struct iovec p = {}; - char *value; - - 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); + 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; } - case ARG_VERITY_DATA: - r = parse_path_argument(optarg, false, &arg_verity_settings.data_path); + 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; - case ARG_SYSTEM_CALL_FILTER: { + 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; - + 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 log_error_errno(r, "Failed to parse link journal mode %s", arg); + arg_settings_mask |= SETTING_LINK_JOURNAL; break; - case 'P': - case ARG_PIPE: - arg_console_mode = CONSOLE_PIPE; - arg_settings_mask |= SETTING_CONSOLE_MODE; + 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; - case ARG_NO_PAGER: - arg_pager_flags |= PAGER_DISABLE; - break; + OPTION_GROUP("Mounts"): {} - case ARG_SET_CREDENTIAL: - r = machine_credential_set(&arg_credentials, optarg); + 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_LOAD_CREDENTIAL: - r = machine_credential_load(&arg_credentials, 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 r; + return log_error_errno(r, "Failed to parse --inaccessible= argument %s: %m", arg); + arg_settings_mask |= SETTING_CUSTOM_MOUNTS; + break; - arg_settings_mask |= SETTING_CREDENTIALS; + 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; - 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("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; - if (strv_extend(&arg_bind_user, optarg) < 0) + 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; + + OPTION_GROUP("Input/Output"): {} + + 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_SUPPRESS_SYNC: - r = parse_boolean_argument("--suppress-sync=", optarg, &arg_suppress_sync); + 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; - - arg_settings_mask |= SETTING_SUPPRESS_SYNC; break; - case ARG_IMAGE_POLICY: - r = parse_image_policy_argument(optarg, &arg_image_policy); + 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_BACKGROUND: - r = parse_background_argument(optarg, &arg_background); + 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_CLEANUP: - arg_cleanup = true; - break; + OPTION_GROUP("Other"): {} - case ARG_NO_ASK_PASSWORD: - arg_ask_password = false; + 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();