Skip to content

Commit 2a38b51

Browse files
committed
add clone generator and test case
1 parent ef8a9f1 commit 2a38b51

8 files changed

Lines changed: 363 additions & 0 deletions

File tree

src/basic/special.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
#define SPECIAL_PCRFS_ROOT_SERVICE "systemd-pcrfs-root.service"
9898
#define SPECIAL_VALIDATEFS_SERVICE "systemd-validatefs@.service"
9999
#define SPECIAL_HIBERNATE_RESUME_SERVICE "systemd-hibernate-resume.service"
100+
#define SPECIAL_CLONE_TARGET "clone.target"
100101

101102
/* Services systemd relies on */
102103
#define SPECIAL_DBUS_SERVICE "dbus.service"

src/clone/clone-generator.c

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2+
3+
#include <stdlib.h>
4+
#include <sys/stat.h>
5+
6+
#include "alloc-util.h"
7+
#include "errno-util.h"
8+
#include "dropin.h"
9+
#include "escape.h"
10+
#include "fd-util.h"
11+
#include "fileio.h"
12+
#include "generator.h"
13+
#include "hashmap.h"
14+
#include "log.h"
15+
#include "path-util.h"
16+
#include "special.h"
17+
#include "string-util.h"
18+
#include "unit-name.h"
19+
20+
typedef struct clone_device {
21+
char *uuid;
22+
char *datadev;
23+
char *name;
24+
char *options;
25+
bool clone;
26+
} clone_device;
27+
28+
static const char *arg_dest = NULL;
29+
static const char *arg_runtime_directory = NULL;
30+
static Hashmap *arg_disks = NULL;
31+
32+
static clone_device* clone_device_free(clone_device *d) {
33+
if (!d)
34+
return NULL;
35+
36+
free(d->uuid);
37+
free(d->datadev);
38+
free(d->name);
39+
free(d->options);
40+
return mfree(d);
41+
}
42+
43+
static int generate_clone_units(const char *clone_name, const char *source_dev, const char *dest_dev,
44+
const char *metadata_dev, const char *options) {
45+
46+
/* unit files for each device */
47+
_cleanup_fclose_ FILE *f = NULL;
48+
_cleanup_free_ char *source_unit = NULL, *dest_unit = NULL, *metadata_unit = NULL;
49+
_cleanup_free_ char *escaped_source = NULL, *escaped_dest = NULL, *escaped_metadata = NULL;
50+
_cleanup_free_ char *e = NULL, *unit = NULL;
51+
_cleanup_free_ char *clone_dev_path = NULL;
52+
const char *dmname;
53+
int r;
54+
55+
assert(clone_name);
56+
assert(source_dev);
57+
assert(dest_dev);
58+
assert(metadata_dev);
59+
60+
/* create clone_dev_path that holds path for new cloned device */
61+
clone_dev_path = path_join("/dev/mapper", clone_name);
62+
if (!clone_dev_path)
63+
return log_oom();
64+
65+
/* escape clone name */
66+
e = unit_name_escape(clone_name);
67+
if (!e)
68+
return log_oom();
69+
70+
/* Generate unit name for the clone service */
71+
r = unit_name_build("systemd-clone", e, ".service", &unit);
72+
if (r < 0)
73+
return log_error_errno(r, "Failed to generate unit name: %m");
74+
75+
log_info("unit name=%s", unit);
76+
77+
/* Generate unit names for dependencies */
78+
r = unit_name_from_path(source_dev, ".device", &source_unit);
79+
if (r < 0)
80+
return log_error_errno(r, "Failed to generate source device unit name: %m");
81+
82+
r = unit_name_from_path(dest_dev, ".device", &dest_unit);
83+
if (r < 0)
84+
return log_error_errno(r, "Failed to generate dest device unit name: %m");
85+
86+
r = unit_name_from_path(metadata_dev, ".device", &metadata_unit);
87+
if (r < 0)
88+
return log_error_errno(r, "Failed to generate metadata device unit name: %m");
89+
90+
/* Escape device paths for ExecStart command */
91+
escaped_source = cescape(source_dev);
92+
if (!escaped_source)
93+
return log_oom();
94+
95+
escaped_dest = cescape(dest_dev);
96+
if (!escaped_dest)
97+
return log_oom();
98+
99+
escaped_metadata = cescape(metadata_dev);
100+
if (!escaped_metadata)
101+
return log_oom();
102+
103+
r = generator_open_unit_file(arg_dest, /* source = */ NULL, unit, &f);
104+
if (r < 0)
105+
return r;
106+
107+
/* Check if we're using loop devices and add setup if needed */
108+
bool setup_loop = startswith(source_dev, "/dev/loop") && startswith(dest_dev, "/dev/loop") && startswith(metadata_dev, "/dev/loop");
109+
110+
fprintf(f,
111+
"[Unit]\n"
112+
"Description=Create dm-clone device %s\n"
113+
"Documentation=man:dmsetup(8) man:fstab(5) man:systemd-fstab-generator(8)\n"
114+
"DefaultDependencies=no\n",
115+
clone_dev_path);
116+
117+
if (!setup_loop) {
118+
fprintf(f,
119+
"BindsTo=%s %s %s\n"
120+
"Requires=%s %s %s\n"
121+
"After=%s %s %s\n",
122+
source_unit, dest_unit, metadata_unit,
123+
source_unit, dest_unit, metadata_unit,
124+
source_unit, dest_unit, metadata_unit);
125+
}
126+
127+
fprintf(f,
128+
"Before=blockdev@dev-mapper-%s.target\n"
129+
"Wants=blockdev@dev-mapper-%s.target\n"
130+
"Conflicts=shutdown.target\n"
131+
"\n"
132+
"[Service]\n"
133+
"Type=oneshot\n"
134+
"RemainAfterExit=yes\n",
135+
e, e);
136+
137+
if (setup_loop)
138+
fprintf(f, "ExecStartPre=/usr/share/script.sh\n");
139+
140+
fprintf(f,
141+
"ExecStart=" SYSTEMD_CLONE_PATH " add '%s' '%s' '%s' '%s' '%s'\n"
142+
"ExecStop=" SYSTEMD_CLONE_PATH " remove %s\n"
143+
"TimeoutSec=0\n",
144+
clone_name, escaped_source, escaped_dest, escaped_metadata, "",
145+
clone_name);
146+
147+
r = fflush_and_check(f);
148+
if (r < 0)
149+
return log_error_errno(r, "Failed to write unit %s: %m", unit);
150+
151+
// symlink unit file to enable it
152+
dmname = strjoina("dev-mapper-", e, ".device");
153+
r = generator_add_symlink(arg_dest, dmname, "requires", unit);
154+
if (r < 0)
155+
return r;
156+
157+
/* Extend device timeout to allow clone service to complete */
158+
r = write_drop_in(arg_dest, dmname, 40, "device-timeout",
159+
"# Automatically generated by systemd-clone-generator\n\n"
160+
"[Unit]\n"
161+
"JobTimeoutSec=infinity\n");
162+
if (r < 0)
163+
log_warning_errno(r, "Failed to write device timeout drop-in: %m");
164+
165+
/* Add to clone.target so it starts at boot */
166+
r = generator_add_symlink(arg_dest, SPECIAL_CLONE_TARGET, "requires", unit);
167+
if (r < 0)
168+
return r;
169+
170+
return 0;
171+
}
172+
173+
static int add_clone_devices(void) {
174+
_cleanup_fclose_ FILE *f = NULL;
175+
unsigned clone_line = 0;
176+
int r, ret = 0;
177+
const char *fname;
178+
179+
fname = getenv("SYSTEMD_CLONETAB") ?: "/etc/clonetab";
180+
181+
log_info("Parsing %s", fname);
182+
r = fopen_unlocked(fname, "re", &f);
183+
if (r < 0) {
184+
if (errno != ENOENT)
185+
log_error_errno(errno, "Failed to open %s: %m", fname);
186+
return 0;
187+
}
188+
189+
for (;;) {
190+
_cleanup_free_ char *line = NULL, *src = NULL, *name = NULL, *dst = NULL, *meta = NULL, *options = NULL;
191+
int k;
192+
193+
r = read_stripped_line(f, LONG_LINE_MAX, &line);
194+
if (r < 0)
195+
return log_error_errno(r, "Failed to read %s: %m", fname);
196+
if (r == 0)
197+
break;
198+
199+
clone_line++;
200+
201+
if (IN_SET(line[0], 0, '#'))
202+
continue;
203+
204+
k = sscanf(line, "%ms %ms %ms %ms %ms", &name, &src, &dst, &meta, &options);
205+
if (k < 4 || k > 5) {
206+
log_error("Failed to parse %s:%u, ignoring.", fname, clone_line);
207+
continue;
208+
}
209+
210+
RET_GATHER(ret, generate_clone_units(name, src, dst, meta, options));
211+
}
212+
213+
return ret;
214+
}
215+
216+
DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(clone_device_hash_ops, char, string_hash_func, string_compare_func,
217+
clone_device, clone_device_free);
218+
219+
220+
static int run(const char *dest, const char *dest_early, const char *dest_late) {
221+
int r;
222+
223+
// dest usually is /run/systemd/generator
224+
assert_se(arg_dest = dest);
225+
226+
arg_runtime_directory = getenv("RUNTIME_DIRECTORY") ?: "/run/systemd/dev-clone";
227+
228+
arg_disks = hashmap_new(&clone_device_hash_ops);
229+
if (!arg_disks)
230+
return log_oom();
231+
232+
r = add_clone_devices();
233+
return r;
234+
}
235+
236+
DEFINE_MAIN_GENERATOR_FUNCTION(run);

