diff mbox series

[v5] discover/grub: Add blscfg command support to parse BootLoaderSpec files

Message ID 20180322084123.3345-1-javierm@redhat.com
State Accepted
Headers show
Series [v5] discover/grub: Add blscfg command support to parse BootLoaderSpec files | expand

Commit Message

Javier Martinez Canillas March 22, 2018, 8:41 a.m. UTC
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@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 v5:
- Allow the default option to be selected against option->{id,name}.
- Set option->id to the BLS filename.
- Add test for using the title or the filename to set default option.

Changes in v4:
- Add support for default option.
- Set option->id to dev->id and option->name as done by grub2 without BLS.

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                          | 241 +++++++++++++++++++++++
 discover/grub2/builtins.c                        |   9 +-
 discover/grub2/script.c                          |   2 +-
 discover/parser.c                                |  16 ++
 discover/parser.h                                |   8 +
 test/parser/Makefile.am                          |   5 +
 test/parser/test-grub2-blscfg-default-filename.c |  29 +++
 test/parser/test-grub2-blscfg-default-title.c    |  36 ++++
 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 ++++++
 13 files changed, 511 insertions(+), 2 deletions(-)
 create mode 100644 discover/grub2/blscfg.c
 create mode 100644 test/parser/test-grub2-blscfg-default-filename.c
 create mode 100644 test/parser/test-grub2-blscfg-default-title.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

Comments

Sam Mendoza-Jonas March 23, 2018, 2:09 a.m. UTC | #1
On Thu, 2018-03-22 at 09:41 +0100, 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@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 v5:
> - Allow the default option to be selected against option->{id,name}.
> - Set option->id to the BLS filename.
> - Add test for using the title or the filename to set default option.

Thanks for following up on all the comments, merged as 91ce1a8f.

Cheers,
Sam

> 
> Changes in v4:
> - Add support for default option.
> - Set option->id to dev->id and option->name as done by grub2 without BLS.
> 
> 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                          | 241 +++++++++++++++++++++++
>  discover/grub2/builtins.c                        |   9 +-
>  discover/grub2/script.c                          |   2 +-
>  discover/parser.c                                |  16 ++
>  discover/parser.h                                |   8 +
>  test/parser/Makefile.am                          |   5 +
>  test/parser/test-grub2-blscfg-default-filename.c |  29 +++
>  test/parser/test-grub2-blscfg-default-title.c    |  36 ++++
>  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 ++++++
>  13 files changed, 511 insertions(+), 2 deletions(-)
>  create mode 100644 discover/grub2/blscfg.c
>  create mode 100644 test/parser/test-grub2-blscfg-default-filename.c
>  create mode 100644 test/parser/test-grub2-blscfg-default-title.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..0f69f7e29106
> --- /dev/null
> +++ b/discover/grub2/blscfg.c
> @@ -0,0 +1,241 @@
> +
> +#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 bool option_is_default(struct grub2_script *script,
> +			      struct boot_option *option)
> +{
> +	const char *var;
> +
> +	var = script_env_get(script, "default");
> +	if (!var)
> +		return false;
> +
> +	if (!strcmp(var, option->id))
> +		return true;
> +
> +	return !strcmp(var, option->name);
> +}
> +
> +static void bls_finish(struct conf_context *conf)
> +{
> +	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;
> +	char *filename;
> +
> +	if (!state->image) {
> +		device_handler_status_dev_info(dc->handler, dc->device,
> +					       _("linux field not found in %s"),
> +					       state->filename);
> +		return;
> +	}
> +
> +	filename = basename(state->filename);
> +	filename[strlen(filename) - strlen(".conf")] = '\0';
> +
> +	option->id = talloc_strdup(option, filename);
> +
> +	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, state->image);
> +
> +	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);
> +
> +	option->is_default = option_is_default(state->script, option);
> +
> +	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/grub2/script.c b/discover/grub2/script.c
> index ed81a202c01f..1a802b97943e 100644
> --- a/discover/grub2/script.c
> +++ b/discover/grub2/script.c
> @@ -103,7 +103,7 @@ static bool is_delim(char c)
>  }
>  
>  static bool option_is_default(struct grub2_script *script,
> -		struct discover_boot_option *opt, const char *id)
> +			      struct discover_boot_option *opt, const char *id)
>  {
>  	unsigned int default_idx;
>  	const char *var;
> 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..3479d88cdb23 100644
> --- a/test/parser/Makefile.am
> +++ b/test/parser/Makefile.am
> @@ -40,6 +40,11 @@ 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-default-filename \
> +	test/parser/test-grub2-blscfg-default-title \
> +	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-default-filename.c b/test/parser/test-grub2-blscfg-default-filename.c
> new file mode 100644
> index 000000000000..fb740599d1b0
> --- /dev/null
> +++ b/test/parser/test-grub2-blscfg-default-filename.c
> @@ -0,0 +1,29 @@
> +#include "parser-test.h"
> +
> +#if 0 /* PARSER_EMBEDDED_CONFIG */
> +set default=6c063c8e48904f2684abde8eea303f41-4.15.2-302.fc28.x86_64
> +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");
> +
> +	test_read_conf_embedded(test, "/boot/grub2/grub.cfg");
> +
> +	test_run_parser(test, "grub2");
> +
> +	ctx = test->ctx;
> +
> +	opt = get_boot_option(ctx, 0);
> +
> +	check_is_default(opt);
> +}
> diff --git a/test/parser/test-grub2-blscfg-default-title.c b/test/parser/test-grub2-blscfg-default-title.c
> new file mode 100644
> index 000000000000..94acf80bd549
> --- /dev/null
> +++ b/test/parser/test-grub2-blscfg-default-title.c
> @@ -0,0 +1,36 @@
> +#include "parser-test.h"
> +
> +#if 0 /* PARSER_EMBEDDED_CONFIG */
> +load_env
> +set default=${saved_entry}
> +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"
> +			     "saved_entry=Fedora (4.15.2-302.fc28.x86_64) 28 (Twenty Eight)\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_is_default(opt);
> +}
> 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)
diff mbox series

