Merge branch 'devel-2.0' into devel-2.1
[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):
193     """Stop an instance.
194
195     """
196     self._RemoveConfigFile(instance.name)
197     if force:
198       command = ["xm", "destroy", instance.name]
199     else:
200       command = ["xm", "shutdown", instance.name]
201     result = utils.RunCmd(command)
202
203     if result.failed:
204       raise errors.HypervisorError("Failed to stop instance %s: %s, %s" %
205                                    (instance.name, result.fail_reason,
206                                     result.output))
207
208   def RebootInstance(self, instance):
209     """Reboot an instance.
210
211     """
212     ini_info = self.GetInstanceInfo(instance.name)
213
214     result = utils.RunCmd(["xm", "reboot", instance.name])
215     if result.failed:
216       raise errors.HypervisorError("Failed to reboot instance %s: %s, %s" %
217                                    (instance.name, result.fail_reason,
218                                     result.output))
219
220     def _CheckInstance():
221       new_info = self.GetInstanceInfo(instance.name)
222
223       # check if the domain ID has changed or the run time has decreased
224       if new_info[1] != ini_info[1] or new_info[5] < ini_info[5]:
225         return
226
227       raise utils.RetryAgain()
228
229     try:
230       utils.Retry(_CheckInstance, self.REBOOT_RETRY_INTERVAL,
231                   self.REBOOT_RETRY_INTERVAL * self.REBOOT_RETRY_COUNT)
232     except utils.RetryTimeout:
233       raise errors.HypervisorError("Failed to reboot instance %s: instance"
234                                    " did not reboot in the expected interval" %
235                                    (instance.name, ))
236
237   def GetNodeInfo(self):
238     """Return information about the node.
239
240     @return: a dict with the following keys (memory values in MiB):
241           - memory_total: the total memory size on the node
242           - memory_free: the available memory on the node for instances
243           - memory_dom0: the memory used by the node itself, if available
244           - nr_cpus: total number of CPUs
245           - nr_nodes: in a NUMA system, the number of domains
246           - nr_sockets: the number of physical CPU sockets in the node
247
248     """
249     # note: in xen 3, memory has changed to total_memory
250     result = utils.RunCmd(["xm", "info"])
251     if result.failed:
252       logging.error("Can't run 'xm info' (%s): %s", result.fail_reason,
253                     result.output)
254       return None
255
256     xmoutput = result.stdout.splitlines()
257     result = {}
258     cores_per_socket = threads_per_core = nr_cpus = None
259     for line in xmoutput:
260       splitfields = line.split(":", 1)
261
262       if len(splitfields) > 1:
263         key = splitfields[0].strip()
264         val = splitfields[1].strip()
265         if key == 'memory' or key == 'total_memory':
266           result['memory_total'] = int(val)
267         elif key == 'free_memory':
268           result['memory_free'] = int(val)
269         elif key == 'nr_cpus':
270           nr_cpus = result['cpu_total'] = int(val)
271         elif key == 'nr_nodes':
272           result['cpu_nodes'] = int(val)
273         elif key == 'cores_per_socket':
274           cores_per_socket = int(val)
275         elif key == 'threads_per_core':
276           threads_per_core = int(val)
277
278     if (cores_per_socket is not None and
279         threads_per_core is not None and nr_cpus is not None):
280       result['cpu_sockets'] = nr_cpus / (cores_per_socket * threads_per_core)
281
282     dom0_info = self.GetInstanceInfo("Domain-0")
283     if dom0_info is not None:
284       result['memory_dom0'] = dom0_info[2]
285
286     return result
287
288   @classmethod
289   def GetShellCommandForConsole(cls, instance, hvparams, beparams):
290     """Return a command for connecting to the console of an instance.
291
292     """
293     return "xm console %s" % instance.name
294
295
296   def Verify(self):
297     """Verify the hypervisor.
298
299     For Xen, this verifies that the xend process is running.
300
301     """
302     result = utils.RunCmd(["xm", "info"])
303     if result.failed:
304       return "'xm info' failed: %s, %s" % (result.fail_reason, result.output)
305
306   @staticmethod
307   def _GetConfigFileDiskData(block_devices):
308     """Get disk directive for xen config file.
309
310     This method builds the xen config disk directive according to the
311     given disk_template and block_devices.
312
313     @param block_devices: list of tuples (cfdev, rldev):
314         - cfdev: dict containing ganeti config disk part
315         - rldev: ganeti.bdev.BlockDev object
316
317     @return: string containing disk directive for xen instance config file
318
319     """
320     FILE_DRIVER_MAP = {
321       constants.FD_LOOP: "file",
322       constants.FD_BLKTAP: "tap:aio",
323       }
324     disk_data = []
325     if len(block_devices) > 24:
326       # 'z' - 'a' = 24
327       raise errors.HypervisorError("Too many disks")
328     # FIXME: instead of this hardcoding here, each of PVM/HVM should
329     # directly export their info (currently HVM will just sed this info)
330     namespace = ["sd" + chr(i + ord('a')) for i in range(24)]
331     for sd_name, (cfdev, dev_path) in zip(namespace, block_devices):
332       if cfdev.mode == constants.DISK_RDWR:
333         mode = "w"
334       else:
335         mode = "r"
336       if cfdev.dev_type == constants.LD_FILE:
337         line = "'%s:%s,%s,%s'" % (FILE_DRIVER_MAP[cfdev.physical_id[0]],
338                                   dev_path, sd_name, mode)
339       else:
340         line = "'phy:%s,%s,%s'" % (dev_path, sd_name, mode)
341       disk_data.append(line)
342
343     return disk_data
344
345   def MigrationInfo(self, instance):
346     """Get instance information to perform a migration.
347
348     @type instance: L{objects.Instance}
349     @param instance: instance to be migrated
350     @rtype: string
351     @return: content of the xen config file
352
353     """
354     return self._ReadConfigFile(instance.name)
355
356   def AcceptInstance(self, instance, info, target):
357     """Prepare to accept an instance.
358
359     @type instance: L{objects.Instance}
360     @param instance: instance to be accepted
361     @type info: string
362     @param info: content of the xen config file on the source node
363     @type target: string
364     @param target: target host (usually ip), on this node
365
366     """
367     pass
368
369   def FinalizeMigration(self, instance, info, success):
370     """Finalize an instance migration.
371
372     After a successful migration we write the xen config file.
373     We do nothing on a failure, as we did not change anything at accept time.
374
375     @type instance: L{objects.Instance}
376     @param instance: instance whose migration is being aborted
377     @type info: string
378     @param info: content of the xen config file on the source node
379     @type success: boolean
380     @param success: whether the migration was a success or a failure
381
382     """
383     if success:
384       self._WriteConfigFileStatic(instance.name, info)
385
386   def MigrateInstance(self, instance, target, live):
387     """Migrate an instance to a target node.
388
389     The migration will not be attempted if the instance is not
390     currently running.
391
392     @type instance: L{objects.Instance}
393     @param instance: the instance to be migrated
394     @type target: string
395     @param target: ip address of the target node
396     @type live: boolean
397     @param live: perform a live migration
398
399     """
400     if self.GetInstanceInfo(instance.name) is None:
401       raise errors.HypervisorError("Instance not running, cannot migrate")
402
403     port = instance.hvparams[constants.HV_MIGRATION_PORT]
404
405     if not utils.TcpPing(target, port, live_port_needed=True):
406       raise errors.HypervisorError("Remote host %s not listening on port"
407                                    " %s, cannot migrate" % (target, port))
408
409     args = ["xm", "migrate", "-p", "%d" % port]
410     if live:
411       args.append("-l")
412     args.extend([instance.name, target])
413     result = utils.RunCmd(args)
414     if result.failed:
415       raise errors.HypervisorError("Failed to migrate instance %s: %s" %
416                                    (instance.name, result.output))
417     # remove old xen file after migration succeeded
418     try:
419       self._RemoveConfigFile(instance.name)
420     except EnvironmentError:
421       logging.exception("Failure while removing instance config file")
422
423   @classmethod
424   def PowercycleNode(cls):
425     """Xen-specific powercycle.
426
427     This first does a Linux reboot (which triggers automatically a Xen
428     reboot), and if that fails it tries to do a Xen reboot. The reason
429     we don't try a Xen reboot first is that the xen reboot launches an
430     external command which connects to the Xen hypervisor, and that
431     won't work in case the root filesystem is broken and/or the xend
432     daemon is not working.
433
434     """
435     try:
436       cls.LinuxPowercycle()
437     finally:
438       utils.RunCmd(["xm", "debug", "R"])
439
440
441 class XenPvmHypervisor(XenHypervisor):
442   """Xen PVM hypervisor interface"""
443
444   PARAMETERS = {
445     constants.HV_USE_BOOTLOADER: hv_base.NO_CHECK,
446     constants.HV_BOOTLOADER_PATH: hv_base.OPT_FILE_CHECK,
447     constants.HV_BOOTLOADER_ARGS: hv_base.NO_CHECK,
448     constants.HV_KERNEL_PATH: hv_base.REQ_FILE_CHECK,
449     constants.HV_INITRD_PATH: hv_base.OPT_FILE_CHECK,
450     constants.HV_ROOT_PATH: hv_base.REQUIRED_CHECK,
451     constants.HV_KERNEL_ARGS: hv_base.NO_CHECK,
452     constants.HV_MIGRATION_PORT: hv_base.NET_PORT_CHECK,
453     }
454
455   @classmethod
456   def _WriteConfigFile(cls, instance, block_devices):
457     """Write the Xen config file for the instance.
458
459     """
460     hvp = instance.hvparams
461     config = StringIO()
462     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
463
464     # if bootloader is True, use bootloader instead of kernel and ramdisk
465     # parameters.
466     if hvp[constants.HV_USE_BOOTLOADER]:
467       # bootloader handling
468       bootloader_path = hvp[constants.HV_BOOTLOADER_PATH]
469       if bootloader_path:
470         config.write("bootloader = '%s'\n" % bootloader_path)
471       else:
472         raise errors.HypervisorError("Bootloader enabled, but missing"
473                                      " bootloader path")
474
475       bootloader_args = hvp[constants.HV_BOOTLOADER_ARGS]
476       if bootloader_args:
477         config.write("bootargs = '%s'\n" % bootloader_args)
478     else:
479       # kernel handling
480       kpath = hvp[constants.HV_KERNEL_PATH]
481       config.write("kernel = '%s'\n" % kpath)
482
483       # initrd handling
484       initrd_path = hvp[constants.HV_INITRD_PATH]
485       if initrd_path:
486         config.write("ramdisk = '%s'\n" % initrd_path)
487
488     # rest of the settings
489     config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
490     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
491     config.write("name = '%s'\n" % instance.name)
492
493     vif_data = []
494     for nic in instance.nics:
495       nic_str = "mac=%s" % (nic.mac)
496       ip = getattr(nic, "ip", None)
497       if ip is not None:
498         nic_str += ", ip=%s" % ip
499       vif_data.append("'%s'" % nic_str)
500       if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
501         nic_str += ", bridge=%s" % nic.nicparams[constants.NIC_LINK]
502
503     disk_data = cls._GetConfigFileDiskData(block_devices)
504
505     config.write("vif = [%s]\n" % ",".join(vif_data))
506     config.write("disk = [%s]\n" % ",".join(disk_data))
507
508     config.write("root = '%s'\n" % hvp[constants.HV_ROOT_PATH])
509     config.write("on_poweroff = 'destroy'\n")
510     config.write("on_reboot = 'restart'\n")
511     config.write("on_crash = 'restart'\n")
512     config.write("extra = '%s'\n" % hvp[constants.HV_KERNEL_ARGS])
513     # just in case it exists
514     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
515     try:
516       utils.WriteFile("/etc/xen/%s" % instance.name, data=config.getvalue())
517     except EnvironmentError, err:
518       raise errors.HypervisorError("Cannot write Xen instance confile"
519                                    " file /etc/xen/%s: %s" %
520                                    (instance.name, err))
521
522     return True
523
524
525 class XenHvmHypervisor(XenHypervisor):
526   """Xen HVM hypervisor interface"""
527
528   ANCILLARY_FILES = XenHypervisor.ANCILLARY_FILES + [
529     constants.VNC_PASSWORD_FILE,
530     ]
531
532   PARAMETERS = {
533     constants.HV_ACPI: hv_base.NO_CHECK,
534     constants.HV_BOOT_ORDER: (True, ) +
535       (lambda x: x and len(x.strip("acdn")) == 0,
536        "Invalid boot order specified, must be one or more of [acdn]",
537        None, None),
538     constants.HV_CDROM_IMAGE_PATH: hv_base.OPT_FILE_CHECK,
539     constants.HV_DISK_TYPE:
540       hv_base.ParamInSet(True, constants.HT_HVM_VALID_DISK_TYPES),
541     constants.HV_NIC_TYPE:
542       hv_base.ParamInSet(True, constants.HT_HVM_VALID_NIC_TYPES),
543     constants.HV_PAE: hv_base.NO_CHECK,
544     constants.HV_VNC_BIND_ADDRESS:
545       (False, utils.IsValidIP,
546        "VNC bind address is not a valid IP address", None, None),
547     constants.HV_KERNEL_PATH: hv_base.REQ_FILE_CHECK,
548     constants.HV_DEVICE_MODEL: hv_base.REQ_FILE_CHECK,
549     constants.HV_VNC_PASSWORD_FILE: hv_base.REQ_FILE_CHECK,
550     constants.HV_MIGRATION_PORT: hv_base.NET_PORT_CHECK,
551     constants.HV_USE_LOCALTIME: hv_base.NO_CHECK,
552     }
553
554   @classmethod
555   def _WriteConfigFile(cls, instance, block_devices):
556     """Create a Xen 3.1 HVM config file.
557
558     """
559     hvp = instance.hvparams
560
561     config = StringIO()
562     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
563
564     # kernel handling
565     kpath = hvp[constants.HV_KERNEL_PATH]
566     config.write("kernel = '%s'\n" % kpath)
567
568     config.write("builder = 'hvm'\n")
569     config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
570     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
571     config.write("name = '%s'\n" % instance.name)
572     if hvp[constants.HV_PAE]:
573       config.write("pae = 1\n")
574     else:
575       config.write("pae = 0\n")
576     if hvp[constants.HV_ACPI]:
577       config.write("acpi = 1\n")
578     else:
579       config.write("acpi = 0\n")
580     config.write("apic = 1\n")
581     config.write("device_model = '%s'\n" % hvp[constants.HV_DEVICE_MODEL])
582     config.write("boot = '%s'\n" % hvp[constants.HV_BOOT_ORDER])
583     config.write("sdl = 0\n")
584     config.write("usb = 1\n")
585     config.write("usbdevice = 'tablet'\n")
586     config.write("vnc = 1\n")
587     if hvp[constants.HV_VNC_BIND_ADDRESS] is None:
588       config.write("vnclisten = '%s'\n" % constants.VNC_DEFAULT_BIND_ADDRESS)
589     else:
590       config.write("vnclisten = '%s'\n" % hvp[constants.HV_VNC_BIND_ADDRESS])
591
592     if instance.network_port > constants.VNC_BASE_PORT:
593       display = instance.network_port - constants.VNC_BASE_PORT
594       config.write("vncdisplay = %s\n" % display)
595       config.write("vncunused = 0\n")
596     else:
597       config.write("# vncdisplay = 1\n")
598       config.write("vncunused = 1\n")
599
600     vnc_pwd_file = hvp[constants.HV_VNC_PASSWORD_FILE]
601     try:
602       password = utils.ReadFile(vnc_pwd_file)
603     except EnvironmentError, err:
604       raise errors.HypervisorError("Failed to open VNC password file %s: %s" %
605                                    (vnc_pwd_file, err))
606
607     config.write("vncpasswd = '%s'\n" % password.rstrip())
608
609     config.write("serial = 'pty'\n")
610     if hvp[constants.HV_USE_LOCALTIME]:
611       config.write("localtime = 1\n")
612
613     vif_data = []
614     nic_type = hvp[constants.HV_NIC_TYPE]
615     if nic_type is None:
616       # ensure old instances don't change
617       nic_type_str = ", type=ioemu"
618     elif nic_type == constants.HT_NIC_PARAVIRTUAL:
619       nic_type_str = ", type=paravirtualized"
620     else:
621       nic_type_str = ", model=%s, type=ioemu" % nic_type
622     for nic in instance.nics:
623       nic_str = "mac=%s%s" % (nic.mac, nic_type_str)
624       ip = getattr(nic, "ip", None)
625       if ip is not None:
626         nic_str += ", ip=%s" % ip
627       vif_data.append("'%s'" % nic_str)
628       if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
629         nic_str += ", bridge=%s" % nic.nicparams[constants.NIC_LINK]
630
631     config.write("vif = [%s]\n" % ",".join(vif_data))
632     disk_data = cls._GetConfigFileDiskData(block_devices)
633     disk_type = hvp[constants.HV_DISK_TYPE]
634     if disk_type in (None, constants.HT_DISK_IOEMU):
635       replacement = ",ioemu:hd"
636     else:
637       replacement = ",hd"
638     disk_data = [line.replace(",sd", replacement) for line in disk_data]
639     iso_path = hvp[constants.HV_CDROM_IMAGE_PATH]
640     if iso_path:
641       iso = "'file:%s,hdc:cdrom,r'" % iso_path
642       disk_data.append(iso)
643
644     config.write("disk = [%s]\n" % (",".join(disk_data)))
645
646     config.write("on_poweroff = 'destroy'\n")
647     config.write("on_reboot = 'restart'\n")
648     config.write("on_crash = 'restart'\n")
649     # just in case it exists
650     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
651     try:
652       utils.WriteFile("/etc/xen/%s" % instance.name,
653                       data=config.getvalue())
654     except EnvironmentError, err:
655       raise errors.HypervisorError("Cannot write Xen instance confile"
656                                    " file /etc/xen/%s: %s" %
657                                    (instance.name, err))
658
659     return True