mirror of
https://github.com/libguestfs/libguestfs.git
synced 2026-03-22 07:03:38 +00:00
475 lines
16 KiB
OCaml
475 lines
16 KiB
OCaml
(* virt-v2v
|
|
* Copyright (C) 2009-2018 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 Unix
|
|
|
|
open Std_utils
|
|
open Tools_utils
|
|
open Unix_utils
|
|
open JSON_parser
|
|
open Common_gettext.Gettext
|
|
|
|
open Types
|
|
open Utils
|
|
|
|
(* Timeout waiting for new Cinder volumes to move to "available" state.
|
|
* We assume this could be quite a long time on backends which want
|
|
* to preallocate the storage.
|
|
*)
|
|
let available_timeout = 300 (* seconds *)
|
|
|
|
(* Timeout waiting for Cinder volumes to attach to the appliance. *)
|
|
let attach_timeout = 60 (* seconds *)
|
|
|
|
(* The -oo options supported by this output method. *)
|
|
type os_options = {
|
|
(* The server name or UUID of the conversion appliance where
|
|
* virt-v2v is currently running. In future we may be able
|
|
* to make this optional and derive it from the OpenStack
|
|
* metadata service instead.
|
|
*)
|
|
server_id : string;
|
|
|
|
(* All other OpenStack parameters, passed through unmodified
|
|
* on the openstack command line.
|
|
*)
|
|
authentication : string list;
|
|
|
|
(* Optional guest_id which, if present, is saved as
|
|
* Cinder volume property virt_v2v_guest_id on every disk
|
|
* associated with this guest.
|
|
*)
|
|
guest_id : string option;
|
|
|
|
(* This setting is used by the test suite. *)
|
|
dev_disk_by_id : string option;
|
|
}
|
|
|
|
let print_output_options () =
|
|
printf (f_"virt-v2v -oo server-id=<NAME|UUID> [os-*=...]
|
|
|
|
Specify the name or UUID of the conversion appliance using
|
|
|
|
virt-v2v ... -o openstack -oo server-id=<NAME|UUID>
|
|
|
|
When virt-v2v runs it will attach the Cinder volumes to the
|
|
conversion appliance, so this name or UUID must be the name
|
|
of the virtual machine on OpenStack where virt-v2v is running.
|
|
|
|
In addition, all usual OpenStack “os-*” parameters or “OS_*”
|
|
environment variables can be used.
|
|
|
|
Openstack “--os-*” parameters must be written as “virt-v2v -oo os-*”.
|
|
|
|
For example:
|
|
|
|
virt-v2v -oo os-username=<NAME>
|
|
|
|
equivalent to openstack: --os-username=<NAME>
|
|
or the environment variable: OS_USERNAME=<NAME>
|
|
|
|
virt-v2v -oo os-project-name=<NAME>
|
|
|
|
equivalent to openstack: --os-project-name=<NAME>
|
|
or the environment variable: OS_PROJECT_NAME=<NAME>
|
|
|
|
The os-* parameters and environment variables are optional.
|
|
")
|
|
|
|
let parse_output_options options =
|
|
let server_id = ref None in
|
|
let dev_disk_by_id = ref None in
|
|
let guest_id = ref None in
|
|
let authentication = ref [] in
|
|
List.iter (
|
|
function
|
|
| "server-id", v ->
|
|
server_id := Some v
|
|
| "dev-disk-by-id", v ->
|
|
dev_disk_by_id := Some v
|
|
| "guest-id", v ->
|
|
guest_id := Some v
|
|
| k, v ->
|
|
(* Accumulate any remaining/unknown -oo parameters
|
|
* into the authentication list, where they will be
|
|
* pass unmodified through to the openstack command.
|
|
*)
|
|
let opt = sprintf "--%s=%s" k v in
|
|
authentication := opt :: !authentication
|
|
) options;
|
|
let server_id =
|
|
match !server_id with
|
|
| None ->
|
|
error (f_"openstack: -oo server-id=<NAME|UUID> not present");
|
|
| Some server_id -> server_id in
|
|
let authentication = List.rev !authentication in
|
|
let guest_id = !guest_id in
|
|
let dev_disk_by_id = !dev_disk_by_id in
|
|
{ server_id; authentication; guest_id; dev_disk_by_id }
|
|
|
|
(* UTC conversion time. *)
|
|
let iso_time =
|
|
let time = time () in
|
|
let tm = gmtime time in
|
|
sprintf "%04d/%02d/%02d %02d:%02d:%02d"
|
|
(tm.tm_year + 1900) (tm.tm_mon + 1) tm.tm_mday
|
|
tm.tm_hour tm.tm_min tm.tm_sec
|
|
|
|
class output_openstack output_conn output_password output_storage
|
|
(os_options : os_options) =
|
|
|
|
(* The extra command line parameters derived from -oo etc. *)
|
|
let extra_args =
|
|
let args = ref os_options.authentication in
|
|
Option.may (fun oc -> List.push_back args (sprintf "--os-auth-url=%s" oc))
|
|
output_conn;
|
|
!args in
|
|
|
|
(* We use this convenient wrapper around [Tools_utils.run_command]
|
|
* for two reasons: (1) Because we want to run openstack with
|
|
* extra_args. (2) OpenStack commands are noisy so we want to
|
|
* direct stdout to /dev/null unless we're in verbose mode.
|
|
*)
|
|
let run_openstack_command args =
|
|
let cmd = [ "openstack" ] @ extra_args @ args in
|
|
let stdout_fd =
|
|
if verbose () then None
|
|
else Some (openfile "/dev/null" [O_WRONLY] 0) in
|
|
(* Note that run_command will close stdout_fd if defined. *)
|
|
Tools_utils.run_command ?stdout_fd cmd
|
|
in
|
|
|
|
(* Similar to above, run the openstack command and capture the
|
|
* JSON document printed by the command. Note you must add
|
|
* '-f json' to the args yourself.
|
|
*)
|
|
let run_openstack_command_capture_json args =
|
|
let cmd = [ "openstack" ] @ extra_args @ args in
|
|
|
|
let json, chan = Filename.open_temp_file "v2vopenstack" ".json" in
|
|
unlink_on_exit json;
|
|
let fd = descr_of_out_channel chan in
|
|
|
|
(* Note that Tools_utils.run_command closes fd. *)
|
|
if Tools_utils.run_command ~stdout_fd:fd cmd <> 0 then
|
|
None
|
|
else (
|
|
let json = json_parser_tree_parse_file json in
|
|
debug "openstack: JSON parsed as: %s"
|
|
(JSON.string_of_doc ~fmt:JSON.Indented ["", json]);
|
|
Some json
|
|
)
|
|
in
|
|
|
|
(* Create a new Cinder volume and wait for its status to change to
|
|
* "available". Returns the volume id.
|
|
*)
|
|
let create_cinder_volume name description size =
|
|
(* Cinder volumes are allocated in increments of 1 GB. Weird. *)
|
|
let size_gb =
|
|
let s = roundup64 size 1073741824L in
|
|
let s = s /^ 1073741824L in
|
|
Int64.to_string s in
|
|
|
|
let args = ref [] in
|
|
List.push_back_list args [ "volume"; "create";
|
|
"-f"; "json";
|
|
"--size"; size_gb;
|
|
"--description"; description;
|
|
"--non-bootable";
|
|
"--read-write" ];
|
|
Option.may (
|
|
fun os ->
|
|
List.push_back_list args [ "--type"; os ]
|
|
) output_storage;
|
|
List.push_back args name;
|
|
|
|
let json =
|
|
match run_openstack_command_capture_json !args with
|
|
| None ->
|
|
error (f_"openstack: failed to create a cinder volume, see earlier error messages")
|
|
| Some json -> json in
|
|
let id = object_get_string "id" json in
|
|
|
|
(* Wait for the volume state to change to "available". *)
|
|
let args = [ "volume"; "show"; "-f"; "json"; id ] in
|
|
with_timeout
|
|
(s_"wait for cinder volume status to change to \"available\"")
|
|
available_timeout
|
|
(fun () ->
|
|
match run_openstack_command_capture_json args with
|
|
| None ->
|
|
error (f_"openstack: failed to query cinder volume status, see earlier error messages")
|
|
| Some json ->
|
|
match object_get_string "status" json with
|
|
| "creating" -> None
|
|
| "available" -> Some () (* done *)
|
|
| status ->
|
|
error (f_"openstack: unknown volume status \"%s\": expected \"creating\" or \"available\"") status
|
|
);
|
|
|
|
id
|
|
in
|
|
|
|
(* Delete a cinder volume.
|
|
*
|
|
* This ignores errors since the only time we are doing this is on
|
|
* the failure path.
|
|
*)
|
|
let delete_cinder_volume id =
|
|
let args = [ "volume"; "delete"; id ] in
|
|
ignore (run_openstack_command args)
|
|
in
|
|
|
|
(* Update metadata on a cinder volume. *)
|
|
let update_cinder_volume_metadata ?bootable ?description
|
|
?(image_properties = [])
|
|
?(volume_properties = [])
|
|
id =
|
|
let args = ref [ "volume"; "set" ] in
|
|
|
|
Option.may (
|
|
fun bootable ->
|
|
List.push_back args
|
|
(if bootable then "--bootable" else "--non-bootable")
|
|
) bootable;
|
|
|
|
Option.may (
|
|
fun description ->
|
|
List.push_back_list args ["--description"; description]
|
|
) description;
|
|
|
|
let image_properties =
|
|
List.flatten (
|
|
List.map (
|
|
fun (k, v) -> [ "--image-property"; sprintf "%s=%s" k v ]
|
|
) image_properties
|
|
) in
|
|
List.push_back_list args image_properties;
|
|
|
|
let volume_properties =
|
|
List.flatten (
|
|
List.map (
|
|
fun (k, v) -> [ "--property"; sprintf "%s=%s" k v ]
|
|
) volume_properties
|
|
) in
|
|
List.push_back_list args volume_properties;
|
|
|
|
List.push_back args id;
|
|
|
|
if run_openstack_command !args <> 0 then
|
|
error (f_"openstack: failed to set image properties on cinder volume, see earlier error messages")
|
|
in
|
|
|
|
(* Attach volume to current VM and wait for it to appear.
|
|
* Returns the block device name.
|
|
*)
|
|
let attach_volume id =
|
|
let args = [ "server"; "add"; "volume";
|
|
os_options.server_id; id ] in
|
|
if run_openstack_command args <> 0 then
|
|
error (f_"openstack: failed to attach cinder volume to VM, see earlier error messages");
|
|
|
|
(* We expect the disk to appear under /dev/disk/by-id.
|
|
*
|
|
* In theory the serial number of the disk should be the
|
|
* volume ID. However the practical reality is:
|
|
*
|
|
* (1) Only the first 20 characters are included by OpenStack.
|
|
* (2) udev(?) adds extra stuff
|
|
*
|
|
* So look for any file under /dev/disk/by-id which contains
|
|
* the prefix of the volume ID as a substring.
|
|
*)
|
|
let dev_disk_by_id =
|
|
Option.default "/dev/disk/by-id" os_options.dev_disk_by_id in
|
|
let prefix_len = 16 (* maybe 20, but be safe *) in
|
|
let prefix_id =
|
|
if String.length id > prefix_len then String.sub id 0 prefix_len
|
|
else id in
|
|
|
|
with_timeout
|
|
(sprintf (f_"waiting for cinder volume %s to attach to the conversion appliance") id)
|
|
attach_timeout
|
|
(fun () ->
|
|
let entries =
|
|
try Sys.readdir dev_disk_by_id
|
|
(* It's possible for /dev/disk/by-id to not exist, since it's
|
|
* only created by udev on demand, so ignore this error.
|
|
*)
|
|
with Sys_error _ -> [||] in
|
|
let entries = Array.to_list entries in
|
|
let entries =
|
|
List.filter (fun e -> String.find e prefix_id >= 0) entries in
|
|
match entries with
|
|
| d :: _ -> Some (dev_disk_by_id // d)
|
|
| [] -> None
|
|
);
|
|
in
|
|
|
|
(* Detach volume from current VM. This does not wait and doesn't
|
|
* check for errors, since either we're on the failure path and/or
|
|
* there's nothing we could do with the error anyway.
|
|
*)
|
|
let detach_volume id =
|
|
let args = [ "server"; "remove"; "volume";
|
|
os_options.server_id; id ] in
|
|
ignore (run_openstack_command args)
|
|
in
|
|
|
|
object
|
|
inherit output
|
|
|
|
method precheck () =
|
|
(* Run the openstack command simply to check we can connect
|
|
* with the provided authentication parameters/environment
|
|
* variables. Issuing a token should have only a tiny
|
|
* overhead.
|
|
*)
|
|
let args = [ "token"; "issue" ] in
|
|
if run_openstack_command args <> 0 then
|
|
error (f_"openstack: precheck failed, there may be a problem with authentication, see earlier error messages")
|
|
|
|
method as_options =
|
|
"-o openstack" ^
|
|
(match output_conn with
|
|
| None -> ""
|
|
| Some oc -> " -oc " ^ oc) ^
|
|
(match output_password with
|
|
| None -> ""
|
|
| Some op -> " -op " ^ op)
|
|
|
|
method supported_firmware = [ TargetBIOS ]
|
|
|
|
(* List of Cinder volume IDs. *)
|
|
val mutable volume_ids = []
|
|
(* If we didn't finish successfully, delete on exit. *)
|
|
val mutable delete_volumes_on_exit = true
|
|
|
|
(* Create the Cinder volumes, wait for them to attach to the
|
|
* appliance, and return the paths of the /dev devices.
|
|
*)
|
|
method prepare_targets source overlays
|
|
target_buses guestcaps inspect target_firmware =
|
|
(* Set up an at-exit handler so we:
|
|
* (1) Unconditionally detach volumes.
|
|
* (2) Delete the volumes, but only if conversion was not successful.
|
|
*)
|
|
at_exit (
|
|
fun () ->
|
|
List.iter detach_volume volume_ids;
|
|
if delete_volumes_on_exit then (
|
|
(* XXX We probably need to wait for the previous
|
|
* detach operation to complete - unclear how.
|
|
*)
|
|
List.iter delete_cinder_volume volume_ids;
|
|
volume_ids <- []
|
|
)
|
|
);
|
|
|
|
(* Set a known description for volumes, then change it later
|
|
* when conversion is successful. In theory this would allow
|
|
* some kind of garbage collection for unfinished conversions
|
|
* in the case that virt-v2v crashes.
|
|
*)
|
|
let description =
|
|
sprintf "virt-v2v temporary volume for %s" source.s_name in
|
|
|
|
(* Create the Cinder volumes. *)
|
|
List.iter (
|
|
fun (_, ov) ->
|
|
(* Unclear what we should set the name to, so just make
|
|
* something related to the guest name. Cinder volume
|
|
* names do not need to be unique.
|
|
*)
|
|
let name = sprintf "%s-%s" source.s_name ov.ov_sd in
|
|
|
|
(* Create the cinder volume and add the returned volume
|
|
* ID to the volume_ids list.
|
|
*)
|
|
let id = create_cinder_volume name description ov.ov_virtual_size in
|
|
volume_ids <- volume_ids @ [id]
|
|
) overlays;
|
|
|
|
(* Attach volume IDs to the conversion appliance and wait
|
|
* for the device nodes to appear.
|
|
*)
|
|
List.map (
|
|
fun id ->
|
|
let dev = attach_volume id in
|
|
TargetFile dev
|
|
) volume_ids
|
|
|
|
method create_metadata source targets
|
|
target_buses guestcaps inspect target_firmware =
|
|
let nr_disks = List.length targets in
|
|
assert (nr_disks = List.length volume_ids);
|
|
assert (nr_disks >= 1);
|
|
|
|
(* Image properties are only set on the first disk.
|
|
*
|
|
* In addition we set the first disk to bootable
|
|
* (XXX see RHBZ#1308535 for why this is wrong).
|
|
*)
|
|
let image_properties =
|
|
Openstack_image_properties.create source target_buses
|
|
guestcaps inspect target_firmware in
|
|
update_cinder_volume_metadata ~bootable:true ~image_properties
|
|
(List.hd volume_ids);
|
|
|
|
(* For all disks we update the description to a "non-temporary"
|
|
* description (see above) and set volume properties.
|
|
*)
|
|
List.iteri (
|
|
fun i id ->
|
|
let description =
|
|
sprintf "%s disk %d/%d converted by virt-v2v"
|
|
source.s_name (i+1) nr_disks in
|
|
|
|
let volume_properties = ref [
|
|
"virt_v2v_version", Guestfs_config.package_version_full;
|
|
"virt_v2v_conversion_date", iso_time;
|
|
"virt_v2v_guest_name", source.s_name;
|
|
"virt_v2v_disk_index", sprintf "%d/%d" (i+1) nr_disks;
|
|
] in
|
|
(match source.s_genid with
|
|
| None -> ()
|
|
| Some genid ->
|
|
List.push_back volume_properties
|
|
("virt_v2v_vm_generation_id", genid)
|
|
);
|
|
(match os_options.guest_id with
|
|
| None -> ()
|
|
| Some guest_id ->
|
|
List.push_back volume_properties
|
|
("virt_v2v_guest_id", guest_id)
|
|
);
|
|
let volume_properties = !volume_properties in
|
|
|
|
update_cinder_volume_metadata ~description ~volume_properties id
|
|
) volume_ids;
|
|
|
|
(* Successful so don't delete on exit. *)
|
|
delete_volumes_on_exit <- false
|
|
|
|
end
|
|
|
|
let output_openstack = new output_openstack
|
|
let () = Modules_list.register_output_module "openstack"
|