(* 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