Patch

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..0f69f7e29106
--- /dev/null
+++ b/discover/grub2/blscfg.c
@@ -0,0 +1,241 @@ 
+
+#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 bool option_is_default(struct grub2_script *script,
+			      struct boot_option *option)
+{
+	const char *var;
+
+	var = script_env_get(script, "default");
+	if (!var)
+		return false;
+
+	if (!strcmp(var, option->id))
+		return true;
+
+	return !strcmp(var, option->name);
+}
+
+static void bls_finish(struct conf_context *conf)
+{
+	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;
+	char *filename;
+
+	if (!state->image) {
+		device_handler_status_dev_info(dc->handler, dc->device,
+					       _("linux field not found in %s"),
+					       state->filename);
+		return;
+	}
+
+	filename = basename(state->filename);
+	filename[strlen(filename) - strlen(".conf")] = '\0';
+
+	option->id = talloc_strdup(option, filename);
+
+	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, state->image);
+
+	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);
+
+	option->is_default = option_is_default(state->script, option);
+
+	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/grub2/script.c b/discover/grub2/script.c
index ed81a202c01f..1a802b97943e 100644
--- a/discover/grub2/script.c
+++ b/discover/grub2/script.c
@@ -103,7 +103,7 @@  static bool is_delim(char c)
 }
 
 static bool option_is_default(struct grub2_script *script,
-		struct discover_boot_option *opt, const char *id)
+			      struct discover_boot_option *opt, const char *id)
 {
 	unsigned int default_idx;
 	const char *var;
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..3479d88cdb23 100644
--- a/test/parser/Makefile.am
+++ b/test/parser/Makefile.am
@@ -40,6 +40,11 @@  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-default-filename \
+	test/parser/test-grub2-blscfg-default-title \
+	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-default-filename.c b/test/parser/test-grub2-blscfg-default-filename.c
new file mode 100644
index 000000000000..fb740599d1b0
--- /dev/null
+++ b/test/parser/test-grub2-blscfg-default-filename.c
@@ -0,0 +1,29 @@ 
+#include "parser-test.h"
+
+#if 0 /* PARSER_EMBEDDED_CONFIG */
+set default=6c063c8e48904f2684abde8eea303f41-4.15.2-302.fc28.x86_64
+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");
+
+	test_read_conf_embedded(test, "/boot/grub2/grub.cfg");
+
+	test_run_parser(test, "grub2");
+
+	ctx = test->ctx;
+
+	opt = get_boot_option(ctx, 0);
+
+	check_is_default(opt);
+}
diff --git a/test/parser/test-grub2-blscfg-default-title.c b/test/parser/test-grub2-blscfg-default-title.c
new file mode 100644
index 000000000000..94acf80bd549
--- /dev/null
+++ b/test/parser/test-grub2-blscfg-default-title.c
@@ -0,0 +1,36 @@ 
+#include "parser-test.h"
+
+#if 0 /* PARSER_EMBEDDED_CONFIG */
+load_env
+set default=${saved_entry}
+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"
+			     "saved_entry=Fedora (4.15.2-302.fc28.x86_64) 28 (Twenty Eight)\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_is_default(opt);
+}
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)