Code and docstring style fixes
[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   REBOOT_RETRY_COUNT = 60
46   REBOOT_RETRY_INTERVAL = 10
47
48   ANCILLARY_FILES = [
49     '/etc/xen/xend-config.sxp',
50     '/etc/xen/scripts/vif-bridge',
51     ]
52
53   @classmethod
54   def _WriteConfigFile(cls, instance, block_devices):
55     """Write the Xen config file for the instance.
56
57     """
58     raise NotImplementedError
59
60   @staticmethod
61   def _WriteConfigFileStatic(instance_name, data):
62     """Write the Xen config file for the instance.
63
64     This version of the function just writes the config file from static data.
65
66     """
67     utils.WriteFile("/etc/xen/%s" % instance_name, data=data)
68
69   @staticmethod
70   def _ReadConfigFile(instance_name):
71     """Returns the contents of the instance config file.
72
73     """
74     try:
75       file_content = utils.ReadFile("/etc/xen/%s" % instance_name)
76     except EnvironmentError, err:
77       raise errors.HypervisorError("Failed to load Xen config file: %s" % err)
78     return file_content
79
80   @staticmethod
81   def _RemoveConfigFile(instance_name):
82     """Remove the xen configuration file.
83
84     """
85     utils.RemoveFile("/etc/xen/%s" % instance_name)
86
87   @staticmethod
88   def _GetXMList(include_node):
89     """Return the list of running instances.
90
91     If the include_node argument is True, then we return information
92     for dom0 also, otherwise we filter that from the return value.
93
94     @return: list of (name, id, memory, vcpus, state, time spent)
95
96     """
97     for _ in range(5):
98       result = utils.RunCmd(["xm", "list"])
99       if not result.failed:
100         break
101       logging.error("xm list failed (%s): %s", result.fail_reason,
102                     result.output)
103       time.sleep(1)
104
105     if result.failed:
106       raise errors.HypervisorError("xm list failed, retries"
107                                    " exceeded (%s): %s" %
108                                    (result.fail_reason, result.output))
109
110     # skip over the heading
111     lines = result.stdout.splitlines()[1:]
112     result = []
113     for line in lines:
114       # The format of lines is:
115       # Name      ID Mem(MiB) VCPUs State  Time(s)
116       # Domain-0   0  3418     4 r-----    266.2
117       data = line.split()
118       if len(data) != 6:
119         raise errors.HypervisorError("Can't parse output of xm list,"
120                                      " line: %s" % line)
121       try:
122         data[1] = int(data[1])
123         data[2] = int(data[2])
124         data[3] = int(data[3])
125         data[5] = float(data[5])
126       except ValueError, err:
127         raise errors.HypervisorError("Can't parse output of xm list,"
128                                      " line: %s, error: %s" % (line, err))
129
130       # skip the Domain-0 (optional)
131       if include_node or data[0] != 'Domain-0':
132         result.append(data)
133
134     return result
135
136   def ListInstances(self):
137     """Get the list of running instances.
138
139     """
140     xm_list = self._GetXMList(False)
141     names = [info[0] for info in xm_list]
142     return names
143
144   def GetInstanceInfo(self, instance_name):
145     """Get instance properties.
146
147     @param instance_name: the instance name
148
149     @return: tuple (name, id, memory, vcpus, stat, times)
150
151     """
152     xm_list = self._GetXMList(instance_name=="Domain-0")
153     result = None
154     for data in xm_list:
155       if data[0] == instance_name:
156         result = data
157         break
158     return result
159
160   def GetAllInstancesInfo(self):
161     """Get properties of all instances.
162
163     @return: list of tuples (name, id, memory, vcpus, stat, times)
164
165     """
166     xm_list = self._GetXMList(False)
167     return xm_list
168
169   def StartInstance(self, instance, block_devices):
170     """Start an instance.
171
172     """
173     self._WriteConfigFile(instance, block_devices)
174     result = utils.RunCmd(["xm", "create", instance.name])
175
176     if result.failed:
177       raise errors.HypervisorError("Failed to start instance %s: %s (%s)" %
178                                    (instance.name, result.fail_reason,
179                                     result.output))
180
181   def StopInstance(self, instance, force=False, retry=False):
182     """Stop an instance.
183
184     """
185     if retry:
186       return
187     self._RemoveConfigFile(instance.name)
188     if force:
189       command = ["xm", "destroy", instance.name]
190     else:
191       command = ["xm", "shutdown", instance.name]
192     result = utils.RunCmd(command)
193
194     if result.failed:
195       raise errors.HypervisorError("Failed to stop instance %s: %s, %s" %
196                                    (instance.name, result.fail_reason,
197                                     result.output))
198
199   def RebootInstance(self, instance):
200     """Reboot an instance.
201
202     """
203     ini_info = self.GetInstanceInfo(instance.name)
204     result = utils.RunCmd(["xm", "reboot", instance.name])
205
206     if result.failed:
207       raise errors.HypervisorError("Failed to reboot instance %s: %s, %s" %
208                                    (instance.name, result.fail_reason,
209                                     result.output))
210     done = False
211     retries = self.REBOOT_RETRY_COUNT
212     while retries > 0:
213       new_info = self.GetInstanceInfo(instance.name)
214       # check if the domain ID has changed or the run time has
215       # decreased
216       if new_info[1] != ini_info[1] or new_info[5] < ini_info[5]:
217         done = True
218         break
219       time.sleep(self.REBOOT_RETRY_INTERVAL)
220       retries -= 1
221
222     if not done:
223       raise errors.HypervisorError("Failed to reboot instance %s: instance"
224                                    " did not reboot in the expected interval" %
225                                    (instance.name, ))
226
227   def GetNodeInfo(self):
228     """Return information about the node.
229
230     @return: a dict with the following keys (memory values in MiB):
231           - memory_total: the total memory size on the node
232           - memory_free: the available memory on the node for instances
233           - memory_dom0: the memory used by the node itself, if available
234           - nr_cpus: total number of CPUs
235           - nr_nodes: in a NUMA system, the number of domains
236           - nr_sockets: the number of physical CPU sockets in the node
237
238     """
239     # note: in xen 3, memory has changed to total_memory
240     result = utils.RunCmd(["xm", "info"])
241     if result.failed:
242       logging.error("Can't run 'xm info' (%s): %s", result.fail_reason,
243                     result.output)
244       return None
245
246     xmoutput = result.stdout.splitlines()
247     result = {}
248     cores_per_socket = threads_per_core = nr_cpus = None
249     for line in xmoutput:
250       splitfields = line.split(":", 1)
251
252       if len(splitfields) > 1:
253         key = splitfields[0].strip()
254         val = splitfields[1].strip()
255         if key == 'memory' or key == 'total_memory':
256           result['memory_total'] = int(val)
257         elif key == 'free_memory':
258           result['memory_free'] = int(val)
259         elif key == 'nr_cpus':
260           nr_cpus = result['cpu_total'] = int(val)
261         elif key == 'nr_nodes':
262           result['cpu_nodes'] = int(val)
263         elif key == 'cores_per_socket':
264           cores_per_socket = int(val)
265         elif key == 'threads_per_core':
266           threads_per_core = int(val)
267
268     if (cores_per_socket is not None and
269         threads_per_core is not None and nr_cpus is not None):
270       result['cpu_sockets'] = nr_cpus / (cores_per_socket * threads_per_core)
271
272     dom0_info = self.GetInstanceInfo("Domain-0")
273     if dom0_info is not None:
274       result['memory_dom0'] = dom0_info[2]
275
276     return result
277
278   @classmethod
279   def GetShellCommandForConsole(cls, instance, hvparams, beparams):
280     """Return a command for connecting to the console of an instance.
281
282     """
283     return "xm console %s" % instance.name
284
285
286   def Verify(self):
287     """Verify the hypervisor.
288
289     For Xen, this verifies that the xend process is running.
290
291     """
292     result = utils.RunCmd(["xm", "info"])
293     if result.failed:
294       return "'xm info' failed: %s, %s" % (result.fail_reason, result.output)
295
296   @staticmethod
297   def _GetConfigFileDiskData(disk_template, block_devices):
298     """Get disk directive for xen config file.
299
300     This method builds the xen config disk directive according to the
301     given disk_template and block_devices.
302
303     @param disk_template: string containing instance disk template
304     @param block_devices: list of tuples (cfdev, rldev):
305         - cfdev: dict containing ganeti config disk part
306         - rldev: ganeti.bdev.BlockDev object
307
308     @return: string containing disk directive for xen instance config file
309
310     """
311     FILE_DRIVER_MAP = {
312       constants.FD_LOOP: "file",
313       constants.FD_BLKTAP: "tap:aio",
314       }
315     disk_data = []
316     if len(block_devices) > 24:
317       # 'z' - 'a' = 24
318       raise errors.HypervisorError("Too many disks")
319     # FIXME: instead of this hardcoding here, each of PVM/HVM should
320     # directly export their info (currently HVM will just sed this info)
321     namespace = ["sd" + chr(i + ord('a')) for i in range(24)]
322     for sd_name, (cfdev, dev_path) in zip(namespace, block_devices):
323       if cfdev.mode == constants.DISK_RDWR:
324         mode = "w"
325       else:
326         mode = "r"
327       if cfdev.dev_type == constants.LD_FILE:
328         line = "'%s:%s,%s,%s'" % (FILE_DRIVER_MAP[cfdev.physical_id[0]],
329                                   dev_path, sd_name, mode)
330       else:
331         line = "'phy:%s,%s,%s'" % (dev_path, sd_name, mode)
332       disk_data.append(line)
333
334     return disk_data
335
336   def MigrationInfo(self, instance):
337     """Get instance information to perform a migration.
338
339     @type instance: L{objects.Instance}
340     @param instance: instance to be migrated
341     @rtype: string
342     @return: content of the xen config file
343
344     """
345     return self._ReadConfigFile(instance.name)
346
347   def AcceptInstance(self, instance, info, target):
348     """Prepare to accept an instance.
349
350     @type instance: L{objects.Instance}
351     @param instance: instance to be accepted
352     @type info: string
353     @param info: content of the xen config file on the source node
354     @type target: string
355     @param target: target host (usually ip), on this node
356
357     """
358     pass
359
360   def FinalizeMigration(self, instance, info, success):
361     """Finalize an instance migration.
362
363     After a successful migration we write the xen config file.
364     We do nothing on a failure, as we did not change anything at accept time.
365
366     @type instance: L{objects.Instance}
367     @param instance: instance whose migration is being aborted
368     @type info: string
369     @param info: content of the xen config file on the source node
370     @type success: boolean
371     @param success: whether the migration was a success or a failure
372
373     """
374     if success:
375       self._WriteConfigFileStatic(instance.name, info)
376
377   def MigrateInstance(self, instance, target, live):
378     """Migrate an instance to a target node.
379
380     The migration will not be attempted if the instance is not
381     currently running.
382
383     @type instance: string
384     @param instance: instance name
385     @type target: string
386     @param target: ip address of the target node
387     @type live: boolean
388     @param live: perform a live migration
389
390     """
391     if self.GetInstanceInfo(instance) is None:
392       raise errors.HypervisorError("Instance not running, cannot migrate")
393     args = ["xm", "migrate"]
394     if live:
395       args.append("-l")
396     args.extend([instance, target])
397     result = utils.RunCmd(args)
398     if result.failed:
399       raise errors.HypervisorError("Failed to migrate instance %s: %s" %
400                                    (instance, result.output))
401     # remove old xen file after migration succeeded
402     try:
403       self._RemoveConfigFile(instance)
404     except EnvironmentError:
405       logging.exception("Failure while removing instance config file")
406
407   @classmethod
408   def PowercycleNode(cls):
409     """Xen-specific powercycle.
410
411     This first does a Linux reboot (which triggers automatically a Xen
412     reboot), and if that fails it tries to do a Xen reboot. The reason
413     we don't try a Xen reboot first is that the xen reboot launches an
414     external command which connects to the Xen hypervisor, and that
415     won't work in case the root filesystem is broken and/or the xend
416     daemon is not working.
417
418     """
419     try:
420       cls.LinuxPowercycle()
421     finally:
422       utils.RunCmd(["xm", "debug", "R"])
423
424
425 class XenPvmHypervisor(XenHypervisor):
426   """Xen PVM hypervisor interface"""
427
428   PARAMETERS = {
429     constants.HV_USE_BOOTLOADER: hv_base.NO_CHECK,
430     constants.HV_BOOTLOADER_PATH: hv_base.OPT_FILE_CHECK,
431     constants.HV_BOOTLOADER_ARGS: hv_base.NO_CHECK,
432     constants.HV_KERNEL_PATH: hv_base.REQ_FILE_CHECK,
433     constants.HV_INITRD_PATH: hv_base.OPT_FILE_CHECK,
434     constants.HV_ROOT_PATH: hv_base.REQUIRED_CHECK,
435     constants.HV_KERNEL_ARGS: hv_base.NO_CHECK,
436     }
437
438   @classmethod
439   def _WriteConfigFile(cls, instance, block_devices):
440     """Write the Xen config file for the instance.
441
442     """
443     hvp = instance.hvparams
444     config = StringIO()
445     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
446
447     # if bootloader is True, use bootloader instead of kernel and ramdisk
448     # parameters.
449     if hvp[constants.HV_USE_BOOTLOADER]:
450       # bootloader handling
451       bootloader_path = hvp[constants.HV_BOOTLOADER_PATH]
452       if bootloader_path:
453         config.write("bootloader = '%s'\n" % bootloader_path)
454       else:
455         raise errors.HypervisorError("Bootloader enabled, but missing"
456                                      " bootloader path")
457
458       bootloader_args = hvp[constants.HV_BOOTLOADER_ARGS]
459       if bootloader_args:
460         config.write("bootargs = '%s'\n" % bootloader_args)
461     else:
462       # kernel handling
463       kpath = hvp[constants.HV_KERNEL_PATH]
464       config.write("kernel = '%s'\n" % kpath)
465
466       # initrd handling
467       initrd_path = hvp[constants.HV_INITRD_PATH]
468       if initrd_path:
469         config.write("ramdisk = '%s'\n" % initrd_path)
470
471     # rest of the settings
472     config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
473     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
474     config.write("name = '%s'\n" % instance.name)
475
476     vif_data = []
477     for nic in instance.nics:
478       nic_str = "mac=%s" % (nic.mac)
479       ip = getattr(nic, "ip", None)
480       if ip is not None:
481         nic_str += ", ip=%s" % ip
482       vif_data.append("'%s'" % nic_str)
483       if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
484         nic_str += ", bridge=%s" % nic.nicparams[constants.NIC_LINK]
485
486     config.write("vif = [%s]\n" % ",".join(vif_data))
487     config.write("disk = [%s]\n" % ",".join(
488                  cls._GetConfigFileDiskData(instance.disk_template,
489                                             block_devices)))
490
491     config.write("root = '%s'\n" % hvp[constants.HV_ROOT_PATH])
492     config.write("on_poweroff = 'destroy'\n")
493     config.write("on_reboot = 'restart'\n")
494     config.write("on_crash = 'restart'\n")
495     config.write("extra = '%s'\n" % hvp[constants.HV_KERNEL_ARGS])
496     # just in case it exists
497     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
498     try:
499       utils.WriteFile("/etc/xen/%s" % instance.name, data=config.getvalue())
500     except EnvironmentError, err:
501       raise errors.HypervisorError("Cannot write Xen instance confile"
502                                    " file /etc/xen/%s: %s" %
503                                    (instance.name, err))
504
505     return True
506
507
508 class XenHvmHypervisor(XenHypervisor):
509   """Xen HVM hypervisor interface"""
510
511   ANCILLARY_FILES = XenHypervisor.ANCILLARY_FILES + [
512     constants.VNC_PASSWORD_FILE,
513     ]
514
515   PARAMETERS = {
516     constants.HV_ACPI: hv_base.NO_CHECK,
517     constants.HV_BOOT_ORDER: (True, ) +
518       (lambda x: x and len(x.strip("acdn")) == 0,
519        "Invalid boot order specified, must be one or more of [acdn]",
520        None, None),
521     constants.HV_CDROM_IMAGE_PATH: hv_base.OPT_FILE_CHECK,
522     constants.HV_DISK_TYPE:
523       hv_base.ParamInSet(True, constants.HT_HVM_VALID_DISK_TYPES),
524     constants.HV_NIC_TYPE:
525       hv_base.ParamInSet(True, constants.HT_HVM_VALID_NIC_TYPES),
526     constants.HV_PAE: hv_base.NO_CHECK,
527     constants.HV_VNC_BIND_ADDRESS:
528       (False, utils.IsValidIP,
529        "VNC bind address is not a valid IP address", None, None),
530     constants.HV_KERNEL_PATH: hv_base.REQ_FILE_CHECK,
531     constants.HV_DEVICE_MODEL: hv_base.REQ_FILE_CHECK,
532     constants.HV_VNC_PASSWORD_FILE: hv_base.REQ_FILE_CHECK,
533     }
534
535   @classmethod
536   def _WriteConfigFile(cls, instance, block_devices):
537     """Create a Xen 3.1 HVM config file.
538
539     """
540     hvp = instance.hvparams
541
542     config = StringIO()
543     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
544
545     # kernel handling
546     kpath = hvp[constants.HV_KERNEL_PATH]
547     config.write("kernel = '%s'\n" % kpath)
548
549     config.write("builder = 'hvm'\n")
550     config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
551     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
552     config.write("name = '%s'\n" % instance.name)
553     if hvp[constants.HV_PAE]:
554       config.write("pae = 1\n")
555     else:
556       config.write("pae = 0\n")
557     if hvp[constants.HV_ACPI]:
558       config.write("acpi = 1\n")
559     else:
560       config.write("acpi = 0\n")
561     config.write("apic = 1\n")
562     config.write("device_model = '%s'\n" % hvp[constants.HV_DEVICE_MODEL])
563     config.write("boot = '%s'\n" % hvp[constants.HV_BOOT_ORDER])
564     config.write("sdl = 0\n")
565     config.write("usb = 1\n")
566     config.write("usbdevice = 'tablet'\n")
567     config.write("vnc = 1\n")
568     if hvp[constants.HV_VNC_BIND_ADDRESS] is None:
569       config.write("vnclisten = '%s'\n" % constants.VNC_DEFAULT_BIND_ADDRESS)
570     else:
571       config.write("vnclisten = '%s'\n" % hvp[constants.HV_VNC_BIND_ADDRESS])
572
573     if instance.network_port > constants.VNC_BASE_PORT:
574       display = instance.network_port - constants.VNC_BASE_PORT
575       config.write("vncdisplay = %s\n" % display)
576       config.write("vncunused = 0\n")
577     else:
578       config.write("# vncdisplay = 1\n")
579       config.write("vncunused = 1\n")
580
581     vnc_pwd_file = hvp[constants.HV_VNC_PASSWORD_FILE]
582     try:
583       password = utils.ReadFile(vnc_pwd_file)
584     except EnvironmentError, err:
585       raise errors.HypervisorError("Failed to open VNC password file %s: %s" %
586                                    (vnc_pwd_file, err))
587
588     config.write("vncpasswd = '%s'\n" % password.rstrip())
589
590     config.write("serial = 'pty'\n")
591     config.write("localtime = 1\n")
592
593     vif_data = []
594     nic_type = hvp[constants.HV_NIC_TYPE]
595     if nic_type is None:
596       # ensure old instances don't change
597       nic_type_str = ", type=ioemu"
598     elif nic_type == constants.HT_NIC_PARAVIRTUAL:
599       nic_type_str = ", type=paravirtualized"
600     else:
601       nic_type_str = ", model=%s, type=ioemu" % nic_type
602     for nic in instance.nics:
603       nic_str = "mac=%s%s" % (nic.mac, nic_type_str)
604       ip = getattr(nic, "ip", None)
605       if ip is not None:
606         nic_str += ", ip=%s" % ip
607       vif_data.append("'%s'" % nic_str)
608       if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
609         nic_str += ", bridge=%s" % nic.nicparams[constants.NIC_LINK]
610
611     config.write("vif = [%s]\n" % ",".join(vif_data))
612     disk_data = cls._GetConfigFileDiskData(instance.disk_template,
613                                             block_devices)
614     disk_type = hvp[constants.HV_DISK_TYPE]
615     if disk_type in (None, constants.HT_DISK_IOEMU):
616       replacement = ",ioemu:hd"
617     else:
618       replacement = ",hd"
619     disk_data = [line.replace(",sd", replacement) for line in disk_data]
620     iso_path = hvp[constants.HV_CDROM_IMAGE_PATH]
621     if iso_path:
622       iso = "'file:%s,hdc:cdrom,r'" % iso_path
623       disk_data.append(iso)
624
625     config.write("disk = [%s]\n" % (",".join(disk_data)))
626
627     config.write("on_poweroff = 'destroy'\n")
628     config.write("on_reboot = 'restart'\n")
629     config.write("on_crash = 'restart'\n")
630     # just in case it exists
631     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
632     try:
633       utils.WriteFile("/etc/xen/%s" % instance.name,
634                       data=config.getvalue())
635     except EnvironmentError, err:
636       raise errors.HypervisorError("Cannot write Xen instance confile"
637                                    " file /etc/xen/%s: %s" %
638                                    (instance.name, err))
639
640     return True