vmspawn: rework firmware selection logic

Let's make the firmware file to choose configurable, and enumeratable.

This adds --firmware= to select the formare, and in particular
--firmware=list to show available options.
This commit is contained in:
Lennart Poettering 2024-01-19 18:50:43 +01:00
parent fdc7d50ba4
commit e8ce204d86
4 changed files with 188 additions and 58 deletions

View file

@ -135,7 +135,18 @@
<xi:include href="version-info.xml" xpointer="v255"/></listitem>
</varlistentry>
</variablelist>
<varlistentry>
<term><option>--firmware=</option><replaceable>PATH</replaceable></term>
<listitem><para>Takes an absolute path, or a relative path beginning with
<filename>./</filename>. Specifies a JSON firmware definition file, which allows selecting the
firmware to boot in the VM. If not specified a suitable firmware is automatically discovered. If the
special string <literal>list</literal> is specified lists all discovered firmwares.</para>
<xi:include href="version-info.xml" xpointer="v256"/></listitem>
</varlistentry>
</variablelist>
</refsect2><refsect2>
<title>System Identity Options</title>

View file

@ -81,6 +81,12 @@ typedef struct FirmwareData {
char *vars;
} FirmwareData;
static bool firmware_data_supports_sb(const FirmwareData *fwd) {
assert(fwd);
return strv_contains(fwd->features, "secure-boot");
}
static FirmwareData* firmware_data_free(FirmwareData *fwd) {
if (!fwd)
return NULL;
@ -124,12 +130,134 @@ static int firmware_mapping(const char *name, JsonVariant *v, JsonDispatchFlags
return json_dispatch(v, table, flags, userdata);
}
static int get_firmware_search_dirs(char ***ret) {
int r;
assert(ret);
/* Search in:
* - $XDG_CONFIG_HOME/qemu/firmware
* - /etc/qemu/firmware
* - /usr/share/qemu/firmware
*
* Prioritising entries in "more specific" directories */
_cleanup_free_ char *user_firmware_dir = NULL;
r = xdg_user_config_dir(&user_firmware_dir, "/qemu/firmware");
if (r < 0)
return r;
_cleanup_strv_free_ char **l = NULL;
l = strv_new(user_firmware_dir, "/etc/qemu/firmware", "/usr/share/qemu/firmware");
if (!l)
return log_oom_debug();
*ret = TAKE_PTR(l);
return 0;
}
int list_ovmf_config(char ***ret) {
_cleanup_strv_free_ char **search_dirs = NULL;
int r;
assert(ret);
r = get_firmware_search_dirs(&search_dirs);
if (r < 0)
return r;
r = conf_files_list_strv(
ret,
".json",
/* root= */ NULL,
CONF_FILES_FILTER_MASKED|CONF_FILES_REGULAR,
(const char *const*) search_dirs);
if (r < 0)
return log_debug_errno(r, "Failed to list firmware files: %m");
return 0;
}
static int load_firmware_data(const char *path, FirmwareData **ret) {
int r;
assert(path);
assert(ret);
_cleanup_(json_variant_unrefp) JsonVariant *json = NULL;
r = json_parse_file(
/* f= */ NULL,
path,
/* flags= */ 0,
&json,
/* ret_line= */ NULL,
/* ret_column= */ NULL);
if (r < 0)
return r;
static const JsonDispatch table[] = {
{ "description", JSON_VARIANT_STRING, NULL, 0, JSON_MANDATORY },
{ "interface-types", JSON_VARIANT_ARRAY, NULL, 0, JSON_MANDATORY },
{ "mapping", JSON_VARIANT_OBJECT, firmware_mapping, 0, JSON_MANDATORY },
{ "targets", JSON_VARIANT_ARRAY, NULL, 0, JSON_MANDATORY },
{ "features", JSON_VARIANT_ARRAY, json_dispatch_strv, offsetof(FirmwareData, features), JSON_MANDATORY },
{ "tags", JSON_VARIANT_ARRAY, NULL, 0, JSON_MANDATORY },
{}
};
_cleanup_(firmware_data_freep) FirmwareData *fwd = NULL;
fwd = new0(FirmwareData, 1);
if (!fwd)
return -ENOMEM;
r = json_dispatch(json, table, JSON_ALLOW_EXTENSIONS, fwd);
if (r < 0)
return r;
*ret = TAKE_PTR(fwd);
return 0;
}
static int ovmf_config_make(FirmwareData *fwd, OvmfConfig **ret) {
assert(fwd);
assert(ret);
_cleanup_free_ OvmfConfig *config = NULL;
config = new(OvmfConfig, 1);
if (!config)
return -ENOMEM;
*config = (OvmfConfig) {
.path = TAKE_PTR(fwd->firmware),
.vars = TAKE_PTR(fwd->vars),
.supports_sb = firmware_data_supports_sb(fwd),
};
*ret = TAKE_PTR(config);
return 0;
}
int load_ovmf_config(const char *path, OvmfConfig **ret) {
_cleanup_(firmware_data_freep) FirmwareData *fwd = NULL;
int r;
assert(path);
assert(ret);
r = load_firmware_data(path, &fwd);
if (r < 0)
return r;
return ovmf_config_make(fwd, ret);
}
int find_ovmf_config(int search_sb, OvmfConfig **ret) {
_cleanup_(ovmf_config_freep) OvmfConfig *config = NULL;
_cleanup_free_ char *user_firmware_dir = NULL;
_cleanup_strv_free_ char **conf_files = NULL;
int r;
assert(ret);
/* Search in:
* - $XDG_CONFIG_HOME/qemu/firmware
* - /etc/qemu/firmware
@ -138,79 +266,35 @@ int find_ovmf_config(int search_sb, OvmfConfig **ret) {
* Prioritising entries in "more specific" directories
*/
r = xdg_user_config_dir(&user_firmware_dir, "/qemu/firmware");
r = list_ovmf_config(&conf_files);
if (r < 0)
return r;
r = conf_files_list_strv(&conf_files, ".json", NULL, CONF_FILES_FILTER_MASKED|CONF_FILES_REGULAR,
STRV_MAKE_CONST(user_firmware_dir, "/etc/qemu/firmware", "/usr/share/qemu/firmware"));
if (r < 0)
return log_debug_errno(r, "Failed to list config files: %m");
STRV_FOREACH(file, conf_files) {
_cleanup_(firmware_data_freep) FirmwareData *fwd = NULL;
_cleanup_(json_variant_unrefp) JsonVariant *config_json = NULL;
_cleanup_free_ char *contents = NULL;
size_t contents_sz = 0;
r = read_full_file(*file, &contents, &contents_sz);
if (r == -ENOMEM)
return r;
r = load_firmware_data(*file, &fwd);
if (r < 0) {
log_debug_errno(r, "Failed to read contents of %s - ignoring: %m", *file);
continue;
}
r = json_parse(contents, 0, &config_json, NULL, NULL);
if (r == -ENOMEM)
return r;
if (r < 0) {
log_debug_errno(r, "Failed to parse the JSON in %s - ignoring: %m", *file);
continue;
}
static const JsonDispatch table[] = {
{ "description", JSON_VARIANT_STRING, NULL, 0, JSON_MANDATORY },
{ "interface-types", JSON_VARIANT_ARRAY, NULL, 0, JSON_MANDATORY },
{ "mapping", JSON_VARIANT_OBJECT, firmware_mapping, 0, JSON_MANDATORY },
{ "targets", JSON_VARIANT_ARRAY, NULL, 0, JSON_MANDATORY },
{ "features", JSON_VARIANT_ARRAY, json_dispatch_strv, offsetof(FirmwareData, features), JSON_MANDATORY },
{ "tags", JSON_VARIANT_ARRAY, NULL, 0, JSON_MANDATORY },
{}
};
fwd = new0(FirmwareData, 1);
if (!fwd)
return -ENOMEM;
r = json_dispatch(config_json, table, JSON_ALLOW_EXTENSIONS, fwd);
if (r == -ENOMEM)
return r;
if (r < 0) {
log_debug_errno(r, "Failed to extract the required fields from the JSON in %s - ignoring: %m", *file);
log_debug_errno(r, "Failed to load JSON file '%s', skipping: %m", *file);
continue;
}
if (strv_contains(fwd->features, "enrolled-keys")) {
log_debug("Skipping %s, firmware has enrolled keys which has been known to cause issues", *file);
log_debug("Skipping %s, firmware has enrolled keys which has been known to cause issues.", *file);
continue;
}
bool sb_present = strv_contains(fwd->features, "secure-boot");
/* exclude firmware which doesn't match our Secure Boot requirements */
if (search_sb >= 0 && search_sb != sb_present) {
log_debug("Skipping %s, firmware doesn't fit required Secure Boot configuration", *file);
if (search_sb >= 0 && !!search_sb != firmware_data_supports_sb(fwd)) {
log_debug("Skipping %s, firmware doesn't fit required Secure Boot configuration.", *file);
continue;
}
config = new0(OvmfConfig, 1);
if (!config)
return -ENOMEM;
r = ovmf_config_make(fwd, &config);
if (r < 0)
return r;
config->path = TAKE_PTR(fwd->firmware);
config->vars = TAKE_PTR(fwd->vars);
config->supports_sb = sb_present;
log_debug("Selected firmware definition %s.", *file);
break;
}

View file

@ -31,6 +31,8 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(OvmfConfig*, ovmf_config_free);
int qemu_check_kvm_support(void);
int qemu_check_vsock_support(void);
int find_ovmf_config(int search_sb, OvmfConfig **ret_ovmf_config);
int list_ovmf_config(char ***ret);
int load_ovmf_config(const char *path, OvmfConfig **ret);
int find_ovmf_config(int search_sb, OvmfConfig **ret);
int find_qemu_binary(char **ret_qemu_binary);
int vsock_fix_child_cid(unsigned *machine_cid, const char *machine, int *ret_child_sock);

View file

@ -49,12 +49,14 @@ static int arg_secure_boot = -1;
static MachineCredentialContext arg_credentials = {};
static SettingsMask arg_settings_mask = 0;
static char **arg_parameters = NULL;
static char *arg_firmware = NULL;
STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
STATIC_DESTRUCTOR_REGISTER(arg_machine, freep);
STATIC_DESTRUCTOR_REGISTER(arg_qemu_smp, freep);
STATIC_DESTRUCTOR_REGISTER(arg_parameters, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_credentials, machine_credential_context_done);
STATIC_DESTRUCTOR_REGISTER(arg_firmware, freep);
static int help(void) {
_cleanup_free_ char *link = NULL;
@ -83,6 +85,7 @@ static int help(void) {
" --qemu-gui Start QEMU in graphical mode\n"
" --secure-boot=BOOL Configure whether to search for firmware which\n"
" supports Secure Boot\n"
" --firmware=PATH|list Select firmware definition file (or list available)\n"
"\n%3$sSystem Identity:%4$s\n"
" -M --machine=NAME Set the machine name for the container\n"
"\n%3$sCredentials:%4$s\n"
@ -115,6 +118,7 @@ static int parse_argv(int argc, char *argv[]) {
ARG_SECURE_BOOT,
ARG_SET_CREDENTIAL,
ARG_LOAD_CREDENTIAL,
ARG_FIRMWARE,
};
static const struct option options[] = {
@ -132,6 +136,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "secure-boot", required_argument, NULL, ARG_SECURE_BOOT },
{ "set-credential", required_argument, NULL, ARG_SET_CREDENTIAL },
{ "load-credential", required_argument, NULL, ARG_LOAD_CREDENTIAL },
{ "firmware", required_argument, NULL, ARG_FIRMWARE },
{}
};
@ -242,6 +247,31 @@ static int parse_argv(int argc, char *argv[]) {
break;
}
case ARG_FIRMWARE:
if (streq(optarg, "list")) {
_cleanup_strv_free_ char **l = NULL;
r = list_ovmf_config(&l);
if (r < 0)
return log_error_errno(r, "Failed to list firmwares: %m");
bool nl = false;
fputstrv(stdout, l, "\n", &nl);
if (nl)
putchar('\n');
return 0;
}
if (!isempty(optarg) && !path_is_absolute(optarg) && !startswith(optarg, "./"))
return log_error_errno(SYNTHETIC_ERRNO(errno), "Absolute path or path starting with './' required.");
r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_firmware);
if (r < 0)
return r;
break;
case '?':
return -EINVAL;
@ -460,7 +490,10 @@ static int run_virtual_machine(void) {
use_kvm = r;
}
r = find_ovmf_config(arg_secure_boot, &ovmf_config);
if (arg_firmware)
r = load_ovmf_config(arg_firmware, &ovmf_config);
else
r = find_ovmf_config(arg_secure_boot, &ovmf_config);
if (r < 0)
return log_error_errno(r, "Failed to find OVMF config: %m");