import: add generator that synthesizes download jobs from kernel cmdline

This commit is contained in:
Lennart Poettering 2024-06-25 09:55:16 +02:00
parent f596658811
commit 5f87b035fa
5 changed files with 498 additions and 0 deletions

View 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',

View File

@ -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>

View File

@ -415,6 +415,16 @@
<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>

View File

@ -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);

View 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',