[Libguestfs] [PATCH v2v 2/2] v2v: Copy static IP address information over for Windows guests (RHBZ#1626503).

Richard W.M. Jones rjones at redhat.com
Mon Apr 15 15:54:42 UTC 2019


For Linux the guest itself remembers the IP address associated with
each MAC address.  Thus it doesn't matter if the interface type
changes (ie. to virtio-net), because as long as we preserve the MAC
address the guest will use the same IP address or the same DHCP
configuration.

However on Windows this association is not maintained by MAC address.
In fact the MAC address isn't saved anywhere in the guest registry.
(It seems instead this is likely done through PCI device type and
address which we don't record at the moment and is almost impossible
to preserve.)  When a guest which doesn't use DHCP is migrated, the
guest sees the brand new virtio-net devices and doesn't know what to
do with them, and meanwhile the right static IPs are still associated
with the old and now-defunct interfaces in the registry.

We cannot collect the required information from within the guest.
However we can collect it outside the tool by some other means
(eg. using VMware Tools APIs) and present this information to virt-v2v
which then writes it into the Windows guest at firstboot time.

This commit adds the --force-interface option which creates a
Powershell script to set network adapters at firstboot.  An option
such as:

  virt-v2v ... --force-interface 00:0c:29:e6:3d:9d,192.168.0.89,192.168.0.1,24,192.168.0.254

approximately turns into this script:

  # Wait for the netkvm (virtio-net) driver to become active.
  $adapters = @()
  While (-Not $adapters) {
      Start-Sleep -Seconds 5
      $adapters = Get-NetAdapter -Physical |
                     Where DriverFileName -eq "netkvm.sys"
  }
  $mac_address = '00-0c-29-e6-3d-9d'
  $ifindex = (Get-NetAdapter -Physical |
                 Where MacAddress -eq $mac_address).ifIndex
  if ($ifindex) {
      New-NetIPAddress -InterfaceIndex $ifindex
                       -IPAddress '192.168.0.89'
                       -DefaultGateway '192.168.0.1'
                       -PrefixLength 24
      Set-DnsClientServerAddress -InterfaceIndex $ifindex
                       -ServerAddresses ('192.168.0.254')
  }

Thanks: Brett Thurber for diagnosing the problem and suggesting paths
towards a fix.
---
 v2v/cmdline.ml         | 28 +++++++++++++++-
 v2v/cmdline.mli        |  1 +
 v2v/convert_linux.ml   |  2 +-
 v2v/convert_windows.ml | 75 +++++++++++++++++++++++++++++++++++++++++-
 v2v/modules_list.ml    |  2 +-
 v2v/modules_list.mli   |  2 +-
 v2v/types.ml           |  8 +++++
 v2v/types.mli          |  9 +++++
 v2v/v2v.ml             |  7 ++--
 v2v/virt-v2v.pod       | 18 ++++++++++
 10 files changed, 144 insertions(+), 8 deletions(-)

diff --git a/v2v/cmdline.ml b/v2v/cmdline.ml
index 641eed017..271684249 100644
--- a/v2v/cmdline.ml
+++ b/v2v/cmdline.ml
@@ -33,6 +33,7 @@ type cmdline = {
   compressed : bool;
   debug_overlays : bool;
   do_copy : bool;
+  force_interfaces : force_interface list;
   in_place : bool;
   network_map : Networks.t;
   output_alloc : output_allocation;
@@ -99,6 +100,26 @@ let parse_cmdline () =
     set_input_option_compat k v
   in
 
+  let force_interfaces = ref [] in
+  let add_force_interface str =
+    let add if_mac_addr if_ip_address if_default_gateway if_prefix_length
+            if_nameservers =
+      List.push_back force_interfaces
+                     { if_mac_addr; if_ip_address; if_default_gateway;
+                       if_prefix_length; if_nameservers }
+    in
+    match String.nsplit "," str with
+    | [] | [_] ->
+       error (f_"invalid --force-interface option")
+    | [mac; ip] -> add mac ip None None []
+    | [mac; ip; gw] -> add mac ip (Some gw) None []
+    | mac :: ip :: gw :: len :: nameservers ->
+       let len =
+         try int_of_string len with
+         | Failure _ -> error (f_"cannot parse --force-interface prefix length field as an integer: %s") len in
+       add mac ip (Some gw) (Some len) nameservers
+  in
+
   let network_map = Networks.create () in
   let add_network str =
     match String.split ":" str with
@@ -204,6 +225,8 @@ let parse_cmdline () =
                                     s_"Compress output file (-of qcow2 only)";
     [ L"debug-overlay"; L"debug-overlays" ], Getopt.Set debug_overlays,
                                     s_"Save overlay files";
+    [ L"force-interface" ], Getopt.String ("mac,ip[,gw[,len[,ns,ns,...]]]", add_force_interface),
+                                    s_"Force interface to IP address";
     [ S 'i' ],       Getopt.String (i_options, set_input_mode),
                                     s_"Set input mode (default: libvirt)";
     [ M"ic" ],       Getopt.String ("uri", set_string_option_once "-ic" input_conn),
@@ -319,6 +342,7 @@ read the man page virt-v2v(1).
   let compressed = !compressed in
   let debug_overlays = !debug_overlays in
   let do_copy = !do_copy in
+  let force_interfaces = !force_interfaces in
   let input_conn = !input_conn in
   let input_format = !input_format in
   let input_mode = !input_mode in
@@ -364,6 +388,7 @@ read the man page virt-v2v(1).
     pr "io/oo\n";
     pr "mac-option\n";
     pr "bandwidth-option\n";
+    pr "force-interface-option\n";
     List.iter (pr "input:%s\n") (Modules_list.input_modules ());
     List.iter (pr "output:%s\n") (Modules_list.output_modules ());
     List.iter (pr "convert:%s\n") (Modules_list.convert_modules ());
@@ -696,7 +721,8 @@ read the man page virt-v2v(1).
       output_format, output_alloc in
 
   {
-    bandwidth; compressed; debug_overlays; do_copy; in_place;
+    bandwidth; compressed; debug_overlays; do_copy;
+    force_interfaces; in_place;
     network_map; output_alloc; output_format; output_name;
     print_estimate; print_source; root_choice;
     ks = opthandle.ks;
diff --git a/v2v/cmdline.mli b/v2v/cmdline.mli
index 1c9e6c258..a43adb142 100644
--- a/v2v/cmdline.mli
+++ b/v2v/cmdline.mli
@@ -23,6 +23,7 @@ type cmdline = {
   compressed : bool;
   debug_overlays : bool;
   do_copy : bool;
+  force_interfaces : Types.force_interface list;
   in_place : bool;
   network_map : Networks.t;
   output_alloc : Types.output_allocation;
diff --git a/v2v/convert_linux.ml b/v2v/convert_linux.ml
index f9e811c8d..1ada36115 100644
--- a/v2v/convert_linux.ml
+++ b/v2v/convert_linux.ml
@@ -34,7 +34,7 @@ open Linux_kernels
 module G = Guestfs
 
 (* The conversion function. *)
-let convert (g : G.guestfs) inspect source output rcaps =
+let convert (g : G.guestfs) inspect source output rcaps _ =
   (*----------------------------------------------------------------------*)
   (* Inspect the guest first.  We already did some basic inspection in
    * the common v2v.ml code, but that has to deal with generic guests
diff --git a/v2v/convert_windows.ml b/v2v/convert_windows.ml
index 7ea56592c..5894d857c 100644
--- a/v2v/convert_windows.ml
+++ b/v2v/convert_windows.ml
@@ -38,7 +38,7 @@ module G = Guestfs
  * time the Windows VM is booted on KVM.
  *)
 
-let convert (g : G.guestfs) inspect source output rcaps =
+let convert (g : G.guestfs) inspect source output rcaps force_interfaces =
   (*----------------------------------------------------------------------*)
   (* Inspect the Windows guest. *)
 
@@ -228,6 +228,8 @@ let convert (g : G.guestfs) inspect source output rcaps =
     Registry.with_hive_write g inspect.i_windows_software_hive
                              update_software_hive;
 
+    configure_network_interfaces net_driver;
+
     fix_ntfs_heads ();
 
     fix_win_esp ();
@@ -603,6 +605,77 @@ if errorlevel 3010 exit /b 0
     | None ->
        warning (f_"could not find registry key HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion")
 
+  and configure_network_interfaces net_driver =
+    (* If we were asked to force network interfaces to have particular
+     * static IP addresses then it is done here by installing a
+     * Powershell script which runs at boot.
+     *)
+    if force_interfaces <> [] then (
+      let psh_filename = "v2vnetcf.ps1" in
+      let psh = ref [] in
+      let add = List.push_back psh in
+
+      add "Set-PSDebug -Trace 1";
+      add "";
+
+      (* If virtio-net was added to the registry, we must wait for
+       * it to be installed at runtime.
+       *)
+      if net_driver = Virtio_net then (
+        add "# Wait for the netkvm (virtio-net) driver to become active.";
+        add "$adapters = @()";
+        add "While (-Not $adapters) {";
+        add "    Start-Sleep -Seconds 5";
+        add "    $adapters = Get-NetAdapter -Physical | Where DriverFileName -eq \"netkvm.sys\"";
+        add "    Write-Host \"adapters = '$adapters'\"";
+        add "}";
+        add ""
+      );
+
+      List.iter (
+        fun { if_mac_addr; if_ip_address; if_default_gateway;
+              if_prefix_length; if_nameservers } ->
+          add (sprintf "$mac_address = '%s'"
+                       (String.replace if_mac_addr ":" "-"));
+          add "$ifindex = (Get-NetAdapter -Physical | Where MacAddress -eq $mac_address).ifIndex";
+          add "if ($ifindex) {";
+
+          add "    Write-Host \"setting IP address of adapter at $ifindex\"";
+
+          (* New-NetIPAddress command *)
+          let args = ref [] in
+          List.push_back args "-InterfaceIndex";
+          List.push_back args "$ifindex";
+          List.push_back args "-IPAddress";
+          List.push_back args (sprintf "'%s'" if_ip_address);
+          (match if_default_gateway with
+           | None -> ()
+           | Some gw ->
+              List.push_back args "-DefaultGateway";
+              List.push_back args (sprintf "'%s'" gw)
+          );
+          (match if_prefix_length with
+           | None -> ()
+           | Some len ->
+              List.push_back args "-PrefixLength";
+              List.push_back args (string_of_int len)
+          );
+          let cmd1 = "New-NetIPAddress " ^ String.concat " " !args in
+          add ("    " ^ cmd1);
+
+          (* Set-DnsClientServerAddress command *)
+          if if_nameservers <> [] then (
+            add (sprintf "    Set-DnsClientServerAddress -InterfaceIndex $ifindex -ServerAddresses (%s)"
+                         (String.concat "," (List.map (sprintf "'%s'") if_nameservers)))
+          );
+          add "}";
+          add ""
+      ) force_interfaces;
+
+      (* Install the Powershell script to run at firstboot. *)
+      Windows.install_firstboot_powershell g inspect psh_filename !psh
+    ) (* force_interfaces <> [] *)
+
   and fix_ntfs_heads () =
     (* NTFS hardcodes the number of heads on the drive which created
        it in the filesystem header. Modern versions of Windows
diff --git a/v2v/modules_list.ml b/v2v/modules_list.ml
index a0a74aaf2..d6a6fbb72 100644
--- a/v2v/modules_list.ml
+++ b/v2v/modules_list.ml
@@ -38,7 +38,7 @@ type inspection_fn = Types.inspect -> bool
 
 type conversion_fn =
   Guestfs.guestfs -> Types.inspect -> Types.source -> Types.output_settings ->
-  Types.requested_guestcaps -> Types.guestcaps
+  Types.requested_guestcaps -> Types.force_interface list -> Types.guestcaps
 
 let convert_modules = ref []
 
diff --git a/v2v/modules_list.mli b/v2v/modules_list.mli
index 3e80d3e23..3c63232ed 100644
--- a/v2v/modules_list.mli
+++ b/v2v/modules_list.mli
@@ -34,7 +34,7 @@ type inspection_fn = Types.inspect -> bool
 
 type conversion_fn =
   Guestfs.guestfs -> Types.inspect -> Types.source -> Types.output_settings ->
-  Types.requested_guestcaps -> Types.guestcaps
+  Types.requested_guestcaps -> Types.force_interface list -> Types.guestcaps
 
 val register_convert_module : inspection_fn -> string -> conversion_fn -> unit
 (** [register_convert_module inspect_fn name fn] registers a
diff --git a/v2v/types.ml b/v2v/types.ml
index 0dac7cc65..b69bb9f9e 100644
--- a/v2v/types.ml
+++ b/v2v/types.ml
@@ -510,6 +510,14 @@ type bandwidth =
 | StaticBandwidth of string
 | DynamicBandwidth of string option * string
 
+type force_interface = {
+  if_mac_addr : string;
+  if_ip_address : string;
+  if_default_gateway : string option;
+  if_prefix_length : int option;
+  if_nameservers : string list;
+}
+
 class virtual input = object
   method precheck () = ()
   method virtual as_options : string
diff --git a/v2v/types.mli b/v2v/types.mli
index 1425fe3cf..3d0f0e05f 100644
--- a/v2v/types.mli
+++ b/v2v/types.mli
@@ -366,6 +366,15 @@ type bandwidth =
 | DynamicBandwidth of string option * string
 (** [--bandwidth] and [--bandwidth-file] options. *)
 
+type force_interface = {
+  if_mac_addr : string;
+  if_ip_address : string;
+  if_default_gateway : string option;
+  if_prefix_length : int option;
+  if_nameservers : string list;
+}
+(** [--force-interface] option. *)
+
 (** {2 Input object}
 
     This is subclassed for the various input [-i] options.
diff --git a/v2v/v2v.ml b/v2v/v2v.ml
index 5fb85bb38..e36eab2d4 100644
--- a/v2v/v2v.ml
+++ b/v2v/v2v.ml
@@ -133,7 +133,7 @@ let rec main () =
       | In_place ->
          rcaps_from_source source in
 
-    do_convert g inspect source output rcaps in
+    do_convert g inspect source output rcaps cmdline.force_interfaces in
 
   g#umount_all ();
 
@@ -557,7 +557,7 @@ and estimate_target_size mpstats overlays =
   )
 
 (* Conversion. *)
-and do_convert g inspect source output rcaps =
+and do_convert g inspect source output rcaps interfaces =
   (match inspect.i_product_name with
   | "unknown" ->
     message (f_"Converting the guest to run on KVM")
@@ -573,7 +573,8 @@ and do_convert g inspect source output rcaps =
   debug "picked conversion module %s" conversion_name;
   debug "requested caps: %s" (string_of_requested_guestcaps rcaps);
   let guestcaps =
-    convert g inspect source (output :> Types.output_settings) rcaps in
+    convert g inspect source (output :> Types.output_settings) rcaps
+            interfaces in
   debug "%s" (string_of_guestcaps guestcaps);
 
   (* Did we manage to install virtio drivers? *)
diff --git a/v2v/virt-v2v.pod b/v2v/virt-v2v.pod
index 8ba141be9..93d8f4b7b 100644
--- a/v2v/virt-v2v.pod
+++ b/v2v/virt-v2v.pod
@@ -237,6 +237,24 @@ Note this options only applies to keys and passphrases for encrypted
 devices and partitions, not for passwords used to connect to remote
 servers.
 
+=item B<--force-interface> mac,ip[,gw[,len[,nameserver,nameserver,...]]]
+
+Force a particular interface (controlled by its MAC address) to have a
+static IP address after boot.
+
+The fields in the parameter are: C<mac> is the MAC address in normal
+C<:>-delimited format.  C<ip> is the IP address.  C<gw> is the
+optional gateway IP address.  C<len> is the subnet mask length (an
+integer).  The final parameters are zero or more nameserver IP
+addresses.
+
+This option can be supplied zero or more times.
+
+You only need to use this option for certain broken guests such as
+Windows which are unable to preserve MAC:IP address mappings
+automatically.  It is currently ignored for Linux guests since they do
+not have this problem.
+
 =item B<-i> B<disk>
 
 Set the input method to I<disk>.
-- 
2.20.1




More information about the Libguestfs mailing list