Actually mark drives as read-only if so configured
[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.mode == constants.DISK_RDWR:
281         mode = "w"
282       else:
283         mode = "r"
284       if cfdev.dev_type == constants.LD_FILE:
285         line = "'%s:%s,%s,%s'" % (FILE_DRIVER_MAP[cfdev.physical_id[0]],
286                                   dev_path, sd_name, mode)
287       else:
288         line = "'phy:%s,%s,%s'" % (dev_path, sd_name, mode)
289       disk_data.append(line)
290
291     return disk_data
292
293   def MigrationInfo(self, instance):
294     """Get instance information to perform a migration.
295
296     @type instance: L{objects.Instance}
297     @param instance: instance to be migrated
298     @rtype: string
299     @return: content of the xen config file
300
301     """
302     return self._ReadConfigFile(instance.name)
303
304   def AcceptInstance(self, instance, info, target):
305     """Prepare to accept an instance.
306
307     @type instance: L{objects.Instance}
308     @param instance: instance to be accepted
309     @type info: string
310     @param info: content of the xen config file on the source node
311     @type target: string
312     @param target: target host (usually ip), on this node
313
314     """
315     pass
316
317   def FinalizeMigration(self, instance, info, success):
318     """Finalize an instance migration.
319
320     After a successful migration we write the xen config file.
321     We do nothing on a failure, as we did not change anything at accept time.
322
323     @type instance: L{objects.Instance}
324     @param instance: instance whose migration is being aborted
325     @type info: string
326     @param info: content of the xen config file on the source node
327     @type success: boolean
328     @param success: whether the migration was a success or a failure
329
330     """
331     if success:
332       self._WriteConfigFileStatic(instance.name, info)
333
334   def MigrateInstance(self, instance, target, live):
335     """Migrate an instance to a target node.
336
337     The migration will not be attempted if the instance is not
338     currently running.
339
340     @type instance: string
341     @param instance: instance name
342     @type target: string
343     @param target: ip address of the target node
344     @type live: boolean
345     @param live: perform a live migration
346
347     """
348     if self.GetInstanceInfo(instance) is None:
349       raise errors.HypervisorError("Instance not running, cannot migrate")
350     args = ["xm", "migrate"]
351     if live:
352       args.append("-l")
353     args.extend([instance, target])
354     result = utils.RunCmd(args)
355     if result.failed:
356       raise errors.HypervisorError("Failed to migrate instance %s: %s" %
357                                    (instance, result.output))
358     # remove old xen file after migration succeeded
359     try:
360       self._RemoveConfigFile(instance)
361     except EnvironmentError:
362       logging.exception("Failure while removing instance config file")
363
364
365 class XenPvmHypervisor(XenHypervisor):
366   """Xen PVM hypervisor interface"""
367
368   PARAMETERS = [
369     constants.HV_KERNEL_PATH,
370     constants.HV_INITRD_PATH,
371     constants.HV_ROOT_PATH,
372     ]
373
374   @classmethod
375   def CheckParameterSyntax(cls, hvparams):
376     """Check the given parameters for validity.
377
378     For the PVM hypervisor, this only check the existence of the
379     kernel.
380
381     @type hvparams:  dict
382     @param hvparams: dictionary with parameter names/value
383     @raise errors.HypervisorError: when a parameter is not valid
384
385     """
386     super(XenPvmHypervisor, cls).CheckParameterSyntax(hvparams)
387
388     if not hvparams[constants.HV_KERNEL_PATH]:
389       raise errors.HypervisorError("Need a kernel for the instance")
390
391     if not os.path.isabs(hvparams[constants.HV_KERNEL_PATH]):
392       raise errors.HypervisorError("The kernel path must be an absolute path")
393
394     if not hvparams[constants.HV_ROOT_PATH]:
395       raise errors.HypervisorError("Need a root partition for the instance")
396
397     if hvparams[constants.HV_INITRD_PATH]:
398       if not os.path.isabs(hvparams[constants.HV_INITRD_PATH]):
399         raise errors.HypervisorError("The initrd path must be an absolute path"
400                                      ", if defined")
401
402   def ValidateParameters(self, hvparams):
403     """Check the given parameters for validity.
404
405     For the PVM hypervisor, this only check the existence of the
406     kernel.
407
408     """
409     super(XenPvmHypervisor, self).ValidateParameters(hvparams)
410
411     kernel_path = hvparams[constants.HV_KERNEL_PATH]
412     if not os.path.isfile(kernel_path):
413       raise errors.HypervisorError("Instance kernel '%s' not found or"
414                                    " not a file" % kernel_path)
415     initrd_path = hvparams[constants.HV_INITRD_PATH]
416     if initrd_path and not os.path.isfile(initrd_path):
417       raise errors.HypervisorError("Instance initrd '%s' not found or"
418                                    " not a file" % initrd_path)
419
420   @classmethod
421   def _WriteConfigFile(cls, instance, block_devices, extra_args):
422     """Write the Xen config file for the instance.
423
424     """
425     config = StringIO()
426     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
427
428     # kernel handling
429     kpath = instance.hvparams[constants.HV_KERNEL_PATH]
430     config.write("kernel = '%s'\n" % kpath)
431
432     # initrd handling
433     initrd_path = instance.hvparams[constants.HV_INITRD_PATH]
434     if initrd_path:
435       config.write("ramdisk = '%s'\n" % initrd_path)
436
437     # rest of the settings
438     config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
439     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
440     config.write("name = '%s'\n" % instance.name)
441
442     vif_data = []
443     for nic in instance.nics:
444       nic_str = "mac=%s, bridge=%s" % (nic.mac, nic.bridge)
445       ip = getattr(nic, "ip", None)
446       if ip is not None:
447         nic_str += ", ip=%s" % ip
448       vif_data.append("'%s'" % nic_str)
449
450     config.write("vif = [%s]\n" % ",".join(vif_data))
451     config.write("disk = [%s]\n" % ",".join(
452                  cls._GetConfigFileDiskData(instance.disk_template,
453                                             block_devices)))
454
455     rpath = instance.hvparams[constants.HV_ROOT_PATH]
456     config.write("root = '%s ro'\n" % rpath)
457     config.write("on_poweroff = 'destroy'\n")
458     config.write("on_reboot = 'restart'\n")
459     config.write("on_crash = 'restart'\n")
460     if extra_args:
461       config.write("extra = '%s'\n" % extra_args)
462     # just in case it exists
463     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
464     try:
465       utils.WriteFile("/etc/xen/%s" % instance.name,
466                       data=config.getvalue())
467     except EnvironmentError, err:
468       raise errors.HypervisorError("Cannot write Xen instance confile"
469                                    " file /etc/xen/%s: %s" %
470                                    (instance.name, err))
471
472     return True
473
474
475 class XenHvmHypervisor(XenHypervisor):
476   """Xen HVM hypervisor interface"""
477
478   PARAMETERS = [
479     constants.HV_ACPI,
480     constants.HV_BOOT_ORDER,
481     constants.HV_CDROM_IMAGE_PATH,
482     constants.HV_DISK_TYPE,
483     constants.HV_NIC_TYPE,
484     constants.HV_PAE,
485     constants.HV_VNC_BIND_ADDRESS,
486     ]
487
488   @classmethod
489   def CheckParameterSyntax(cls, hvparams):
490     """Check the given parameter syntax.
491
492     """
493     super(XenHvmHypervisor, cls).CheckParameterSyntax(hvparams)
494     # boot order verification
495     boot_order = hvparams[constants.HV_BOOT_ORDER]
496     if len(boot_order.strip("acdn")) != 0:
497       raise errors.HypervisorError("Invalid boot order '%s' specified,"
498                                    " must be one or more of [acdn]" %
499                                    boot_order)
500     # device type checks
501     nic_type = hvparams[constants.HV_NIC_TYPE]
502     if nic_type not in constants.HT_HVM_VALID_NIC_TYPES:
503       raise errors.HypervisorError("Invalid NIC type %s specified for Xen HVM"
504                                    " hypervisor" % nic_type)
505     disk_type = hvparams[constants.HV_DISK_TYPE]
506     if disk_type not in constants.HT_HVM_VALID_DISK_TYPES:
507       raise errors.HypervisorError("Invalid disk type %s specified for Xen HVM"
508                                    " hypervisor" % disk_type)
509     # vnc_bind_address verification
510     vnc_bind_address = hvparams[constants.HV_VNC_BIND_ADDRESS]
511     if vnc_bind_address is not None:
512       if not utils.IsValidIP(vnc_bind_address):
513         raise errors.OpPrereqError("given VNC bind address '%s' doesn't look"
514                                    " like a valid IP address" %
515                                    vnc_bind_address)
516
517     iso_path = hvparams[constants.HV_CDROM_IMAGE_PATH]
518     if iso_path and not os.path.isabs(iso_path):
519       raise errors.HypervisorError("The path to the HVM CDROM image must"
520                                    " be an absolute path or None, not %s" %
521                                    iso_path)
522
523   def ValidateParameters(self, hvparams):
524     """Check the given parameters for validity.
525
526     For the PVM hypervisor, this only check the existence of the
527     kernel.
528
529     @type hvparams:  dict
530     @param hvparams: dictionary with parameter names/value
531     @raise errors.HypervisorError: when a parameter is not valid
532
533     """
534     super(XenHvmHypervisor, self).ValidateParameters(hvparams)
535
536     # hvm_cdrom_image_path verification
537     iso_path = hvparams[constants.HV_CDROM_IMAGE_PATH]
538     if iso_path and not os.path.isfile(iso_path):
539       raise errors.HypervisorError("The HVM CDROM image must either be a"
540                                    " regular file or a symlink pointing to"
541                                    " an existing regular file, not %s" %
542                                    iso_path)
543
544   @classmethod
545   def _WriteConfigFile(cls, instance, block_devices, extra_args):
546     """Create a Xen 3.1 HVM config file.
547
548     """
549     config = StringIO()
550     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
551     config.write("kernel = '/usr/lib/xen/boot/hvmloader'\n")
552     config.write("builder = 'hvm'\n")
553     config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
554     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
555     config.write("name = '%s'\n" % instance.name)
556     if instance.hvparams[constants.HV_PAE]:
557       config.write("pae = 1\n")
558     else:
559       config.write("pae = 0\n")
560     if instance.hvparams[constants.HV_ACPI]:
561       config.write("acpi = 1\n")
562     else:
563       config.write("acpi = 0\n")
564     config.write("apic = 1\n")
565     arch = os.uname()[4]
566     if '64' in arch:
567       config.write("device_model = '/usr/lib64/xen/bin/qemu-dm'\n")
568     else:
569       config.write("device_model = '/usr/lib/xen/bin/qemu-dm'\n")
570     if instance.hvparams[constants.HV_BOOT_ORDER] is None:
571       config.write("boot = '%s'\n" % constants.HT_HVM_DEFAULT_BOOT_ORDER)
572     else:
573       config.write("boot = '%s'\n" % instance.hvparams["boot_order"])
574     config.write("sdl = 0\n")
575     config.write("usb = 1\n")
576     config.write("usbdevice = 'tablet'\n")
577     config.write("vnc = 1\n")
578     if instance.hvparams[constants.HV_VNC_BIND_ADDRESS] is None:
579       config.write("vnclisten = '%s'\n" % constants.VNC_DEFAULT_BIND_ADDRESS)
580     else:
581       config.write("vnclisten = '%s'\n" %
582                    instance.hvparams["vnc_bind_address"])
583
584     if instance.network_port > constants.HT_HVM_VNC_BASE_PORT:
585       display = instance.network_port - constants.HT_HVM_VNC_BASE_PORT
586       config.write("vncdisplay = %s\n" % display)
587       config.write("vncunused = 0\n")
588     else:
589       config.write("# vncdisplay = 1\n")
590       config.write("vncunused = 1\n")
591
592     try:
593       password = utils.ReadFile(constants.VNC_PASSWORD_FILE)
594     except EnvironmentError, err:
595       raise errors.HypervisorError("Failed to open VNC password file %s: %s" %
596                                    (constants.VNC_PASSWORD_FILE, err))
597
598     config.write("vncpasswd = '%s'\n" % password.rstrip())
599
600     config.write("serial = 'pty'\n")
601     config.write("localtime = 1\n")
602
603     vif_data = []
604     nic_type = instance.hvparams[constants.HV_NIC_TYPE]
605     if nic_type is None:
606       # ensure old instances don't change
607       nic_type_str = ", type=ioemu"
608     elif nic_type == constants.HT_HVM_DEV_PARAVIRTUAL:
609       nic_type_str = ", type=paravirtualized"
610     else:
611       nic_type_str = ", model=%s, type=ioemu" % nic_type
612     for nic in instance.nics:
613       nic_str = "mac=%s, bridge=%s%s" % (nic.mac, nic.bridge, nic_type_str)
614       ip = getattr(nic, "ip", None)
615       if ip is not None:
616         nic_str += ", ip=%s" % ip
617       vif_data.append("'%s'" % nic_str)
618
619     config.write("vif = [%s]\n" % ",".join(vif_data))
620     disk_data = cls._GetConfigFileDiskData(instance.disk_template,
621                                             block_devices)
622     disk_type = instance.hvparams[constants.HV_DISK_TYPE]
623     if disk_type in (None, constants.HT_HVM_DEV_IOEMU):
624       replacement = ",ioemu:hd"
625     else:
626       replacement = ",hd"
627     disk_data = [line.replace(",sd", replacement) for line in disk_data]
628     iso_path = instance.hvparams[constants.HV_CDROM_IMAGE_PATH]
629     if iso_path:
630       iso = "'file:%s,hdc:cdrom,r'" % iso_path
631       disk_data.append(iso)
632
633     config.write("disk = [%s]\n" % (",".join(disk_data)))
634
635     config.write("on_poweroff = 'destroy'\n")
636     config.write("on_reboot = 'restart'\n")
637     config.write("on_crash = 'restart'\n")
638     if extra_args:
639       config.write("extra = '%s'\n" % extra_args)
640     # just in case it exists
641     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
642     try:
643       utils.WriteFile("/etc/xen/%s" % instance.name,
644                       data=config.getvalue())
645     except EnvironmentError, err:
646       raise errors.HypervisorError("Cannot write Xen instance confile"
647                                    " file /etc/xen/%s: %s" %
648                                    (instance.name, err))
649
650     return True