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