Fix exception handling for lock timeout in RAPI
[ganeti-local] / lib / hypervisor.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007 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 """Module that abstracts the virtualisation interface
23
24 """
25
26 import time
27 import os
28 import re
29 from cStringIO import StringIO
30
31 from ganeti import utils
32 from ganeti import logger
33 from ganeti import ssconf
34 from ganeti import constants
35 from ganeti import errors
36 from ganeti.errors import HypervisorError
37
38
39 def GetHypervisor():
40   """Return a Hypervisor instance.
41
42   This function parses the cluster hypervisor configuration file and
43   instantiates a class based on the value of this file.
44
45   """
46   ht_kind = ssconf.SimpleStore().GetHypervisorType()
47   if ht_kind == constants.HT_XEN_PVM30:
48     cls = XenPvmHypervisor
49   elif ht_kind == constants.HT_FAKE:
50     cls = FakeHypervisor
51   elif ht_kind == constants.HT_XEN_HVM31:
52     cls = XenHvmHypervisor
53   else:
54     raise HypervisorError("Unknown hypervisor type '%s'" % ht_kind)
55   return cls()
56
57
58 class BaseHypervisor(object):
59   """Abstract virtualisation technology interface
60
61   The goal is that all aspects of the virtualisation technology must
62   be abstracted away from the rest of code.
63
64   """
65   def __init__(self):
66     pass
67
68   def StartInstance(self, instance, block_devices, extra_args):
69     """Start an instance."""
70     raise NotImplementedError
71
72   def StopInstance(self, instance, force=False):
73     """Stop an instance."""
74     raise NotImplementedError
75
76   def RebootInstance(self, instance):
77     """Reboot an instance."""
78     raise NotImplementedError
79
80   def ListInstances(self):
81     """Get the list of running instances."""
82     raise NotImplementedError
83
84   def GetInstanceInfo(self, instance_name):
85     """Get instance properties.
86
87     Args:
88       instance_name: the instance name
89
90     Returns:
91       (name, id, memory, vcpus, state, times)
92
93     """
94     raise NotImplementedError
95
96   def GetAllInstancesInfo(self):
97     """Get properties of all instances.
98
99     Returns:
100       [(name, id, memory, vcpus, stat, times),...]
101     """
102     raise NotImplementedError
103
104   def GetNodeInfo(self):
105     """Return information about the node.
106
107     The return value is a dict, which has to have the following items:
108       (all values in MiB)
109       - memory_total: the total memory size on the node
110       - memory_free: the available memory on the node for instances
111       - memory_dom0: the memory used by the node itself, if available
112
113     """
114     raise NotImplementedError
115
116   @staticmethod
117   def GetShellCommandForConsole(instance):
118     """Return a command for connecting to the console of an instance.
119
120     """
121     raise NotImplementedError
122
123   def Verify(self):
124     """Verify the hypervisor.
125
126     """
127     raise NotImplementedError
128
129
130 class XenHypervisor(BaseHypervisor):
131   """Xen generic hypervisor interface
132
133   This is the Xen base class used for both Xen PVM and HVM. It contains
134   all the functionality that is identical for both.
135
136   """
137
138   @staticmethod
139   def _WriteConfigFile(instance, block_devices, extra_args):
140     """Write the Xen config file for the instance.
141
142     """
143     raise NotImplementedError
144
145   @staticmethod
146   def _RemoveConfigFile(instance):
147     """Remove the xen configuration file.
148
149     """
150     utils.RemoveFile("/etc/xen/%s" % instance.name)
151
152   @staticmethod
153   def _GetXMList(include_node):
154     """Return the list of running instances.
155
156     If the `include_node` argument is True, then we return information
157     for dom0 also, otherwise we filter that from the return value.
158
159     The return value is a list of (name, id, memory, vcpus, state, time spent)
160
161     """
162     for dummy in range(5):
163       result = utils.RunCmd(["xm", "list"])
164       if not result.failed:
165         break
166       logger.Error("xm list failed (%s): %s" % (result.fail_reason,
167                                                 result.output))
168       time.sleep(1)
169
170     if result.failed:
171       raise HypervisorError("xm list failed, retries exceeded (%s): %s" %
172                             (result.fail_reason, result.stderr))
173
174     # skip over the heading
175     lines = result.stdout.splitlines()[1:]
176     result = []
177     for line in lines:
178       # The format of lines is:
179       # Name      ID Mem(MiB) VCPUs State  Time(s)
180       # Domain-0   0  3418     4 r-----    266.2
181       data = line.split()
182       if len(data) != 6:
183         raise HypervisorError("Can't parse output of xm list, line: %s" % line)
184       try:
185         data[1] = int(data[1])
186         data[2] = int(data[2])
187         data[3] = int(data[3])
188         data[5] = float(data[5])
189       except ValueError, err:
190         raise HypervisorError("Can't parse output of xm list,"
191                               " line: %s, error: %s" % (line, err))
192
193       # skip the Domain-0 (optional)
194       if include_node or data[0] != 'Domain-0':
195         result.append(data)
196
197     return result
198
199   def ListInstances(self):
200     """Get the list of running instances.
201
202     """
203     xm_list = self._GetXMList(False)
204     names = [info[0] for info in xm_list]
205     return names
206
207   def GetInstanceInfo(self, instance_name):
208     """Get instance properties.
209
210     Args:
211       instance_name: the instance name
212
213     Returns:
214       (name, id, memory, vcpus, stat, times)
215     """
216     xm_list = self._GetXMList(instance_name=="Domain-0")
217     result = None
218     for data in xm_list:
219       if data[0] == instance_name:
220         result = data
221         break
222     return result
223
224   def GetAllInstancesInfo(self):
225     """Get properties of all instances.
226
227     Returns:
228       [(name, id, memory, vcpus, stat, times),...]
229     """
230     xm_list = self._GetXMList(False)
231     return xm_list
232
233   def StartInstance(self, instance, block_devices, extra_args):
234     """Start an instance."""
235     self._WriteConfigFile(instance, block_devices, extra_args)
236     result = utils.RunCmd(["xm", "create", instance.name])
237
238     if result.failed:
239       raise HypervisorError("Failed to start instance %s: %s (%s)" %
240                             (instance.name, result.fail_reason, result.output))
241
242   def StopInstance(self, instance, force=False):
243     """Stop an instance."""
244     self._RemoveConfigFile(instance)
245     if force:
246       command = ["xm", "destroy", instance.name]
247     else:
248       command = ["xm", "shutdown", instance.name]
249     result = utils.RunCmd(command)
250
251     if result.failed:
252       raise HypervisorError("Failed to stop instance %s: %s" %
253                             (instance.name, result.fail_reason))
254
255   def RebootInstance(self, instance):
256     """Reboot an instance."""
257     result = utils.RunCmd(["xm", "reboot", instance.name])
258
259     if result.failed:
260       raise HypervisorError("Failed to reboot instance %s: %s" %
261                             (instance.name, result.fail_reason))
262
263   def GetNodeInfo(self):
264     """Return information about the node.
265
266     The return value is a dict, which has to have the following items:
267       (all values in MiB)
268       - memory_total: the total memory size on the node
269       - memory_free: the available memory on the node for instances
270       - memory_dom0: the memory used by the node itself, if available
271
272     """
273     # note: in xen 3, memory has changed to total_memory
274     result = utils.RunCmd(["xm", "info"])
275     if result.failed:
276       logger.Error("Can't run 'xm info': %s" % result.fail_reason)
277       return None
278
279     xmoutput = result.stdout.splitlines()
280     result = {}
281     for line in xmoutput:
282       splitfields = line.split(":", 1)
283
284       if len(splitfields) > 1:
285         key = splitfields[0].strip()
286         val = splitfields[1].strip()
287         if key == 'memory' or key == 'total_memory':
288           result['memory_total'] = int(val)
289         elif key == 'free_memory':
290           result['memory_free'] = int(val)
291         elif key == 'nr_cpus':
292           result['cpu_total'] = int(val)
293     dom0_info = self.GetInstanceInfo("Domain-0")
294     if dom0_info is not None:
295       result['memory_dom0'] = dom0_info[2]
296
297     return result
298
299   @staticmethod
300   def GetShellCommandForConsole(instance):
301     """Return a command for connecting to the console of an instance.
302
303     """
304     raise NotImplementedError
305
306
307   def Verify(self):
308     """Verify the hypervisor.
309
310     For Xen, this verifies that the xend process is running.
311
312     """
313     if not utils.CheckDaemonAlive('/var/run/xend.pid', 'xend'):
314       return "xend daemon is not running"
315
316
317 class XenPvmHypervisor(XenHypervisor):
318   """Xen PVM hypervisor interface"""
319
320   @staticmethod
321   def _WriteConfigFile(instance, block_devices, extra_args):
322     """Write the Xen config file for the instance.
323
324     """
325     config = StringIO()
326     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
327
328     # kernel handling
329     if instance.kernel_path in (None, constants.VALUE_DEFAULT):
330       kpath = constants.XEN_KERNEL
331     else:
332       if not os.path.exists(instance.kernel_path):
333         raise errors.HypervisorError("The kernel %s for instance %s is"
334                                      " missing" % (instance.kernel_path,
335                                                    instance.name))
336       kpath = instance.kernel_path
337     config.write("kernel = '%s'\n" % kpath)
338
339     # initrd handling
340     if instance.initrd_path in (None, constants.VALUE_DEFAULT):
341       if os.path.exists(constants.XEN_INITRD):
342         initrd_path = constants.XEN_INITRD
343       else:
344         initrd_path = None
345     elif instance.initrd_path == constants.VALUE_NONE:
346       initrd_path = None
347     else:
348       if not os.path.exists(instance.initrd_path):
349         raise errors.HypervisorError("The initrd %s for instance %s is"
350                                      " missing" % (instance.initrd_path,
351                                                    instance.name))
352       initrd_path = instance.initrd_path
353
354     if initrd_path:
355       config.write("ramdisk = '%s'\n" % initrd_path)
356
357     # rest of the settings
358     config.write("memory = %d\n" % instance.memory)
359     config.write("vcpus = %d\n" % instance.vcpus)
360     config.write("name = '%s'\n" % instance.name)
361
362     vif_data = []
363     for nic in instance.nics:
364       nic_str = "mac=%s, bridge=%s" % (nic.mac, nic.bridge)
365       ip = getattr(nic, "ip", None)
366       if ip is not None:
367         nic_str += ", ip=%s" % ip
368       vif_data.append("'%s'" % nic_str)
369
370     config.write("vif = [%s]\n" % ",".join(vif_data))
371
372     disk_data = ["'phy:%s,%s,w'" % (rldev.dev_path, cfdev.iv_name)
373                  for cfdev, rldev in block_devices]
374     config.write("disk = [%s]\n" % ",".join(disk_data))
375
376     config.write("root = '/dev/sda ro'\n")
377     config.write("on_poweroff = 'destroy'\n")
378     config.write("on_reboot = 'restart'\n")
379     config.write("on_crash = 'restart'\n")
380     if extra_args:
381       config.write("extra = '%s'\n" % extra_args)
382     # just in case it exists
383     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
384     try:
385       f = open("/etc/xen/%s" % instance.name, "w")
386       try:
387         f.write(config.getvalue())
388       finally:
389         f.close()
390     except IOError, err:
391       raise errors.OpExecError("Cannot write Xen instance confile"
392                                " file /etc/xen/%s: %s" % (instance.name, err))
393     return True
394
395   @staticmethod
396   def GetShellCommandForConsole(instance):
397     """Return a command for connecting to the console of an instance.
398
399     """
400     return "xm console %s" % instance.name
401
402
403 class FakeHypervisor(BaseHypervisor):
404   """Fake hypervisor interface.
405
406   This can be used for testing the ganeti code without having to have
407   a real virtualisation software installed.
408
409   """
410   _ROOT_DIR = constants.RUN_DIR + "/ganeti-fake-hypervisor"
411
412   def __init__(self):
413     BaseHypervisor.__init__(self)
414     if not os.path.exists(self._ROOT_DIR):
415       os.mkdir(self._ROOT_DIR)
416
417   def ListInstances(self):
418     """Get the list of running instances.
419
420     """
421     return os.listdir(self._ROOT_DIR)
422
423   def GetInstanceInfo(self, instance_name):
424     """Get instance properties.
425
426     Args:
427       instance_name: the instance name
428
429     Returns:
430       (name, id, memory, vcpus, stat, times)
431     """
432     file_name = "%s/%s" % (self._ROOT_DIR, instance_name)
433     if not os.path.exists(file_name):
434       return None
435     try:
436       fh = file(file_name, "r")
437       try:
438         inst_id = fh.readline().strip()
439         memory = fh.readline().strip()
440         vcpus = fh.readline().strip()
441         stat = "---b-"
442         times = "0"
443         return (instance_name, inst_id, memory, vcpus, stat, times)
444       finally:
445         fh.close()
446     except IOError, err:
447       raise HypervisorError("Failed to list instance %s: %s" %
448                             (instance_name, err))
449
450   def GetAllInstancesInfo(self):
451     """Get properties of all instances.
452
453     Returns:
454       [(name, id, memory, vcpus, stat, times),...]
455     """
456     data = []
457     for file_name in os.listdir(self._ROOT_DIR):
458       try:
459         fh = file(self._ROOT_DIR+"/"+file_name, "r")
460         inst_id = "-1"
461         memory = "0"
462         stat = "-----"
463         times = "-1"
464         try:
465           inst_id = fh.readline().strip()
466           memory = fh.readline().strip()
467           vcpus = fh.readline().strip()
468           stat = "---b-"
469           times = "0"
470         finally:
471           fh.close()
472         data.append((file_name, inst_id, memory, vcpus, stat, times))
473       except IOError, err:
474         raise HypervisorError("Failed to list instances: %s" % err)
475     return data
476
477   def StartInstance(self, instance, force, extra_args):
478     """Start an instance.
479
480     For the fake hypervisor, it just creates a file in the base dir,
481     creating an exception if it already exists. We don't actually
482     handle race conditions properly, since these are *FAKE* instances.
483
484     """
485     file_name = self._ROOT_DIR + "/%s" % instance.name
486     if os.path.exists(file_name):
487       raise HypervisorError("Failed to start instance %s: %s" %
488                             (instance.name, "already running"))
489     try:
490       fh = file(file_name, "w")
491       try:
492         fh.write("0\n%d\n%d\n" % (instance.memory, instance.vcpus))
493       finally:
494         fh.close()
495     except IOError, err:
496       raise HypervisorError("Failed to start instance %s: %s" %
497                             (instance.name, err))
498
499   def StopInstance(self, instance, force=False):
500     """Stop an instance.
501
502     For the fake hypervisor, this just removes the file in the base
503     dir, if it exist, otherwise we raise an exception.
504
505     """
506     file_name = self._ROOT_DIR + "/%s" % instance.name
507     if not os.path.exists(file_name):
508       raise HypervisorError("Failed to stop instance %s: %s" %
509                             (instance.name, "not running"))
510     utils.RemoveFile(file_name)
511
512   def RebootInstance(self, instance):
513     """Reboot an instance.
514
515     For the fake hypervisor, this does nothing.
516
517     """
518     return
519
520   def GetNodeInfo(self):
521     """Return information about the node.
522
523     The return value is a dict, which has to have the following items:
524       (all values in MiB)
525       - memory_total: the total memory size on the node
526       - memory_free: the available memory on the node for instances
527       - memory_dom0: the memory used by the node itself, if available
528
529     """
530     # global ram usage from the xm info command
531     # memory                 : 3583
532     # free_memory            : 747
533     # note: in xen 3, memory has changed to total_memory
534     try:
535       fh = file("/proc/meminfo")
536       try:
537         data = fh.readlines()
538       finally:
539         fh.close()
540     except IOError, err:
541       raise HypervisorError("Failed to list node info: %s" % err)
542
543     result = {}
544     sum_free = 0
545     for line in data:
546       splitfields = line.split(":", 1)
547
548       if len(splitfields) > 1:
549         key = splitfields[0].strip()
550         val = splitfields[1].strip()
551         if key == 'MemTotal':
552           result['memory_total'] = int(val.split()[0])/1024
553         elif key in ('MemFree', 'Buffers', 'Cached'):
554           sum_free += int(val.split()[0])/1024
555         elif key == 'Active':
556           result['memory_dom0'] = int(val.split()[0])/1024
557     result['memory_free'] = sum_free
558
559     cpu_total = 0
560     try:
561       fh = open("/proc/cpuinfo")
562       try:
563         cpu_total = len(re.findall("(?m)^processor\s*:\s*[0-9]+\s*$",
564                                    fh.read()))
565       finally:
566         fh.close()
567     except EnvironmentError, err:
568       raise HypervisorError("Failed to list node info: %s" % err)
569     result['cpu_total'] = cpu_total
570
571     return result
572
573   @staticmethod
574   def GetShellCommandForConsole(instance):
575     """Return a command for connecting to the console of an instance.
576
577     """
578     return "echo Console not available for fake hypervisor"
579
580   def Verify(self):
581     """Verify the hypervisor.
582
583     For the fake hypervisor, it just checks the existence of the base
584     dir.
585
586     """
587     if not os.path.exists(self._ROOT_DIR):
588       return "The required directory '%s' does not exist." % self._ROOT_DIR
589
590
591 class XenHvmHypervisor(XenHypervisor):
592   """Xen HVM hypervisor interface"""
593
594   @staticmethod
595   def _WriteConfigFile(instance, block_devices, extra_args):
596     """Create a Xen 3.1 HVM config file.
597
598     """
599     config = StringIO()
600     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
601     config.write("kernel = '/usr/lib/xen/boot/hvmloader'\n")
602     config.write("builder = 'hvm'\n")
603     config.write("memory = %d\n" % instance.memory)
604     config.write("vcpus = %d\n" % instance.vcpus)
605     config.write("name = '%s'\n" % instance.name)
606     if instance.hvm_pae:
607       config.write("pae = 1\n")
608     else:
609       config.write("pae = 0\n")
610     if instance.hvm_acpi:
611       config.write("acpi = 1\n")
612     else:
613       config.write("acpi = 0\n")
614     config.write("apic = 1\n")
615     arch = os.uname()[4]
616     if '64' in arch:
617       config.write("device_model = '/usr/lib64/xen/bin/qemu-dm'\n")
618     else:
619       config.write("device_model = '/usr/lib/xen/bin/qemu-dm'\n")
620     if instance.hvm_boot_order is None:
621       config.write("boot = '%s'\n" % constants.HT_HVM_DEFAULT_BOOT_ORDER)
622     else:
623       config.write("boot = '%s'\n" % instance.hvm_boot_order)
624     config.write("sdl = 0\n")
625     config.write("usb = 1\n");
626     config.write("usbdevice = 'tablet'\n");
627     config.write("vnc = 1\n")
628     config.write("vnclisten = '%s'\n" % instance.vnc_bind_address)
629
630     if instance.network_port > constants.HT_HVM_VNC_BASE_PORT:
631       display = instance.network_port - constants.HT_HVM_VNC_BASE_PORT
632       config.write("vncdisplay = %s\n" % display)
633       config.write("vncunused = 0\n")
634     else:
635       config.write("# vncdisplay = 1\n")
636       config.write("vncunused = 1\n")
637
638     try:
639       password_file = open(constants.VNC_PASSWORD_FILE, "r")
640       try:
641         password = password_file.readline()
642       finally:
643         password_file.close()
644     except IOError:
645       raise errors.OpExecError("failed to open VNC password file %s " %
646                                constants.VNC_PASSWORD_FILE)
647
648     config.write("vncpasswd = '%s'\n" % password.rstrip())
649
650     config.write("serial = 'pty'\n")
651     config.write("localtime = 1\n")
652
653     vif_data = []
654     for nic in instance.nics:
655       nic_str = "mac=%s, bridge=%s, type=ioemu" % (nic.mac, nic.bridge)
656       ip = getattr(nic, "ip", None)
657       if ip is not None:
658         nic_str += ", ip=%s" % ip
659       vif_data.append("'%s'" % nic_str)
660
661     config.write("vif = [%s]\n" % ",".join(vif_data))
662
663     disk_data = ["'phy:%s,%s,w'" %
664                  (rldev.dev_path, cfdev.iv_name.replace("sd", "ioemu:hd"))
665                  for cfdev, rldev in block_devices]
666
667     if instance.hvm_cdrom_image_path is None:
668       config.write("disk = [%s]\n" % (",".join(disk_data)))
669     else:
670       iso = "'file:%s,hdc:cdrom,r'" % (instance.hvm_cdrom_image_path)
671       config.write("disk = [%s, %s]\n" % (",".join(disk_data), iso))
672
673     config.write("on_poweroff = 'destroy'\n")
674     config.write("on_reboot = 'restart'\n")
675     config.write("on_crash = 'restart'\n")
676     if extra_args:
677       config.write("extra = '%s'\n" % extra_args)
678     # just in case it exists
679     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
680     try:
681       f = open("/etc/xen/%s" % instance.name, "w")
682       try:
683         f.write(config.getvalue())
684       finally:
685         f.close()
686     except IOError, err:
687       raise errors.OpExecError("Cannot write Xen instance confile"
688                                " file /etc/xen/%s: %s" % (instance.name, err))
689     return True
690
691   @staticmethod
692   def GetShellCommandForConsole(instance):
693     """Return a command for connecting to the console of an instance.
694
695     """
696     if instance.network_port is None:
697       raise errors.OpExecError("no console port defined for %s"
698                                % instance.name)
699     elif instance.vnc_bind_address == constants.BIND_ADDRESS_GLOBAL:
700       raise errors.OpExecError("no PTY console, connect to %s:%s via VNC"
701                                % (instance.primary_node,
702                                   instance.network_port))
703     else:
704       raise errors.OpExecError("no PTY console, connect to %s:%s via VNC"
705                                % (instance.vnc_bind_address,
706                                   instance.network_port))