v2v: Copy static IP address information over for Windows guests (RHBZ#1626503).

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 --mac ..:ip:.. sub-option which creates a
Powershell script to set network adapters at firstboot.  An option
such as:

  --mac 00:0c:29:e6:3d:9d:ip: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.
This commit is contained in:
Richard W.M. Jones
2018-12-04 16:09:42 +00:00
parent e1e9b3845e
commit dfd9fac743
10 changed files with 150 additions and 16 deletions

View File

@@ -41,11 +41,12 @@ type cmdline = {
print_estimate : bool;
print_source : bool;
root_choice : root_choice;
static_ips : static_ip list;
ks : Tools_utils.key_store;
}
(* Matches --mac command line parameters. *)
let mac_re = PCRE.compile ~anchored:true "([[:xdigit:]]{2}:[[:xdigit:]]{2}:[[:xdigit:]]{2}:[[:xdigit:]]{2}:[[:xdigit:]]{2}:[[:xdigit:]]{2}):(network|bridge):(.*)"
let mac_re = PCRE.compile ~anchored:true "([[:xdigit:]]{2}:[[:xdigit:]]{2}:[[:xdigit:]]{2}:[[:xdigit:]]{2}:[[:xdigit:]]{2}:[[:xdigit:]]{2}):(network|bridge|ip):(.*)"
let parse_cmdline () =
let bandwidth = ref None in
@@ -100,6 +101,7 @@ let parse_cmdline () =
in
let network_map = Networks.create () in
let static_ips = ref [] in
let add_network str =
match String.split ":" str with
| "", "" ->
@@ -122,11 +124,30 @@ let parse_cmdline () =
if not (PCRE.matches mac_re str) then
error (f_"cannot parse --mac \"%s\" parameter") str;
let mac = PCRE.sub 1 and out = PCRE.sub 3 in
let vnet_type =
match PCRE.sub 2 with
| "network" -> Network | "bridge" -> Bridge
| _ -> assert false in
Networks.add_mac network_map mac vnet_type out
match PCRE.sub 2 with
| "network" ->
Networks.add_mac network_map mac Network out
| "bridge" ->
Networks.add_mac network_map mac Bridge out
| "ip" ->
let add if_mac_addr if_ip_address if_default_gateway
if_prefix_length if_nameservers =
List.push_back static_ips
{ if_mac_addr; if_ip_address; if_default_gateway;
if_prefix_length; if_nameservers }
in
(match String.nsplit "," out with
| [] ->
error (f_"invalid --mac ip option")
| [ip] -> add mac ip None None []
| [ip; gw] -> add mac ip (Some gw) None []
| ip :: gw :: len :: nameservers ->
let len =
try int_of_string len with
| Failure _ -> error (f_"cannot parse --mac ip prefix length field as an integer: %s") len in
add mac ip (Some gw) (Some len) nameservers
);
| _ -> assert false
in
let no_trim_warning _ =
@@ -218,8 +239,8 @@ let parse_cmdline () =
s_"Input transport";
[ L"in-place" ], Getopt.Set in_place,
s_"Only tune the guest in the input VM";
[ L"mac" ], Getopt.String ("mac:network|bridge:out", add_mac),
s_"Map NIC to network or bridge";
[ L"mac" ], Getopt.String ("mac:network|bridge|ip:out", add_mac),
s_"Map NIC to network or bridge or assign static IP";
[ S 'n'; L"network" ], Getopt.String ("in:out", add_network),
s_"Map network in to out";
[ L"no-copy" ], Getopt.Clear do_copy,
@@ -347,6 +368,7 @@ read the man page virt-v2v(1).
let print_source = !print_source in
let qemu_boot = !qemu_boot in
let root_choice = !root_choice in
let static_ips = !static_ips in
(* No arguments and machine-readable mode? Print out some facts
* about what this binary supports.
@@ -364,6 +386,7 @@ read the man page virt-v2v(1).
pr "io/oo\n";
pr "mac-option\n";
pr "bandwidth-option\n";
pr "mac-ip-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 ());
@@ -698,7 +721,7 @@ read the man page virt-v2v(1).
{
bandwidth; compressed; debug_overlays; do_copy; in_place;
network_map; output_alloc; output_format; output_name;
print_estimate; print_source; root_choice;
print_estimate; print_source; root_choice; static_ips;
ks = opthandle.ks;
},
input, output

View File

@@ -31,6 +31,7 @@ type cmdline = {
print_estimate : bool;
print_source : bool;
root_choice : Types.root_choice;
static_ips : Types.static_ip list;
ks : Tools_utils.key_store;
}

View File

@@ -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

View File

@@ -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 static_ips =
(*----------------------------------------------------------------------*)
(* 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,78 @@ 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 static_ips <> [] then (
let psh_filename = "v2vnetcf.ps1" in
let psh = ref [] in
let add = List.push_back psh in
add "# Uncomment this line for lots of debug output.";
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 ""
) static_ips;
(* Install the Powershell script to run at firstboot. *)
Windows.install_firstboot_powershell g inspect psh_filename !psh
) (* static_ips <> [] *)
and fix_ntfs_heads () =
(* NTFS hardcodes the number of heads on the drive which created
it in the filesystem header. Modern versions of Windows

View File

@@ -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.static_ip list -> Types.guestcaps
let convert_modules = ref []

View File

@@ -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.static_ip list -> Types.guestcaps
val register_convert_module : inspection_fn -> string -> conversion_fn -> unit
(** [register_convert_module inspect_fn name fn] registers a

View File

@@ -510,6 +510,14 @@ type bandwidth =
| StaticBandwidth of string
| DynamicBandwidth of string option * string
type static_ip = {
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

View File

@@ -366,6 +366,15 @@ type bandwidth =
| DynamicBandwidth of string option * string
(** [--bandwidth] and [--bandwidth-file] options. *)
type static_ip = {
if_mac_addr : string;
if_ip_address : string;
if_default_gateway : string option;
if_prefix_length : int option;
if_nameservers : string list;
}
(** [--mac ..:ip:..] option. *)
(** {2 Input object}
This is subclassed for the various input [-i] options.

View File

@@ -127,7 +127,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.static_ips in
g#umount_all ();
@@ -551,7 +551,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")
@@ -567,7 +567,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? *)

View File

@@ -412,6 +412,24 @@ Map source NIC MAC address to a network or bridge.
See L</Networks and bridges> below.
=item B<--mac> aa:bb:cc:dd:ee:ffB<:ip:>ipaddr[,gw[,len[,ns,ns,...]]]
Force a particular interface (controlled by its MAC address) to have a
static IP address after boot.
The fields in the parameter are: C<ipaddr> 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 to static IP address mappings
automatically. You don't need to use it if Windows is using DHCP. It
is currently ignored for Linux guests since they do not have this
problem.
=item B<--machine-readable>
=item B<--machine-readable>=format