]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
machine: add support for importing containers from plain directories
authorLennart Poettering <lennart@poettering.net>
Wed, 10 Oct 2018 19:20:08 +0000 (21:20 +0200)
committerLennart Poettering <lennart@poettering.net>
Mon, 26 Nov 2018 17:09:01 +0000 (18:09 +0100)
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
meson.build
src/basic/rm-rf.h
src/import/import-fs.c [new file with mode: 0644]
src/import/importd.c
src/import/meson.build
src/import/org.freedesktop.import1.conf
src/machine/machinectl.c

index fc61613fb61c4f3de37804d033279487dcccccdd..e403c51e28f0a2f4dcf59c78e613b8b0cddc2339 100644 (file)
         <command>cancel-transfer</command>.</para></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><command>import-fs</command> <replaceable>DIRECTORY</replaceable> [<replaceable>NAME</replaceable>]</term>
+
+        <listitem><para>Imports a container image stored in a local directory into
+        <filename>/var/lib/machines/</filename>, operates similar to <command>import-tar</command> or
+        <command>import-raw</command>, but the first argument is the source directory. If supported, this command will
+        create btrfs snapshot or subvolume for the new image.</para></listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><command>export-tar</command> <replaceable>NAME</replaceable> [<replaceable>FILE</replaceable>]</term>
         <term><command>export-raw</command> <replaceable>NAME</replaceable> [<replaceable>FILE</replaceable>]</term>
index 5dc25d03dc7a21407866cee4ae22701d43cfd5bd..1f2595573649370c37b2bd7e7bbd91e30d5c1490 100644 (file)
@@ -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
index 0a2d7f0358ea78cfba3d2bc1f953664bdb0097e6..3ee2b97e3768ba967bd5fd1388ffbc33558bf2c8 100644 (file)
@@ -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 (file)
index 0000000..3836f87
--- /dev/null
@@ -0,0 +1,323 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <getopt.h>
+
+#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;
+}
index 260705bcad737d95dba43c57cbd61bec099e71e5..6fa8342172aa68496c180be9af29d8b8287f7803 100644 (file)
@@ -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))
+                switch (t->type) {
+
+                case TRANSFER_IMPORT_TAR:
+                case TRANSFER_IMPORT_RAW:
                         cmd[k++] = SYSTEMD_IMPORT_PATH;
-                else if (IN_SET(t->type, TRANSFER_EXPORT_TAR, TRANSFER_EXPORT_RAW))
+                        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;
-                else
+                        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) {
 
-                if (IN_SET(t->type, TRANSFER_IMPORT_TAR, TRANSFER_EXPORT_TAR, TRANSFER_PULL_TAR))
+                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),
index 283ba08c676b0672efc05288fa8f50a12f7e5537..1c15fd883fec6761499a6280cbb67c9db6e57c9a 100644 (file)
@@ -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
index 74c21f6410681c155468faa1c9ab1eab6c6c9fb6..2fdb2ba77c5fd0197abb2b0d894823751c68315b 100644 (file)
                        send_interface="org.freedesktop.import1.Manager"
                        send_member="ImportRaw"/>
 
+                <allow send_destination="org.freedesktop.import1"
+                       send_interface="org.freedesktop.import1.Manager"
+                       send_member="ImportFileSystem"/>
+
                 <allow send_destination="org.freedesktop.import1"
                        send_interface="org.freedesktop.import1.Manager"
                        send_member="ExportTar"/>
index 094ee9d360e508f7133241e69ec3b69dd7c689de..158cf73c283d46e492b20f3b2a2fbb0e98dabe1e 100644 (file)
@@ -5,6 +5,7 @@
 #include <fcntl.h>
 #include <getopt.h>
 #include <locale.h>
+#include <math.h>
 #include <net/if.h>
 #include <netinet/in.h>
 #include <string.h>
@@ -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          },