diff --git a/src/bless-boot/bless-boot.c b/src/bless-boot/bless-boot.c index e0afb3611c..3680dfdc18 100644 --- a/src/bless-boot/bless-boot.c +++ b/src/bless-boot/bless-boot.c @@ -339,7 +339,7 @@ static int make_bad(const char *prefix, uint64_t done, const char *suffix, char return 0; } -VERB(verb_status, "status", NULL, VERB_ANY, 1, VERB_DEFAULT, "Show status of current boot loader entry"); +VERB_DEFAULT_NOARG(verb_status, "status", "Show status of current boot loader entry"); static int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_free_ char *path = NULL, *prefix = NULL, *suffix = NULL, *good = NULL, *bad = NULL; uint64_t left, done; diff --git a/src/busctl/busctl.c b/src/busctl/busctl.c index 79ee1c55e8..9bf990d3db 100644 --- a/src/busctl/busctl.c +++ b/src/busctl/busctl.c @@ -176,8 +176,7 @@ static void notify_bus_error(const sd_bus_error *error) { (void) sd_notifyf(/* unset_environment= */ false, "BUSERROR=%s", error->name); } -VERB(verb_list, "list", NULL, VERB_ANY, 1, VERB_DEFAULT, - "List bus names"); +VERB_DEFAULT_NOARG(verb_list, "list", "List bus names"); static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_strv_free_ char **acquired = NULL, **activatable = NULL; _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; diff --git a/src/creds/creds.c b/src/creds/creds.c index 95af91c120..a4087fc2be 100644 --- a/src/creds/creds.c +++ b/src/creds/creds.c @@ -305,8 +305,7 @@ static int add_credentials_to_table(Table *t, bool encrypted) { return 1; /* Creds dir set */ } -VERB(verb_list, "list", NULL, VERB_ANY, 1, VERB_DEFAULT, - "Show list of passed credentials"); +VERB_DEFAULT_NOARG(verb_list, "list", "Show list of passed credentials"); static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(table_unrefp) Table *t = NULL; int r, q; diff --git a/src/factory-reset/factory-reset-tool.c b/src/factory-reset/factory-reset-tool.c index e26e948e93..c21e84c2ef 100644 --- a/src/factory-reset/factory-reset-tool.c +++ b/src/factory-reset/factory-reset-tool.c @@ -99,7 +99,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { return 1; } -VERB(verb_status, "status", NULL, VERB_ANY, 1, VERB_DEFAULT, "Report current factory reset status"); +VERB_DEFAULT_NOARG(verb_status, "status", "Report current factory reset status"); static int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) { static const int exit_status_table[_FACTORY_RESET_MODE_MAX] = { /* Report current mode also as via exit status, but only return a subset of states */ diff --git a/src/hostname/hostnamectl.c b/src/hostname/hostnamectl.c index 257b31fb88..ab21fcca1f 100644 --- a/src/hostname/hostnamectl.c +++ b/src/hostname/hostnamectl.c @@ -545,7 +545,7 @@ static int get_hostname_based_on_flag(sd_bus *bus) { return get_one_name(bus, attr, NULL); } -VERB(verb_show_status, "status", NULL, VERB_ANY, 1, VERB_DEFAULT, "Show current hostname settings"); +VERB_DEFAULT_NOARG(verb_show_status, "status", "Show current hostname settings"); static int verb_show_status(int argc, char *argv[], uintptr_t _data, void *userdata) { sd_bus *bus = userdata; int r; diff --git a/src/import/importctl.c b/src/import/importctl.c index d4a6483f36..0140d0f67d 100644 --- a/src/import/importctl.c +++ b/src/import/importctl.c @@ -833,7 +833,7 @@ static int verb_export_raw(int argc, char *argv[], uintptr_t _data, void *userda return transfer_image_common(bus, m); } -VERB(verb_list_transfers, "list-transfers", NULL, VERB_ANY, 1, VERB_DEFAULT, "Show list of transfers in progress"); +VERB_DEFAULT_NOARG(verb_list_transfers, "list-transfers", "Show list of transfers in progress"); static int verb_list_transfers(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; diff --git a/src/locale/localectl.c b/src/locale/localectl.c index e20d5da9a0..f1f9ffbce0 100644 --- a/src/locale/localectl.c +++ b/src/locale/localectl.c @@ -147,8 +147,7 @@ static int print_status_info(StatusInfo *i) { return table_print_or_warn(table); } -VERB(verb_show_status, "status", NULL, VERB_ANY, 1, VERB_DEFAULT, - "Show current locale settings"); +VERB_DEFAULT_NOARG(verb_show_status, "status", "Show current locale settings"); static int verb_show_status(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(status_info_clear) StatusInfo info = {}; static const struct bus_properties_map map[] = { diff --git a/src/login/loginctl.c b/src/login/loginctl.c index 5647621ba5..39c048e708 100644 --- a/src/login/loginctl.c +++ b/src/login/loginctl.c @@ -269,8 +269,7 @@ static int list_sessions_table_add_fallback(Table *table, sd_bus_message *reply, VERB_GROUP("Session Commands"); -VERB(verb_list_sessions, "list-sessions", NULL, VERB_ANY, 1, VERB_DEFAULT, - "List sessions"); +VERB_DEFAULT_NOARG(verb_list_sessions, "list-sessions", "List sessions"); static int verb_list_sessions(int argc, char *argv[], uintptr_t _data, void *userdata) { sd_bus *bus = ASSERT_PTR(userdata); _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; diff --git a/src/machine/machinectl.c b/src/machine/machinectl.c index 3d7f782a8c..c0f9fd51e0 100644 --- a/src/machine/machinectl.c +++ b/src/machine/machinectl.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include #include @@ -33,6 +32,7 @@ #include "format-ifname.h" #include "format-table.h" #include "format-util.h" +#include "help-util.h" #include "hostname-util.h" #include "import-util.h" #include "in-addr-util.h" @@ -44,6 +44,7 @@ #include "machine-util.h" #include "main-func.h" #include "nulstr-util.h" +#include "options.h" #include "osc-context.h" #include "output-mode.h" #include "pager.h" @@ -284,6 +285,9 @@ static int show_table(Table *table, const char *word) { return 0; } +VERB_GROUP("Machine Commands"); + +VERB_DEFAULT_NOARG(verb_list_machines, "list", "List running VMs and containers"); static int verb_list_machines(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; @@ -361,66 +365,6 @@ static int verb_list_machines(int argc, char *argv[], uintptr_t _data, void *use return show_table(table, "machines"); } -static int verb_list_images(int argc, char *argv[], uintptr_t _data, void *userdata) { - - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; - _cleanup_(table_unrefp) Table *table = NULL; - sd_bus *bus = ASSERT_PTR(userdata); - int r; - - pager_open(arg_pager_flags); - - r = bus_call_method(bus, bus_machine_mgr, "ListImages", &error, &reply, NULL); - if (r < 0) - return log_error_errno(r, "Could not get images: %s", bus_error_message(&error, r)); - - table = table_new("name", "type", "ro", "usage", "created", "modified"); - if (!table) - return log_oom(); - - if (arg_full) - table_set_width(table, 0); - - (void) table_set_align_percent(table, TABLE_HEADER_CELL(3), 100); - - r = sd_bus_message_enter_container(reply, SD_BUS_TYPE_ARRAY, "(ssbttto)"); - if (r < 0) - return bus_log_parse_error(r); - - for (;;) { - uint64_t crtime, mtime, size; - const char *name, *type; - int ro_int; - - r = sd_bus_message_read(reply, "(ssbttto)", &name, &type, &ro_int, &crtime, &mtime, &size, NULL); - if (r < 0) - return bus_log_parse_error(r); - if (r == 0) - break; - - if (name[0] == '.' && !arg_all) - continue; - - r = table_add_many(table, - TABLE_STRING, name, - TABLE_STRING, type, - TABLE_BOOLEAN, ro_int, - TABLE_SET_COLOR, ro_int ? ansi_highlight_red() : NULL, - TABLE_SIZE, size, - TABLE_TIMESTAMP, crtime, - TABLE_TIMESTAMP, mtime); - if (r < 0) - return table_log_add_error(r); - } - - r = sd_bus_message_exit_container(reply); - if (r < 0) - return bus_log_parse_error(r); - - return show_table(table, "images"); -} - static int show_unit_cgroup( sd_bus *bus, const char *unit, @@ -683,7 +627,6 @@ static int map_netif(sd_bus *bus, const char *member, sd_bus_message *m, sd_bus_ } static int show_machine_info(const char *verb, sd_bus *bus, const char *path, bool *new_line) { - static const struct bus_properties_map map[] = { { "Name", "s", NULL, offsetof(MachineStatusInfo, name) }, { "Class", "s", NULL, offsetof(MachineStatusInfo, class) }, @@ -752,6 +695,10 @@ static int show_machine_properties(sd_bus *bus, const char *path, bool *new_line return r; } +VERB(verb_show_machine, "status", "NAME…", 2, VERB_ANY, 0, + "Show VM/container details"); +VERB(verb_show_machine, "show", "[NAME…]", VERB_ANY, VERB_ANY, 0, + "Show properties of one or more VMs/containers"); static int verb_show_machine(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; bool properties, new_line = false; @@ -792,6 +739,819 @@ static int verb_show_machine(int argc, char *argv[], uintptr_t _data, void *user return r; } +static int make_service_name(const char *name, char **ret) { + int r; + + assert(name); + assert(ret); + assert(arg_runner >= 0 && arg_runner < _RUNNER_MAX); + + if (!hostname_is_valid(name, 0)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid machine name %s.", name); + + r = unit_name_build(machine_runner_unit_prefix_table[arg_runner], name, ".service", ret); + if (r < 0) + return log_error_errno(r, "Failed to build unit name: %m"); + + return 0; +} + +static int image_exists(sd_bus *bus, const char *name) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(bus); + assert(name); + + r = bus_call_method(bus, bus_machine_mgr, "GetImage", &error, NULL, "s", name); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_IMAGE)) + return 0; + + return log_error_errno(r, "Failed to check whether image %s exists: %s", name, bus_error_message(&error, r)); + } + + return 1; +} + +VERB(verb_start_machine, "start", "NAME…", 2, VERB_ANY, 0, + "Start container as a service"); +static int verb_start_machine(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(bus_wait_for_jobs_freep) BusWaitForJobs *w = NULL; + sd_bus *bus = ASSERT_PTR(userdata); + int r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + ask_password_agent_open_if_enabled(arg_transport, arg_ask_password); + + r = bus_wait_for_jobs_new(bus, &w); + if (r < 0) + return log_error_errno(r, "Could not watch jobs: %m"); + + for (int i = 1; i < argc; i++) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_free_ char *unit = NULL; + const char *object; + + r = make_service_name(argv[i], &unit); + if (r < 0) + return r; + + r = image_exists(bus, argv[i]); + if (r < 0) + return r; + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENXIO), + "Machine image '%s' does not exist.", + argv[i]); + + r = bus_call_method( + bus, + bus_systemd_mgr, + "StartUnit", + &error, + &reply, + "ss", unit, "fail"); + if (r < 0) + return log_error_errno(r, "Failed to start unit: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "o", &object); + if (r < 0) + return bus_log_parse_error(r); + + r = bus_wait_for_jobs_add(w, object); + if (r < 0) + return log_oom(); + } + + r = bus_wait_for_jobs(w, arg_quiet, NULL); + if (r < 0) + return r; + + return 0; +} + +static int parse_machine_uid(const char *spec, const char **machine, char **uid) { + /* + * Whatever is specified in the spec takes priority over global arguments. + */ + char *_uid = NULL; + const char *_machine = NULL; + + assert(uid); + assert(machine); + + if (spec) { + const char *at; + + at = strchr(spec, '@'); + if (at) { + if (at == spec) + /* Do the same as ssh and refuse "@host". */ + return -EINVAL; + + _machine = at + 1; + _uid = strndup(spec, at - spec); + if (!_uid) + return -ENOMEM; + } else + _machine = spec; + }; + + if (arg_uid && !_uid) { + _uid = strdup(arg_uid); + if (!_uid) + return -ENOMEM; + } + + *uid = _uid; + *machine = isempty(_machine) ? ".host" : _machine; + return 0; +} + +static int process_forward(sd_event *event, sd_bus_slot *machine_removed_slot, int master, PTYForwardFlags flags, const char *name) { + int r; + + assert(event); + assert(machine_removed_slot); + assert(master >= 0); + assert(name); + + if (!arg_quiet) { + if (streq(name, ".host")) + log_info("Connected to the local host. Press ^] three times within 1s to exit session."); + else + log_info("Connected to machine %s. Press ^] three times within 1s to exit session.", name); + } + + _cleanup_(osc_context_closep) sd_id128_t osc_context_id = SD_ID128_NULL; + if (!terminal_is_dumb()) { + r = osc_context_open_container(name, /* ret_seq= */ NULL, &osc_context_id); + if (r < 0) + return r; + } + + r = sd_event_set_signal_exit(event, true); + if (r < 0) + return log_error_errno(r, "Failed to enable SIGINT/SITERM handling: %m"); + + _cleanup_(pty_forward_freep) PTYForward *forward = NULL; + r = pty_forward_new(event, master, flags, &forward); + if (r < 0) + return log_error_errno(r, "Failed to create PTY forwarder: %m"); + + /* No userdata should not set previously. */ + assert_se(!sd_bus_slot_set_userdata(machine_removed_slot, forward)); + + r = sd_event_loop(event); + if (r < 0) + return log_error_errno(r, "Failed to run event loop: %m"); + + bool machine_died = + FLAGS_SET(flags, PTY_FORWARD_IGNORE_VHANGUP) && + !pty_forward_vhangup_honored(forward); + + if (!arg_quiet) { + if (machine_died) + log_info("Machine %s terminated.", name); + else if (streq(name, ".host")) + log_info("Connection to the local host terminated."); + else + log_info("Connection to machine %s terminated.", name); + } + + return 0; +} + +static int on_machine_removed(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { + PTYForward *forward = ASSERT_PTR(userdata); + int r; + + assert(m); + + /* Tell the forwarder to exit on the next vhangup(), so that we still flush out what might be queued + * and exit then. */ + + r = pty_forward_honor_vhangup(forward); + if (r < 0) { + /* On error, quit immediately. */ + log_error_errno(r, "Failed to make PTY forwarder honor vhangup(): %m"); + (void) sd_event_exit(sd_bus_get_event(sd_bus_message_get_bus(m)), EXIT_FAILURE); + } + + return 0; +} + +VERB(verb_login_machine, "login", "[NAME]", VERB_ANY, 2, 0, + "Get a login prompt in a container or on the local host"); +static int verb_login_machine(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_slot_unrefp) sd_bus_slot *slot = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + int master = -1, r; + sd_bus *bus = ASSERT_PTR(userdata); + const char *match, *machine; + + if (!strv_isempty(arg_setenv) || arg_uid) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--setenv= and --uid= are not supported for 'login'. Use 'shell' instead."); + + if (!IN_SET(arg_transport, BUS_TRANSPORT_LOCAL, BUS_TRANSPORT_MACHINE)) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Login only supported on local machines."); + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + r = sd_event_default(&event); + if (r < 0) + return log_error_errno(r, "Failed to get event loop: %m"); + + r = sd_bus_attach_event(bus, event, 0); + if (r < 0) + return log_error_errno(r, "Failed to attach bus to event loop: %m"); + + machine = argc < 2 || isempty(argv[1]) ? ".host" : argv[1]; + + match = strjoina("type='signal'," + "sender='org.freedesktop.machine1'," + "path='/org/freedesktop/machine1',", + "interface='org.freedesktop.machine1.Manager'," + "member='MachineRemoved'," + "arg0='", machine, "'"); + + r = sd_bus_add_match_async(bus, &slot, match, on_machine_removed, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to request machine removal match: %m"); + + r = bus_call_method(bus, bus_machine_mgr, "OpenMachineLogin", &error, &reply, "s", machine); + if (r < 0) + return log_error_errno(r, "Failed to get login PTY: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "hs", &master, NULL); + if (r < 0) + return bus_log_parse_error(r); + + return process_forward(event, slot, master, PTY_FORWARD_IGNORE_VHANGUP, machine); +} + +VERB(verb_shell_machine, "shell", "[[USER@]NAME [COMMAND…]]", VERB_ANY, VERB_ANY, 0, + "Invoke a shell (or other command) in a container or on the local host"); +static int verb_shell_machine(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL, *m = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_slot_unrefp) sd_bus_slot *slot = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + int master = -1, r; + sd_bus *bus = ASSERT_PTR(userdata); + const char *match, *machine, *path; + _cleanup_free_ char *uid = NULL; + + if (!IN_SET(arg_transport, BUS_TRANSPORT_LOCAL, BUS_TRANSPORT_MACHINE)) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Shell only supported on local machines."); + + if (terminal_is_dumb()) { + /* Set TERM=dumb if we are running on a dumb terminal or with a pipe. + * Otherwise, we will get unwanted OSC sequences. */ + if (!strv_find_prefix(arg_setenv, "TERM=")) + if (strv_extend(&arg_setenv, "TERM=dumb") < 0) + return log_oom(); + } else + /* Pass $TERM & Co. to shell session, if not explicitly specified. */ + FOREACH_STRING(v, "TERM=", "COLORTERM=", "NO_COLOR=") { + if (strv_find_prefix(arg_setenv, v)) + continue; + + const char *t = strv_find_prefix(environ, v); + if (!t) + continue; + + if (strv_extend(&arg_setenv, t) < 0) + return log_oom(); + } + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + r = sd_event_default(&event); + if (r < 0) + return log_error_errno(r, "Failed to get event loop: %m"); + + r = sd_bus_attach_event(bus, event, 0); + if (r < 0) + return log_error_errno(r, "Failed to attach bus to event loop: %m"); + + r = parse_machine_uid(argc >= 2 ? argv[1] : NULL, &machine, &uid); + if (r < 0) + return log_error_errno(r, "Failed to parse machine specification: %m"); + + match = strjoina("type='signal'," + "sender='org.freedesktop.machine1'," + "path='/org/freedesktop/machine1',", + "interface='org.freedesktop.machine1.Manager'," + "member='MachineRemoved'," + "arg0='", machine, "'"); + + r = sd_bus_add_match_async(bus, &slot, match, on_machine_removed, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to request machine removal match: %m"); + + r = bus_message_new_method_call(bus, &m, bus_machine_mgr, "OpenMachineShell"); + if (r < 0) + return bus_log_create_error(r); + + path = argc < 3 || isempty(argv[2]) ? NULL : argv[2]; + + r = sd_bus_message_append(m, "sss", machine, uid, path); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append_strv(m, strv_length(argv) <= 3 ? NULL : argv + 2); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append_strv(m, arg_setenv); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, 0, &error, &reply); + if (r < 0) + return log_error_errno(r, "Failed to get shell PTY: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "hs", &master, NULL); + if (r < 0) + return bus_log_parse_error(r); + + return process_forward(event, slot, master, /* flags= */ 0, machine); +} + +static int verb_poweroff_machine(int argc, char *argv[], uintptr_t data, void *userdata); + +VERB(verb_enable_machine, "enable", "NAME…", 2, VERB_ANY, 0, + "Enable automatic container start at boot"); +VERB(verb_enable_machine, "disable", "NAME…", 2, VERB_ANY, 0, + "Disable automatic container start at boot"); +static int verb_enable_machine(int argc, char *argv[], uintptr_t data, void *userdata) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + const char *method; + sd_bus *bus = ASSERT_PTR(userdata); + int r; + bool enable; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + enable = streq(argv[0], "enable"); + method = enable ? "EnableUnitFiles" : "DisableUnitFiles"; + + r = bus_message_new_method_call(bus, &m, bus_systemd_mgr, method); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_open_container(m, 'a', "s"); + if (r < 0) + return bus_log_create_error(r); + + if (enable) { + r = sd_bus_message_append(m, "s", "machines.target"); + if (r < 0) + return bus_log_create_error(r); + } + + for (int i = 1; i < argc; i++) { + _cleanup_free_ char *unit = NULL; + + r = make_service_name(argv[i], &unit); + if (r < 0) + return r; + + r = image_exists(bus, argv[i]); + if (r < 0) + return r; + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENXIO), + "Machine image '%s' does not exist.", + argv[i]); + + r = sd_bus_message_append(m, "s", unit); + if (r < 0) + return bus_log_create_error(r); + } + + r = sd_bus_message_close_container(m); + if (r < 0) + return bus_log_create_error(r); + + if (enable) + r = sd_bus_message_append(m, "bb", false, false); + else + r = sd_bus_message_append(m, "b", false); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, 0, &error, &reply); + if (r < 0) + return log_error_errno(r, "Failed to enable or disable unit: %s", bus_error_message(&error, r)); + + if (enable) { + r = sd_bus_message_read(reply, "b", NULL); + if (r < 0) + return bus_log_parse_error(r); + } + + r = bus_deserialize_and_dump_unit_file_changes(reply, arg_quiet); + if (r < 0) + return r; + + r = bus_service_manager_reload(bus); + if (r < 0) + return r; + + if (arg_now) { + _cleanup_strv_free_ char **new_args = NULL; + + new_args = strv_new(enable ? "start" : "poweroff"); + if (!new_args) + return log_oom(); + + r = strv_extend_strv(&new_args, argv + 1, /* filter_duplicates= */ false); + if (r < 0) + return log_oom(); + + if (enable) + return verb_start_machine(strv_length(new_args), new_args, data, userdata); + + return verb_poweroff_machine(strv_length(new_args), new_args, data, userdata); + } + + return 0; +} + +/* Look up the controlAddress of a machine by calling machined's Machine.List varlink interface. */ +static int machine_get_control_address(const char *machine_name, char **ret) { + _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; + sd_json_variant *reply = NULL; + const char *error_id = NULL; + int r; + + assert(machine_name); + assert(ret); + + if (arg_transport != BUS_TRANSPORT_LOCAL) + return -EOPNOTSUPP; + + _cleanup_free_ char *p = NULL; + r = runtime_directory_generic(arg_runtime_scope, "systemd/machine/io.systemd.Machine", &p); + if (r < 0) + return log_error_errno(r, "Failed to determine Machine varlink socket path: %m"); + + r = sd_varlink_connect_address(&vl, p); + if (r < 0) + return log_error_errno(r, "Failed to connect to machined varlink: %m"); + + r = sd_varlink_callbo( + vl, + "io.systemd.Machine.List", + &reply, + &error_id, + SD_JSON_BUILD_PAIR_STRING("name", machine_name)); + if (r < 0) + return log_error_errno(r, "Failed to list machine: %m"); + if (error_id) + return log_error_errno(sd_varlink_error_to_errno(error_id, reply), + "Failed to look up machine '%s': %s", machine_name, error_id); + + sd_json_variant *addr = sd_json_variant_by_key(reply, "controlAddress"); + if (!addr || !sd_json_variant_is_string(addr)) + return -EOPNOTSUPP; /* No varlink control socket, caller decides whether to log */ + + char *a = strdup(sd_json_variant_string(addr)); + if (!a) + return log_oom(); + + *ret = a; + return 0; +} + +static int verb_machine_control_one(const char *machine_name, const char *method) { + _cleanup_free_ char *address = NULL; + int r; + + r = machine_get_control_address(machine_name, &address); + if (r < 0) + return r; + + _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; + r = sd_varlink_connect_address(&vl, address); + if (r < 0) + return log_error_errno(r, "Failed to connect to machine control socket: %m"); + + sd_json_variant *reply = NULL; + const char *error_id = NULL; + r = sd_varlink_call(vl, method, /* parameters= */ NULL, &reply, &error_id); + if (r < 0) + return log_error_errno(r, "Failed to call %s: %m", method); + if (error_id) + return log_error_errno(sd_varlink_error_to_errno(error_id, reply), + "Machine control call failed: %s", error_id); + + return 0; +} + +VERB(verb_poweroff_machine, "poweroff", "NAME…", 2, VERB_ANY, 0, + "Power off one or more machines"); +VERB(verb_poweroff_machine, "stop", "NAME…", 2, VERB_ANY, 0, + /* help= */ NULL); /* Convenience alias */ +static int verb_poweroff_machine(int argc, char *argv[], uintptr_t data, void *userdata) { + sd_bus *bus = ASSERT_PTR(userdata); + int r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + for (int i = 1; i < argc; i++) { + /* VM with varlink control socket: QMP graceful powerdown */ + r = verb_machine_control_one(argv[i], "io.systemd.MachineInstance.PowerOff"); + if (r >= 0) + continue; + if (r != -EOPNOTSUPP) + return r; + + /* Not a VM: signal-based poweroff */ + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + r = bus_call_method(bus, bus_machine_mgr, "KillMachine", &error, NULL, + "ssi", argv[i], "leader", (int32_t) (SIGRTMIN+4)); + if (r < 0) + return log_error_errno(r, "Could not kill machine: %s", bus_error_message(&error, r)); + } + + return 0; +} + +VERB(verb_reboot_machine, "reboot", "NAME…", 2, VERB_ANY, 0, + "Reboot one or more machines"); +VERB(verb_reboot_machine, "restart", "NAME…", 2, VERB_ANY, 0, + /* help= */ NULL); /* Convenience alias */ +static int verb_reboot_machine(int argc, char *argv[], uintptr_t data, void *userdata) { + sd_bus *bus = ASSERT_PTR(userdata); + int r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + for (int i = 1; i < argc; i++) { + r = verb_machine_control_one(argv[i], "io.systemd.MachineInstance.Reboot"); + if (r >= 0) + continue; + if (r != -EOPNOTSUPP) + return r; + + /* Container fallback: SIGINT to init (sysvinit + systemd compatible) */ + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + r = bus_call_method(bus, bus_machine_mgr, "KillMachine", &error, NULL, + "ssi", argv[i], "leader", (int32_t) SIGINT); + if (r < 0) + return log_error_errno(r, "Could not reboot machine '%s': %s", argv[i], bus_error_message(&error, r)); + } + + return 0; +} + +static int verb_vm_control(int argc, char *argv[], const char *method) { + int r; + + for (int i = 1; i < argc; i++) { + r = verb_machine_control_one(argv[i], method); + if (r == -EOPNOTSUPP) + return log_error_errno(r, "Machine '%s' does not expose a varlink control socket.", argv[i]); + if (r < 0) + return r; + } + + return 0; +} + +VERB(verb_pause, "pause", "NAME…", 2, VERB_ANY, 0, + "Pause one or more machines"); +static int verb_pause(int argc, char *argv[], uintptr_t _data, void *userdata) { + return verb_vm_control(argc, argv, "io.systemd.MachineInstance.Pause"); +} + +VERB(verb_resume, "resume", "NAME…", 2, VERB_ANY, 0, + "Resume one or more paused machines"); +static int verb_resume(int argc, char *argv[], uintptr_t _data, void *userdata) { + return verb_vm_control(argc, argv, "io.systemd.MachineInstance.Resume"); +} + +VERB(verb_terminate_machine, "terminate", "NAME…", 2, VERB_ANY, 0, + "Terminate one or more machines"); +static int verb_terminate_machine(int argc, char *argv[], uintptr_t _data, void *userdata) { + sd_bus *bus = ASSERT_PTR(userdata); + int r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + for (int i = 1; i < argc; i++) { + /* VM with varlink control socket: QMP quit (immediate termination) */ + r = verb_machine_control_one(argv[i], "io.systemd.MachineInstance.Terminate"); + if (r >= 0) + continue; + if (r != -EOPNOTSUPP) + return r; + + /* Not a VM or no varlink socket: fall back to machined */ + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + r = bus_call_method(bus, bus_machine_mgr, "TerminateMachine", &error, NULL, "s", argv[i]); + if (r < 0) + return log_error_errno(r, "Could not terminate machine: %s", bus_error_message(&error, r)); + } + + return 0; +} + +VERB(verb_kill_machine, "kill", "NAME…", 2, VERB_ANY, 0, + "Send signal to processes of a machine"); +static int verb_kill_machine(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus *bus = ASSERT_PTR(userdata); + int r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + if (!arg_kill_whom) + arg_kill_whom = "all"; + + for (int i = 1; i < argc; i++) { + r = bus_call_method( + bus, + bus_machine_mgr, + "KillMachine", + &error, + NULL, + "ssi", argv[i], arg_kill_whom, arg_signal); + if (r < 0) + return log_error_errno(r, "Could not kill machine: %s", bus_error_message(&error, r)); + } + + return 0; +} + +static const char* select_copy_method(bool copy_from, bool force) { + if (force) + return copy_from ? "CopyFromMachineWithFlags" : "CopyToMachineWithFlags"; + else + return copy_from ? "CopyFromMachine" : "CopyToMachine"; +} + +VERB(verb_copy_files, "copy-to", "NAME PATH [PATH]", 3, 4, 0, + "Copy files from the host to a container"); +VERB(verb_copy_files, "copy-from", "NAME PATH [PATH]", 3, 4, 0, + "Copy files from a container to the host"); +static int verb_copy_files(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_free_ char *abs_host_path = NULL; + char *dest, *host_path, *container_path; + sd_bus *bus = ASSERT_PTR(userdata); + bool copy_from; + int r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + copy_from = streq(argv[0], "copy-from"); + dest = argv[3] ?: argv[2]; + host_path = copy_from ? dest : argv[2]; + container_path = copy_from ? argv[2] : dest; + + if (!path_is_absolute(host_path)) { + r = path_make_absolute_cwd(host_path, &abs_host_path); + if (r < 0) + return log_error_errno(r, "Failed to make path absolute: %m"); + + host_path = abs_host_path; + } + + r = bus_message_new_method_call( + bus, + &m, + bus_machine_mgr, + select_copy_method(copy_from, arg_force)); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append( + m, + "sss", + argv[1], + copy_from ? container_path : host_path, + copy_from ? host_path : container_path); + if (r < 0) + return bus_log_create_error(r); + + if (arg_force) { + r = sd_bus_message_append(m, "t", (uint64_t) MACHINE_COPY_REPLACE); + if (r < 0) + return bus_log_create_error(r); + } + + /* This is a slow operation, hence turn off any method call timeouts */ + r = sd_bus_call(bus, m, USEC_INFINITY, &error, NULL); + if (r < 0) + return log_error_errno(r, "Failed to copy: %s", bus_error_message(&error, r)); + + return 0; +} + +VERB(verb_bind_mount, "bind", "NAME PATH [PATH]", 3, 4, 0, + "Bind mount a path from the host into a container"); +static int verb_bind_mount(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus *bus = ASSERT_PTR(userdata); + int r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + r = bus_call_method( + bus, + bus_machine_mgr, + "BindMountMachine", + &error, + NULL, + "sssbb", + argv[1], + argv[2], + argv[3], + arg_read_only, + arg_mkdir); + if (r < 0) + return log_error_errno(r, "Failed to bind mount: %s", bus_error_message(&error, r)); + + return 0; +} + +VERB_GROUP("Image Commands"); + +VERB(verb_list_images, "list-images", /* argspec= */ NULL, VERB_ANY, 1, 0, + "Show available container and VM images"); +static int verb_list_images(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(table_unrefp) Table *table = NULL; + sd_bus *bus = ASSERT_PTR(userdata); + int r; + + pager_open(arg_pager_flags); + + r = bus_call_method(bus, bus_machine_mgr, "ListImages", &error, &reply, NULL); + if (r < 0) + return log_error_errno(r, "Could not get images: %s", bus_error_message(&error, r)); + + table = table_new("name", "type", "ro", "usage", "created", "modified"); + if (!table) + return log_oom(); + + if (arg_full) + table_set_width(table, 0); + + (void) table_set_align_percent(table, TABLE_HEADER_CELL(3), 100); + + r = sd_bus_message_enter_container(reply, SD_BUS_TYPE_ARRAY, "(ssbttto)"); + if (r < 0) + return bus_log_parse_error(r); + + for (;;) { + uint64_t crtime, mtime, size; + const char *name, *type; + int ro_int; + + r = sd_bus_message_read(reply, "(ssbttto)", &name, &type, &ro_int, &crtime, &mtime, &size, NULL); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + if (name[0] == '.' && !arg_all) + continue; + + r = table_add_many(table, + TABLE_STRING, name, + TABLE_STRING, type, + TABLE_BOOLEAN, ro_int, + TABLE_SET_COLOR, ro_int ? ansi_highlight_red() : NULL, + TABLE_SIZE, size, + TABLE_TIMESTAMP, crtime, + TABLE_TIMESTAMP, mtime); + if (r < 0) + return table_log_add_error(r); + } + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + return show_table(table, "images"); +} + static int print_image_hostname(sd_bus *bus, const char *name) { _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; const char *hn; @@ -927,7 +1687,6 @@ static void print_image_status_info(sd_bus *bus, ImageStatusInfo *i) { } static int show_image_info(sd_bus *bus, const char *path, bool *new_line) { - static const struct bus_properties_map map[] = { { "Name", "s", NULL, offsetof(ImageStatusInfo, name) }, { "Path", "s", NULL, offsetof(ImageStatusInfo, path) }, @@ -989,7 +1748,6 @@ static void print_pool_status_info(sd_bus *bus, PoolStatusInfo *i) { } static int show_pool_info(sd_bus *bus) { - static const struct bus_properties_map map[] = { { "PoolPath", "s", NULL, offsetof(PoolStatusInfo, path) }, { "PoolUsage", "t", NULL, offsetof(PoolStatusInfo, usage) }, @@ -1043,6 +1801,10 @@ static int show_image_properties(sd_bus *bus, const char *path, bool *new_line) return r; } +VERB(verb_show_image, "image-status", "[NAME…]", VERB_ANY, VERB_ANY, 0, + "Show image details"); +VERB(verb_show_image, "show-image", "[NAME…]", VERB_ANY, VERB_ANY, 0, + "Show properties of image"); static int verb_show_image(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; bool properties, new_line = false; @@ -1087,650 +1849,6 @@ static int verb_show_image(int argc, char *argv[], uintptr_t _data, void *userda return r; } -static int verb_kill_machine(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - sd_bus *bus = ASSERT_PTR(userdata); - int r; - - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); - - if (!arg_kill_whom) - arg_kill_whom = "all"; - - for (int i = 1; i < argc; i++) { - r = bus_call_method( - bus, - bus_machine_mgr, - "KillMachine", - &error, - NULL, - "ssi", argv[i], arg_kill_whom, arg_signal); - if (r < 0) - return log_error_errno(r, "Could not kill machine: %s", bus_error_message(&error, r)); - } - - return 0; -} - -static int verb_machine_control_one(const char *machine_name, const char *method); - -static int verb_reboot_machine(int argc, char *argv[], uintptr_t data, void *userdata) { - sd_bus *bus = ASSERT_PTR(userdata); - int r; - - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); - - for (int i = 1; i < argc; i++) { - r = verb_machine_control_one(argv[i], "io.systemd.MachineInstance.Reboot"); - if (r >= 0) - continue; - if (r != -EOPNOTSUPP) - return r; - - /* Container fallback: SIGINT to init (sysvinit + systemd compatible) */ - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - r = bus_call_method(bus, bus_machine_mgr, "KillMachine", &error, NULL, - "ssi", argv[i], "leader", (int32_t) SIGINT); - if (r < 0) - return log_error_errno(r, "Could not reboot machine '%s': %s", argv[i], bus_error_message(&error, r)); - } - - return 0; -} - -static int verb_poweroff_machine(int argc, char *argv[], uintptr_t data, void *userdata) { - sd_bus *bus = ASSERT_PTR(userdata); - int r; - - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); - - for (int i = 1; i < argc; i++) { - /* VM with varlink control socket: QMP graceful powerdown */ - r = verb_machine_control_one(argv[i], "io.systemd.MachineInstance.PowerOff"); - if (r >= 0) - continue; - if (r != -EOPNOTSUPP) - return r; - - /* Not a VM: signal-based poweroff */ - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - r = bus_call_method(bus, bus_machine_mgr, "KillMachine", &error, NULL, - "ssi", argv[i], "leader", (int32_t) (SIGRTMIN+4)); - if (r < 0) - return log_error_errno(r, "Could not kill machine: %s", bus_error_message(&error, r)); - } - - return 0; -} - -static int verb_terminate_machine(int argc, char *argv[], uintptr_t _data, void *userdata) { - sd_bus *bus = ASSERT_PTR(userdata); - int r; - - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); - - for (int i = 1; i < argc; i++) { - /* VM with varlink control socket: QMP quit (immediate termination) */ - r = verb_machine_control_one(argv[i], "io.systemd.MachineInstance.Terminate"); - if (r >= 0) - continue; - if (r != -EOPNOTSUPP) - return r; - - /* Not a VM or no varlink socket: fall back to machined */ - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - r = bus_call_method(bus, bus_machine_mgr, "TerminateMachine", &error, NULL, "s", argv[i]); - if (r < 0) - return log_error_errno(r, "Could not terminate machine: %s", bus_error_message(&error, r)); - } - - return 0; -} - -/* Look up the controlAddress of a machine by calling machined's Machine.List varlink interface. */ -static int machine_get_control_address(const char *machine_name, char **ret) { - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; - sd_json_variant *reply = NULL; - const char *error_id = NULL; - int r; - - assert(machine_name); - assert(ret); - - if (arg_transport != BUS_TRANSPORT_LOCAL) - return -EOPNOTSUPP; - - _cleanup_free_ char *p = NULL; - r = runtime_directory_generic(arg_runtime_scope, "systemd/machine/io.systemd.Machine", &p); - if (r < 0) - return log_error_errno(r, "Failed to determine Machine varlink socket path: %m"); - - r = sd_varlink_connect_address(&vl, p); - if (r < 0) - return log_error_errno(r, "Failed to connect to machined varlink: %m"); - - r = sd_varlink_callbo( - vl, - "io.systemd.Machine.List", - &reply, - &error_id, - SD_JSON_BUILD_PAIR_STRING("name", machine_name)); - if (r < 0) - return log_error_errno(r, "Failed to list machine: %m"); - if (error_id) - return log_error_errno(sd_varlink_error_to_errno(error_id, reply), - "Failed to look up machine '%s': %s", machine_name, error_id); - - sd_json_variant *addr = sd_json_variant_by_key(reply, "controlAddress"); - if (!addr || !sd_json_variant_is_string(addr)) - return -EOPNOTSUPP; /* No varlink control socket, caller decides whether to log */ - - char *a = strdup(sd_json_variant_string(addr)); - if (!a) - return log_oom(); - - *ret = a; - return 0; -} - -static int verb_machine_control_one(const char *machine_name, const char *method) { - _cleanup_free_ char *address = NULL; - int r; - - r = machine_get_control_address(machine_name, &address); - if (r < 0) - return r; - - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; - r = sd_varlink_connect_address(&vl, address); - if (r < 0) - return log_error_errno(r, "Failed to connect to machine control socket: %m"); - - sd_json_variant *reply = NULL; - const char *error_id = NULL; - r = sd_varlink_call(vl, method, /* parameters= */ NULL, &reply, &error_id); - if (r < 0) - return log_error_errno(r, "Failed to call %s: %m", method); - if (error_id) - return log_error_errno(sd_varlink_error_to_errno(error_id, reply), - "Machine control call failed: %s", error_id); - - return 0; -} - -static int verb_vm_control(int argc, char *argv[], const char *method) { - int r; - - for (int i = 1; i < argc; i++) { - r = verb_machine_control_one(argv[i], method); - if (r == -EOPNOTSUPP) - return log_error_errno(r, "Machine '%s' does not expose a varlink control socket.", argv[i]); - if (r < 0) - return r; - } - - return 0; -} - -static int verb_pause(int argc, char *argv[], uintptr_t _data, void *userdata) { - return verb_vm_control(argc, argv, "io.systemd.MachineInstance.Pause"); -} - -static int verb_resume(int argc, char *argv[], uintptr_t _data, void *userdata) { - return verb_vm_control(argc, argv, "io.systemd.MachineInstance.Resume"); -} - -static const char *select_copy_method(bool copy_from, bool force) { - if (force) - return copy_from ? "CopyFromMachineWithFlags" : "CopyToMachineWithFlags"; - else - return copy_from ? "CopyFromMachine" : "CopyToMachine"; -} - -static int verb_copy_files(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; - _cleanup_free_ char *abs_host_path = NULL; - char *dest, *host_path, *container_path; - sd_bus *bus = ASSERT_PTR(userdata); - bool copy_from; - int r; - - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); - - copy_from = streq(argv[0], "copy-from"); - dest = argv[3] ?: argv[2]; - host_path = copy_from ? dest : argv[2]; - container_path = copy_from ? argv[2] : dest; - - if (!path_is_absolute(host_path)) { - r = path_make_absolute_cwd(host_path, &abs_host_path); - if (r < 0) - return log_error_errno(r, "Failed to make path absolute: %m"); - - host_path = abs_host_path; - } - - r = bus_message_new_method_call( - bus, - &m, - bus_machine_mgr, - select_copy_method(copy_from, arg_force)); - if (r < 0) - return bus_log_create_error(r); - - r = sd_bus_message_append( - m, - "sss", - argv[1], - copy_from ? container_path : host_path, - copy_from ? host_path : container_path); - if (r < 0) - return bus_log_create_error(r); - - if (arg_force) { - r = sd_bus_message_append(m, "t", (uint64_t) MACHINE_COPY_REPLACE); - if (r < 0) - return bus_log_create_error(r); - } - - /* This is a slow operation, hence turn off any method call timeouts */ - r = sd_bus_call(bus, m, USEC_INFINITY, &error, NULL); - if (r < 0) - return log_error_errno(r, "Failed to copy: %s", bus_error_message(&error, r)); - - return 0; -} - -static int verb_bind_volume(int argc, char *argv[], uintptr_t _data, void *userdata) { - int r; - - if (arg_transport != BUS_TRANSPORT_LOCAL) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), - "bind-volume is only supported on the local transport."); - - _cleanup_(bind_volume_freep) BindVolume *bv = NULL; - r = bind_volume_parse(argv[2], &bv); - if (r < 0) - return log_error_errno(r, "Failed to parse bind-volume argument '%s': %m", argv[2]); - - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); - - /* Locate and connect to the target machine before acquiring storage, so a missing - * machine doesn't trigger 'create=new' side effects on the StorageProvider. */ - _cleanup_free_ char *address = NULL; - r = machine_get_control_address(argv[1], &address); - if (r == -EOPNOTSUPP) - return log_error_errno(r, "Machine '%s' does not expose a varlink control socket.", argv[1]); - if (r < 0) - return r; - - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; - r = sd_varlink_connect_address(&vl, address); - if (r < 0) - return log_error_errno(r, "Failed to connect to machine control socket %s: %m", address); - - r = sd_varlink_set_allow_fd_passing_output(vl, true); - if (r < 0) - return log_error_errno(r, "Failed to enable fd passing on varlink connection: %m"); - - _cleanup_(storage_acquire_reply_done) StorageAcquireReply reply = STORAGE_ACQUIRE_REPLY_INIT; - _cleanup_free_ char *acquire_error_id = NULL; - r = storage_acquire_volume(arg_runtime_scope, bv, arg_ask_password, &acquire_error_id, &reply); - if (r < 0) { - if (acquire_error_id) - return log_error_errno(r, "Failed to acquire storage volume '%s:%s' from provider: %s", - bv->provider, bv->volume, acquire_error_id); - return log_error_errno(r, "Failed to acquire storage volume '%s:%s' from provider: %m", - bv->provider, bv->volume); - } - - int fd_index = sd_varlink_push_fd(vl, reply.fd); - if (fd_index < 0) - return log_error_errno(fd_index, "Failed to push storage fd onto varlink connection: %m"); - TAKE_FD(reply.fd); - - _cleanup_free_ char *name = strjoin(bv->provider, ":", bv->volume); - if (!name) - return log_oom(); - - sd_json_variant *vl_reply = NULL; - const char *error_id = NULL; - r = sd_varlink_callbo( - vl, - "io.systemd.MachineInstance.AddStorage", - &vl_reply, &error_id, - SD_JSON_BUILD_PAIR_INTEGER("fileDescriptorIndex", fd_index), - SD_JSON_BUILD_PAIR_STRING("name", name), - JSON_BUILD_PAIR_STRING_NON_EMPTY("config", bv->config)); - if (r < 0) - return log_error_errno(r, "Failed to call io.systemd.MachineInstance.AddStorage: %m"); - if (error_id) - return log_error_errno(sd_varlink_error_to_errno(error_id, vl_reply), - "AddStorage failed for '%s': %s", name, error_id); - - return 0; -} - -static int verb_unbind_volume(int argc, char *argv[], uintptr_t _data, void *userdata) { - int r; - - if (arg_transport != BUS_TRANSPORT_LOCAL) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), - "unbind-volume is only supported on the local transport."); - - r = machine_storage_name_split(argv[2], /* ret_provider= */ NULL, /* ret_volume= */ NULL); - if (r == -ENOMEM) - return log_oom(); - if (r < 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Invalid unbind-volume name '%s', expected ':'.", argv[2]); - - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); - - _cleanup_free_ char *address = NULL; - r = machine_get_control_address(argv[1], &address); - if (r == -EOPNOTSUPP) - return log_error_errno(r, "Machine '%s' does not expose a varlink control socket.", argv[1]); - if (r < 0) - return r; - - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; - r = sd_varlink_connect_address(&vl, address); - if (r < 0) - return log_error_errno(r, "Failed to connect to machine control socket %s: %m", address); - - sd_json_variant *reply = NULL; - const char *error_id = NULL; - r = sd_varlink_callbo( - vl, - "io.systemd.MachineInstance.RemoveStorage", - &reply, &error_id, - SD_JSON_BUILD_PAIR_STRING("name", argv[2])); - if (r < 0) - return log_error_errno(r, "Failed to call io.systemd.MachineInstance.RemoveStorage: %m"); - if (error_id) - return log_error_errno(sd_varlink_error_to_errno(error_id, reply), - "RemoveStorage failed for '%s': %s", argv[2], error_id); - - return 0; -} - -static int verb_bind_mount(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - sd_bus *bus = ASSERT_PTR(userdata); - int r; - - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); - - r = bus_call_method( - bus, - bus_machine_mgr, - "BindMountMachine", - &error, - NULL, - "sssbb", - argv[1], - argv[2], - argv[3], - arg_read_only, - arg_mkdir); - if (r < 0) - return log_error_errno(r, "Failed to bind mount: %s", bus_error_message(&error, r)); - - return 0; -} - -static int on_machine_removed(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { - PTYForward *forward = ASSERT_PTR(userdata); - int r; - - assert(m); - - /* Tell the forwarder to exit on the next vhangup(), so that we still flush out what might be queued - * and exit then. */ - - r = pty_forward_honor_vhangup(forward); - if (r < 0) { - /* On error, quit immediately. */ - log_error_errno(r, "Failed to make PTY forwarder honor vhangup(): %m"); - (void) sd_event_exit(sd_bus_get_event(sd_bus_message_get_bus(m)), EXIT_FAILURE); - } - - return 0; -} - -static int process_forward(sd_event *event, sd_bus_slot *machine_removed_slot, int master, PTYForwardFlags flags, const char *name) { - int r; - - assert(event); - assert(machine_removed_slot); - assert(master >= 0); - assert(name); - - if (!arg_quiet) { - if (streq(name, ".host")) - log_info("Connected to the local host. Press ^] three times within 1s to exit session."); - else - log_info("Connected to machine %s. Press ^] three times within 1s to exit session.", name); - } - - _cleanup_(osc_context_closep) sd_id128_t osc_context_id = SD_ID128_NULL; - if (!terminal_is_dumb()) { - r = osc_context_open_container(name, /* ret_seq= */ NULL, &osc_context_id); - if (r < 0) - return r; - } - - r = sd_event_set_signal_exit(event, true); - if (r < 0) - return log_error_errno(r, "Failed to enable SIGINT/SITERM handling: %m"); - - _cleanup_(pty_forward_freep) PTYForward *forward = NULL; - r = pty_forward_new(event, master, flags, &forward); - if (r < 0) - return log_error_errno(r, "Failed to create PTY forwarder: %m"); - - /* No userdata should not set previously. */ - assert_se(!sd_bus_slot_set_userdata(machine_removed_slot, forward)); - - r = sd_event_loop(event); - if (r < 0) - return log_error_errno(r, "Failed to run event loop: %m"); - - bool machine_died = - FLAGS_SET(flags, PTY_FORWARD_IGNORE_VHANGUP) && - !pty_forward_vhangup_honored(forward); - - if (!arg_quiet) { - if (machine_died) - log_info("Machine %s terminated.", name); - else if (streq(name, ".host")) - log_info("Connection to the local host terminated."); - else - log_info("Connection to machine %s terminated.", name); - } - - return 0; -} - -static int parse_machine_uid(const char *spec, const char **machine, char **uid) { - /* - * Whatever is specified in the spec takes priority over global arguments. - */ - char *_uid = NULL; - const char *_machine = NULL; - - assert(uid); - assert(machine); - - if (spec) { - const char *at; - - at = strchr(spec, '@'); - if (at) { - if (at == spec) - /* Do the same as ssh and refuse "@host". */ - return -EINVAL; - - _machine = at + 1; - _uid = strndup(spec, at - spec); - if (!_uid) - return -ENOMEM; - } else - _machine = spec; - }; - - if (arg_uid && !_uid) { - _uid = strdup(arg_uid); - if (!_uid) - return -ENOMEM; - } - - *uid = _uid; - *machine = isempty(_machine) ? ".host" : _machine; - return 0; -} - -static int verb_login_machine(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_slot_unrefp) sd_bus_slot *slot = NULL; - _cleanup_(sd_event_unrefp) sd_event *event = NULL; - int master = -1, r; - sd_bus *bus = ASSERT_PTR(userdata); - const char *match, *machine; - - if (!strv_isempty(arg_setenv) || arg_uid) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "--setenv= and --uid= are not supported for 'login'. Use 'shell' instead."); - - if (!IN_SET(arg_transport, BUS_TRANSPORT_LOCAL, BUS_TRANSPORT_MACHINE)) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), - "Login only supported on local machines."); - - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); - - r = sd_event_default(&event); - if (r < 0) - return log_error_errno(r, "Failed to get event loop: %m"); - - r = sd_bus_attach_event(bus, event, 0); - if (r < 0) - return log_error_errno(r, "Failed to attach bus to event loop: %m"); - - machine = argc < 2 || isempty(argv[1]) ? ".host" : argv[1]; - - match = strjoina("type='signal'," - "sender='org.freedesktop.machine1'," - "path='/org/freedesktop/machine1',", - "interface='org.freedesktop.machine1.Manager'," - "member='MachineRemoved'," - "arg0='", machine, "'"); - - r = sd_bus_add_match_async(bus, &slot, match, on_machine_removed, NULL, NULL); - if (r < 0) - return log_error_errno(r, "Failed to request machine removal match: %m"); - - r = bus_call_method(bus, bus_machine_mgr, "OpenMachineLogin", &error, &reply, "s", machine); - if (r < 0) - return log_error_errno(r, "Failed to get login PTY: %s", bus_error_message(&error, r)); - - r = sd_bus_message_read(reply, "hs", &master, NULL); - if (r < 0) - return bus_log_parse_error(r); - - return process_forward(event, slot, master, PTY_FORWARD_IGNORE_VHANGUP, machine); -} - -static int verb_shell_machine(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL, *m = NULL; - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_slot_unrefp) sd_bus_slot *slot = NULL; - _cleanup_(sd_event_unrefp) sd_event *event = NULL; - int master = -1, r; - sd_bus *bus = ASSERT_PTR(userdata); - const char *match, *machine, *path; - _cleanup_free_ char *uid = NULL; - - if (!IN_SET(arg_transport, BUS_TRANSPORT_LOCAL, BUS_TRANSPORT_MACHINE)) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), - "Shell only supported on local machines."); - - if (terminal_is_dumb()) { - /* Set TERM=dumb if we are running on a dumb terminal or with a pipe. - * Otherwise, we will get unwanted OSC sequences. */ - if (!strv_find_prefix(arg_setenv, "TERM=")) - if (strv_extend(&arg_setenv, "TERM=dumb") < 0) - return log_oom(); - } else - /* Pass $TERM & Co. to shell session, if not explicitly specified. */ - FOREACH_STRING(v, "TERM=", "COLORTERM=", "NO_COLOR=") { - if (strv_find_prefix(arg_setenv, v)) - continue; - - const char *t = strv_find_prefix(environ, v); - if (!t) - continue; - - if (strv_extend(&arg_setenv, t) < 0) - return log_oom(); - } - - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); - - r = sd_event_default(&event); - if (r < 0) - return log_error_errno(r, "Failed to get event loop: %m"); - - r = sd_bus_attach_event(bus, event, 0); - if (r < 0) - return log_error_errno(r, "Failed to attach bus to event loop: %m"); - - r = parse_machine_uid(argc >= 2 ? argv[1] : NULL, &machine, &uid); - if (r < 0) - return log_error_errno(r, "Failed to parse machine specification: %m"); - - match = strjoina("type='signal'," - "sender='org.freedesktop.machine1'," - "path='/org/freedesktop/machine1',", - "interface='org.freedesktop.machine1.Manager'," - "member='MachineRemoved'," - "arg0='", machine, "'"); - - r = sd_bus_add_match_async(bus, &slot, match, on_machine_removed, NULL, NULL); - if (r < 0) - return log_error_errno(r, "Failed to request machine removal match: %m"); - - r = bus_message_new_method_call(bus, &m, bus_machine_mgr, "OpenMachineShell"); - if (r < 0) - return bus_log_create_error(r); - - path = argc < 3 || isempty(argv[2]) ? NULL : argv[2]; - - r = sd_bus_message_append(m, "sss", machine, uid, path); - if (r < 0) - return bus_log_create_error(r); - - r = sd_bus_message_append_strv(m, strv_length(argv) <= 3 ? NULL : argv + 2); - if (r < 0) - return bus_log_create_error(r); - - r = sd_bus_message_append_strv(m, arg_setenv); - if (r < 0) - return bus_log_create_error(r); - - r = sd_bus_call(bus, m, 0, &error, &reply); - if (r < 0) - return log_error_errno(r, "Failed to get shell PTY: %s", bus_error_message(&error, r)); - - r = sd_bus_message_read(reply, "hs", &master, NULL); - if (r < 0) - return bus_log_parse_error(r); - - return process_forward(event, slot, master, /* flags= */ 0, machine); -} - static int normalize_nspawn_filename(const char *name, char **ret_file) { _cleanup_free_ char *file = NULL; @@ -1771,6 +1889,8 @@ static int get_settings_path(const char *name, char **ret_path) { return -ENOENT; } +VERB(verb_edit_settings, "edit", "NAME|FILE…", 2, VERB_ANY, 0, + "Edit settings of one or more VMs/containers"); static int verb_edit_settings(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(edit_file_context_done) EditFileContext context = {}; int r; @@ -1841,6 +1961,8 @@ static int verb_edit_settings(int argc, char *argv[], uintptr_t _data, void *use return do_edit_files_and_install(&context); } +VERB(verb_cat_settings, "cat", "NAME|FILE…", 2, VERB_ANY, 0, + "Show settings of one or more VMs/containers"); static int verb_cat_settings(int argc, char *argv[], uintptr_t _data, void *userdata) { int r = 0; @@ -1892,6 +2014,80 @@ static int verb_cat_settings(int argc, char *argv[], uintptr_t _data, void *user return r; } +VERB(verb_clone_image, "clone", "NAME NAME", 3, 3, 0, + "Clone an image"); +static int verb_clone_image(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + sd_bus *bus = ASSERT_PTR(userdata); + int r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + r = bus_message_new_method_call(bus, &m, bus_machine_mgr, "CloneImage"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "ssb", argv[1], argv[2], arg_read_only); + if (r < 0) + return bus_log_create_error(r); + + /* This is a slow operation, hence turn off any method call timeouts */ + r = sd_bus_call(bus, m, USEC_INFINITY, &error, NULL); + if (r < 0) + return log_error_errno(r, "Could not clone image: %s", bus_error_message(&error, r)); + + return 0; +} + +VERB(verb_rename_image, "rename", "NAME NAME", 3, 3, 0, + "Rename an image"); +static int verb_rename_image(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus *bus = ASSERT_PTR(userdata); + int r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + r = bus_call_method( + bus, + bus_machine_mgr, + "RenameImage", + &error, + NULL, + "ss", argv[1], argv[2]); + if (r < 0) + return log_error_errno(r, "Could not rename image: %s", bus_error_message(&error, r)); + + return 0; +} + +VERB(verb_read_only_image, "read-only", "NAME [BOOL]", 2, 3, 0, + "Mark or unmark image read-only"); +static int verb_read_only_image(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus *bus = ASSERT_PTR(userdata); + int b = true, r; + + if (argc > 2) { + b = parse_boolean(argv[2]); + if (b < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Failed to parse boolean argument: %s", + argv[2]); + } + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + r = bus_call_method(bus, bus_machine_mgr, "MarkImageReadOnly", &error, NULL, "sb", argv[1], b); + if (r < 0) + return log_error_errno(r, "Could not mark image read-only: %s", bus_error_message(&error, r)); + + return 0; +} + +VERB(verb_remove_image, "remove", "NAME…", 2, VERB_ANY, 0, + "Remove an image"); static int verb_remove_image(int argc, char *argv[], uintptr_t _data, void *userdata) { sd_bus *bus = ASSERT_PTR(userdata); int r; @@ -1919,260 +2115,8 @@ static int verb_remove_image(int argc, char *argv[], uintptr_t _data, void *user return 0; } -static int verb_rename_image(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - sd_bus *bus = ASSERT_PTR(userdata); - int r; - - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); - - r = bus_call_method( - bus, - bus_machine_mgr, - "RenameImage", - &error, - NULL, - "ss", argv[1], argv[2]); - if (r < 0) - return log_error_errno(r, "Could not rename image: %s", bus_error_message(&error, r)); - - return 0; -} - -static int verb_clone_image(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; - sd_bus *bus = ASSERT_PTR(userdata); - int r; - - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); - - r = bus_message_new_method_call(bus, &m, bus_machine_mgr, "CloneImage"); - if (r < 0) - return bus_log_create_error(r); - - r = sd_bus_message_append(m, "ssb", argv[1], argv[2], arg_read_only); - if (r < 0) - return bus_log_create_error(r); - - /* This is a slow operation, hence turn off any method call timeouts */ - r = sd_bus_call(bus, m, USEC_INFINITY, &error, NULL); - if (r < 0) - return log_error_errno(r, "Could not clone image: %s", bus_error_message(&error, r)); - - return 0; -} - -static int verb_read_only_image(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - sd_bus *bus = ASSERT_PTR(userdata); - int b = true, r; - - if (argc > 2) { - b = parse_boolean(argv[2]); - if (b < 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Failed to parse boolean argument: %s", - argv[2]); - } - - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); - - r = bus_call_method(bus, bus_machine_mgr, "MarkImageReadOnly", &error, NULL, "sb", argv[1], b); - if (r < 0) - return log_error_errno(r, "Could not mark image read-only: %s", bus_error_message(&error, r)); - - return 0; -} - -static int image_exists(sd_bus *bus, const char *name) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - int r; - - assert(bus); - assert(name); - - r = bus_call_method(bus, bus_machine_mgr, "GetImage", &error, NULL, "s", name); - if (r < 0) { - if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_IMAGE)) - return 0; - - return log_error_errno(r, "Failed to check whether image %s exists: %s", name, bus_error_message(&error, r)); - } - - return 1; -} - -static int make_service_name(const char *name, char **ret) { - int r; - - assert(name); - assert(ret); - assert(arg_runner >= 0 && arg_runner < _RUNNER_MAX); - - if (!hostname_is_valid(name, 0)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Invalid machine name %s.", name); - - r = unit_name_build(machine_runner_unit_prefix_table[arg_runner], name, ".service", ret); - if (r < 0) - return log_error_errno(r, "Failed to build unit name: %m"); - - return 0; -} - -static int verb_start_machine(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(bus_wait_for_jobs_freep) BusWaitForJobs *w = NULL; - sd_bus *bus = ASSERT_PTR(userdata); - int r; - - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); - ask_password_agent_open_if_enabled(arg_transport, arg_ask_password); - - r = bus_wait_for_jobs_new(bus, &w); - if (r < 0) - return log_error_errno(r, "Could not watch jobs: %m"); - - for (int i = 1; i < argc; i++) { - _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; - _cleanup_free_ char *unit = NULL; - const char *object; - - r = make_service_name(argv[i], &unit); - if (r < 0) - return r; - - r = image_exists(bus, argv[i]); - if (r < 0) - return r; - if (r == 0) - return log_error_errno(SYNTHETIC_ERRNO(ENXIO), - "Machine image '%s' does not exist.", - argv[i]); - - r = bus_call_method( - bus, - bus_systemd_mgr, - "StartUnit", - &error, - &reply, - "ss", unit, "fail"); - if (r < 0) - return log_error_errno(r, "Failed to start unit: %s", bus_error_message(&error, r)); - - r = sd_bus_message_read(reply, "o", &object); - if (r < 0) - return bus_log_parse_error(r); - - r = bus_wait_for_jobs_add(w, object); - if (r < 0) - return log_oom(); - } - - r = bus_wait_for_jobs(w, arg_quiet, NULL); - if (r < 0) - return r; - - return 0; -} - -static int verb_enable_machine(int argc, char *argv[], uintptr_t data, void *userdata) { - _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL; - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - const char *method; - sd_bus *bus = ASSERT_PTR(userdata); - int r; - bool enable; - - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); - - enable = streq(argv[0], "enable"); - method = enable ? "EnableUnitFiles" : "DisableUnitFiles"; - - r = bus_message_new_method_call(bus, &m, bus_systemd_mgr, method); - if (r < 0) - return bus_log_create_error(r); - - r = sd_bus_message_open_container(m, 'a', "s"); - if (r < 0) - return bus_log_create_error(r); - - if (enable) { - r = sd_bus_message_append(m, "s", "machines.target"); - if (r < 0) - return bus_log_create_error(r); - } - - for (int i = 1; i < argc; i++) { - _cleanup_free_ char *unit = NULL; - - r = make_service_name(argv[i], &unit); - if (r < 0) - return r; - - r = image_exists(bus, argv[i]); - if (r < 0) - return r; - if (r == 0) - return log_error_errno(SYNTHETIC_ERRNO(ENXIO), - "Machine image '%s' does not exist.", - argv[i]); - - r = sd_bus_message_append(m, "s", unit); - if (r < 0) - return bus_log_create_error(r); - } - - r = sd_bus_message_close_container(m); - if (r < 0) - return bus_log_create_error(r); - - if (enable) - r = sd_bus_message_append(m, "bb", false, false); - else - r = sd_bus_message_append(m, "b", false); - if (r < 0) - return bus_log_create_error(r); - - r = sd_bus_call(bus, m, 0, &error, &reply); - if (r < 0) - return log_error_errno(r, "Failed to enable or disable unit: %s", bus_error_message(&error, r)); - - if (enable) { - r = sd_bus_message_read(reply, "b", NULL); - if (r < 0) - return bus_log_parse_error(r); - } - - r = bus_deserialize_and_dump_unit_file_changes(reply, arg_quiet); - if (r < 0) - return r; - - r = bus_service_manager_reload(bus); - if (r < 0) - return r; - - if (arg_now) { - _cleanup_strv_free_ char **new_args = NULL; - - new_args = strv_new(enable ? "start" : "poweroff"); - if (!new_args) - return log_oom(); - - r = strv_extend_strv(&new_args, argv + 1, /* filter_duplicates= */ false); - if (r < 0) - return log_oom(); - - if (enable) - return verb_start_machine(strv_length(new_args), new_args, data, userdata); - - return verb_poweroff_machine(strv_length(new_args), new_args, data, userdata); - } - - return 0; -} - +VERB(verb_set_limit, "set-limit", "[NAME] BYTES", 2, 3, 0, + "Set image or pool size limit (disk quota)"); static int verb_set_limit(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; sd_bus *bus = userdata; @@ -2203,6 +2147,8 @@ static int verb_set_limit(int argc, char *argv[], uintptr_t _data, void *userdat return 0; } +VERB(verb_clean_images, "clean", /* argspec= */ NULL, VERB_ANY, 1, 0, + "Remove hidden (or all) images"); static int verb_clean_images(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL; _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; @@ -2257,11 +2203,130 @@ static int verb_clean_images(int argc, char *argv[], uintptr_t _data, void *user return 0; } -static int chainload_importctl(int argc, char *argv[]) { +VERB(verb_bind_volume, "bind-volume", "MACHINE PROVIDER:VOLUME[:…]", 3, 3, 0, + "Attach a volume to a running machine"); +static int verb_bind_volume(int argc, char *argv[], uintptr_t _data, void *userdata) { + int r; + + if (arg_transport != BUS_TRANSPORT_LOCAL) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "bind-volume is only supported on the local transport."); + + _cleanup_(bind_volume_freep) BindVolume *bv = NULL; + r = bind_volume_parse(argv[2], &bv); + if (r < 0) + return log_error_errno(r, "Failed to parse bind-volume argument '%s': %m", argv[2]); + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + /* Locate and connect to the target machine before acquiring storage, so a missing + * machine doesn't trigger 'create=new' side effects on the StorageProvider. */ + _cleanup_free_ char *address = NULL; + r = machine_get_control_address(argv[1], &address); + if (r == -EOPNOTSUPP) + return log_error_errno(r, "Machine '%s' does not expose a varlink control socket.", argv[1]); + if (r < 0) + return r; + + _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; + r = sd_varlink_connect_address(&vl, address); + if (r < 0) + return log_error_errno(r, "Failed to connect to machine control socket %s: %m", address); + + r = sd_varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return log_error_errno(r, "Failed to enable fd passing on varlink connection: %m"); + + _cleanup_(storage_acquire_reply_done) StorageAcquireReply reply = STORAGE_ACQUIRE_REPLY_INIT; + _cleanup_free_ char *acquire_error_id = NULL; + r = storage_acquire_volume(arg_runtime_scope, bv, arg_ask_password, &acquire_error_id, &reply); + if (r < 0) { + if (acquire_error_id) + return log_error_errno(r, "Failed to acquire storage volume '%s:%s' from provider: %s", + bv->provider, bv->volume, acquire_error_id); + return log_error_errno(r, "Failed to acquire storage volume '%s:%s' from provider: %m", + bv->provider, bv->volume); + } + + int fd_index = sd_varlink_push_fd(vl, reply.fd); + if (fd_index < 0) + return log_error_errno(fd_index, "Failed to push storage fd onto varlink connection: %m"); + TAKE_FD(reply.fd); + + _cleanup_free_ char *name = strjoin(bv->provider, ":", bv->volume); + if (!name) + return log_oom(); + + sd_json_variant *vl_reply = NULL; + const char *error_id = NULL; + r = sd_varlink_callbo( + vl, + "io.systemd.MachineInstance.AddStorage", + &vl_reply, &error_id, + SD_JSON_BUILD_PAIR_INTEGER("fileDescriptorIndex", fd_index), + SD_JSON_BUILD_PAIR_STRING("name", name), + JSON_BUILD_PAIR_STRING_NON_EMPTY("config", bv->config)); + if (r < 0) + return log_error_errno(r, "Failed to call io.systemd.MachineInstance.AddStorage: %m"); + if (error_id) + return log_error_errno(sd_varlink_error_to_errno(error_id, vl_reply), + "AddStorage failed for '%s': %s", name, error_id); + + return 0; +} + +VERB(verb_unbind_volume, "unbind-volume", "MACHINE PROVIDER:VOLUME", 3, 3, 0, + "Detach a volume from a running machine"); +static int verb_unbind_volume(int argc, char *argv[], uintptr_t _data, void *userdata) { + int r; + + if (arg_transport != BUS_TRANSPORT_LOCAL) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "unbind-volume is only supported on the local transport."); + + r = machine_storage_name_split(argv[2], /* ret_provider= */ NULL, /* ret_volume= */ NULL); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid unbind-volume name '%s', expected ':'.", argv[2]); + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + _cleanup_free_ char *address = NULL; + r = machine_get_control_address(argv[1], &address); + if (r == -EOPNOTSUPP) + return log_error_errno(r, "Machine '%s' does not expose a varlink control socket.", argv[1]); + if (r < 0) + return r; + + _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; + r = sd_varlink_connect_address(&vl, address); + if (r < 0) + return log_error_errno(r, "Failed to connect to machine control socket %s: %m", address); + + sd_json_variant *reply = NULL; + const char *error_id = NULL; + r = sd_varlink_callbo( + vl, + "io.systemd.MachineInstance.RemoveStorage", + &reply, &error_id, + SD_JSON_BUILD_PAIR_STRING("name", argv[2])); + if (r < 0) + return log_error_errno(r, "Failed to call io.systemd.MachineInstance.RemoveStorage: %m"); + if (error_id) + return log_error_errno(sd_varlink_error_to_errno(error_id, reply), + "RemoveStorage failed for '%s': %s", argv[2], error_id); + + return 0; +} + +static int chainload_importctl(char **args) { int r; if (!arg_quiet) - log_notice("The 'machinectl %1$s' command has been replaced by 'importctl -m %1$s'. Redirecting invocation.", argv[optind]); + log_notice("The 'machinectl %1$s' command has been replaced by 'importctl -m %1$s'. " + "Redirecting invocation.", args[0]); _cleanup_strv_free_ char **c = strv_new("importctl", "--class=machine"); @@ -2292,7 +2357,7 @@ static int chainload_importctl(int argc, char *argv[]) { if (strv_extend_many(&c, "--format", arg_format) < 0) return log_oom(); - if (strv_extend_strv(&c, argv + optind, /* filter_duplicates= */ false) < 0) + if (strv_extend_strv(&c, args, /* filter_duplicates= */ false) < 0) return log_oom(); if (DEBUG_LOGGING) { @@ -2305,465 +2370,285 @@ static int chainload_importctl(int argc, char *argv[]) { } static int help(void) { - _cleanup_free_ char *link = NULL; + static const char* const vgroups[] = { + "Machine Commands", + "Image Commands", + }; + + Table *vtables[ELEMENTSOF(vgroups)] = {}; + CLEANUP_ELEMENTS(vtables, table_unref_array_clear); + _cleanup_(table_unrefp) Table *options = NULL; int r; + for (size_t i = 0; i < ELEMENTSOF(vgroups); i++) { + r = verbs_get_help_table_group(vgroups[i], &vtables[i]); + if (r < 0) + return r; + } + + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + + assert_cc(ELEMENTSOF(vtables) == 2); + (void) table_sync_column_widths(0, options, vtables[0], vtables[1]); + pager_open(arg_pager_flags); - r = terminal_urlify_man("machinectl", "1", &link); - if (r < 0) - return log_oom(); + help_cmdline("[OPTIONS…] COMMAND …"); - printf("%1$s [OPTIONS...] COMMAND ...\n\n" - "%5$sSend control commands to or query the virtual machine and container%6$s\n" - "%5$sregistration manager.%6$s\n" - "\n%3$sMachine Commands:%4$s\n" - " list List running VMs and containers\n" - " status NAME... Show VM/container details\n" - " show [NAME...] Show properties of one or more VMs/containers\n" - " start NAME... Start container as a service\n" - " login [NAME] Get a login prompt in a container or on the\n" - " local host\n" - " shell [[USER@]NAME [COMMAND...]]\n" - " Invoke a shell (or other command) in a container\n" - " or on the local host\n" - " enable NAME... Enable automatic container start at boot\n" - " disable NAME... Disable automatic container start at boot\n" - " poweroff NAME... Power off one or more machines\n" - " reboot NAME... Reboot one or more machines\n" - " pause NAME... Pause one or more machines\n" - " resume NAME... Resume one or more paused machines\n" - " terminate NAME... Terminate one or more machines\n" - " kill NAME... Send signal to processes of a machine\n" - " copy-to NAME PATH [PATH] Copy files from the host to a container\n" - " copy-from NAME PATH [PATH] Copy files from a container to the host\n" - " bind NAME PATH [PATH] Bind mount a path from the host into a container\n" - "\n%3$sImage Commands:%4$s\n" - " list-images Show available container and VM images\n" - " image-status [NAME...] Show image details\n" - " show-image [NAME...] Show properties of image\n" - " edit NAME|FILE... Edit settings of one or more VMs/containers\n" - " cat NAME|FILE... Show settings of one or more VMs/containers\n" - " clone NAME NAME Clone an image\n" - " rename NAME NAME Rename an image\n" - " read-only NAME [BOOL] Mark or unmark image read-only\n" - " remove NAME... Remove an image\n" - " set-limit [NAME] BYTES Set image or pool size limit (disk quota)\n" - " clean Remove hidden (or all) images\n" - "\n%3$sOptions:%4$s\n" - " -h --help Show this help\n" - " --version Show package version\n" - " --no-pager Do not pipe output into a pager\n" - " --no-legend Do not show the headers and footers\n" - " --no-ask-password Do not ask for system passwords\n" - " -H --host=[USER@]HOST Operate on remote host\n" - " -M --machine=CONTAINER Operate on local container\n" - " --system Connect to system machine manager\n" - " --user Connect to user machine manager\n" - " -p --property=NAME Show only properties by this name\n" - " --value When showing properties, only print the value\n" - " -P NAME Equivalent to --value --property=NAME\n" - " -q --quiet Suppress output\n" - " -a --all Show all properties, including empty ones\n" - " -l --full Do not ellipsize output\n" - " --kill-whom=WHOM Whom to send signal to\n" - " -s --signal=SIGNAL Which signal to send\n" - " --uid=USER Specify user ID to invoke shell as\n" - " -E --setenv=VAR[=VALUE] Add an environment variable for shell\n" - " --read-only Create read-only bind mount or clone\n" - " --mkdir Create directory before bind mounting, if missing\n" - " -n --lines=INTEGER Number of journal entries to show\n" - " --max-addresses=INTEGER Number of internet addresses to show at most\n" - " -o --output=STRING Change journal output mode (short, short-precise,\n" - " short-iso, short-iso-precise, short-full,\n" - " short-monotonic, short-unix, short-delta,\n" - " json, json-pretty, json-sse, json-seq, cat,\n" - " verbose, export, with-unit)\n" - " --force Replace target file when copying, if necessary\n" - " --now Start or power off container after enabling or\n" - " disabling it\n" - " --runner=RUNNER Select between nspawn and vmspawn as the runner\n" - " -V Short for --runner=vmspawn\n" - "\nSee the %2$s for details.\n", - program_invocation_short_name, - link, - ansi_underline(), - ansi_normal(), + printf("\n%1$s%2$sSend control commands to or query the virtual machine and container%3$s\n" + "%1$s%2$sregistration manager.%3$s\n", ansi_highlight(), + ansi_add_italics(), ansi_normal()); + for (size_t i = 0; i < ELEMENTSOF(vgroups); i++) { + help_section(vgroups[i]); + r = table_print_or_warn(vtables[i]); + if (r < 0) + return r; + } + + help_section("Options"); + r = table_print_or_warn(options); + if (r < 0) + return r; + + help_man_page_reference("machinectl", "1"); + return 0; } -static int verb_help(int argc, char *argv[], uintptr_t _data, void *userdata) { - return help(); -} - -static int parse_argv(int argc, char *argv[]) { - - enum { - ARG_VERSION = 0x100, - ARG_NO_PAGER, - ARG_NO_LEGEND, - ARG_VALUE, - ARG_KILL_WHOM, - ARG_READ_ONLY, - ARG_MKDIR, - ARG_NO_ASK_PASSWORD, - ARG_VERIFY, - ARG_RUNNER, - ARG_NOW, - ARG_FORCE, - ARG_FORMAT, - ARG_UID, - ARG_MAX_ADDRESSES, - ARG_SYSTEM, - ARG_USER, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "property", required_argument, NULL, 'p' }, - { "value", no_argument, NULL, ARG_VALUE }, - { "all", no_argument, NULL, 'a' }, - { "full", no_argument, NULL, 'l' }, - { "no-pager", no_argument, NULL, ARG_NO_PAGER }, - { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, - { "kill-whom", required_argument, NULL, ARG_KILL_WHOM }, - { "signal", required_argument, NULL, 's' }, - { "host", required_argument, NULL, 'H' }, - { "machine", required_argument, NULL, 'M' }, - { "read-only", no_argument, NULL, ARG_READ_ONLY }, - { "mkdir", no_argument, NULL, ARG_MKDIR }, - { "quiet", no_argument, NULL, 'q' }, - { "lines", required_argument, NULL, 'n' }, - { "output", required_argument, NULL, 'o' }, - { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD }, - { "verify", required_argument, NULL, ARG_VERIFY }, - { "runner", required_argument, NULL, ARG_RUNNER }, - { "now", no_argument, NULL, ARG_NOW }, - { "force", no_argument, NULL, ARG_FORCE }, - { "format", required_argument, NULL, ARG_FORMAT }, - { "uid", required_argument, NULL, ARG_UID }, - { "setenv", required_argument, NULL, 'E' }, - { "max-addresses", required_argument, NULL, ARG_MAX_ADDRESSES }, - { "user", no_argument, NULL, ARG_USER }, - { "system", no_argument, NULL, ARG_SYSTEM }, - {} - }; - - bool reorder = false; - int c, r, shell = -1; +VERB_COMMON_HELP_HIDDEN(help); +static int parse_argv(int argc, char *argv[], char ***ret_args) { assert(argc >= 0); assert(argv); + assert(ret_args); - /* 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; - - for (;;) { - static const char option_string[] = "-hp:P:als:H:M:qn:o:E:V"; - - c = getopt_long(argc, argv, option_string + reorder, options, NULL); - if (c < 0) - break; + /* For "shell" we don't want option reordering; options specified after the command should be passed + * to the program to execute, and not processed by us. For other verbs, we consume all options as + * usual. To make this work, start with OPTION_PARSER_RETURN_POSITIONAL_ARGS and switch to either + * OPTION_PARSER_STOP_AT_FIRST_NONOPTION or OPTION_PARSER_NORMAL after we've seen the verb and one + * more argument after that. */ + OptionParser opts = { argc, argv, OPTION_PARSER_RETURN_POSITIONAL_ARGS }; + _cleanup_strv_free_ char **args = NULL; + int r; + FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { - case 1: /* getopt_long() returns 1 if "-" was the first character of the option string, and a - * non-option argument was discovered. */ + OPTION_POSITIONAL: + assert(opts.mode == OPTION_PARSER_RETURN_POSITIONAL_ARGS); - assert(!reorder); - - /* We generally are fine with the fact that getopt_long() reorders the command line, and looks - * for switches after the main verb. However, for "shell" we really don't want that, since we - * want that switches specified after the machine name are passed to the program to execute, - * and not processed by us. To make this possible, we'll first invoke getopt_long() with - * reordering disabled (i.e. with the "-" prefix in the option string), looking for the first - * non-option parameter. If it's the verb "shell" we remember its position and continue - * processing options. In this case, as soon as we hit the next non-option argument we found - * the machine name, and stop further processing. If the first non-option argument is any other - * verb than "shell" we switch to normal reordering mode and continue processing arguments - * normally. */ - - if (shell >= 0) { - /* If we already found the "shell" verb on the command line, and now found the next - * non-option argument, then this is the machine name and we should stop processing - * further arguments. */ - optind--; /* don't process this argument, go one step back */ - goto done; - } - if (streq(optarg, "shell")) - /* Remember the position of the "shell" verb, and continue processing normally. */ - shell = optind - 1; - else { - int saved_optind; - - /* OK, this is some other verb. In this case, turn on reordering again, and continue - * processing normally. */ - reorder = true; - - /* We changed the option string. getopt_long() only looks at it again if we invoke it - * at least once with a reset option index. Hence, let's reset the option index here, - * then invoke getopt_long() again (ignoring what it has to say, after all we most - * likely already processed it), and the bump the option index so that we read the - * intended argument again. */ - saved_optind = optind; - optind = 0; - (void) getopt_long(argc, argv, option_string + reorder, options, NULL); - optind = saved_optind - 1; /* go one step back, process this argument again */ - } + if (!args && !streq(opts.arg, "shell")) + opts.mode = OPTION_PARSER_NORMAL; + else if (args) + opts.mode = OPTION_PARSER_STOP_AT_FIRST_NONOPTION; + r = strv_extend(&args, opts.arg); + if (r < 0) + return log_oom(); break; - case 'h': + OPTION_COMMON_HELP: return help(); - case ARG_VERSION: + OPTION_COMMON_VERSION: return version(); - case 'p': - case 'P': - r = strv_extend(&arg_property, optarg); + OPTION_COMMON_HOST: + arg_transport = BUS_TRANSPORT_REMOTE; + arg_host = opts.arg; + break; + + OPTION_COMMON_MACHINE: + arg_transport = BUS_TRANSPORT_MACHINE; + arg_host = opts.arg; + break; + + OPTION_LONG("system", NULL, "Connect to system machine manager"): + arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; + break; + + OPTION_LONG("user", NULL, "Connect to user machine manager"): + arg_runtime_scope = RUNTIME_SCOPE_USER; + break; + + OPTION_LONG("value", NULL, "When showing properties, only print the value"): + SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_ONLY_VALUE, true); + break; + + OPTION('p', "property", "NAME", "Show only properties by this name"): {} + OPTION_SHORT('P', "NAME", "Equivalent to --value --property=NAME"): + r = strv_extend(&arg_property, opts.arg); if (r < 0) return log_oom(); /* If the user asked for a particular property, show it to them, even if empty. */ SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_SHOW_EMPTY, true); - if (c == 'p') - break; - _fallthrough_; + if (opts.opt->short_code == 'P') + SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_ONLY_VALUE, true); - case ARG_VALUE: - SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_ONLY_VALUE, true); break; - case 'a': + OPTION('a', "all", NULL, "Show all properties, including empty ones"): SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_SHOW_EMPTY, true); arg_all = true; break; - case 'l': + OPTION('l', "full", NULL, "Do not ellipsize output"): arg_full = true; break; - case 'n': - if (safe_atou(optarg, &arg_lines) < 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Failed to parse lines '%s'", optarg); + OPTION_LONG("kill-whom", "WHOM", "Whom to send signal to"): + arg_kill_whom = opts.arg; break; - case 'o': - if (streq(optarg, "help")) + OPTION('s', "signal", "SIGNAL", "Which signal to send"): + r = parse_signal_argument(opts.arg, &arg_signal); + if (r <= 0) + return r; + break; + + OPTION_LONG("uid", "USER", "Specify user ID to invoke shell as"): + arg_uid = opts.arg; + break; + + OPTION('E', "setenv", "VAR[=VALUE]", "Add an environment variable for shell"): + r = strv_env_replace_strdup_passthrough(&arg_setenv, opts.arg); + if (r < 0) + return log_error_errno(r, "Cannot assign environment variable %s: %m", opts.arg); + break; + + OPTION_LONG("read-only", NULL, "Create read-only bind mount or clone"): + arg_read_only = true; + break; + + OPTION_LONG("mkdir", NULL, "Create directory before bind mounting, if missing"): + arg_mkdir = true; + break; + + OPTION('n', "lines", "INTEGER", "Number of journal entries to show"): + if (safe_atou(opts.arg, &arg_lines) < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Failed to parse lines '%s'", opts.arg); + break; + + OPTION_LONG("max-addresses", "INTEGER", + "Number of internet addresses to show at most"): + if (streq(opts.arg, "all")) + arg_max_addresses = UINT_MAX; + else if (safe_atou(opts.arg, &arg_max_addresses) < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid number of addresses: %s", opts.arg); + break; + + OPTION('o', "output", "STRING", + "Change journal output mode (short, short-precise, short-iso, " + "short-iso-precise, short-full, short-monotonic, short-unix, short-delta, " + "json, json-pretty, json-sse, json-seq, cat, verbose, export, with-unit)"): + if (streq(opts.arg, "help")) return DUMP_STRING_TABLE(output_mode, OutputMode, _OUTPUT_MODE_MAX); - r = output_mode_from_string(optarg); + r = output_mode_from_string(opts.arg); if (r < 0) - return log_error_errno(r, "Unknown output '%s'.", optarg); + return log_error_errno(r, "Unknown output '%s'.", opts.arg); arg_output = r; if (OUTPUT_MODE_IS_JSON(arg_output)) arg_legend = false; break; - case ARG_NO_PAGER: - arg_pager_flags |= PAGER_DISABLE; + OPTION_LONG("force", NULL, "Replace target file when copying, if necessary"): + arg_force = true; break; - case ARG_NO_LEGEND: - arg_legend = false; + OPTION_LONG("now", NULL, + "Start or power off container after enabling or disabling it"): + arg_now = true; break; - case ARG_KILL_WHOM: - arg_kill_whom = optarg; - break; - - case 's': - r = parse_signal_argument(optarg, &arg_signal); - if (r <= 0) - return r; - break; - - case ARG_NO_ASK_PASSWORD: - arg_ask_password = false; - break; - - case 'H': - arg_transport = BUS_TRANSPORT_REMOTE; - arg_host = optarg; - break; - - case 'M': - arg_transport = BUS_TRANSPORT_MACHINE; - arg_host = optarg; - break; - - case ARG_READ_ONLY: - arg_read_only = true; - break; - - case ARG_MKDIR: - arg_mkdir = true; - break; - - case 'q': - arg_quiet = true; - break; - - case ARG_VERIFY: - if (streq(optarg, "help")) - return DUMP_STRING_TABLE(import_verify, ImportVerify, _IMPORT_VERIFY_MAX); - - r = import_verify_from_string(optarg); + OPTION_LONG("runner", "RUNNER", + "Select between nspawn and vmspawn as the runner"): + r = machine_runner_from_string(opts.arg); if (r < 0) - return log_error_errno(r, "Failed to parse --verify= setting: %s", optarg); - arg_verify = r; - break; - - case 'V': - arg_runner = RUNNER_VMSPAWN; - break; - - case ARG_RUNNER: - r = machine_runner_from_string(optarg); - if (r < 0) - return log_error_errno(r, "Failed to parse --runner= setting: %s", optarg); + return log_error_errno(r, "Failed to parse --runner= setting: %s", opts.arg); arg_runner = r; break; - case ARG_NOW: - arg_now = true; + OPTION_SHORT('V', NULL, "Short for --runner=vmspawn"): + arg_runner = RUNNER_VMSPAWN; break; - case ARG_FORCE: - arg_force = true; - break; + /* Hidden options below */ - case ARG_FORMAT: - if (!STR_IN_SET(optarg, "uncompressed", "xz", "gzip", "bzip2", "zstd")) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Unknown format: %s", optarg); + OPTION_LONG("verify", "MODE", /* help= */ NULL): + if (streq(opts.arg, "help")) + return DUMP_STRING_TABLE(import_verify, ImportVerify, _IMPORT_VERIFY_MAX); - arg_format = optarg; - break; - - case ARG_UID: - arg_uid = optarg; - break; - - case 'E': - r = strv_env_replace_strdup_passthrough(&arg_setenv, optarg); + r = import_verify_from_string(opts.arg); if (r < 0) - return log_error_errno(r, "Cannot assign environment variable %s: %m", optarg); + return log_error_errno(r, "Failed to parse --verify= setting: %s", opts.arg); + arg_verify = r; break; - case ARG_MAX_ADDRESSES: - if (streq(optarg, "all")) - arg_max_addresses = UINT_MAX; - else if (safe_atou(optarg, &arg_max_addresses) < 0) + OPTION_LONG("format", "FORMAT", /* help= */ NULL): + if (!STR_IN_SET(opts.arg, "uncompressed", "xz", "gzip", "bzip2", "zstd")) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Invalid number of addresses: %s", optarg); + "Unknown format: %s", opts.arg); + + arg_format = opts.arg; break; - case ARG_USER: - arg_runtime_scope = RUNTIME_SCOPE_USER; + OPTION_COMMON_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; break; - case ARG_SYSTEM: - arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; + OPTION_COMMON_NO_LEGEND: + arg_legend = false; break; - case '?': - return -EINVAL; + OPTION_COMMON_NO_ASK_PASSWORD: + arg_ask_password = false; + break; - default: - assert_not_reached(); + OPTION('q', "quiet", NULL, "Suppress output"): + arg_quiet = true; + break; } - } -done: - if (shell >= 0) { - char *t; - - /* We found the "shell" verb while processing the argument list. Since we turned off reordering of the - * argument list initially let's readjust it now, and move the "shell" verb to the back. */ - - optind -= 1; /* place the option index where the "shell" verb will be placed */ - - t = argv[shell]; - for (int i = shell; i < optind; i++) - argv[i] = argv[i+1]; - argv[optind] = t; - } + /* We gathered some positional args in 'args' ourselves. Append the remaining ones. */ + if (strv_extend_strv(&args, option_parser_get_args(&opts), /* filter_duplicates= */ false) < 0) + return log_oom(); + *ret_args = TAKE_PTR(args); return 1; } -static int machinectl_main(int argc, char *argv[], sd_bus *bus) { - - static const Verb verbs[] = { - { "help", VERB_ANY, VERB_ANY, 0, verb_help }, - { "list", VERB_ANY, 1, VERB_DEFAULT, verb_list_machines }, - { "list-images", VERB_ANY, 1, 0, verb_list_images }, - { "status", 2, VERB_ANY, 0, verb_show_machine }, - { "image-status", VERB_ANY, VERB_ANY, 0, verb_show_image }, - { "show", VERB_ANY, VERB_ANY, 0, verb_show_machine }, - { "show-image", VERB_ANY, VERB_ANY, 0, verb_show_image }, - { "terminate", 2, VERB_ANY, 0, verb_terminate_machine }, - { "reboot", 2, VERB_ANY, 0, verb_reboot_machine }, - { "restart", 2, VERB_ANY, 0, verb_reboot_machine }, /* Convenience alias */ - { "poweroff", 2, VERB_ANY, 0, verb_poweroff_machine }, - { "stop", 2, VERB_ANY, 0, verb_poweroff_machine }, /* Convenience alias */ - { "kill", 2, VERB_ANY, 0, verb_kill_machine }, - { "pause", 2, VERB_ANY, 0, verb_pause }, - { "resume", 2, VERB_ANY, 0, verb_resume }, - { "login", VERB_ANY, 2, 0, verb_login_machine }, - { "shell", VERB_ANY, VERB_ANY, 0, verb_shell_machine }, - { "bind", 3, 4, 0, verb_bind_mount }, - { "bind-volume", 3, 3, 0, verb_bind_volume }, - { "unbind-volume", 3, 3, 0, verb_unbind_volume }, - { "edit", 2, VERB_ANY, 0, verb_edit_settings }, - { "cat", 2, VERB_ANY, 0, verb_cat_settings }, - { "copy-to", 3, 4, 0, verb_copy_files }, - { "copy-from", 3, 4, 0, verb_copy_files }, - { "remove", 2, VERB_ANY, 0, verb_remove_image }, - { "rename", 3, 3, 0, verb_rename_image }, - { "clone", 3, 3, 0, verb_clone_image }, - { "read-only", 2, 3, 0, verb_read_only_image }, - { "start", 2, VERB_ANY, 0, verb_start_machine }, - { "enable", 2, VERB_ANY, 0, verb_enable_machine }, - { "disable", 2, VERB_ANY, 0, verb_enable_machine }, - { "set-limit", 2, 3, 0, verb_set_limit }, - { "clean", VERB_ANY, 1, 0, verb_clean_images }, - {} - }; - - return dispatch_verb(argc, argv, verbs, bus); -} - static int run(int argc, char *argv[]) { _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_strv_free_ char **args = NULL; int r; setlocale(LC_ALL, ""); log_setup(); - r = parse_argv(argc, argv); + r = parse_argv(argc, argv, &args); if (r <= 0) return r; journal_browse_prepare(); - if (STRPTR_IN_SET(argv[optind], - "import-tar", "import-raw", "import-fs", - "export-tar", "export-raw", - "pull-tar", "pull-raw", - "list-transfers", "cancel-transfer")) - return chainload_importctl(argc, argv); + if (args && STR_IN_SET(args[0], + "import-tar", "import-raw", "import-fs", + "export-tar", "export-raw", + "pull-tar", "pull-raw", + "list-transfers", "cancel-transfer")) + return chainload_importctl(args); r = bus_connect_transport(arg_transport, arg_host, arg_runtime_scope, &bus); if (r < 0) @@ -2771,7 +2656,7 @@ static int run(int argc, char *argv[]) { (void) sd_bus_set_allow_interactive_authorization(bus, arg_ask_password); - return machinectl_main(argc, argv, bus); + return dispatch_verb_with_args(args, bus); } DEFINE_MAIN_FUNCTION(run); diff --git a/src/measure/measure-tool.c b/src/measure/measure-tool.c index d5bb5bea23..b8acd6d4b0 100644 --- a/src/measure/measure-tool.c +++ b/src/measure/measure-tool.c @@ -416,8 +416,7 @@ static int validate_stub(void) { return 0; } -VERB(verb_status, "status", NULL, VERB_ANY, 1, VERB_DEFAULT, - "Show current PCR values"); +VERB_DEFAULT_NOARG(verb_status, "status", "Show current PCR values"); static int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; int r; diff --git a/src/oom/oomctl.c b/src/oom/oomctl.c index 82ffe0e837..f6d451612b 100644 --- a/src/oom/oomctl.c +++ b/src/oom/oomctl.c @@ -65,8 +65,7 @@ static int help(void) { VERB_COMMON_HELP_HIDDEN(help); -VERB(verb_dump_state, "dump", NULL, VERB_ANY, 1, VERB_DEFAULT, - "Output the current state of systemd-oomd"); +VERB_DEFAULT_NOARG(verb_dump_state, "dump", "Output the current state of systemd-oomd"); static int verb_dump_state(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c index 4ebe3f995b..8315384b4d 100644 --- a/src/pcrlock/pcrlock.c +++ b/src/pcrlock/pcrlock.c @@ -2500,7 +2500,7 @@ static int event_log_load_and_process(EventLog **ret) { return 0; } -VERB(verb_show_log, "log", NULL, VERB_ANY, 1, VERB_DEFAULT, +VERB_DEFAULT_NOARG(verb_show_log, "log", "Show measurement log"); static int verb_show_log(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *log_table = NULL, *pcr_table = NULL; diff --git a/src/portable/portablectl.c b/src/portable/portablectl.c index c70d506346..763acfc258 100644 --- a/src/portable/portablectl.c +++ b/src/portable/portablectl.c @@ -253,7 +253,7 @@ static int maybe_reload(sd_bus **bus) { return bus_service_manager_reload(*bus); } -VERB(verb_list_images, "list", NULL, VERB_ANY, 1, VERB_DEFAULT, +VERB_DEFAULT_NOARG(verb_list_images, "list", "List available portable service images (default)"); static int verb_list_images(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; diff --git a/src/shared/verbs.h b/src/shared/verbs.h index 4fb0486f75..e071cc5d89 100644 --- a/src/shared/verbs.h +++ b/src/shared/verbs.h @@ -60,6 +60,8 @@ typedef struct { VERB_SCOPE(scope, d, v, /* a= */ NULL, /* amin= */ VERB_ANY, /* amax= */ 1, /* f= */ 0, h) #define VERB_NOARG(d, v, h) \ VERB_SCOPE_NOARG(static, d, v, h) +#define VERB_DEFAULT_NOARG(d, v, h) \ + VERB_SCOPE(static, d, v, /* a= */ NULL, /* amin= */ VERB_ANY, /* amax= */ 1, /* f= */ VERB_DEFAULT, h) /* Magic entry in the table (which will not be returned) that designates the start of the group . * The macro works as a separator between groups and must be between other VERB* stanzas. */ diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c index 40d39b7654..ff00196cc6 100644 --- a/src/sysext/sysext.c +++ b/src/sysext/sysext.c @@ -2305,7 +2305,7 @@ static int merge(ImageClass image_class, return 1; } -VERB(verb_status, "status", NULL, VERB_ANY, 1, VERB_DEFAULT, "Show current merge status (default)"); +VERB_DEFAULT_NOARG(verb_status, "status", "Show current merge status (default)"); static int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(table_unrefp) Table *t = NULL; int r, ret = 0; diff --git a/src/timedate/timedatectl.c b/src/timedate/timedatectl.c index c35b090035..7607b1ac4b 100644 --- a/src/timedate/timedatectl.c +++ b/src/timedate/timedatectl.c @@ -180,7 +180,7 @@ static int print_status_info(const StatusInfo *i) { return 0; } -VERB(verb_status, "status", NULL, VERB_ANY, 1, VERB_DEFAULT, "Show current time settings"); +VERB_DEFAULT_NOARG(verb_status, "status", "Show current time settings"); static int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) { StatusInfo info = {}; static const struct bus_properties_map map[] = { diff --git a/test/units/TEST-13-NSPAWN.machined.sh b/test/units/TEST-13-NSPAWN.machined.sh index de51daa24c..6b07137ef8 100755 --- a/test/units/TEST-13-NSPAWN.machined.sh +++ b/test/units/TEST-13-NSPAWN.machined.sh @@ -120,6 +120,7 @@ machinectl disable long-running long-running long-running container1 [[ "$(TERM=dumb machinectl shell testuser@ /usr/bin/bash -c 'echo -ne $FOO')" == "" ]] [[ "$(TERM=dumb machinectl shell --setenv=FOO=bar testuser@ /usr/bin/bash -c 'echo -ne $FOO')" == "bar" ]] +[[ "$(TERM=dumb machinectl shell testuser@ --setenv=FOO=bar /usr/bin/bash -c 'echo -ne $FOO')" == "bar" ]] [[ "$(machinectl show --property=State --value long-running)" == "running" ]] # Equivalent to machinectl kill --signal=SIGRTMIN+4 --kill-whom=leader