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