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