[PATCH v3] discover/grub: Add blscfg command support to parse BootLoaderSpec files
Brett Grandbois
brett.grandbois at opengear.com
Wed Mar 21 10:39:25 AEDT 2018
It also just occurred to me that the BLS filename itself minus the
'.conf' extension should also be used as a comparison for the is_default
check from this line in the spec:
> The file name of the file is used for identification of the boot item,
> but shall never be presented to the user in the UI.
And possibly even used as the basis for option->id rather than state->image?
On 21/03/18 08:45, Brett Grandbois wrote:
> [This sender failed our fraud detection checks and may not be who they
> appear to be. Learn about spoofing at http://aka.ms/LearnAboutSpoofing]
>
> Tested it out and it works. I just noticed however that there is no
> support for default in it so somewhere in bls_finish it would be nice to
> have an option->is_default check. The concept of index doesn't seem to
> apply in BLS like it does in a list of menuentries so probably the best
> way to go is to do the default env comparison on title or machine_id if
> either exist.
>
> I know I originally suggested it, but looking at the implementation I
> can see that having option->name be a mash up of machine_id and version
> isn't the way to go. It looks much cleaner to just have:
>
> if title
>
> else if machine_id
>
> else if version
>
> else
>
>
> which then makes the default check easy has you can do it based on an
> option->name comparison afterwards.
>
> Brett
>
>
> On 19/03/18 19:27, Javier Martinez Canillas wrote:
>> The BootLoaderSpec (BLS) defines a file format for boot configurations,
>> so bootloaders can parse these files and create their boot menu entries
>> by using the information provided by them [0].
>>
>> This allow to configure the boot items as drop-in files in a directory
>> instead of having to parse and modify a bootloader configuration file.
>>
>> The GRUB 2 bootloader provides a blscfg command that parses these files
>> and creates menu entries using this information. Add support for it.
>>
>> [0]: https://www.freedesktop.org/wiki/Specifications/BootLoaderSpec/
>>
>> Signed-off-by: Javier Martinez Canillas <javierm at redhat.com>
>>
>> ---
>>
>> Hello,
>>
>> From Fedora 28 there will be an option to use BootLoaderSpec
>> snippets to
>> update GRUB's boot menu entries. So I'm posting this patch to allow this
>> to also work on ppc64 machines using petitboot, instead of
>> grub-ieee1275.
>>
>> This can be tested by creating a BLS config under /boot/loader/entries,
>> for example following /boot/loader/entries/4.15.6-300.fc27.x86_64.conf:
>>
>> title Fedora (4.15.6-300.fc27.x86_64) 27 (Twenty Seven)
>> linux /vmlinuz-4.15.6-300.fc27.x86_64
>> initrd /initramfs-4.15.6-300.fc27.x86_64.img
>> options root=/dev/mapper/fedora-root ro rd.lvm.lv=fedora/root
>> rd.luks.uuid=luks-0b078909-4a1c-4a57-91b8-b9f724e86a1a
>> rd.lvm.lv=fedora/swap rhgb quiet LANG=en_US.UTF-8
>> id fedora-20180214051518-4.15.6-300.fc27.x86_64.x86_64
>> grub_users $grub_users
>> grub_arg --unrestricted
>> grub_class kernel
>>
>> And a grub.cfg that calls the blscfg command, it could just be the
>> following:
>>
>> blscfg
>>
>> Best regards,
>> Javier
>>
>> Changes in v3:
>> - Populate boot option id using the linux field instead of title field.
>> - Don't fill boot option name using the optional title field, instead
>> fill this from optional fields that are present in the BLS fragment.
>> - Add support for the devicetree field.
>>
>> Changes in v2:
>> - Allow optional fields, only require the linux field to be present.
>> - Remove unused identifier from bls_filter() struct dirent * param.
>> - Remove redundant check in builtin_blscfg() while loop.
>>
>> discover/grub2/Makefile.am | 1 +
>> discover/grub2/blscfg.c | 221
>> +++++++++++++++++++++++++++
>> discover/grub2/builtins.c | 9 +-
>> discover/parser.c | 16 ++
>> discover/parser.h | 8 +
>> test/parser/Makefile.am | 3 +
>> test/parser/test-grub2-blscfg-multiple-bls.c | 44 ++++++
>> test/parser/test-grub2-blscfg-opts-config.c | 29 ++++
>> test/parser/test-grub2-blscfg-opts-grubenv.c | 34 +++++
>> test/parser/utils.c | 59 +++++++
>> 10 files changed, 423 insertions(+), 1 deletion(-)
>> create mode 100644 discover/grub2/blscfg.c
>> create mode 100644 test/parser/test-grub2-blscfg-multiple-bls.c
>> create mode 100644 test/parser/test-grub2-blscfg-opts-config.c
>> create mode 100644 test/parser/test-grub2-blscfg-opts-grubenv.c
>>
>> diff --git a/discover/grub2/Makefile.am b/discover/grub2/Makefile.am
>> index 130ede88e18c..b240106d7a54 100644
>> --- a/discover/grub2/Makefile.am
>> +++ b/discover/grub2/Makefile.am
>> @@ -15,6 +15,7 @@
>> noinst_PROGRAMS += discover/grub2/grub2-parser.ro
>>
>> discover_grub2_grub2_parser_ro_SOURCES = \
>> + discover/grub2/blscfg.c \
>> discover/grub2/builtins.c \
>> discover/grub2/env.c \
>> discover/grub2/grub2.h \
>> diff --git a/discover/grub2/blscfg.c b/discover/grub2/blscfg.c
>> new file mode 100644
>> index 000000000000..5677aa081531
>> --- /dev/null
>> +++ b/discover/grub2/blscfg.c
>> @@ -0,0 +1,221 @@
>> +
>> +#define _GNU_SOURCE
>> +
>> +#include <assert.h>
>> +#include <stdlib.h>
>> +#include <string.h>
>> +#include <dirent.h>
>> +
>> +#include <log/log.h>
>> +#include <file/file.h>
>> +#include <talloc/talloc.h>
>> +#include <i18n/i18n.h>
>> +
>> +#include "grub2.h"
>> +#include "discover/parser-conf.h"
>> +#include "discover/parser.h"
>> +
>> +#define BLS_DIR "/loader/entries"
>> +
>> +struct bls_state {
>> + struct discover_boot_option *opt;
>> + struct grub2_script *script;
>> + const char *filename;
>> + const char *title;
>> + const char *version;
>> + const char *machine_id;
>> + const char *image;
>> + const char *initrd;
>> + const char *dtb;
>> +};
>> +
>> +static void bls_process_pair(struct conf_context *conf, const char
>> *name,
>> + char *value)
>> +{
>> + struct bls_state *state = conf->parser_info;
>> + struct discover_boot_option *opt = state->opt;
>> + struct boot_option *option = opt->option;
>> + const char *boot_args;
>> +
>> + if (streq(name, "title")) {
>> + state->title = talloc_strdup(state, value);
>> + return;
>> + }
>> +
>> + if (streq(name, "version")) {
>> + state->version = talloc_strdup(state, value);
>> + return;
>> + }
>> +
>> + if (streq(name, "machine-id")) {
>> + state->machine_id = talloc_strdup(state, value);
>> + return;
>> + }
>> +
>> + if (streq(name, "linux")) {
>> + state->image = talloc_strdup(state, value);
>> + return;
>> + }
>> +
>> + if (streq(name, "initrd")) {
>> + state->initrd = talloc_strdup(state, value);
>> + return;
>> + }
>> +
>> + if (streq(name, "devicetree")) {
>> + state->dtb = talloc_strdup(state, value);
>> + return;
>> + }
>> +
>> + if (streq(name, "options")) {
>> + if (value[0] == '$') {
>> + boot_args = script_env_get(state->script, value
>> + 1);
>> + if (!boot_args)
>> + return;
>> +
>> + option->boot_args = talloc_strdup(opt, boot_args);
>> + } else {
>> + option->boot_args = talloc_strdup(opt, value);
>> + }
>> + return;
>> + }
>> +}
>> +
>> +static void bls_finish(struct conf_context *conf)
>> +{
>> + struct discover_device *dev = conf->dc->device;
>> + struct bls_state *state = conf->parser_info;
>> + struct discover_context *dc = conf->dc;
>> + struct discover_boot_option *opt = state->opt;
>> + struct boot_option *option = opt->option;
>> + const char *root;
>> +
>> + if (!state->image) {
>> + device_handler_status_dev_info(dc->handler, dc->device,
>> + _("linux field not found
>> in %s"),
>> + state->filename);
>> + return;
>> + }
>> +
>> + option->id = talloc_asprintf(option, "%s#%s", dev->device->id,
>> + state->image);
>> +
>> + if (state->title)
>> + option->name = talloc_strdup(option, state->title);
>> + else if (state->machine_id && state->version)
>> + option->name = talloc_asprintf(option, "%s %s",
>> + state->machine_id,
>> + state->version);
>> + else if (state->version)
>> + option->name = talloc_strdup(option, state->version);
>> + else
>> + option->name = talloc_strdup(option, option->id);
>> +
>> + root = script_env_get(state->script, "root");
>> +
>> + opt->boot_image = create_grub2_resource(opt, conf->dc->device,
>> + root, state->image);
>> +
>> + if (state->initrd)
>> + opt->initrd = create_grub2_resource(opt, conf->dc->device,
>> + root, state->initrd);
>> +
>> + if (state->dtb)
>> + opt->dtb = create_grub2_resource(opt, conf->dc->device,
>> + root, state->dtb);
>> + discover_context_add_boot_option(dc, opt);
>> +
>> + device_handler_status_dev_info(dc->handler, dc->device,
>> + _("Created menu entry from BLS
>> file %s"),
>> + state->filename);
>> +}
>> +
>> +static int bls_filter(const struct dirent *ent)
>> +{
>> + int offset = strlen(ent->d_name) - strlen(".conf");
>> +
>> + if (offset < 0)
>> + return 0;
>> +
>> + return strncmp(ent->d_name + offset, ".conf", strlen(".conf"))
>> == 0;
>> +}
>> +
>> +static int bls_sort(const struct dirent **ent_a, const struct dirent
>> **ent_b)
>> +{
>> + return strverscmp((*ent_b)->d_name, (*ent_a)->d_name);
>> +}
>> +
>> +int builtin_blscfg(struct grub2_script *script,
>> + void *data __attribute__((unused)),
>> + int argc __attribute__((unused)),
>> + char *argv[] __attribute__((unused)));
>> +
>> +int builtin_blscfg(struct grub2_script *script,
>> + void *data __attribute__((unused)),
>> + int argc __attribute__((unused)),
>> + char *argv[] __attribute__((unused)))
>> +{
>> + struct discover_context *dc = script->ctx;
>> + struct dirent **bls_entries;
>> + struct conf_context *conf;
>> + struct bls_state *state;
>> + char *buf, *filename;
>> + int n, len, rc = -1;
>> +
>> + conf = talloc_zero(dc, struct conf_context);
>> + if (!conf)
>> + return rc;
>> +
>> + conf->dc = dc;
>> + conf->get_pair = conf_get_pair_space;
>> + conf->process_pair = bls_process_pair;
>> + conf->finish = bls_finish;
>> +
>> + n = parser_scandir(dc, BLS_DIR, &bls_entries, bls_filter,
>> bls_sort);
>> + if (n <= 0)
>> + goto err;
>> +
>> + while (n--) {
>> + filename = talloc_asprintf(dc, BLS_DIR"/%s",
>> + bls_entries[n]->d_name);
>> + if (!filename)
>> + break;
>> +
>> + state = talloc_zero(conf, struct bls_state);
>> + if (!state)
>> + break;
>> +
>> + state->opt = discover_boot_option_create(dc, dc->device);
>> + if (!state->opt)
>> + break;
>> +
>> + state->script = script;
>> + state->filename = filename;
>> + conf->parser_info = state;
>> +
>> + rc = parser_request_file(dc, dc->device, filename,
>> &buf, &len);
>> + if (rc)
>> + break;
>> +
>> + conf_parse_buf(conf, buf, len);
>> +
>> + talloc_free(buf);
>> + talloc_free(state);
>> + talloc_free(filename);
>> + free(bls_entries[n]);
>> + }
>> +
>> + if (n > 0) {
>> + device_handler_status_dev_info(dc->handler, dc->device,
>> + _("Scanning %s failed"),
>> + BLS_DIR);
>> + do {
>> + free(bls_entries[n]);
>> + } while (n-- > 0);
>> + }
>> +
>> + free(bls_entries);
>> +err:
>> + talloc_free(conf);
>> + return rc;
>> +}
>> diff --git a/discover/grub2/builtins.c b/discover/grub2/builtins.c
>> index c16b6390225a..e42821a64a9a 100644
>> --- a/discover/grub2/builtins.c
>> +++ b/discover/grub2/builtins.c
>> @@ -330,7 +330,10 @@ extern int builtin_load_env(struct grub2_script
>> *script,
>> int builtin_save_env(struct grub2_script *script,
>> void *data __attribute__((unused)),
>> int argc, char *argv[]);
>> -
>> +int builtin_blscfg(struct grub2_script *script,
>> + void *data __attribute__((unused)),
>> + int argc __attribute__((unused)),
>> + char *argv[] __attribute__((unused)));
>>
>> static struct {
>> const char *name;
>> @@ -380,6 +383,10 @@ static struct {
>> .name = "save_env",
>> .fn = builtin_save_env,
>> },
>> + {
>> + .name = "blscfg",
>> + .fn = builtin_blscfg,
>> + }
>> };
>>
>> static const char *nops[] = {
>> diff --git a/discover/parser.c b/discover/parser.c
>> index 5598f963e236..9fe1925d94c4 100644
>> --- a/discover/parser.c
>> +++ b/discover/parser.c
>> @@ -128,6 +128,22 @@ out:
>> return -1;
>> }
>>
>> +int parser_scandir(struct discover_context *ctx, const char *dirname,
>> + struct dirent ***files, int (*filter)(const struct
>> dirent *),
>> + int (*comp)(const struct dirent **, const struct
>> dirent **))
>> +{
>> + char *path;
>> + int n;
>> +
>> + path = talloc_asprintf(ctx, "%s%s", ctx->device->mount_path,
>> dirname);
>> + if (!path)
>> + return -1;
>> +
>> + n = scandir(path, files, filter, comp);
>> + talloc_free(path);
>> + return n;
>> +}
>> +
>> void iterate_parsers(struct discover_context *ctx)
>> {
>> struct p_item* i;
>> diff --git a/discover/parser.h b/discover/parser.h
>> index fc165c5aeda4..bff52e30d09f 100644
>> --- a/discover/parser.h
>> +++ b/discover/parser.h
>> @@ -5,6 +5,7 @@
>> #include <sys/types.h>
>> #include <sys/stat.h>
>> #include <unistd.h>
>> +#include <dirent.h>
>>
>> #include "device-handler.h"
>>
>> @@ -76,5 +77,12 @@ int parser_request_url(struct discover_context
>> *ctx, struct pb_url *url,
>> int parser_stat_path(struct discover_context *ctx,
>> struct discover_device *dev, const char *path,
>> struct stat *statbuf);
>> +/* Function used to list the files on a directory. The dirname should
>> + * be relative to the discover context device mount path. It returns
>> + * the number of files returned in files or a negative value on error.
>> + */
>> +int parser_scandir(struct discover_context *ctx, const char *dirname,
>> + struct dirent ***files, int (*filter)(const struct
>> dirent *),
>> + int (*comp)(const struct dirent **, const struct
>> dirent **));
>>
>> #endif /* _PARSER_H */
>> diff --git a/test/parser/Makefile.am b/test/parser/Makefile.am
>> index a0795dbcf899..b943408c4942 100644
>> --- a/test/parser/Makefile.am
>> +++ b/test/parser/Makefile.am
>> @@ -40,6 +40,9 @@ parser_TESTS = \
>> test/parser/test-grub2-parser-error \
>> test/parser/test-grub2-test-file-ops \
>> test/parser/test-grub2-single-yocto \
>> + test/parser/test-grub2-blscfg-multiple-bls \
>> + test/parser/test-grub2-blscfg-opts-config \
>> + test/parser/test-grub2-blscfg-opts-grubenv \
>> test/parser/test-kboot-single \
>> test/parser/test-yaboot-empty \
>> test/parser/test-yaboot-single \
>> diff --git a/test/parser/test-grub2-blscfg-multiple-bls.c
>> b/test/parser/test-grub2-blscfg-multiple-bls.c
>> new file mode 100644
>> index 000000000000..8fd218c371e8
>> --- /dev/null
>> +++ b/test/parser/test-grub2-blscfg-multiple-bls.c
>> @@ -0,0 +1,44 @@
>> +#include "parser-test.h"
>> +
>> +#if 0 /* PARSER_EMBEDDED_CONFIG */
>> +blscfg
>> +#endif
>> +
>> +void run_test(struct parser_test *test)
>> +{
>> + struct discover_boot_option *opt;
>> + struct discover_context *ctx;
>> +
>> + test_add_file_string(test, test->ctx->device,
>> +
>> "/loader/entries/6c063c8e48904f2684abde8eea303f41-4.15.2-302.fc28.x86_64.conf",
>> + "title Fedora (4.15.2-302.fc28.x86_64) 28
>> (Twenty Eight)\n"
>> + "linux /vmlinuz-4.15.2-302.fc28.x86_64\n"
>> + "initrd
>> /initramfs-4.15.2-302.fc28.x86_64.img\n"
>> + "options root=/dev/mapper/fedora-root ro
>> rd.lvm.lv=fedora/root\n\n");
>> +
>> + test_add_file_string(test, test->ctx->device,
>> +
>> "/loader/entries/6c063c8e48904f2684abde8eea303f41-4.14.18-300.fc28.x86_64.conf",
>> + "title Fedora (4.14.18-300.fc28.x86_64) 28
>> (Twenty Eight)\n"
>> + "linux /vmlinuz-4.14.18-300.fc28.x86_64\n"
>> + "initrd
>> /initramfs-4.14.18-300.fc28.x86_64.img\n"
>> + "options root=/dev/mapper/fedora-root ro
>> rd.lvm.lv=fedora/root\n");
>> +
>> + test_read_conf_embedded(test, "/boot/grub2/grub.cfg");
>> +
>> + test_run_parser(test, "grub2");
>> +
>> + ctx = test->ctx;
>> +
>> + check_boot_option_count(ctx, 2);
>> + opt = get_boot_option(ctx, 0);
>> +
>> + check_name(opt, "Fedora (4.15.2-302.fc28.x86_64) 28 (Twenty
>> Eight)");
>> + check_resolved_local_resource(opt->boot_image, ctx->device,
>> + "/vmlinuz-4.15.2-302.fc28.x86_64");
>> +
>> + opt = get_boot_option(ctx, 1);
>> +
>> + check_name(opt, "Fedora (4.14.18-300.fc28.x86_64) 28 (Twenty
>> Eight)");
>> + check_resolved_local_resource(opt->initrd, ctx->device,
>> + "/initramfs-4.14.18-300.fc28.x86_64.img");
>> +}
>> diff --git a/test/parser/test-grub2-blscfg-opts-config.c
>> b/test/parser/test-grub2-blscfg-opts-config.c
>> new file mode 100644
>> index 000000000000..856aae2adf5f
>> --- /dev/null
>> +++ b/test/parser/test-grub2-blscfg-opts-config.c
>> @@ -0,0 +1,29 @@
>> +#include "parser-test.h"
>> +
>> +#if 0 /* PARSER_EMBEDDED_CONFIG */
>> +set kernelopts=root=/dev/mapper/fedora-root ro rd.lvm.lv=fedora/root
>> +blscfg
>> +#endif
>> +
>> +void run_test(struct parser_test *test)
>> +{
>> + struct discover_boot_option *opt;
>> + struct discover_context *ctx;
>> +
>> + test_add_file_string(test, test->ctx->device,
>> +
>> "/loader/entries/6c063c8e48904f2684abde8eea303f41-4.15.2-302.fc28.x86_64.conf",
>> + "title Fedora (4.15.2-302.fc28.x86_64) 28
>> (Twenty Eight)\n"
>> + "linux /vmlinuz-4.15.2-302.fc28.x86_64\n"
>> + "initrd
>> /initramfs-4.15.2-302.fc28.x86_64.img\n"
>> + "options $kernelopts\n");
>> +
>> + test_read_conf_embedded(test, "/boot/grub2/grub.cfg");
>> +
>> + test_run_parser(test, "grub2");
>> +
>> + ctx = test->ctx;
>> +
>> + opt = get_boot_option(ctx, 0);
>> +
>> + check_args(opt, "root=/dev/mapper/fedora-root ro
>> rd.lvm.lv=fedora/root");
>> +}
>> diff --git a/test/parser/test-grub2-blscfg-opts-grubenv.c
>> b/test/parser/test-grub2-blscfg-opts-grubenv.c
>> new file mode 100644
>> index 000000000000..c77c589b7707
>> --- /dev/null
>> +++ b/test/parser/test-grub2-blscfg-opts-grubenv.c
>> @@ -0,0 +1,34 @@
>> +#include "parser-test.h"
>> +
>> +#if 0 /* PARSER_EMBEDDED_CONFIG */
>> +load_env
>> +blscfg
>> +#endif
>> +
>> +void run_test(struct parser_test *test)
>> +{
>> + struct discover_boot_option *opt;
>> + struct discover_context *ctx;
>> +
>> + test_add_file_string(test, test->ctx->device,
>> + "/boot/grub2/grubenv",
>> + "# GRUB Environment Block\n"
>> + "kernelopts=root=/dev/mapper/fedora-root ro rd.lvm.lv=fedora/root\n");
>> +
>> + test_add_file_string(test, test->ctx->device,
>> +
>> "/loader/entries/6c063c8e48904f2684abde8eea303f41-4.15.2-302.fc28.x86_64.conf",
>> + "title Fedora (4.15.2-302.fc28.x86_64) 28
>> (Twenty Eight)\n"
>> + "linux /vmlinuz-4.15.2-302.fc28.x86_64\n"
>> + "initrd
>> /initramfs-4.15.2-302.fc28.x86_64.img\n"
>> + "options $kernelopts\n");
>> +
>> + test_read_conf_embedded(test, "/boot/grub2/grub.cfg");
>> +
>> + test_run_parser(test, "grub2");
>> +
>> + ctx = test->ctx;
>> +
>> + opt = get_boot_option(ctx, 0);
>> +
>> + check_args(opt, "root=/dev/mapper/fedora-root ro
>> rd.lvm.lv=fedora/root");
>> +}
>> diff --git a/test/parser/utils.c b/test/parser/utils.c
>> index 8900bd72bebd..683bba7d0379 100644
>> --- a/test/parser/utils.c
>> +++ b/test/parser/utils.c
>> @@ -309,6 +309,65 @@ int parser_replace_file(struct discover_context
>> *ctx,
>> return 0;
>> }
>>
>> +int parser_scandir(struct discover_context *ctx, const char *dirname,
>> + struct dirent ***files, int (*filter)(const struct
>> dirent *)
>> + __attribute__((unused)),
>> + int (*comp)(const struct dirent **, const struct
>> dirent **)
>> + __attribute__((unused)))
>> +{
>> + struct parser_test *test = ctx->test_data;
>> + struct test_file *f;
>> + char *filename;
>> + struct dirent **dirents = NULL, **new_dirents;
>> + int n = 0, namelen;
>> +
>> + list_for_each_entry(&test->files, f, list) {
>> + if (f->dev != ctx->device)
>> + continue;
>> +
>> + filename = strrchr(f->name, '/');
>> + if (!filename)
>> + continue;
>> +
>> + namelen = strlen(filename);
>> +
>> + if (strncmp(f->name, dirname, strlen(f->name) - namelen))
>> + continue;
>> +
>> + if (!dirents) {
>> + dirents = malloc(sizeof(struct dirent *));
>> + } else {
>> + new_dirents = realloc(dirents, sizeof(struct
>> dirent *)
>> + * (n + 1));
>> + if (!new_dirents)
>> + goto err_cleanup;
>> +
>> + dirents = new_dirents;
>> + }
>> +
>> + dirents[n] = malloc(sizeof(struct dirent) + namelen + 1);
>> +
>> + if (!dirents[n])
>> + goto err_cleanup;
>> +
>> + strcpy(dirents[n]->d_name, filename + 1);
>> + n++;
>> + }
>> +
>> + *files = dirents;
>> +
>> + return n;
>> +
>> +err_cleanup:
>> + do {
>> + free(dirents[n]);
>> + } while (n-- > 0);
>> +
>> + free(dirents);
>> +
>> + return -1;
>> +}
>> +
>> struct load_url_result *load_url_async(void *ctx, struct pb_url *url,
>> load_url_complete async_cb, void *async_data,
>> waiter_cb stdout_cb, void *stdout_data)
>
> _______________________________________________
> Petitboot mailing list
> Petitboot at lists.ozlabs.org
> https://lists.ozlabs.org/listinfo/petitboot
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.ozlabs.org/pipermail/petitboot/attachments/20180321/8d0daf62/attachment-0001.html>
More information about the Petitboot
mailing list