]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
homed: add automatic grow/shrink ("rebalancing")
authorLennart Poettering <lennart@poettering.net>
Tue, 2 Nov 2021 22:11:59 +0000 (23:11 +0100)
committerLennart Poettering <lennart@poettering.net>
Thu, 25 Nov 2021 17:28:44 +0000 (18:28 +0100)
meson.build
src/home/home-util.h
src/home/homed-home-bus.c
src/home/homed-home.c
src/home/homed-home.h
src/home/homed-manager.c
src/home/homed-manager.h
src/home/homework.c
src/home/user-record-util.c
src/home/user-record-util.h
src/shared/user-record.h

index 6263e7c0fc447a164097eac6f24420d2dbc8cb52..97732b62434b8b3b566b669ec1e6a6ad4f574f49 100644 (file)
@@ -2387,7 +2387,8 @@ if conf.get('ENABLE_HOMED') == 1
                 link_with : [libshared],
                 dependencies : [threads,
                                 libcrypt,
-                                libopenssl],
+                                libopenssl,
+                                libm],
                 install_rpath : rootlibexecdir,
                 install : true,
                 install_dir : rootlibexecdir)
index ca4c068f371cf601cbbb4e926a60ca78c3ffe313..69a88000f8456f26dba3c97752ab80b7f3ebf8f0 100644 (file)
 #define USER_DISK_SIZE_MIN (UINT64_C(5)*1024*1024)
 #define USER_DISK_SIZE_MAX (UINT64_C(5)*1024*1024*1024*1024)
 
-/* The default disk size to use when nothing else is specified, relative to free disk space */
-#define USER_DISK_SIZE_DEFAULT_PERCENT 85
+/* The default disk size to use when nothing else is specified, relative to free disk space. We calculate
+ * this from the default rebalancing weights, so that what we create initially doesn't immediately require
+ * rebalancing. */
+#define USER_DISK_SIZE_DEFAULT_PERCENT ((unsigned) ((100 * REBALANCE_WEIGHT_DEFAULT) / (REBALANCE_WEIGHT_DEFAULT + REBALANCE_WEIGHT_BACKING)))
+
+/* This should be 83% right now, i.e. 100 of (100 + 20). Let's protect us against accidental changes. */
+assert_cc(USER_DISK_SIZE_DEFAULT_PERCENT == 83U);
 
 bool suitable_user_name(const char *name);
 int suitable_realm(const char *realm);
index c71256d15e55b0ae48ab6ca36df7d3bd7b719e7d..9e9f537d6c883499faddfe457988992a3dfbe1c6 100644 (file)
@@ -485,7 +485,7 @@ int bus_home_method_resize(
         if (r == 0)
                 return 1; /* Will call us back */
 
-        r = home_resize(h, sz, secret, error);
+        r = home_resize(h, sz, secret, /* automatic= */ false, error);
         if (r < 0)
                 return r;
 
index 2cc1f8b384a95e8942776dba706b0c62703a7865..470c7f07f6209f6734799282c232754326f84544 100644 (file)
@@ -161,6 +161,7 @@ int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret) {
 
         (void) bus_manager_emit_auto_login_changed(m);
         (void) bus_home_emit_change(home);
+        (void) manager_schedule_rebalance(m, /* immediately= */ false);
 
         if (ret)
                 *ret = TAKE_PTR(home);
@@ -193,6 +194,8 @@ Home *home_free(Home *h) {
 
                 if (h->manager->gc_focus == h)
                         h->manager->gc_focus = NULL;
+
+                (void) manager_schedule_rebalance(h->manager, /* immediately= */ false);
         }
 
         user_record_unref(h->record);
@@ -489,6 +492,7 @@ static void home_set_state(Home *h, HomeState state) {
                  * enqueue it for GC too. */
 
                 home_schedule_operation(h, NULL, NULL);
+                manager_reschedule_rebalance(h->manager);
                 manager_enqueue_gc(h->manager, h);
         }
 }
