diff --git a/man/repart.d.xml b/man/repart.d.xml
index 028929ac9e..598cdc2310 100644
--- a/man/repart.d.xml
+++ b/man/repart.d.xml
@@ -652,6 +652,24 @@
+
+ BlockDeviceReplace=
+
+ Takes a path to a mountpoint. Its filesystem's backing device will be replaced
+ with the newly created partition. This works only for btrfs filesystems. This option is ignored for already existing partitions.
+
+ If after moving the filesystem, an error happens before the writing of the partition table is
+ finished, there will be an attempt to move the filesystem back to its original device. But if that
+ attempt also fails, the filesystem might be living on a partition that does not exist in the
+ partition table and will be lost on reboot. This feature is intended to save initially volatile
+ mounted filesystems into the new disk with no important data, like a live boot system. They might
+ need to be recreated after a failure writing the partition table.
+
+ This option is not compatible with .
+
+
+
+
Encrypt=
diff --git a/src/repart/repart.c b/src/repart/repart.c
index a2958f3212..1620cc3530 100644
--- a/src/repart/repart.c
+++ b/src/repart/repart.c
@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include
+#include
#include
#include
#include
@@ -250,6 +251,7 @@ typedef enum ProgressPhase {
PROGRESS_LOADING_DEFINITIONS,
PROGRESS_LOADING_TABLE,
PROGRESS_OPENING_COPY_BLOCK_SOURCES,
+ PROGRESS_OPENING_BLOCK_DEVICE_REPLACE_SOURCES,
PROGRESS_ACQUIRING_PARTITION_LABELS,
PROGRESS_MINIMIZING,
PROGRESS_PLACING,
@@ -260,6 +262,7 @@ typedef enum ProgressPhase {
PROGRESS_ADJUSTING_PARTITION,
PROGRESS_WRITING_TABLE,
PROGRESS_REREADING_TABLE,
+ PROGRESS_REPLACING_DEVICE,
_PROGRESS_PHASE_MAX,
_PROGRESS_PHASE_INVALID = -EINVAL,
} ProgressPhase;
@@ -423,6 +426,27 @@ DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(subvolume_hash_ops, char, path_has
typedef struct Context Context;
+typedef struct {
+ uint64_t devid;
+ int mountpoint_fd;
+ int source_fd;
+ char *source_path;
+ uint64_t source_size;
+ bool done;
+} BtrfsReplacement;
+
+static BtrfsReplacement* btrfs_replacement_free(BtrfsReplacement *b) {
+ if (!b)
+ return NULL;
+
+ safe_close(b->mountpoint_fd);
+ safe_close(b->source_fd);
+ free(b->source_path);
+ return mfree(b);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(BtrfsReplacement*, btrfs_replacement_free);
+
typedef struct Partition {
Context *context;
@@ -468,6 +492,8 @@ typedef struct Partition {
uint64_t copy_blocks_done;
char *format;
+ char *block_device_replace;
+ BtrfsReplacement *btrfs_replaced;
char **exclude_files_source;
char **exclude_files_target;
char **make_directories;
@@ -630,19 +656,21 @@ static const char *minimize_mode_table[_MINIMIZE_MODE_MAX] = {
};
static const char *progress_phase_table[_PROGRESS_PHASE_MAX] = {
- [PROGRESS_LOADING_DEFINITIONS] = "loading-definitions",
- [PROGRESS_LOADING_TABLE] = "loading-table",
- [PROGRESS_OPENING_COPY_BLOCK_SOURCES] = "opening-copy-block-sources",
- [PROGRESS_ACQUIRING_PARTITION_LABELS] = "acquiring-partition-labels",
- [PROGRESS_MINIMIZING] = "minimizing",
- [PROGRESS_PLACING] = "placing",
- [PROGRESS_WIPING_DISK] = "wiping-disk",
- [PROGRESS_WIPING_PARTITION] = "wiping-partition",
- [PROGRESS_COPYING_PARTITION] = "copying-partition",
- [PROGRESS_FORMATTING_PARTITION] = "formatting-partition",
- [PROGRESS_ADJUSTING_PARTITION] = "adjusting-partition",
- [PROGRESS_WRITING_TABLE] = "writing-table",
- [PROGRESS_REREADING_TABLE] = "rereading-table",
+ [PROGRESS_LOADING_DEFINITIONS] = "loading-definitions",
+ [PROGRESS_LOADING_TABLE] = "loading-table",
+ [PROGRESS_OPENING_COPY_BLOCK_SOURCES] = "opening-copy-block-sources",
+ [PROGRESS_OPENING_BLOCK_DEVICE_REPLACE_SOURCES] = "opening-block-device-replace-sources",
+ [PROGRESS_ACQUIRING_PARTITION_LABELS] = "acquiring-partition-labels",
+ [PROGRESS_MINIMIZING] = "minimizing",
+ [PROGRESS_PLACING] = "placing",
+ [PROGRESS_WIPING_DISK] = "wiping-disk",
+ [PROGRESS_WIPING_PARTITION] = "wiping-partition",
+ [PROGRESS_COPYING_PARTITION] = "copying-partition",
+ [PROGRESS_FORMATTING_PARTITION] = "formatting-partition",
+ [PROGRESS_ADJUSTING_PARTITION] = "adjusting-partition",
+ [PROGRESS_WRITING_TABLE] = "writing-table",
+ [PROGRESS_REREADING_TABLE] = "rereading-table",
+ [PROGRESS_REPLACING_DEVICE] = "replacing-device",
};
static uint64_t determine_grain_size(uint64_t sector_size) {
@@ -807,6 +835,8 @@ static Partition* partition_free(Partition *p) {
safe_close(p->copy_blocks_fd);
free(p->format);
+ free(p->block_device_replace);
+ btrfs_replacement_free(p->btrfs_replaced);
strv_free(p->exclude_files_source);
strv_free(p->exclude_files_target);
strv_free(p->make_directories);
@@ -851,6 +881,8 @@ static void partition_foreignize(Partition *p) {
p->copy_blocks_root = NULL;
p->format = mfree(p->format);
+ p->block_device_replace = mfree(p->block_device_replace);
+ p->btrfs_replaced = btrfs_replacement_free(p->btrfs_replaced);
p->exclude_files_source = strv_free(p->exclude_files_source);
p->exclude_files_target = strv_free(p->exclude_files_target);
p->make_directories = strv_free(p->make_directories);
@@ -1161,6 +1193,8 @@ static uint64_t partition_min_size(const Context *context, const Partition *p) {
if (p->copy_blocks_size != UINT64_MAX)
assert_se(INC_SAFE(&d, round_up_size(p->copy_blocks_size, context->grain_size)));
+ else if (p->btrfs_replaced)
+ assert_se(INC_SAFE(&d, round_up_size(p->btrfs_replaced->source_size, context->grain_size)));
else if (p->format || p->encrypt != ENCRYPT_OFF) {
uint64_t f;
@@ -2924,6 +2958,7 @@ static int partition_read_definition(
{ "Partition", "AddValidateFS", config_parse_tristate, 0, &p->add_validatefs },
{ "Partition", "FileSystemSectorSize", config_parse_fs_sector_size, 0, &p->fs_sector_size },
{ "Partition", "Discard", config_parse_tristate, 0, &p->discard },
+ { "Partition", "BlockDeviceReplace", config_parse_path, 0, &p->block_device_replace },
{}
};
_cleanup_free_ char *filename = NULL;
@@ -2974,6 +3009,18 @@ static int partition_read_definition(
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"Format=/CopyFiles=/MakeDirectories=/MakeSymlinks= and CopyBlocks= cannot be combined, refusing.");
+ if (p->block_device_replace && (p->format || partition_needs_populate(p)))
+ return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+ "Format=/CopyFiles=/MakeDirectories=/MakeSymlinks= and BlockDeviceReplace= cannot be combined, refusing.");
+
+ if ((p->copy_blocks_path || p->copy_blocks_auto) && p->block_device_replace)
+ return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+ "CopyBlocks= and BlockDeviceReplace= cannot be combined, refusing.");
+
+ if (p->block_device_replace && arg_offline == 1)
+ return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
+ "BlockDeviceReplace= is incompatible with --offline=yes, refusing.");
+
if (partition_needs_populate(p) && streq_ptr(p->format, "swap"))
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"Format=swap and CopyFiles=/MakeDirectories=/MakeSymlinks= cannot be combined, refusing.");
@@ -2983,7 +3030,7 @@ static int partition_read_definition(
if (p->type.designator == PARTITION_SWAP)
format = "swap";
- else if (partition_needs_populate(p) || (p->encrypt != ENCRYPT_OFF && !(p->copy_blocks_path || p->copy_blocks_auto)))
+ else if (partition_needs_populate(p) || (p->encrypt != ENCRYPT_OFF && !(p->copy_blocks_path || p->copy_blocks_auto || p->block_device_replace)))
/* Pick "vfat" as file system for esp and xbootldr partitions, otherwise default to "ext4". */
format = IN_SET(p->type.designator, PARTITION_ESP, PARTITION_XBOOTLDR) ? "vfat" : "ext4";
@@ -7186,6 +7233,102 @@ static int finalize_extra_mkfs_options(const Partition *p, const char *root, cha
return 0;
}
+static int context_block_device_replace(Context *context) {
+ int r;
+
+ assert(context);
+
+ LIST_FOREACH(partitions, p, context->partitions) {
+ _cleanup_(partition_target_freep) PartitionTarget *t = NULL;
+
+ if (p->dropped)
+ continue;
+
+ if (PARTITION_EXISTS(p))
+ continue;
+
+ if (!p->btrfs_replaced)
+ continue;
+
+ if (partition_defer(context, p))
+ continue;
+
+ assert(!p->btrfs_replaced->done);
+
+ (void) context_notify(context, PROGRESS_REPLACING_DEVICE, p->definition_path, UINT_MAX);
+
+ assert(p->offset != UINT64_MAX);
+ assert(p->new_size != UINT64_MAX);
+
+ r = partition_target_prepare(context, p,
+ p->new_size,
+ /* need_path= */ true,
+ &t);
+ if (r < 0)
+ return r;
+
+ if (p->encrypt != ENCRYPT_OFF) {
+ r = partition_encrypt(context, p, t, /* offline= */ false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to encrypt device: %m");
+ }
+
+ log_info("Replacing partition %" PRIu64 ".", p->partno);
+
+ /* btrfs_replace calls a synchronous ioctl and will return when replace is finished */
+ r = btrfs_replace(p->btrfs_replaced->mountpoint_fd, p->btrfs_replaced->devid, partition_target_path(t));
+ if (r < 0)
+ return log_error_errno(r, "Failed to replace btrfs device on partition %" PRIu64 ": %m", p->partno);
+
+ p->btrfs_replaced->done = true;
+
+ if (t->decrypted)
+ t->decrypted->keep = true;
+
+ log_info("Successfully replaced partition %" PRIu64 ".", p->partno);
+ }
+
+ return 0;
+}
+
+static void context_btrfs_replace_resize(Context *context) {
+ int r;
+
+ assert(context);
+
+ LIST_FOREACH(partitions, p, context->partitions) {
+ if (!p->btrfs_replaced)
+ continue;
+
+ if (!p->btrfs_replaced->done)
+ continue;
+
+ r = btrfs_resize_max(p->btrfs_replaced->mountpoint_fd, p->btrfs_replaced->devid);
+ if (r < 0)
+ log_warning_errno(r, "Could not resize btrfs filesystem moved to partition %" PRIu64 ", proceeding without resizing: %m", p->partno);
+ else
+ log_info("Successfully resized partition %" PRIu64 ".", p->partno);
+ }
+}
+
+static void context_btrfs_replace_back(Context *context) {
+ int r;
+
+ assert(context);
+
+ LIST_FOREACH(partitions, p, context->partitions) {
+ if (!p->btrfs_replaced)
+ continue;
+
+ if (!p->btrfs_replaced->done)
+ continue;
+
+ r = btrfs_replace(p->btrfs_replaced->mountpoint_fd, p->btrfs_replaced->devid, p->btrfs_replaced->source_path);
+ if (r < 0)
+ log_warning_errno(r, "Could not move back btrfs filesystem from partition %" PRIu64 ", leaving it on new device: %m", p->partno);
+ }
+}
+
static int context_mkfs(Context *context) {
int r;
@@ -8335,6 +8478,15 @@ static int context_write_partition_table(Context *context) {
if (r < 0)
goto error;
+ /* We are now moving destructively btrfs filesystems into the disk before we have written the
+ * partitions. This is OK because the main use case is that the btrfs filesystems moved are initially
+ * volatile (in ram disk for example) with little data to save. But we do not want to finish the gpt
+ * table in case we lose power and reboot and try to boot that incomplete disk.
+ */
+ r = context_block_device_replace(context);
+ if (r < 0)
+ goto error;
+
r = context_mangle_partitions(context);
if (r < 0)
goto error;
@@ -8349,6 +8501,8 @@ static int context_write_partition_table(Context *context) {
goto error;
}
+ context_btrfs_replace_resize(context);
+
r = context_partscan(context);
if (r < 0)
return r;
@@ -8358,6 +8512,8 @@ static int context_write_partition_table(Context *context) {
return 0;
error:
+ context_btrfs_replace_back(context);
+
if (context->needs_rescan)
(void) context_partscan(context);
@@ -8884,9 +9040,6 @@ static int context_open_copy_block_paths(
assert(context);
- if (!context->partitions)
- return 0;
-
LIST_FOREACH(partitions, p, context->partitions) {
_cleanup_close_ int source_fd = -EBADF;
_cleanup_free_ char *opened = NULL;
@@ -8995,6 +9148,72 @@ static int context_open_copy_block_paths(
return 0;
}
+static int context_open_btrfs_filesystems(Context *context) {
+ int r;
+
+ assert(context);
+
+ LIST_FOREACH(partitions, p, context->partitions) {
+ _cleanup_(btrfs_replacement_freep) BtrfsReplacement *replacement = NULL;
+
+ if (p->dropped)
+ continue;
+
+ if (PARTITION_EXISTS(p))
+ continue;
+
+ if (!p->block_device_replace)
+ continue;
+
+ if (partition_defer(context, p))
+ continue;
+
+ (void) context_notify(context, PROGRESS_OPENING_BLOCK_DEVICE_REPLACE_SOURCES, p->definition_path, UINT_MAX);
+
+ replacement = new(BtrfsReplacement, 1);
+ if (!replacement)
+ return log_oom();
+
+ *replacement = (BtrfsReplacement) {
+ .mountpoint_fd = -EBADF,
+ .source_fd = -EBADF,
+ };
+
+ replacement->mountpoint_fd = xopenat(AT_FDCWD, p->block_device_replace, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
+ if (replacement->mountpoint_fd < 0)
+ return log_error_errno(replacement->mountpoint_fd, "Failed to open mountpoint %s for btrfs filesystem: %m", p->block_device_replace);
+
+ r = fd_is_fs_type(replacement->mountpoint_fd, BTRFS_SUPER_MAGIC);
+ if (r < 0)
+ return log_error_errno(r, "Failed to check filesystem for mountpoint %s: %m", p->block_device_replace);
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Mountpoint %s is not a btrfs filesystem", p->block_device_replace);
+
+ r = btrfs_get_block_device_at_full(replacement->mountpoint_fd, "", &replacement->devid, &replacement->source_path, /* ret= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to find device id for btrfs filesystem: %m");
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Btrfs filesystem has multiple devices.");
+
+ /* We need to keep the source device open otherwise, it might be collected. */
+ replacement->source_fd = open(replacement->source_path, O_RDONLY|O_CLOEXEC);
+ if (replacement->source_fd < 0)
+ return log_error_errno(errno, "Failed to open source device %s: %m", replacement->source_path);
+
+ r = fd_verify_block(replacement->source_fd);
+ if (r < 0)
+ return log_error_errno(r, "Device %s is not a block device: %m", replacement->source_path);
+
+ r = blockdev_get_device_size(replacement->source_fd, &replacement->source_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get device size %s: %m", replacement->source_path);
+
+ p->btrfs_replaced = TAKE_PTR(replacement);
+ }
+
+ return 0;
+}
+
static int fd_apparent_size(int fd, uint64_t *ret) {
off_t initial = 0;
uint64_t size = 0;
@@ -11154,6 +11373,10 @@ static int vl_method_run(
if (r < 0)
return r;
+ r = context_open_btrfs_filesystems(context);
+ if (r < 0)
+ return r;
+
r = context_acquire_partition_uuids_and_labels(context);
if (r < 0)
return r;
@@ -11451,6 +11674,10 @@ static int run(int argc, char *argv[]) {
if (r < 0)
return r;
+ r = context_open_btrfs_filesystems(context);
+ if (r < 0)
+ return r;
+
/* Make sure each partition has a unique UUID and unique label */
r = context_acquire_partition_uuids_and_labels(context);
if (r < 0)
diff --git a/src/shared/btrfs-util.c b/src/shared/btrfs-util.c
index 33d6af3942..daa67e6f5c 100644
--- a/src/shared/btrfs-util.c
+++ b/src/shared/btrfs-util.c
@@ -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));
+}
diff --git a/src/shared/btrfs-util.h b/src/shared/btrfs-util.h
index afa54cde4c..da7c897945 100644
--- a/src/shared/btrfs-util.h
+++ b/src/shared/btrfs-util.h
@@ -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);