Some small fixes
[ganeti-local] / lib / hypervisor / hv_xen.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Xen hypervisors
23
24 """
25
26 import os
27 import os.path
28 import time
29 import logging
30 from cStringIO import StringIO
31
32 from ganeti import constants
33 from ganeti import errors
34 from ganeti import utils
35 from ganeti.hypervisor import hv_base
36
37
38 class XenHypervisor(hv_base.BaseHypervisor):
39   """Xen generic hypervisor interface
40
41   This is the Xen base class used for both Xen PVM and HVM. It contains
42   all the functionality that is identical for both.
43
44   """
45
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 (memory 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           - nr_cpus: total number of CPUs
207           - nr_nodes: in a NUMA system, the number of domains
208           - nr_sockets: the number of physical CPU sockets in the node
209
210     """
211     # note: in xen 3, memory has changed to total_memory
212     result = utils.RunCmd(["xm", "info"])
213     if result.failed:
214       logging.error("Can't run 'xm info' (%s): %s", result.fail_reason,
215                     result.output)
216       return None
217
218     xmoutput = result.stdout.splitlines()
219     result = {}
220     cores_per_socket = threads_per_core = nr_cpus = None
221     for line in xmoutput:
222       splitfields = line.split(":", 1)
223
224       if len(splitfields) > 1:
225         key = splitfields[0].strip()
226         val = splitfields[1].strip()
227         if key == 'memory' or key == 'total_memory':
228           result['memory_total'] = int(val)
229         elif key == 'free_memory':
230           result['memory_free'] = int(val)
231         elif key == 'nr_cpus':
232           nr_cpus = result['cpu_total'] = int(val)
233         elif key == 'nr_nodes':
234           result['cpu_nodes'] = int(val)
235         elif key == 'cores_per_socket':
236           cores_per_socket = int(val)
237         elif key == 'threads_per_core':
238           threads_per_core = int(val)
239
240     if (cores_per_socket is not None and
241         threads_per_core is not None and nr_cpus is not None):
242       result['cpu_sockets'] = nr_cpus / (cores_per_socket * threads_per_core)
243
244     dom0_info = self.GetInstanceInfo("Domain-0")
245     if dom0_info is not None:
246       result['memory_dom0'] = dom0_info[2]
247
248     return result
249
250   @classmethod
251   def GetShellCommandForConsole(cls, instance, hvparams, beparams):
252     """Return a command for connecting to the console of an instance.
253
254     """
255     return "xm console %s" % instance.name
256
257
258   def Verify(self):
259     """Verify the hypervisor.
260
261     For Xen, this verifies that the xend process is running.
262
263     """
264     result = utils.RunCmd(["xm", "info"])
265     if result.failed:
266       return "'xm info' failed: %s" % result.fail_reason
267
268   @staticmethod
269   def _GetConfigFileDiskData(disk_template, block_devices):
270     """Get disk directive for xen config file.
271
272     This method builds the xen config disk directive according to the
273     given disk_template and block_devices.
274
275     @param disk_template: string containing instance disk template
276     @param block_devices: list of tuples (cfdev, rldev):
277         - cfdev: dict containing ganeti config disk part
278         - rldev: ganeti.bdev.BlockDev object
279
280     @return: string containing disk directive for xen instance config file
281
282     """
283     FILE_DRIVER_MAP = {
284       constants.FD_LOOP: "file",
285       constants.FD_BLKTAP: "tap:aio",
286       }
287     disk_data = []
288     if len(block_devices) > 24:
289       # 'z' - 'a' = 24
290       raise errors.HypervisorError("Too many disks")
291     # FIXME: instead of this hardcoding here, each of PVM/HVM should
292     # directly export their info (currently HVM will just sed this info)
293     namespace = ["sd" + chr(i + ord('a')) for i in range(24)]
294     for sd_name, (cfdev, dev_path) in zip(namespace, block_devices):
295       if cfdev.mode == constants.DISK_RDWR:
296         mode = "w"
297       else:
298         mode = "r"
299       if cfdev.dev_type == constants.LD_FILE:
300         line = "'%s:%s,%s,%s'" % (FILE_DRIVER_MAP[cfdev.physical_id[0]],
301                                   dev_path, sd_name, mode)
302       else:
303         line = "'phy:%s,%s,%s'" % (dev_path, sd_name, mode)
304       disk_data.append(line)
305
306     return disk_data
307
308   def MigrationInfo(self, instance):
309     """Get instance information to perform a migration.
310
311     @type instance: L{objects.Instance}
312     @param instance: instance to be migrated
313     @rtype: string
314     @return: content of the xen config file
315
316     """
317     return self._ReadConfigFile(instance.name)
318
319   def AcceptInstance(self, instance, info, target):
320     """Prepare to accept an instance.
321
322     @type instance: L{objects.Instance}
323     @param instance: instance to be accepted
324     @type info: string
325     @param info: content of the xen config file on the source node
326     @type target: string
327     @param target: target host (usually ip), on this node
328
329     """
330     pass
331
332   def FinalizeMigration(self, instance, info, success):
333     """Finalize an instance migration.
334
335     After a successful migration we write the xen config file.
336     We do nothing on a failure, as we did not change anything at accept time.
337
338     @type instance: L{objects.Instance}
339     @param instance: instance whose migration is being aborted
340     @type info: string
341     @param info: content of the xen config file on the source node
342     @type success: boolean
343     @param success: whether the migration was a success or a failure
344
345     """
346     if success:
347       self._WriteConfigFileStatic(instance.name, info)
348
349   def MigrateInstance(self, instance, target, live):
350     """Migrate an instance to a target node.
351
352     The migration will not be attempted if the instance is not
353     currently running.
354
355     @type instance: string
356     @param instance: instance name
357     @type target: string
358     @param target: ip address of the target node
359     @type live: boolean
360     @param live: perform a live migration
361
362     """
363     if self.GetInstanceInfo(instance) is None:
364       raise errors.HypervisorError("Instance not running, cannot migrate")
365     args = ["xm", "migrate"]
366     if live:
367       args.append("-l")
368     args.extend([instance, target])
369     result = utils.RunCmd(args)
370     if result.failed:
371       raise errors.HypervisorError("Failed to migrate instance %s: %s" %
372                                    (instance, result.output))
373     # remove old xen file after migration succeeded
374     try:
375       self._RemoveConfigFile(instance)
376     except EnvironmentError:
377       logging.exception("Failure while removing instance config file")
378
379
380 class XenPvmHypervisor(XenHypervisor):
381   """Xen PVM hypervisor interface"""
382
383   PARAMETERS = [
384     constants.HV_KERNEL_PATH,
385     constants.HV_INITRD_PATH,
386     constants.HV_ROOT_PATH,
387     ]
388
389   @classmethod
390   def CheckParameterSyntax(cls, hvparams):
391     """Check the given parameters for validity.
392
393     For the PVM hypervisor, this only check the existence of the
394     kernel.
395
396     @type hvparams:  dict
397     @param hvparams: dictionary with parameter names/value
398     @raise errors.HypervisorError: when a parameter is not valid
399
400     """
401     super(XenPvmHypervisor, cls).CheckParameterSyntax(hvparams)
402
403     if not hvparams[constants.HV_KERNEL_PATH]:
404       raise errors.HypervisorError("Need a kernel for the instance")
405
406     if not os.path.isabs(hvparams[constants.HV_KERNEL_PATH]):
407       raise errors.HypervisorError("The kernel path must be an absolute path")
408
409     if not hvparams[constants.HV_ROOT_PATH]:
410       raise errors.HypervisorError("Need a root partition for the instance")
411
412     if hvparams[constants.HV_INITRD_PATH]:
413       if not os.path.isabs(hvparams[constants.HV_INITRD_PATH]):
414         raise errors.HypervisorError("The initrd path must be an absolute path"
415                                      ", if defined")
416
417   def ValidateParameters(self, hvparams):
418     """Check the given parameters for validity.
419
420     For the PVM hypervisor, this only check the existence of the
421     kernel.
422
423     """
424     super(XenPvmHypervisor, self).ValidateParameters(hvparams)
425
426     kernel_path = hvparams[constants.HV_KERNEL_PATH]
427     if not os.path.isfile(kernel_path):
428       raise errors.HypervisorError("Instance kernel '%s' not found or"
429                                    " not a file" % kernel_path)
430     initrd_path = hvparams[constants.HV_INITRD_PATH]
431     if initrd_path and not os.path.isfile(initrd_path):
432       raise errors.HypervisorError("Instance initrd '%s' not found or"
433                                    " not a file" % initrd_path)
434
435   @classmethod
436   def _WriteConfigFile(cls, instance, block_devices, extra_args):
437     """Write the Xen config file for the instance.
438
439     """
440     config = StringIO()
441     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
442
443     # kernel handling
444     kpath = instance.hvparams[constants.HV_KERNEL_PATH]
445     config.write("kernel = '%s'\n" % kpath)
446
447     # initrd handling
448     initrd_path = instance.hvparams[constants.HV_INITRD_PATH]
449     if initrd_path:
450       config.write("ramdisk = '%s'\n" % initrd_path)
451
452     # rest of the settings
453     config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
454     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
455     config.write("name = '%s'\n" % instance.name)
456
457     vif_data = []
458     for nic in instance.nics:
459       nic_str = "mac=%s, bridge=%s" % (nic.mac, nic.bridge)
460       ip = getattr(nic, "ip", None)
461       if ip is not None:
462         nic_str += ", ip=%s" % ip
463       vif_data.append("'%s'" % nic_str)
464
465     config.write("vif = [%s]\n" % ",".join(vif_data))
466     config.write("disk = [%s]\n" % ",".join(
467                  cls._GetConfigFileDiskData(instance.disk_template,
468                                             block_devices)))
469
470     rpath = instance.hvparams[constants.HV_ROOT_PATH]
471     config.write("root = '%s ro'\n" % rpath)
472     config.write("on_poweroff = 'destroy'\n")
473     config.write("on_reboot = 'restart'\n")
474     config.write("on_crash = 'restart'\n")
475     if extra_args:
476       config.write("extra = '%s'\n" % extra_args)
477     # just in case it exists
478     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
479     try:
480       utils.WriteFile("/etc/xen/%s" % instance.name,
481                       data=config.getvalue())
482     except EnvironmentError, err:
483       raise errors.HypervisorError("Cannot write Xen instance confile"
484                                    " file /etc/xen/%s: %s" %
485                                    (instance.name, err))
486
487     return True
488
489
490 class XenHvmHypervisor(XenHypervisor):
491   """Xen HVM hypervisor interface"""
492
493   PARAMETERS = [
494     constants.HV_ACPI,
495     constants.HV_BOOT_ORDER,
496     constants.HV_CDROM_IMAGE_PATH,
497     constants.HV_DISK_TYPE,
498     constants.HV_NIC_TYPE,
499     constants.HV_PAE,
500     constants.HV_VNC_BIND_ADDRESS,
501     ]
502
503   @classmethod
504   def CheckParameterSyntax(cls, hvparams):
505     """Check the given parameter syntax.
506
507     """
508     super(XenHvmHypervisor, cls).CheckParameterSyntax(hvparams)
509     # boot order verification
510     boot_order = hvparams[constants.HV_BOOT_ORDER]
511     if not boot_order or len(boot_order.strip("acdn")) != 0:
512       raise errors.HypervisorError("Invalid boot order '%s' specified,"
513                                    " must be one or more of [acdn]" %
514                                    boot_order)
515     # device type checks
516     nic_type = hvparams[constants.HV_NIC_TYPE]
517     if nic_type not in constants.HT_HVM_VALID_NIC_TYPES:
518       raise errors.HypervisorError("Invalid NIC type %s specified for the Xen"
519                                    " HVM hypervisor. Please choose one of: %s"
520                                    % (nic_type,
521                                       constants.HT_HVM_VALID_NIC_TYPES))
522     disk_type = hvparams[constants.HV_DISK_TYPE]
523     if disk_type not in constants.HT_HVM_VALID_DISK_TYPES:
524       raise errors.HypervisorError("Invalid disk type %s specified for the Xen"
525                                    " HVM hypervisor. Please choose one of: %s"
526                                    % (disk_type,
527                                       constants.HT_HVM_VALID_DISK_TYPES))
528     # vnc_bind_address verification
529     vnc_bind_address = hvparams[constants.HV_VNC_BIND_ADDRESS]
530     if vnc_bind_address:
531       if not utils.IsValidIP(vnc_bind_address):
532         raise errors.OpPrereqError("given VNC bind address '%s' doesn't look"
533                                    " like a valid IP address" %
534                                    vnc_bind_address)
535
536     iso_path = hvparams[constants.HV_CDROM_IMAGE_PATH]
537     if iso_path and not os.path.isabs(iso_path):
538       raise errors.HypervisorError("The path to the HVM CDROM image must"
539                                    " be an absolute path or None, not %s" %
540                                    iso_path)
541
542   def ValidateParameters(self, hvparams):
543     """Check the given parameters for validity.
544
545     For the PVM hypervisor, this only check the existence of the
546     kernel.
547
548     @type hvparams:  dict
549     @param hvparams: dictionary with parameter names/value
550     @raise errors.HypervisorError: when a parameter is not valid
551
552     """
553     super(XenHvmHypervisor, self).ValidateParameters(hvparams)
554
555     # hvm_cdrom_image_path verification
556     iso_path = hvparams[constants.HV_CDROM_IMAGE_PATH]
557     if iso_path and not os.path.isfile(iso_path):
558       raise errors.HypervisorError("The HVM CDROM image must either be a"
559                                    " regular file or a symlink pointing to"
560                                    " an existing regular file, not %s" %
561                                    iso_path)
562
563   @classmethod
564   def _WriteConfigFile(cls, instance, block_devices, extra_args):
565     """Create a Xen 3.1 HVM config file.
566
567     """
568     config = StringIO()
569     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
570     config.write("kernel = '/usr/lib/xen/boot/hvmloader'\n")
571     config.write("builder = 'hvm'\n")
572     config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
573     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
574     config.write("name = '%s'\n" % instance.name)
575     if instance.hvparams[constants.HV_PAE]:
576       config.write("pae = 1\n")
577     else:
578       config.write("pae = 0\n")
579     if instance.hvparams[constants.HV_ACPI]:
580       config.write("acpi = 1\n")
581     else:
582       config.write("acpi = 0\n")
583     config.write("apic = 1\n")
584     arch = os.uname()[4]
585     if '64' in arch:
586       config.write("device_model = '/usr/lib64/xen/bin/qemu-dm'\n")
587     else:
588       config.write("device_model = '/usr/lib/xen/bin/qemu-dm'\n")
589     config.write("boot = '%s'\n" % instance.hvparams[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 instance.hvparams[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" %
598                    instance.hvparams["vnc_bind_address"])
599
600     if instance.network_port > constants.VNC_BASE_PORT:
601       display = instance.network_port - constants.VNC_BASE_PORT
602       config.write("vncdisplay = %s\n" % display)
603       config.write("vncunused = 0\n")
604     else:
605       config.write("# vncdisplay = 1\n")
606       config.write("vncunused = 1\n")
607
608     try:
609       password = utils.ReadFile(constants.VNC_PASSWORD_FILE)
610     except EnvironmentError, err:
611       raise errors.HypervisorError("Failed to open VNC password file %s: %s" %
612                                    (constants.VNC_PASSWORD_FILE, err))
613
614     config.write("vncpasswd = '%s'\n" % password.rstrip())
615
616     config.write("serial = 'pty'\n")
617     config.write("localtime = 1\n")
618
619     vif_data = []
620     nic_type = instance.hvparams[constants.HV_NIC_TYPE]
621     if nic_type is None:
622       # ensure old instances don't change
623       nic_type_str = ", type=ioemu"
624     elif nic_type == constants.HT_NIC_PARAVIRTUAL:
625       nic_type_str = ", type=paravirtualized"
626     else:
627       nic_type_str = ", model=%s, type=ioemu" % nic_type
628     for nic in instance.nics:
629       nic_str = "mac=%s, bridge=%s%s" % (nic.mac, nic.bridge, nic_type_str)
630       ip = getattr(nic, "ip", None)
631       if ip is not None:
632         nic_str += ", ip=%s" % ip
633       vif_data.append("'%s'" % nic_str)
634
635     config.write("vif = [%s]\n" % ",".join(vif_data))
636     disk_data = cls._GetConfigFileDiskData(instance.disk_template,
637                                             block_devices)
638     disk_type = instance.hvparams[constants.HV_DISK_TYPE]
639     if disk_type in (None, constants.HT_DISK_IOEMU):
640       replacement = ",ioemu:hd"
641     else:
642       replacement = ",hd"
643     disk_data = [line.replace(",sd", replacement) for line in disk_data]
644     iso_path = instance.hvparams[constants.HV_CDROM_IMAGE_PATH]
645     if iso_path:
646       iso = "'file:%s,hdc:cdrom,r'" % iso_path
647       disk_data.append(iso)
648
649     config.write("disk = [%s]\n" % (",".join(disk_data)))
650
651     config.write("on_poweroff = 'destroy'\n")
652     config.write("on_reboot = 'restart'\n")
653     config.write("on_crash = 'restart'\n")
654     if extra_args:
655       config.write("extra = '%s'\n" % extra_args)
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