From: Lennart Poettering Date: Tue, 2 Nov 2021 22:11:59 +0000 (+0100) Subject: homed: add automatic grow/shrink ("rebalancing") X-Git-Tag: v250-rc1~146^2~4 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=d357b80d331ebed709b7a9d71e014c319ba5bd79;p=thirdparty%2Fsystemd.git homed: add automatic grow/shrink ("rebalancing") --- diff --git a/meson.build b/meson.build index 6263e7c0fc4..97732b62434 100644 --- a/meson.build +++ b/meson.build @@ -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) diff --git a/src/home/home-util.h b/src/home/home-util.h index ca4c068f371..69a88000f84 100644 --- a/src/home/home-util.h +++ b/src/home/home-util.h @@ -16,8 +16,13 @@ #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); diff --git a/src/home/homed-home-bus.c b/src/home/homed-home-bus.c index c71256d15e5..9e9f537d6c8 100644 --- a/src/home/homed-home-bus.c +++ b/src/home/homed-home-bus.c @@ -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; diff --git a/src/home/homed-home.c b/src/home/homed-home.c index 2cc1f8b384a..470c7f07f62 100644 --- a/src/home/homed-home.c +++ b/src/home/homed-home.c @@ -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", diff --git a/src/home/homed-home.h b/src/home/homed-home.h index 37c54b05572..0f314aad93c 100644 --- a/src/home/homed-home.h +++ b/src/home/homed-home.h @@ -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); diff --git a/src/home/homed-manager.c b/src/home/homed-manager.c index a97a0dee766..3851234a37b 100644 --- a/src/home/homed-manager.c +++ b/src/home/homed-manager.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -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; +} diff --git a/src/home/homed-manager.h b/src/home/homed-manager.h index f5659636c38..cf6d58b2586 100644 --- a/src/home/homed-manager.h +++ b/src/home/homed-manager.h @@ -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); diff --git a/src/home/homework.c b/src/home/homework.c index 520b6b6f061..db6c15242ff 100644 --- a/src/home/homework.c +++ b/src/home/homework.c @@ -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")) diff --git a/src/home/user-record-util.c b/src/home/user-record-util.c index c4746cedc66..f4578783f3a 100644 --- a/src/home/user-record-util.c +++ b/src/home/user-record-util.c @@ -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; +} diff --git a/src/home/user-record-util.h b/src/home/user-record-util.h index 74f4a0eaab2..508e2bdb8d9 100644 --- a/src/home/user-record-util.h +++ b/src/home/user-record-util.h @@ -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); diff --git a/src/shared/user-record.h b/src/shared/user-record.h index 276b9150678..9bd77cffeb2 100644 --- a/src/shared/user-record.h +++ b/src/shared/user-record.h @@ -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