Catch BlockDeviceError when starting instance
[ganeti-local] / lib / hypervisor / hv_xen.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Xen hypervisors
23
24 """
25
26 import os
27 import os.path
28 import time
29 import logging
30 from cStringIO import StringIO
31
32 from ganeti import constants
33 from ganeti import errors
34 from ganeti import utils
35 from ganeti.hypervisor import hv_base
36
37
38 class XenHypervisor(hv_base.BaseHypervisor):
39   """Xen generic hypervisor interface
40
41   This is the Xen base class used for both Xen PVM and HVM. It contains
42   all the functionality that is identical for both.
43
44   """
45
46   @classmethod
47   def _WriteConfigFile(cls, instance, block_devices, extra_args):
48     """Write the Xen config file for the instance.
49
50     """
51     raise NotImplementedError
52
53   @staticmethod
54   def _RemoveConfigFile(instance):
55     """Remove the xen configuration file.
56
57     """
58     utils.RemoveFile("/etc/xen/%s" % instance.name)
59
60   @staticmethod
61   def _GetXMList(include_node):
62     """Return the list of running instances.
63
64     If the include_node argument is True, then we return information
65     for dom0 also, otherwise we filter that from the return value.
66
67     @return: list of (name, id, memory, vcpus, state, time spent)
68
69     """
70     for dummy in range(5):
71       result = utils.RunCmd(["xm", "list"])
72       if not result.failed:
73         break
74       logging.error("xm list failed (%s): %s", result.fail_reason,
75                     result.output)
76       time.sleep(1)
77
78     if result.failed:
79       raise errors.HypervisorError("xm list failed, retries"
80                                    " exceeded (%s): %s" %
81                                    (result.fail_reason, result.stderr))
82
83     # skip over the heading
84     lines = result.stdout.splitlines()[1:]
85     result = []
86     for line in lines:
87       # The format of lines is:
88       # Name      ID Mem(MiB) VCPUs State  Time(s)
89       # Domain-0   0  3418     4 r-----    266.2
90       data = line.split()
91       if len(data) != 6:
92         raise errors.HypervisorError("Can't parse output of xm list,"
93                                      " line: %s" % line)
94       try:
95         data[1] = int(data[1])
96         data[2] = int(data[2])
97         data[3] = int(data[3])
98         data[5] = float(data[5])
99       except ValueError, err:
100         raise errors.HypervisorError("Can't parse output of xm list,"
101                                      " line: %s, error: %s" % (line, err))
102
103       # skip the Domain-0 (optional)
104       if include_node or data[0] != 'Domain-0':
105         result.append(data)
106
107     return result
108
109   def ListInstances(self):
110     """Get the list of running instances.
111
112     """
113     xm_list = self._GetXMList(False)
114     names = [info[0] for info in xm_list]
115     return names
116
117   def GetInstanceInfo(self, instance_name):
118     """Get instance properties.
119
120     @param instance_name: the instance name
121
122     @return: tuple (name, id, memory, vcpus, stat, times)
123
124     """
125     xm_list = self._GetXMList(instance_name=="Domain-0")
126     result = None
127     for data in xm_list:
128       if data[0] == instance_name:
129         result = data
130         break
131     return result
132
133   def GetAllInstancesInfo(self):
134     """Get properties of all instances.
135
136     @return: list of tuples (name, id, memory, vcpus, stat, times)
137
138     """
139     xm_list = self._GetXMList(False)
140     return xm_list
141
142   def StartInstance(self, instance, block_devices, extra_args):
143     """Start an instance.
144
145     """
146     self._WriteConfigFile(instance, block_devices, extra_args)
147     result = utils.RunCmd(["xm", "create", instance.name])
148
149     if result.failed:
150       raise errors.HypervisorError("Failed to start instance %s: %s (%s)" %
151                                    (instance.name, result.fail_reason,
152                                     result.output))
153
154   def StopInstance(self, instance, force=False):
155     """Stop an instance.
156
157     """
158     self._RemoveConfigFile(instance)
159     if force:
160       command = ["xm", "destroy", instance.name]
161     else:
162       command = ["xm", "shutdown", instance.name]
163     result = utils.RunCmd(command)
164
165     if result.failed:
166       raise errors.HypervisorError("Failed to stop instance %s: %s" %
167                                    (instance.name, result.fail_reason))
168
169   def RebootInstance(self, instance):
170     """Reboot an instance.
171
172     """
173     result = utils.RunCmd(["xm", "reboot", instance.name])
174
175     if result.failed:
176       raise errors.HypervisorError("Failed to reboot instance %s: %s" %
177                                    (instance.name, result.fail_reason))
178
179   def GetNodeInfo(self):
180     """Return information about the node.
181
182     @return: a dict with the following keys (values in MiB):
183           - memory_total: the total memory size on the node
184           - memory_free: the available memory on the node for instances
185           - memory_dom0: the memory used by the node itself, if available
186
187     """
188     # note: in xen 3, memory has changed to total_memory
189     result = utils.RunCmd(["xm", "info"])
190     if result.failed:
191       logging.error("Can't run 'xm info' (%s): %s", result.fail_reason,
192                     result.output)
193       return None
194
195     xmoutput = result.stdout.splitlines()
196     result = {}
197     for line in xmoutput:
198       splitfields = line.split(":", 1)
199
200       if len(splitfields) > 1:
201         key = splitfields[0].strip()
202         val = splitfields[1].strip()
203         if key == 'memory' or key == 'total_memory':
204           result['memory_total'] = int(val)
205         elif key == 'free_memory':
206           result['memory_free'] = int(val)
207         elif key == 'nr_cpus':
208           result['cpu_total'] = int(val)
209     dom0_info = self.GetInstanceInfo("Domain-0")
210     if dom0_info is not None:
211       result['memory_dom0'] = dom0_info[2]
212
213     return result
214
215   @staticmethod
216   def GetShellCommandForConsole(instance):
217     """Return a command for connecting to the console of an instance.
218
219     """
220     return "xm console %s" % instance.name
221
222
223   def Verify(self):
224     """Verify the hypervisor.
225
226     For Xen, this verifies that the xend process is running.
227
228     """
229     result = utils.RunCmd(["xm", "info"])
230     if result.failed:
231       return "'xm info' failed: %s" % result.fail_reason
232
233   @staticmethod
234   def _GetConfigFileDiskData(disk_template, block_devices):
235     """Get disk directive for xen config file.
236
237     This method builds the xen config disk directive according to the
238     given disk_template and block_devices.
239
240     @param disk_template: string containing instance disk template
241     @param block_devices: list of tuples (cfdev, rldev):
242         - cfdev: dict containing ganeti config disk part
243         - rldev: ganeti.bdev.BlockDev object
244
245     @return: string containing disk directive for xen instance config file
246
247     """
248     FILE_DRIVER_MAP = {
249       constants.FD_LOOP: "file",
250       constants.FD_BLKTAP: "tap:aio",
251       }
252     disk_data = []
253     if len(block_devices) > 24:
254       # 'z' - 'a' = 24
255       raise errors.HypervisorError("Too many disks")
256     # FIXME: instead of this hardcoding here, each of PVM/HVM should
257     # directly export their info (currently HVM will just sed this info)
258     namespace = ["sd" + chr(i + ord('a')) for i in range(24)]
259     for sd_name, (cfdev, dev_path) in zip(namespace, block_devices):
260       if cfdev.dev_type == constants.LD_FILE:
261         line = "'%s:%s,%s,w'" % (FILE_DRIVER_MAP[cfdev.physical_id[0]],
262                                  dev_path, sd_name)
263       else:
264         line = "'phy:%s,%s,w'" % (dev_path, sd_name)
265       disk_data.append(line)
266
267     return disk_data
268
269   def MigrateInstance(self, instance, target, live):
270     """Migrate an instance to a target node.
271
272     Arguments:
273       - instance: the name of the instance
274       - target: the ip of the target node
275       - live: whether to do live migration or not
276
277     Returns: none, errors will be signaled by exception.
278
279     The migration will not be attempted if the instance is not
280     currently running.
281
282     """
283     if self.GetInstanceInfo(instance) is None:
284       raise errors.HypervisorError("Instance not running, cannot migrate")
285     args = ["xm", "migrate"]
286     if live:
287       args.append("-l")
288     args.extend([instance, target])
289     result = utils.RunCmd(args)
290     if result.failed:
291       raise errors.HypervisorError("Failed to migrate instance %s: %s" %
292                                    (instance, result.output))
293
294
295 class XenPvmHypervisor(XenHypervisor):
296   """Xen PVM hypervisor interface"""
297
298   PARAMETERS = [
299     constants.HV_KERNEL_PATH,
300     constants.HV_INITRD_PATH,
301     ]
302
303   @classmethod
304   def CheckParameterSyntax(cls, hvparams):
305     """Check the given parameters for validity.
306
307     For the PVM hypervisor, this only check the existence of the
308     kernel.
309
310     @type hvparams:  dict
311     @param hvparams: dictionary with parameter names/value
312     @raise errors.HypervisorError: when a parameter is not valid
313
314     """
315     super(XenPvmHypervisor, cls).CheckParameterSyntax(hvparams)
316
317     if not hvparams[constants.HV_KERNEL_PATH]:
318       raise errors.HypervisorError("Need a kernel for the instance")
319
320     if not os.path.isabs(hvparams[constants.HV_KERNEL_PATH]):
321       raise errors.HypervisorError("The kernel path must an absolute path")
322
323     if hvparams[constants.HV_INITRD_PATH]:
324       if not os.path.isabs(hvparams[constants.HV_INITRD_PATH]):
325         raise errors.HypervisorError("The initrd path must an absolute path"
326                                      ", if defined")
327
328   def ValidateParameters(self, hvparams):
329     """Check the given parameters for validity.
330
331     For the PVM hypervisor, this only check the existence of the
332     kernel.
333
334     """
335     super(XenPvmHypervisor, self).ValidateParameters(hvparams)
336
337     kernel_path = hvparams[constants.HV_KERNEL_PATH]
338     if not os.path.isfile(kernel_path):
339       raise errors.HypervisorError("Instance kernel '%s' not found or"
340                                    " not a file" % kernel_path)
341     initrd_path = hvparams[constants.HV_INITRD_PATH]
342     if initrd_path and not os.path.isfile(initrd_path):
343       raise errors.HypervisorError("Instance initrd '%s' not found or"
344                                    " not a file" % initrd_path)
345
346   @classmethod
347   def _WriteConfigFile(cls, instance, block_devices, extra_args):
348     """Write the Xen config file for the instance.
349
350     """
351     config = StringIO()
352     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
353
354     # kernel handling
355     kpath = instance.hvparams[constants.HV_KERNEL_PATH]
356     config.write("kernel = '%s'\n" % kpath)
357
358     # initrd handling
359     initrd_path = instance.hvparams[constants.HV_INITRD_PATH]
360     if initrd_path:
361       config.write("ramdisk = '%s'\n" % initrd_path)
362
363     # rest of the settings
364     config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
365     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
366     config.write("name = '%s'\n" % instance.name)
367
368     vif_data = []
369     for nic in instance.nics:
370       nic_str = "mac=%s, bridge=%s" % (nic.mac, nic.bridge)
371       ip = getattr(nic, "ip", None)
372       if ip is not None:
373         nic_str += ", ip=%s" % ip
374       vif_data.append("'%s'" % nic_str)
375
376     config.write("vif = [%s]\n" % ",".join(vif_data))
377     config.write("disk = [%s]\n" % ",".join(
378                  cls._GetConfigFileDiskData(instance.disk_template,
379                                             block_devices)))
380     config.write("root = '/dev/sda ro'\n")
381     config.write("on_poweroff = 'destroy'\n")
382     config.write("on_reboot = 'restart'\n")
383     config.write("on_crash = 'restart'\n")
384     if extra_args:
385       config.write("extra = '%s'\n" % extra_args)
386     # just in case it exists
387     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
388     try:
389       f = open("/etc/xen/%s" % instance.name, "w")
390       try:
391         f.write(config.getvalue())
392       finally:
393         f.close()
394     except IOError, err:
395       raise errors.OpExecError("Cannot write Xen instance confile"
396                                " file /etc/xen/%s: %s" % (instance.name, err))
397     return True
398
399
400 class XenHvmHypervisor(XenHypervisor):
401   """Xen HVM hypervisor interface"""
402
403   PARAMETERS = [
404     constants.HV_ACPI,
405     constants.HV_BOOT_ORDER,
406     constants.HV_CDROM_IMAGE_PATH,
407     constants.HV_DISK_TYPE,
408     constants.HV_NIC_TYPE,
409     constants.HV_PAE,
410     constants.HV_VNC_BIND_ADDRESS,
411     ]
412
413   @classmethod
414   def CheckParameterSyntax(cls, hvparams):
415     """Check the given parameter syntax.
416
417     """
418     super(XenHvmHypervisor, cls).CheckParameterSyntax(hvparams)
419     # boot order verification
420     boot_order = hvparams[constants.HV_BOOT_ORDER]
421     if len(boot_order.strip("acdn")) != 0:
422       raise errors.HypervisorError("Invalid boot order '%s' specified,"
423                                    " must be one or more of [acdn]" %
424                                    boot_order)
425     # device type checks
426     nic_type = hvparams[constants.HV_NIC_TYPE]
427     if nic_type not in constants.HT_HVM_VALID_NIC_TYPES:
428       raise errors.HypervisorError("Invalid NIC type %s specified for Xen HVM"
429                                    " hypervisor" % nic_type)
430     disk_type = hvparams[constants.HV_DISK_TYPE]
431     if disk_type not in constants.HT_HVM_VALID_DISK_TYPES:
432       raise errors.HypervisorError("Invalid disk type %s specified for Xen HVM"
433                                    " hypervisor" % disk_type)
434     # vnc_bind_address verification
435     vnc_bind_address = hvparams[constants.HV_VNC_BIND_ADDRESS]
436     if vnc_bind_address is not None:
437       if not utils.IsValidIP(vnc_bind_address):
438         raise errors.OpPrereqError("given VNC bind address '%s' doesn't look"
439                                    " like a valid IP address" %
440                                    vnc_bind_address)
441
442     iso_path = hvparams[constants.HV_CDROM_IMAGE_PATH]
443     if iso_path and not os.path.isabs(iso_path):
444       raise errors.HypervisorError("The path to the HVM CDROM image must"
445                                    " be an absolute path or None, not %s" %
446                                    iso_path)
447
448   def ValidateParameters(self, hvparams):
449     """Check the given parameters for validity.
450
451     For the PVM hypervisor, this only check the existence of the
452     kernel.
453
454     @type hvparams:  dict
455     @param hvparams: dictionary with parameter names/value
456     @raise errors.HypervisorError: when a parameter is not valid
457
458     """
459     super(XenHvmHypervisor, self).ValidateParameters(hvparams)
460
461     # hvm_cdrom_image_path verification
462     iso_path = hvparams[constants.HV_CDROM_IMAGE_PATH]
463     if iso_path and not os.path.isfile(iso_path):
464       raise errors.HypervisorError("The HVM CDROM image must either be a"
465                                    " regular file or a symlink pointing to"
466                                    " an existing regular file, not %s" %
467                                    iso_path)
468
469   @classmethod
470   def _WriteConfigFile(cls, instance, block_devices, extra_args):
471     """Create a Xen 3.1 HVM config file.
472
473     """
474     config = StringIO()
475     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
476     config.write("kernel = '/usr/lib/xen/boot/hvmloader'\n")
477     config.write("builder = 'hvm'\n")
478     config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
479     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
480     config.write("name = '%s'\n" % instance.name)
481     if instance.hvparams[constants.HV_PAE]:
482       config.write("pae = 1\n")
483     else:
484       config.write("pae = 0\n")
485     if instance.hvparams[constants.HV_ACPI]:
486       config.write("acpi = 1\n")
487     else:
488       config.write("acpi = 0\n")
489     config.write("apic = 1\n")
490     arch = os.uname()[4]
491     if '64' in arch:
492       config.write("device_model = '/usr/lib64/xen/bin/qemu-dm'\n")
493     else:
494       config.write("device_model = '/usr/lib/xen/bin/qemu-dm'\n")
495     if instance.hvparams[constants.HV_BOOT_ORDER] is None:
496       config.write("boot = '%s'\n" % constants.HT_HVM_DEFAULT_BOOT_ORDER)
497     else:
498       config.write("boot = '%s'\n" % instance.hvparams["boot_order"])
499     config.write("sdl = 0\n")
500     config.write("usb = 1\n")
501     config.write("usbdevice = 'tablet'\n")
502     config.write("vnc = 1\n")
503     if instance.hvparams[constants.HV_VNC_BIND_ADDRESS] is None:
504       config.write("vnclisten = '%s'\n" % constants.VNC_DEFAULT_BIND_ADDRESS)
505     else:
506       config.write("vnclisten = '%s'\n" %
507                    instance.hvparams["vnc_bind_address"])
508
509     if instance.network_port > constants.HT_HVM_VNC_BASE_PORT:
510       display = instance.network_port - constants.HT_HVM_VNC_BASE_PORT
511       config.write("vncdisplay = %s\n" % display)
512       config.write("vncunused = 0\n")
513     else:
514       config.write("# vncdisplay = 1\n")
515       config.write("vncunused = 1\n")
516
517     try:
518       password_file = open(constants.VNC_PASSWORD_FILE, "r")
519       try:
520         password = password_file.readline()
521       finally:
522         password_file.close()
523     except IOError:
524       raise errors.OpExecError("failed to open VNC password file %s " %
525                                constants.VNC_PASSWORD_FILE)
526
527     config.write("vncpasswd = '%s'\n" % password.rstrip())
528
529     config.write("serial = 'pty'\n")
530     config.write("localtime = 1\n")
531
532     vif_data = []
533     nic_type = instance.hvparams[constants.HV_NIC_TYPE]
534     if nic_type is None:
535       # ensure old instances don't change
536       nic_type_str = ", type=ioemu"
537     elif nic_type == constants.HT_HVM_DEV_PARAVIRTUAL:
538       nic_type_str = ", type=paravirtualized"
539     else:
540       nic_type_str = ", model=%s, type=ioemu" % nic_type
541     for nic in instance.nics:
542       nic_str = "mac=%s, bridge=%s%s" % (nic.mac, nic.bridge, nic_type_str)
543       ip = getattr(nic, "ip", None)
544       if ip is not None:
545         nic_str += ", ip=%s" % ip
546       vif_data.append("'%s'" % nic_str)
547
548     config.write("vif = [%s]\n" % ",".join(vif_data))
549     disk_data = cls._GetConfigFileDiskData(instance.disk_template,
550                                             block_devices)
551     disk_type = instance.hvparams[constants.HV_DISK_TYPE]
552     if disk_type in (None, constants.HT_HVM_DEV_IOEMU):
553       replacement = ",ioemu:hd"
554     else:
555       replacement = ",hd"
556     disk_data = [line.replace(",sd", replacement) for line in disk_data]
557     iso_path = instance.hvparams[constants.HV_CDROM_IMAGE_PATH]
558     if iso_path:
559       iso = "'file:%s,hdc:cdrom,r'" % iso_path
560       disk_data.append(iso)
561
562     config.write("disk = [%s]\n" % (",".join(disk_data)))
563
564     config.write("on_poweroff = 'destroy'\n")
565     config.write("on_reboot = 'restart'\n")
566     config.write("on_crash = 'restart'\n")
567     if extra_args:
568       config.write("extra = '%s'\n" % extra_args)
569     # just in case it exists
570     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
571     try:
572       f = open("/etc/xen/%s" % instance.name, "w")
573       try:
574         f.write(config.getvalue())
575       finally:
576         f.close()
577     except IOError, err:
578       raise errors.OpExecError("Cannot write Xen instance confile"
579                                " file /etc/xen/%s: %s" % (instance.name, err))
580     return True