(* virt-ctrl: A graphical management tool.
(C) Copyright 2007 Richard W.M. Jones, Red Hat Inc.
http://libvirt.org/
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., 675 Mass Ave, Cambridge, MA 02139, USA.
*)
open Printf
open Unix
open ExtList
open Xml
open Virt_ctrl_gettext.Gettext
let (//) = Filename.concat
type virt_type =
| VT_HVM | VT_Xen | UnknownVirtType
type arch =
| I386 | X86_64 | IA64 | PPC | PPC64 | SPARC | SPARC64
| OtherArch of string
| UnknownArch
type hv_name = (* implicitly ordered worst->best *)
| UnknownHVType | QEMU | Xen | KVM
(* List of supported distros. The name field must be unique but
* doesn't need to be constant.
*)
let distros = [
"Fedora 8 (i386, 32 bit, Xen paravirtualized)", (
VT_Xen, I386
);
"Fedora 8 (i386, 32 bit, fully virtualized)", (
VT_HVM, I386
);
"Fedora 8 (x86-64, 64 bit, Xen paravirtualized)", (
VT_Xen, X86_64
);
"Fedora 8 (x86-64, 64 bit, fully virtualized)", (
VT_HVM, X86_64
);
"Fedora 9 (i386, 32 bit, Xen paravirtualized)", (
VT_Xen, I386
);
"Fedora 9 (i386, 32 bit, fully virtualized)", (
VT_HVM, I386
);
"Fedora 9 (x86-64, 64 bit, Xen paravirtualized)", (
VT_Xen, X86_64
);
"Fedora 9 (x86-64, 64 bit, fully virtualized)", (
VT_HVM, X86_64
);
]
let string_of_virt_type = function
| VT_HVM -> "hvm"
| VT_Xen -> "xen"
| UnknownVirtType -> invalid_arg "string_of_virt_type"
let string_of_architecture = function
| I386 -> "i386"
| X86_64 -> "x86_64"
| IA64 -> "ia64"
| PPC -> "ppc"
| PPC64 -> "ppc64"
| SPARC -> "sparc"
| SPARC64 -> "sparc64"
| OtherArch arch -> arch
| UnknownArch -> invalid_arg "string_of_architecture"
let architecture_of_string = function
| str when
String.length str = 4 &&
(str.[0] = 'i' || str.[0] = 'I') &&
(str.[1] >= '3' && str.[1] <= '6') &&
str.[2] = '8' && str.[3] = '6' -> I386
| "x86_64" | "X86_64" | "x86-64" | "X86-64" -> X86_64
| "ia64" | "IA64" -> IA64
| "ppc" | "PPC" | "ppc32" | "PPC32" -> PPC
| "ppc64" | "PPC64" -> PPC64
| "sparc" | "SPARC" | "sparc32" | "SPARC32" -> SPARC
| "sparc64" | "SPARC64" -> SPARC64
| str -> OtherArch str
let string_of_hv_name = function
| Xen -> "Xen"
| QEMU -> "QEMU"
| KVM -> "KVM"
| UnknownHVType -> "unknown"
(* Choose a disk location for images.
* This changed in libvirt 0.4.something.
*)
let disk_location =
let dirs = [
"/var/lib/libvirt/images";
"/var/lib/libvirt/xen";
] in
let dirs =
try dirs @ [Sys.getenv "HOME"]
with Not_found -> dirs in
try
List.find (
fun d ->
try (stat d).st_kind = S_DIR
with Unix_error _ -> false
) dirs
with
Not_found -> "/var/tmp"
type edit_status = UserEdit | ProgramEdit | Disconnected
let rec find_map f = function
| [] -> raise Not_found
| x :: xs ->
match f x with
| Some y -> y
| None -> find_map f xs
(* Install guest dialog. *)
let rec install_guest parent conn_id () =
let title = s_"Install new guest" in
let position = `CENTER_ON_PARENT in
let dlg = GWindow.dialog ~title ~position ~parent ~modal:true () in
(* We will enter the Gtk main loop recursively. Wire up close and
* other buttons to quit the recursive main loop.
*)
ignore (dlg#connect#destroy ~callback:GMain.quit);
ignore (dlg#event#connect#delete
~callback:(fun _ -> GMain.quit (); false));
(* Action area. *)
let cancel_button =
GButton.button ~label:(s_"Cancel") ~packing:dlg#action_area#pack () in
ignore (cancel_button#connect#clicked ~callback:dlg#destroy);
let install_button =
GButton.button ~label:(s_"Install") ~packing:dlg#action_area#pack () in
(* Connections. *)
let conns = get_connections () in
let frame =
GBin.frame ~label:(s_"Hypervisor connection")
~packing:dlg#vbox#pack () in
let hbox = GPack.hbox ~packing:frame#add () in
ignore (GMisc.label ~text:(s_"Connection:") ~xalign:1.0
~packing:hbox#pack ());
let conn_combo, (conn_model : GTree.list_store), conn_col_id, conn_active =
populate_conns conns hbox#pack conn_id in
(* Distro. *)
let frame =
GBin.frame ~label:(s_"Operating system") ~packing:dlg#vbox#pack () in
let (distro_combo, (distro_list, distro_col_name)) =
GEdit.combo_box_text ~packing:frame#add () in
(* VM name. *)
let frame =
GBin.frame ~label:(s_"Guest details") ~packing:dlg#vbox#pack () in
let tbl = GPack.table ~columns:3 ~rows:5 ~packing:frame#add () in
ignore (GMisc.label ~text:(s_"Name:") ~xalign:1.0
~packing:(tbl#attach ~top:0 ~left:0) ());
let name_entry =
GEdit.entry ~width_chars:32
~packing:(tbl#attach ~top:0 ~left:1) () in
(* Disk image. *)
ignore (GMisc.label ~text:(s_"Disk image:") ~xalign:1.0
~packing:(tbl#attach ~top:1 ~left:0) ());
let disk_entry =
GEdit.entry ~width_chars:48
~packing:(tbl#attach ~top:1 ~left:1) () in
let hbox = GPack.hbox ~packing:(tbl#attach ~top:1 ~left:2) () in
let disk_size =
GEdit.spin_button ~numeric:true ~digits:1
~packing:hbox#pack () in
disk_size#adjustment#set_bounds ~lower:2.0 ~upper:999.0 ~step_incr:0.5 ();
disk_size#adjustment#set_value 8.0;
ignore (GMisc.label ~text:" GB" ~packing:hbox#pack ());
(* Network. *)
ignore (GMisc.label ~text:(s_"Network:") ~xalign:1.0
~packing:(tbl#attach ~top:2 ~left:0) ());
let (net_combo, _) =
GEdit.combo_box_text
~strings:["default"] (* XXX not implemented *)
~packing:(tbl#attach ~top:2 ~left:1) () in
net_combo#set_active 0;
(* RAM. *)
ignore (GMisc.label ~text:(s_"RAM:") ~xalign:1.0
~packing:(tbl#attach ~top:3 ~left:0) ());
let hbox = GPack.hbox ~packing:(tbl#attach ~top:3 ~left:1) () in
let ram =
GEdit.spin_button ~numeric:true ~digits:0
~packing:hbox#pack () in
ram#adjustment#set_bounds ~lower:128.0 ~upper:131_072.0 ~step_incr:32.0 ();
ram#adjustment#set_value 512.0;
ignore (GMisc.label ~text:" MB" ~packing:hbox#pack ());
(* CPUs. *)
ignore (GMisc.label ~text:(s_"CPUs:") ~xalign:1.0
~packing:(tbl#attach ~top:4 ~left:0) ());
let hbox = GPack.hbox ~packing:(tbl#attach ~top:4 ~left:1) () in
let vcpus =
GEdit.spin_button ~numeric:true ~digits:0
~packing:hbox#pack () in
vcpus#adjustment#set_bounds ~lower:1.0 ~upper:4.0 ~step_incr:1.0 ();
vcpus#adjustment#set_value 1.0;
let () =
(* When connection combo changes we need to repopulate list of distros. *)
ignore (
conn_combo#connect#changed
~callback:(
fun () ->
repopulate_distros conns conn_combo conn_model conn_col_id
distro_combo distro_list distro_col_name
)
);
(* When distro changes we need to change the name, provided the name
* hasn't been edited by the user (in which case we leave it alone).
*
* We have to use a shared reference so that we don't disconnect
* the signal when we are editing the field (not the user). Ugh ...
*)
let edit_status = ref UserEdit in
let distro_changed_id =
distro_combo#connect#changed
~callback:(
fun () ->
let row = distro_combo#active_iter in
match row with
| None -> () (* no distro selected *)
| Some row ->
let name = distro_list#get ~row ~column:distro_col_name in
let name = short_name_of_distro name in
let old_status = !edit_status in
edit_status := ProgramEdit;
name_entry#set_text name;
disk_entry#set_text (disk_location // name ^ ".img");
edit_status := old_status
) in
ignore (
name_entry#connect#changed ~callback:(
fun () ->
if !edit_status = UserEdit then (
distro_combo#misc#disconnect distro_changed_id;
edit_status := Disconnected (* prevent multiple disconnections *)
)
)
);
ignore (
disk_entry#connect#changed ~callback:(
fun () ->
if !edit_status = UserEdit then (
distro_combo#misc#disconnect distro_changed_id;
edit_status := Disconnected (* prevent multiple disconnections *)
)
)
);
(* Wire up the install button. *)
ignore (
install_button#connect#clicked ~callback:(
fun () ->
(* Get the required settings. *)
let conn_id =
match conn_combo#active_iter with
| None -> (* no connection selected *)
invalid_arg (s_"No connection selected")
| Some row ->
conn_model#get ~row ~column:conn_col_id in
let distro =
match distro_combo#active_iter with
| None -> (* no distro selected *)
invalid_arg (s_"No operating system selected")
| Some row ->
distro_list#get ~row ~column:distro_col_name in
let name = name_entry#text in
let disk = disk_entry#text in
let disk_size = disk_size#value in
let net = "default" (* XXX network not implemented *) in
let ram = ram#value_as_int in
let vcpus = vcpus#value_as_int in
dlg#destroy ();
start_install parent conns
conn_id distro name disk disk_size net ram vcpus
)
) in
(* See combobox.ml example in %doc directory.
* NB. Don't use ~active label above. It is buggy and won't trigger
* the selection callback.
*)
conn_combo#set_active conn_active;
(* Set focus on the distro. *)
distro_combo#misc#grab_focus ();
dlg#show ();
(* Enter Gtk main loop recursively. *)
GMain.main ()
(* Populate the list of connections. *)
and populate_conns conns packing conn_id =
(* Model/columns. *)
let cols = new GTree.column_list in
let col_name = cols#add Gobject.Data.string in
let col_id = cols#add Gobject.Data.int in
let model = GTree.list_store cols in
(* View. *)
let conn_combo = GEdit.combo_box ~model ~packing () in
let renderer = GTree.cell_renderer_text [] in
conn_combo#pack renderer;
ignore (conn_combo#add_attribute renderer "text" col_name);
(* Populate the connection combo box. *)
let active = ref 0 in
List.iteri (
fun i (id, (conn, hostname, guests)) ->
if id = conn_id then active := i;
let name =
(* Get unique list of all HV types supported. *)
let hvs = List.map (fun (_, _, domains) -> domains) guests in
let hvs = List.concat hvs in
let hvs = List.map fst hvs in
let hvs = List.unique hvs in
let hvs = List.sort hvs in
(* Make a printable string for this connection. *)
sprintf "%s (%s)" hostname
(String.concat "/" (List.map string_of_hv_name hvs)) in
let row = model#append () in
model#set ~row ~column:col_name name;
model#set ~row ~column:col_id conn_id
) conns;
conn_combo, model, col_id, !active
(* When connection combo selection changes, repopulate list of distros. *)
and repopulate_distros conns conn_combo conn_model conn_col_id
distro_combo distro_list distro_col_name =
(* Clear the distro list. *)
distro_list#clear ();
let row = conn_combo#active_iter in
match row with
| None -> () (* no connection selected *)
| Some row ->
let conn_id = conn_model#get ~row ~column:conn_col_id in
let conn, hostname, guests = List.assoc conn_id conns in
(* Get the distros which match one of the guest types. *)
let distros = List.map get_matching_distros guests in
let distros = List.concat distros in
let distros = List.unique distros in
let distros = List.sort distros in
(* Populate the list with distros which match. *)
List.iter (
fun (name, _) ->
let row = distro_list#append () in
distro_list#set ~row ~column:distro_col_name name
) distros;
if List.length distros > 0 then distro_combo#set_active 0
and get_matching_distros (virt_type, arch, domains) =
List.filter (
fun (_, (virt_type', arch')) ->
virt_type = virt_type' && arch = arch'
) distros
(* Get a short name from the selected distro's name. *)
and short_name_of_distro name =
let len = String.length name in
let buf = Buffer.create 16 in
let rec loop i =
if i < len then (
let c = name.[i] in
match c with
| 'A' .. 'Z' ->
Buffer.add_char buf (Char.chr (Char.code c + 0x20));
loop (i+1)
| 'a' .. 'z' | '0' .. '9' ->
Buffer.add_char buf c;
loop (i+1)
| '(' -> () (* stop at first '(' character *)
| _ -> loop (i+1)
)
in
loop 0;
Buffer.contents buf
(* Get connection details, mainly from the capabilities XML. *)
and get_connections () =
let conns = Vc_connections.get_conns () in
List.map (
fun (conn_id, conn) ->
let hostname =
match Vc_connections.get_hostname conn_id with
| Some hostname -> hostname
| None -> sprintf "Conn #%d" conn_id in
(* Get some idea of what the hypervisor supports. *)
let guests =
let caps = Vc_connections.get_capabilities conn_id in
match caps with
| Some (Element ("capabilities", _, children)) ->
List.filter_map (
function
| Element ("guest", _, children) -> Some children
| _ -> None
) children
| _ -> [] in (* XXX should do better if no caps *)
let guests =
List.map (
fun guest ->
let os_type =
try
find_map (
function
| Element ("os_type", _, [PCData "hvm"]) -> Some VT_HVM
| Element ("os_type", _, [PCData "xen"]) -> Some VT_Xen
| _ -> None
) guest
with
Not_found -> UnknownVirtType in
let arch, domains =
try
find_map (
function
| Element ("arch", attrs, children) ->
let arch =
try architecture_of_string (List.assoc "name" attrs)
with Not_found -> UnknownArch in
let domains =
List.filter_map (
function
| Element ("domain", attrs, children) ->
let domtype =
try
match List.assoc "type" attrs with
| "xen" -> Xen
| "qemu" -> QEMU
| "kvm" -> KVM
| _ -> UnknownHVType
with Not_found -> UnknownHVType in
Some (domtype, children)
| _ -> None
) children in
Some (arch, domains)
| _ -> None
) guest
with
Not_found -> UnknownArch, [] in
(os_type, arch, domains)
) guests in
(conn_id, (conn, hostname, guests))
) conns
(* Perform (or at least start off) the install. *)
and start_install parent conns
conn_id distro name disk disk_size net ram vcpus =
(* Create new toplevel dialog to show the progress of the steps. *)
let title = s_"Guest installing" in
let position = `CENTER_ON_PARENT in
let dlg = GWindow.dialog
~title ~position ~parent ~modal:false ~width:480 () in
let frame =
GBin.frame ~label:(s_ "Download kernel") ~packing:dlg#vbox#pack () in
let kernel_bar = GRange.progress_bar ~packing:frame#add () in
let frame =
GBin.frame ~label:(s_ "Download initrd") ~packing:dlg#vbox#pack () in
let initrd_bar = GRange.progress_bar ~packing:frame#add () in
let frame =
GBin.frame ~label:(s_ "Create disk image") ~packing:dlg#vbox#pack () in
let disk_bar = GRange.progress_bar ~packing:frame#add () in
(* We will enter the Gtk main loop recursively. Wire up close and
* other buttons to quit the recursive main loop.
*)
ignore (dlg#connect#destroy ~callback:GMain.quit);
ignore (dlg#event#connect#delete
~callback:(fun _ -> GMain.quit (); false));
(* Action area. *)
let cancel_button =
GButton.button ~label:(s_"Cancel") ~packing:dlg#action_area#pack () in
ignore (cancel_button#connect#clicked ~callback:dlg#destroy);
(*
(* Create the disk image if necessary.
* XXX Should have checked earlier if it already exists.
*)
let disk_exists =
try (stat disk).st_kind = S_REG
with Unix_error _ -> false in
if not disk_exists then (
try
let fd = openfile disk [O_WRONLY;O_CREAT] in
let onemeg = 1024*1024 in
let buffer = String.make onemeg '\000' in
let nmegs = int_of_float (disk_size *. 1024.) in
for i = 0 to nmegs-1 do
write fd buffer 0 onemeg
done;
close fd
with
exn ->
(* Remove the disk image. *)
(try unlink disk with _ -> ());
(* Re-raise the original exception. *)
raise exn
);
*)
(* Get the distro by name. *)
let virt_type, arch = List.assoc distro distros in
(* Generate the XML configuration. *)
let xml =
(* Shortcut to make "value". *)
let leaf name value = Xml.Element (name, [], [Xml.PCData value]) in
(* ... and the _other_ sort of leaf (god I hate XML). *)
let tleaf name attribs = Xml.Element (name, attribs, []) in
(* Standard stuff for every domain. *)
let name = leaf "name" name in
let uuid = leaf "uuid" (random_uuid ()) in
let maxmem, memory =
let m = string_of_int (ram * 1024) in
leaf "maxmem" m, leaf "memory" m in
let vcpus = leaf "vcpu" (string_of_int vcpus) in
(* section, describes kernel, boot locations. *)
(* Put it all together in . *)
Xml.Element (
"domain",
[ "type", string_of_virt_type virt_type ],
name :: uuid :: memory :: maxmem :: vcpus :: []
) in
let xml = Xml.to_string_fmt xml in
prerr_endline xml;
dlg#show ();
(* Enter Gtk main loop recursively. *)
GMain.main ()
(* Generate a random MAC address in the Xen-reserved space. *)
and random_mac_address () =
let random =
List.map (sprintf "%02x") (
List.map (fun _ -> Random.int 256) [0;0;0]
) in
String.concat ":" ("00"::"16"::"3e"::random)
(* Generate a random UUID. *)
and random_uuid =
let hex = "0123456789abcdef" in
fun () ->
let str = String.create 32 in
for i = 0 to 31 do str.[i] <- hex.[Random.int 16] done;
str