/* * Copyright (C) 2012 Jan Luebbe * Copyright (C) 2015 Marc Kleine-Budde * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ #define pr_fmt(fmt) "bootchooser: " fmt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define BOOTCHOOSER_PREFIX "global.bootchooser" static char *available_targets; static char *state_prefix; static int global_default_attempts = 3; static int global_default_priority = 1; static int disable_on_zero_attempts; static int retry; static int last_boot_successful; struct bootchooser { struct list_head targets; struct bootchooser_target *last_chosen; struct state *state; char *state_prefix; int refs; int verbose; int dryrun; }; struct bootchooser_target { struct bootchooser *bootchooser; struct list_head list; /* state */ unsigned int priority; unsigned int remaining_attempts; int id; /* spec */ char *name; unsigned int default_attempts; unsigned int default_priority; char *boot; char *prefix; char *state_prefix; }; enum reset_attempts { RESET_ATTEMPTS_POWER_ON, RESET_ATTEMPTS_ALL_ZERO, }; static unsigned long reset_attempts; enum reset_priorities { RESET_PRIORITIES_ALL_ZERO, }; static unsigned long reset_priorities; static int bootchooser_target_ok(struct bootchooser_target *target, const char **reason) { if (!target->priority) { if (reason) *reason = "Target disabled (priority = 0)"; return false; } if (!target->remaining_attempts) { if (reason) *reason = "remaining attempts = 0"; return false; } if (reason) *reason = "target OK"; return true; } static void pr_target(struct bootchooser_target *target) { const char *reason; int ok; ok = bootchooser_target_ok(target, &reason); printf("%s\n" " id: %u\n" " priority: %u\n" " default_priority: %u\n" " remaining attempts: %u\n" " default attempts: %u\n" " boot: '%s'\n", target->name, target->id, target->priority, target->default_priority, target->remaining_attempts, target->default_attempts, target->boot); if (!ok) printf(" disabled due to %s\n", reason); } static int pr_setenv(struct bootchooser *bc, const char *fmt, ...) { va_list ap; int ret = 0; char *str, *val; const char *oldval; va_start(ap, fmt); str = bvasprintf(fmt, ap); va_end(ap); if (!str) return -ENOMEM; val = strchr(str, '='); if (!val) { ret = -EINVAL; goto err; } *val++ = '\0'; oldval = getenv(str); if (!oldval || strcmp(oldval, val)) { if (bc->state) ret = setenv(str, val); else ret = nvvar_add(str, val); } err: free(str); return ret; } static const char *pr_getenv(const char *fmt, ...) { va_list ap; char *str; const char *val; va_start(ap, fmt); str = bvasprintf(fmt, ap); va_end(ap); if (!str) return NULL; val = getenv(str); free(str); return val; } static int getenv_u32(const char *prefix, const char *name, uint32_t *retval) { char *str; const char *val; str = xasprintf("%s.%s", prefix, name); val = getenv(str); free(str); if (!val) return -ENOENT; *retval = simple_strtoul(val, NULL, 0); return 0; } static int bootchooser_target_compare(struct list_head *a, struct list_head *b) { struct bootchooser_target *bootchooser_a = list_entry(a, struct bootchooser_target, list); struct bootchooser_target *bootchooser_b = list_entry(b, struct bootchooser_target, list); /* order with descending priority */ return bootchooser_a->priority >= bootchooser_b->priority ? -1 : 1; } /** * bootchooser_target_new - Create a new bootchooser target * @bc: The bootchooser * @name: The name of the new target * * Parses the variables associated with @name, creates a bootchooser * target from it and returns it. */ static struct bootchooser_target *bootchooser_target_new(struct bootchooser *bc, const char *name) { struct bootchooser_target *target = xzalloc(sizeof(*target)); const char *val; int ret; target->name = xstrdup(name); target->prefix = basprintf("%s.%s", BOOTCHOOSER_PREFIX, name); target->state_prefix = basprintf("%s.%s", bc->state_prefix, name); target->default_attempts = global_default_attempts; target->default_priority = global_default_priority; getenv_u32(target->prefix, "default_priority", &target->default_priority); getenv_u32(target->prefix, "default_attempts", &target->default_attempts); ret = getenv_u32(target->state_prefix, "priority", &target->priority); if (ret) { pr_warn("Cannot read priority for target %s, using default %d\n", target->name, target->default_priority); target->priority = target->default_priority; } ret = getenv_u32(target->state_prefix, "remaining_attempts", &target->remaining_attempts); if (ret) { pr_warn("Cannot read remaining attempts for target %s, using default %d\n", target->name, target->default_attempts); target->remaining_attempts = target->default_attempts; } if (target->remaining_attempts && !target->priority) { pr_warn("Disabled target %s has remaining attempts %d, setting to 0\n", target->name, target->remaining_attempts); target->remaining_attempts = 0; } val = pr_getenv("%s.boot", target->prefix); if (!val) val = target->name; target->boot = xstrdup(val); return target; } /** * bootchooser_target_by_id - Return a target given its id * * Each target has an id, simply counted by the order they appear in * global.bootchooser.targets. We start counting at one to leave 0 * for detection of uninitialized variables. */ static struct bootchooser_target *bootchooser_target_by_id(struct bootchooser *bc, uint32_t id) { struct bootchooser_target *target; list_for_each_entry(target, &bc->targets, list) if (target->id == id) return target; return NULL; } /** * bootchooser_target_disable - Disable a bootchooser target */ static void bootchooser_target_disable(struct bootchooser_target *target) { target->priority = 0; target->remaining_attempts = 0; } /** * bootchooser_target_enabled - test if a target is enabled * * Returns true if a target is enabled, false if it's not. */ static bool bootchooser_target_enabled(struct bootchooser_target *target) { return target->priority != 0; } /** * bootchooser_reset_attempts - reset remaining attempts of targets * * Reset the remaining_attempts counter of all enabled targets * to their default values. */ static void bootchooser_reset_attempts(struct bootchooser *bc) { struct bootchooser_target *target; bootchooser_for_each_target(bc, target) { if (bootchooser_target_enabled(target)) bootchooser_target_set_attempts(target, -1); } } /** * bootchooser_reset_priorities - reset priorities of targets * * Reset the priorities counter of all targets to their default * values. */ static void bootchooser_reset_priorities(struct bootchooser *bc) { struct bootchooser_target *target; bootchooser_for_each_target(bc, target) bootchooser_target_set_priority(target, -1); } static struct bootchooser *bootchooser; /** * bootchooser_get - get a reference to the bootchooser * * When no bootchooser is initialized this function allocates the bootchooser * and initializes it with the different globalvars and state variables. The * bootchooser is returned. Subsequent calls will return a reference to the same * bootchooser. */ struct bootchooser *bootchooser_get(void) { struct bootchooser *bc; struct bootchooser_target *target; char *targets, *str, *freep = NULL, *delim; int ret = -EINVAL, id = 1; uint32_t last_chosen; static int attempts_resetted; if (bootchooser) { bootchooser->refs++; return bootchooser; } bc = xzalloc(sizeof(*bc)); if (*state_prefix) { if (IS_ENABLED(CONFIG_STATE)) { char *state_devname; delim = strchr(state_prefix, '.'); if (delim) state_devname = xstrndup(state_prefix, delim - state_prefix); else state_devname = xstrdup(state_prefix); bc->state_prefix = xstrdup(state_prefix); bc->state = state_by_name(state_devname); if (!bc->state) { pr_err("Cannot get state '%s'\n", state_devname); free(state_devname); ret = -ENODEV; goto err; } free(state_devname); } else { pr_err("State disabled, cannot use nv.state_prefix=%s\n", state_prefix); ret = -ENODEV; goto err; } } else { bc->state_prefix = xstrdup("nv.bootchooser"); } INIT_LIST_HEAD(&bc->targets); freep = targets = xstrdup(available_targets); while (1) { str = strsep(&targets, " "); if (!str || !*str) break; target = bootchooser_target_new(bc, str); if (!IS_ERR(target)) { target->id = id; list_add_sort(&target->list, &bc->targets, bootchooser_target_compare); } id++; } if (id == 1) { pr_err("Target list $global.bootchooser.targets is empty\n"); goto err; } if (list_empty(&bc->targets)) { pr_err("No targets could be initialized\n"); goto err; } free(freep); if (test_bit(RESET_PRIORITIES_ALL_ZERO, &reset_priorities)) { int priority = 0; bootchooser_for_each_target(bc, target) priority += target->priority; if (!priority) { pr_info("All targets disabled, re-enabling them\n"); bootchooser_reset_priorities(bc); } } if (test_bit(RESET_ATTEMPTS_POWER_ON, &reset_attempts) && reset_source_get() == RESET_POR && !attempts_resetted) { pr_info("Power-on Reset, resetting remaining attempts\n"); bootchooser_reset_attempts(bc); attempts_resetted = 1; } if (test_bit(RESET_ATTEMPTS_ALL_ZERO, &reset_attempts)) { int attempts = 0; bootchooser_for_each_target(bc, target) attempts += target->remaining_attempts; if (!attempts) { pr_info("All enabled targets have 0 remaining attempts, resetting them\n"); bootchooser_reset_attempts(bc); } } ret = getenv_u32(bc->state_prefix, "last_chosen", &last_chosen); if (!ret && last_chosen > 0) { bc->last_chosen = bootchooser_target_by_id(bc, last_chosen); if (!bc->last_chosen) pr_warn("Last booted target with id %d does not exist\n", last_chosen); } if (bc->last_chosen && last_boot_successful) bootchooser_target_set_attempts(bc->last_chosen, -1); if (disable_on_zero_attempts) { bootchooser_for_each_target(bc, target) { if (!target->remaining_attempts) { pr_info("target %s has 0 remaining attempts, disabling\n", target->name); bootchooser_target_disable(target); } } } bootchooser = bc; bootchooser->refs = 1; return bc; err: free(freep); free(bc); return ERR_PTR(ret); } /** * bootchooser_save - save a bootchooser to the backing store * @bc: The bootchooser instance to save * * Return: 0 for success, negative error code otherwise */ int bootchooser_save(struct bootchooser *bc) { struct bootchooser_target *target; int ret; if (bc->last_chosen) pr_setenv(bc, "%s.last_chosen=%d", bc->state_prefix, bc->last_chosen->id); list_for_each_entry(target, &bc->targets, list) { ret = pr_setenv(bc, "%s.remaining_attempts=%d", target->state_prefix, target->remaining_attempts); if (ret) return ret; ret = pr_setenv(bc, "%s.priority=%d", target->state_prefix, target->priority); if (ret) return ret; } if (IS_ENABLED(CONFIG_STATE) && bc->state) { ret = state_save(bc->state); if (ret) { pr_err("Cannot save state: %s\n", strerror(-ret)); return ret; } } else { ret = nvvar_save(); if (ret) { pr_err("Cannot save nv variables: %s\n", strerror(-ret)); return ret; } } return 0; } /** * bootchooser_put - return a bootchooser reference * @bc: The bootchooser instance * * This returns a reference to the bootchooser. If it is the last reference the * bootchooser is saved and the associated memory is freed. * * Return: 0 for success or a negative error code. An error can occur when * bootchooser_save fails to write to the storage, nevertheless the * bootchooser reference is still released. */ int bootchooser_put(struct bootchooser *bc) { struct bootchooser_target *target, *tmp; int ret; bc->refs--; if (bc->refs) return 0; ret = bootchooser_save(bc); if (ret) pr_err("Failed to save bootchooser state: %s\n", strerror(-ret)); list_for_each_entry_safe(target, tmp, &bc->targets, list) { free(target->boot); free(target->prefix); free(target->state_prefix); free(target->name); free(target); } free(bc); bootchooser = NULL; return ret; } /** * bootchooser_info - Show information about a bootchooser instance * @bc: The bootchooser */ void bootchooser_info(struct bootchooser *bc) { struct bootchooser_target *target; const char *reason; int count = 0; printf("Good targets (first will be booted next):\n"); list_for_each_entry(target, &bc->targets, list) { if (bootchooser_target_ok(target, NULL)) { count++; pr_target(target); } } if (!count) printf("none\n"); count = 0; printf("\nDisabled targets:\n"); list_for_each_entry(target, &bc->targets, list) { if (!bootchooser_target_ok(target, &reason)) { count++; pr_target(target); } } if (!count) printf("none\n"); printf("\nlast booted target: %s\n", bc->last_chosen ? bc->last_chosen->name : "unknown"); } /** * bootchooser_get_target - get the target that shall be booted next * @bc: The bootchooser * * This is the heart of the bootchooser. This function selects the next * target to boot and returns it. The remaining_attempts counter of the * selected target is decreased and the bootchooser state is saved to the * backend. * * Return: The next target */ static struct bootchooser_target *bootchooser_get_target(struct bootchooser *bc) { struct bootchooser_target *target; list_for_each_entry(target, &bc->targets, list) { if (bootchooser_target_ok(target, NULL)) goto found; } pr_err("No valid targets found:\n"); list_for_each_entry(target, &bc->targets, list) pr_target(target); return ERR_PTR(-ENOENT); found: target->remaining_attempts--; if (bc->verbose) pr_info("name=%s decrementing remaining_attempts to %d\n", target->name, target->remaining_attempts); if (bc->verbose) pr_info("selected target '%s', boot '%s'\n", target->name, target->boot); bc->last_chosen = target; bootchooser_save(bc); return target; } /** * bootchooser_target_name - get the name of a target * @target: The target * * Given a bootchooser target this function returns its name. * * Return: The name of the target */ const char *bootchooser_target_name(struct bootchooser_target *target) { return target->name; } /** * bootchooser_target_by_name - get a target from name * @bc: The bootchooser * @name: The name of the target to retrieve * * Given a name this function returns the corresponding target. * * Return: The target if found, NULL otherwise */ struct bootchooser_target *bootchooser_target_by_name(struct bootchooser *bc, const char *name) { struct bootchooser_target *target; bootchooser_for_each_target(bc, target) if (!strcmp(target->name, name)) return target; return NULL; } /** * bootchooser_target_set_attempts - set remaining attempts of a target * @target: The target to change * @attempts: The number of attempts * * This sets the number of remaining attempts for a bootchooser target. * If @attempts is < 0 then the remaining attempts is reset to the default * value. * * Return: 0 for success, negative error code otherwise */ int bootchooser_target_set_attempts(struct bootchooser_target *target, int attempts) { if (attempts >= 0) target->remaining_attempts = attempts; else target->remaining_attempts = target->default_attempts; return 0; } /** * bootchooser_target_set_priority - set priority of a target * @target: The target to change * @priority: The priority * * This sets the priority of a bootchooser target. If @priority is < 0 * then the priority reset to the default value. * * Return: 0 for success, negative error code otherwise */ int bootchooser_target_set_priority(struct bootchooser_target *target, int priority) { if (priority >= 0) target->priority = priority; else target->priority = target->default_priority; return 0; } /** * bootchooser_target_first - get the first target from a bootchooser * @bc: The bootchooser * * Gets the first target from a bootchooser, used for the bootchooser * target iterator. * * Return: The first target or NULL if no target exists */ struct bootchooser_target *bootchooser_target_first(struct bootchooser *bc) { return list_first_entry_or_null(&bc->targets, struct bootchooser_target, list); } /** * bootchooser_target_next - get the next target from a bootchooser * @bc: The bootchooser * @target: The current target * * Gets the next target from a bootchooser, used for the bootchooser * target iterator. * * Return: The first target or NULL if no more targets exist */ struct bootchooser_target *bootchooser_target_next(struct bootchooser *bc, struct bootchooser_target *target) { struct list_head *next = target->list.next; if (next == &bc->targets) return NULL; return list_entry(next, struct bootchooser_target, list); } /** * bootchooser_last_boot_successful - tell that the last boot was successful * * This tells bootchooser that the last boot was successful. */ void bootchooser_last_boot_successful(void) { last_boot_successful = true; } /** * bootchooser_get_last_chosen - get the target which was chosen last time * @bc: The bootchooser * * Bootchooser stores the id of the target which was last booted in * .last_chosen. This function returns the target associated * with this id. * * Return: The target which was booted last time */ struct bootchooser_target *bootchooser_get_last_chosen(struct bootchooser *bc) { if (!bc->last_chosen) return ERR_PTR(-ENODEV); return bc->last_chosen; } static int bootchooser_boot_one(struct bootchooser *bc, int *tryagain) { char *system; struct bootentries *entries; struct bootentry *entry; struct bootchooser_target *target; int ret = 0; entries = bootentries_alloc(); target = bootchooser_get_target(bc); if (IS_ERR(target)) { ret = PTR_ERR(target); *tryagain = 0; goto out; } system = basprintf("bootchooser.active=%s", target->name); globalvar_add_simple("linux.bootargs.bootchooser", system); free(system); ret = bootentry_create_from_name(entries, target->boot); if (ret <= 0) { printf("Nothing bootable found on '%s'\n", target->boot); *tryagain = 1; ret = -ENODEV; goto out; } last_boot_successful = false; ret = -ENOENT; bootentries_for_each_entry(entries, entry) { ret = boot_entry(entry, bc->verbose, bc->dryrun); if (!ret) { *tryagain = 0; goto out; } } *tryagain = 1; out: globalvar_set_match("linux.bootargs.bootchooser", NULL); bootentries_free(entries); return ret; } int bootchooser_boot(struct bootchooser *bc) { int ret, tryagain; do { ret = bootchooser_boot_one(bc, &tryagain); if (!retry) break; } while (tryagain); return ret; } static int bootchooser_entry_boot(struct bootentry *entry, int verbose, int dryrun) { struct bootchooser *bc; int ret; bc = bootchooser_get(); if (IS_ERR(bc)) return PTR_ERR(bc); bc->verbose = verbose; bc->dryrun = dryrun; ret = bootchooser_boot(bc); bootchooser_put(bc); return ret; } static void bootchooser_release(struct bootentry *entry) { free(entry); } /** * bootchooser_create_bootentry - create a boot entry * @entries: The list of bootentries * * This adds a bootchooser to the list of boot entries. Called * by the 'boot' code. * * Return: The number of entries added to the list */ static int bootchooser_add_entry(struct bootentries *entries, const char *name) { struct bootchooser *bc; struct bootentry *entry; if (strcmp(name, "bootchooser")) return 0; bc = bootchooser_get(); if (IS_ERR(bc)) return PTR_ERR(bc); entry = xzalloc(sizeof(*entry)); entry->boot = bootchooser_entry_boot; entry->release = bootchooser_release; entry->title = xstrdup("bootchooser"); entry->description = xstrdup("bootchooser"); bootentries_add_entry(entries, entry); bootchooser_put(bc); return 1; } static const char * const reset_attempts_names[] = { [RESET_ATTEMPTS_POWER_ON] = "power-on", [RESET_ATTEMPTS_ALL_ZERO] = "all-zero", }; static const char * const reset_priorities_names[] = { [RESET_PRIORITIES_ALL_ZERO] = "all-zero", }; static int bootchooser_init(void) { state_prefix = xstrdup(""); available_targets = xstrdup(""); globalvar_add_simple_bool("bootchooser.disable_on_zero_attempts", &disable_on_zero_attempts); globalvar_add_simple_bool("bootchooser.retry", &retry); globalvar_add_simple_string("bootchooser.targets", &available_targets); globalvar_add_simple_string("bootchooser.state_prefix", &state_prefix); globalvar_add_simple_int("bootchooser.default_attempts", &global_default_attempts, "%u"); globalvar_add_simple_int("bootchooser.default_priority", &global_default_priority, "%u"); globalvar_add_simple_bitmask("bootchooser.reset_attempts", &reset_attempts, reset_attempts_names, ARRAY_SIZE(reset_attempts_names)); globalvar_add_simple_bitmask("bootchooser.reset_priorities", &reset_priorities, reset_priorities_names, ARRAY_SIZE(reset_priorities_names)); bootentry_register_provider(bootchooser_add_entry); return 0; } device_initcall(bootchooser_init); BAREBOX_MAGICVAR_NAMED(global_bootchooser_disable_on_zero_attempts, global.bootchooser.disable_on_zero_attempts, "bootchooser: Disable target when remaining attempts counter reaches 0"); BAREBOX_MAGICVAR_NAMED(global_bootchooser_retry, global.bootchooser.retry, "bootchooser: Try again when booting a target fails"); BAREBOX_MAGICVAR_NAMED(global_bootchooser_targets, global.bootchooser.targets, "bootchooser: Space separated list of target names"); BAREBOX_MAGICVAR_NAMED(global_bootchooser_default_attempts, global.bootchooser.default_attempts, "bootchooser: Default number of attempts for a target"); BAREBOX_MAGICVAR_NAMED(global_bootchooser_default_priority, global.bootchooser.default_priority, "bootchooser: Default priority for a target"); BAREBOX_MAGICVAR_NAMED(global_bootchooser_state_prefix, global.bootchooser.state_prefix, "bootchooser: state name prefix, empty for nv backend");