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