diff options
Diffstat (limited to 'common/bootchooser.c')
-rw-r--r-- | common/bootchooser.c | 928 |
1 files changed, 928 insertions, 0 deletions
diff --git a/common/bootchooser.c b/common/bootchooser.c new file mode 100644 index 0000000000..9c110f267e --- /dev/null +++ b/common/bootchooser.c @@ -0,0 +1,928 @@ +/* + * Copyright (C) 2012 Jan Luebbe <j.luebbe@pengutronix.de> + * Copyright (C) 2015 Marc Kleine-Budde <mkl@pengutronix.de> + * + * 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 <bootchooser.h> +#include <environment.h> +#include <globalvar.h> +#include <magicvar.h> +#include <command.h> +#include <libfile.h> +#include <common.h> +#include <malloc.h> +#include <printk.h> +#include <xfuncs.h> +#include <envfs.h> +#include <errno.h> +#include <fcntl.h> +#include <ioctl.h> +#include <libbb.h> +#include <state.h> +#include <stdio.h> +#include <init.h> +#include <crc.h> +#include <net.h> +#include <fs.h> +#include <reset_source.h> + +#include <linux/kernel.h> +#include <linux/list.h> +#include <linux/err.h> + +#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 bootentry entry; + struct list_head targets; + struct bootchooser_target *last_chosen; + + struct state *state; + char *state_prefix; + + 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); +} + +/** + * bootchooser_get - get a bootchooser instance + * + * This evaluates the different globalvars and eventually state variables, + * creates a bootchooser instance from it and returns it. + */ +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; + + bc = xzalloc(sizeof(*bc)); + + if (*state_prefix) { + if (IS_ENABLED(CONFIG_STATE)) { + char *state_devname; + + delim = strchr(state_prefix, '.'); + if (!delim) { + pr_err("state_prefix '%s' has invalid format\n", + state_prefix); + goto err; + } + state_devname = xstrndup(state_prefix, delim - state_prefix); + bc->state_prefix = xstrdup(state_prefix); + bc->state = state_by_name(state_devname); + if (!bc->state) { + free(state_devname); + pr_err("Cannot get state '%s'\n", + 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); + } + } + + } + + 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 - release a bootchooser instance + * @bc: The bootchooser instance + * + * This releases a bootchooser instance and the memory associated with it. + */ +int bootchooser_put(struct bootchooser *bc) +{ + struct bootchooser_target *target, *tmp; + int ret; + + 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); + + 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 + */ +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 + * <state_prefix>.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; +} + +static int bootchooser_boot(struct bootentry *entry, int verbose, int dryrun) +{ + struct bootchooser *bc = container_of(entry, struct bootchooser, + entry); + int ret, tryagain; + + bc->verbose = verbose; + bc->dryrun = dryrun; + + do { + ret = bootchooser_boot_one(bc, &tryagain); + + if (!retry) + break; + } while (tryagain); + + return ret; +} + +static void bootchooser_release(struct bootentry *entry) +{ + struct bootchooser *bc = container_of(entry, struct bootchooser, + entry); + + bootchooser_put(bc); +} + +/** + * 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 + */ +int bootchooser_create_bootentry(struct bootentries *entries) +{ + struct bootchooser *bc = bootchooser_get(); + + if (IS_ERR(bc)) + return PTR_ERR(bc); + + bc->entry.boot = bootchooser_boot; + bc->entry.release = bootchooser_release; + bc->entry.title = xstrdup("bootchooser"); + bc->entry.description = xstrdup("bootchooser"); + + bootentries_add_entry(entries, &bc->entry); + + 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)); + 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"); |