From: Lennart Poettering Date: Tue, 25 Jun 2024 07:55:16 +0000 (+0200) Subject: import: add generator that synthesizes download jobs from kernel cmdline X-Git-Tag: v257-rc1~1056^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=5f87b035fad1682a32806d5b3597e9f742a5d286;p=thirdparty%2Fsystemd.git import: add generator that synthesizes download jobs from kernel cmdline --- diff --git a/man/rules/meson.build b/man/rules/meson.build index 9b8a29c5647..fda14d55bd5 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 00000000000..108509d7d46 --- /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 d9fbae25eec..f8c27d04acc 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 00000000000..29fa5aad320 --- /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 184dd7bbf2d..45500edb433 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',