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