clonesetup: add support to clone devices via /etc/clonetab
Adds dm-clone device setup at boot via a new /etc/clonetab config file, following the crypttab/veritytab pattern. - Add systemd-clonesetup-generator to parse /etc/clonetab and generate units. - Add systemd-clonesetup binary to create/remove dm-clone devices via ioctl. - Add clonesetup.target for ordering dm-clone activation at boot. - Add region_size= option in clonetab to configure dm-clone hydration granularity. - Add clonetab(5) and systemd-clonesetup-generator(8) man pages. Fixes: https://github.com/systemd/systemd/issues/39500
This commit is contained in:
committed by
Lennart Poettering
parent
c0c5ba9662
commit
104970a8bd
@@ -76,6 +76,10 @@ All tools:
|
||||
`/etc/veritytab`. Only useful for debugging. Currently only supported by
|
||||
`systemd-veritysetup-generator`.
|
||||
|
||||
* `$SYSTEMD_CLONETAB` — if set, use this path instead of
|
||||
`/etc/clonetab`. Only useful for debugging. Currently only supported by
|
||||
`systemd-clonesetup-generator`.
|
||||
|
||||
* `$SYSTEMD_DEFAULT_HOSTNAME` — override the compiled-in fallback hostname
|
||||
(relevant in particular for the system manager and `systemd-hostnamed`).
|
||||
Must be a valid hostname (either a single label or a FQDN).
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<?xml version="1.0"?>
|
||||
<!--*-nxml-*-->
|
||||
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
|
||||
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
|
||||
<!--
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
-->
|
||||
<refentry id="clonetab" xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
|
||||
<refentryinfo>
|
||||
<title>clonetab</title>
|
||||
<productname>systemd</productname>
|
||||
</refentryinfo>
|
||||
|
||||
<refmeta>
|
||||
<refentrytitle>clonetab</refentrytitle>
|
||||
<manvolnum>5</manvolnum>
|
||||
</refmeta>
|
||||
|
||||
<refnamediv>
|
||||
<refname>clonetab</refname>
|
||||
<refpurpose>Configuration for dm-clone block devices</refpurpose>
|
||||
</refnamediv>
|
||||
|
||||
<refsynopsisdiv>
|
||||
<para><filename>/etc/clonetab</filename></para>
|
||||
</refsynopsisdiv>
|
||||
|
||||
<refsect1>
|
||||
<title>Description</title>
|
||||
|
||||
<para>The <filename>/etc/clonetab</filename> file describes
|
||||
dm-clone block devices that are set up during system boot.</para>
|
||||
|
||||
<para>Empty lines and lines starting with the <literal>#</literal>
|
||||
character are ignored. Each of the remaining lines describes one
|
||||
dm-clone device. Fields are delimited by white space.</para>
|
||||
|
||||
<para>Each line is in the form<programlisting><replaceable>name</replaceable> <replaceable>source-device</replaceable> <replaceable>destination-device</replaceable> <replaceable>metadata-device</replaceable> [<replaceable>options</replaceable>]</programlisting>
|
||||
The first four fields are mandatory, the fifth is optional.</para>
|
||||
|
||||
<para>The five fields of <filename>/etc/clonetab</filename> are defined as follows:</para>
|
||||
|
||||
<orderedlist>
|
||||
|
||||
<listitem><para>The first field contains the name of the resulting dm-clone device; its
|
||||
block device is set up below <filename>/dev/mapper/</filename>.</para></listitem>
|
||||
|
||||
<listitem><para>The second field contains a path to the read-only source block device.
|
||||
This is the device whose data is cloned to the destination device. Reads to regions not
|
||||
yet hydrated are served directly from this device.</para></listitem>
|
||||
|
||||
<listitem><para>The third field contains a path to the writable destination block device.
|
||||
The source device's data is copied here in the background. It must be at least as
|
||||
large as the source device.</para></listitem>
|
||||
|
||||
<listitem><para>The fourth field contains a path to the metadata block device. This
|
||||
small device tracks which regions of the destination have been hydrated and is managed
|
||||
exclusively by dm-clone.</para></listitem>
|
||||
|
||||
<listitem><para>The fifth field, if present, contains comma-separated <literal>key=value</literal>
|
||||
options. The following option is supported:</para>
|
||||
|
||||
<variablelist>
|
||||
<varlistentry>
|
||||
<term><option>region-size=<replaceable>BYTES</replaceable></option></term>
|
||||
<listitem><para>Controls the granularity of background hydration copying — how much
|
||||
data is copied at a time. Region size is specified in bytes (standard suffixes like
|
||||
<literal>K</literal>, <literal>M</literal>, <literal>G</literal> are supported), and must
|
||||
correspond to a power of two between 4 KiB and 1 GiB. For example,
|
||||
<literal>region-size=4K</literal> or <literal>region-size=4096</literal> sets a 4 KiB region size.</para>
|
||||
|
||||
<para>
|
||||
One region is the atomic unit dm-clone tracks: it is either fully hydrated (copied to the destination)
|
||||
or not, never partially. If a copy is interrupted mid-region, that whole region is
|
||||
retried from scratch on next boot. Smaller regions mean finer progress tracking;
|
||||
larger regions reduce metadata overhead. Defaults to <literal>4K</literal>. For background, see
|
||||
<ulink url="https://www.kernel.org/doc/html/latest/admin-guide/device-mapper/dm-clone.html">dm-clone kernel documentation</ulink>.
|
||||
</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
|
||||
<para>If no options are needed, the field may be omitted entirely or
|
||||
<literal>-</literal> may be used as a placeholder.</para>
|
||||
</listitem>
|
||||
|
||||
</orderedlist>
|
||||
|
||||
<para>At early boot and when the system manager configuration is reloaded, this file is
|
||||
translated into native systemd units by
|
||||
<citerefentry><refentrytitle>systemd-clonesetup-generator</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<title>Examples</title>
|
||||
|
||||
<example>
|
||||
<title>Simple clone without options</title>
|
||||
<para>Clone a source device to a destination, using a separate metadata device:</para>
|
||||
<programlisting>mydevice /dev/sdb /dev/sdc /dev/sdd</programlisting>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
<title>Clone with custom region size</title>
|
||||
<para>Clone a source device to a destination with a custom region size of 8 KiB:</para>
|
||||
<programlisting>mydevice /dev/sdb /dev/sdc /dev/sdd region-size=8K</programlisting>
|
||||
</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-clonesetup-generator</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
|
||||
<member><citerefentry project='die-net'><refentrytitle>dmsetup</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
|
||||
</simplelist></para>
|
||||
</refsect1>
|
||||
|
||||
</refentry>
|
||||
@@ -15,6 +15,7 @@ manpages = [
|
||||
['bootup', '7', [], ''],
|
||||
['busctl', '1', [], ''],
|
||||
['capsule@.service', '5', [], ''],
|
||||
['clonetab', '5', [], ''],
|
||||
['coredump.conf', '5', ['coredump.conf.d'], 'ENABLE_COREDUMP'],
|
||||
['coredumpctl', '1', [], 'ENABLE_COREDUMP'],
|
||||
['crypttab', '5', [], 'HAVE_LIBCRYPTSETUP'],
|
||||
@@ -1024,6 +1025,11 @@ manpages = [
|
||||
['systemd-cat', '1', [], ''],
|
||||
['systemd-cgls', '1', [], ''],
|
||||
['systemd-cgtop', '1', [], ''],
|
||||
['systemd-clonesetup',
|
||||
'8',
|
||||
['systemd-clonesetup@.service'],
|
||||
''],
|
||||
['systemd-clonesetup-generator', '8', [], ''],
|
||||
['systemd-coredump',
|
||||
'8',
|
||||
['systemd-coredump.socket', 'systemd-coredump@.service'],
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0"?>
|
||||
<!--*-nxml-*-->
|
||||
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
|
||||
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
|
||||
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
|
||||
<refentry id="systemd-clonesetup-generator" xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
|
||||
<refentryinfo>
|
||||
<title>systemd-clonesetup-generator</title>
|
||||
<productname>systemd</productname>
|
||||
</refentryinfo>
|
||||
|
||||
<refmeta>
|
||||
<refentrytitle>systemd-clonesetup-generator</refentrytitle>
|
||||
<manvolnum>8</manvolnum>
|
||||
</refmeta>
|
||||
|
||||
<refnamediv>
|
||||
<refname>systemd-clonesetup-generator</refname>
|
||||
<refpurpose>Unit generator for <filename>/etc/clonetab</filename></refpurpose>
|
||||
</refnamediv>
|
||||
|
||||
<refsynopsisdiv>
|
||||
<para><filename>/usr/lib/systemd/system-generators/systemd-clonesetup-generator</filename></para>
|
||||
</refsynopsisdiv>
|
||||
|
||||
<refsect1>
|
||||
<title>Description</title>
|
||||
|
||||
<para><filename>systemd-clonesetup-generator</filename> is a generator that translates
|
||||
<filename>/etc/clonetab</filename> into native systemd units early at boot and when
|
||||
configuration of the system manager is reloaded. This will create
|
||||
<citerefentry><refentrytitle>systemd-clonesetup@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
|
||||
units as necessary.</para>
|
||||
|
||||
<para><filename>systemd-clonesetup-generator</filename> implements
|
||||
<citerefentry><refentrytitle>systemd.generator</refentrytitle><manvolnum>7</manvolnum></citerefentry>.</para>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<title>See Also</title>
|
||||
<para><simplelist type="inline">
|
||||
<member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
|
||||
<member><citerefentry><refentrytitle>clonetab</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
|
||||
<member><citerefentry><refentrytitle>systemd-clonesetup@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
|
||||
<member><citerefentry project='die-net'><refentrytitle>dmsetup</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
|
||||
</simplelist></para>
|
||||
</refsect1>
|
||||
|
||||
</refentry>
|
||||
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0"?>
|
||||
<!--*-nxml-*-->
|
||||
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
|
||||
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
|
||||
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
|
||||
<refentry id="systemd-clonesetup" xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
|
||||
<refentryinfo>
|
||||
<title>systemd-clonesetup</title>
|
||||
<productname>systemd</productname>
|
||||
</refentryinfo>
|
||||
|
||||
<refmeta>
|
||||
<refentrytitle>systemd-clonesetup</refentrytitle>
|
||||
<manvolnum>8</manvolnum>
|
||||
</refmeta>
|
||||
|
||||
<refnamediv>
|
||||
<refname>systemd-clonesetup</refname>
|
||||
<refname>systemd-clonesetup@.service</refname>
|
||||
<refpurpose>Set up and tear down dm-clone devices</refpurpose>
|
||||
</refnamediv>
|
||||
|
||||
<refsynopsisdiv>
|
||||
<cmdsynopsis>
|
||||
<command>systemd-clonesetup</command>
|
||||
<arg choice="plain">add</arg>
|
||||
<arg choice="plain">NAME</arg>
|
||||
<arg choice="plain">SOURCE-DEVICE</arg>
|
||||
<arg choice="plain">DEST-DEVICE</arg>
|
||||
<arg choice="plain">META-DEVICE</arg>
|
||||
<arg choice="opt">OPTIONS</arg>
|
||||
</cmdsynopsis>
|
||||
|
||||
<cmdsynopsis>
|
||||
<command>systemd-clonesetup</command>
|
||||
<arg choice="plain">remove</arg>
|
||||
<arg choice="plain">NAME</arg>
|
||||
</cmdsynopsis>
|
||||
|
||||
<para><filename>systemd-clonesetup@.service</filename></para>
|
||||
</refsynopsisdiv>
|
||||
|
||||
<refsect1>
|
||||
<title>Description</title>
|
||||
|
||||
<para><command>systemd-clonesetup</command> is used to set up (with <command>add</command>) and tear down
|
||||
(with <command>remove</command>) dm-clone devices. It is primarily used via
|
||||
<filename>systemd-clonesetup@.service</filename> units generated from
|
||||
<filename>/etc/clonetab</filename> by
|
||||
<citerefentry><refentrytitle>systemd-clonesetup-generator</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
|
||||
</para>
|
||||
|
||||
<para>The positional arguments <parameter>NAME</parameter>, <parameter>SOURCE-DEVICE</parameter>,
|
||||
<parameter>DEST-DEVICE</parameter>, <parameter>META-DEVICE</parameter>, and <parameter>OPTIONS</parameter>
|
||||
have the same meaning as the fields in
|
||||
<citerefentry><refentrytitle>clonetab</refentrytitle><manvolnum>5</manvolnum></citerefentry>.</para>
|
||||
|
||||
<para>Currently, the supported option in <parameter>OPTIONS</parameter> is
|
||||
<varname>region-size=</varname><replaceable>BYTES</replaceable>, where
|
||||
<replaceable>BYTES</replaceable> is measured in bytes (standard suffixes like
|
||||
<literal>K</literal>, <literal>M</literal>, <literal>G</literal> are supported).</para>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<title>See Also</title>
|
||||
<para><simplelist type="inline">
|
||||
<member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
|
||||
<member><citerefentry><refentrytitle>clonetab</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
|
||||
<member><citerefentry><refentrytitle>systemd-clonesetup-generator</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
|
||||
<member><citerefentry><refentrytitle>systemd.generator</refentrytitle><manvolnum>7</manvolnum></citerefentry></member>
|
||||
<member><citerefentry project='die-net'><refentrytitle>dmsetup</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
|
||||
</simplelist></para>
|
||||
</refsect1>
|
||||
|
||||
</refentry>
|
||||
@@ -302,6 +302,7 @@ conf.set_quoted('SYSTEMD_USERWORK_PATH', libexecdir / 'syst
|
||||
conf.set_quoted('SYSTEMD_MOUNTWORK_PATH', libexecdir / 'systemd-mountwork')
|
||||
conf.set_quoted('SYSTEMD_NSRESOURCEWORK_PATH', libexecdir / 'systemd-nsresourcework')
|
||||
conf.set_quoted('SYSTEMD_VERITYSETUP_PATH', libexecdir / 'systemd-veritysetup')
|
||||
conf.set_quoted('SYSTEMD_CLONESETUP_PATH', libexecdir / 'systemd-clonesetup')
|
||||
conf.set_quoted('SYSTEM_CONFIG_UNIT_DIR', pkgsysconfdir / 'system')
|
||||
conf.set_quoted('SYSTEM_DATA_UNIT_DIR', systemunitdir)
|
||||
conf.set_quoted('SYSTEM_ENV_GENERATOR_DIR', systemenvgeneratordir)
|
||||
@@ -2141,6 +2142,7 @@ subdir('src/bootctl')
|
||||
subdir('src/busctl')
|
||||
subdir('src/cgls')
|
||||
subdir('src/cgtop')
|
||||
subdir('src/clonesetup')
|
||||
subdir('src/coredump')
|
||||
subdir('src/creds')
|
||||
subdir('src/cryptenroll')
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "alloc-util.h"
|
||||
#include "clonesetup-util.h"
|
||||
#include "dropin.h"
|
||||
#include "errno-util.h"
|
||||
#include "fd-util.h"
|
||||
#include "fileio.h"
|
||||
#include "generator.h"
|
||||
#include "log.h"
|
||||
#include "path-util.h"
|
||||
#include "specifier.h"
|
||||
#include "unit-name.h"
|
||||
|
||||
static const char *arg_dest = NULL;
|
||||
|
||||
/* Generate unit files that call the systemd-clonesetup binary to create or remove clone devices. */
|
||||
static int generate_clone_units(
|
||||
const char *clone_name,
|
||||
const char *source_dev,
|
||||
const char *dest_dev,
|
||||
const char *metadata_dev,
|
||||
const char *options) {
|
||||
|
||||
/* unit files for each device */
|
||||
_cleanup_fclose_ FILE *f = NULL;
|
||||
int r;
|
||||
|
||||
assert(clone_name);
|
||||
assert(source_dev);
|
||||
assert(dest_dev);
|
||||
assert(metadata_dev);
|
||||
|
||||
/* Escape clone name for unit specifiers and then ExecStart/ExecStop parsing. */
|
||||
_cleanup_free_ char *clone_name_spec_escaped = NULL;
|
||||
clone_name_spec_escaped = specifier_escape(clone_name);
|
||||
if (!clone_name_spec_escaped)
|
||||
return log_oom();
|
||||
|
||||
/* create clone_dev_path that holds path for new cloned device */
|
||||
_cleanup_free_ char *clone_dev_path = NULL, *clone_dev_path_escaped = NULL,
|
||||
*clone_dev_path_unit_escaped = NULL;
|
||||
clone_dev_path = path_join("/dev/mapper", clone_name);
|
||||
if (!clone_dev_path)
|
||||
return log_oom();
|
||||
|
||||
clone_dev_path_escaped = specifier_escape(clone_dev_path);
|
||||
if (!clone_dev_path_escaped)
|
||||
return log_oom();
|
||||
|
||||
r = unit_name_path_escape(clone_dev_path, &clone_dev_path_unit_escaped);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to escape clone device path: %m");
|
||||
|
||||
_cleanup_free_ char *e = NULL, *unit = NULL;
|
||||
/* escape clone name */
|
||||
e = unit_name_escape(clone_name);
|
||||
if (!e)
|
||||
return log_oom();
|
||||
|
||||
/* Generate unit name for the clone service */
|
||||
r = unit_name_build("systemd-clonesetup", e, ".service", &unit);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to generate unit name: %m");
|
||||
|
||||
/* Generate unit names for dependencies */
|
||||
_cleanup_free_ char *source_unit = NULL, *dest_unit = NULL, *metadata_unit = NULL;
|
||||
r = unit_name_from_path(source_dev, ".device", &source_unit);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to generate source device unit name: %m");
|
||||
|
||||
r = unit_name_from_path(dest_dev, ".device", &dest_unit);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to generate dest device unit name: %m");
|
||||
|
||||
r = unit_name_from_path(metadata_dev, ".device", &metadata_unit);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to generate metadata device unit name: %m");
|
||||
|
||||
/* Escape device paths for unit specifiers and then ExecStart parsing. */
|
||||
_cleanup_free_ char *source_spec_escaped = NULL,
|
||||
*dest_spec_escaped = NULL, *metadata_spec_escaped = NULL,
|
||||
*options_spec_escaped = NULL;
|
||||
source_spec_escaped = specifier_escape(source_dev);
|
||||
if (!source_spec_escaped)
|
||||
return log_oom();
|
||||
|
||||
dest_spec_escaped = specifier_escape(dest_dev);
|
||||
if (!dest_spec_escaped)
|
||||
return log_oom();
|
||||
|
||||
metadata_spec_escaped = specifier_escape(metadata_dev);
|
||||
if (!metadata_spec_escaped)
|
||||
return log_oom();
|
||||
|
||||
if (options) {
|
||||
options_spec_escaped = specifier_escape(options);
|
||||
if (!options_spec_escaped)
|
||||
return log_oom();
|
||||
}
|
||||
|
||||
r = generator_open_unit_file(arg_dest, /* source = */ NULL, unit, &f);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
/* With DefaultDependencies=no, order after udev so backing /dev nodes are ready in early boot.
|
||||
* The : exec prefix on ExecStart=/ExecStop= disables $ { } env-var expansion by the manager. */
|
||||
fprintf(f,
|
||||
"[Unit]\n"
|
||||
"Description=Create dm-clone device %1$s\n"
|
||||
"Documentation=man:clonetab(5) man:systemd-clonesetup(8) man:systemd-clonesetup-generator(8)\n"
|
||||
"DefaultDependencies=no\n"
|
||||
"BindsTo=%2$s %3$s %4$s\n"
|
||||
"After=%2$s %3$s %4$s systemd-udevd-kernel.socket\n"
|
||||
"Before=blockdev@%5$s.target clonesetup.target shutdown.target\n"
|
||||
"Wants=blockdev@%5$s.target\n"
|
||||
"Conflicts=shutdown.target\n"
|
||||
"\n"
|
||||
"[Service]\n"
|
||||
"Type=oneshot\n"
|
||||
"RemainAfterExit=yes\n"
|
||||
"ExecStart=:" SYSTEMD_CLONESETUP_PATH " add '%6$s' '%7$s' '%8$s' '%9$s' '%10$s'\n"
|
||||
"ExecStop=:" SYSTEMD_CLONESETUP_PATH " remove '%6$s'\n"
|
||||
"TimeoutSec=0\n",
|
||||
clone_dev_path_escaped,
|
||||
source_unit, dest_unit, metadata_unit,
|
||||
clone_dev_path_unit_escaped,
|
||||
clone_name_spec_escaped, source_spec_escaped, dest_spec_escaped,
|
||||
metadata_spec_escaped, options_spec_escaped ?: "-");
|
||||
|
||||
r = fflush_and_check(f);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to write unit %s: %m", unit);
|
||||
|
||||
/* symlink unit file to enable it */
|
||||
_cleanup_free_ char *dmname = NULL;
|
||||
r = unit_name_from_path(clone_dev_path, ".device", &dmname);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to generate clone device unit name: %m");
|
||||
|
||||
r = generator_add_symlink(arg_dest, dmname, "requires", unit);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
/* Extend device timeout to allow clone service to complete */
|
||||
r = write_drop_in(arg_dest, dmname, 40, "device-timeout",
|
||||
"# Automatically generated by systemd-clonesetup-generator\n\n"
|
||||
"[Unit]\n"
|
||||
"JobRunningTimeoutSec=infinity\n");
|
||||
if (r < 0)
|
||||
log_warning_errno(r, "Failed to write device timeout drop-in: %m");
|
||||
|
||||
/* Add to clonesetup.target so it starts at boot */
|
||||
r = generator_add_symlink(arg_dest, "clonesetup.target", "requires", unit);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int add_clone_devices(void) {
|
||||
_cleanup_fclose_ FILE *f = NULL;
|
||||
unsigned clone_line = 0;
|
||||
int r, ret = 0;
|
||||
const char *fname;
|
||||
|
||||
fname = secure_getenv("SYSTEMD_CLONETAB") ?: "/etc/clonetab";
|
||||
|
||||
r = fopen_unlocked(fname, "re", &f);
|
||||
if (r < 0) {
|
||||
if (r != -ENOENT)
|
||||
log_error_errno(r, "Failed to open %s: %m", fname);
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
_cleanup_free_ char *line = NULL, *src = NULL,
|
||||
*name = NULL, *dst = NULL, *meta = NULL, *options = NULL;
|
||||
int k;
|
||||
|
||||
r = read_stripped_line(f, LONG_LINE_MAX, &line);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to read %s: %m", fname);
|
||||
if (r == 0)
|
||||
break;
|
||||
|
||||
clone_line++;
|
||||
|
||||
if (IN_SET(line[0], 0, '#'))
|
||||
continue;
|
||||
|
||||
k = sscanf(line, "%ms %ms %ms %ms %ms", &name, &src, &dst, &meta, &options);
|
||||
if (k < 4 || k > 5) {
|
||||
log_warning("Failed to parse %s:%u, ignoring.", fname, clone_line);
|
||||
continue;
|
||||
}
|
||||
|
||||
log_debug("Processing %s:%u", fname, clone_line);
|
||||
r = validate_fields(name, src, dst, meta, options);
|
||||
if (r < 0)
|
||||
continue;
|
||||
RET_GATHER(ret, generate_clone_units(name, src, dst, meta, options));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* This generator reads /etc/clonetab and for each entry, writes unit files
|
||||
* (creates systemd-clonesetup@<name>.service and clonesetup.target.requires/systemd-clonesetup@<name>.service)
|
||||
* that clonesetup.target requires, and that run systemd-clonesetup (add device at boot,
|
||||
* remove it at shutdown); systemd-clonesetup (used in systemd-clonesetup@.service) is the binary that
|
||||
* uses device-mapper ioctls to create and remove the dm-clone devices.
|
||||
* clonesetup.target groups these units so they run together at boot.
|
||||
* Boot chain: sysinit.target has clonesetup.target in sysinit.target.wants/ (see units/meson.build),
|
||||
* so at boot clonesetup.target starts and pulls in these units via clonesetup.target.requires/. */
|
||||
static int run(const char *dest, const char *dest_early, const char *dest_late) {
|
||||
|
||||
/* dest usually is /run/systemd/generator */
|
||||
assert_se(arg_dest = dest);
|
||||
|
||||
return add_clone_devices();
|
||||
}
|
||||
|
||||
DEFINE_MAIN_GENERATOR_FUNCTION(run);
|
||||
@@ -0,0 +1,276 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <linux/dm-ioctl.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "sd-device.h"
|
||||
|
||||
#include "clonesetup-ioctl.h"
|
||||
#include "device-private.h"
|
||||
#include "errno-util.h"
|
||||
#include "fd-util.h"
|
||||
#include "log.h"
|
||||
#include "memory-util.h"
|
||||
#include "string-util.h"
|
||||
|
||||
/* Returns the size in bytes of the block device at dev_path.
|
||||
* Loading the dm-clone table needs the source device size in sectors; sysfs
|
||||
* reports size in 512-byte sectors. This reads sysfs and returns bytes so the
|
||||
* caller can divide by 512 and pass the sector count to dm_clone_load_table(). */
|
||||
static int get_size(const char *dev_path, uint64_t *ret_size) {
|
||||
_cleanup_(sd_device_unrefp) sd_device *dev = NULL;
|
||||
uint64_t size, temp_size;
|
||||
int r;
|
||||
|
||||
assert(dev_path);
|
||||
assert(ret_size);
|
||||
|
||||
r = sd_device_new_from_devname(&dev, dev_path);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to create device from '%s': %m", dev_path);
|
||||
|
||||
r = device_get_sysattr_u64(dev, "size", &size);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to get device size for '%s': %m", dev_path);
|
||||
|
||||
/* sysfs 'size' is in 512-byte sectors */
|
||||
temp_size = u64_multiply_safe(size, 512);
|
||||
if (temp_size == 0 && size != 0)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW),
|
||||
"Device size overflow for '%s'", dev_path);
|
||||
|
||||
*ret_size = temp_size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Common helper used to run dm ioctls. */
|
||||
static int dm_ioctl_run(const char *name, uint32_t cmd, struct dm_ioctl *data, size_t data_size) {
|
||||
_cleanup_close_ int fd = -EBADF;
|
||||
struct dm_ioctl *dm = data;
|
||||
int r;
|
||||
|
||||
assert(name);
|
||||
assert(data);
|
||||
assert(data_size >= sizeof(struct dm_ioctl));
|
||||
|
||||
dm->version[0] = DM_VERSION_MAJOR;
|
||||
dm->version[1] = DM_VERSION_MINOR;
|
||||
dm->version[2] = DM_VERSION_PATCHLEVEL;
|
||||
dm->data_size = data_size;
|
||||
|
||||
if (strlen(name) >= sizeof_field(struct dm_ioctl, name))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(ENAMETOOLONG),
|
||||
"DM device name too long: %s", name);
|
||||
strncpy_exact(dm->name, name, sizeof(dm->name));
|
||||
|
||||
fd = open("/dev/mapper/control", O_RDWR | O_CLOEXEC);
|
||||
if (fd < 0)
|
||||
return log_error_errno(errno, "Failed to open /dev/mapper/control: %m");
|
||||
|
||||
r = RET_NERRNO(ioctl(fd, cmd, dm));
|
||||
if (r < 0) {
|
||||
if (r == -ENXIO && cmd == DM_DEV_REMOVE) {
|
||||
log_full_errno(LOG_DEBUG, r, "Device \"%s\" is already inactive, ignoring: %m", dm->name);
|
||||
return 0;
|
||||
}
|
||||
return log_error_errno(r, "DM ioctl failed: %m");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* First dm ioctl needed to create a device. */
|
||||
static int dm_clone_create(const char *name) {
|
||||
int r;
|
||||
assert(name);
|
||||
|
||||
struct dm_ioctl dm = {};
|
||||
r = dm_ioctl_run(name, DM_DEV_CREATE, &dm, sizeof(dm));
|
||||
if (r < 0) {
|
||||
if (r == -EEXIST)
|
||||
return log_error_errno(r, "Device '/dev/mapper/%s' already exists.", name);
|
||||
return log_error_errno(r, "Failed to create DM device '%s': %m", name);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Second dm ioctl needed to create a device. */
|
||||
static int dm_clone_load_table(const char *name, uint64_t size_sectors, const char *target_params) {
|
||||
char *params_buf;
|
||||
size_t params_len, dm_size;
|
||||
_cleanup_free_ struct dm_ioctl *dm = NULL;
|
||||
struct dm_target_spec *tgt;
|
||||
|
||||
assert(name);
|
||||
assert(target_params);
|
||||
|
||||
params_len = strlen(target_params) + 1;
|
||||
/* ensure that dm_size is always aligned, so it makes the buffer actually match what .next claims */
|
||||
dm_size = ALIGN8(sizeof(struct dm_ioctl)) + ALIGN8(sizeof(struct dm_target_spec) + params_len);
|
||||
dm = malloc0(dm_size);
|
||||
if (!dm)
|
||||
return log_oom();
|
||||
*dm = (struct dm_ioctl) {
|
||||
.data_start = ALIGN8(sizeof(struct dm_ioctl)),
|
||||
.target_count = 1,
|
||||
};
|
||||
|
||||
tgt = CAST_ALIGN_PTR(struct dm_target_spec, (uint8_t *) dm + dm->data_start);
|
||||
*tgt = (struct dm_target_spec) {
|
||||
.length = size_sectors,
|
||||
/* Per linux/dm-ioctl.h: next is the byte offset from this dm_target_spec to the next one,
|
||||
* rounded up to 8-byte alignment. With target_count == 1 next == 0 works, but set it
|
||||
* correctly to avoid silent breakage if a second target is ever added. */
|
||||
.next = ALIGN8(sizeof(struct dm_target_spec) + params_len),
|
||||
};
|
||||
strncpy(tgt->target_type, "clone", sizeof(tgt->target_type));
|
||||
|
||||
params_buf = (char *) tgt + ALIGN8(sizeof(struct dm_target_spec));
|
||||
memcpy(params_buf, target_params, params_len);
|
||||
|
||||
return dm_ioctl_run(name, DM_TABLE_LOAD, dm, dm_size);
|
||||
}
|
||||
|
||||
/* Third and final dm ioctl needed to create a device. */
|
||||
static int dm_clone_activate(const char *name) {
|
||||
assert(name);
|
||||
|
||||
struct dm_ioctl dm = {};
|
||||
|
||||
return dm_ioctl_run(name, DM_DEV_SUSPEND, &dm, sizeof(dm));
|
||||
}
|
||||
|
||||
/* Calls multiple dm ioctls to create device. */
|
||||
int dm_clone_create_device(
|
||||
const char *name,
|
||||
const char *source_dev,
|
||||
const char *dest_dev,
|
||||
const char *metadata_dev,
|
||||
uint64_t region_size_bytes) {
|
||||
|
||||
_cleanup_free_ char *target_params = NULL;
|
||||
uint64_t src_dev_size_sectors, src_dev_size, region_size_sectors;
|
||||
int r;
|
||||
|
||||
assert(name);
|
||||
assert(source_dev);
|
||||
assert(dest_dev);
|
||||
assert(metadata_dev);
|
||||
|
||||
r = get_size(source_dev, &src_dev_size);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
/* The device mapper kernel API always uses 512-byte sectors, regardless of the
|
||||
* physical block size of the device (all DM targets use sector_t which is 512B).
|
||||
*
|
||||
* get_size internally uses sysfs i.e. /sys/block/<disk>/size which also reports device size in
|
||||
* 512-byte sectors. Before returning, get_size multiplies the size returned by sysfs to bytes. So we
|
||||
* divide the received byte size by 512 to get the sector count for the DM table. */
|
||||
assert(src_dev_size % 512 == 0);
|
||||
src_dev_size_sectors = src_dev_size / 512;
|
||||
|
||||
assert(region_size_bytes % 512 == 0);
|
||||
region_size_sectors = region_size_bytes / 512;
|
||||
|
||||
/* dm-clone target params: <metadata_dev> <dest_dev> <source_dev> <region_size> <hydration_threshold> [options]
|
||||
* region_size = region size in sectors, configurable via clonetab (default 8 = 4KB regions with
|
||||
* 512-byte sectors) 1 = hydration threshold (regions to hydrate per batch) no_hydration = don't
|
||||
* start automatic background hydration
|
||||
*
|
||||
* The DM table "target_params" string is passed directly to the kernel via ioctl(fd, DM_TABLE_LOAD,
|
||||
* ...) in dm_clone_load_table as a raw byte buffer. The kernel's DM table parser
|
||||
* (drivers/md/dm-table.c) simply splits the params string on whitespace, so the only constraint is
|
||||
* that the paths in params - metadata_dev, dest_dev, source_dev, and region_size must not contain
|
||||
* spaces, which standard /dev/ paths never do, so the below args do NOT require shell escaping */
|
||||
if (asprintf(&target_params, "%s %s %s %" PRIu64 " 1 no_hydration",
|
||||
metadata_dev, dest_dev, source_dev, region_size_sectors) < 0)
|
||||
return log_oom();
|
||||
|
||||
r = dm_clone_create(name);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = dm_clone_load_table(name, src_dev_size_sectors, target_params);
|
||||
if (r < 0)
|
||||
goto fail;
|
||||
|
||||
r = dm_clone_activate(name);
|
||||
if (r < 0)
|
||||
goto fail;
|
||||
|
||||
log_info("Device %s active.", name);
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
(void) dm_clone_remove_device_deferred(name);
|
||||
return r;
|
||||
}
|
||||
|
||||
/* Calls dm ioctl to send a message to the device. dm_ioctl is the kernel's generic device mapper envelope —
|
||||
* every ioctl needs it. dm_target_msg is specific to the "send a message" operation
|
||||
* */
|
||||
int dm_clone_send_message(const char *name, const char *message) {
|
||||
_cleanup_free_ struct dm_ioctl *dm = NULL;
|
||||
struct dm_target_msg *msg;
|
||||
size_t dm_size, msg_len;
|
||||
|
||||
assert(name);
|
||||
assert(message);
|
||||
|
||||
msg_len = strlen(message) + 1;
|
||||
/* need to take into account both headers in size calculation */
|
||||
dm_size = ALIGN8(sizeof(struct dm_ioctl)) + sizeof(struct dm_target_msg) + msg_len;
|
||||
dm = malloc0(dm_size);
|
||||
if (!dm)
|
||||
return log_oom();
|
||||
*dm = (struct dm_ioctl) {
|
||||
/* with ALIGN8 call below, dm_target_msg starts at ALIGN8(sizeof(struct dm_ioctl)) which is
|
||||
* already aligned, so the dm_target_msg struct lands correctly */
|
||||
.data_start = ALIGN8(sizeof(struct dm_ioctl)),
|
||||
};
|
||||
|
||||
msg = CAST_ALIGN_PTR(struct dm_target_msg, (uint8_t *) dm + dm->data_start);
|
||||
memcpy(msg->message, message, msg_len);
|
||||
|
||||
return dm_ioctl_run(name, DM_TARGET_MSG, dm, dm_size);
|
||||
}
|
||||
|
||||
/* Calls dm ioctl to remove a device. flags if set can be used
|
||||
* for deferred remove - e.g. DM_DEFERRED_REMOVE */
|
||||
static int dm_clone_remove_device_full(const char *name, uint32_t flags) {
|
||||
struct dm_ioctl dm = {
|
||||
.flags = flags,
|
||||
};
|
||||
|
||||
assert(name);
|
||||
|
||||
return dm_ioctl_run(name, DM_DEV_REMOVE, &dm, sizeof(dm));
|
||||
}
|
||||
|
||||
/* Calls dm ioctl to remove a device. */
|
||||
int dm_clone_remove_device(const char *name) {
|
||||
int r;
|
||||
|
||||
assert(name);
|
||||
r = dm_clone_remove_device_full(name, 0);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
log_info("Device %s inactive.", name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Calls dm ioctl for deferred removal i.e. DM_DEFERRED_REMOVE */
|
||||
int dm_clone_remove_device_deferred(const char *name) {
|
||||
int r;
|
||||
|
||||
assert(name);
|
||||
r = dm_clone_remove_device_full(name, DM_DEFERRED_REMOVE);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
log_info("Device %s marked for deferred removal.", name);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
int dm_clone_create_device(
|
||||
const char *name,
|
||||
const char *source_dev,
|
||||
const char *dest_dev,
|
||||
const char *metadata_dev,
|
||||
uint64_t region_size_bytes);
|
||||
|
||||
int dm_clone_send_message(const char *name, const char *message);
|
||||
|
||||
int dm_clone_remove_device(const char *name);
|
||||
int dm_clone_remove_device_deferred(const char *name);
|
||||
@@ -0,0 +1,37 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include "clonesetup-util.h"
|
||||
#include "log.h"
|
||||
#include "path-util.h"
|
||||
#include "string-util.h"
|
||||
|
||||
int validate_dev_path(const char *what, const char *path) {
|
||||
if (!string_is_safe(path, 0) || !path_is_normalized(path) ||
|
||||
!path_is_absolute(path) || !path_startswith(path, "/dev/"))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
||||
"Invalid %s device path '%s'.", what, path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int validate_fields(const char *name, const char *src, const char *dst,
|
||||
const char *meta, const char *options) {
|
||||
int r;
|
||||
|
||||
if (!filename_is_valid(name))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid clone name '%s'.", name);
|
||||
|
||||
r = validate_dev_path("source", src);
|
||||
if (r < 0)
|
||||
return r;
|
||||
r = validate_dev_path("destination", dst);
|
||||
if (r < 0)
|
||||
return r;
|
||||
r = validate_dev_path("metadata", meta);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (options && !string_is_safe(options, 0))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid options '%s'.", options);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#pragma once
|
||||
|
||||
int validate_dev_path(const char *what, const char *path);
|
||||
int validate_fields(const char *name, const char *src, const char *dst,
|
||||
const char *meta, const char *options);
|
||||
@@ -0,0 +1,192 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "alloc-util.h"
|
||||
#include "build.h"
|
||||
#include "clonesetup-ioctl.h"
|
||||
#include "clonesetup-util.h"
|
||||
#include "extract-word.h"
|
||||
#include "format-table.h"
|
||||
#include "help-util.h"
|
||||
#include "log.h"
|
||||
#include "main-func.h"
|
||||
#include "options.h"
|
||||
#include "parse-util.h"
|
||||
#include "string-util.h"
|
||||
#include "strv.h" /* strv_skip */
|
||||
#include "verbs.h"
|
||||
|
||||
/* region-size: size of each dm-clone region in bytes. Handled internally in bytes, but must correspond to a
|
||||
* power of 2 between 4K and 1G per dm-clone kernel docs. */
|
||||
#define CLONE_REGION_SIZE_DEFAULT_BYTES (UINT64_C(1) << 12) /* 4 KiB */
|
||||
#define CLONE_REGION_SIZE_MIN_BYTES (UINT64_C(1) << 12) /* 4 KiB */
|
||||
#define CLONE_REGION_SIZE_MAX_BYTES (UINT64_C(1) << 30) /* 1 GiB */
|
||||
|
||||
static int parse_clone_options(const char *options, uint64_t *ret_region_size_bytes) {
|
||||
uint64_t region_size_bytes = CLONE_REGION_SIZE_DEFAULT_BYTES;
|
||||
|
||||
assert(ret_region_size_bytes);
|
||||
|
||||
for (;;) {
|
||||
_cleanup_free_ char *word = NULL;
|
||||
const char *val;
|
||||
int r;
|
||||
|
||||
/* extract_first_word: *
|
||||
* Returns > 0 — successfully extracted a word *
|
||||
* Returns 0 — no more words (end of string) *
|
||||
* Returns < 0 — actual error (e.g. memory allocation failure) */
|
||||
r = extract_first_word(&options, &word, ",", EXTRACT_DONT_COALESCE_SEPARATORS);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to parse clone options: %m");
|
||||
if (r == 0)
|
||||
break;
|
||||
|
||||
/* treat - as empty — common placeholders for "no options" */
|
||||
if (streq(word, "-"))
|
||||
continue;
|
||||
|
||||
if ((val = startswith(word, "region-size="))) {
|
||||
uint64_t r_size_bytes;
|
||||
/* parse_size handles suffixes like K, M, G automatically */
|
||||
r = parse_size(val, 1024, &r_size_bytes);
|
||||
if (r < 0)
|
||||
log_warning_errno(r, "Failed to parse region-size= value '%s', using default.", val);
|
||||
else if (!ISPOWEROF2(r_size_bytes) || r_size_bytes < CLONE_REGION_SIZE_MIN_BYTES || r_size_bytes > CLONE_REGION_SIZE_MAX_BYTES)
|
||||
log_warning("region-size=%s must be a power of two between 4K and 1G, using default.", val);
|
||||
else
|
||||
region_size_bytes = r_size_bytes;
|
||||
} else
|
||||
/* currently only region-size is supported */
|
||||
log_warning("Unknown clone option '%s', ignoring.", word);
|
||||
}
|
||||
*ret_region_size_bytes = region_size_bytes;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* dm-clone device creation workflow:
|
||||
* 1. Create the dm-clone device
|
||||
* 2. Enable background hydration */
|
||||
static int clone_device(
|
||||
const char *clone_name,
|
||||
const char *source_dev,
|
||||
const char *dest_dev,
|
||||
const char *metadata_dev,
|
||||
const char *options) {
|
||||
|
||||
int r;
|
||||
|
||||
assert(clone_name);
|
||||
assert(source_dev);
|
||||
assert(dest_dev);
|
||||
assert(metadata_dev);
|
||||
|
||||
uint64_t region_size_bytes;
|
||||
r = parse_clone_options(options, ®ion_size_bytes);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = dm_clone_create_device(clone_name, source_dev, dest_dev, metadata_dev, region_size_bytes);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = dm_clone_send_message(clone_name, "enable_hydration");
|
||||
if (r < 0) {
|
||||
(void) dm_clone_remove_device_deferred(clone_name);
|
||||
return r;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
VERB(verb_add, "add", "NAME SOURCE DEST METADATA [OPTIONS]", 5, 6, 0, "Create a dm-clone device");
|
||||
|
||||
/* Arguments: systemd-clonesetup add NAME SOURCE DEST METADATA [OPTIONS] */
|
||||
static int verb_add(int argc, char *argv[], uintptr_t data, void *userdata) {
|
||||
int r;
|
||||
assert(argc >= 5 && argc <= 6);
|
||||
|
||||
const char *name = ASSERT_PTR(argv[1]);
|
||||
const char *src = ASSERT_PTR(argv[2]);
|
||||
const char *dst = ASSERT_PTR(argv[3]);
|
||||
const char *meta = ASSERT_PTR(argv[4]);
|
||||
|
||||
const char *options = argc == 6 ? argv[5] : "";
|
||||
|
||||
r = validate_fields(name, src, dst, meta, options);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
log_debug("%s %s %s %s %s opts=%s", __func__,
|
||||
name, src, dst, meta, options);
|
||||
return clone_device(name, src, dst, meta, options);
|
||||
}
|
||||
|
||||
VERB(verb_remove, "remove", "NAME", 2, 2, 0, "Remove a dm-clone device");
|
||||
|
||||
static int verb_remove(int argc, char *argv[], uintptr_t data, void *userdata) {
|
||||
const char *name = ASSERT_PTR(argv[1]);
|
||||
int r;
|
||||
|
||||
r = dm_clone_remove_device(name);
|
||||
if (r == -EBUSY)
|
||||
return dm_clone_remove_device_deferred(name);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int help(void) {
|
||||
_cleanup_(table_unrefp) Table *options = NULL;
|
||||
int r;
|
||||
|
||||
r = option_parser_get_help_table(&options);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
help_cmdline("add NAME SOURCE DEST METADATA [OPTIONS]");
|
||||
help_cmdline("remove NAME");
|
||||
help_abstract("Add or remove a dm-clone device.");
|
||||
help_section("Options");
|
||||
r = table_print_or_warn(options);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
help_man_page_reference("systemd-clonesetup", "8");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_argv(int argc, char *argv[]) {
|
||||
assert(argc >= 0);
|
||||
assert(argv);
|
||||
|
||||
OptionParser opts = { argc, argv };
|
||||
FOREACH_OPTION_OR_RETURN(c, &opts)
|
||||
switch (c) {
|
||||
|
||||
OPTION_COMMON_HELP:
|
||||
return help();
|
||||
|
||||
OPTION_COMMON_VERSION:
|
||||
return version();
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* systemd-clonesetup uses device-mapper ioctls to create and remove the
|
||||
* dm-clone devices. */
|
||||
static int run(int argc, char *argv[]) {
|
||||
int r;
|
||||
|
||||
log_setup();
|
||||
umask(0022);
|
||||
|
||||
r = parse_argv(argc, argv);
|
||||
if (r <= 0)
|
||||
return r;
|
||||
|
||||
return dispatch_verb(strv_skip(argv, 1), /* userdata= */ NULL);
|
||||
}
|
||||
|
||||
DEFINE_MAIN_FUNCTION(run);
|
||||
@@ -0,0 +1,19 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
systemd_clonesetup_sources = files(
|
||||
'clonesetup-ioctl.c',
|
||||
'clonesetup.c',
|
||||
)
|
||||
|
||||
executables += [
|
||||
libexec_template + {
|
||||
'name' : 'systemd-clonesetup',
|
||||
'sources' : systemd_clonesetup_sources,
|
||||
'export' : files('clonesetup-util.c'),
|
||||
},
|
||||
generator_template + {
|
||||
'name' : 'systemd-clonesetup-generator',
|
||||
'sources' : files('clonesetup-generator.c'),
|
||||
'import' : ['systemd-clonesetup'],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
integration_tests += [
|
||||
integration_test_template + {
|
||||
'name' : fs.name(meson.current_source_dir()),
|
||||
'vm' : true,
|
||||
},
|
||||
]
|
||||
@@ -105,6 +105,7 @@ foreach dirname : [
|
||||
'TEST-90-RESTRICT-FSACCESS',
|
||||
'TEST-91-LIVEUPDATE',
|
||||
'TEST-92-TPM2-SWTPM',
|
||||
'TEST-93-CLONESETUP',
|
||||
]
|
||||
subdir(dirname)
|
||||
endforeach
|
||||
|
||||
Executable
+344
@@ -0,0 +1,344 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
set -eux
|
||||
set -o pipefail
|
||||
set -E
|
||||
|
||||
# shellcheck source=test/units/test-control.sh
|
||||
. "$(dirname "$0")"/test-control.sh
|
||||
|
||||
CLONESETUP_BIN=/usr/lib/systemd/systemd-clonesetup
|
||||
# Test clonesetup generator and systemd-clonesetup
|
||||
# Disable pager usage so interactive/manual runs do not block in `less`.
|
||||
export SYSTEMD_PAGER=cat
|
||||
export SYSTEMD_LESS=
|
||||
|
||||
create_loop_triplet() {
|
||||
local workdir="${1:?}"
|
||||
local src_img dst_img meta_img
|
||||
local loop_src loop_dst loop_meta
|
||||
|
||||
src_img="$workdir/source.img"
|
||||
dst_img="$workdir/dest.img"
|
||||
meta_img="$workdir/meta.img"
|
||||
|
||||
truncate -s 32M "$src_img"
|
||||
truncate -s 32M "$dst_img"
|
||||
truncate -s 8M "$meta_img"
|
||||
|
||||
loop_src="$(losetup --show --find "$src_img")"
|
||||
loop_dst="$(losetup --show --find "$dst_img")"
|
||||
loop_meta="$(losetup --show --find "$meta_img")"
|
||||
|
||||
# Wait for udev to process new loop devices before they are consumed.
|
||||
udevadm settle --timeout=60
|
||||
|
||||
printf '%s %s %s\n' "$loop_src" "$loop_dst" "$loop_meta"
|
||||
}
|
||||
|
||||
cleanup_loop_triplet() {
|
||||
local loop_src="${1:-}"
|
||||
local loop_dst="${2:-}"
|
||||
local loop_meta="${3:-}"
|
||||
|
||||
[[ -n "$loop_src" ]] && losetup -d "$loop_src"
|
||||
[[ -n "$loop_dst" ]] && losetup -d "$loop_dst"
|
||||
[[ -n "$loop_meta" ]] && losetup -d "$loop_meta"
|
||||
}
|
||||
|
||||
at_exit() {
|
||||
set +e
|
||||
|
||||
rm -f /etc/clonetab
|
||||
[[ -e /tmp/clonetab.bak ]] && cp -fv /tmp/clonetab.bak /etc/clonetab
|
||||
dmsetup remove testclonesetup 2>/dev/null || true
|
||||
|
||||
systemctl daemon-reload
|
||||
}
|
||||
|
||||
at_error() {
|
||||
local rc="$?"
|
||||
local source="${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}"
|
||||
local line="${BASH_LINENO[0]:-0}"
|
||||
local func="${FUNCNAME[1]:-main}"
|
||||
|
||||
echo "ERROR: rc=$rc at $source:$line ($func): $BASH_COMMAND" >&2
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
trap at_exit EXIT
|
||||
trap at_error ERR
|
||||
|
||||
clonesetup_start_and_check() {
|
||||
local volume unit
|
||||
|
||||
volume="${1:?}"
|
||||
unit="systemd-clonesetup@$volume.service"
|
||||
|
||||
# The unit existence check should always pass
|
||||
[[ "$(systemctl show -P LoadState "$unit")" == loaded ]]
|
||||
systemctl list-unit-files "$unit"
|
||||
|
||||
systemctl start "$unit"
|
||||
# wait for udev to create /dev/mapper/ node after DM device activation
|
||||
udevadm settle --timeout=10
|
||||
systemctl status "$unit"
|
||||
test -e "/dev/mapper/$volume"
|
||||
dmsetup status "$volume"
|
||||
|
||||
systemctl stop "$unit"
|
||||
# wait for udev to finish processing so the device node state is in sync
|
||||
# before the API returns.
|
||||
udevadm settle --timeout=10
|
||||
test ! -e "/dev/mapper/$volume"
|
||||
}
|
||||
|
||||
prereq() {
|
||||
# Skip when kernel lacks dm-clone (CONFIG_DM_CLONE)
|
||||
modprobe dm_clone 2>/dev/null || true
|
||||
if [[ ! -d /sys/module/dm_clone ]]; then
|
||||
echo "no dm-clone" >/skipped
|
||||
exit 77
|
||||
fi
|
||||
echo "Found required kernel module: dm_clone"
|
||||
}
|
||||
|
||||
prereq
|
||||
|
||||
# Backup existing clonetab if any
|
||||
[[ -e /etc/clonetab ]] && cp -fv /etc/clonetab /tmp/clonetab.bak
|
||||
|
||||
# Create test clonetab
|
||||
clonetab_create() {
|
||||
local loop_name="${1:?}"
|
||||
local loop_src="${2:?}"
|
||||
local loop_dst="${3:?}"
|
||||
local loop_meta="${4:?}"
|
||||
local loop_options="${5:?}"
|
||||
|
||||
cat >/etc/clonetab <<EOF
|
||||
# name source dest metadata options
|
||||
$loop_name $loop_src $loop_dst $loop_meta $loop_options
|
||||
EOF
|
||||
|
||||
}
|
||||
|
||||
# Verify --help and --version work
|
||||
testcase_help_version() {
|
||||
$CLONESETUP_BIN --help
|
||||
$CLONESETUP_BIN --version
|
||||
}
|
||||
|
||||
test_long_name() {
|
||||
local loop_src="${1:?}"
|
||||
local loop_dst="${2:?}"
|
||||
local loop_meta="${3:?}"
|
||||
|
||||
# Test device name too long — must fail cleanly with ENAMETOOLONG (no DM device created)
|
||||
local long_name
|
||||
long_name="$(printf 'a%.0s' {1..128})"
|
||||
if $CLONESETUP_BIN add "$long_name" "$loop_src" "$loop_dst" "$loop_meta" - ; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -e "/dev/mapper/$long_name" ]]; then
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_missing_etc_clonetab() {
|
||||
local loop_src="${1:?}"
|
||||
local loop_dst="${2:?}"
|
||||
local loop_meta="${3:?}"
|
||||
|
||||
# Remove clonetab and reload — generator must produce no units
|
||||
rm -f /etc/clonetab
|
||||
systemctl daemon-reload
|
||||
if systemctl is-enabled systemd-clonesetup@testclonesetup.service; then
|
||||
return 1
|
||||
fi
|
||||
clonetab_create "testclonesetup" "$loop_src" "$loop_dst" "$loop_meta" "-"
|
||||
systemctl daemon-reload
|
||||
}
|
||||
|
||||
testcase_idempotent_stop() {
|
||||
# Removing a non-existent device must succeed (ENXIO treated as already-inactive)
|
||||
$CLONESETUP_BIN remove testclonesetup-nonexistent
|
||||
}
|
||||
|
||||
test_no_device_leak() {
|
||||
local loop_dst="${1:?}"
|
||||
local loop_meta="${2:?}"
|
||||
|
||||
# Bad source path — DM_TABLE_LOAD must fail and device must be cleaned up
|
||||
if $CLONESETUP_BIN add testleak /dev/nonexistent "$loop_dst" "$loop_meta" -; then
|
||||
return 1
|
||||
fi
|
||||
if dmsetup info testleak; then
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
testcase_region_size() {
|
||||
local region_workdir region_loop_src region_loop_dst region_loop_meta
|
||||
|
||||
# Use a dedicated loop triplet so metadata from previous test cases cannot affect
|
||||
# region-size validation.
|
||||
region_workdir="$(mktemp -d)"
|
||||
read -r region_loop_src region_loop_dst region_loop_meta < <(create_loop_triplet "$region_workdir")
|
||||
|
||||
# Custom region-size=8K — verify a non-default bytes value is accepted.
|
||||
$CLONESETUP_BIN add testregion "$region_loop_src" "$region_loop_dst" "$region_loop_meta" region-size=8K
|
||||
dmsetup info testregion
|
||||
$CLONESETUP_BIN remove testregion
|
||||
|
||||
cleanup_loop_triplet "$region_loop_src" "$region_loop_dst" "$region_loop_meta"
|
||||
rm -rf "$region_workdir"
|
||||
}
|
||||
|
||||
testcase_main_flow() {
|
||||
local workdir loop_src loop_dst loop_meta
|
||||
|
||||
workdir="$(mktemp -d)"
|
||||
read -r loop_src loop_dst loop_meta < <(create_loop_triplet "$workdir")
|
||||
|
||||
clonetab_create "testclonesetup" "$loop_src" "$loop_dst" "$loop_meta" "-"
|
||||
|
||||
# Run the generator explicitly for coverage
|
||||
mkdir -p /tmp/clonesetup-generator.out
|
||||
/usr/lib/systemd/system-generators/systemd-clonesetup-generator /tmp/clonesetup-generator.out/ /tmp/clonesetup-generator.out/ /tmp/clonesetup-generator.out/
|
||||
|
||||
# Verify generator output
|
||||
test -f /tmp/clonesetup-generator.out/systemd-clonesetup@testclonesetup.service
|
||||
test -d /tmp/clonesetup-generator.out/clonesetup.target.requires
|
||||
test -d /tmp/clonesetup-generator.out/dev-mapper-testclonesetup.device.requires
|
||||
test -f /tmp/clonesetup-generator.out/dev-mapper-testclonesetup.device.d/40-device-timeout.conf
|
||||
|
||||
# Reload systemd to pick up generated units
|
||||
systemctl daemon-reload
|
||||
systemctl list-unit-files "systemd-clonesetup@*"
|
||||
|
||||
# Check clonesetup.target exists
|
||||
systemctl show clonesetup.target
|
||||
|
||||
# Test clonesetup service
|
||||
clonesetup_start_and_check testclonesetup
|
||||
|
||||
test_long_name "$loop_src" "$loop_dst" "$loop_meta"
|
||||
test_missing_etc_clonetab "$loop_src" "$loop_dst" "$loop_meta"
|
||||
test_no_device_leak "$loop_dst" "$loop_meta"
|
||||
cleanup_loop_triplet "$loop_src" "$loop_dst" "$loop_meta"
|
||||
rm -rf "$workdir"
|
||||
}
|
||||
|
||||
testcase_escape_coverage() {
|
||||
local workdir loop_src loop_dst loop_meta
|
||||
local new_src new_dst new_meta
|
||||
|
||||
workdir="$(mktemp -d)"
|
||||
read -r loop_src loop_dst loop_meta < <(create_loop_triplet "$workdir")
|
||||
|
||||
new_src="${loop_src}%foo"
|
||||
new_dst="${loop_dst}%bar"
|
||||
new_meta="${loop_meta}%baz"
|
||||
clonetab_create "testescapepaths" "$new_src" "$new_dst" "$new_meta" "-"
|
||||
|
||||
# Run the generator explicitly for coverage
|
||||
rm -rf /tmp/clonesetup-generator.out
|
||||
mkdir -p /tmp/clonesetup-generator.out
|
||||
/usr/lib/systemd/system-generators/systemd-clonesetup-generator /tmp/clonesetup-generator.out/ /tmp/clonesetup-generator.out/ /tmp/clonesetup-generator.out/
|
||||
|
||||
unit=/tmp/clonesetup-generator.out/systemd-clonesetup@testescapepaths.service
|
||||
test -f "$unit"
|
||||
exp_src="${new_src//%/%%}"
|
||||
exp_dst="${new_dst//%/%%}"
|
||||
exp_meta="${new_meta//%/%%}"
|
||||
grep -F "$exp_src" "$unit"
|
||||
grep -F "$exp_dst" "$unit"
|
||||
grep -F "$exp_meta" "$unit"
|
||||
|
||||
cleanup_loop_triplet "$loop_src" "$loop_dst" "$loop_meta"
|
||||
rm -rf "$workdir"
|
||||
}
|
||||
|
||||
testcase_invalid_path_rejected() {
|
||||
# Space in a device path token should fail parsing/validation and produce no unit.
|
||||
cat >/etc/clonetab <<EOF
|
||||
# name source dest metadata options
|
||||
testinvalidspace /dev/loop0 /dev/with\ space /dev/loop2 -
|
||||
EOF
|
||||
|
||||
rm -rf /tmp/clonesetup-generator.out
|
||||
mkdir -p /tmp/clonesetup-generator.out
|
||||
/usr/lib/systemd/system-generators/systemd-clonesetup-generator /tmp/clonesetup-generator.out/ /tmp/clonesetup-generator.out/ /tmp/clonesetup-generator.out/
|
||||
|
||||
test ! -f /tmp/clonesetup-generator.out/systemd-clonesetup@testinvalidspace.service
|
||||
}
|
||||
|
||||
testcase_invalid_options_rejected() {
|
||||
# Double-quote tokens in options should be rejected and produce no unit.
|
||||
cat >/etc/clonetab <<EOF
|
||||
# name source dest metadata options
|
||||
testinvalidopts /dev/loop0 /dev/loop1 /dev/loop2 ""
|
||||
EOF
|
||||
|
||||
rm -rf /tmp/clonesetup-generator.out
|
||||
mkdir -p /tmp/clonesetup-generator.out
|
||||
/usr/lib/systemd/system-generators/systemd-clonesetup-generator /tmp/clonesetup-generator.out/ /tmp/clonesetup-generator.out/ /tmp/clonesetup-generator.out/
|
||||
|
||||
test ! -f /tmp/clonesetup-generator.out/systemd-clonesetup@testinvalidopts.service
|
||||
}
|
||||
|
||||
testcase_backslash() {
|
||||
# Backslash in a device path token should fail parsing/validation and produce no unit.
|
||||
cat >/etc/clonetab <<EOF
|
||||
# name source dest metadata options
|
||||
testbackslash /dev/\x2e\x2e/foo /dev/loop1 /dev/loop2 -
|
||||
EOF
|
||||
|
||||
rm -rf /tmp/clonesetup-generator.out
|
||||
mkdir -p /tmp/clonesetup-generator.out
|
||||
/usr/lib/systemd/system-generators/systemd-clonesetup-generator /tmp/clonesetup-generator.out/ /tmp/clonesetup-generator.out/ /tmp/clonesetup-generator.out/
|
||||
|
||||
test ! -f /tmp/clonesetup-generator.out/systemd-clonesetup@testbackslash.service
|
||||
}
|
||||
|
||||
testcase_dot_prefix_name() {
|
||||
# when clone name starts with dot(.), generator produces unit names that match what pid1 actually
|
||||
# creates. the generator should NOT produce dev-mapper-\x2efoo.device — a unit that never exists.
|
||||
cat >/etc/clonetab <<EOF
|
||||
# name source dest metadata options
|
||||
.testdot /dev/loop0 /dev/loop1 /dev/loop2 -
|
||||
EOF
|
||||
|
||||
rm -rf /tmp/clonesetup-generator.out
|
||||
mkdir -p /tmp/clonesetup-generator.out
|
||||
/usr/lib/systemd/system-generators/systemd-clonesetup-generator /tmp/clonesetup-generator.out/ /tmp/clonesetup-generator.out/ /tmp/clonesetup-generator.out/
|
||||
|
||||
test -d /tmp/clonesetup-generator.out/dev-mapper-.testdot.device.requires
|
||||
test ! -d /tmp/clonesetup-generator.out/dev-mapper-\\x2etestdot.device.requires
|
||||
grep 'blockdev@dev-mapper-.testdot.target' /tmp/clonesetup-generator.out/systemd-clonesetup@\\x2etestdot.service
|
||||
}
|
||||
|
||||
testcase_omit_options() {
|
||||
# Omitting the options field entirely (4-column clonetab) must work the same as passing -.
|
||||
# This tests that the service starts successfully — not just that a unit file is generated.
|
||||
local workdir loop_src loop_dst loop_meta
|
||||
|
||||
workdir="$(mktemp -d)"
|
||||
read -r loop_src loop_dst loop_meta < <(create_loop_triplet "$workdir")
|
||||
|
||||
cat >/etc/clonetab <<EOF
|
||||
# name source dest metadata (no options column)
|
||||
testnooptions $loop_src $loop_dst $loop_meta
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
clonesetup_start_and_check testnooptions
|
||||
|
||||
cleanup_loop_triplet "$loop_src" "$loop_dst" "$loop_meta"
|
||||
rm -rf "$workdir"
|
||||
}
|
||||
run_testcases
|
||||
|
||||
touch /testok
|
||||
@@ -0,0 +1,12 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# This file is part of systemd.
|
||||
#
|
||||
# systemd is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation; either version 2.1 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
[Unit]
|
||||
Description=Local dm-clone Devices
|
||||
Documentation=man:clonetab(5) man:systemd-clonesetup(8) man:systemd-clonesetup-generator(8)
|
||||
@@ -13,6 +13,10 @@ units = [
|
||||
{ 'file' : 'breakpoint-pre-udev.service.in' },
|
||||
{ 'file' : 'capsule@.service.in' },
|
||||
{ 'file' : 'capsule.slice' },
|
||||
{
|
||||
'file' : 'clonesetup.target',
|
||||
'symlinks' : ['sysinit.target.wants/'],
|
||||
},
|
||||
{ 'file' : 'console-getty.service.in' },
|
||||
{ 'file' : 'container-getty@.service.in' },
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user