]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
import: add generator that synthesizes download jobs from kernel cmdline
authorLennart Poettering <lennart@poettering.net>
Tue, 25 Jun 2024 07:55:16 +0000 (09:55 +0200)
committerLennart Poettering <lennart@poettering.net>
Tue, 25 Jun 2024 07:57:42 +0000 (09:57 +0200)
man/rules/meson.build
man/systemd-import-generator.xml [new file with mode: 0644]
man/systemd.system-credentials.xml
src/import/import-generator.c [new file with mode: 0644]
src/import/meson.build

index 9b8a29c5647f5ab685ddd8eb17c59b7ffb82d112..fda14d55bd5f989e8f3f15acb2b710fcc81635d5 100644 (file)
@@ -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 (file)
index 0000000..108509d
--- /dev/null
@@ -0,0 +1,194 @@
+<?xml version="1.0"?>
+<!--*-nxml-*-->
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+  "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd" [
+<!ENTITY % entities SYSTEM "custom-entities.ent" >
+%entities;
+]>
+<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
+<refentry id="systemd-import-generator"
+          xmlns:xi="http://www.w3.org/2001/XInclude">
+
+  <refentryinfo>
+    <title>systemd-import-generator</title>
+    <productname>systemd</productname>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle>systemd-import-generator</refentrytitle>
+    <manvolnum>8</manvolnum>
+  </refmeta>
+
+  <refnamediv>
+    <refname>systemd-import-generator</refname>
+    <refpurpose>Generator for automatically downloading disk images at boot</refpurpose>
+  </refnamediv>
+
+  <refsynopsisdiv>
+    <para><filename>/usr/lib/systemd/system-generators/systemd-import-generator</filename></para>
+  </refsynopsisdiv>
+
+  <refsect1>
+    <title>Description</title>
+
+    <para><command>systemd-import-generator</command> may be used to automatically download disk images
+    (tarballs or DDIs) via
+    <citerefentry><refentrytitle>systemd-importd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+    at boot, based on parameters on the kernel command line or via system credentials. This is useful for
+    automatically deploying an
+    <citerefentry><refentrytitle>systemd-confext</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+    <citerefentry><refentrytitle>systemd-sysext</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+    <citerefentry><refentrytitle>systemd-nspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry>/
+    <citerefentry><refentrytitle>systemd-vmspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry> or
+    <citerefentry><refentrytitle>systemd-portabled.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+    image at boot. This provides functionality equivalent to
+    <citerefentry><refentrytitle>importctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>, but
+    accessible via the kernel command line and system credentials.</para>
+
+    <para><filename>systemd-import-generator</filename> implements
+    <citerefentry><refentrytitle>systemd.generator</refentrytitle><manvolnum>7</manvolnum></citerefentry>.</para>
+  </refsect1>
+
+  <refsect1>
+    <title>Kernel Command Line</title>
+
+    <para><filename>systemd-import-generator</filename> understands the following
+    <citerefentry><refentrytitle>kernel-command-line</refentrytitle><manvolnum>7</manvolnum></citerefentry>
+    parameters:</para>
+
+    <variablelist class='kernel-commandline-options'>
+      <varlistentry>
+        <term><varname>systemd.pull=</varname></term>
+
+        <listitem><para>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
+        <literal>http://</literal>, <literal>https://</literal>, <literal>file://</literal> schemes. The
+        option string itself is a comma separated list of options:</para>
+
+        <variablelist>
+          <varlistentry>
+            <term>rw</term>
+            <term>ro</term>
+
+            <listitem><para>Controls whether to mark the local image as read-only. If not
+            specified read-only defaults to off.</para>
+
+            <xi:include href="version-info.xml" xpointer="v257"/></listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term>verify=</term>
+
+            <listitem><para>Controls whether to cryptographically validate the download before installing it
+            in place. Takes one of <literal>no</literal>, <literal>checksum</literal> or
+            <literal>signature</literal> (the latter being the default if not specified). For details see the
+            <option>--verify=</option> of
+            <citerefentry><refentrytitle>importctl</refentrytitle><manvolnum>1</manvolnum></citerefentry></para>
+
+            <xi:include href="version-info.xml" xpointer="v257"/></listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term>sysext</term>
+            <term>confext</term>
+            <term>machine</term>
+            <term>portable</term>
+
+            <listitem><para>Controls the image class to download, and thus ultimately the target directory
+            for the image, depending on this choice the target directory
+            <filename>/var/lib/extensions/</filename>, <filename>/var/lib/confexts/</filename>,
+            <filename>/var/lib/machines/</filename> or <filename>/var/lib/portables/</filename> is
+            selected.</para>
+
+            <para>Specification of exactly one of these options is mandatory.</para>
+
+            <xi:include href="version-info.xml" xpointer="v257"/></listitem>
+          </varlistentry>
+
+          <varlistentry>
+            <term>tar</term>
+            <term>raw</term>
+
+            <listitem><para>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).</para>
+
+            <para>Specification of exactly one of these options is mandatory.</para>
+
+            <xi:include href="version-info.xml" xpointer="v257"/></listitem>
+          </varlistentry>
+        </variablelist>
+
+        <xi:include href="version-info.xml" xpointer="v257"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><varname>systemd.pull.success_action=</varname></term>
+        <term><varname>systemd.pull.failure_action=</varname></term>
+
+        <listitem><para>Controls whether to execute an action such as reboot, power-off and similar after
+        completing the download successfully, or unsuccessfully. See
+        <varname>SuccessAction=</varname>/<varname>FailureAction=</varname> on
+        <citerefentry><refentrytitle>systemd.unit</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
+        details about the available actions. If not specified no action is taken, and the system will
+        continue to boot normally.</para>
+
+        <xi:include href="version-info.xml" xpointer="v257"/></listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>Credentials</title>
+
+    <para><command>systemd-import-generator</command> supports the system credentials logic. The following
+    credentials are used when passed in:</para>
+
+    <variablelist class='system-credentials'>
+      <varlistentry>
+        <term><varname>import.pull</varname></term>
+
+        <listitem><para>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
+        <varname>systemd.pull=</varname> kernel command line option described above.</para>
+
+        <xi:include href="version-info.xml" xpointer="v257"/></listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>Examples</title>
+
+    <example>
+      <title>Download Configuration Extension</title>
+
+      <programlisting>systemd.pull=raw,confext::https://example.com/myconfext.raw.gz</programlisting>
+
+      <para>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.</para>
+    </example>
+
+    <example>
+      <title>Download System Extension (Without Validation)</title>
+
+      <programlisting>systemd.pull=tar,sysext,verify=no::https://example.com/mysysext.tar.gz</programlisting>
+
+      <para>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!</para>
+    </example>
+  </refsect1>
+
+  <refsect1>
+    <title>See Also</title>
+    <para><simplelist type="inline">
+      <member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>systemd-importd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>kernel-command-line</refentrytitle><manvolnum>7</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>systemd.system-credentials</refentrytitle><manvolnum>7</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>importctl</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
+    </simplelist></para>
+  </refsect1>
+</refentry>
index d9fbae25eecf5780f9685bd7d59237156f23e80d..f8c27d04acc300864a7122ae87b175619008e866 100644 (file)
           <xi:include href="version-info.xml" xpointer="v256"/>
         </listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term><varname>import.pull</varname></term>
+        <listitem>
+          <para>Specified disk images (tarballs and DDIs) to automatically download and install at boot. For details see
+          <citerefentry><refentrytitle>systemd-import-generator</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para>
+
+          <xi:include href="version-info.xml" xpointer="v257"/>
+        </listitem>
+      </varlistentry>
     </variablelist>
   </refsect1>
 
diff --git a/src/import/import-generator.c b/src/import/import-generator.c
new file mode 100644 (file)
index 0000000..29fa5aa
--- /dev/null
@@ -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);
index 184dd7bbf2dcbd9e03a3886e2ba8f1eaadef61a5..45500edb433b9dcc58bf973b48a63c0d58778159 100644 (file)
@@ -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',