1
1
mirror of https://github.com/systemd/systemd-stable.git synced 2025-01-05 09:17:44 +03:00

Merge pull request #21411 from poettering/homed-maximize

homed: add concept for "maximizing" home dirs
This commit is contained in:
Lennart Poettering 2021-11-19 09:22:11 +01:00 committed by GitHub
commit 5c9da90d1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 260 additions and 67 deletions

View File

@ -849,7 +849,11 @@
<literal>xfs</literal> and <literal>btrfs</literal> the home directory may be grown while the user is
logged in, and on the latter also shrunk while the user is logged in. If the
<literal>subvolume</literal>, <literal>directory</literal>, <literal>fscrypt</literal> storage
mechanisms are used, resizing will change file system quota.</para></listitem>
mechanisms are used, resizing will change file system quota. The size parameter may make use of the
usual suffixes B, K, M, G, T (to the base of 1024). The special strings <literal>min</literal> and
<literal>max</literal> may be specified in place of a numeric size value, for minimizing or
maximizing disk space assigned to the home area, taking constraints of the file system, disk usage inside
the home area and on the backing storage into account.</para></listitem>
</varlistentry>
<varlistentry>

View File

@ -1763,6 +1763,32 @@ static int passwd_home(int argc, char *argv[], void *userdata) {
return 0;
}
static int parse_disk_size(const char *t, uint64_t *ret) {
int r;
assert(t);
assert(ret);
if (streq(t, "min"))
*ret = 0;
else if (streq(t, "max"))
*ret = UINT64_MAX-1; /* Largest size that isn't UINT64_MAX special marker */
else {
uint64_t ds;
r = parse_size(t, 1024, &ds);
if (r < 0)
return log_error_errno(r, "Failed to parse disk size parameter: %s", t);
if (ds >= UINT64_MAX) /* UINT64_MAX has special meaning for us ("dont change"), refuse */
return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Disk size out of range: %s", t);
*ret = ds;
}
return 0;
}
static int resize_home(int argc, char *argv[], void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(user_record_unrefp) UserRecord *secret = NULL;
@ -1781,9 +1807,9 @@ static int resize_home(int argc, char *argv[], void *userdata) {
"Relative disk size specification currently not supported when resizing.");
if (argc > 2) {
r = parse_size(argv[2], 1024, &ds);
r = parse_disk_size(argv[2], &ds);
if (r < 0)
return log_error_errno(r, "Failed to parse disk size parameter: %s", argv[2]);
return r;
}
if (arg_disk_size != UINT64_MAX) {
@ -2907,9 +2933,9 @@ static int parse_argv(int argc, char *argv[]) {
r = parse_permyriad(optarg);
if (r < 0) {
r = parse_size(optarg, 1024, &arg_disk_size);
r = parse_disk_size(optarg, &arg_disk_size);
if (r < 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Disk size '%s' not valid.", optarg);
return r;
r = drop_from_identity("diskSizeRelative");
if (r < 0)

View File

@ -1164,55 +1164,67 @@ int home_setup_luks(
PasswordCache *cache,
UserRecord **ret_luks_home) {
sd_id128_t found_partition_uuid, found_luks_uuid, found_fs_uuid;
sd_id128_t found_partition_uuid = SD_ID128_NULL, found_luks_uuid = SD_ID128_NULL, found_fs_uuid = SD_ID128_NULL;
_cleanup_(user_record_unrefp) UserRecord *luks_home = NULL;
_cleanup_(erase_and_freep) void *volume_key = NULL;
size_t volume_key_size = 0;
uint64_t offset, size;
struct stat st;
int r;
assert(h);
assert(setup);
assert(setup->dm_name);
assert(setup->dm_node);
assert(setup->root_fd < 0);
assert(!setup->crypt_device);
assert(!setup->loop);
assert(user_record_storage(h) == USER_LUKS);
r = dlopen_cryptsetup();
if (r < 0)
return r;
r = make_dm_names(h, setup);
if (r < 0)
return r;
/* Reuse the image fd if it has already been opened by an earlier step */
if (setup->image_fd < 0) {
setup->image_fd = open_image_file(h, force_image_path, &st);
if (setup->image_fd < 0)
return setup->image_fd;
} else if (fstat(setup->image_fd, &st) < 0)
return log_error_errno(errno, "Failed to stat image: %m");
if (FLAGS_SET(flags, HOME_SETUP_ALREADY_ACTIVATED)) {
struct loop_info64 info;
const char *n;
r = luks_open(h,
setup,
cache,
&found_luks_uuid,
&volume_key,
&volume_key_size);
if (r < 0)
return r;
if (!setup->crypt_device) {
r = luks_open(h,
setup,
cache,
&found_luks_uuid,
&volume_key,
&volume_key_size);
if (r < 0)
return r;
}
r = luks_validate_home_record(setup->crypt_device, h, volume_key, cache, &luks_home);
if (r < 0)
return r;
if (ret_luks_home) {
r = luks_validate_home_record(setup->crypt_device, h, volume_key, cache, &luks_home);
if (r < 0)
return r;
}
n = sym_crypt_get_device_name(setup->crypt_device);
if (!n)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine backing device for DM %s.", setup->dm_name);
r = loop_device_open(n, O_RDWR, &setup->loop);
if (r < 0)
return log_error_errno(r, "Failed to open loopback device %s: %m", n);
if (!setup->loop) {
r = loop_device_open(n, O_RDWR, &setup->loop);
if (r < 0)
return log_error_errno(r, "Failed to open loopback device %s: %m", n);
}
if (ioctl(setup->loop->fd, LOOP_GET_STATUS64, &info) < 0) {
_cleanup_free_ char *sysfs = NULL;
struct stat st;
if (!IN_SET(errno, ENOTTY, EINVAL))
return log_error_errno(errno, "Failed to get block device metrics of %s: %m", n);
@ -1264,14 +1276,20 @@ int home_setup_luks(
log_info("Discovered used loopback device %s.", setup->loop->node);
setup->root_fd = open(user_record_home_directory(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
if (setup->root_fd < 0)
return log_error_errno(errno, "Failed to open home directory: %m");
if (setup->root_fd < 0) {
setup->root_fd = open(user_record_home_directory(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
if (setup->root_fd < 0)
return log_error_errno(errno, "Failed to open home directory: %m");
}
} else {
_cleanup_free_ char *fstype = NULL, *subdir = NULL;
bool has_stat = false;
const char *ip;
struct stat st;
/* When we aren't reopening the home directory we are allocating it fresh, hence the relevant
* objects can't be allocated yet. */
assert(setup->root_fd < 0);
assert(!setup->crypt_device);
assert(!setup->loop);
ip = force_image_path ?: user_record_image_path(h);
@ -1279,15 +1297,6 @@ int home_setup_luks(
if (!subdir)
return log_oom();
/* Reuse the image fd if it has already been opened by an earlier step */
if (setup->image_fd < 0) {
setup->image_fd = open_image_file(h, force_image_path, &st);
if (setup->image_fd < 0)
return setup->image_fd;
has_stat = true;
}
r = luks_validate(setup->image_fd, user_record_user_name_and_realm(h), h->partition_uuid, &found_partition_uuid, &offset, &size);
if (r < 0)
return log_error_errno(r, "Failed to validate disk label: %m");
@ -1298,7 +1307,7 @@ int home_setup_luks(
setup->do_mark_clean = true;
if (!user_record_luks_discard(h)) {
r = run_fallocate(setup->image_fd, has_stat ? &st : NULL);
r = run_fallocate(setup->image_fd, &st);
if (r < 0)
return r;
}
@ -1331,9 +1340,11 @@ int home_setup_luks(
setup->undo_dm = true;
r = luks_validate_home_record(setup->crypt_device, h, volume_key, cache, &luks_home);
if (r < 0)
return r;
if (ret_luks_home) {
r = luks_validate_home_record(setup->crypt_device, h, volume_key, cache, &luks_home);
if (r < 0)
return r;
}
r = fs_validate(setup->dm_node, h->file_system_uuid, &fstype, &found_fs_uuid);
if (r < 0)
@ -1359,13 +1370,21 @@ int home_setup_luks(
setup->do_offline_fallocate = !(setup->do_offline_fitrim = user_record_luks_offline_discard(h));
}
setup->found_partition_uuid = found_partition_uuid;
setup->found_luks_uuid = found_luks_uuid;
setup->found_fs_uuid = found_fs_uuid;
if (!sd_id128_is_null(found_partition_uuid))
setup->found_partition_uuid = found_partition_uuid;
if (!sd_id128_is_null(found_luks_uuid))
setup->found_luks_uuid = found_luks_uuid;
if (!sd_id128_is_null(found_fs_uuid))
setup->found_fs_uuid = found_fs_uuid;
setup->partition_offset = offset;
setup->partition_size = size;
setup->volume_key = TAKE_PTR(volume_key);
setup->volume_key_size = volume_key_size;
if (volume_key) {
erase_and_free(setup->volume_key);
setup->volume_key = TAKE_PTR(volume_key);
setup->volume_key_size = volume_key_size;
}
if (ret_luks_home)
*ret_luks_home = TAKE_PTR(luks_home);
@ -1941,7 +1960,6 @@ static int calculate_disk_size(UserRecord *h, const char *parent_dir, uint64_t *
static int home_truncate(
UserRecord *h,
int fd,
const char *path,
uint64_t size) {
bool trunc;
@ -1949,7 +1967,6 @@ static int home_truncate(
assert(h);
assert(fd >= 0);
assert(path);
trunc = user_record_luks_discard(h);
if (!trunc) {
@ -1967,14 +1984,14 @@ static int home_truncate(
if (r < 0) {
if (ERRNO_IS_DISK_SPACE(errno)) {
log_error_errno(errno, "Not enough disk space to allocate home.");
log_debug_errno(errno, "Not enough disk space to allocate home of size %s.", FORMAT_BYTES(size));
return -ENOSPC; /* make recognizable */
}
return log_error_errno(errno, "Failed to truncate home image %s: %m", path);
return log_error_errno(errno, "Failed to truncate home image: %m");
}
return 0;
return !trunc; /* Return == 0 if we managed to truncate, > 0 if we managed to allocate */
}
int home_create_luks(
@ -2156,7 +2173,7 @@ int home_create_luks(
log_full_errno(ERRNO_IS_NOT_SUPPORTED(r) ? LOG_DEBUG : LOG_WARNING, r,
"Failed to set file attributes on %s, ignoring: %m", setup->temporary_image_path);
r = home_truncate(h, setup->image_fd, setup->temporary_image_path, host_size);
r = home_truncate(h, setup->image_fd, host_size);
if (r < 0)
return r;
@ -2745,6 +2762,39 @@ static int get_smallest_fs_size(int fd, uint64_t *ret) {
return 0;
}
static int get_largest_image_size(int fd, const struct stat *st, uint64_t *ret) {
uint64_t used, avail, sum;
struct statfs sfs;
int r;
assert(fd >= 0);
assert(st);
assert(ret);
/* Determines the maximum file size we might be able to grow the image file referenced by the fd to. */
r = stat_verify_regular(st);
if (r < 0)
return log_error_errno(r, "Image file is not a regular file, refusing: %m");
if (syncfs(fd) < 0)
return log_error_errno(errno, "Failed to synchronize file system backing image file: %m");
if (fstatfs(fd, &sfs) < 0)
return log_error_errno(errno, "Failed to statfs() image file: %m");
used = (uint64_t) st->st_blocks * 512;
avail = (uint64_t) sfs.f_bsize * sfs.f_bavail;
if (avail > UINT64_MAX - used)
sum = UINT64_MAX;
else
sum = avail + used;
*ret = DISK_SIZE_ROUND_DOWN(MIN(sum, USER_DISK_SIZE_MAX));
return 0;
}
static int resize_fs_loop(
UserRecord *h,
HomeSetup *setup,
@ -2832,6 +2882,77 @@ static int resize_fs_loop(
return 0;
}
static int resize_image_loop(
UserRecord *h,
HomeSetup *setup,
uint64_t old_image_size,
uint64_t new_image_size,
uint64_t *ret_image_size) {
uint64_t current_image_size;
unsigned n_iterations = 0;
int r;
assert(h);
assert(setup);
assert(setup->image_fd >= 0);
/* A bisection loop trying to find the closest size to what the user asked for. (Well, we bisect like
* this only when we *grow* the image if we shrink the image then there's no need to bisect.) */
current_image_size = old_image_size;
for (uint64_t lower_boundary = old_image_size, upper_boundary = new_image_size, try_image_size = new_image_size;;) {
bool worked;
n_iterations++;
r = home_truncate(h, setup->image_fd, try_image_size);
if (r < 0) {
if (!ERRNO_IS_DISK_SPACE(r) || new_image_size < old_image_size) /* Not a disk space issue? Not trying to grow? */
return r;
log_debug_errno(r, "Growing from %s to %s didn't work, not enough space on backing disk.", FORMAT_BYTES(current_image_size), FORMAT_BYTES(try_image_size));
worked = false;
} else if (r > 0) { /* Success: allocation worked */
log_debug("Resizing from %s to %s via allocation worked successfully.", FORMAT_BYTES(current_image_size), FORMAT_BYTES(try_image_size));
current_image_size = try_image_size;
worked = true;
} else { /* Success, but through truncation, not allocation. */
log_debug("Resizing from %s to %s via truncation worked successfully.", FORMAT_BYTES(old_image_size), FORMAT_BYTES(try_image_size));
current_image_size = try_image_size;
break; /* there's no point in the bisection logic if this was plain truncation and
* not allocation, let's exit immediately. */
}
if (new_image_size < old_image_size) /* If we are shrinking we are done after one iteration */
break;
/* If we are growing then let's adjust our bisection boundaries and try again */
if (worked)
lower_boundary = MAX(lower_boundary, try_image_size);
else
upper_boundary = MIN(upper_boundary, try_image_size);
if (lower_boundary >= upper_boundary) {
log_debug("Image can't be grown further (range to try is empty).");
break;
}
try_image_size = DISK_SIZE_ROUND_DOWN(lower_boundary + (upper_boundary - lower_boundary) / 2);
if (try_image_size <= lower_boundary || try_image_size >= upper_boundary) {
log_debug("Image can't be grown further (remaining range to try too small).");
break;
}
}
log_debug("Bisection loop completed after %u iterations.", n_iterations);
if (ret_image_size)
*ret_image_size = current_image_size;
return 0;
}
int home_resize_luks(
UserRecord *h,
HomeSetupFlags flags,
@ -2969,11 +3090,17 @@ int home_resize_luks(
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Old partition doesn't fit in backing storage, refusing.");
if (S_ISREG(st.st_mode)) {
uint64_t partition_table_extra;
uint64_t partition_table_extra, largest_size;
partition_table_extra = old_image_size - setup->partition_size;
if (new_image_size <= partition_table_extra)
r = get_largest_image_size(setup->image_fd, &st, &largest_size);
if (r < 0)
return r;
if (new_image_size > largest_size)
new_image_size = largest_size;
if (new_image_size < partition_table_extra)
new_image_size = partition_table_extra;
new_partition_size = DISK_SIZE_ROUND_DOWN(new_image_size - partition_table_extra);
@ -3090,12 +3217,35 @@ int home_resize_luks(
if (new_fs_size > old_fs_size) { /* → Grow */
if (S_ISREG(st.st_mode)) {
uint64_t resized_image_size;
/* Grow file size */
r = home_truncate(h, image_fd, ip, new_image_size);
r = resize_image_loop(h, setup, old_image_size, new_image_size, &resized_image_size);
if (r < 0)
return r;
log_info("Growing of image file completed.");
if (resized_image_size == old_image_size) {
log_info("Couldn't change image size.");
return 0;
}
assert(resized_image_size > old_image_size);
log_info("Growing of image file from %s to %s completed.", FORMAT_BYTES(old_image_size), FORMAT_BYTES(resized_image_size));
if (resized_image_size < new_image_size) {
uint64_t sub;
/* If the growing we managed to do is smaller than what we wanted we need to
* adjust the partition/file system sizes we are going for, too */
sub = new_image_size - resized_image_size;
assert(new_partition_size >= sub);
new_partition_size -= sub;
assert(new_fs_size >= sub);
new_fs_size -= sub;
}
new_image_size = resized_image_size;
} else {
assert(S_ISBLK(st.st_mode));
assert(new_image_size == old_image_size);
@ -3240,9 +3390,11 @@ int home_resize_luks(
if (r < 0)
return r;
r = home_setup_done(setup);
if (r < 0)
return r;
if (!FLAGS_SET(flags, HOME_SETUP_RESIZE_DONT_UNDO)) {
r = home_setup_done(setup);
if (r < 0)
return r;
}
log_info("Everything completed.");

View File

@ -63,6 +63,7 @@ typedef enum HomeSetupFlags {
HOME_SETUP_RESIZE_MINIMIZE = 1 << 3, /* Shrink to minimal size */
HOME_SETUP_RESIZE_DONT_GROW = 1 << 4, /* If the resize would grow, gracefully terminate operation */
HOME_SETUP_RESIZE_DONT_SHRINK = 1 << 5, /* If the resize would shrink, gracefully terminate operation */
HOME_SETUP_RESIZE_DONT_UNDO = 1 << 6, /* Leave loopback/DM device context open after successful operation */
} HomeSetupFlags;
int home_setup_done(HomeSetup *setup);

View File

@ -27,9 +27,19 @@ inspect() {
systemd-analyze log-level debug
systemd-analyze log-target console
# Create a tmpfs to use as backing store for the home dir. That way we can enforce a size limit nicely.
mkdir -p /home-pool
mount -t tmpfs tmpfs /home-pool -o size=290M
# we enable --luks-discard= since we run our tests in a tight VM, hence don't
# needlessly pressure for storage
NEWPASSWORD=xEhErW0ndafV4s homectl create test-user --disk-size=256M --luks-discard=yes
# needlessly pressure for storage. We also set the cheapest KDF, since we don't
# want to waste CI CPU cycles on it.
NEWPASSWORD=xEhErW0ndafV4s homectl create test-user \
--disk-size=256M \
--luks-discard=yes \
--image-path=/home-pool/test-user.home \
--luks-pbkdf-type=pbkdf2 \
--luks-pbkdf-time-cost=1ms
inspect test-user
PASSWORD=xEhErW0ndafV4s homectl authenticate test-user
@ -77,14 +87,14 @@ if ! systemd-detect-virt -cq ; then
inspect test-user
# minimize while inactive
PASSWORD=xEhErW0ndafV4s homectl resize test-user 0
PASSWORD=xEhErW0ndafV4s homectl resize test-user min
inspect test-user
PASSWORD=xEhErW0ndafV4s homectl activate test-user
inspect test-user
# grow while active
PASSWORD=xEhErW0ndafV4s homectl resize test-user 300M
PASSWORD=xEhErW0ndafV4s homectl resize test-user max
inspect test-user
# minimize while active