diff --git a/man/rules/meson.build b/man/rules/meson.build
index 9b8a29c564..fda14d55bd 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -953,6 +953,7 @@ manpages = [
['systemd-hostnamed.service', '8', ['systemd-hostnamed'], 'ENABLE_HOSTNAMED'],
['systemd-hwdb', '8', [], 'ENABLE_HWDB'],
['systemd-id128', '1', [], ''],
+ ['systemd-import-generator', '8', [], ''],
['systemd-importd.service', '8', ['systemd-importd'], 'ENABLE_IMPORTD'],
['systemd-inhibit', '1', [], ''],
['systemd-initctl.service',
diff --git a/man/systemd-import-generator.xml b/man/systemd-import-generator.xml
new file mode 100644
index 0000000000..108509d7d4
--- /dev/null
+++ b/man/systemd-import-generator.xml
@@ -0,0 +1,194 @@
+
+
+
+%entities;
+]>
+
+
+
+
+ systemd-import-generator
+ systemd
+
+
+
+ systemd-import-generator
+ 8
+
+
+
+ systemd-import-generator
+ Generator for automatically downloading disk images at boot
+
+
+
+ /usr/lib/systemd/system-generators/systemd-import-generator
+
+
+
+ Description
+
+ systemd-import-generator may be used to automatically download disk images
+ (tarballs or DDIs) via
+ systemd-importd.service8
+ at boot, based on parameters on the kernel command line or via system credentials. This is useful for
+ automatically deploying an
+ systemd-confext8,
+ systemd-sysext8,
+ systemd-nspawn1/
+ systemd-vmspawn1 or
+ systemd-portabled.service8
+ image at boot. This provides functionality equivalent to
+ importctl1, but
+ accessible via the kernel command line and system credentials.
+
+ systemd-import-generator implements
+ systemd.generator7.
+
+
+
+ Kernel Command Line
+
+ systemd-import-generator understands the following
+ kernel-command-line7
+ parameters:
+
+
+
+ systemd.pull=
+
+ This option takes a colon separate triplet of option string, local target image name
+ and remote URL. The local target image name can be specified as an empty string, in which case the
+ name is derived from the specified remote URL. The remote URL must using the
+ http://, https://, file:// schemes. The
+ option string itself is a comma separated list of options:
+
+
+
+ rw
+ ro
+
+ Controls whether to mark the local image as read-only. If not
+ specified read-only defaults to off.
+
+
+
+
+
+ verify=
+
+ Controls whether to cryptographically validate the download before installing it
+ in place. Takes one of no, checksum or
+ signature (the latter being the default if not specified). For details see the
+ of
+ importctl1
+
+
+
+
+
+ sysext
+ confext
+ machine
+ portable
+
+ Controls the image class to download, and thus ultimately the target directory
+ for the image, depending on this choice the target directory
+ /var/lib/extensions/, /var/lib/confexts/,
+ /var/lib/machines/ or /var/lib/portables/ is
+ selected.
+
+ Specification of exactly one of these options is mandatory.
+
+
+
+
+
+ tar
+ raw
+
+ Controls the type of resource to download, i.e. a (possibly compressed) tarball
+ that needs to be unpacked into a file system tree, or (possibly compressed) raw disk image (DDI).
+
+ Specification of exactly one of these options is mandatory.
+
+
+
+
+
+
+
+
+
+ systemd.pull.success_action=
+ systemd.pull.failure_action=
+
+ Controls whether to execute an action such as reboot, power-off and similar after
+ completing the download successfully, or unsuccessfully. See
+ SuccessAction=/FailureAction= on
+ systemd.unit5 for
+ details about the available actions. If not specified no action is taken, and the system will
+ continue to boot normally.
+
+
+
+
+
+
+
+ Credentials
+
+ systemd-import-generator supports the system credentials logic. The following
+ credentials are used when passed in:
+
+
+
+ import.pull
+
+ This credential should be a text file, with each line referencing one download
+ operation. Each line should follow the same format as the value of the
+ systemd.pull= kernel command line option described above.
+
+
+
+
+
+
+
+ Examples
+
+
+ Download Configuration Extension
+
+ systemd.pull=raw,confext::https://example.com/myconfext.raw.gz
+
+ With a kernel command line option like the above a configuration extension DDI is downloaded
+ automatically at boot from the specified URL, validated cryptographically, uncompressed and installed.
+
+
+
+ Download System Extension (Without Validation)
+
+ systemd.pull=tar,sysext,verify=no::https://example.com/mysysext.tar.gz
+
+ With a kernel command line option like the above a system extension tarball is downloaded
+ automatically at boot from the specified URL, uncompressed and installed – without any cryptographic
+ validation. This is useful for development purposes in virtual machines and containers. Warning: do not
+ deploy a system with validation disabled like this!
+
+
+
+
+ See Also
+
+ systemd1
+ systemd-importd.service8
+ kernel-command-line7
+ systemd.system-credentials7
+ importctl1
+
+
+
diff --git a/man/systemd.system-credentials.xml b/man/systemd.system-credentials.xml
index d9fbae25ee..f8c27d04ac 100644
--- a/man/systemd.system-credentials.xml
+++ b/man/systemd.system-credentials.xml
@@ -415,6 +415,16 @@
+
+
+ import.pull
+
+ Specified disk images (tarballs and DDIs) to automatically download and install at boot. For details see
+ systemd-import-generator8.
+
+
+
+
diff --git a/src/import/import-generator.c b/src/import/import-generator.c
new file mode 100644
index 0000000000..29fa5aad32
--- /dev/null
+++ b/src/import/import-generator.c
@@ -0,0 +1,288 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-json.h"
+
+#include "creds-util.h"
+#include "discover-image.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "generator.h"
+#include "import-util.h"
+#include "json-util.h"
+#include "proc-cmdline.h"
+#include "specifier.h"
+#include "web-util.h"
+
+static const char *arg_dest = NULL;
+static char *arg_success_action = NULL;
+static char *arg_failure_action = NULL;
+static sd_json_variant **arg_transfers = NULL;
+static size_t arg_n_transfers = 0;
+
+STATIC_DESTRUCTOR_REGISTER(arg_success_action, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_failure_action, freep);
+STATIC_ARRAY_DESTRUCTOR_REGISTER(arg_transfers, arg_n_transfers, sd_json_variant_unref_many);
+
+static int parse_pull_expression(const char *v) {
+ const char *p = v;
+ int r;
+
+ assert(v);
+
+ _cleanup_free_ char *options = NULL;
+ r = extract_first_word(&p, &options, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract option string from pull expression '%s': %m", v);
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No option string in pull expression '%s': %m", v);
+
+ _cleanup_free_ char *local = NULL;
+ r = extract_first_word(&p, &local, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract local name from pull expression '%s': %m", v);
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No local string in pull expression '%s': %m", v);
+
+ if (!http_url_is_valid(p) && !file_url_is_valid(p))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid URL, refusing: %s", p);
+ _cleanup_free_ char *remote = strdup(p);
+ if (!remote)
+ return log_oom();
+
+ if (isempty(local))
+ local = mfree(local);
+ else if (!image_name_is_valid(local))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid image name, refusing: %s", local);
+
+ ImportType type = _IMPORT_TYPE_INVALID;
+ ImageClass class = _IMAGE_CLASS_INVALID;
+ ImportVerify verify = IMPORT_VERIFY_SIGNATURE;
+ bool ro = false;
+
+ const char *o = options;
+ for (;;) {
+ _cleanup_free_ char *opt = NULL;
+
+ r = extract_first_word(&o, &opt, ",", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract option from pull option expression '%s': %m", options);
+ if (r == 0)
+ break;
+
+ const char *suffix;
+
+ if (streq(opt, "ro"))
+ ro = true;
+ else if (streq(opt, "rw"))
+ ro = false;
+ else if ((suffix = startswith(opt, "verify="))) {
+
+ ImportVerify w = import_verify_from_string(suffix);
+
+ if (w < 0)
+ log_warning_errno(w, "Unknown verification mode, ignoring: %s", suffix);
+ else
+ verify = w;
+ } else {
+ ImageClass c;
+
+ c = image_class_from_string(opt);
+ if (c < 0) {
+ ImportType t;
+
+ t = import_type_from_string(opt);
+ if (t < 0)
+ log_warning_errno(c, "Unknown pull option, ignoring: %s", opt);
+ else
+ type = t;
+ } else
+ class = c;
+ }
+ }
+
+ if (type < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No image type (raw, tar) specified in pull expression, refusing: %s", v);
+ if (class < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No image class (machine, portable, sysext, confext) specified in pull expression, refusing: %s", v);
+
+ if (!GREEDY_REALLOC(arg_transfers, arg_n_transfers + 1))
+ return log_oom();
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL;
+
+ r = sd_json_buildo(
+ &j,
+ SD_JSON_BUILD_PAIR("remote", SD_JSON_BUILD_STRING(remote)),
+ SD_JSON_BUILD_PAIR_CONDITION(!!local, "local", SD_JSON_BUILD_STRING(local)),
+ SD_JSON_BUILD_PAIR("class", JSON_BUILD_STRING_UNDERSCORIFY(image_class_to_string(class))),
+ SD_JSON_BUILD_PAIR("type", JSON_BUILD_STRING_UNDERSCORIFY(import_type_to_string(type))),
+ SD_JSON_BUILD_PAIR("readOnly", SD_JSON_BUILD_BOOLEAN(ro)),
+ SD_JSON_BUILD_PAIR("verify", JSON_BUILD_STRING_UNDERSCORIFY(import_verify_to_string(verify))));
+ if (r < 0)
+ return log_error_errno(r, "Failed to build import JSON object: %m");
+
+ arg_transfers[arg_n_transfers++] = TAKE_PTR(j);
+ return 0;
+}
+
+static int parse_proc_cmdline_item(const char *key, const char *value, void *data) {
+ int r;
+
+ if (proc_cmdline_key_streq(key, "systemd.pull")) {
+
+ if (proc_cmdline_value_missing(key, value))
+ return 0;
+
+ r = parse_pull_expression(value);
+ if (r < 0)
+ log_warning_errno(r, "Failed to parse %s expression, ignoring: %s", key, value);
+
+ } else if (proc_cmdline_key_streq(key, "systemd.pull.success_action")) {
+
+ if (proc_cmdline_value_missing(key, value))
+ return 0;
+
+ return free_and_strdup_warn(&arg_success_action, value);
+
+ } else if (proc_cmdline_key_streq(key, "systemd.pull.failure_action")) {
+
+ if (proc_cmdline_value_missing(key, value))
+ return 0;
+
+ return free_and_strdup_warn(&arg_failure_action, value);
+ }
+
+ return 0;
+}
+
+static int parse_credentials(void) {
+ _cleanup_free_ char *b = NULL;
+ size_t sz = 0;
+ int r;
+
+ r = read_credential_with_decryption("import.pull", (void**) &b, &sz);
+ if (r <= 0)
+ return r;
+
+ _cleanup_fclose_ FILE *f = NULL;
+ f = fmemopen_unlocked(b, sz, "r");
+ if (!f)
+ return log_oom();
+
+ for (;;) {
+ _cleanup_free_ char *item = NULL;
+
+ r = read_stripped_line(f, LINE_MAX, &item);
+ if (r == 0)
+ break;
+ if (r < 0) {
+ log_error_errno(r, "Failed to parse credential 'ssh.listen': %m");
+ break;
+ }
+
+ if (startswith(item, "#"))
+ continue;
+
+ r = parse_pull_expression(item);
+ if (r < 0)
+ log_warning_errno(r, "Failed to parse expression, ignoring: %s", item);
+ }
+
+ return 0;
+}
+
+static int transfer_generate(sd_json_variant *v, size_t c) {
+ int r;
+
+ assert(v);
+
+ _cleanup_free_ char *service = NULL;
+ if (asprintf(&service, "import%zu.service", c) < 0)
+ return log_oom();
+
+ _cleanup_fclose_ FILE *f = NULL;
+ r = generator_open_unit_file(arg_dest, /* source = */ NULL, service, &f);
+ if (r < 0)
+ return r;
+
+ const char *remote = sd_json_variant_string(sd_json_variant_by_key(v, "remote"));
+
+ fprintf(f,
+ "[Unit]\n"
+ "Description=Download of %s\n"
+ "Documentation=man:systemd-import-generator(8)\n"
+ "SourcePath=/proc/cmdline\n"
+ "Requires=systemd-importd.socket\n"
+ "After=systemd-importd.socket\n"
+ "Conflicts=shutdown.target\n"
+ "Before=shutdown.target\n"
+ "DefaultDependencies=no\n",
+ remote);
+
+ if (arg_success_action)
+ fprintf(f, "SuccessAction=%s\n",
+ arg_success_action);
+
+ if (arg_failure_action)
+ fprintf(f, "FailureAction=%s\n",
+ arg_failure_action);
+
+ const char *class = sd_json_variant_string(sd_json_variant_by_key(v, "class"));
+ if (streq_ptr(class, "sysext"))
+ fputs("Before=systemd-sysext.service\n", f);
+ else if (streq_ptr(class, "confext"))
+ fputs("Before=systemd-confext.service\n", f);
+
+ /* Assume network resource unless URL is file:// */
+ if (!file_url_is_valid(remote))
+ fputs("Wants=network-online.target\n"
+ "After=network-online.target\n", f);
+
+ fputs("\n"
+ "[Service]\n"
+ "Type=oneshot\n", f);
+
+ _cleanup_free_ char *formatted = NULL;
+ r = sd_json_variant_format(v, /* flags= */ 0, &formatted);
+ if (r < 0)
+ return log_error_errno(r, "Failed to format import JSON data: %m");
+
+ _cleanup_free_ char *escaped = specifier_escape(formatted);
+ if (!escaped)
+ return log_oom();
+
+ fprintf(f, "ExecStart=:varlinkctl call -q --more /run/systemd/io.systemd.Import io.systemd.Import.Pull '%s'\n",
+ escaped);
+
+ r = fflush_and_check(f);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write unit %s: %m", service);
+
+ return generator_add_symlink(arg_dest, "multi-user.target", "wants", service);
+}
+
+static int generate(void) {
+ size_t c = 0;
+ int r = 0;
+
+ FOREACH_ARRAY(i, arg_transfers, arg_n_transfers)
+ RET_GATHER(r, transfer_generate(*i, c++));
+
+ return r;
+}
+
+static int run(const char *dest, const char *dest_early, const char *dest_late) {
+ int r;
+
+ assert_se(arg_dest = dest);
+
+ r = proc_cmdline_parse(parse_proc_cmdline_item, NULL, PROC_CMDLINE_RD_STRICT|PROC_CMDLINE_STRIP_RD_PREFIX);
+ if (r < 0)
+ log_warning_errno(r, "Failed to parse kernel command line, ignoring: %m");
+
+ (void) parse_credentials();
+
+ return generate();
+}
+
+DEFINE_MAIN_GENERATOR_FUNCTION(run);
diff --git a/src/import/meson.build b/src/import/meson.build
index 184dd7bbf2..45500edb43 100644
--- a/src/import/meson.build
+++ b/src/import/meson.build
@@ -110,6 +110,11 @@ executables += [
'conditions' : ['ENABLE_IMPORTD'],
'sources' : files('importctl.c'),
},
+ generator_template + {
+ 'name' : 'systemd-import-generator',
+ 'sources' : files('import-generator.c'),
+ 'conditions' : ['ENABLE_IMPORTD'],
+ },
test_template + {
'sources' : files(
'test-qcow2.c',