@@ -727,6 +731,7 @@ static void home_fixate_finish(Home *h, int ret, UserRecord *hr) {
         /* Reset the state to "invalid", which makes home_get_state() test if the image exists and returns
          * HOME_ABSENT vs. HOME_INACTIVE as necessary. */
         home_set_state(h, _HOME_STATE_INVALID);
+        (void) manager_schedule_rebalance(h->manager, /* immediately= */ false);
         return;
 
 fail:
@@ -781,6 +786,9 @@ static void home_activate_finish(Home *h, int ret, UserRecord *hr) {
 finish:
         h->current_operation = operation_result_unref(h->current_operation, r, &error);
         home_set_state(h, _HOME_STATE_INVALID);
+
+        if (r >= 0)
+                (void) manager_schedule_rebalance(h->manager, /* immediately= */ true);
 }
 
 static void home_deactivate_finish(Home *h, int ret, UserRecord *hr) {
@@ -803,6 +811,9 @@ static void home_deactivate_finish(Home *h, int ret, UserRecord *hr) {
 finish:
         h->current_operation = operation_result_unref(h->current_operation, r, &error);
         home_set_state(h, _HOME_STATE_INVALID);
+
+        if (r >= 0)
+                (void) manager_schedule_rebalance(h->manager, /* immediately= */ true);
 }
 
 static void home_remove_finish(Home *h, int ret, UserRecord *hr) {
@@ -841,6 +852,8 @@ static void home_remove_finish(Home *h, int ret, UserRecord *hr) {
 
         /* Unload this record from memory too now. */
         h = home_free(h);
+
+        (void) manager_schedule_rebalance(m, /* immediately= */ true);
         return;
 
 fail:
@@ -885,6 +898,8 @@ static void home_create_finish(Home *h, int ret, UserRecord *hr) {
 
         h->current_operation = operation_result_unref(h->current_operation, 0, NULL);
         home_set_state(h, _HOME_STATE_INVALID);
+
+        (void) manager_schedule_rebalance(h->manager, /* immediately= */ true);
 }
 
 static void home_change_finish(Home *h, int ret, UserRecord *hr) {
@@ -918,6 +933,7 @@ static void home_change_finish(Home *h, int ret, UserRecord *hr) {
         }
 
         log_debug("Change operation of %s completed.", h->user_name);
+        (void) manager_schedule_rebalance(h->manager, /* immediately= */ false);
         r = 0;
 
 finish:
@@ -1683,7 +1699,12 @@ int home_update(Home *h, UserRecord *hr, sd_bus_error *error) {
         return 0;
 }
 
-int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, sd_bus_error *error) {
+int home_resize(Home *h,
+                uint64_t disk_size,
+                UserRecord *secret,
+                bool automatic,
+                sd_bus_error *error) {
+
         _cleanup_(user_record_unrefp) UserRecord *c = NULL;
         HomeState state;
         int r;
@@ -1711,6 +1732,12 @@ int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, sd_bus_error *e
         if (r < 0)
                 return r;
 
+        /* If the user didn't specify any size explicitly and rebalancing is on, then the disk size is
+         * determined by automatic rebalancing and hence not user configured but determined by us and thus
+         * applied anyway. */
+        if (disk_size == UINT64_MAX && h->record->rebalance_weight != REBALANCE_WEIGHT_OFF)
+                return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Disk size is being determined by automatic disk space rebalancing.");
+
         if (disk_size == UINT64_MAX || disk_size == h->record->disk_size) {
                 if (h->record->disk_size == UINT64_MAX)
                         return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "No disk size to resize to specified.");
@@ -1732,6 +1759,11 @@ int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, sd_bus_error *e
                 if (r < 0)
                         return r;
 
+                /* If user picked an explicit size, then turn off rebalancing, so that we don't undo what user chose */
+                r = user_record_set_rebalance_weight(c, REBALANCE_WEIGHT_OFF);
+                if (r < 0)
+                        return r;
+
                 r = user_record_update_last_changed(c, false);
                 if (r == -ECHRNG)
                         return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Record last change time of %s is newer than current time, cannot update.", h->user_name);
@@ -1746,7 +1778,7 @@ int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, sd_bus_error *e
                 c = TAKE_PTR(signed_c);
         }
 
-        r = home_update_internal(h, "resize", c, secret, error);
+        r = home_update_internal(h, automatic ? "resize-auto" : "resize", c, secret, error);
         if (r < 0)
                 return r;
 
@@ -2965,6 +2997,8 @@ static int on_pending(sd_event_source *s, void *userdata) {
         if (r < 0)
                 return log_error_errno(r, "Failed to disable event source: %m");
 
+        /* No operations pending anymore, maybe this is a good time to trigger a rebalancing */
+        manager_reschedule_rebalance(h->manager);
         return 0;
 }
 
@@ -3121,6 +3155,35 @@ int home_wait_for_worker(Home *h) {
         return 1;
 }
 
+bool home_shall_rebalance(Home *h) {
+        HomeState state;
+
+        assert(h);
+
+        /* Determines if the home directory is a candidate for rebalancing */
+
+        if (!user_record_shall_rebalance(h->record))
+                return false;
+
+        state = home_get_state(h);
+        if (!HOME_STATE_SHALL_REBALANCE(state))
+                return false;
+
+        return true;
+}
+
+bool home_is_busy(Home *h) {
+        assert(h);
+
+        if (h->current_operation)
+                return true;
+
+        if (!ordered_set_isempty(h->pending_operations))
+                return true;
+
+        return HOME_STATE_IS_EXECUTING_OPERATION(home_get_state(h));
+}
+
 static const char* const home_state_table[_HOME_STATE_MAX] = {
         [HOME_UNFIXATED]                   = "unfixated",
         [HOME_ABSENT]                      = "absent",
index 37c54b05572163f68d7e5ce0173b7651746f7631..0f314aad93c9fcf572bb361d5072f99835fe078c 100644 (file)
@@ -88,6 +88,8 @@ static inline bool HOME_STATE_SHALL_PIN(HomeState state) {
                       HOME_AUTHENTICATING_FOR_ACQUIRE);
 }
 
+#define HOME_STATE_SHALL_REBALANCE(state) HOME_STATE_SHALL_PIN(state)
+
 static inline bool HOME_STATE_MAY_RETRY_DEACTIVATE(HomeState state) {
         /* Indicates when to leave the deactivate retry timer active */
         return IN_SET(state,
@@ -165,6 +167,12 @@ struct Home {
 
         /* An fd that locks the backing file of LUKS home dirs with a BSD lock. */
         int luks_lock_fd;
+
+        /* Space metrics during rebalancing */
+        uint64_t rebalance_size, rebalance_usage, rebalance_free, rebalance_min, rebalance_weight, rebalance_goal;
+
+        /* Whether a rebalance operation is pending */
+        bool rebalance_pending;
 };
 
 int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret);
@@ -183,7 +191,7 @@ int home_deactivate(Home *h, bool force, sd_bus_error *error);
 int home_create(Home *h, UserRecord *secret, sd_bus_error *error);
 int home_remove(Home *h, sd_bus_error *error);
 int home_update(Home *h, UserRecord *new_record, sd_bus_error *error);
-int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, sd_bus_error *error);
+int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, bool automatic, sd_bus_error *error);
 int home_passwd(Home *h, UserRecord *new_secret, UserRecord *old_secret, sd_bus_error *error);
 int home_unregister(Home *h, sd_bus_error *error);
 int home_lock(Home *h, sd_bus_error *error);
@@ -208,5 +216,9 @@ int home_set_current_message(Home *h, sd_bus_message *m);
 
 int home_wait_for_worker(Home *h);
 
+bool home_shall_rebalance(Home *h);
+
+bool home_is_busy(Home *h);
+
 const char *home_state_to_string(HomeState state);
 HomeState home_state_from_string(const char *s);
index a97a0dee766cb3b1c985c6aefbd67fcef7c3ef57..3851234a37b2c65cf6263eb27acd5c1e58c6c495 100644 (file)
@@ -3,6 +3,7 @@
 #include <grp.h>
 #include <linux/fs.h>
 #include <linux/magic.h>
+#include <math.h>
 #include <openssl/pem.h>
 #include <pwd.h>
 #include <sys/ioctl.h>
@@ -35,7 +36,9 @@
 #include "process-util.h"
 #include "quota-util.h"
 #include "random-util.h"
+#include "resize-fs.h"
 #include "socket-util.h"
+#include "sort-util.h"
 #include "stat-util.h"
 #include "strv.h"
 #include "sync-util.h"
@@ -201,6 +204,7 @@ int manager_new(Manager **ret) {
 
         *m = (Manager) {
                 .default_storage = _USER_STORAGE_INVALID,
+                .rebalance_interval_usec = 2 * USEC_PER_MINUTE, /* initially, rebalance every 2min */
         };
 
         r = manager_parse_config_file(m);
@@ -259,6 +263,7 @@ Manager* manager_free(Manager *m) {
         m->deferred_rescan_event_source = sd_event_source_unref(m->deferred_rescan_event_source);
         m->deferred_gc_event_source = sd_event_source_unref(m->deferred_gc_event_source);
         m->deferred_auto_login_event_source = sd_event_source_unref(m->deferred_auto_login_event_source);
+        m->rebalance_event_source = sd_event_source_unref(m->rebalance_event_source);
 
         sd_event_unref(m->event);
 
@@ -1770,3 +1775,408 @@ int manager_enqueue_gc(Manager *m, Home *focus) {
         (void) sd_event_source_set_description(m->deferred_gc_event_source, "deferred-gc");
         return 1;
 }
+
+static bool manager_shall_rebalance(Manager *m) {
+        Home *h;
+
+        assert(m);
+
+        if (IN_SET(m->rebalance_state, REBALANCE_PENDING, REBALANCE_SHRINKING, REBALANCE_GROWING))
+                return true;
+
+        HASHMAP_FOREACH(h, m->homes_by_name)
+                if (home_shall_rebalance(h))
+                        return true;
+
+        return false;
+}
+
+static int home_cmp(Home *const*a, Home *const*b) {
+        int r;
+
+        assert(a);
+        assert(*a);
+        assert(b);
+        assert(*b);
+
+        /* Order user records by their weight (and by their name, to make things stable). We put the records
+         * with the heighest weight last, since we distribute space from the beginning and round down, hence
+         * later entries tend to get slightly more than earlier entries. */
+
+        r = CMP(user_record_rebalance_weight((*a)->record), user_record_rebalance_weight((*b)->record));
+        if (r != 0)
+                return r;
+
+        return strcmp((*a)->user_name, (*b)->user_name);
+}
+
+static int manager_rebalance_calculate(Manager *m) {
+        uint64_t weight_sum, free_sum, usage_sum = 0, min_free = UINT64_MAX;
+        _cleanup_free_ Home **array = NULL;
+        bool relevant = false;
+        struct statfs sfs;
+        int c = 0, r;
+        Home *h;
+
+        assert(m);
+
+        if (statfs(get_home_root(), &sfs) < 0)
+                return log_error_errno(errno, "Failed to statfs() /home: %m");
+
+        free_sum = (uint64_t) sfs.f_bsize * sfs.f_bavail; /* This much free space is available on the
+                                                           * underlying pool directory */
+
+        weight_sum = REBALANCE_WEIGHT_BACKING; /* Grant the underlying pool directory a fixed weight of 20
+                                                * (home dirs get 100 by default, i.e. 5x more). This weight
+                                                * is not configurable, the per-home weights are. */
+
+        HASHMAP_FOREACH(h, m->homes_by_name) {
+                statfs_f_type_t fstype;
+                h->rebalance_pending = false; /* First, reset the flag, we only want it to be true for the
+                                               * homes that qualify for rebalancing */
+
+                if (!home_shall_rebalance(h)) /* Only look at actual candidates */
+                        continue;
+
+                if (home_is_busy(h))
+                        return -EBUSY; /* Let's not rebalance if there's a busy home directory. */
+
+                r = home_get_disk_status(
+                                h,
+                                &h->rebalance_size,
+                                &h->rebalance_usage,
+                                &h->rebalance_free,
+                                NULL,
+                                NULL,
+                                &fstype,
+                                NULL);
+                if (r < 0) {
+                        log_warning_errno(r, "Failed to get free space of home '%s', ignoring.", h->user_name);
+                        continue;
+                }
+
+                if (h->rebalance_free > UINT64_MAX - free_sum)
+                        return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), "Rebalance free overflow");
+                free_sum += h->rebalance_free;
+
+                if (h->rebalance_usage > UINT64_MAX - usage_sum)
+                        return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), "Rebalance usage overflow");
+                usage_sum += h->rebalance_usage;
+
+                h->rebalance_weight = user_record_rebalance_weight(h->record);
+                if (h->rebalance_weight > UINT64_MAX - weight_sum)
+                        return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), "Rebalance weight overflow");
+                weight_sum += h->rebalance_weight;
+
+                h->rebalance_min = minimal_size_by_fs_magic(fstype);
+
+                if (!GREEDY_REALLOC(array, c+1))
+                        return log_oom();
+
+                array[c++] = h;
+        }
+
+        if (c == 0) {
+                log_debug("No homes to rebalance.");
+                return 0;
+        }
+
+        assert(weight_sum > 0);
+
+        log_debug("Disk space usage by all home directories to rebalance: %s — available disk space: %s",
+                  FORMAT_BYTES(usage_sum), FORMAT_BYTES(free_sum));
+
+        /* Bring the home directories in a well-defined order, so that we distribute space in a reproducible
+         * way for the same parameters. */
+        typesafe_qsort(array, c, home_cmp);
+
+        for (int i = 0; i < c; i++) {
+                uint64_t new_free;
+                double d;
+
+                h = array[i];
+
+                assert(h->rebalance_free <= free_sum);
+                assert(h->rebalance_usage <= usage_sum);
+                assert(h->rebalance_weight <= weight_sum);
+
+                d = ((double) (free_sum / 4096) * (double) h->rebalance_weight) / (double) weight_sum; /* Calculate new space for this home in units of 4K */
+
+                /* Convert from units of 4K back to bytes */
+                if (d >= (double) (UINT64_MAX/4096))
+                        new_free = UINT64_MAX;
+                else
+                        new_free = (uint64_t) d * 4096;
+
+                /* Subtract the weight and assigned space from the sums now, to distribute the rounding noise
+                 * to the remaining home dirs */
+                free_sum = LESS_BY(free_sum, new_free);
+                weight_sum = LESS_BY(weight_sum, h->rebalance_weight);
+
+                /* Keep track of home directory with the least amount of space left: we want to schedule the
+                 * next rebalance more quickly if this is low */
+                if (new_free < min_free)
+                        min_free = h->rebalance_size;
+
+                if (new_free > UINT64_MAX - h->rebalance_usage)
+                        h->rebalance_goal = UINT64_MAX-1; /* maximum size */
+                else {
+                        h->rebalance_goal = h->rebalance_usage + new_free;
+
+                        if (h->rebalance_min != UINT64_MAX && h->rebalance_goal < h->rebalance_min)
+                                h->rebalance_goal = h->rebalance_min;
+                }
+
+                /* Skip over this home if the state doesn't match the operation */
+                if ((m->rebalance_state == REBALANCE_SHRINKING && h->rebalance_goal > h->rebalance_size) ||
+                    (m->rebalance_state == REBALANCE_GROWING && h->rebalance_goal < h->rebalance_size))
+                        h->rebalance_pending = false;
+                else {
+                        log_debug("Rebalancing home directory '%s' %s → %s.", h->user_name,
+                                  FORMAT_BYTES(h->rebalance_size), FORMAT_BYTES(h->rebalance_goal));
+                        h->rebalance_pending = true;
+                }
+
+                if ((fabs((double) h->rebalance_size - (double) h->rebalance_goal) * 100 / (double) h->rebalance_size) >= 5.0)
+                        relevant = true;
+        }
+
+        /* Scale next rebalancing interval based on the least amount of space of any of the home
+         * directories. We pick a time in the range 1min … 15min, scaled by log2(min_free), so that:
+         * 10M → ~0.7min, 100M → ~2.7min, 1G → ~4.6min, 10G → ~6.5min, 100G ~8.4 */
+        m->rebalance_interval_usec = (usec_t) CLAMP((LESS_BY(log2(min_free), 22)*15*USEC_PER_MINUTE)/26,
+                                                    1 * USEC_PER_MINUTE,
+                                                    15 * USEC_PER_MINUTE);
+
+
+        log_debug("Rebalancing interval set to %s.", FORMAT_TIMESPAN(m->rebalance_interval_usec, USEC_PER_MSEC));
+
+        /* Let's suppress small resizes, growing/shrinking file systems isn't free after all */
+        if (!relevant) {
+                log_debug("Skipping rebalancing, since all calculated size changes are below ±5%%.");
+                return 0;
+        }
+
+        return c;
+}
+
+static int manager_rebalance_apply(Manager *m) {
+        int c = 0, r;
+        Home *h;
+
+        assert(m);
+
+        HASHMAP_FOREACH(h, m->homes_by_name) {
+                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+
+                if (!h->rebalance_pending)
+                        continue;
+
+                h->rebalance_pending = false;
+
+                r = home_resize(h, h->rebalance_goal, /* secret= */ NULL, /* automatic= */ true, &error);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to resize home '%s' for rebalancing, ignoring: %s",
+                                          h->user_name, bus_error_message(&error, r));
+                else
+                        c++;
+        }
+
+        return c;
+}
+
+static int manager_rebalance_now(Manager *m) {
+        RebalanceState busy_state; /* the state to revert to when operation fails if busy */
+        int r;
+
+        assert(m);
+
+        log_debug("Rebalancing now...");
+
+        /* We maintain a simple state engine here to keep track of what we are doing. We'll first shrink all
+         * homes that shall be shrinked and then grow all homes that shall be grown, so that they can take up
+         * the space now freed. */
+
+        for (;;) {
+                switch (m->rebalance_state) {
+
+                case REBALANCE_IDLE:
+                case REBALANCE_PENDING:
+                case REBALANCE_WAITING:
+                        /* First shrink large home dirs */
+                        m->rebalance_state = REBALANCE_SHRINKING;
+                        busy_state = REBALANCE_PENDING;
+                        log_debug("Shrinking phase..");
+                        break;
+
+                case REBALANCE_SHRINKING:
+                        /* Then grow small home dirs */
+                        m->rebalance_state = REBALANCE_GROWING;
+                        busy_state = REBALANCE_SHRINKING;
+                        log_debug("Growing phase..");
+                        break;
+
+                case REBALANCE_GROWING:
+                        /* Finally, we are done */
+                        log_info("Rebalancing complete.");
+                        m->rebalance_state = REBALANCE_IDLE;
+                        r = 0;
+                        goto finish;
+
+                case REBALANCE_OFF:
+                default:
+                        assert_not_reached();
+                }
+
+                r = manager_rebalance_calculate(m);
+                if (r == -EBUSY) {
+                        /* Calculations failed because one home directory is currently busy. Revert to a state that
+                         * tells us what to do next. */
+                        log_debug("Can't enter phase, busy.");
+                        m->rebalance_state = busy_state;
+                        return r;
+                }
+                if (r < 0)
+                        goto finish;
+                if (r == 0)
+                        continue; /* got to next step immediately, if there's nothing to do */
+
+                r = manager_rebalance_apply(m);
+                if (r < 0)
+                        goto finish;
+                if (r > 0)
+                        break; /* At least one resize operation is now pending, we are done for now */
+
+                /* If there was nothing to apply, go for next state right-away */
+        }
+
+        return 0;
+
+finish:
+        /* Reset state and schedule next rebalance */
+        m->rebalance_state = REBALANCE_IDLE;
+        (void) manager_schedule_rebalance(m, /* immediately= */ false);
+        return r;
+}
+
+static int on_rebalance_timer(sd_event_source *s, usec_t t, void *userdata) {
+        Manager *m = userdata;
+
+        assert(s);
+        assert(m);
+        assert(IN_SET(m->rebalance_state, REBALANCE_WAITING, REBALANCE_PENDING, REBALANCE_SHRINKING, REBALANCE_GROWING));
+
+        (void) manager_rebalance_now(m);
+        return 0;
+}
+
+int manager_schedule_rebalance(Manager *m, bool immediately) {
+        int r;
+
+        assert(m);
+
+        /* Check if there are any records where rebalancing is requested */
+        if (!manager_shall_rebalance(m)) {
+                log_debug("Not scheduling rebalancing, not needed.");
+                goto turn_off;
+        }
+
+        if (immediately) {
+                /* If we are told to rebalance immediately, then mark a rebalance as pending (even if we area
+                 * already running one) */
+
+                if (m->rebalance_event_source) {
+                        r = sd_event_source_set_time(m->rebalance_event_source, 0);
+                        if (r < 0) {
+                                log_error_errno(r, "Failed to schedule immediate rebalancing: %m");
+                                goto turn_off;
+                        }
+
+                        r = sd_event_source_set_enabled(m->rebalance_event_source, SD_EVENT_ONESHOT);
+                        if (r < 0) {
+                                log_error_errno(r, "Failed to enable rebalancing event source: %m");
+                                goto turn_off;
+                        }
+                } else {
+                        r = sd_event_add_time(m->event, &m->rebalance_event_source, CLOCK_MONOTONIC, 0, USEC_PER_SEC, on_rebalance_timer, m);
+                        if (r < 0) {
+                                log_error_errno(r, "Failed to allocate rebalance event source: %m");
+                                goto turn_off;
+                        }
+
+                        r = sd_event_source_set_priority(m->rebalance_event_source, SD_EVENT_PRIORITY_IDLE + 10);
+                        if (r < 0) {
+                                log_error_errno(r, "Failed to set rebalance event source priority: %m");
+                                goto turn_off;
+                        }
+
+                        (void) sd_event_source_set_description(m->rebalance_event_source, "rebalance");
+
+                }
+
+                if (!IN_SET(m->rebalance_state, REBALANCE_PENDING, REBALANCE_SHRINKING, REBALANCE_GROWING))
+                        m->rebalance_state = REBALANCE_PENDING;
+
+                log_debug("Scheduled immediate rebalancing...");
+                return 0;
+        }
+
+        /* If we are told to schedule a rebalancing eventually, then do so only if we are not executing
+         * anything yet. Also if we have something scheduled already, leave it in place */
+        if (!IN_SET(m->rebalance_state, REBALANCE_OFF, REBALANCE_IDLE))
+                return 0;
+
+        if (m->rebalance_event_source) {
+                r = sd_event_source_set_time_relative(m->rebalance_event_source, m->rebalance_interval_usec);
+                if (r < 0) {
+                        log_error_errno(r, "Failed to schedule immediate rebalancing: %m");
+                        goto turn_off;
+                }
+
+                r = sd_event_source_set_enabled(m->rebalance_event_source, SD_EVENT_ONESHOT);
+                if (r < 0) {
+                        log_error_errno(r, "Failed to enable rebalancing event source: %m");
+                        goto turn_off;
+                }
+        } else {
+                r = sd_event_add_time_relative(m->event, &m->rebalance_event_source, CLOCK_MONOTONIC, m->rebalance_interval_usec, USEC_PER_SEC, on_rebalance_timer, m);
+                if (r < 0) {
+                        log_error_errno(r, "Failed to allocate rebalance event source: %m");
+                        goto turn_off;
+                }
+
+                r = sd_event_source_set_priority(m->rebalance_event_source, SD_EVENT_PRIORITY_IDLE + 10);
+                if (r < 0) {
+                        log_error_errno(r, "Failed to set rebalance event source priority: %m");
+                        goto turn_off;
+                }
+
+                (void) sd_event_source_set_description(m->rebalance_event_source, "rebalance");
+        }
+
+        m->rebalance_state = REBALANCE_WAITING; /* We managed to enqueue a timer event, we now wait until it fires */
+        log_debug("Scheduled rebalancing in %s...", FORMAT_TIMESPAN(m->rebalance_interval_usec, 0));
+        return 0;
+
+turn_off:
+        m->rebalance_event_source = sd_event_source_disable_unref(m->rebalance_event_source);
+        m->rebalance_state = REBALANCE_OFF;
+        return r;
+}
+
+int manager_reschedule_rebalance(Manager *m) {
+        int r;
+
+        assert(m);
+
+        /* If a rebalance is pending reschedules it so it gets executed immediately */
+
+        if (!IN_SET(m->rebalance_state, REBALANCE_PENDING, REBALANCE_SHRINKING, REBALANCE_GROWING))
+                return 0;
+
+        r = manager_schedule_rebalance(m, /* immediately= */ true);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
index f5659636c382719a579fe5d57904c5ccbdba05c7..cf6d58b2586e2db468454a73e7b8268b58f65696 100644 (file)
@@ -13,6 +13,18 @@ typedef struct Manager Manager;
 #include "homed-home.h"
 #include "varlink.h"
 
+/* The LUKS free disk space rebalancing logic goes through this state machine */
+typedef enum RebalanceState {
+        REBALANCE_OFF,       /* No rebalancing enabled */
+        REBALANCE_IDLE,      /* Rebalancing enabled, but currently nothing scheduled */
+        REBALANCE_WAITING,   /* Rebalancing has been requested for a later point in time */
+        REBALANCE_PENDING,   /* Rebalancing has been requested and will be executed ASAP */
+        REBALANCE_SHRINKING, /* Rebalancing ongoing, and we are running all shrinking operations */
+        REBALANCE_GROWING,   /* Rebalancing ongoign, and we are running all growing operations */
+        _REBALANCE_STATE_MAX,
+        _REBALANCE_STATE_INVALID = -1,
+} RebalanceState;
+
 struct Manager {
         sd_event *event;
         sd_bus *bus;
@@ -39,6 +51,8 @@ struct Manager {
         sd_event_source *deferred_gc_event_source;
         sd_event_source *deferred_auto_login_event_source;
 
+        sd_event_source *rebalance_event_source;
+
         Home *gc_focus;
 
         VarlinkServer *varlink_server;
@@ -46,6 +60,9 @@ struct Manager {
 
         EVP_PKEY *private_key; /* actually a pair of private and public key */
         Hashmap *public_keys; /* key name [char*] → publick key [EVP_PKEY*] */
+
+        RebalanceState rebalance_state;
+        usec_t rebalance_interval_usec;
 };
 
 int manager_new(Manager **ret);
@@ -59,6 +76,9 @@ int manager_augment_record_with_uid(Manager *m, UserRecord *hr);
 int manager_enqueue_rescan(Manager *m);
 int manager_enqueue_gc(Manager *m, Home *focus);
 
+int manager_schedule_rebalance(Manager *m, bool immediately);
+int manager_reschedule_rebalance(Manager *m);
+
 int manager_verify_user_record(Manager *m, UserRecord *hr);
 
 int manager_acquire_key_pair(Manager *m);
index 520b6b6f06152ec6f9d0a2e548eca1724263235c..db6c15242ff67161c0c54c88683fa33c4d8466f2 100644 (file)
@@ -1639,7 +1639,7 @@ static int home_update(UserRecord *h, UserRecord **ret) {
         return 0;
 }
 
-static int home_resize(UserRecord *h, UserRecord **ret) {
+static int home_resize(UserRecord *h, bool automatic, UserRecord **ret) {
         _cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT;
         _cleanup_(password_cache_free) PasswordCache cache = {};
         HomeSetupFlags flags = 0;
@@ -1651,15 +1651,26 @@ static int home_resize(UserRecord *h, UserRecord **ret) {
         if (h->disk_size == UINT64_MAX)
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No target size specified, refusing.");
 
-        r = user_record_authenticate(h, h, &cache, /* strict_verify= */ true);
-        if (r < 0)
-                return r;
-        assert(r > 0); /* Insist that a password was verified */
+        if (automatic)
+                /* In automatic mode don't want to ask the user for the password, hence load it from the kernel keyring */
+                password_cache_load_keyring(h, &cache);
+        else {
+                /* In manual mode let's ensure the user is fully authenticated */
+                r = user_record_authenticate(h, h, &cache, /* strict_verify= */ true);
+                if (r < 0)
+                        return r;
+                assert(r > 0); /* Insist that a password was verified */
+        }
 
         r = home_validate_update(h, &setup, &flags);
         if (r < 0)
                 return r;
 
+        /* In automatic mode let's skip syncing identities, because we can't validate them, since we can't
+         * ask the user for reauthentication */
+        if (automatic)
+                flags |= HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES;
+
         switch (user_record_storage(h)) {
 
         case USER_LUKS:
@@ -1931,8 +1942,10 @@ static int run(int argc, char *argv[]) {
                 r = home_remove(home);
         else if (streq(argv[1], "update"))
                 r = home_update(home, &new_home);
-        else if (streq(argv[1], "resize"))
-                r = home_resize(home, &new_home);
+        else if (streq(argv[1], "resize")) /* Resize on user request */
+                r = home_resize(home, false, &new_home);
+        else if (streq(argv[1], "resize-auto")) /* Automatic resize */
+                r = home_resize(home, true, &new_home);
         else if (streq(argv[1], "passwd"))
                 r = home_passwd(home, &new_home);
         else if (streq(argv[1], "inspect"))
index c4746cedc66065541a87e41da9f9c0f2a9d1e181..f4578783f3aa8a91fa99463db0f2013fbbe9e126 100644 (file)
@@ -1387,3 +1387,129 @@ int user_record_is_supported(UserRecord *hr, sd_bus_error *error) {
 
         return 0;
 }
+
+bool user_record_shall_rebalance(UserRecord *h) {
+        assert(h);
+
+        if (user_record_rebalance_weight(h) == REBALANCE_WEIGHT_OFF)
+                return false;
+
+        if (user_record_storage(h) != USER_LUKS)
+                return false;
+
+        if (!path_startswith(user_record_image_path(h), get_home_root())) /* This is the only pool we rebalance in */
+                return false;
+
+        return true;
+}
+
+int user_record_set_rebalance_weight(UserRecord *h, uint64_t weight) {
+        _cleanup_(json_variant_unrefp) JsonVariant *new_per_machine_array = NULL, *machine_id_variant = NULL,
+                *machine_id_array = NULL, *per_machine_entry = NULL;
+        _cleanup_free_ JsonVariant **array = NULL;
+        size_t idx = SIZE_MAX, n;
+        JsonVariant *per_machine;
+        sd_id128_t mid;
+        int r;
+
+        assert(h);
+
+        if (!h->json)
+                return -EUNATCH;
+
+        r = sd_id128_get_machine(&mid);
+        if (r < 0)
+                return r;
+
+        r = json_variant_new_id128(&machine_id_variant, mid);
+        if (r < 0)
+                return r;
+
+        r = json_variant_new_array(&machine_id_array, (JsonVariant*[]) { machine_id_variant }, 1);
+        if (r < 0)
+                return r;
+
+        per_machine = json_variant_by_key(h->json, "perMachine");
+        if (per_machine) {
+                if (!json_variant_is_array(per_machine))
+                        return -EINVAL;
+
+                n = json_variant_elements(per_machine);
+
+                array = new(JsonVariant*, n + 1);
+                if (!array)
+                        return -ENOMEM;
+
+                for (size_t i = 0; i < n; i++) {
+                        JsonVariant *m;
+
+                        array[i] = json_variant_by_index(per_machine, i);
+
+                        if (!json_variant_is_object(array[i]))
+                                return -EINVAL;
+
+                        m = json_variant_by_key(array[i], "matchMachineId");
+                        if (!m) {
+                                /* No machineId field? Let's ignore this, but invalidate what we found so far */
+                                idx = SIZE_MAX;
+                                continue;
+                        }
+
+                        if (json_variant_equal(m, machine_id_variant) ||
+                            json_variant_equal(m, machine_id_array)) {
+                                /* Matches exactly what we are looking for. Let's use this */
+                                idx = i;
+                                continue;
+                        }
+
+                        r = per_machine_id_match(m, JSON_PERMISSIVE);
+                        if (r < 0)
+                                return r;
+                        if (r > 0)
+                                /* Also matches what we are looking for, but with a broader match. In this
+                                 * case let's ignore this entry, and add a new specific one to the end. */
+                                idx = SIZE_MAX;
+                }
+
+                if (idx == SIZE_MAX)
+                        idx = n++; /* Nothing suitable found, place new entry at end */
+                else
+                        per_machine_entry = json_variant_ref(array[idx]);
+
+        } else {
+                array = new(JsonVariant*, 1);
+                if (!array)
+                        return -ENOMEM;
+
+                idx = 0;
+                n = 1;
+        }
+
+        if (!per_machine_entry) {
+                r = json_variant_set_field(&per_machine_entry, "matchMachineId", machine_id_array);
+                if (r < 0)
+                        return r;
+        }
+
+        if (weight == REBALANCE_WEIGHT_UNSET)
+                r = json_variant_set_field(&per_machine_entry, "rebalanceWeight", NULL); /* set explicitly to NULL (so that the perMachine setting we are setting here can override the global setting) */
+        else
+                r = json_variant_set_field_unsigned(&per_machine_entry, "rebalanceWeight", weight);
+        if (r < 0)
+                return r;
+
+        assert(idx < n);
+        array[idx] = per_machine_entry;
+
+        r = json_variant_new_array(&new_per_machine_array, array, n);
+        if (r < 0)
+                return r;
+
+        r = json_variant_set_field(&h->json, "perMachine", new_per_machine_array);
+        if (r < 0)
+                return r;
+
+        h->rebalance_weight = weight;
+        h->mask |= USER_RECORD_PER_MACHINE;
+        return 0;
+}
index 74f4a0eaab28fe6c170a29aa7b9122173435a2be..508e2bdb8d9dbfd9eeb23f001203b70d7bfd8988 100644 (file)
@@ -60,3 +60,6 @@ int user_record_bad_authentication(UserRecord *h);
 int user_record_ratelimit(UserRecord *h);
 
 int user_record_is_supported(UserRecord *hr, sd_bus_error *error);
+
+bool user_record_shall_rebalance(UserRecord *h);
+int user_record_set_rebalance_weight(UserRecord *h, uint64_t weight);
index 276b9150678407de2c6500d498972610bf133ddb..9bd77cffeb2ecfa9360093b96a070680c1407622 100644 (file)
@@ -223,6 +223,7 @@ typedef enum AutoResizeMode {
 
 #define REBALANCE_WEIGHT_OFF UINT64_C(0)
 #define REBALANCE_WEIGHT_DEFAULT UINT64_C(100)
+#define REBALANCE_WEIGHT_BACKING UINT64_C(20)
 #define REBALANCE_WEIGHT_MIN UINT64_C(1)
 #define REBALANCE_WEIGHT_MAX UINT64_C(10000)
 #define REBALANCE_WEIGHT_UNSET UINT64_MAX