repart: Add BtrfsReplace= (#41109)

This is a series of commits which adds a feature needed by GNOME OS'
installer. This was show during All Systems Go 2025 talk:
https://cfp.all-systems-go.io/all-systems-go-2025/talk/QRJVL3/

To sum up this PR, this changes first systemd-repart to use BLKPG
partition instead of loop devices when possible. We need then to always
rescan the partitions to try remove partitions if it failed. We allow
encrypted partitions to stay activated and with a chosen name. And we
add a new partition configuration `BtrfsReplace=`.

Note that "replace" comes from the command `btrfs replace`. But in the
case of systemd-repart, maybe "inplace" or "move" would make more sense.
I open to suggestions.

If it is better I can split this into several PRs.

The commits:

## repart: Reuse the backing fd for fdisk

Because fdisk_assign_device tries to open block devices with O_EXCL,
when it does it blocks cryptsetup from using partition block devices for
the same disk.

Since we already have a file descriptor for the device, we can just
share it and use fdisk_assign_device_by_fd instead.

## repart: Use blkpg partitions instead of loop devices when possible

We will want to allow future features to keep some devices mounted or
active. So in order to avoid leaving a mess of many loop devices, we can
just already use the partition block device already.

## repart: Rescan disk on failure if we create blkpg partitions on the
fly

Since we did not write the partition table, then the created partitions
should get removed on error.

## repart: Allow keeping luks2 volumes opened

## repart: Add BtrfsReplace=

BtrfsReplace=/mntpnt will move the btrfs filesystem from mount point to
the partition created. After moving, it will resize to take the whole
partition.
This is useful for OS installers that move a live system into a disk and
do not require a reboot.

## repart: Add VolumeName=

When a luks2 device mapper is to be kept alive after execution of
systemd-cryptsetup, the name of the volume will be taken from this
value.

## test: Add test for repart's BtrfsReplace
This commit is contained in:
Daan De Meyer
2026-05-12 22:24:51 +02:00
committed by GitHub
9 changed files with 699 additions and 91 deletions
+73 -4
View File
@@ -23,6 +23,7 @@
#include "rm-rf.h"
#include "sparse-endian.h"
#include "stat-util.h"
#include "stdio-util.h"
#include "string-util.h"
#include "time-util.h"
@@ -95,14 +96,20 @@ int btrfs_subvol_get_read_only_fd(int fd) {
return !!(flags & BTRFS_SUBVOL_RDONLY);
}
int btrfs_get_block_device_at(int dir_fd, const char *path, dev_t *ret) {
int btrfs_get_block_device_at_full(int dir_fd, const char *path, uint64_t *ret_devid, char **ret_path, dev_t *ret) {
struct btrfs_ioctl_fs_info_args fsi = {};
_cleanup_close_ int fd = -EBADF;
uint64_t id;
int r;
/*
* Returns:
* ret_devid - the device id in the filesystem for the returned block device
* ret_path - the path to the returned block device
* ret - the returned block device
*/
assert(dir_fd >= 0 || IN_SET(dir_fd, AT_FDCWD, XAT_FDROOT));
assert(ret);
fd = xopenat(dir_fd, path, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
if (fd < 0)
@@ -119,7 +126,12 @@ int btrfs_get_block_device_at(int dir_fd, const char *path, dev_t *ret) {
/* We won't do this for btrfs RAID */
if (fsi.num_devices != 1) {
*ret = 0;
if (ret_devid)
*ret_devid = 0;
if (ret_path)
*ret_path = NULL;
if (ret)
*ret = 0;
return 0;
}
@@ -128,6 +140,7 @@ int btrfs_get_block_device_at(int dir_fd, const char *path, dev_t *ret) {
.devid = id,
};
struct stat st;
_cleanup_free_ char *device_path = NULL;
if (ioctl(fd, BTRFS_IOC_DEV_INFO, &di) < 0) {
if (errno == ENODEV)
@@ -155,7 +168,16 @@ int btrfs_get_block_device_at(int dir_fd, const char *path, dev_t *ret) {
if (major(st.st_rdev) == 0)
return -ENODEV;
*ret = st.st_rdev;
device_path = strdup((char*) di.path);
if (!device_path)
return -ENOMEM;
if (ret_path)
*ret_path = TAKE_PTR(device_path);
if (ret)
*ret = st.st_rdev;
if (ret_devid)
*ret_devid = id;
return 1;
}
@@ -2122,3 +2144,50 @@ int btrfs_get_file_physical_offset_fd(int fd, uint64_t *ret) {
return -ENODATA;
}
int btrfs_replace(int fdmntpnt, uint64_t device_id, const char *target) {
struct btrfs_ioctl_dev_replace_args replace = {
.cmd = BTRFS_IOCTL_DEV_REPLACE_CMD_START,
.result = UINT64_MAX,
.start = {
.srcdevid = device_id,
.cont_reading_from_srcdev_mode = BTRFS_IOCTL_DEV_REPLACE_CONT_READING_FROM_SRCDEV_MODE_ALWAYS,
},
};
assert(fdmntpnt >= 0);
assert(target);
if (strlen(target) >= sizeof(replace.start.tgtdev_name))
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Path to the btrfs replace target is too long");
strncpy((char *)replace.start.tgtdev_name, target, sizeof(replace.start.tgtdev_name));
if (ioctl(fdmntpnt, BTRFS_IOC_DEV_REPLACE, &replace) < 0)
return -errno;
switch (replace.result) {
case BTRFS_IOCTL_DEV_REPLACE_RESULT_NO_ERROR:
break;
case BTRFS_IOCTL_DEV_REPLACE_RESULT_NOT_STARTED:
return log_debug_errno(SYNTHETIC_ERRNO(ECANCELED), "btrfs replace was not started");
case BTRFS_IOCTL_DEV_REPLACE_RESULT_ALREADY_STARTED:
return log_debug_errno(SYNTHETIC_ERRNO(EALREADY), "btrfs replace was already started on this device");
case BTRFS_IOCTL_DEV_REPLACE_RESULT_SCRUB_INPROGRESS:
return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), "btrfs scrub is in progress");
default:
return log_debug_errno(SYNTHETIC_ERRNO(EIO), "An unknown btrfs error status occurred");
}
return 0;
}
int btrfs_resize_max(int fdmntpnt, uint64_t devid) {
struct btrfs_ioctl_vol_args args = {};
assert(fdmntpnt >= 0);
assert_cc(STRLEN(":max") + DECIMAL_STR_MAX(uint64_t) + 1 <= sizeof(args.name));
xsprintf(args.name, "%" PRIu64 ":max", devid);
return RET_NERRNO(ioctl(fdmntpnt, BTRFS_IOC_RESIZE, &args));
}
+7 -1
View File
@@ -49,7 +49,10 @@ static inline int btrfs_is_subvol(const char *path) {
return btrfs_is_subvol_at(AT_FDCWD, path);
}
int btrfs_get_block_device_at(int dir_fd, const char *path, dev_t *ret);
int btrfs_get_block_device_at_full(int dir_fd, const char *path, uint64_t *ret_devid, char **ret_path, dev_t *ret);
static inline int btrfs_get_block_device_at(int dir_fd, const char *path, dev_t *ret) {
return btrfs_get_block_device_at_full(dir_fd, path, NULL, NULL, ret);
}
static inline int btrfs_get_block_device(const char *path, dev_t *ret) {
return btrfs_get_block_device_at(AT_FDCWD, path, ret);
}
@@ -133,3 +136,6 @@ bool btrfs_might_be_subvol(const struct stat *st) _pure_;
int btrfs_forget_device(const char *path);
int btrfs_get_file_physical_offset_fd(int fd, uint64_t *ret);
int btrfs_replace(int fdmntpnt, uint64_t device_id, const char *target);
int btrfs_resize_max(int fdmntpnt, uint64_t devid);
+2
View File
@@ -23,6 +23,7 @@ DLSYM_PROTOTYPE(fdisk_apply_table) = NULL;
DLSYM_PROTOTYPE(fdisk_ask_get_type) = NULL;
DLSYM_PROTOTYPE(fdisk_ask_string_set_result) = NULL;
DLSYM_PROTOTYPE(fdisk_assign_device) = NULL;
DLSYM_PROTOTYPE(fdisk_assign_device_by_fd) = NULL;
DLSYM_PROTOTYPE(fdisk_create_disklabel) = NULL;
DLSYM_PROTOTYPE(fdisk_delete_partition) = NULL;
DLSYM_PROTOTYPE(fdisk_get_devfd) = NULL;
@@ -97,6 +98,7 @@ int dlopen_fdisk(int log_level) {
DLSYM_ARG(fdisk_ask_get_type),
DLSYM_ARG(fdisk_ask_string_set_result),
DLSYM_ARG(fdisk_assign_device),
DLSYM_ARG(fdisk_assign_device_by_fd),
DLSYM_ARG(fdisk_create_disklabel),
DLSYM_ARG(fdisk_delete_partition),
DLSYM_ARG(fdisk_get_devfd),
+1
View File
@@ -14,6 +14,7 @@ extern DLSYM_PROTOTYPE(fdisk_apply_table);
extern DLSYM_PROTOTYPE(fdisk_ask_get_type);
extern DLSYM_PROTOTYPE(fdisk_ask_string_set_result);
extern DLSYM_PROTOTYPE(fdisk_assign_device);
extern DLSYM_PROTOTYPE(fdisk_assign_device_by_fd);
extern DLSYM_PROTOTYPE(fdisk_create_disklabel);
extern DLSYM_PROTOTYPE(fdisk_delete_partition);
extern DLSYM_PROTOTYPE(fdisk_get_devfd);