src/clone/meson.build

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,8 @@ executables += [
1111
'public' : true,
1212
'sources' : systemd_clone_sources,
1313
},
14+
generator_template + {
15+
'name' : 'systemd-clone-generator',
16+
'sources' : files('clone-generator.c'),
17+
},
1418
]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# SPDX-License-Identifier: LGPL-2.1-or-later
2+
3+
integration_tests += [
4+
integration_test_template + {
5+
'name' : fs.name(meson.current_source_dir()),
6+
'vm' : true,
7+
},
8+
]

test/integration-tests/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ foreach dirname : [
102102
'TEST-87-AUX-UTILS-VM',
103103
'TEST-88-UPGRADE',
104104
'TEST-89-RESOLVED-MDNS',
105+
'TEST-90-CLONE',
105106
]
106107
subdir(dirname)
107108
endforeach

test/units/TEST-90-CLONE.sh

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: LGPL-2.1-or-later
3+
set -eux
4+
set -o pipefail
5+
6+
# Test dm-clone generator and systemd-clone
7+
8+
at_exit() {
9+
set +e
10+
11+
rm -f /etc/clonetab
12+
[[ -e /tmp/clonetab.bak ]] && cp -fv /tmp/clonetab.bak /etc/clonetab
13+
[[ -n "${LOOP_SRC:-}" ]] && losetup -d "$LOOP_SRC"
14+
[[ -n "${LOOP_DST:-}" ]] && losetup -d "$LOOP_DST"
15+
[[ -n "${LOOP_META:-}" ]] && losetup -d "$LOOP_META"
16+
[[ -n "${WORKDIR:-}" ]] && rm -rf "$WORKDIR"
17+
dmsetup remove testclone 2>/dev/null || true
18+
19+
systemctl daemon-reload
20+
}
21+
22+
trap at_exit EXIT
23+
24+
clone_start_and_check() {
25+
local volume unit
26+
27+
volume="${1:?}"
28+
unit="systemd-clone@$volume.service"
29+
30+
# The unit existence check should always pass
31+
[[ "$(systemctl show -P LoadState "$unit")" == loaded ]]
32+
systemctl list-unit-files "$unit"
33+
34+
systemctl start "$unit"
35+
systemctl status "$unit"
36+
test -e "/dev/mapper/$volume"
37+
dmsetup status "$volume"
38+
39+
systemctl stop "$unit"
40+
# Upstream libdevmapper (LVM2) uses dm_udev_wait() (and cookie APIs like
41+
# udevcreatecookie/udevreleasecookie): after a DM operation (create/remove),
42+
# it waits for udev to finish processing so the device node state is in sync
43+
# before the API returns. So "wait for udev" is the standard approach there.
44+
udevadm settle --timeout=10
45+
test ! -e "/dev/mapper/$volume"
46+
}
47+
48+
# Use a common workdir
49+
WORKDIR="$(mktemp -d)"
50+
51+
# Create test images for source, destination, and metadata
52+
IMG_SRC="$WORKDIR/source.img"
53+
IMG_DST="$WORKDIR/dest.img"
54+
IMG_META="$WORKDIR/meta.img"
55+
56+
truncate -s 32M "$IMG_SRC"
57+
truncate -s 32M "$IMG_DST"
58+
truncate -s 8M "$IMG_META"
59+
60+
# Set up loop devices
61+
LOOP_SRC="$(losetup --show --find "$IMG_SRC")"
62+
LOOP_DST="$(losetup --show --find "$IMG_DST")"
63+
LOOP_META="$(losetup --show --find "$IMG_META")"
64+
65+
udevadm settle --timeout=60
66+
67+
# Backup existing clonetab if any
68+
[[ -e /etc/clonetab ]] && cp -fv /etc/clonetab /tmp/clonetab.bak
69+
70+
# Create test clonetab
71+
cat >/etc/clonetab <<EOF
72+
# name source dest metadata options
73+
testclone $LOOP_SRC $LOOP_DST $LOOP_META ""
74+
EOF
75+
76+
# Run the generator explicitly for coverage
77+
mkdir -p /tmp/clone-generator.out
78+
/usr/lib/systemd/system-generators/systemd-clone-generator /tmp/clone-generator.out/ /tmp/clone-generator.out/ /tmp/clone-generator.out/
79+
80+
# Verify generator output
81+
test -f /tmp/clone-generator.out/systemd-clone@testclone.service
82+
test -d /tmp/clone-generator.out/clone.target.requires
83+
test -d /tmp/clone-generator.out/dev-mapper-testclone.device.requires
84+
test -f /tmp/clone-generator.out/dev-mapper-testclone.device.d/40-device-timeout.conf
85+
86+
# Reload systemd to pick up generated units
87+
systemctl daemon-reload
88+
systemctl list-unit-files "systemd-clone@*"
89+
90+
# Check clone.target exists
91+
systemctl show clone.target
92+
93+
# Test clone service
94+
clone_start_and_check testclone
95+
96+
touch /testok
97+

units/clone.target

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# SPDX-License-Identifier: LGPL-2.1-or-later
2+
#
3+
# This file is part of systemd.
4+
#
5+
# systemd is free software; you can redistribute it and/or modify it
6+
# under the terms of the GNU Lesser General Public License as published by
7+
# the Free Software Foundation; either version 2.1 of the License, or
8+
# (at your option) any later version.
9+
10+
[Unit]
11+
Description=Local dm-clone Devices
12+
Documentation=man:systemd.special(7)

units/meson.build

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ units = [
2424
'conditions' : ['HAVE_LIBCRYPTSETUP'],
2525
'symlinks' : ['sysinit.target.wants/'],
2626
},
27+
{
28+
'file' : 'clone.target',
29+
'symlinks' : ['sysinit.target.wants/'],
30+
},
2731
{ 'file' : 'debug-shell.service.in' },
2832
{
2933
'file' : 'dev-hugepages.mount',

0 commit comments

Comments
 (0)