From 1d7579c473dd665df213fbc8a30f032994546246 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 10 Oct 2018 21:20:08 +0200 Subject: [PATCH] machine: add support for importing containers from plain directories Fixes: #2728 This is also supposed to be preparation for doing #10234 eventually, where a very similar operation is requested: instead of importing a tree to /var/lib/machines it would need to be imported into /var/lib/portables/. --- man/machinectl.xml | 9 + meson.build | 12 +- src/basic/rm-rf.h | 8 + src/import/import-fs.c | 323 ++++++++++++++++++++++++ src/import/importd.c | 133 +++++++++- src/import/meson.build | 6 + src/import/org.freedesktop.import1.conf | 4 + src/machine/machinectl.c | 84 +++++- 8 files changed, 561 insertions(+), 18 deletions(-) create mode 100644 src/import/import-fs.c diff --git a/man/machinectl.xml b/man/machinectl.xml index fc61613fb6..e403c51e28 100644 --- a/man/machinectl.xml +++ b/man/machinectl.xml @@ -815,6 +815,15 @@ cancel-transfer. + + import-fs DIRECTORY [NAME] + + Imports a container image stored in a local directory into + /var/lib/machines/, operates similar to import-tar or + import-raw, but the first argument is the source directory. If supported, this command will + create btrfs snapshot or subvolume for the new image. + + export-tar NAME [FILE] export-raw NAME [FILE] diff --git a/meson.build b/meson.build index 5dc25d03dc..1f25955736 100644 --- a/meson.build +++ b/meson.build @@ -227,6 +227,7 @@ conf.set_quoted('ROOTLIBEXECDIR', rootlibexecdir) conf.set_quoted('BOOTLIBDIR', bootlibdir) conf.set_quoted('SYSTEMD_PULL_PATH', join_paths(rootlibexecdir, 'systemd-pull')) conf.set_quoted('SYSTEMD_IMPORT_PATH', join_paths(rootlibexecdir, 'systemd-import')) +conf.set_quoted('SYSTEMD_IMPORT_FS_PATH', join_paths(rootlibexecdir, 'systemd-import-fs')) conf.set_quoted('SYSTEMD_EXPORT_PATH', join_paths(rootlibexecdir, 'systemd-export')) conf.set_quoted('VENDOR_KEYRING_PATH', join_paths(rootlibexecdir, 'import-pubring.gpg')) conf.set_quoted('USER_KEYRING_PATH', join_paths(pkgsysconfdir, 'import-pubring.gpg')) @@ -2126,6 +2127,14 @@ if conf.get('ENABLE_IMPORTD') == 1 install : true, install_dir : rootlibexecdir) + systemd_import_fs = executable('systemd-import-fs', + systemd_import_fs_sources, + include_directories : includes, + link_with : [libshared], + install_rpath : rootlibexecdir, + install : true, + install_dir : rootlibexecdir) + systemd_export = executable('systemd-export', systemd_export_sources, include_directories : includes, @@ -2137,7 +2146,8 @@ if conf.get('ENABLE_IMPORTD') == 1 install_rpath : rootlibexecdir, install : true, install_dir : rootlibexecdir) - public_programs += [systemd_pull, systemd_import, systemd_export] + + public_programs += [systemd_pull, systemd_import, systemd_import_fs, systemd_export] endif if conf.get('ENABLE_REMOTE') == 1 and conf.get('HAVE_LIBCURL') == 1 diff --git a/src/basic/rm-rf.h b/src/basic/rm-rf.h index 0a2d7f0358..3ee2b97e37 100644 --- a/src/basic/rm-rf.h +++ b/src/basic/rm-rf.h @@ -22,3 +22,11 @@ static inline void rm_rf_physical_and_free(char *p) { free(p); } DEFINE_TRIVIAL_CLEANUP_FUNC(char*, rm_rf_physical_and_free); + +/* Similar as above, but also has magic btrfs subvolume powers */ +static inline void rm_rf_subvolume_and_free(char *p) { + PROTECT_ERRNO; + (void) rm_rf(p, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME); + free(p); +} +DEFINE_TRIVIAL_CLEANUP_FUNC(char*, rm_rf_subvolume_and_free); diff --git a/src/import/import-fs.c b/src/import/import-fs.c new file mode 100644 index 0000000000..3836f87570 --- /dev/null +++ b/src/import/import-fs.c @@ -0,0 +1,323 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "alloc-util.h" +#include "btrfs-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "fs-util.h" +#include "hostname-util.h" +#include "import-common.h" +#include "import-util.h" +#include "machine-image.h" +#include "mkdir.h" +#include "ratelimit.h" +#include "rm-rf.h" +#include "string-util.h" +#include "verbs.h" +#include "parse-util.h" + +static bool arg_force = false; +static bool arg_read_only = false; +static const char *arg_image_root = "/var/lib/machines"; + +typedef struct ProgressInfo { + RateLimit limit; + char *path; + uint64_t size; + bool started; + bool logged_incomplete; +} ProgressInfo; + +static volatile sig_atomic_t cancelled = false; + +static void sigterm_sigint(int sig) { + cancelled = true; +} + +static void progress_info_free(ProgressInfo *p) { + free(p->path); +} + +static void progress_show(ProgressInfo *p) { + assert(p); + + /* Show progress only every now and then. */ + if (!ratelimit_below(&p->limit)) + return; + + /* Suppress the first message, start with the second one */ + if (!p->started) { + p->started = true; + return; + } + + /* Mention the list is incomplete before showing first output. */ + if (!p->logged_incomplete) { + log_notice("(Note, file list shown below is incomplete, and is intended as sporadic progress report only.)"); + p->logged_incomplete = true; + } + + if (p->size == 0) + log_info("Copying tree, currently at '%s'...", p->path); + else { + char buffer[FORMAT_BYTES_MAX]; + + log_info("Copying tree, currently at '%s' (@%s)...", p->path, format_bytes(buffer, sizeof(buffer), p->size)); + } +} + +static int progress_path(const char *path, const struct stat *st, void *userdata) { + ProgressInfo *p = userdata; + int r; + + assert(p); + + if (cancelled) + return -EOWNERDEAD; + + r = free_and_strdup(&p->path, path); + if (r < 0) + return r; + + p->size = 0; + + progress_show(p); + return 0; +} + +static int progress_bytes(uint64_t nbytes, void *userdata) { + ProgressInfo *p = userdata; + + assert(p); + assert(p->size != UINT64_MAX); + + if (cancelled) + return -EOWNERDEAD; + + p->size += nbytes; + + progress_show(p); + return 0; +} + +static int import_fs(int argc, char *argv[], void *userdata) { + _cleanup_(rm_rf_subvolume_and_freep) char *temp_path = NULL; + _cleanup_(progress_info_free) ProgressInfo progress = {}; + const char *path = NULL, *local = NULL, *final_path; + _cleanup_close_ int open_fd = -1; + struct sigaction old_sigint_sa, old_sigterm_sa; + static const struct sigaction sa = { + .sa_handler = sigterm_sigint, + .sa_flags = SA_RESTART, + }; + int r, fd; + + if (argc >= 2) + path = argv[1]; + if (isempty(path) || streq(path, "-")) + path = NULL; + + if (argc >= 3) + local = argv[2]; + else if (path) + local = basename(path); + if (isempty(local) || streq(local, "-")) + local = NULL; + + if (local) { + if (!machine_name_is_valid(local)) { + log_error("Local image name '%s' is not valid.", local); + return -EINVAL; + } + + if (!arg_force) { + r = image_find(IMAGE_MACHINE, local, NULL); + if (r < 0) { + if (r != -ENOENT) + return log_error_errno(r, "Failed to check whether image '%s' exists: %m", local); + } else { + log_error("Image '%s' already exists.", local); + return -EEXIST; + } + } + } else + local = "imported"; + + if (path) { + open_fd = open(path, O_DIRECTORY|O_RDONLY|O_CLOEXEC); + if (open_fd < 0) + return log_error_errno(errno, "Failed to open directory to import: %m"); + + fd = open_fd; + + log_info("Importing '%s', saving as '%s'.", path, local); + } else { + _cleanup_free_ char *pretty = NULL; + + fd = STDIN_FILENO; + + (void) fd_get_path(fd, &pretty); + log_info("Importing '%s', saving as '%s'.", strempty(pretty), local); + } + + final_path = strjoina(arg_image_root, "/", local); + + r = tempfn_random(final_path, NULL, &temp_path); + if (r < 0) + return log_oom(); + + (void) mkdir_parents_label(temp_path, 0700); + + RATELIMIT_INIT(progress.limit, 200*USEC_PER_MSEC, 1); + + /* Hook into SIGINT/SIGTERM, so that we can cancel things then */ + assert(sigaction(SIGINT, &sa, &old_sigint_sa) >= 0); + assert(sigaction(SIGTERM, &sa, &old_sigterm_sa) >= 0); + + r = btrfs_subvol_snapshot_fd_full( + fd, + temp_path, + BTRFS_SNAPSHOT_FALLBACK_COPY|BTRFS_SNAPSHOT_RECURSIVE|BTRFS_SNAPSHOT_FALLBACK_DIRECTORY|BTRFS_SNAPSHOT_QUOTA, + progress_path, + progress_bytes, + &progress); + if (r == -EOWNERDEAD) { /* SIGINT + SIGTERM cause this, see signal handler above */ + log_error("Copy cancelled."); + goto finish; + } + if (r < 0) { + log_error_errno(r, "Failed to copy directory: %m"); + goto finish; + } + + (void) import_assign_pool_quota_and_warn(temp_path); + + if (arg_read_only) { + r = import_make_read_only(temp_path); + if (r < 0) { + log_error_errno(r, "Failed to make directory read-only: %m"); + goto finish; + } + } + + if (arg_force) + (void) rm_rf(final_path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME); + + r = rename_noreplace(AT_FDCWD, temp_path, AT_FDCWD, final_path); + if (r < 0) { + log_error_errno(r, "Failed to move image into place: %m"); + goto finish; + } + + temp_path = mfree(temp_path); + + log_info("Exiting."); + +finish: + /* Put old signal handlers into place */ + assert(sigaction(SIGINT, &old_sigint_sa, NULL) >= 0); + assert(sigaction(SIGTERM, &old_sigterm_sa, NULL) >= 0); + + return 0; +} + +static int help(int argc, char *argv[], void *userdata) { + + printf("%s [OPTIONS...] {COMMAND} ...\n\n" + "Import container images from a file system.\n\n" + " -h --help Show this help\n" + " --version Show package version\n" + " --force Force creation of image\n" + " --image-root=PATH Image root directory\n" + " --read-only Create a read-only image\n\n" + "Commands:\n" + " run DIRECTORY [NAME] Import a directory\n", + program_invocation_short_name); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_VERSION = 0x100, + ARG_FORCE, + ARG_IMAGE_ROOT, + ARG_READ_ONLY, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "force", no_argument, NULL, ARG_FORCE }, + { "image-root", required_argument, NULL, ARG_IMAGE_ROOT }, + { "read-only", no_argument, NULL, ARG_READ_ONLY }, + {} + }; + + int c; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + + switch (c) { + + case 'h': + return help(0, NULL, NULL); + + case ARG_VERSION: + return version(); + + case ARG_FORCE: + arg_force = true; + break; + + case ARG_IMAGE_ROOT: + arg_image_root = optarg; + break; + + case ARG_READ_ONLY: + arg_read_only = true; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached("Unhandled option"); + } + + return 1; +} + +static int import_fs_main(int argc, char *argv[]) { + + static const Verb verbs[] = { + { "help", VERB_ANY, VERB_ANY, 0, help }, + { "run", 2, 3, 0, import_fs }, + {} + }; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +int main(int argc, char *argv[]) { + int r; + + setlocale(LC_ALL, ""); + log_parse_environment(); + log_open(); + + r = parse_argv(argc, argv); + if (r <= 0) + goto finish; + + r = import_fs_main(argc, argv); + +finish: + return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/src/import/importd.c b/src/import/importd.c index 260705bcad..6fa8342172 100644 --- a/src/import/importd.c +++ b/src/import/importd.c @@ -10,6 +10,7 @@ #include "bus-util.h" #include "def.h" #include "fd-util.h" +#include "float.h" #include "hostname-util.h" #include "import-util.h" #include "machine-pool.h" @@ -34,6 +35,7 @@ typedef struct Manager Manager; typedef enum TransferType { TRANSFER_IMPORT_TAR, TRANSFER_IMPORT_RAW, + TRANSFER_IMPORT_FS, TRANSFER_EXPORT_TAR, TRANSFER_EXPORT_RAW, TRANSFER_PULL_TAR, @@ -94,6 +96,7 @@ struct Manager { static const char* const transfer_type_table[_TRANSFER_TYPE_MAX] = { [TRANSFER_IMPORT_TAR] = "import-tar", [TRANSFER_IMPORT_RAW] = "import-raw", + [TRANSFER_IMPORT_FS] = "import-fs", [TRANSFER_EXPORT_TAR] = "export-tar", [TRANSFER_EXPORT_RAW] = "export-raw", [TRANSFER_PULL_TAR] = "pull-tar", @@ -156,6 +159,7 @@ static int transfer_new(Manager *m, Transfer **ret) { .stdin_fd = -1, .stdout_fd = -1, .verify = _IMPORT_VERIFY_INVALID, + .progress_percent= (unsigned) -1, }; id = m->current_transfer_id + 1; @@ -177,6 +181,15 @@ static int transfer_new(Manager *m, Transfer **ret) { return 0; } +static double transfer_percent_as_double(Transfer *t) { + assert(t); + + if (t->progress_percent == (unsigned) -1) + return -DBL_MAX; + + return (double) t->progress_percent / 100.0; +} + static void transfer_send_log_line(Transfer *t, const char *line) { int r, priority = LOG_INFO; @@ -357,7 +370,7 @@ static int transfer_start(Transfer *t) { return r; if (r == 0) { const char *cmd[] = { - NULL, /* systemd-import, systemd-export or systemd-pull */ + NULL, /* systemd-import, systemd-import-fs, systemd-export or systemd-pull */ NULL, /* tar, raw */ NULL, /* --verify= */ NULL, /* verify argument */ @@ -390,17 +403,52 @@ static int transfer_start(Transfer *t) { _exit(EXIT_FAILURE); } - if (IN_SET(t->type, TRANSFER_IMPORT_TAR, TRANSFER_IMPORT_RAW)) - cmd[k++] = SYSTEMD_IMPORT_PATH; - else if (IN_SET(t->type, TRANSFER_EXPORT_TAR, TRANSFER_EXPORT_RAW)) - cmd[k++] = SYSTEMD_EXPORT_PATH; - else - cmd[k++] = SYSTEMD_PULL_PATH; + switch (t->type) { - if (IN_SET(t->type, TRANSFER_IMPORT_TAR, TRANSFER_EXPORT_TAR, TRANSFER_PULL_TAR)) + case TRANSFER_IMPORT_TAR: + case TRANSFER_IMPORT_RAW: + cmd[k++] = SYSTEMD_IMPORT_PATH; + break; + + case TRANSFER_IMPORT_FS: + cmd[k++] = SYSTEMD_IMPORT_FS_PATH; + break; + + case TRANSFER_EXPORT_TAR: + case TRANSFER_EXPORT_RAW: + cmd[k++] = SYSTEMD_EXPORT_PATH; + break; + + case TRANSFER_PULL_TAR: + case TRANSFER_PULL_RAW: + cmd[k++] = SYSTEMD_PULL_PATH; + break; + + default: + assert_not_reached("Unexpected transfer type"); + } + + switch (t->type) { + + case TRANSFER_IMPORT_TAR: + case TRANSFER_EXPORT_TAR: + case TRANSFER_PULL_TAR: cmd[k++] = "tar"; - else + break; + + case TRANSFER_IMPORT_RAW: + case TRANSFER_EXPORT_RAW: + case TRANSFER_PULL_RAW: cmd[k++] = "raw"; + break; + + case TRANSFER_IMPORT_FS: + cmd[k++] = "run"; + break; + + default: + break; + } if (t->verify != _IMPORT_VERIFY_INVALID) { cmd[k++] = "--verify"; @@ -704,6 +752,68 @@ static int method_import_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_ return sd_bus_reply_method_return(msg, "uo", id, object); } +static int method_import_fs(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + _cleanup_(transfer_unrefp) Transfer *t = NULL; + int fd, force, read_only, r; + const char *local, *object; + Manager *m = userdata; + uint32_t id; + + assert(msg); + assert(m); + + r = bus_verify_polkit_async( + msg, + CAP_SYS_ADMIN, + "org.freedesktop.import1.import", + NULL, + false, + UID_INVALID, + &m->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = sd_bus_message_read(msg, "hsbb", &fd, &local, &force, &read_only); + if (r < 0) + return r; + + if (!machine_name_is_valid(local)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Local name %s is invalid", local); + + r = setup_machine_directory((uint64_t) -1, error); + if (r < 0) + return r; + + r = transfer_new(m, &t); + if (r < 0) + return r; + + t->type = TRANSFER_IMPORT_FS; + t->force_local = force; + t->read_only = read_only; + + t->local = strdup(local); + if (!t->local) + return -ENOMEM; + + t->stdin_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3); + if (t->stdin_fd < 0) + return -errno; + + r = transfer_start(t); + if (r < 0) + return r; + + object = t->object_path; + id = t->id; + t = NULL; + + return sd_bus_reply_method_return(msg, "uo", id, object); +} + static int method_export_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_error *error) { _cleanup_(transfer_unrefp) Transfer *t = NULL; int fd, r; @@ -879,7 +989,7 @@ static int method_list_transfers(sd_bus_message *msg, void *userdata, sd_bus_err transfer_type_to_string(t->type), t->remote, t->local, - (double) t->progress_percent / 100.0, + transfer_percent_as_double(t), t->object_path); if (r < 0) return r; @@ -975,7 +1085,7 @@ static int property_get_progress( assert(reply); assert(t); - return sd_bus_message_append(reply, "d", (double) t->progress_percent / 100.0); + return sd_bus_message_append(reply, "d", transfer_percent_as_double(t)); } static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_type, transfer_type, TransferType); @@ -998,6 +1108,7 @@ static const sd_bus_vtable manager_vtable[] = { SD_BUS_VTABLE_START(0), SD_BUS_METHOD("ImportTar", "hsbb", "uo", method_import_tar_or_raw, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("ImportRaw", "hsbb", "uo", method_import_tar_or_raw, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("ImportFileSystem", "hsbb", "uo", method_import_fs, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("ExportTar", "shs", "uo", method_export_tar_or_raw, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("ExportRaw", "shs", "uo", method_export_tar_or_raw, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("PullTar", "sssb", "uo", method_pull_tar_or_raw, SD_BUS_VTABLE_UNPRIVILEGED), diff --git a/src/import/meson.build b/src/import/meson.build index 283ba08c67..1c15fd883f 100644 --- a/src/import/meson.build +++ b/src/import/meson.build @@ -38,6 +38,12 @@ systemd_import_sources = files(''' qcow2-util.h '''.split()) +systemd_import_fs_sources = files(''' + import-fs.c + import-common.c + import-common.h +'''.split()) + systemd_export_sources = files(''' export.c export-tar.c diff --git a/src/import/org.freedesktop.import1.conf b/src/import/org.freedesktop.import1.conf index 74c21f6410..2fdb2ba77c 100644 --- a/src/import/org.freedesktop.import1.conf +++ b/src/import/org.freedesktop.import1.conf @@ -54,6 +54,10 @@ send_interface="org.freedesktop.import1.Manager" send_member="ImportRaw"/> + + diff --git a/src/machine/machinectl.c b/src/machine/machinectl.c index 094ee9d360..158cf73c28 100644 --- a/src/machine/machinectl.c +++ b/src/machine/machinectl.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -2133,6 +2134,66 @@ static int import_raw(int argc, char *argv[], void *userdata) { return transfer_image_common(bus, m); } +static int import_fs(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_close_ int fd = -1; + const char *local = NULL, *path = NULL; + sd_bus *bus = userdata; + int r; + + assert(bus); + + if (argc >= 2) + path = argv[1]; + if (isempty(path) || streq(path, "-")) + path = NULL; + + if (argc >= 3) + local = argv[2]; + else if (path) + local = basename(path); + if (isempty(local) || streq(local, "-")) + local = NULL; + + if (!local) { + log_error("Need either path or local name."); + return -EINVAL; + } + + if (!machine_name_is_valid(local)) { + log_error("Local name %s is not a suitable machine name.", local); + return -EINVAL; + } + + if (path) { + fd = open(path, O_DIRECTORY|O_RDONLY|O_CLOEXEC); + if (fd < 0) + return log_error_errno(errno, "Failed to open directory '%s': %m", path); + } + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.import1", + "/org/freedesktop/import1", + "org.freedesktop.import1.Manager", + "ImportFileSystem"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append( + m, + "hsbb", + fd >= 0 ? fd : STDIN_FILENO, + local, + arg_force, + arg_read_only); + if (r < 0) + return bus_log_create_error(r); + + return transfer_image_common(bus, m); +} + static void determine_compression_from_filename(const char *p) { if (arg_format) return; @@ -2464,12 +2525,21 @@ static int list_transfers(int argc, char *argv[], void *userdata) { (int) max_remote, "REMOTE"); for (j = 0; j < n_transfers; j++) - printf("%*" PRIu32 " %*u%% %-*s %-*s %-*s\n", - (int) MAX(2U, DECIMAL_STR_WIDTH(max_id)), transfers[j].id, - (int) 6, (unsigned) (transfers[j].progress * 100), - (int) max_type, transfers[j].type, - (int) max_local, transfers[j].local, - (int) max_remote, transfers[j].remote); + + if (transfers[j].progress < 0) + printf("%*" PRIu32 " %*s %-*s %-*s %-*s\n", + (int) MAX(2U, DECIMAL_STR_WIDTH(max_id)), transfers[j].id, + (int) 7, "n/a", + (int) max_type, transfers[j].type, + (int) max_local, transfers[j].local, + (int) max_remote, transfers[j].remote); + else + printf("%*" PRIu32 " %*u%% %-*s %-*s %-*s\n", + (int) MAX(2U, DECIMAL_STR_WIDTH(max_id)), transfers[j].id, + (int) 6, (unsigned) (transfers[j].progress * 100), + (int) max_type, transfers[j].type, + (int) max_local, transfers[j].local, + (int) max_remote, transfers[j].remote); if (arg_legend) { if (n_transfers > 0) @@ -2687,6 +2757,7 @@ static int help(int argc, char *argv[], void *userdata) { " pull-raw URL [NAME] Download a RAW container or VM image\n" " import-tar FILE [NAME] Import a local TAR container image\n" " import-raw FILE [NAME] Import a local RAW container or VM image\n" + " import-fs DIRECTORY [NAME] Import a local directory container image\n" " export-tar NAME [FILE] Export a TAR container image locally\n" " export-raw NAME [FILE] Export a RAW container or VM image locally\n" " list-transfers Show list of downloads in progress\n" @@ -3008,6 +3079,7 @@ static int machinectl_main(int argc, char *argv[], sd_bus *bus) { { "disable", 2, VERB_ANY, 0, enable_machine }, { "import-tar", 2, 3, 0, import_tar }, { "import-raw", 2, 3, 0, import_raw }, + { "import-fs", 2, 3, 0, import_fs }, { "export-tar", 2, 3, 0, export_tar }, { "export-raw", 2, 3, 0, export_raw }, { "pull-tar", 2, 3, 0, pull_tar },