a253981803ec12e3ece82523c9788eb22dd404cd
[mclu.git] / mclu_boot.ml
1 (* mclu: Mini Cloud
2  * Copyright (C) 2014-2015 Red Hat Inc.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17  *)
18
19 (* Implement 'mclu boot'. *)
20
21 module C = Libvirt.Connect
22 module D = Libvirt.Domain
23 module MS = Mclu_status
24
25 open Printf
26
27 open Utils
28
29 let memory = ref 0L                     (* 0 = choose for me *)
30 let set_memory s =
31   try memory := bytes_of_human_size s
32   with Not_found ->
33     eprintf "mclu: don't understand --memory parameter '%s'
34 Try something like --memory 1G\n" s;
35     exit 1
36 let size = ref 0L                       (* 0 = default *)
37 let set_size s =
38   try size := bytes_of_human_size s
39   with Not_found ->
40     eprintf "mclu: don't understand --size parameter '%s'
41 Try something like --size 20G\n" s;
42     exit 1
43 let timezone = ref ""                   (* "" = no timezone set *)
44 let vcpus = ref 0                       (* 0 = choose for me *)
45
46 let open_console = ref false
47 let open_viewer = ref false
48
49 let get_arg_speclist () = Arg.align [
50   "--console",  Arg.Set open_console, " Open the serial console";
51   "--cpus",     Arg.Set_int vcpus, "n Number of virtual CPUs";
52   "--memory",   Arg.String set_memory, "nnG Amount of RAM to give guest";
53   "--ram",      Arg.String set_memory, "nnG Amount of RAM to give guest";
54   "--size",     Arg.String set_size, "nnG Size of disk to give guest";
55   "--timezone", Arg.Set_string timezone, "TZ Set timezone of guest";
56   "--vcpus",    Arg.Set_int vcpus, "n Number of virtual CPUs";
57   "--viewer",   Arg.Set open_viewer, " Open the graphical console";
58 ]
59
60 let boot ~verbose template name =
61   let templates = Template.templates () in
62
63   (* Does the template exist? *)
64   let template_filename =
65     try List.assoc template templates
66     with Not_found ->
67       eprintf "mclu: template %s not found
68 Try `mclu list --templates' to list all known templates.\n" template;
69       exit 1 in
70
71   (* Probe the template for various features. *)
72   let template_info = Template.probe ~verbose template_filename in
73
74   (* Decide how much RAM we will give the guest.  This affects our
75    * choice of node, so do it early.
76    *)
77   let memory = !memory in
78   let memory =
79     if memory > 0L then (
80       (* User requested, just check it's above the minimum. *)
81       match template_info.Template.minimum_memory with
82       | None -> memory
83       | Some min when min > memory ->
84         eprintf "mclu: minimum memory for this template is %s\n"
85           (human_size min);
86         exit 1
87       | Some _ -> memory
88     ) else (
89       (* User didn't request any memory setting, use the recommended. *)
90       match template_info.Template.recommended_memory with
91       | Some memory -> memory
92       | None -> 4L *^ 1024L *^ 1024L *^ 1024L (* 4 GB *)
93     ) in
94
95   (* Check what's running. *)
96   let summary = MS.node_guest_summary ~verbose () in
97
98   (* Did the user request a specific host?  If not, choose one. *)
99   let hostname, name =
100     match name_parse name with
101     | Some hostname, name -> hostname, name
102     | None, name ->
103       (* Choose the first host with enough free memory. *)
104       let nodes = List.filter (
105         fun { MS.free_memory = free_memory } -> free_memory >= memory
106       ) summary in
107       match nodes with
108       | [] ->
109         eprintf "mclu: no node with enough free memory found
110 Try: `mclu status' and `mclu on <node>'\n";
111         exit 1
112       | node :: _ ->
113         let hostname =
114           node.MS.node_status.MS.node.Mclu_conf.hostname in
115         hostname, name in
116
117   (* Check there isn't a guest with this name running anywhere
118    * in the cluster already.
119    *)
120   List.iter (
121     fun ({ MS.active_guests = guests } as node) ->
122       List.iter (
123         fun { Mclu_list.dom_name = n } ->
124           if name = n then (
125             let hostname =
126               node.MS.node_status.MS.node.Mclu_conf.hostname
127             in
128             eprintf "mclu: there is already a guest called '%s' (running on %s)\n"
129               name hostname;
130             exit 1
131           )
132       ) guests
133   ) summary;
134
135   (* Convert hostname to a specific node, and check it is up. *)
136   let node =
137     try List.find (
138       fun node ->
139         node.MS.node_status.MS.node.Mclu_conf.hostname = hostname
140     ) summary
141     with Not_found ->
142       eprintf "mclu: no node is called '%s'\n" hostname;
143       exit 1 in
144   if not node.MS.node_status.MS.node_on then (
145     eprintf "mclu: node '%s' is switched off
146 Try: `mclu on %s'\n" hostname hostname;
147     exit 1
148   );
149
150   (* Where we upload the template and image on remote. *)
151   let format, extension = "qcow2", "qcow2" in
152   let remote_filename = sprintf "/tmp/mclu%s.sh" (string_random8 ()) in
153   let remote_image = sprintf "/var/tmp/%s.%s" name extension in
154
155   (* Get ready to generate the guest XML. *)
156   let vcpus = !vcpus in
157   let vcpus =
158     if vcpus > 0 then vcpus
159     else min 4 node.MS.node_status.MS.node_info.C.cpus in
160   let mac_addr =
161     sprintf "52:54:00:%02x:%02x:%02x"
162       (Random.int 256) (Random.int 256) (Random.int 256) in
163
164   (* Generate the guest XML.  XXX Quoting. *)
165   let xml = sprintf "\
166 <domain type='kvm'>
167   <name>%s</name>
168   <memory unit='KiB'>%Ld</memory>
169   <currentMemory unit='KiB'>%Ld</currentMemory>
170   <vcpu>%d</vcpu>
171   <os>
172     <type>hvm</type>
173     <boot dev='hd'/>
174   </os>
175   <features>
176     <acpi/>
177     <apic/>
178     <pae/>
179   </features>
180   <cpu mode='host-model' fallback='allow' />
181   <clock offset='utc'>
182     <timer name='rtc' tickpolicy='catchup'/>
183     <timer name='pit' tickpolicy='delay'/>
184     <timer name='hpet' present='no'/>
185   </clock>
186   <on_poweroff>destroy</on_poweroff>
187   <on_reboot>restart</on_reboot>
188   <on_crash>restart</on_crash>
189   <devices>
190 " name (memory /^ 1024L) (memory /^ 1024L) vcpus in
191
192   let xml = xml ^ sprintf "\
193   <disk type='file' device='disk'>
194     <driver name='qemu' type='%s' cache='none' io='native'/>
195     <source file='%s'/>
196 " format remote_image in
197   let xml = xml ^
198     match template_info.Template.disk_bus with
199     | Some "ide" ->
200       "      <target dev='sda' bus='ide'/>\n"
201     | Some "virtio-scsi" | None ->
202       "      <target dev='sda' bus='scsi'/>\n"
203     | Some bus ->
204       eprintf "mclu: unknown disk-bus: %s\n" bus;
205       exit 1 in
206   let xml = xml ^ "\
207     </disk>
208 " in
209
210   let xml = xml ^
211     if template_info.Template.disk_bus = Some "virtio-scsi" then
212       "  <controller type='scsi' index='0' model='virtio-scsi'/>\n"
213     else
214       "" in
215
216   (* XXX Don't hard-code bridge name here. *)
217   let network_model =
218     match template_info with
219     | { Template.network_model = None } -> "virtio"
220     | { Template.network_model = Some d } -> d in
221   let xml = xml ^ sprintf "\
222     <interface type='bridge'>
223       <mac address='%s'/>
224       <source bridge='br0'/>
225       <model type='%s'/>
226     </interface>
227 " mac_addr network_model in
228
229   let xml = xml ^ "\
230     <serial type='pty'>
231       <target port='0'/>
232     </serial>
233     <console type='pty'>
234       <target type='serial' port='0'/>
235     </console>
236     <input type='tablet' bus='usb'/>
237     <input type='mouse' bus='ps2'/>
238     <input type='keyboard' bus='ps2'/>
239     <graphics type='vnc' autoport='yes'/>
240     <video>
241       <model type='cirrus' vram='9216' heads='1'/>
242     </video>
243   </devices>
244 </domain>" in
245
246   (* Copy the template to remote and build the guest. *)
247   let cmd =
248     sprintf "scp %s root@%s:%s"
249       (quote template_filename) (quote hostname) remote_filename in
250   if verbose then printf "%s\n%!" cmd;
251   if Sys.command cmd <> 0 then (
252     eprintf "mclu: scp template to remote failed\n";
253     exit 1
254   );
255   let cmd =
256     (* XXX Don't hard-code network_bridge here. *)
257     sprintf "ssh root@%s \
258 LIBGUESTFS_BACKEND_SETTINGS=network_bridge=br0 \
259 base_image=%s \
260 format=%s \
261 name=%s \
262 output=%s \
263 %s \
264 %s \
265 %s build"
266       (quote hostname)
267       (quote template_info.Template.base_image) (* base_image *)
268       format (* format *)
269       name (* name *)
270       (quote remote_image) (* output *)
271       (match !size with
272       | 0L -> ""
273       | size -> sprintf "size=%Ldb" size) (* size *)
274       (match !timezone with
275       | "" -> ""
276       | tz -> quote "timezone=" ^ tz)   (* timezone *)
277       remote_filename in
278   if verbose then printf "%s\n%!" cmd;
279   if Sys.command cmd <> 0 then (
280     eprintf "mclu: remote build failed\n";
281     exit 1
282   );
283
284   (* Start the guest. *)
285   let dom =
286     try
287       let conn =
288         let name = node.MS.node_status.MS.node.Mclu_conf.libvirt_uri in
289         C.connect ~name () in
290       let dom = D.create_xml conn xml [] in
291       printf "mclu: %s:%s started\n" hostname (D.get_name dom);
292       dom
293     with Libvirt.Virterror msg ->
294       eprintf "mclu: %s: %s\n" hostname (Libvirt.Virterror.to_string msg);
295       exit 1 in
296
297   (* Graphical console? *)
298   if !open_viewer then
299     Mclu_viewer.viewer ~verbose ~host:hostname (D.get_name dom);
300
301   (* Serial console?  (Interactive, so run it last) *)
302   if !open_console then
303     Mclu_console.console ~verbose ~host:hostname (D.get_name dom)
304
305 let run ~verbose = function
306   | [ template; name ] ->
307     boot ~verbose template name
308   | _ ->
309     eprintf "Usage: mclu boot <template> <[host:]name>\n";
310     exit 1