0aa956032acc0950f88a46d04d0130ce41a61d97
[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_name):
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.name)
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     # remove old xen file after migration succeeded
294     try:
295       self._RemoveConfigFile(instance)
296     except EnvironmentError, err:
297       logger.Error("Failure while removing instance config file: %s" %
298                    str(err))
299
300
301 class XenPvmHypervisor(XenHypervisor):
302   """Xen PVM hypervisor interface"""
303
304   PARAMETERS = [
305     constants.HV_KERNEL_PATH,
306     constants.HV_INITRD_PATH,
307     ]
308
309   @classmethod
310   def CheckParameterSyntax(cls, hvparams):
311     """Check the given parameters for validity.
312
313     For the PVM hypervisor, this only check the existence of the
314     kernel.
315
316     @type hvparams:  dict
317     @param hvparams: dictionary with parameter names/value
318     @raise errors.HypervisorError: when a parameter is not valid
319
320     """
321     super(XenPvmHypervisor, cls).CheckParameterSyntax(hvparams)
322
323     if not hvparams[constants.HV_KERNEL_PATH]:
324       raise errors.HypervisorError("Need a kernel for the instance")
325
326     if not os.path.isabs(hvparams[constants.HV_KERNEL_PATH]):
327       raise errors.HypervisorError("The kernel path must an absolute path")
328
329     if hvparams[constants.HV_INITRD_PATH]:
330       if not os.path.isabs(hvparams[constants.HV_INITRD_PATH]):
331         raise errors.HypervisorError("The initrd path must an absolute path"
332                                      ", if defined")
333
334   def ValidateParameters(self, hvparams):
335     """Check the given parameters for validity.
336
337     For the PVM hypervisor, this only check the existence of the
338     kernel.
339
340     """
341     super(XenPvmHypervisor, self).ValidateParameters(hvparams)
342
343     kernel_path = hvparams[constants.HV_KERNEL_PATH]
344     if not os.path.isfile(kernel_path):
345       raise errors.HypervisorError("Instance kernel '%s' not found or"
346                                    " not a file" % kernel_path)
347     initrd_path = hvparams[constants.HV_INITRD_PATH]
348     if initrd_path and not os.path.isfile(initrd_path):
349       raise errors.HypervisorError("Instance initrd '%s' not found or"
350                                    " not a file" % initrd_path)
351
352   @classmethod
353   def _WriteConfigFile(cls, instance, block_devices, extra_args):
354     """Write the Xen config file for the instance.
355
356     """
357     config = StringIO()
358     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
359
360     # kernel handling
361     kpath = instance.hvparams[constants.HV_KERNEL_PATH]
362     config.write("kernel = '%s'\n" % kpath)
363
364     # initrd handling
365     initrd_path = instance.hvparams[constants.HV_INITRD_PATH]
366     if initrd_path:
367       config.write("ramdisk = '%s'\n" % initrd_path)
368
369     # rest of the settings
370     config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
371     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
372     config.write("name = '%s'\n" % instance.name)
373
374     vif_data = []
375     for nic in instance.nics:
376       nic_str = "mac=%s, bridge=%s" % (nic.mac, nic.bridge)
377       ip = getattr(nic, "ip", None)
378       if ip is not None:
379         nic_str += ", ip=%s" % ip
380       vif_data.append("'%s'" % nic_str)
381
382     config.write("vif = [%s]\n" % ",".join(vif_data))
383     config.write("disk = [%s]\n" % ",".join(
384                  cls._GetConfigFileDiskData(instance.disk_template,
385                                             block_devices)))
386     config.write("root = '/dev/sda ro'\n")
387     config.write("on_poweroff = 'destroy'\n")
388     config.write("on_reboot = 'restart'\n")
389     config.write("on_crash = 'restart'\n")
390     if extra_args:
391       config.write("extra = '%s'\n" % extra_args)
392     # just in case it exists
393     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
394     try:
395       f = open("/etc/xen/%s" % instance.name, "w")
396       try:
397         f.write(config.getvalue())
398       finally:
399         f.close()
400     except IOError, err:
401       raise errors.OpExecError("Cannot write Xen instance confile"
402                                " file /etc/xen/%s: %s" % (instance.name, err))
403     return True
404
405
406 class XenHvmHypervisor(XenHypervisor):
407   """Xen HVM hypervisor interface"""
408
409   PARAMETERS = [
410     constants.HV_ACPI,
411     constants.HV_BOOT_ORDER,
412     constants.HV_CDROM_IMAGE_PATH,
413     constants.HV_DISK_TYPE,
414     constants.HV_NIC_TYPE,
415     constants.HV_PAE,
416     constants.HV_VNC_BIND_ADDRESS,
417     ]
418
419   @classmethod
420   def CheckParameterSyntax(cls, hvparams):
421     """Check the given parameter syntax.
422
423     """
424     super(XenHvmHypervisor, cls).CheckParameterSyntax(hvparams)
425     # boot order verification
426     boot_order = hvparams[constants.HV_BOOT_ORDER]
427     if len(boot_order.strip("acdn")) != 0:
428       raise errors.HypervisorError("Invalid boot order '%s' specified,"
429                                    " must be one or more of [acdn]" %
430                                    boot_order)
431     # device type checks
432     nic_type = hvparams[constants.HV_NIC_TYPE]
433     if nic_type not in constants.HT_HVM_VALID_NIC_TYPES:
434       raise errors.HypervisorError("Invalid NIC type %s specified for Xen HVM"
435                                    " hypervisor" % nic_type)
436     disk_type = hvparams[constants.HV_DISK_TYPE]
437     if disk_type not in constants.HT_HVM_VALID_DISK_TYPES:
438       raise errors.HypervisorError("Invalid disk type %s specified for Xen HVM"
439                                    " hypervisor" % disk_type)
440     # vnc_bind_address verification
441     vnc_bind_address = hvparams[constants.HV_VNC_BIND_ADDRESS]
442     if vnc_bind_address is not None:
443       if not utils.IsValidIP(vnc_bind_address):
444         raise errors.OpPrereqError("given VNC bind address '%s' doesn't look"
445                                    " like a valid IP address" %
446                                    vnc_bind_address)
447
448     iso_path = hvparams[constants.HV_CDROM_IMAGE_PATH]
449     if iso_path and not os.path.isabs(iso_path):
450       raise errors.HypervisorError("The path to the HVM CDROM image must"
451                                    " be an absolute path or None, not %s" %
452                                    iso_path)
453
454   def ValidateParameters(self, hvparams):
455     """Check the given parameters for validity.
456
457     For the PVM hypervisor, this only check the existence of the
458     kernel.
459
460     @type hvparams:  dict
461     @param hvparams: dictionary with parameter names/value
462     @raise errors.HypervisorError: when a parameter is not valid
463
464     """
465     super(XenHvmHypervisor, self).ValidateParameters(hvparams)
466
467     # hvm_cdrom_image_path verification
468     iso_path = hvparams[constants.HV_CDROM_IMAGE_PATH]
469     if iso_path and not os.path.isfile(iso_path):
470       raise errors.HypervisorError("The HVM CDROM image must either be a"
471                                    " regular file or a symlink pointing to"
472                                    " an existing regular file, not %s" %
473                                    iso_path)
474
475   @classmethod
476   def _WriteConfigFile(cls, instance, block_devices, extra_args):
477     """Create a Xen 3.1 HVM config file.
478
479     """
480     config = StringIO()
481     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
482     config.write("kernel = '/usr/lib/xen/boot/hvmloader'\n")
483     config.write("builder = 'hvm'\n")
484     config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
485     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
486     config.write("name = '%s'\n" % instance.name)
487     if instance.hvparams[constants.HV_PAE]:
488       config.write("pae = 1\n")
489     else:
490       config.write("pae = 0\n")
491     if instance.hvparams[constants.HV_ACPI]:
492       config.write("acpi = 1\n")
493     else:
494       config.write("acpi = 0\n")
495     config.write("apic = 1\n")
496     arch = os.uname()[4]
497     if '64' in arch:
498       config.write("device_model = '/usr/lib64/xen/bin/qemu-dm'\n")
499     else:
500       config.write("device_model = '/usr/lib/xen/bin/qemu-dm'\n")
501     if instance.hvparams[constants.HV_BOOT_ORDER] is None:
502       config.write("boot = '%s'\n" % constants.HT_HVM_DEFAULT_BOOT_ORDER)
503     else:
504       config.write("boot = '%s'\n" % instance.hvparams["boot_order"])
505     config.write("sdl = 0\n")
506     config.write("usb = 1\n")
507     config.write("usbdevice = 'tablet'\n")
508     config.write("vnc = 1\n")
509     if instance.hvparams[constants.HV_VNC_BIND_ADDRESS] is None:
510       config.write("vnclisten = '%s'\n" % constants.VNC_DEFAULT_BIND_ADDRESS)
511     else:
512       config.write("vnclisten = '%s'\n" %
513                    instance.hvparams["vnc_bind_address"])
514
515     if instance.network_port > constants.HT_HVM_VNC_BASE_PORT:
516       display = instance.network_port - constants.HT_HVM_VNC_BASE_PORT
517       config.write("vncdisplay = %s\n" % display)
518       config.write("vncunused = 0\n")
519     else:
520       config.write("# vncdisplay = 1\n")
521       config.write("vncunused = 1\n")
522
523     try:
524       password_file = open(constants.VNC_PASSWORD_FILE, "r")
525       try:
526         password = password_file.readline()
527       finally:
528         password_file.close()
529     except IOError:
530       raise errors.OpExecError("failed to open VNC password file %s " %
531                                constants.VNC_PASSWORD_FILE)
532
533     config.write("vncpasswd = '%s'\n" % password.rstrip())
534
535     config.write("serial = 'pty'\n")
536     config.write("localtime = 1\n")
537
538     vif_data = []
539     nic_type = instance.hvparams[constants.HV_NIC_TYPE]
540     if nic_type is None:
541       # ensure old instances don't change
542       nic_type_str = ", type=ioemu"
543     elif nic_type == constants.HT_HVM_DEV_PARAVIRTUAL:
544       nic_type_str = ", type=paravirtualized"
545     else:
546       nic_type_str = ", model=%s, type=ioemu" % nic_type
547     for nic in instance.nics:
548       nic_str = "mac=%s, bridge=%s%s" % (nic.mac, nic.bridge, nic_type_str)
549       ip = getattr(nic, "ip", None)
550       if ip is not None:
551         nic_str += ", ip=%s" % ip
552       vif_data.append("'%s'" % nic_str)
553
554     config.write("vif = [%s]\n" % ",".join(vif_data))
555     disk_data = cls._GetConfigFileDiskData(instance.disk_template,
556                                             block_devices)
557     disk_type = instance.hvparams[constants.HV_DISK_TYPE]
558     if disk_type in (None, constants.HT_HVM_DEV_IOEMU):
559       replacement = ",ioemu:hd"
560     else:
561       replacement = ",hd"
562     disk_data = [line.replace(",sd", replacement) for line in disk_data]
563     iso_path = instance.hvparams[constants.HV_CDROM_IMAGE_PATH]
564     if iso_path:
565       iso = "'file:%s,hdc:cdrom,r'" % iso_path
566       disk_data.append(iso)
567
568     config.write("disk = [%s]\n" % (",".join(disk_data)))
569
570     config.write("on_poweroff = 'destroy'\n")
571     config.write("on_reboot = 'restart'\n")
572     config.write("on_crash = 'restart'\n")
573     if extra_args:
574       config.write("extra = '%s'\n" % extra_args)
575     # just in case it exists
576     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
577     try:
578       f = open("/etc/xen/%s" % instance.name, "w")
579       try:
580         f.write(config.getvalue())
581       finally:
582         f.close()
583     except IOError, err:
584       raise errors.OpExecError("Cannot write Xen instance confile"
585                                " file /etc/xen/%s: %s" % (instance.name, err))
586     return True