diff options
Diffstat (limited to 'src/portable/portablectl.c')
-rw-r--r-- | src/portable/portablectl.c | 1459 |
1 files changed, 1459 insertions, 0 deletions
diff --git a/src/portable/portablectl.c b/src/portable/portablectl.c new file mode 100644 index 0000000..1588b17 --- /dev/null +++ b/src/portable/portablectl.c @@ -0,0 +1,1459 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <getopt.h> + +#include "sd-bus.h" + +#include "alloc-util.h" +#include "build.h" +#include "bus-error.h" +#include "bus-locator.h" +#include "bus-unit-util.h" +#include "bus-wait-for-jobs.h" +#include "chase.h" +#include "constants.h" +#include "dirent-util.h" +#include "env-file.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-table.h" +#include "fs-util.h" +#include "locale-util.h" +#include "main-func.h" +#include "os-util.h" +#include "pager.h" +#include "parse-argument.h" +#include "parse-util.h" +#include "path-util.h" +#include "portable.h" +#include "pretty-print.h" +#include "spawn-polkit-agent.h" +#include "string-util.h" +#include "strv.h" +#include "terminal-util.h" +#include "verbs.h" + +static PagerFlags arg_pager_flags = 0; +static bool arg_legend = true; +static bool arg_ask_password = true; +static bool arg_quiet = false; +static const char *arg_profile = "default"; +static const char* arg_copy_mode = NULL; +static bool arg_runtime = false; +static bool arg_reload = true; +static bool arg_cat = false; +static BusTransport arg_transport = BUS_TRANSPORT_LOCAL; +static const char *arg_host = NULL; +static bool arg_enable = false; +static bool arg_now = false; +static bool arg_no_block = false; +static char **arg_extension_images = NULL; +static bool arg_force = false; + +STATIC_DESTRUCTOR_REGISTER(arg_extension_images, strv_freep); + +static bool is_portable_managed(const char *unit) { + return ENDSWITH_SET(unit, ".service", ".target", ".socket", ".path", ".timer"); +} + +static int determine_image(const char *image, bool permit_non_existing, char **ret) { + int r; + + /* If the specified name is a valid image name, we pass it as-is to portabled, which will search for it in the + * usual search directories. Otherwise we presume it's a path, and will normalize it on the client's side + * (among other things, to make the path independent of the client's working directory) before passing it + * over. */ + + if (image_name_is_valid(image)) { + char *c; + + if (!arg_quiet && laccess(image, F_OK) >= 0) + log_warning("Ambiguous invocation: current working directory contains file matching non-path argument '%s', ignoring. " + "Prefix argument with './' to force reference to file in current working directory.", image); + + c = strdup(image); + if (!c) + return log_oom(); + + *ret = c; + return 0; + } + + if (arg_transport != BUS_TRANSPORT_LOCAL) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Operations on images by path not supported when connecting to remote systems."); + + r = chase(image, NULL, CHASE_TRAIL_SLASH | (permit_non_existing ? CHASE_NONEXISTENT : 0), ret, NULL); + if (r < 0) + return log_error_errno(r, "Cannot normalize specified image path '%s': %m", image); + + return 0; +} + +static int attach_extensions_to_message(sd_bus_message *m, const char *method, char **extensions) { + int r; + + assert(m); + assert(method); + + /* The new methods also have flags parameters that are independent of the extensions */ + if (strv_isempty(extensions) && !endswith(method, "WithExtensions")) + return 0; + + r = sd_bus_message_open_container(m, 'a', "s"); + if (r < 0) + return bus_log_create_error(r); + + STRV_FOREACH(p, extensions) { + _cleanup_free_ char *resolved_extension_image = NULL; + + r = determine_image( + *p, + startswith_strv(method, STRV_MAKE("Get", "Detach")), + &resolved_extension_image); + if (r < 0) + return r; + + r = sd_bus_message_append(m, "s", resolved_extension_image); + 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); + + return 0; +} + +static int extract_prefix(const char *path, char **ret) { + _cleanup_free_ char *name = NULL, *bn = NULL; + const char *underscore; + size_t m; + int r; + + r = path_extract_filename(path, &bn); + if (r < 0) + return r; + + underscore = strchr(bn, '_'); + if (underscore) + m = underscore - bn; + else { + const char *e; + + e = endswith(bn, ".raw"); + if (!e) + e = strchr(bn, 0); + + m = e - bn; + } + + name = strndup(bn, m); + if (!name) + return -ENOMEM; + + /* A slightly reduced version of what's permitted in unit names. With ':' and '\' are removed, as well as '_' + * which we use as delimiter for the second part of the image string, which we ignore for now. */ + if (!in_charset(name, DIGITS LETTERS "-.")) + return -EINVAL; + + if (!filename_is_valid(name)) + return -EINVAL; + + *ret = TAKE_PTR(name); + return 0; +} + +static int determine_matches(const char *image, char **l, bool allow_any, char ***ret) { + _cleanup_strv_free_ char **k = NULL; + int r; + + /* Determine the matches to apply. If the list is empty we derive the match from the image name. If the list + * contains exactly the "-" we return a wildcard list (which is the empty list), but only if this is expressly + * permitted. */ + + if (strv_isempty(l)) { + char *prefix; + + r = extract_prefix(image, &prefix); + if (r < 0) + return log_error_errno(r, "Failed to extract prefix of image name '%s': %m", image); + + if (!arg_quiet) + log_info("(Matching unit files with prefix '%s'.)", prefix); + + r = strv_consume(&k, prefix); + if (r < 0) + return log_oom(); + + } else if (strv_equal(l, STRV_MAKE("-"))) { + + if (!allow_any) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Refusing all unit file match."); + + if (!arg_quiet) + log_info("(Matching all unit files.)"); + } else { + + k = strv_copy(l); + if (!k) + return log_oom(); + + if (!arg_quiet) { + _cleanup_free_ char *joined = NULL; + + joined = strv_join(k, "', '"); + if (!joined) + return log_oom(); + + log_info("(Matching unit files with prefixes '%s'.)", joined); + } + } + + *ret = TAKE_PTR(k); + + return 0; +} + +static int acquire_bus(sd_bus **bus) { + int r; + + assert(bus); + + if (*bus) + return 0; + + r = bus_connect_transport(arg_transport, arg_host, RUNTIME_SCOPE_SYSTEM, bus); + if (r < 0) + return bus_log_connect_error(r, arg_transport); + + (void) sd_bus_set_allow_interactive_authorization(*bus, arg_ask_password); + + return 0; +} + +static int maybe_reload(sd_bus **bus) { + int r; + + if (!arg_reload) + return 0; + + r = acquire_bus(bus); + if (r < 0) + return r; + + return bus_service_manager_reload(*bus); +} + +static int get_image_metadata(sd_bus *bus, const char *image, char **matches, sd_bus_message **reply) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + uint64_t flags = arg_force ? PORTABLE_FORCE_EXTENSION : 0; + const char *method; + int r; + + assert(bus); + assert(reply); + + method = strv_isempty(arg_extension_images) && !arg_force ? "GetImageMetadata" : "GetImageMetadataWithExtensions"; + + r = bus_message_new_method_call(bus, &m, bus_portable_mgr, method); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", image); + if (r < 0) + return bus_log_create_error(r); + + r = attach_extensions_to_message(m, method, arg_extension_images); + if (r < 0) + return r; + + r = sd_bus_message_append_strv(m, matches); + if (r < 0) + return bus_log_create_error(r); + + if (streq(method, "GetImageMetadataWithExtensions")) { + r = sd_bus_message_append(m, "t", flags); + 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 inspect image metadata: %s", bus_error_message(&error, r)); + + return 0; +} + +static int inspect_image(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_strv_free_ char **matches = NULL; + _cleanup_free_ char *image = NULL; + bool nl = false, header = false; + const char *path; + const void *data; + size_t sz; + int r; + + r = determine_image(argv[1], false, &image); + if (r < 0) + return r; + + r = determine_matches(argv[1], argv + 2, true, &matches); + if (r < 0) + return r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + r = get_image_metadata(bus, image, matches, &reply); + if (r < 0) + return r; + + r = sd_bus_message_read(reply, "s", &path); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_read_array(reply, 'y', &data, &sz); + if (r < 0) + return bus_log_parse_error(r); + + pager_open(arg_pager_flags); + + if (arg_cat) { + printf("%s-- OS Release: --%s\n", ansi_highlight(), ansi_normal()); + fwrite(data, sz, 1, stdout); + fflush(stdout); + nl = true; + } else { + _cleanup_free_ char *pretty_portable = NULL, *pretty_os = NULL; + _cleanup_fclose_ FILE *f = NULL; + + f = fmemopen_unlocked((void*) data, sz, "r"); + if (!f) + return log_error_errno(errno, "Failed to open /etc/os-release buffer: %m"); + + r = parse_env_file(f, "/etc/os-release", + "PORTABLE_PRETTY_NAME", &pretty_portable, + "PRETTY_NAME", &pretty_os); + if (r < 0) + return log_error_errno(r, "Failed to parse /etc/os-release: %m"); + + printf("Image:\n\t%s\n" + "Portable Service:\n\t%s\n" + "Operating System:\n\t%s\n", + path, + strna(pretty_portable), + strna(pretty_os)); + } + + if (!strv_isempty(arg_extension_images)) { + /* If we specified any extensions, we'll first get back exactly the paths (and + * extension-release content) for each one of the arguments. */ + + r = sd_bus_message_enter_container(reply, 'a', "{say}"); + if (r < 0) + return bus_log_parse_error(r); + + for (size_t i = 0; i < strv_length(arg_extension_images); ++i) { + const char *name; + + r = sd_bus_message_enter_container(reply, 'e', "say"); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + r = sd_bus_message_read(reply, "s", &name); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_read_array(reply, 'y', &data, &sz); + if (r < 0) + return bus_log_parse_error(r); + + if (arg_cat) { + if (nl) + fputc('\n', stdout); + + printf("%s-- Extension Release: %s --%s\n", ansi_highlight(), name, ansi_normal()); + fwrite(data, sz, 1, stdout); + fflush(stdout); + nl = true; + } else { + _cleanup_free_ char *pretty_portable = NULL, *sysext_pretty_os = NULL, + *sysext_level = NULL, *sysext_id = NULL, + *sysext_version_id = NULL, *sysext_scope = NULL, + *portable_prefixes = NULL, *id = NULL, *version_id = NULL, + *sysext_image_id = NULL, *sysext_image_version = NULL, + *sysext_build_id = NULL, *confext_pretty_os = NULL, + *confext_level = NULL, *confext_id = NULL, + *confext_version_id = NULL, *confext_scope = NULL, + *confext_image_id = NULL, *confext_image_version = NULL, + *confext_build_id = NULL; + _cleanup_fclose_ FILE *f = NULL; + + f = fmemopen_unlocked((void*) data, sz, "r"); + if (!f) + return log_error_errno(errno, "Failed to open extension-release buffer: %m"); + + r = parse_env_file(f, name, + "SYSEXT_ID", &sysext_id, + "SYSEXT_VERSION_ID", &sysext_version_id, + "SYSEXT_BUILD_ID", &sysext_build_id, + "SYSEXT_IMAGE_ID", &sysext_image_id, + "SYSEXT_IMAGE_VERSION", &sysext_image_version, + "SYSEXT_SCOPE", &sysext_scope, + "SYSEXT_LEVEL", &sysext_level, + "SYSEXT_PRETTY_NAME", &sysext_pretty_os, + "CONFEXT_ID", &confext_id, + "CONFEXT_VERSION_ID", &confext_version_id, + "CONFEXT_BUILD_ID", &confext_build_id, + "CONFEXT_IMAGE_ID", &confext_image_id, + "CONFEXT_IMAGE_VERSION", &confext_image_version, + "CONFEXT_SCOPE", &confext_scope, + "CONFEXT_LEVEL", &confext_level, + "CONFEXT_PRETTY_NAME", &confext_pretty_os, + "ID", &id, + "VERSION_ID", &version_id, + "PORTABLE_PRETTY_NAME", &pretty_portable, + "PORTABLE_PREFIXES", &portable_prefixes); + if (r < 0) + return log_error_errno(r, "Failed to parse extension release from '%s': %m", name); + + printf("Extension:\n\t%s\n" + "\tExtension Scope:\n\t\t%s\n" + "\tExtension Compatibility Level:\n\t\t%s\n" + "\tExtension Compatibility OS:\n\t\t%s\n" + "\tExtension Compatibility OS Version:\n\t\t%s\n" + "\tPortable Service:\n\t\t%s\n" + "\tPortable Prefixes:\n\t\t%s\n" + "\tExtension Image:\n\t\t%s%s%s %s%s%s\n", + name, + strna(sysext_scope ?: confext_scope), + strna(sysext_level ?: confext_level), + strna(id), + strna(version_id), + strna(pretty_portable), + strna(portable_prefixes), + strempty(sysext_pretty_os ?: confext_pretty_os), + (sysext_pretty_os ?: confext_pretty_os) ? " (" : "ID: ", + strna(sysext_id ?: sysext_image_id ?: confext_id ?: confext_image_id), + (sysext_pretty_os ?: confext_pretty_os) ? "" : "Version: ", + strna(sysext_version_id ?: sysext_image_version ?: sysext_build_id ?: confext_version_id ?: confext_image_version ?: confext_build_id), + (sysext_pretty_os ?: confext_pretty_os) ? ")" : ""); + } + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + } + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + } + + r = sd_bus_message_enter_container(reply, 'a', "{say}"); + if (r < 0) + return bus_log_parse_error(r); + + for (;;) { + const char *name; + + r = sd_bus_message_enter_container(reply, 'e', "say"); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + r = sd_bus_message_read(reply, "s", &name); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_read_array(reply, 'y', &data, &sz); + if (r < 0) + return bus_log_parse_error(r); + + if (arg_cat) { + if (nl) + fputc('\n', stdout); + + printf("%s-- Unit file: %s --%s\n", ansi_highlight(), name, ansi_normal()); + fwrite(data, sz, 1, stdout); + fflush(stdout); + nl = true; + } else { + if (!header) { + fputs("Unit files:\n", stdout); + header = true; + } + + fputc('\t', stdout); + fputs(name, stdout); + fputc('\n', stdout); + } + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + } + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + return 0; +} + +static int print_changes(sd_bus_message *m) { + int r; + + if (arg_quiet) + return 0; + + r = sd_bus_message_enter_container(m, 'a', "(sss)"); + if (r < 0) + return bus_log_parse_error(r); + + for (;;) { + const char *type, *path, *source; + + r = sd_bus_message_read(m, "(sss)", &type, &path, &source); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + if (streq(type, "symlink")) + log_info("Created symlink %s %s %s.", path, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), source); + else if (streq(type, "copy")) { + if (isempty(source)) + log_info("Copied %s.", path); + else + log_info("Copied %s %s %s.", source, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), path); + } else if (streq(type, "unlink")) + log_info("Removed %s.", path); + else if (streq(type, "write")) + log_info("Written %s.", path); + else if (streq(type, "mkdir")) + log_info("Created directory %s.", path); + else + log_error("Unexpected change: %s/%s/%s", type, path, source); + } + + r = sd_bus_message_exit_container(m); + if (r < 0) + return r; + + return 0; +} + +static int maybe_enable_disable(sd_bus *bus, const char *path, bool enable) { + _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; + _cleanup_strv_free_ char **names = NULL; + const uint64_t flags = UNIT_FILE_PORTABLE | (arg_runtime ? UNIT_FILE_RUNTIME : 0); + int r; + + if (!arg_enable) + return 0; + + names = strv_new(path, NULL); + if (!names) + return log_oom(); + + r = bus_message_new_method_call( + bus, + &m, + bus_systemd_mgr, + enable ? "EnableUnitFilesWithFlags" : "DisableUnitFilesWithFlags"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append_strv(m, names); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "t", flags); + 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 %s the portable service %s: %s", + enable ? "enable" : "disable", path, bus_error_message(&error, r)); + + if (enable) { + r = sd_bus_message_skip(reply, "b"); + if (r < 0) + return bus_log_parse_error(r); + } + + (void) bus_deserialize_and_dump_unit_file_changes(reply, arg_quiet); + + return 0; +} + +static int maybe_start_stop_restart(sd_bus *bus, const char *path, const char *method, BusWaitForJobs *wait) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_free_ char *name = NULL; + const char *job = NULL; + int r; + + assert(STR_IN_SET(method, "StartUnit", "StopUnit", "RestartUnit")); + + if (!arg_now) + return 0; + + r = path_extract_filename(path, &name); + if (r < 0) + return log_error_errno(r, "Failed to extract file name from '%s': %m", path); + + r = bus_call_method( + bus, + bus_systemd_mgr, + method, + &error, + &reply, + "ss", name, "replace"); + if (r < 0) + return log_error_errno(r, "Failed to call %s on the portable service %s: %s", + method, + path, + bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "o", &job); + if (r < 0) + return bus_log_parse_error(r); + + if (!arg_quiet) + log_info("Queued %s to call %s on portable service %s.", job, method, name); + + if (wait) { + r = bus_wait_for_jobs_add(wait, job); + if (r < 0) + return log_error_errno(r, "Failed to watch %s job to call %s on %s: %m", + job, method, name); + } + + return 0; +} + +static int maybe_enable_start(sd_bus *bus, sd_bus_message *reply) { + _cleanup_(bus_wait_for_jobs_freep) BusWaitForJobs *wait = NULL; + int r; + + if (!arg_enable && !arg_now) + return 0; + + if (!arg_no_block) { + r = bus_wait_for_jobs_new(bus, &wait); + if (r < 0) + return log_error_errno(r, "Could not watch jobs: %m"); + } + + r = sd_bus_message_rewind(reply, true); + if (r < 0) + return r; + r = sd_bus_message_enter_container(reply, 'a', "(sss)"); + if (r < 0) + return bus_log_parse_error(r); + + for (;;) { + char *type, *path, *source; + + r = sd_bus_message_read(reply, "(sss)", &type, &path, &source); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + if (STR_IN_SET(type, "symlink", "copy") && is_portable_managed(path)) { + (void) maybe_enable_disable(bus, path, true); + (void) maybe_start_stop_restart(bus, path, "StartUnit", wait); + } + } + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return r; + + if (!arg_no_block) { + r = bus_wait_for_jobs(wait, arg_quiet, NULL); + if (r < 0) + return r; + } + + return 0; +} + +static int maybe_stop_enable_restart(sd_bus *bus, sd_bus_message *reply) { + _cleanup_(bus_wait_for_jobs_freep) BusWaitForJobs *wait = NULL; + int r; + + if (!arg_enable && !arg_now) + return 0; + + if (!arg_no_block) { + r = bus_wait_for_jobs_new(bus, &wait); + if (r < 0) + return log_error_errno(r, "Could not watch jobs: %m"); + } + + r = sd_bus_message_rewind(reply, true); + if (r < 0) + return r; + + /* First we get a list of units that were definitely removed, not just re-attached, + * so we can also stop them if the user asked us to. */ + r = sd_bus_message_enter_container(reply, 'a', "(sss)"); + if (r < 0) + return bus_log_parse_error(r); + + for (;;) { + char *type, *path, *source; + + r = sd_bus_message_read(reply, "(sss)", &type, &path, &source); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + if (streq(type, "unlink") && is_portable_managed(path)) + (void) maybe_start_stop_restart(bus, path, "StopUnit", wait); + } + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return r; + + /* Then we get a list of units that were either added or changed, so that we can + * enable them and/or restart them if the user asked us to. */ + r = sd_bus_message_enter_container(reply, 'a', "(sss)"); + if (r < 0) + return bus_log_parse_error(r); + + for (;;) { + char *type, *path, *source; + + r = sd_bus_message_read(reply, "(sss)", &type, &path, &source); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + if (STR_IN_SET(type, "symlink", "copy") && is_portable_managed(path)) { + (void) maybe_enable_disable(bus, path, true); + (void) maybe_start_stop_restart(bus, path, "RestartUnit", wait); + } + } + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return r; + + if (!arg_no_block) { + r = bus_wait_for_jobs(wait, arg_quiet, NULL); + if (r < 0) + return r; + } + + return 0; +} + +static int maybe_stop_disable(sd_bus *bus, char *image, char *argv[]) { + _cleanup_(bus_wait_for_jobs_freep) BusWaitForJobs *wait = NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_strv_free_ char **matches = NULL; + int r; + + if (!arg_enable && !arg_now) + return 0; + + r = determine_matches(argv[1], argv + 2, true, &matches); + if (r < 0) + return r; + + r = bus_wait_for_jobs_new(bus, &wait); + if (r < 0) + return log_error_errno(r, "Could not watch jobs: %m"); + + r = get_image_metadata(bus, image, matches, &reply); + if (r < 0) + return r; + + r = sd_bus_message_skip(reply, "say"); + if (r < 0) + return bus_log_parse_error(r); + + /* If we specified any extensions or --force (which makes the request go through the new + * WithExtensions calls), we'll first get an array of extension-release metadata. */ + if (!strv_isempty(arg_extension_images) || arg_force) { + r = sd_bus_message_skip(reply, "a{say}"); + if (r < 0) + return bus_log_parse_error(r); + } + + r = sd_bus_message_enter_container(reply, 'a', "{say}"); + if (r < 0) + return bus_log_parse_error(r); + + for (;;) { + const char *name; + + r = sd_bus_message_enter_container(reply, 'e', "say"); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + r = sd_bus_message_read(reply, "s", &name); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_skip(reply, "ay"); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + (void) maybe_start_stop_restart(bus, name, "StopUnit", wait); + (void) maybe_enable_disable(bus, name, false); + } + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + /* Stopping must always block or the detach will fail if the unit is still running */ + r = bus_wait_for_jobs(wait, arg_quiet, NULL); + if (r < 0) + return r; + + return 0; +} + +static int attach_reattach_image(int argc, char *argv[], const char *method) { + _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; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_strv_free_ char **matches = NULL; + _cleanup_free_ char *image = NULL; + int r; + + assert(method); + assert(STR_IN_SET(method, "AttachImage", "ReattachImage", "AttachImageWithExtensions", "ReattachImageWithExtensions")); + + r = determine_image(argv[1], false, &image); + if (r < 0) + return r; + + r = determine_matches(argv[1], argv + 2, false, &matches); + if (r < 0) + return r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + r = bus_message_new_method_call(bus, &m, bus_portable_mgr, method); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", image); + if (r < 0) + return bus_log_create_error(r); + + r = attach_extensions_to_message(m, method, arg_extension_images); + if (r < 0) + return r; + + r = sd_bus_message_append_strv(m, matches); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", arg_profile); + if (r < 0) + return bus_log_create_error(r); + + if (STR_IN_SET(method, "AttachImageWithExtensions", "ReattachImageWithExtensions")) { + uint64_t flags = (arg_runtime ? PORTABLE_RUNTIME : 0) | (arg_force ? PORTABLE_FORCE_ATTACH | PORTABLE_FORCE_EXTENSION : 0); + + r = sd_bus_message_append(m, "st", arg_copy_mode, flags); + } else + r = sd_bus_message_append(m, "bs", arg_runtime, arg_copy_mode); + 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, "%s failed: %s", method, bus_error_message(&error, r)); + + (void) maybe_reload(&bus); + + print_changes(reply); + + if (STR_IN_SET(method, "AttachImage", "AttachImageWithExtensions")) + (void) maybe_enable_start(bus, reply); + else { + /* ReattachImage returns 2 lists - removed units first, and changed/added second */ + print_changes(reply); + (void) maybe_stop_enable_restart(bus, reply); + } + + return 0; +} + +static int attach_image(int argc, char *argv[], void *userdata) { + return attach_reattach_image(argc, argv, strv_isempty(arg_extension_images) && !arg_force ? "AttachImage" : "AttachImageWithExtensions"); +} + +static int reattach_image(int argc, char *argv[], void *userdata) { + return attach_reattach_image(argc, argv, strv_isempty(arg_extension_images) && !arg_force ? "ReattachImage" : "ReattachImageWithExtensions"); +} + +static int detach_image(int argc, char *argv[], 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; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_free_ char *image = NULL; + const char *method; + int r; + + r = determine_image(argv[1], true, &image); + if (r < 0) + return r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + (void) maybe_stop_disable(bus, image, argv); + + method = strv_isempty(arg_extension_images) && !arg_force ? "DetachImage" : "DetachImageWithExtensions"; + + r = bus_message_new_method_call(bus, &m, bus_portable_mgr, method); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", image); + if (r < 0) + return bus_log_create_error(r); + + r = attach_extensions_to_message(m, method, arg_extension_images); + if (r < 0) + return r; + + if (streq(method, "DetachImage")) + r = sd_bus_message_append(m, "b", arg_runtime); + else { + uint64_t flags = (arg_runtime ? PORTABLE_RUNTIME : 0) | (arg_force ? PORTABLE_FORCE_ATTACH | PORTABLE_FORCE_EXTENSION : 0); + + r = sd_bus_message_append(m, "t", flags); + } + 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, "%s failed: %s", method, bus_error_message(&error, r)); + + (void) maybe_reload(&bus); + + print_changes(reply); + return 0; +} + +static int list_images(int argc, char *argv[], 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_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(table_unrefp) Table *table = NULL; + int r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + r = bus_call_method(bus, bus_portable_mgr, "ListImages", &error, &reply, NULL); + if (r < 0) + return log_error_errno(r, "Failed to list images: %s", bus_error_message(&error, r)); + + table = table_new("name", "type", "ro", "crtime", "mtime", "usage", "state"); + if (!table) + return log_oom(); + + r = sd_bus_message_enter_container(reply, 'a', "(ssbtttso)"); + if (r < 0) + return bus_log_parse_error(r); + + for (;;) { + const char *name, *type, *state; + uint64_t crtime, mtime, usage; + int ro_int; + + r = sd_bus_message_read(reply, "(ssbtttso)", &name, &type, &ro_int, &crtime, &mtime, &usage, &state, NULL); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + 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_TIMESTAMP, crtime, + TABLE_TIMESTAMP, mtime, + TABLE_SIZE, usage, + TABLE_STRING, state, + TABLE_SET_COLOR, !streq(state, "detached") ? ansi_highlight_green() : NULL); + 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); + + if (table_get_rows(table) > 1) { + r = table_set_sort(table, (size_t) 0); + if (r < 0) + return table_log_sort_error(r); + + table_set_header(table, arg_legend); + + r = table_print(table, NULL); + if (r < 0) + return table_log_print_error(r); + } + + if (arg_legend) { + if (table_get_rows(table) > 1) + printf("\n%zu images listed.\n", table_get_rows(table) - 1); + else + printf("No images.\n"); + } + + return 0; +} + +static int remove_image(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, i; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + for (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_portable_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 read_only_image(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int b = true, r; + + if (argc > 2) { + b = parse_boolean(argv[2]); + if (b < 0) + return log_error_errno(b, "Failed to parse boolean argument: %s", argv[2]); + } + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + r = bus_call_method(bus, bus_portable_mgr, "MarkImageReadOnly", &error, NULL, "sb", argv[1], b); + if (r < 0) + return log_error_errno(r, "Could not mark image read-only: %s", bus_error_message(&error, r)); + + return 0; +} + +static int set_limit(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + uint64_t limit; + int r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + if (STR_IN_SET(argv[argc-1], "-", "none", "infinity")) + limit = UINT64_MAX; + else { + r = parse_size(argv[argc-1], 1024, &limit); + if (r < 0) + return log_error_errno(r, "Failed to parse size: %s", argv[argc-1]); + } + + if (argc > 2) + /* With two arguments changes the quota limit of the specified image */ + r = bus_call_method(bus, bus_portable_mgr, "SetImageLimit", &error, NULL, "st", argv[1], limit); + else + /* With one argument changes the pool quota limit */ + r = bus_call_method(bus, bus_portable_mgr, "SetPoolLimit", &error, NULL, "t", limit); + + if (r < 0) + return log_error_errno(r, "Could not set limit: %s", bus_error_message(&error, r)); + + return 0; +} + +static int is_image_attached(int argc, char *argv[], 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; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_free_ char *image = NULL; + const char *state, *method; + int r; + + r = determine_image(argv[1], true, &image); + if (r < 0) + return r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + method = strv_isempty(arg_extension_images) ? "GetImageState" : "GetImageStateWithExtensions"; + + r = bus_message_new_method_call(bus, &m, bus_portable_mgr, method); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", image); + if (r < 0) + return bus_log_create_error(r); + + r = attach_extensions_to_message(m, method, arg_extension_images); + if (r < 0) + return r; + + if (!strv_isempty(arg_extension_images)) { + r = sd_bus_message_append(m, "t", UINT64_C(0)); + 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, "%s failed: %s", method, bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "s", &state); + if (r < 0) + return r; + + if (!arg_quiet) + puts(state); + + return streq(state, "detached"); +} + +static int dump_profiles(void) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_strv_free_ char **l = NULL; + int r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + r = bus_get_property_strv(bus, bus_portable_mgr, "Profiles", &error, &l); + if (r < 0) + return log_error_errno(r, "Failed to acquire list of profiles: %s", bus_error_message(&error, r)); + + if (arg_legend) + log_info("Available unit profiles:"); + + STRV_FOREACH(i, l) { + fputs(*i, stdout); + fputc('\n', stdout); + } + + return 0; +} + +static int help(int argc, char *argv[], void *userdata) { + _cleanup_free_ char *link = NULL; + int r; + + pager_open(arg_pager_flags); + + r = terminal_urlify_man("portablectl", "1", &link); + if (r < 0) + return log_oom(); + + printf("%s [OPTIONS...] COMMAND ...\n\n" + "%sAttach or detach portable services from the local system.%s\n" + "\nCommands:\n" + " list List available portable service images\n" + " attach NAME|PATH [PREFIX...]\n" + " Attach the specified portable service image\n" + " detach NAME|PATH [PREFIX...]\n" + " Detach the specified portable service image\n" + " reattach NAME|PATH [PREFIX...]\n" + " Reattach the specified portable service image\n" + " inspect NAME|PATH [PREFIX...]\n" + " Show details of specified portable service image\n" + " is-attached NAME|PATH Query if portable service image is attached\n" + " read-only NAME|PATH [BOOL] Mark or unmark portable service image read-only\n" + " remove NAME|PATH... Remove a portable service image\n" + " set-limit [NAME|PATH] Set image or pool size limit (disk quota)\n" + "\nOptions:\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" + " -q --quiet Suppress informational messages\n" + " -p --profile=PROFILE Pick security profile for portable service\n" + " --copy=copy|auto|symlink Prefer copying or symlinks if possible\n" + " --runtime Attach portable service until next reboot only\n" + " --no-reload Don't reload the system and service manager\n" + " --cat When inspecting include unit and os-release file\n" + " contents\n" + " --enable Immediately enable/disable the portable service\n" + " after attach/detach\n" + " --now Immediately start/stop the portable service after\n" + " attach/before detach\n" + " --no-block Don't block waiting for attach --now to complete\n" + " --extension=PATH Extend the image with an overlay\n" + " --force Skip 'already active' check when attaching or\n" + " detaching an image (with extensions)\n" + "\nSee the %s for details.\n", + program_invocation_short_name, + ansi_highlight(), + ansi_normal(), + link); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + int r; + + enum { + ARG_VERSION = 0x100, + ARG_NO_PAGER, + ARG_NO_LEGEND, + ARG_NO_ASK_PASSWORD, + ARG_COPY, + ARG_RUNTIME, + ARG_NO_RELOAD, + ARG_CAT, + ARG_ENABLE, + ARG_NOW, + ARG_NO_BLOCK, + ARG_EXTENSION, + ARG_FORCE, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, + { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD }, + { "host", required_argument, NULL, 'H' }, + { "machine", required_argument, NULL, 'M' }, + { "quiet", no_argument, NULL, 'q' }, + { "profile", required_argument, NULL, 'p' }, + { "copy", required_argument, NULL, ARG_COPY }, + { "runtime", no_argument, NULL, ARG_RUNTIME }, + { "no-reload", no_argument, NULL, ARG_NO_RELOAD }, + { "cat", no_argument, NULL, ARG_CAT }, + { "enable", no_argument, NULL, ARG_ENABLE }, + { "now", no_argument, NULL, ARG_NOW }, + { "no-block", no_argument, NULL, ARG_NO_BLOCK }, + { "extension", required_argument, NULL, ARG_EXTENSION }, + { "force", no_argument, NULL, ARG_FORCE }, + {} + }; + + assert(argc >= 0); + assert(argv); + + for (;;) { + int c; + + c = getopt_long(argc, argv, "hH:M:qp:", options, NULL); + if (c < 0) + break; + + switch (c) { + + case 'h': + return help(0, NULL, NULL); + + case ARG_VERSION: + return version(); + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_NO_LEGEND: + arg_legend = false; + 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 'q': + arg_quiet = true; + break; + + case 'p': + if (streq(optarg, "help")) + return dump_profiles(); + + if (!filename_is_valid(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unit profile name not valid: %s", optarg); + + arg_profile = optarg; + break; + + case ARG_COPY: + if (streq(optarg, "auto")) + arg_copy_mode = NULL; + else if (STR_IN_SET(optarg, "copy", "symlink")) + arg_copy_mode = optarg; + else if (streq(optarg, "help")) { + puts("auto\n" + "copy\n" + "symlink"); + return 0; + } else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Failed to parse --copy= argument: %s", optarg); + + break; + + case ARG_RUNTIME: + arg_runtime = true; + break; + + case ARG_NO_RELOAD: + arg_reload = false; + break; + + case ARG_CAT: + arg_cat = true; + break; + + case ARG_ENABLE: + arg_enable = true; + break; + + case ARG_NOW: + arg_now = true; + break; + + case ARG_NO_BLOCK: + arg_no_block = true; + break; + + case ARG_EXTENSION: + r = strv_extend(&arg_extension_images, optarg); + if (r < 0) + return log_oom(); + break; + + case ARG_FORCE: + arg_force = true; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + } + + return 1; +} + +static int run(int argc, char *argv[]) { + static const Verb verbs[] = { + { "help", VERB_ANY, VERB_ANY, 0, help }, + { "list", VERB_ANY, 1, VERB_DEFAULT, list_images }, + { "attach", 2, VERB_ANY, 0, attach_image }, + { "detach", 2, VERB_ANY, 0, detach_image }, + { "inspect", 2, VERB_ANY, 0, inspect_image }, + { "is-attached", 2, 2, 0, is_image_attached }, + { "read-only", 2, 3, 0, read_only_image }, + { "remove", 2, VERB_ANY, 0, remove_image }, + { "set-limit", 3, 3, 0, set_limit }, + { "reattach", 2, VERB_ANY, 0, reattach_image }, + {} + }; + + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +DEFINE_MAIN_FUNCTION(run); |