From 3f96c89a520de7cb8857bac7b0d3d17252bf5ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 15 May 2026 18:29:29 +0200 Subject: [PATCH 1/4] machinectl: reorder verb functions to match --help The net diff is negative because some spurious whitespace and forward declarations were dropped. One new forward declaration was added. (For verb_poweroff_machine. The func could be moved, but I think it's better to keep it adjacent to verb_reboot_machine which is very similar.) --- src/machine/machinectl.c | 1936 +++++++++++++++++++------------------- 1 file changed, 966 insertions(+), 970 deletions(-) diff --git a/src/machine/machinectl.c b/src/machine/machinectl.c index 3d7f782a8c..1e704e4d96 100644 --- a/src/machine/machinectl.c +++ b/src/machine/machinectl.c @@ -361,66 +361,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 +623,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) }, @@ -792,711 +731,133 @@ static int verb_show_machine(int argc, char *argv[], uintptr_t _data, void *user return r; } -static int print_image_hostname(sd_bus *bus, const char *name) { - _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; - const char *hn; +static int make_service_name(const char *name, char **ret) { int r; - r = bus_call_method(bus, bus_machine_mgr, "GetImageHostname", NULL, &reply, "s", name); - if (r < 0) - return r; + assert(name); + assert(ret); + assert(arg_runner >= 0 && arg_runner < _RUNNER_MAX); - r = sd_bus_message_read(reply, "s", &hn); - if (r < 0) - return r; + if (!hostname_is_valid(name, 0)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid machine name %s.", name); - if (!isempty(hn)) - printf("\tHostname: %s\n", hn); + 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 print_image_machine_id(sd_bus *bus, const char *name) { - _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; - sd_id128_t id; - int r; - - r = bus_call_method(bus, bus_machine_mgr, "GetImageMachineID", NULL, &reply, "s", name); - if (r < 0) - return r; - - r = bus_message_read_id128(reply, &id); - if (r < 0) - return r; - - if (!sd_id128_is_null(id)) - printf(" Machine ID: " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(id)); - - return 0; -} - -static int print_image_machine_info(sd_bus *bus, const char *name) { - _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; - int r; - - r = bus_call_method(bus, bus_machine_mgr, "GetImageMachineInfo", NULL, &reply, "s", name); - if (r < 0) - return r; - - r = sd_bus_message_enter_container(reply, 'a', "{ss}"); - if (r < 0) - return r; - - for (;;) { - const char *p, *q; - - r = sd_bus_message_read(reply, "{ss}", &p, &q); - if (r < 0) - return r; - if (r == 0) - break; - - if (streq(p, "DEPLOYMENT")) - printf(" Deployment: %s\n", q); - } - - r = sd_bus_message_exit_container(reply); - if (r < 0) - return r; - - return 0; -} - -typedef struct ImageStatusInfo { - const char *name; - const char *path; - const char *type; - bool read_only; - usec_t crtime; - usec_t mtime; - uint64_t usage; - uint64_t limit; - uint64_t usage_exclusive; - uint64_t limit_exclusive; -} ImageStatusInfo; - -static void print_image_status_info(sd_bus *bus, ImageStatusInfo *i) { - assert(bus); - assert(i); - - if (i->name) { - fputs(i->name, stdout); - putchar('\n'); - } - - if (i->type) - printf("\t Type: %s\n", i->type); - - if (i->path) - printf("\t Path: %s\n", i->path); - - (void) print_image_hostname(bus, i->name); - (void) print_image_machine_id(bus, i->name); - (void) print_image_machine_info(bus, i->name); - - print_os_release(bus, "GetImageOSRelease", i->name, "\t OS: "); - - printf("\t RO: %s%s%s\n", - i->read_only ? ansi_highlight_red() : "", - i->read_only ? "read-only" : "writable", - i->read_only ? ansi_normal() : ""); - - if (timestamp_is_set(i->crtime)) - printf("\t Created: %s; %s\n", - FORMAT_TIMESTAMP(i->crtime), FORMAT_TIMESTAMP_RELATIVE(i->crtime)); - - if (timestamp_is_set(i->mtime)) - printf("\tModified: %s; %s\n", - FORMAT_TIMESTAMP(i->mtime), FORMAT_TIMESTAMP_RELATIVE(i->mtime)); - - if (i->usage != UINT64_MAX) { - if (i->usage_exclusive != i->usage && i->usage_exclusive != UINT64_MAX) - printf("\t Usage: %s (exclusive: %s)\n", - FORMAT_BYTES(i->usage), FORMAT_BYTES(i->usage_exclusive)); - else - printf("\t Usage: %s\n", FORMAT_BYTES(i->usage)); - } - - if (i->limit != UINT64_MAX) { - if (i->limit_exclusive != i->limit && i->limit_exclusive != UINT64_MAX) - printf("\t Limit: %s (exclusive: %s)\n", - FORMAT_BYTES(i->limit), FORMAT_BYTES(i->limit_exclusive)); - else - printf("\t Limit: %s\n", FORMAT_BYTES(i->limit)); - } -} - -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) }, - { "Type", "s", NULL, offsetof(ImageStatusInfo, type) }, - { "ReadOnly", "b", NULL, offsetof(ImageStatusInfo, read_only) }, - { "CreationTimestamp", "t", NULL, offsetof(ImageStatusInfo, crtime) }, - { "ModificationTimestamp", "t", NULL, offsetof(ImageStatusInfo, mtime) }, - { "Usage", "t", NULL, offsetof(ImageStatusInfo, usage) }, - { "Limit", "t", NULL, offsetof(ImageStatusInfo, limit) }, - { "UsageExclusive", "t", NULL, offsetof(ImageStatusInfo, usage_exclusive) }, - { "LimitExclusive", "t", NULL, offsetof(ImageStatusInfo, limit_exclusive) }, - {} - }; - +static int image_exists(sd_bus *bus, const char *name) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; - ImageStatusInfo info = {}; int r; assert(bus); - assert(path); - assert(new_line); + assert(name); - r = bus_map_all_properties(bus, - "org.freedesktop.machine1", - path, - map, - BUS_MAP_BOOLEAN_AS_BOOL, - &error, - &m, - &info); - if (r < 0) - return log_error_errno(r, "Could not get properties: %s", bus_error_message(&error, r)); + 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; - if (*new_line) - printf("\n"); - *new_line = true; + return log_error_errno(r, "Failed to check whether image %s exists: %s", name, bus_error_message(&error, r)); + } - print_image_status_info(bus, &info); - - return r; + return 1; } -typedef struct PoolStatusInfo { - const char *path; - uint64_t usage; - uint64_t limit; -} PoolStatusInfo; - -static void print_pool_status_info(sd_bus *bus, PoolStatusInfo *i) { - if (i->path) - printf("\t Path: %s\n", i->path); - - if (i->usage != UINT64_MAX) - printf("\t Usage: %s\n", FORMAT_BYTES(i->usage)); - - if (i->limit != UINT64_MAX) - printf("\t Limit: %s\n", FORMAT_BYTES(i->limit)); -} - -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) }, - { "PoolLimit", "t", NULL, offsetof(PoolStatusInfo, limit) }, - {} - }; - - PoolStatusInfo info = { - .usage = UINT64_MAX, - .limit = UINT64_MAX, - }; - +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_(sd_bus_message_unrefp) sd_bus_message *m = NULL; - int r; - - assert(bus); - - r = bus_map_all_properties(bus, - "org.freedesktop.machine1", - "/org/freedesktop/machine1", - map, - 0, - &error, - &m, - &info); - if (r < 0) - return log_error_errno(r, "Could not get properties: %s", bus_error_message(&error, r)); - - print_pool_status_info(bus, &info); - - return 0; -} - -static int show_image_properties(sd_bus *bus, const char *path, bool *new_line) { - int r; - - assert(bus); - assert(path); - assert(new_line); - - if (*new_line) - printf("\n"); - - *new_line = true; - - r = bus_print_all_properties(bus, "org.freedesktop.machine1", path, NULL, arg_property, arg_print_flags, NULL); - if (r < 0) - log_error_errno(r, "Could not get properties: %m"); - - return r; -} - -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; + _cleanup_(bus_wait_for_jobs_freep) BusWaitForJobs *w = NULL; sd_bus *bus = ASSERT_PTR(userdata); - int r = 0; + int r; - properties = !strstr(argv[0], "status"); + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + ask_password_agent_open_if_enabled(arg_transport, arg_ask_password); - pager_open(arg_pager_flags); - - if (argc <= 1) { - - /* If no argument is specified, inspect the manager - * itself */ - - if (properties) - r = show_image_properties(bus, "/org/freedesktop/machine1", &new_line); - else - r = show_pool_info(bus); - if (r < 0) - return r; - } + 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; - const char *path = NULL; + _cleanup_free_ char *unit = NULL; + const char *object; - r = bus_call_method(bus, bus_machine_mgr, "GetImage", &error, &reply, "s", argv[i]); + r = make_service_name(argv[i], &unit); if (r < 0) - return log_error_errno(r, "Could not get path to image: %s", bus_error_message(&error, r)); + return r; - r = sd_bus_message_read(reply, "o", &path); + 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); - if (properties) - r = show_image_properties(bus, path, &new_line); - else - r = show_image_info(bus, path, &new_line); - } - - 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); + r = bus_wait_for_jobs_add(w, object); if (r < 0) - return log_error_errno(r, "Could not kill machine: %s", bus_error_message(&error, r)); + return log_oom(); } - 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); + r = bus_wait_for_jobs(w, arg_quiet, NULL); 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; +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; - 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); + 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; } @@ -1554,41 +915,22 @@ static int process_forward(sd_event *event, sd_bus_slot *machine_removed_slot, i 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; +static int on_machine_removed(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { + PTYForward *forward = ASSERT_PTR(userdata); + int r; - assert(uid); - assert(machine); + assert(m); - if (spec) { - const char *at; + /* Tell the forwarder to exit on the next vhangup(), so that we still flush out what might be queued + * and exit then. */ - 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; + 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); } - *uid = _uid; - *machine = isempty(_machine) ? ".host" : _machine; return 0; } @@ -1731,6 +1073,734 @@ static int verb_shell_machine(int argc, char *argv[], uintptr_t _data, void *use return process_forward(event, slot, master, /* flags= */ 0, machine); } +static int verb_poweroff_machine(int argc, char *argv[], uintptr_t data, void *userdata); + +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; +} + +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_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; +} + +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 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; +} + +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"; +} + +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_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 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; + int r; + + r = bus_call_method(bus, bus_machine_mgr, "GetImageHostname", NULL, &reply, "s", name); + if (r < 0) + return r; + + r = sd_bus_message_read(reply, "s", &hn); + if (r < 0) + return r; + + if (!isempty(hn)) + printf("\tHostname: %s\n", hn); + + return 0; +} + +static int print_image_machine_id(sd_bus *bus, const char *name) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + sd_id128_t id; + int r; + + r = bus_call_method(bus, bus_machine_mgr, "GetImageMachineID", NULL, &reply, "s", name); + if (r < 0) + return r; + + r = bus_message_read_id128(reply, &id); + if (r < 0) + return r; + + if (!sd_id128_is_null(id)) + printf(" Machine ID: " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(id)); + + return 0; +} + +static int print_image_machine_info(sd_bus *bus, const char *name) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + int r; + + r = bus_call_method(bus, bus_machine_mgr, "GetImageMachineInfo", NULL, &reply, "s", name); + if (r < 0) + return r; + + r = sd_bus_message_enter_container(reply, 'a', "{ss}"); + if (r < 0) + return r; + + for (;;) { + const char *p, *q; + + r = sd_bus_message_read(reply, "{ss}", &p, &q); + if (r < 0) + return r; + if (r == 0) + break; + + if (streq(p, "DEPLOYMENT")) + printf(" Deployment: %s\n", q); + } + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return r; + + return 0; +} + +typedef struct ImageStatusInfo { + const char *name; + const char *path; + const char *type; + bool read_only; + usec_t crtime; + usec_t mtime; + uint64_t usage; + uint64_t limit; + uint64_t usage_exclusive; + uint64_t limit_exclusive; +} ImageStatusInfo; + +static void print_image_status_info(sd_bus *bus, ImageStatusInfo *i) { + assert(bus); + assert(i); + + if (i->name) { + fputs(i->name, stdout); + putchar('\n'); + } + + if (i->type) + printf("\t Type: %s\n", i->type); + + if (i->path) + printf("\t Path: %s\n", i->path); + + (void) print_image_hostname(bus, i->name); + (void) print_image_machine_id(bus, i->name); + (void) print_image_machine_info(bus, i->name); + + print_os_release(bus, "GetImageOSRelease", i->name, "\t OS: "); + + printf("\t RO: %s%s%s\n", + i->read_only ? ansi_highlight_red() : "", + i->read_only ? "read-only" : "writable", + i->read_only ? ansi_normal() : ""); + + if (timestamp_is_set(i->crtime)) + printf("\t Created: %s; %s\n", + FORMAT_TIMESTAMP(i->crtime), FORMAT_TIMESTAMP_RELATIVE(i->crtime)); + + if (timestamp_is_set(i->mtime)) + printf("\tModified: %s; %s\n", + FORMAT_TIMESTAMP(i->mtime), FORMAT_TIMESTAMP_RELATIVE(i->mtime)); + + if (i->usage != UINT64_MAX) { + if (i->usage_exclusive != i->usage && i->usage_exclusive != UINT64_MAX) + printf("\t Usage: %s (exclusive: %s)\n", + FORMAT_BYTES(i->usage), FORMAT_BYTES(i->usage_exclusive)); + else + printf("\t Usage: %s\n", FORMAT_BYTES(i->usage)); + } + + if (i->limit != UINT64_MAX) { + if (i->limit_exclusive != i->limit && i->limit_exclusive != UINT64_MAX) + printf("\t Limit: %s (exclusive: %s)\n", + FORMAT_BYTES(i->limit), FORMAT_BYTES(i->limit_exclusive)); + else + printf("\t Limit: %s\n", FORMAT_BYTES(i->limit)); + } +} + +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) }, + { "Type", "s", NULL, offsetof(ImageStatusInfo, type) }, + { "ReadOnly", "b", NULL, offsetof(ImageStatusInfo, read_only) }, + { "CreationTimestamp", "t", NULL, offsetof(ImageStatusInfo, crtime) }, + { "ModificationTimestamp", "t", NULL, offsetof(ImageStatusInfo, mtime) }, + { "Usage", "t", NULL, offsetof(ImageStatusInfo, usage) }, + { "Limit", "t", NULL, offsetof(ImageStatusInfo, limit) }, + { "UsageExclusive", "t", NULL, offsetof(ImageStatusInfo, usage_exclusive) }, + { "LimitExclusive", "t", NULL, offsetof(ImageStatusInfo, limit_exclusive) }, + {} + }; + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + ImageStatusInfo info = {}; + int r; + + assert(bus); + assert(path); + assert(new_line); + + r = bus_map_all_properties(bus, + "org.freedesktop.machine1", + path, + map, + BUS_MAP_BOOLEAN_AS_BOOL, + &error, + &m, + &info); + if (r < 0) + return log_error_errno(r, "Could not get properties: %s", bus_error_message(&error, r)); + + if (*new_line) + printf("\n"); + *new_line = true; + + print_image_status_info(bus, &info); + + return r; +} + +typedef struct PoolStatusInfo { + const char *path; + uint64_t usage; + uint64_t limit; +} PoolStatusInfo; + +static void print_pool_status_info(sd_bus *bus, PoolStatusInfo *i) { + if (i->path) + printf("\t Path: %s\n", i->path); + + if (i->usage != UINT64_MAX) + printf("\t Usage: %s\n", FORMAT_BYTES(i->usage)); + + if (i->limit != UINT64_MAX) + printf("\t Limit: %s\n", FORMAT_BYTES(i->limit)); +} + +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) }, + { "PoolLimit", "t", NULL, offsetof(PoolStatusInfo, limit) }, + {} + }; + + PoolStatusInfo info = { + .usage = UINT64_MAX, + .limit = UINT64_MAX, + }; + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + int r; + + assert(bus); + + r = bus_map_all_properties(bus, + "org.freedesktop.machine1", + "/org/freedesktop/machine1", + map, + 0, + &error, + &m, + &info); + if (r < 0) + return log_error_errno(r, "Could not get properties: %s", bus_error_message(&error, r)); + + print_pool_status_info(bus, &info); + + return 0; +} + +static int show_image_properties(sd_bus *bus, const char *path, bool *new_line) { + int r; + + assert(bus); + assert(path); + assert(new_line); + + if (*new_line) + printf("\n"); + + *new_line = true; + + r = bus_print_all_properties(bus, "org.freedesktop.machine1", path, NULL, arg_property, arg_print_flags, NULL); + if (r < 0) + log_error_errno(r, "Could not get properties: %m"); + + return r; +} + +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; + sd_bus *bus = ASSERT_PTR(userdata); + int r = 0; + + properties = !strstr(argv[0], "status"); + + pager_open(arg_pager_flags); + + if (argc <= 1) { + + /* If no argument is specified, inspect the manager + * itself */ + + if (properties) + r = show_image_properties(bus, "/org/freedesktop/machine1", &new_line); + else + r = show_pool_info(bus); + if (r < 0) + return r; + } + + for (int i = 1; i < argc; i++) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + const char *path = NULL; + + r = bus_call_method(bus, bus_machine_mgr, "GetImage", &error, &reply, "s", argv[i]); + if (r < 0) + return log_error_errno(r, "Could not get path to image: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "o", &path); + if (r < 0) + return bus_log_parse_error(r); + + if (properties) + r = show_image_properties(bus, path, &new_line); + else + r = show_image_info(bus, path, &new_line); + } + + return r; +} + static int normalize_nspawn_filename(const char *name, char **ret_file) { _cleanup_free_ char *file = NULL; @@ -1892,53 +1962,6 @@ static int verb_cat_settings(int argc, char *argv[], uintptr_t _data, void *user return r; } -static int verb_remove_image(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++) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; - - r = bus_message_new_method_call(bus, &m, bus_machine_mgr, "RemoveImage"); - if (r < 0) - return bus_log_create_error(r); - - r = sd_bus_message_append(m, "s", argv[i]); - 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 remove image: %s", bus_error_message(&error, r)); - } - - 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; @@ -1963,6 +1986,26 @@ static int verb_clone_image(int argc, char *argv[], uintptr_t _data, void *userd 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_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); @@ -1985,189 +2028,28 @@ static int verb_read_only_image(int argc, char *argv[], uintptr_t _data, void *u 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; +static int verb_remove_image(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); - 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; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = 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 = 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"); + r = bus_message_new_method_call(bus, &m, bus_machine_mgr, "RemoveImage"); 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); + r = sd_bus_message_append(m, "s", argv[i]); 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); + /* 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 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 log_error_errno(r, "Could not remove image: %s", bus_error_message(&error, r)); } return 0; @@ -2257,6 +2139,120 @@ static int verb_clean_images(int argc, char *argv[], uintptr_t _data, void *user 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 chainload_importctl(int argc, char *argv[]) { int r; From 6b8c3af359265ac958b9e42237d6068dcc8b0264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 15 May 2026 18:34:03 +0200 Subject: [PATCH 2/4] machinectl: reorder cases in parse_argv() to match order in --help --no-pager, --no-legend, --no-ask-password, --quiet are shifted to the end. Co-developed-by: Claude Opus 4.7 (1M context) --- src/machine/machinectl.c | 148 +++++++++++++++++++-------------------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/src/machine/machinectl.c b/src/machine/machinectl.c index 1e704e4d96..657c69e2a1 100644 --- a/src/machine/machinectl.c +++ b/src/machine/machinectl.c @@ -2520,6 +2520,24 @@ static int parse_argv(int argc, char *argv[]) { case ARG_VERSION: return version(); + case 'H': + arg_transport = BUS_TRANSPORT_REMOTE; + arg_host = optarg; + break; + + case 'M': + arg_transport = BUS_TRANSPORT_MACHINE; + arg_host = optarg; + break; + + case ARG_SYSTEM: + arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; + break; + + case ARG_USER: + arg_runtime_scope = RUNTIME_SCOPE_USER; + break; + case 'p': case 'P': r = strv_extend(&arg_property, optarg); @@ -2546,12 +2564,48 @@ static int parse_argv(int argc, char *argv[]) { arg_full = 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_UID: + arg_uid = optarg; + break; + + case 'E': + r = strv_env_replace_strdup_passthrough(&arg_setenv, optarg); + if (r < 0) + return log_error_errno(r, "Cannot assign environment variable %s: %m", optarg); + break; + + case ARG_READ_ONLY: + arg_read_only = true; + break; + + case ARG_MKDIR: + arg_mkdir = true; + break; + case 'n': if (safe_atou(optarg, &arg_lines) < 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse lines '%s'", optarg); break; + case ARG_MAX_ADDRESSES: + if (streq(optarg, "all")) + arg_max_addresses = UINT_MAX; + else if (safe_atou(optarg, &arg_max_addresses) < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid number of addresses: %s", optarg); + break; + case 'o': if (streq(optarg, "help")) return DUMP_STRING_TABLE(output_mode, OutputMode, _OUTPUT_MODE_MAX); @@ -2565,48 +2619,24 @@ static int parse_argv(int argc, char *argv[]) { arg_legend = false; break; - case ARG_NO_PAGER: - arg_pager_flags |= PAGER_DISABLE; + case ARG_FORCE: + arg_force = true; break; - case ARG_NO_LEGEND: - arg_legend = false; + case ARG_NOW: + arg_now = true; break; - case ARG_KILL_WHOM: - arg_kill_whom = optarg; + case ARG_RUNNER: + r = machine_runner_from_string(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse --runner= setting: %s", optarg); + + arg_runner = r; 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; + case 'V': + arg_runner = RUNNER_VMSPAWN; break; case ARG_VERIFY: @@ -2619,26 +2649,6 @@ static int parse_argv(int argc, char *argv[]) { 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); - - arg_runner = r; - break; - - case ARG_NOW: - arg_now = true; - break; - - case ARG_FORCE: - arg_force = true; - break; - case ARG_FORMAT: if (!STR_IN_SET(optarg, "uncompressed", "xz", "gzip", "bzip2", "zstd")) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), @@ -2647,30 +2657,20 @@ static int parse_argv(int argc, char *argv[]) { arg_format = optarg; break; - case ARG_UID: - arg_uid = optarg; + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; break; - case 'E': - r = strv_env_replace_strdup_passthrough(&arg_setenv, optarg); - if (r < 0) - return log_error_errno(r, "Cannot assign environment variable %s: %m", optarg); + case ARG_NO_LEGEND: + arg_legend = false; break; - case ARG_MAX_ADDRESSES: - if (streq(optarg, "all")) - arg_max_addresses = UINT_MAX; - else if (safe_atou(optarg, &arg_max_addresses) < 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Invalid number of addresses: %s", optarg); + case ARG_NO_ASK_PASSWORD: + arg_ask_password = false; break; - case ARG_USER: - arg_runtime_scope = RUNTIME_SCOPE_USER; - break; - - case ARG_SYSTEM: - arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; + case 'q': + arg_quiet = true; break; case '?': From cab5c5ef8177a077e0b6f61bd31903e61ad2cc72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 15 May 2026 19:29:36 +0200 Subject: [PATCH 3/4] shared/verbs: add VERB_DEFAULT_NOARG --- src/bless-boot/bless-boot.c | 2 +- src/busctl/busctl.c | 3 +-- src/creds/creds.c | 3 +-- src/factory-reset/factory-reset-tool.c | 2 +- src/hostname/hostnamectl.c | 2 +- src/import/importctl.c | 2 +- src/locale/localectl.c | 3 +-- src/login/loginctl.c | 3 +-- src/measure/measure-tool.c | 3 +-- src/oom/oomctl.c | 3 +-- src/pcrlock/pcrlock.c | 2 +- src/portable/portablectl.c | 2 +- src/shared/verbs.h | 2 ++ src/sysext/sysext.c | 2 +- src/timedate/timedatectl.c | 2 +- 15 files changed, 16 insertions(+), 20 deletions(-) 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/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[] = { From 3b37439afb88cfe55e111ad1a5421bd57405b3b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 15 May 2026 19:19:15 +0200 Subject: [PATCH 4/4] machinectl: convert to OPTION and VERB macros For the shell verb we want switches specified after the program name to be passed to the program to execute, not processed by us. Mirror the approach in 'userdbctl ssh-authorized-keys': start with OPTION_PARSER_RETURN_POSITIONAL_ARGS, then lates switch to STOP_AT_FIRST_NONOPTION for "shell" or NORMAL otherwise. VERB declarations are placed directly above each function; functions that dispatch multiple verb names get stacked VERB() declarations. chainload_importctl() now takes the args strv instead of relying on the global optind. --help output is mostly the same. --no-pager/--no-legend/--no-ask-password/-q/--quiet are now at the end. bind-volume/unbind-volume are documented. Fixes c9f461a8067996c6b0c3ac3bf6f9097aedbf4734. Co-developed-by: Claude Opus 4.7 (1M context) --- src/machine/machinectl.c | 537 ++++++++++---------------- test/units/TEST-13-NSPAWN.machined.sh | 1 + 2 files changed, 214 insertions(+), 324 deletions(-) diff --git a/src/machine/machinectl.c b/src/machine/machinectl.c index 657c69e2a1..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; @@ -691,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; @@ -767,6 +775,8 @@ static int image_exists(sd_bus *bus, const char *name) { 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; @@ -934,6 +944,8 @@ static int on_machine_removed(sd_bus_message *m, void *userdata, sd_bus_error *r 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; @@ -985,6 +997,8 @@ static int verb_login_machine(int argc, char *argv[], uintptr_t _data, void *use 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; @@ -1075,6 +1089,10 @@ static int verb_shell_machine(int argc, char *argv[], uintptr_t _data, void *use 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; @@ -1242,6 +1260,10 @@ static int verb_machine_control_one(const char *machine_name, const char *method 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; @@ -1267,6 +1289,10 @@ static int verb_poweroff_machine(int argc, char *argv[], uintptr_t data, void *u 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; @@ -1305,14 +1331,20 @@ static int verb_vm_control(int argc, char *argv[], const char *method) { 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; @@ -1337,6 +1369,8 @@ static int verb_terminate_machine(int argc, char *argv[], uintptr_t _data, void 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); @@ -1369,6 +1403,10 @@ static const char* select_copy_method(bool copy_from, bool force) { 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; @@ -1424,6 +1462,8 @@ static int verb_copy_files(int argc, char *argv[], uintptr_t _data, void *userda 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); @@ -1449,6 +1489,10 @@ static int verb_bind_mount(int argc, char *argv[], uintptr_t _data, void *userda 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; @@ -1757,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; @@ -1841,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; @@ -1911,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; @@ -1962,6 +2014,8 @@ 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; @@ -1986,6 +2040,8 @@ static int verb_clone_image(int argc, char *argv[], uintptr_t _data, void *userd 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); @@ -2006,6 +2062,8 @@ static int verb_rename_image(int argc, char *argv[], uintptr_t _data, void *user 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); @@ -2028,6 +2086,8 @@ static int verb_read_only_image(int argc, char *argv[], uintptr_t _data, void *u 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; @@ -2055,6 +2115,8 @@ static int verb_remove_image(int argc, char *argv[], uintptr_t _data, void *user 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; @@ -2085,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; @@ -2139,6 +2203,8 @@ static int verb_clean_images(int argc, char *argv[], uintptr_t _data, void *user return 0; } +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; @@ -2209,6 +2275,8 @@ static int verb_bind_volume(int argc, char *argv[], uintptr_t _data, void *userd 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; @@ -2253,11 +2321,12 @@ static int verb_unbind_volume(int argc, char *argv[], uintptr_t _data, void *use return 0; } -static int chainload_importctl(int argc, char *argv[]) { +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"); @@ -2288,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) { @@ -2301,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 'H': + OPTION_COMMON_HOST: arg_transport = BUS_TRANSPORT_REMOTE; - arg_host = optarg; + arg_host = opts.arg; break; - case 'M': + OPTION_COMMON_MACHINE: arg_transport = BUS_TRANSPORT_MACHINE; - arg_host = optarg; + arg_host = opts.arg; break; - case ARG_SYSTEM: + OPTION_LONG("system", NULL, "Connect to system machine manager"): arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; break; - case ARG_USER: + OPTION_LONG("user", NULL, "Connect to user machine manager"): arg_runtime_scope = RUNTIME_SCOPE_USER; break; - case 'p': - case 'P': - r = strv_extend(&arg_property, optarg); + 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 ARG_KILL_WHOM: - arg_kill_whom = optarg; + OPTION_LONG("kill-whom", "WHOM", "Whom to send signal to"): + arg_kill_whom = opts.arg; break; - case 's': - r = parse_signal_argument(optarg, &arg_signal); + OPTION('s', "signal", "SIGNAL", "Which signal to send"): + r = parse_signal_argument(opts.arg, &arg_signal); if (r <= 0) return r; break; - case ARG_UID: - arg_uid = optarg; + OPTION_LONG("uid", "USER", "Specify user ID to invoke shell as"): + arg_uid = opts.arg; break; - case 'E': - r = strv_env_replace_strdup_passthrough(&arg_setenv, optarg); + 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", optarg); + return log_error_errno(r, "Cannot assign environment variable %s: %m", opts.arg); break; - case ARG_READ_ONLY: + OPTION_LONG("read-only", NULL, "Create read-only bind mount or clone"): arg_read_only = true; break; - case ARG_MKDIR: + OPTION_LONG("mkdir", NULL, "Create directory before bind mounting, if missing"): arg_mkdir = true; break; - case 'n': - if (safe_atou(optarg, &arg_lines) < 0) + 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'", optarg); + "Failed to parse lines '%s'", opts.arg); break; - case ARG_MAX_ADDRESSES: - if (streq(optarg, "all")) + 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(optarg, &arg_max_addresses) < 0) + else if (safe_atou(opts.arg, &arg_max_addresses) < 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Invalid number of addresses: %s", optarg); + "Invalid number of addresses: %s", opts.arg); break; - case 'o': - if (streq(optarg, "help")) + 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_FORCE: + OPTION_LONG("force", NULL, "Replace target file when copying, if necessary"): arg_force = true; break; - case ARG_NOW: + OPTION_LONG("now", NULL, + "Start or power off container after enabling or disabling it"): arg_now = true; break; - case ARG_RUNNER: - r = machine_runner_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 --runner= setting: %s", optarg); + return log_error_errno(r, "Failed to parse --runner= setting: %s", opts.arg); arg_runner = r; break; - case 'V': + OPTION_SHORT('V', NULL, "Short for --runner=vmspawn"): arg_runner = RUNNER_VMSPAWN; break; - case ARG_VERIFY: - if (streq(optarg, "help")) + /* Hidden options below */ + + OPTION_LONG("verify", "MODE", /* help= */ NULL): + if (streq(opts.arg, "help")) return DUMP_STRING_TABLE(import_verify, ImportVerify, _IMPORT_VERIFY_MAX); - r = import_verify_from_string(optarg); + r = import_verify_from_string(opts.arg); if (r < 0) - return log_error_errno(r, "Failed to parse --verify= setting: %s", optarg); + return log_error_errno(r, "Failed to parse --verify= setting: %s", opts.arg); arg_verify = r; break; - case ARG_FORMAT: - if (!STR_IN_SET(optarg, "uncompressed", "xz", "gzip", "bzip2", "zstd")) + OPTION_LONG("format", "FORMAT", /* help= */ NULL): + if (!STR_IN_SET(opts.arg, "uncompressed", "xz", "gzip", "bzip2", "zstd")) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Unknown format: %s", optarg); + "Unknown format: %s", opts.arg); - arg_format = optarg; + arg_format = opts.arg; break; - case ARG_NO_PAGER: + OPTION_COMMON_NO_PAGER: arg_pager_flags |= PAGER_DISABLE; break; - case ARG_NO_LEGEND: + OPTION_COMMON_NO_LEGEND: arg_legend = false; break; - case ARG_NO_ASK_PASSWORD: + OPTION_COMMON_NO_ASK_PASSWORD: arg_ask_password = false; break; - case 'q': + OPTION('q', "quiet", NULL, "Suppress output"): arg_quiet = true; break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } - } -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) @@ -2767,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/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