[Libguestfs] [PATCH] v2v: fix UEFI bootloader for linux guests

Denis Plotnikov dplotnikov at virtuozzo.com
Fri May 15 07:44:11 UTC 2020


Not all UEFI guests can survive conversion, because of lost bootloader
information in UEFI NVRAM. But some guest can cope with this because they
have a fallback bootloader and use UEFI Removable Media Boot Behavior.
(see https://uefi.org/sites/default/files/resources/UEFI_Spec_2_8_A_Feb14.pdf
3.5.1.1 Removable Media Boot Behavior) to load. If UEFI firmware can't find
a bootloader in its settings it uses the removable media boot behavior to
find a bootloader.

We can fix the guests which don't have such a fallback loader by providing
a temporary one. This bootloader is used for the first boot only, then the
conversion script restores the initial bootloader settings and removes the
temporary loader.

Signed-off-by: Denis Plotnikov <dplotnikov at virtuozzo.com>
---
 v2v/convert_linux.ml      |  15 +++++
 v2v/linux_bootloaders.ml  | 149 ++++++++++++++++++++++++++++++++++++++++++++--
 v2v/linux_bootloaders.mli |   2 +
 3 files changed, 162 insertions(+), 4 deletions(-)

diff --git a/v2v/convert_linux.ml b/v2v/convert_linux.ml
index e91ae12..77a2555 100644
--- a/v2v/convert_linux.ml
+++ b/v2v/convert_linux.ml
@@ -1122,6 +1122,21 @@ shutdown -h -P +0
       Linux.augeas_reload g
     );
 
