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