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:
Simran Singh
2026-07-10 14:01:24 +02:00
committed by Lennart Poettering
parent c0c5ba9662
commit 104970a8bd
18 changed files with 1400 additions and 0 deletions
+4
View File
@@ -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).
+121
View File
@@ -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>
+6
View File
@@ -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'],
+50
View File
@@ -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>
+76
View File
@@ -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>
+2
View File
@@ -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')
+226
View File
@@ -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);
+276
View File
@@ -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;
}
+16
View File
@@ -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);
+37
View File
@@ -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;
}
+6
View File
@@ -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);
+192
View File
@@ -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, &region_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);
+19
View File
@@ -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,
},
]
+1
View File
@@ -105,6 +105,7 @@ foreach dirname : [
'TEST-90-RESTRICT-FSACCESS',
'TEST-91-LIVEUPDATE',
'TEST-92-TPM2-SWTPM',
'TEST-93-CLONESETUP',
]
subdir(dirname)
endforeach
+344
View File
@@ -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
+12
View File
@@ -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)
+4
View File
@@ -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' },
{