]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
homework: also add logic for "maximizing" size of home
authorLennart Poettering <lennart@poettering.net>
Fri, 29 Oct 2021 10:21:41 +0000 (12:21 +0200)
committerLennart Poettering <lennart@poettering.net>
Thu, 18 Nov 2021 23:05:53 +0000 (00:05 +0100)
src/home/homework-luks.c

index 9c4f37703462cf3e7897c73c94543a1c9e13cd6b..57b41d26b831ef409b38d295c186ed1d51dde860 100644 (file)
@@ -1960,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;
@@ -1968,7 +1967,6 @@ static int home_truncate(
 
         assert(h);
         assert(fd >= 0);
-        assert(path);
 
         trunc = user_record_luks_discard(h);
         if (!trunc) {
@@ -1986,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(
@@ -2175,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;
 
@@ -2764,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,
@@ -2851,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,
@@ -2988,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);
@@ -3109,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);