discover: extend URL in UI to support auto-discovery
[petitboot] / discover / device-handler.c
index 64fc9fab2a5031556ea5e27a76be98903629999a..5df070073b4d298c8a49a5488010434b73ff4f32 100644 (file)
@@ -26,6 +26,7 @@
 
 #include "device-handler.h"
 #include "discover-server.h"
+#include "devmapper.h"
 #include "user-event.h"
 #include "platform.h"
 #include "event.h"
 #include "boot.h"
 #include "udev.h"
 #include "network.h"
+#include "ipmi.h"
+
+enum default_priority {
+       DEFAULT_PRIORITY_REMOTE         = 1,
+       DEFAULT_PRIORITY_LOCAL_FIRST    = 2,
+       DEFAULT_PRIORITY_LOCAL_LAST     = 0xfe,
+       DEFAULT_PRIORITY_DISABLED       = 0xff,
+};
 
 struct device_handler {
        struct discover_server  *server;
@@ -48,12 +57,17 @@ struct device_handler {
        struct discover_device  **devices;
        unsigned int            n_devices;
 
+       struct ramdisk_device   **ramdisks;
+       unsigned int            n_ramdisks;
+
        struct waitset          *waitset;
        struct waiter           *timeout_waiter;
        bool                    autoboot_enabled;
        unsigned int            sec_to_boot;
 
        struct discover_boot_option *default_boot_option;
+       int                     default_boot_option_priority;
+
        struct list             unresolved_boot_options;
 
        struct boot_task        *pending_boot;
@@ -309,6 +323,7 @@ struct device_handler *device_handler_init(struct discover_server *server,
 void device_handler_reinit(struct device_handler *handler)
 {
        struct discover_boot_option *opt, *tmp;
+       struct ramdisk_device *ramdisk;
        unsigned int i;
 
        device_handler_cancel_default(handler);
@@ -320,13 +335,20 @@ void device_handler_reinit(struct device_handler *handler)
        list_init(&handler->unresolved_boot_options);
 
        /* drop all devices */
-       for (i = 0; i < handler->n_devices; i++)
+       for (i = 0; i < handler->n_devices; i++) {
                discover_server_notify_device_remove(handler->server,
                                handler->devices[i]->device);
+               ramdisk = handler->devices[i]->ramdisk;
+               talloc_free(handler->devices[i]);
+               talloc_free(ramdisk);
+       }
 
        talloc_free(handler->devices);
        handler->devices = NULL;
        handler->n_devices = 0;
+       talloc_free(handler->ramdisks);
+       handler->ramdisks = NULL;
+       handler->n_ramdisks = 0;
 
        device_handler_reinit_sources(handler);
 }
@@ -429,75 +451,125 @@ static int default_timeout(void *arg)
        return 0;
 }
 
-static bool priority_match(struct boot_priority *prio,
-               struct discover_boot_option *opt)
+struct {
+       enum ipmi_bootdev       ipmi_type;
+       enum device_type        device_type;
+} device_type_map[] = {
+       { IPMI_BOOTDEV_NETWORK, DEVICE_TYPE_NETWORK },
+       { IPMI_BOOTDEV_DISK, DEVICE_TYPE_DISK },
+       { IPMI_BOOTDEV_DISK, DEVICE_TYPE_USB },
+       { IPMI_BOOTDEV_CDROM, DEVICE_TYPE_OPTICAL },
+};
+
+static bool ipmi_device_type_matches(enum ipmi_bootdev ipmi_type,
+               enum device_type device_type)
 {
-       return prio->type == opt->device->device->type ||
-               prio->type == DEVICE_TYPE_ANY;
+       unsigned int i;
+
+       for (i = 0; i < ARRAY_SIZE(device_type_map); i++) {
+               if (device_type_map[i].device_type == device_type)
+                       return device_type_map[i].ipmi_type == ipmi_type;
+       }
+
+       return false;
 }
 
-static int default_option_priority(struct discover_boot_option *opt)
+static int autoboot_option_priority(const struct config *config,
+                               struct discover_boot_option *opt)
 {
-       const struct config *config;
-       struct boot_priority *prio;
+       enum device_type type = opt->device->device->type;
+       const char *uuid = opt->device->uuid;
+       struct autoboot_option *auto_opt;
        unsigned int i;
 
-       config = config_get();
+       for (i = 0; i < config->n_autoboot_opts; i++) {
+               auto_opt = &config->autoboot_opts[i];
+               if (auto_opt->boot_type == BOOT_DEVICE_UUID)
+                       if (!strcmp(auto_opt->uuid, uuid))
+                               return DEFAULT_PRIORITY_LOCAL_FIRST + i;
 
-       for (i = 0; i < config->n_boot_priorities; i++) {
-               prio = &config->boot_priorities[i];
-               if (priority_match(prio, opt))
-                       return prio->priority;
+               if (auto_opt->boot_type == BOOT_DEVICE_TYPE)
+                       if (auto_opt->type == type ||
+                           auto_opt->type == DEVICE_TYPE_ANY)
+                               return DEFAULT_PRIORITY_LOCAL_FIRST + i;
        }
 
-       return 0;
+       return -1;
 }
 
-static bool device_allows_default(struct discover_device *dev)
+/*
+ * We have different priorities to resolve conflicts between boot options that
+ * report to be the default for their device. This function assigns a priority
+ * for these options.
+ */
+static enum default_priority default_option_priority(
+               struct discover_boot_option *opt)
 {
-       const char *dev_str;
+       const struct config *config;
 
-       dev_str = config_get()->boot_device;
+       config = config_get();
 
-       if (!dev_str || !strlen(dev_str))
-               return true;
+       /* We give highest priority to IPMI-configured boot options. If
+        * we have an IPMI bootdev configuration set, then we don't allow
+        * any other defaults */
+       if (config->ipmi_bootdev) {
+               bool ipmi_match = ipmi_device_type_matches(config->ipmi_bootdev,
+                               opt->device->device->type);
+               if (ipmi_match)
+                       return DEFAULT_PRIORITY_REMOTE;
+
+               pb_debug("handler: disabled default priority due to "
+                               "non-matching IPMI type %x\n",
+                               config->ipmi_bootdev);
+               return DEFAULT_PRIORITY_DISABLED;
+       }
 
-       /* default devices are specified by UUIDs at present */
-       if (strcmp(dev->uuid, dev_str))
-               return false;
+       /* Next, try to match the option against the user-defined autoboot
+        * options, either by device UUID or type. */
+       if (config->n_autoboot_opts) {
+               int boot_match = autoboot_option_priority(config, opt);
+               if (boot_match > 0)
+                       return boot_match;
+       }
 
-       return true;
+       /* If the option didn't match any entry in the array, it is disabled */
+       pb_debug("handler: disabled default priority due to "
+                       "non-matching UUID or type\n");
+       return DEFAULT_PRIORITY_DISABLED;
 }
 
 static void set_default(struct device_handler *handler,
                struct discover_boot_option *opt)
 {
-       int new_prio;
+       enum default_priority cur_prio, new_prio;
 
        if (!handler->autoboot_enabled)
                return;
 
-       /* do we allow default-booting from this device? */
-       if (!device_allows_default(opt->device))
-               return;
+       pb_debug("handler: new default option: %s\n", opt->option->id);
 
        new_prio = default_option_priority(opt);
 
-       /* A negative priority indicates that we don't want to boot this device
-        * by default */
-       if (new_prio < 0)
+       /* Anything outside our range prevents a default boot */
+       if (new_prio >= DEFAULT_PRIORITY_DISABLED)
                return;
 
+       pb_debug("handler: calculated priority %d\n", new_prio);
+
        /* Resolve any conflicts: if we have a new default option, it only
         * replaces the current if it has a higher priority. */
        if (handler->default_boot_option) {
-               int cur_prio;
 
-               cur_prio = default_option_priority(
-                                       handler->default_boot_option);
+               cur_prio = handler->default_boot_option_priority;
 
-               if (new_prio > cur_prio) {
+               if (new_prio < cur_prio) {
+                       pb_log("handler: new prio %d beats "
+                                       "old prio %d for %s\n",
+                                       new_prio, cur_prio,
+                                       handler->default_boot_option
+                                               ->option->id);
                        handler->default_boot_option = opt;
+                       handler->default_boot_option_priority = new_prio;
                        /* extend the timeout a little, so the user sees some
                         * indication of the change */
                        handler->sec_to_boot += 2;
@@ -508,8 +580,9 @@ static void set_default(struct device_handler *handler,
 
        handler->sec_to_boot = config_get()->autoboot_timeout_sec;
        handler->default_boot_option = opt;
+       handler->default_boot_option_priority = new_prio;
 
-       pb_log("Boot option %s set as default, timeout %u sec.\n",
+       pb_log("handler: boot option %s set as default, timeout %u sec.\n",
               opt->option->id, handler->sec_to_boot);
 
        default_timeout(handler);
@@ -688,6 +761,95 @@ void device_handler_add_device(struct device_handler *handler,
                network_register_device(handler->network, device);
 }
 
+void device_handler_add_ramdisk(struct device_handler *handler,
+               const char *path)
+{
+       struct ramdisk_device *dev;
+       unsigned int i;
+
+       if (!path)
+               return;
+
+       for (i = 0; i < handler->n_ramdisks; i++)
+               if (!strcmp(handler->ramdisks[i]->path, path))
+                       return;
+
+       dev = talloc_zero(handler, struct ramdisk_device);
+       if (!dev) {
+               pb_log("Failed to allocate memory to track %s\n", path);
+               return;
+       }
+
+       dev->path = talloc_strdup(handler, path);
+
+       handler->ramdisks = talloc_realloc(handler, handler->ramdisks,
+                               struct ramdisk_device *,
+                               handler->n_ramdisks + 1);
+       if (!handler->ramdisks) {
+               pb_log("Failed to reallocate memory"
+                      "- ramdisk tracking inconsistent!\n");
+               return;
+       }
+
+       handler->ramdisks[i] = dev;
+       i = handler->n_ramdisks++;
+}
+
+struct ramdisk_device *device_handler_get_ramdisk(
+               struct device_handler *handler)
+{
+       unsigned int i;
+       char *name;
+       dev_t id;
+
+       /* Check if free ramdisk exists */
+       for (i = 0; i < handler->n_ramdisks; i++)
+               if (!handler->ramdisks[i]->snapshot &&
+                   !handler->ramdisks[i]->origin &&
+                   !handler->ramdisks[i]->base)
+                       return handler->ramdisks[i];
+
+       /* Otherwise create a new one */
+       name = talloc_asprintf(handler, "/dev/ram%d",
+                       handler->n_ramdisks);
+       if (!name) {
+               pb_debug("Failed to allocate memory to name /dev/ram%d",
+                       handler->n_ramdisks);
+               return NULL;
+       }
+
+       id = makedev(1, handler->n_ramdisks);
+       if (mknod(name, S_IFBLK, id)) {
+               if (errno == EEXIST) {
+                       /* We haven't yet received updates for existing
+                        * ramdisks - add and use this one */
+                       pb_debug("Using untracked ramdisk %s\n", name);
+               } else {
+                       pb_log("Failed to create new ramdisk %s: %s\n",
+                              name, strerror(errno));
+                       return NULL;
+               }
+       }
+       device_handler_add_ramdisk(handler, name);
+       talloc_free(name);
+
+       return handler->ramdisks[i];
+}
+
+void device_handler_release_ramdisk(struct discover_device *device)
+{
+       struct ramdisk_device *ramdisk = device->ramdisk;
+
+       talloc_free(ramdisk->snapshot);
+       talloc_free(ramdisk->origin);
+       talloc_free(ramdisk->base);
+
+       ramdisk->snapshot = ramdisk->origin = ramdisk->base = NULL;
+       ramdisk->sectors = 0;
+
+       device->ramdisk = NULL;
+}
+
 /* Start discovery on a hotplugged device. The device will be in our devices
  * array, but has only just been initialised by the hotplug source.
  */
@@ -695,8 +857,22 @@ int device_handler_discover(struct device_handler *handler,
                struct discover_device *dev)
 {
        struct discover_context *ctx;
+       struct boot_status *status;
        int rc;
 
+       status = talloc_zero(handler, struct boot_status);
+       status->type = BOOT_STATUS_INFO;
+       /*
+        * TRANSLATORS: this string will be passed the type and identifier
+        * of the device. For example, the first parameter could be "Disk",
+        * (which will be translated accordingly) and the second a Linux device
+        * identifier like 'sda1' (which will not be translated)
+        */
+       status->message = talloc_asprintf(status, _("Processing %s device %s"),
+                               device_type_display_name(dev->device->type),
+                               dev->device->id);
+       boot_status(handler, status);
+
        process_boot_option_queue(handler);
 
        /* create our context */
@@ -717,6 +893,15 @@ int device_handler_discover(struct device_handler *handler,
        device_handler_discover_context_commit(handler, ctx);
 
 out:
+       /*
+        * TRANSLATORS: the format specifier in this string is a Linux
+        * device identifier, like 'sda1'
+        */
+       status->message = talloc_asprintf(status,_("Processing %s complete"),
+                               dev->device->id);
+       boot_status(handler, status);
+
+       talloc_free(status);
        talloc_free(ctx);
 
        return 0;
@@ -727,6 +912,17 @@ int device_handler_dhcp(struct device_handler *handler,
                struct discover_device *dev, struct event *event)
 {
        struct discover_context *ctx;
+       struct boot_status *status;
+
+       status = talloc_zero(handler, struct boot_status);
+       status->type = BOOT_STATUS_INFO;
+       /*
+        * TRANSLATORS: this format specifier will be the name of a network
+        * device, like 'eth0'.
+        */
+       status->message = talloc_asprintf(status, _("Processing dhcp event on %s"),
+                               dev->device->id);
+       boot_status(handler, status);
 
        /* create our context */
        ctx = device_handler_discover_context_create(handler, dev);
@@ -736,6 +932,15 @@ int device_handler_dhcp(struct device_handler *handler,
 
        device_handler_discover_context_commit(handler, ctx);
 
+       /*
+        * TRANSLATORS: this format specifier will be the name of a network
+        * device, like 'eth0'.
+        */
+       status->message = talloc_asprintf(status,_("Processing %s complete"),
+                               dev->device->id);
+       boot_status(handler, status);
+
+       talloc_free(status);
        talloc_free(ctx);
 
        return 0;
@@ -745,19 +950,30 @@ int device_handler_dhcp(struct device_handler *handler,
 int device_handler_conf(struct device_handler *handler,
                struct discover_device *dev, struct pb_url *url)
 {
-        struct discover_context *ctx;
+       struct discover_context *ctx;
+       struct boot_status *status;
 
-        /* create our context */
-        ctx = device_handler_discover_context_create(handler, dev);
-        ctx->conf_url = url;
+       status = talloc_zero(handler, struct boot_status);
+       status->type = BOOT_STATUS_INFO;
+       status->message = talloc_asprintf(status, _("Processing user config"));
+       boot_status(handler, status);
 
-        iterate_parsers(ctx);
+       /* create our context */
+       ctx = device_handler_discover_context_create(handler, dev);
+       ctx->conf_url = url;
 
-        device_handler_discover_context_commit(handler, ctx);
+       iterate_parsers(ctx);
+
+       device_handler_discover_context_commit(handler, ctx);
 
-        talloc_free(ctx);
+       status->message = talloc_asprintf(status,
+                               _("Processing user config complete"));
+       boot_status(handler, status);
 
-        return 0;
+       talloc_free(status);
+       talloc_free(ctx);
+
+       return 0;
 }
 
 static struct discover_boot_option *find_boot_option_by_id(
@@ -788,7 +1004,7 @@ void device_handler_boot(struct device_handler *handler,
        if (handler->pending_boot)
                boot_cancel(handler->pending_boot);
 
-       platform_finalise_config();
+       platform_pre_boot();
 
        handler->pending_boot = boot(handler, opt, cmd, handler->dry_run,
                        boot_status, handler);
@@ -909,9 +1125,8 @@ static char *device_from_addr(void *ctx, struct pb_url *url)
        return dev;
 }
 
-
 void device_handler_process_url(struct device_handler *handler,
-               const char *url)
+               const char *url, const char *mac, const char *ip)
 {
        struct discover_context *ctx;
        struct discover_device *dev;
@@ -937,11 +1152,25 @@ void device_handler_process_url(struct device_handler *handler,
        event->type = EVENT_TYPE_USER;
        event->action = EVENT_ACTION_CONF;
 
-       event->params = talloc_array(event, struct param, 1);
-       param = &event->params[0];
-       param->name = talloc_strdup(event, "pxeconffile");
-       param->value = talloc_strdup(event, url);
-       event->n_params = 1;
+       if (url[strlen(url) - 1] == '/') {
+               event->params = talloc_array(event, struct param, 3);
+               param = &event->params[0];
+               param->name = talloc_strdup(event, "pxepathprefix");
+               param->value = talloc_strdup(event, url);
+               param = &event->params[1];
+               param->name = talloc_strdup(event, "mac");
+               param->value = talloc_strdup(event, mac);
+               param = &event->params[2];
+               param->name = talloc_strdup(event, "ip");
+               param->value = talloc_strdup(event, ip);
+               event->n_params = 3;
+       } else {
+               event->params = talloc_array(event, struct param, 1);
+               param = &event->params[0];
+               param->name = talloc_strdup(event, "pxeconffile");
+               param->value = talloc_strdup(event, url);
+               event->n_params = 1;
+       }
 
        pb_url = pb_url_parse(event, event->params->value);
        if (!pb_url || !pb_url->host) {
@@ -1027,14 +1256,46 @@ static void device_handler_reinit_sources(struct device_handler *handler)
                        handler->dry_run);
 }
 
+static const char *fs_parameters(struct discover_device *dev,
+                                unsigned int rw_flags)
+{
+       const char *fstype = discover_device_get_param(dev, "ID_FS_TYPE");
+
+       /* XFS journals are not cross-endian compatible; don't try recovery
+        * even if we have a snapshot */
+       if (!strncmp(fstype, "xfs", strlen("xfs")))
+               return "norecovery";
+
+       /* If we have a snapshot available allow touching the filesystem */
+       if (dev->ramdisk)
+               return "";
+
+       if ((rw_flags | MS_RDONLY) != MS_RDONLY)
+               return "";
+
+       /* Avoid writes due to journal replay if we don't have a snapshot */
+       if (!strncmp(fstype, "ext4", strlen("ext4")))
+               return "norecovery";
+
+       return "";
+}
+
+static inline const char *get_device_path(struct discover_device *dev)
+{
+       return dev->ramdisk ? dev->ramdisk->snapshot : dev->device_path;
+}
+
 static bool check_existing_mount(struct discover_device *dev)
 {
        struct stat devstat, mntstat;
+       const char *device_path;
        struct mntent *mnt;
        FILE *fp;
        int rc;
 
-       rc = stat(dev->device_path, &devstat);
+       device_path = get_device_path(dev);
+
+       rc = stat(device_path, &devstat);
        if (rc) {
                pb_debug("%s: stat failed: %s\n", __func__, strerror(errno));
                return false;
@@ -1084,7 +1345,7 @@ static bool check_existing_mount(struct discover_device *dev)
 
 static int mount_device(struct discover_device *dev)
 {
-       const char *fstype;
+       const char *fstype, *device_path;
        int rc;
 
        if (!dev->device_path)
@@ -1100,6 +1361,13 @@ static int mount_device(struct discover_device *dev)
        if (!fstype)
                return 0;
 
+       /* ext3 treats the norecovery option as an error, so mount the device
+        * as an ext4 filesystem instead */
+       if (!strncmp(fstype, "ext3", strlen("ext3"))) {
+               pb_debug("Mounting ext3 filesystem as ext4\n");
+               fstype = talloc_asprintf(dev, "ext4");
+       }
+
        dev->mount_path = join_paths(dev, mount_base(),
                                        dev->device_path);
 
@@ -1109,10 +1377,13 @@ static int mount_device(struct discover_device *dev)
                goto err_free;
        }
 
+       device_path = get_device_path(dev);
+
        pb_log("mounting device %s read-only\n", dev->device_path);
        errno = 0;
-       rc = mount(dev->device_path, dev->mount_path, fstype,
-                       MS_RDONLY | MS_SILENT, "");
+       rc = mount(device_path, dev->mount_path, fstype,
+                       MS_RDONLY | MS_SILENT,
+                       fs_parameters(dev, MS_RDONLY));
        if (!rc) {
                dev->mounted = true;
                dev->mounted_rw = false;
@@ -1121,7 +1392,10 @@ static int mount_device(struct discover_device *dev)
        }
 
        pb_log("couldn't mount device %s: mount failed: %s\n",
-                       dev->device_path, strerror(errno));
+                       device_path, strerror(errno));
+
+       /* If mount fails clean up any snapshot */
+       devmapper_destroy_snapshot(dev);
 
        pb_rmdir_recursive(mount_base(), dev->mount_path);
 err_free:
@@ -1132,17 +1406,21 @@ err_free:
 
 static int umount_device(struct discover_device *dev)
 {
+       const char *device_path;
        int rc;
 
        if (!dev->mounted || !dev->unmount)
                return 0;
 
-       pb_log("unmounting device %s\n", dev->device_path);
+       device_path = get_device_path(dev);
+
+       pb_log("unmounting device %s\n", device_path);
        rc = umount(dev->mount_path);
        if (rc)
                return -1;
 
        dev->mounted = false;
+       devmapper_destroy_snapshot(dev);
 
        pb_rmdir_recursive(mount_base(), dev->mount_path);
 
@@ -1154,36 +1432,88 @@ static int umount_device(struct discover_device *dev)
 
 int device_request_write(struct discover_device *dev, bool *release)
 {
+       const char *fstype, *device_path;
+       const struct config *config;
        int rc;
 
        *release = false;
 
+       config = config_get();
+       if (!config->allow_writes)
+               return -1;
+
        if (!dev->mounted)
                return -1;
 
        if (dev->mounted_rw)
                return 0;
 
-       pb_log("remounting device %s read-write\n", dev->device_path);
-       rc = mount(dev->device_path, dev->mount_path, "",
-                       MS_REMOUNT | MS_SILENT, "");
-       if (rc)
+       fstype = discover_device_get_param(dev, "ID_FS_TYPE");
+
+       device_path = get_device_path(dev);
+
+       pb_log("remounting device %s read-write\n", device_path);
+
+       rc = umount(dev->mount_path);
+       if (rc) {
+               pb_log("Failed to unmount %s: %s\n",
+                      dev->mount_path, strerror(errno));
                return -1;
+       }
+
+       rc = mount(device_path, dev->mount_path, fstype,
+                       MS_SILENT,
+                       fs_parameters(dev, MS_REMOUNT));
+       if (rc)
+               goto mount_ro;
 
        dev->mounted_rw = true;
        *release = true;
        return 0;
+
+mount_ro:
+       pb_log("Unable to remount device %s read-write: %s\n",
+              device_path, strerror(errno));
+       if (mount(device_path, dev->mount_path, fstype,
+                       MS_RDONLY | MS_SILENT,
+                       fs_parameters(dev, MS_RDONLY)))
+               pb_log("Unable to recover mount for %s: %s\n",
+                      device_path, strerror(errno));
+       return -1;
 }
 
 void device_release_write(struct discover_device *dev, bool release)
 {
+       const char *fstype, *device_path;
+
        if (!release)
                return;
 
-       pb_log("remounting device %s read-only\n", dev->device_path);
-       mount(dev->device_path, dev->mount_path, "",
-                       MS_REMOUNT | MS_RDONLY | MS_SILENT, "");
-       dev->mounted_rw = false;
+       device_path = get_device_path(dev);
+
+       fstype = discover_device_get_param(dev, "ID_FS_TYPE");
+
+       pb_log("remounting device %s read-only\n", device_path);
+
+       if (umount(dev->mount_path)) {
+               pb_log("Failed to unmount %s\n", dev->mount_path);
+               return;
+       }
+       dev->mounted_rw = dev->mounted = false;
+
+       if (dev->ramdisk) {
+               devmapper_merge_snapshot(dev);
+               /* device_path becomes stale after merge */
+               device_path = get_device_path(dev);
+       }
+
+       if (mount(device_path, dev->mount_path, fstype,
+                       MS_RDONLY | MS_SILENT,
+                       fs_parameters(dev, MS_RDONLY)))
+               pb_log("Failed to remount %s read-only: %s\n",
+                      device_path, strerror(errno));
+       else
+               dev->mounted = true;
 }
 
 #else