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.
This commit is an interim fix which, in limited situations described
below, copies the static IP address of the old interface to the new
virtio-net (netkvm.sys) interface. The installation of the old IP
address on the new interface is done using a Powershell script.
- Only works for IPv4 addresses.
- Likely only works properly if the guest has a single physical
interface.
- Only works if the guest is getting virtio drivers.
- Only works for Windows >= 7 (because of the Powershell dependency).
A longer term fix for this will likely involve trying to decode the
PCI address information (assuming my guess above is even correct) and
associate that with source hypervisor devices, get the same
information as now from the registry, and use a similar technique as
this to copy to the new interface, but do it based on target MAC
address.
Thanks: Brett Thurber for diagnosing the problem and suggesting paths
towards a fix.
---
common/mlstdutils/std_utils.ml | 9 +++
common/mlstdutils/std_utils.mli | 10 +++
v2v/convert_windows.ml | 131 ++++++++++++++++++++++++++++++++
3 files changed, 150 insertions(+)
diff --git a/common/mlstdutils/std_utils.ml b/common/mlstdutils/std_utils.ml
index df443058f..6b58c1de3 100644
--- a/common/mlstdutils/std_utils.ml
+++ b/common/mlstdutils/std_utils.ml
@@ -269,6 +269,8 @@ module String = struct
else loop (i+1)
in
loop 0
+
+ let unix2dos str = replace str "\n" "\r\n"
end
module List = struct
@@ -836,3 +838,10 @@ let read_first_line_from_file filename =
let is_regular_file path = (* NB: follows symlinks. *)
try (Unix.stat path).Unix.st_kind = Unix.S_REG
with Unix.Unix_error _ -> false
+
+let rec ipv4_prefix_length mask =
+ if mask = 0_l then 0
+ else if mask >= 0x8000_0000_l (* top bit set *) then
+ 1 + ipv4_prefix_length (Int32.shift_left mask 1)
+ else (* mask is invalid if top bit is not set but other bits are set *)
+ invalid_arg "ipv4_prefix_length"
diff --git a/common/mlstdutils/std_utils.mli b/common/mlstdutils/std_utils.mli
index 62cb8e9ff..bc2d52fc6 100644
--- a/common/mlstdutils/std_utils.mli
+++ b/common/mlstdutils/std_utils.mli
@@ -131,6 +131,11 @@ module String : sig
segment of [str] which contains only bytes {!i not} in [reject].
These work exactly like the C functions [strspn] and [strcspn]. *)
+ val unix2dos : string -> string
+ (** Convert string with ordinary Unix-style line-endings to
+ CRLF DOS-style line-endings.
+
+ The same as {!String.replace} [str "\n" "\r\n"]. *)
end
(** Override the String module from stdlib. *)
@@ -447,3 +452,8 @@ val read_first_line_from_file : string -> string
val is_regular_file : string -> bool
(** Checks whether the file is a regular file. *)
+
+val ipv4_prefix_length : int32 -> int
+(** Calculate the prefix length of the given IPv4 network mask
+ (given in host byte order). Raises [Invalid_argument _] if
+ not a network mask. *)
diff --git a/v2v/convert_windows.ml b/v2v/convert_windows.ml
index 2d2b6adfe..610af5793 100644
--- a/v2v/convert_windows.ml
+++ b/v2v/convert_windows.ml
@@ -38,10 +38,23 @@ module G = Guestfs
* time the Windows VM is booted on KVM.
*)
+let ipv4_re = PCRE.compile "^(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)$"
+
let convert (g : G.guestfs) inspect source output rcaps =
(*----------------------------------------------------------------------*)
(* Inspect the Windows guest. *)
+ (* All physical network interfaces. *)
+ let all_physical_interfaces =
+ List.filter (fun { if_name } -> String.is_prefix if_name "Ethernet")
+ inspect.i_interfaces in
+
+ (* All physical network interfaces have static IP addresses? *)
+ let all_physical_interfaces_are_static =
+ all_physical_interfaces <> [] &&
+ List.for_all (fun { if_enable_dhcp } -> if_enable_dhcp = false)
+ all_physical_interfaces in
+
(* If the Windows guest appears to be using group policy. *)
let has_group_policy =
Registry.with_hive_readonly g inspect.i_windows_software_hive
@@ -220,6 +233,16 @@ let convert (g : G.guestfs) inspect source output rcaps =
Registry.with_hive_write g inspect.i_windows_software_hive
update_software_hive;
+ (* If we have only static interfaces then we need to copy the
+ * IP addresses from the old interfaces to the new virtio-net
+ * interfaces. Only works for Win7 and above. (RHBZ#1626503)
+ *)
+ if net_driver = Virtio_net &&
+ all_physical_interfaces_are_static &&
+ (inspect.i_major_version, inspect.i_minor_version) >= (6, 1)
+ then
+ configure_ip_address_transfer_at_firstboot ();
+
fix_ntfs_heads ();
fix_win_esp ();
@@ -595,6 +618,114 @@ if errorlevel 3010 exit /b 0
| None ->
warning (f_"could not find registry key
HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion")
+ and configure_ip_address_transfer_at_firstboot () =
+ (* For guests which don't use DHCP we will need to copy network
+ * information from the first of the old interfaces to the
+ * first of the virtio-net interfaces. Unfortunately Windows
+ * doesn't save information about MAC address so we have no way
+ * to associate which original interface corresponds to which
+ * target interface. Instead we simply apply the settings to
+ * the adapters in sorted name order. XXX (RHBZ#1626503)
+ *)
+
+ assert (all_physical_interfaces <> []); (* checked by caller *)
+
+ let tempdir = sprintf "%s/Temp" inspect.i_windows_systemroot in
+ let psh_filename = "v2v-ip-address-transfer.ps1" in
+ let psh_prologue = "\
+Set-PSDebug -Trace 1
+
+# Wait for the netkvm (virtio-net) driver to become active.
+$adapters = @()
+While (-Not $adapters) {
+ Start-Sleep -Seconds 5
+ $adapters = (Get-NetAdapter | Where DriverFileName -eq
\"netkvm.sys\").InterfaceAlias | Sort-Object
+ Write-Host \"adapters = '$adapters'\"
+}
+
+" in
+
+ (* Append the command(s) to set up each interface. *)
+ let psh_cmds =
+ List.mapi (
+ fun i { if_default_gateway; if_ip_address; if_subnet_mask;
+ if_nameserver } ->
+ let cmd = "New-NetIPAddress" in
+ let args = ref [] in
+ List.push_back args "-InterfaceAlias";
+ List.push_back args (sprintf "$adapters[%d]" i);
+
+ (* Parameters of New-NetIPAddress.
+ *
https://docs.microsoft.com/en-us/powershell/module/nettcpip/new-netipaddr...
+ * Only works for IPv4 right now. We need to find an example
+ * of a Windows guest using IPv6 to see what the registry
+ * contains. XXX
+ *)
+ if if_ip_address <> "" then (
+ List.push_back args "-IPAddress";
+ List.push_back args ("\"" ^ if_ip_address ^
"\"")
+ );
+ if if_default_gateway <> "" then (
+ List.push_back args "-DefaultGateway";
+ List.push_back args ("\"" ^ if_default_gateway ^
"\"")
+ );
+ if PCRE.matches ipv4_re if_subnet_mask then (
+ let a = Int32.of_string (PCRE.sub 1)
+ and b = Int32.of_string (PCRE.sub 2)
+ and c = Int32.of_string (PCRE.sub 3)
+ and d = Int32.of_string (PCRE.sub 4) in
+ (* Can we calculate a mask? *)
+ if a >= 0_l && a <= 255_l && b >= 0_l && b
<= 255_l &&
+ c >= 0_l && c <= 255_l && d >= 0_l && d
<= 255_l then (
+ let a = Int32.shift_left a 24
+ and b = Int32.shift_left b 16
+ and c = Int32.shift_left c 8 in
+ let mask = Int32.logor (Int32.logor (Int32.logor a b) c) d in
+ try
+ let prefix_length = ipv4_prefix_length mask in
+ List.push_back args "-PrefixLength";
+ List.push_back args (string_of_int prefix_length)
+ with Invalid_argument _ -> ()
+ )
+ );
+ let cmd1 = cmd ^ " " ^ String.concat " " !args in
+
+ let cmd2 =
+ if if_nameserver <> [] then (
+ let nslist = List.map (sprintf "\"%s\"") if_nameserver
in
+ let nslist = "(" ^ String.concat "," nslist ^
")" in
+
+ let cmd = "Set-DnsClientServerAddress" in
+ let args = ref [] in
+ List.push_back args "-InterfaceAlias";
+ List.push_back args (sprintf "$adapters[%d]" i);
+ List.push_back args "-ServerAddresses";
+ List.push_back args nslist;
+ cmd ^ " " ^ String.concat " " !args
+ )
+ else "" in
+
+ cmd1 ^ "\n" ^ cmd2 ^ "\n"
+ ) all_physical_interfaces in
+
+ let psh = psh_prologue ^ String.concat "\n" psh_cmds in
+
+ (* Powershell scripts fail completely unless they have DOS line endings. *)
+ let psh = String.unix2dos psh in
+ g#mkdir_p tempdir;
+ g#write (tempdir ^ "/" ^ psh_filename) psh;
+
+ (* Unfortunately Powershell scripts cannot be directly executed
+ * (unless some system config changes are made which for other
+ * reasons we don't want to do) and so we have to run this via
+ * a regular batch file.
+ *)
+ let fb =
+ sprintf "%s\\System32\\WindowsPowerShell\\v1.0\\powershell.exe
-ExecutionPolicy ByPass -file C:%s\\%s"
+ inspect.i_windows_systemroot
+ (String.replace_char tempdir '/' '\\') psh_filename in
+ Firstboot.add_firstboot_script g inspect.i_root "ip address transfer" fb
+
and fix_ntfs_heads () =
(* NTFS hardcodes the number of heads on the drive which created
it in the filesystem header. Modern versions of Windows
--
2.19.1