1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456
|
(* virt-v2v
* Copyright (C) 2017-2025 Red Hat Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*)
open Printf
open Scanf
open Std_utils
open Tools_utils
open Unix_utils
open Common_gettext.Gettext
open Types
open Utils
open Name_from_disk
type vmx_source =
| VMXSourceFile of string (* local file or NFS *)
| VMXSourceSSH of Nbdkit_ssh.password option * Xml.uri (* SSH URI *)
(* The single filename on the command line is interpreted either as
* a local file or a remote SSH URI (only if ‘-it ssh’).
*)
let vmx_source_of_arg input_password input_transport arg =
match input_transport, arg with
| None, arg -> VMXSourceFile arg
| Some `SSH, arg ->
let uri =
try Xml.parse_uri arg
with Invalid_argument _ ->
error (f_"remote vmx ‘%s’ could not be parsed as a URI") arg in
if uri.Xml.uri_scheme <> None && uri.Xml.uri_scheme <> Some "ssh" then
error (f_"vmx URI start with ‘ssh://...’");
if uri.Xml.uri_server = None then
error (f_"vmx URI remote server name omitted");
if uri.Xml.uri_path = None || uri.Xml.uri_path = Some "/" then
error (f_"vmx URI path component looks incorrect");
VMXSourceSSH (input_password, uri)
let rec find_disks vmx vmx_source =
(* Set the s_disk_id field to an incrementing number. *)
List.mapi
(fun i (source, filename) -> { source with s_disk_id = i }, filename)
(find_scsi_disks vmx vmx_source @
find_nvme_disks vmx vmx_source @
find_sata_disks vmx vmx_source @
find_ide_disks vmx vmx_source)
(* Find all SCSI hard disks.
*
* In the VMX file:
* scsi0.virtualDev = "pvscsi" # or may be "lsilogic" etc.
* scsi0:0.deviceType = "disk" | "plainDisk" | "rawDisk" | "scsi-hardDisk"
* | omitted
* scsi0:0.fileName = "guest.vmdk"
*)
and find_scsi_disks vmx vmx_source =
let get_scsi_controller_target ns =
sscanf ns "scsi%d:%d" (fun c t -> c, t)
in
let is_scsi_controller_target ns =
try ignore (get_scsi_controller_target ns); true
with Scanf.Scan_failure _ | End_of_file | Failure _ -> false
in
let scsi_device_types = [ Some "disk"; Some "plaindisk"; Some "rawdisk";
Some "scsi-harddisk"; None ] in
let scsi_controller = Source_SCSI in
find_hdds vmx vmx_source
get_scsi_controller_target is_scsi_controller_target
scsi_device_types scsi_controller
(* Find all NVMe hard disks.
*
* In the VMX file:
* nvme0.pcislotnumber = "192"
* nvme0:0.fileName = "guest.vmdk"
*)
and find_nvme_disks vmx vmx_source =
let get_nvme_controller_target ns =
sscanf ns "nvme%d:%d" (fun c t -> c, t)
in
let is_nvme_controller_target ns =
try ignore (get_nvme_controller_target ns); true
with Scanf.Scan_failure _ | End_of_file | Failure _ -> false
in
let nvme_device_types = [ None ] in
let nvme_controller = Source_NVME in
find_hdds vmx vmx_source
get_nvme_controller_target is_nvme_controller_target
nvme_device_types nvme_controller
(* Find all SATA hard disks.
*
* In the VMX file:
* sata0.pciSlotNumber = "33"
* sata0:3.fileName = "win2019_1.vmdk"
*
* The "deviceType" field must be absent or "disk". Other types here
* include "cdrom-raw" and others indicating a CD-ROM, which we should
* ignore.
*)
and find_sata_disks vmx vmx_source =
let get_sata_controller_target ns =
sscanf ns "sata%d:%d" (fun c t -> c, t)
in
let is_sata_controller_target ns =
try ignore (get_sata_controller_target ns); true
with Scanf.Scan_failure _ | End_of_file | Failure _ -> false
in
let sata_device_types = [ Some "disk"; None ] in
let sata_controller = Source_SATA in
find_hdds vmx vmx_source
get_sata_controller_target is_sata_controller_target
sata_device_types sata_controller
(* Find all IDE hard disks.
*
* In the VMX file:
* ide0:0.deviceType = "ata-hardDisk"
* ide0:0.fileName = "guest.vmdk"
*)
and find_ide_disks vmx vmx_source =
let get_ide_controller_target ns =
sscanf ns "ide%d:%d" (fun c t -> c, t)
in
let is_ide_controller_target ns =
try ignore (get_ide_controller_target ns); true
with Scanf.Scan_failure _ | End_of_file | Failure _ -> false
in
let ide_device_types = [ Some "ata-harddisk" ] in
let ide_controller = Source_IDE in
find_hdds vmx vmx_source
get_ide_controller_target is_ide_controller_target
ide_device_types ide_controller
and find_hdds vmx vmx_source
get_controller_target is_controller_target
device_types controller =
(* Find namespaces matching '(ide|scsi|nvme|sata)X:Y' with suitable
* deviceType.
*)
let hdds =
Parse_vmx.select_namespaces (
function
| [ns] ->
(* Check the namespace is '(ide|scsi|nvme|sata)X:Y' *)
if not (is_controller_target ns) then false
else (
(* Check the deviceType is one we are looking for. *)
let dt = Parse_vmx.get_string vmx [ns; "deviceType"] in
let dt = Option.map String.lowercase_ascii dt in
List.mem dt device_types
)
| _ -> false
) vmx in
(* Map the subset to a list of disks. *)
let hdds =
Parse_vmx.map (
fun path v ->
match path, v with
| [ns; "filename"], Some filename ->
let c, t = get_controller_target ns in
let s = { s_disk_id = (-1); s_controller = Some controller } in
Some (c, t, s, filename)
| _ -> None
) hdds in
let hdds = List.filter_map Fun.id hdds in
(* We don't have a way to return the controllers and targets, so
* just make sure the disks are sorted into order, since Parse_vmx
* won't return them in any particular order.
*)
let hdds = List.sort compare hdds in
List.map (fun (_, _, source, filename) -> source, filename) hdds
(* Find all removable disks.
*
* In the VMX file:
* ide1:0.deviceType = "cdrom-image"
* ide1:0.fileName = "boot.iso"
*
* XXX This only supports IDE CD-ROMs, but we could support SCSI CD-ROMs, SATA
* CD-ROMs, and floppies in future.
*)
and find_removables vmx =
let get_ide_controller_target ns =
sscanf ns "ide%d:%d" (fun c t -> c, t)
in
let is_ide_controller_target ns =
try ignore (get_ide_controller_target ns); true
with Scanf.Scan_failure _ | End_of_file | Failure _ -> false
in
let device_types = [ "atapi-cdrom";
"cdrom-image"; "cdrom-raw" ] in
(* Find namespaces matching 'ideX:Y' with suitable deviceType. *)
let devs =
Parse_vmx.select_namespaces (
function
| [ns] ->
(* Check the namespace is 'ideX:Y' *)
if not (is_ide_controller_target ns) then false
else (
(* Check the deviceType is one we are looking for. *)
match Parse_vmx.get_string vmx [ns; "deviceType"] with
| Some str ->
let str = String.lowercase_ascii str in
List.mem str device_types
| None -> false
)
| _ -> false
) vmx in
(* Map the subset to a list of CD-ROMs. *)
let devs =
Parse_vmx.map (
fun path v ->
match path, v with
| [ns], None ->
let c, t = get_ide_controller_target ns in
let s = { s_removable_type = CDROM;
s_removable_controller = Some Source_IDE;
s_removable_slot = Some (ide_slot c t) } in
Some s
| _ -> None
) devs in
let devs = List.filter_map Fun.id devs in
(* Sort by slot. *)
let devs =
List.sort
(fun { s_removable_slot = s1 } { s_removable_slot = s2 } ->
compare s1 s2)
devs in
devs
and ide_slot c t =
(* Assuming the old master/slave arrangement. *)
c * 2 + t
(* Find all ethernet cards.
*
* In the VMX file:
* ethernet0.virtualDev = "vmxnet3"
* ethernet0.networkName = "VM Network"
* ethernet0.generatedAddress = "00:01:02:03:04:05"
* ethernet0.connectionType = "bridged" # also: "custom", "nat" or not present
*)
and find_nics vmx =
let get_ethernet_port ns =
sscanf ns "ethernet%d" (fun p -> p)
in
let is_ethernet_port ns =
try ignore (get_ethernet_port ns); true
with Scanf.Scan_failure _ | End_of_file | Failure _ -> false
in
(* Find namespaces matching 'ethernetX'. *)
let nics =
Parse_vmx.select_namespaces (
function
| [ns] -> is_ethernet_port ns
| _ -> false
) vmx in
(* Map the subset to a list of NICs. *)
let nics =
Parse_vmx.map (
fun path v ->
match path, v with
| [ns], None ->
let port = get_ethernet_port ns in
let mac = Parse_vmx.get_string vmx [ns; "generatedAddress"] in
let model = Parse_vmx.get_string vmx [ns; "virtualDev"] in
let model =
match model with
| Some m when String.lowercase_ascii m = "e1000" ->
Some Source_e1000
| Some model ->
Some (Source_other_nic (String.lowercase_ascii model))
| None -> None in
let vnet = Parse_vmx.get_string vmx [ns; "networkName"] in
let vnet =
match vnet with
| Some vnet -> vnet
| None -> ns (* "ethernetX" *) in
let vnet_type =
match Parse_vmx.get_string vmx [ns; "connectionType"] with
| Some b when String.lowercase_ascii b = "bridged" ->
Bridge
| Some _ | None -> Network in
Some (port,
{ s_mac = mac; s_nic_model = model;
s_vnet = vnet;
s_vnet_type = vnet_type })
| _ -> None
) nics in
let nics = List.filter_map Fun.id nics in
(* Sort by port. *)
let nics = List.sort compare nics in
let nics = List.map (fun (_, source) -> source) nics in
nics
let parse_domain_from_vmx vmx_source =
let tmpdir =
let t = Mkdtemp.temp_dir "vmx." in
On_exit.rm_rf t;
t in
(* If the transport is SSH, fetch the file from remote, else
* parse it from local.
*)
let vmx =
match vmx_source with
| VMXSourceFile filename -> Parse_vmx.parse_file filename
| VMXSourceSSH (password, uri) ->
let server =
match uri.uri_server with
| None -> assert false (* checked by vmx_source_of_arg *)
| Some server -> server in
let port =
match uri.uri_port with
| i when i <= 0 -> None
| i -> Some (string_of_int i) in
let user = uri.Xml.uri_user in
let path =
match uri.uri_path with
| None -> assert false (* checked by vmx_source_of_arg *)
| Some path -> path in
let filename = tmpdir // "source.vmx" in
Ssh.download_file ?password ?port ~server ?user path filename;
Parse_vmx.parse_file filename in
let name =
match Parse_vmx.get_string vmx ["displayName"] with
| Some s -> s
| None ->
warning (f_"no displayName key found in VMX file");
match vmx_source with
| VMXSourceFile filename -> name_from_disk filename
| VMXSourceSSH (_, uri) ->
match uri.uri_path with
| None -> assert false (* checked by vmx_source_of_arg *)
| Some path -> name_from_disk path in
let genid =
(* See: https://lists.nongnu.org/archive/html/qemu-devel/2018-07/msg02019.html *)
let genid_lo = Parse_vmx.get_int64 vmx ["vm"; "genid"]
and genid_hi = Parse_vmx.get_int64 vmx ["vm"; "genidX"] in
match genid_lo, genid_hi with
| None, None | Some _, None | None, Some _ ->
None
| Some lo, Some hi ->
(* See docs/vm-generation-id-across-hypervisors.txt *)
let sub = String.sub (sprintf "%016Lx%016Lx" lo hi) in
let uuid =
sub 8 8 ^ "-" ^
sub 4 4 ^ "-" ^
sub 0 4 ^ "-" ^
sub 30 2 ^ sub 28 2 ^ "-" ^
sub 26 2 ^ sub 24 2 ^ sub 22 2 ^ sub 20 2 ^ sub 18 2 ^ sub 16 2 in
Some uuid in
let memory_mb =
match Parse_vmx.get_int64 vmx ["memSize"] with
| None -> 32_L (* default is really 32 MB! *)
| Some i -> i in
let memory = memory_mb *^ 1024L *^ 1024L in
let vcpu =
match Parse_vmx.get_int vmx ["numvcpus"] with
| None -> 1
| Some i -> i in
let cpu_topology =
match Parse_vmx.get_int vmx ["cpuid"; "coresPerSocket"] with
| None -> None
| Some cores_per_socket ->
let sockets = vcpu / cores_per_socket in
if sockets <= 0 then (
warning (f_"invalid cpuid.coresPerSocket < number of vCPUs");
None
)
else
Some { s_cpu_sockets = sockets; s_cpu_cores = cores_per_socket;
s_cpu_threads = 1 } in
let firmware =
match Parse_vmx.get_string vmx ["firmware"] with
| None -> BIOS
| Some "efi" -> UEFI
(* Other values are not documented for this field ... *)
| Some fw ->
warning (f_"unknown firmware value '%s', assuming BIOS") fw;
BIOS in
let uefi_secureboot =
Parse_vmx.get_bool vmx ["uefi"; "secureBoot"; "enabled"] |>
Option.value ~default:false in
let sound =
match Parse_vmx.get_string vmx ["sound"; "virtualDev"] with
| Some "sb16" -> Some { s_sound_model = SB16 }
| Some "es1371" -> Some { s_sound_model = ES1370 (* hmmm ... *) }
| Some "hdaudio" -> Some { s_sound_model = ICH6 (* intel-hda *) }
| Some model ->
warning (f_"unknown sound device '%s' ignored") model;
None
| None -> None in
let disks = find_disks vmx vmx_source in
let removables = find_removables vmx in
let nics = find_nics vmx in
let source = {
s_hypervisor = VMware;
s_name = name;
s_genid = genid;
s_memory = memory;
s_vcpu = vcpu;
s_cpu_vendor = None;
s_cpu_model = None;
s_cpu_topology = cpu_topology;
s_features = [];
s_firmware = firmware;
s_uefi_secureboot = uefi_secureboot;
s_display = None;
s_sound = sound;
s_disks = List.map fst disks;
s_removables = removables;
s_nics = nics;
} in
source, List.map snd disks
|