+    (* Some linux uefi setups can't boot after conversion because of
+       lost uefi boot entries. The uefi boot entries are stored in uefi
+       NVRAM. The NVRAM content isn't a part of vm disk content and
+       usualy can't be converted with a vm. If a vm doesn't have uefi
+       fallback path (/EFI/BOOT/BOOT<arch>.efi), the vm is unbootable
+       after conversion. The following function will try to make an uefi
+       fallback path if the vm being converted is an uefi setup.
+    *)
+
+    let efi_fix_script = bootloader#fix_efi_boot () in
+
+    if efi_fix_script <> "" then
+      Firstboot.add_firstboot_script g inspect.i_root
+        "fix uefi boot" efi_fix_script;
+
     (* Delete blkid caches if they exist, since they will refer to the old
      * device names.  blkid will rebuild these on demand.
      *
diff --git a/v2v/linux_bootloaders.ml b/v2v/linux_bootloaders.ml
index de3d107..cdab7bf 100644
--- a/v2v/linux_bootloaders.ml
+++ b/v2v/linux_bootloaders.ml
@@ -36,6 +36,7 @@ class virtual bootloader = object
   method virtual configure_console : unit -> unit
   method virtual remove_console : unit -> unit
   method update () = ()
+  method virtual fix_efi_boot : unit -> string
 end
 
 (* Helper function for SUSE: remove (hdX,X) prefix from a path. *)
@@ -43,6 +44,115 @@ let remove_hd_prefix =
   let rex = PCRE.compile "^\\(hd.*\\)" in
   PCRE.replace rex ""
 
+(* Standard uefi fallback path *)
+let uefi_fallback_path =
+  "/boot/efi/EFI/BOOT/"
+
+(* Helper function checks if 'source' contains 's' *)
+let contains source s =
+    let re = Str.regexp_string s in
+    try
+       ignore (Str.search_forward re source 0);
+       true
+    with Not_found -> false
+
+(* Helper function to get architecture suffixes for uefi files *)
+let get_uefi_arch_suffix arch =
+  let arch_suffix =
+    if contains arch "x86_64" then "X64"
+    else if contains arch "x86_32" then "X32"
+    else "" in
+  arch_suffix
+
+(* Function fixes uefi boot. It's used in both cases: legacy grub and grub2 *)
+let fix_uefi g distro distro_ver grub_config arch =
+    let cant_fix_uefi () = (
+      info (f_"Can't fix UEFI bootloader. VM may not boot.");
+      "" ) in
+
+    let file_exists file =
+      if g#exists file then
+        true
+      else (
+        info (f_"Can't find file: '%s' needed for UEFI bootloader fixing") file;
+        false ) in
+
+    let grub_path = String.sub grub_config 0 (String.rindex grub_config '/') in
+    let uefi_fallback_name =
+      let arch_suffix = get_uefi_arch_suffix arch in
+      if arch_suffix <> "" then
+        String.concat "" [uefi_fallback_path; "BOOT"; arch_suffix; ".EFI"]
+      else
+          "" in
+
+    if uefi_fallback_name = "" then (
+      info (f_"Can't determine UEFI fallback path.");
+      cant_fix_uefi () )
+    else if g#exists uefi_fallback_name then
+      (* don't do anything if uefi fallback exists *)
+      ""
+    else if uefi_fallback_name = "" then
+      cant_fix_uefi ()
+    else (
+      info (f_"Fixing UEFI bootloader.");
+      match distro, distro_ver with
+      | "centos", 6 ->
+        (* to make a bootable uefi centos 6 we need to
+         * copy grub.efi and grub.conf to UEFI fallback path
+         * and rename them to BOOT<arch>.efi and BOOT<arch>.conf
+         * correspondingly *)
+        let uefi_grub_name = String.concat "" [grub_path; "/grub.efi"] in
+        let uefi_grub_conf = String.concat "" [
+                               String.sub uefi_fallback_name 0
+                                 (String.rindex uefi_fallback_name '.');
+                                 ".conf" ] in
+        if file_exists uefi_grub_name && file_exists grub_config then (
+          g#mkdir_p uefi_fallback_path;
+          g#cp uefi_grub_name uefi_fallback_name;
+          g#cp grub_config uefi_grub_conf;
+          let script = sprintf
+"#!/bin/bash
+efibootmgr -c -L \"CentOS 6\"
+rm -rf %s" uefi_fallback_path in
+          script )
+        else
+          cant_fix_uefi ()
+      | "ubuntu", 14 ->
+        (* to make a bootable uefi ubuntu 14 we need to
+         * copy shim<arch>.efi to UEFI fallback path
+         * and rename it to BOOT<arch>.efi, also we copy
+         * grub.efi and grub.cfg to UEFI fallback path without renaming *)
+        let arch_suffix =
+          String.lowercase_ascii (get_uefi_arch_suffix arch) in
+        let shim =
+          String.concat "" [grub_path; "/shim"; arch_suffix; ".efi"] in
+        let uefi_grub_name =
+          String.concat "" [grub_path; "/grub"; arch_suffix; ".efi"] in
+
+        if file_exists shim && file_exists uefi_grub_name
+                            && file_exists grub_config then (
+          g#mkdir_p uefi_fallback_path;
+          g#cp shim uefi_fallback_name;
+          g#cp uefi_grub_name uefi_fallback_path;
+          g#cp grub_config uefi_fallback_path;
+          (* if the shim is at the standard path, clean up uefi fixing
+           * if not, then just don't clean up and leave the temp loader
+           * at UEFI fallback path for simplicity
+           *)
+          if contains shim "/boot/efi/EFI/ubuntu/shim" then
+            sprintf
+"#!/bin/bash
+sudo efibootmgr -c -L ubuntu -l \\\\EFI\\\\ubuntu\\\\shim%s.efi
+rm -rf %s" arch_suffix uefi_fallback_path
+          else
+            "")
+        else
+          cant_fix_uefi ()
+      | _, _ ->
+        info (f_"No UEFI fix rule for %s %d") distro distro_ver;
+        cant_fix_uefi ()
+    )
+
 (* Grub1 (AKA grub-legacy) representation. *)
 class bootloader_grub1 (g : G.guestfs) inspect grub_config =
   let () =
@@ -60,6 +170,16 @@ class bootloader_grub1 (g : G.guestfs) inspect grub_config =
         fun path -> List.mem_assoc path mounts
       ) [ "/boot/grub"; "/boot" ]
     with Not_found -> "" in
+
+  let uefi_active =
+    match inspect.i_firmware with
+    | I_UEFI _ -> true
+    | _ -> false in
+
+  let arch = inspect.i_arch in
+  let distro = inspect.i_distro in
+  let distro_ver_major = inspect.i_major_version in
+
 object
   inherit bootloader
 
@@ -184,6 +304,12 @@ object
     loop paths;
 
     g#aug_save ()
+
+  method fix_efi_boot () =
+    if uefi_active then
+      fix_uefi g distro distro_ver_major grub_config arch
+    else
+      ""
 end
 
 (** The method used to get and set the default kernel in Grub2. *)
@@ -193,7 +319,7 @@ type default_kernel_method =
   | MethodNone  (** No known way. *)
 
 (* Grub2 representation. *)
-class bootloader_grub2 (g : G.guestfs) grub_config =
+class bootloader_grub2 (g : G.guestfs) inspect grub_config =
 
   let grub2_mkconfig_cmd =
     let elems = [
@@ -221,6 +347,15 @@ class bootloader_grub2 (g : G.guestfs) grub_config =
       MethodNone
     ) in
 
+  let uefi_active =
+    match inspect.i_firmware with
+    | I_UEFI _ -> true
+    | _ -> false in
+
+  let arch = inspect.i_arch in
+  let distro = inspect.i_distro in
+  let distro_ver_major = inspect.i_major_version in
+
 object (self)
   inherit bootloader
 
@@ -340,8 +475,14 @@ object (self)
 
   method remove_console = self#grub2_update_console ~remove:true
 
-  method update () =
-    ignore (g#command [| grub2_mkconfig_cmd; "-o"; grub_config |])
+  method update () = (
+    ignore (g#command [| grub2_mkconfig_cmd; "-o"; grub_config |]))
+
+  method fix_efi_boot () =
+    if uefi_active then
+      fix_uefi g distro distro_ver_major grub_config arch
+    else
+      ""
 end
 
 (* Helper type used in detect_bootloader. *)
@@ -390,6 +531,6 @@ let detect_bootloader (g : G.guestfs) inspect =
   let bl =
     match typ with
     | Grub1 -> new bootloader_grub1 g inspect grub_config
-    | Grub2 -> new bootloader_grub2 g grub_config in
+    | Grub2 -> new bootloader_grub2 g inspect grub_config in
   debug "detected bootloader %s at %s" bl#name grub_config;
   bl
diff --git a/v2v/linux_bootloaders.mli b/v2v/linux_bootloaders.mli
index 30cdfe3..c4e1069 100644
--- a/v2v/linux_bootloaders.mli
+++ b/v2v/linux_bootloaders.mli
@@ -44,6 +44,8 @@ class virtual bootloader : object
   (** Update the bootloader: For grub2 only this runs the
       [grub2-mkconfig] command to rebuild the configuration.  This
       is not necessary for grub-legacy. *)
+  method virtual fix_efi_boot : unit -> string
+  (** fix UEFI bootloader and return a clean up script. *)
 end
 (** Encapsulates a Linux boot loader as object. *)
 
-- 
1.8.3.1




More information about the Libguestfs mailing list