Update the dev_path on LVs on rename
[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.errors import HypervisorError
35
36 _HT_XEN30 = "xen-3.0"
37 _HT_FAKE = "fake"
38
39 VALID_HTYPES = (_HT_XEN30, _HT_FAKE)
40
41 def GetHypervisor():
42   """Return a Hypervisor instance.
43
44   This function parses the cluster hypervisor configuration file and
45   instantiates a class based on the value of this file.
46
47   """
48   ht_kind = ssconf.SimpleStore().GetHypervisorType()
49   if ht_kind == _HT_XEN30:
50     cls = XenHypervisor
51   elif ht_kind == _HT_FAKE:
52     cls = FakeHypervisor
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_name):
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 hypervisor interface"""
132
133   @staticmethod
134   def _WriteConfigFile(instance, block_devices, extra_args):
135     """Create a Xen 3.0 config file.
136
137     """
138     config = StringIO()
139     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
140     config.write("kernel = '%s'\n" % constants.XEN_KERNEL)
141     if os.path.exists(constants.XEN_INITRD):
142       config.write("ramdisk = '%s'\n" % constants.XEN_INITRD)
143     config.write("memory = %d\n" % instance.memory)
144     config.write("vcpus = %d\n" % instance.vcpus)
145     config.write("name = '%s'\n" % instance.name)
146
147     vif_data = []
148     for nic in instance.nics:
149       nic_str = "mac=%s, bridge=%s" % (nic.mac, nic.bridge)
150       ip = getattr(nic, "ip", None)
151       if ip is not None:
152         nic_str += ", ip=%s" % ip
153       vif_data.append("'%s'" % nic_str)
154
155     config.write("vif = [%s]\n" % ",".join(vif_data))
156
157     disk_data = ["'phy:%s,%s,w'" % (rldev.dev_path, cfdev.iv_name)
158                  for cfdev, rldev in block_devices]
159     config.write("disk = [%s]\n" % ",".join(disk_data))
160
161     config.write("root = '/dev/sda ro'\n")
162     config.write("on_poweroff = 'destroy'\n")
163     config.write("on_reboot = 'restart'\n")
164     config.write("on_crash = 'restart'\n")
165     if extra_args:
166       config.write("extra = '%s'\n" % extra_args)
167     # just in case it exists
168     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
169     f = open("/etc/xen/%s" % instance.name, "w")
170     f.write(config.getvalue())
171     f.close()
172     return True
173
174   @staticmethod
175   def _RemoveConfigFile(instance):
176     """Remove the xen configuration file.
177
178     """
179     utils.RemoveFile("/etc/xen/%s" % instance.name)
180
181   @staticmethod
182   def _GetXMList(include_node):
183     """Return the list of running instances.
184
185     If the `include_node` argument is True, then we return information
186     for dom0 also, otherwise we filter that from the return value.
187
188     The return value is a list of (name, id, memory, vcpus, state, time spent)
189
190     """
191     for dummy in range(5):
192       result = utils.RunCmd(["xm", "list"])
193       if not result.failed:
194         break
195       logger.Error("xm list failed (%s): %s" % (result.fail_reason,
196                                                 result.output))
197       time.sleep(1)
198
199     if result.failed:
200       raise HypervisorError("xm list failed, retries exceeded (%s): %s" %
201                             (result.fail_reason, result.stderr))
202
203     # skip over the heading and the domain 0 line (optional)
204     if include_node:
205       to_skip = 1
206     else:
207       to_skip = 2
208     lines = result.stdout.splitlines()[to_skip:]
209     result = []
210     for line in lines:
211       # The format of lines is:
212       # Name      ID Mem(MiB) VCPUs State  Time(s)
213       # Domain-0   0  3418     4 r-----    266.2
214       data = line.split()
215       if len(data) != 6:
216         raise HypervisorError("Can't parse output of xm list, line: %s" % line)
217       try:
218         data[1] = int(data[1])
219         data[2] = int(data[2])
220         data[3] = int(data[3])
221         data[5] = float(data[5])
222       except ValueError, err:
223         raise HypervisorError("Can't parse output of xm list,"
224                               " line: %s, error: %s" % (line, err))
225       result.append(data)
226     return result
227
228   def ListInstances(self):
229     """Get the list of running instances.
230
231     """
232     xm_list = self._GetXMList(False)
233     names = [info[0] for info in xm_list]
234     return names
235
236   def GetInstanceInfo(self, instance_name):
237     """Get instance properties.
238
239     Args:
240       instance_name: the instance name
241
242     Returns:
243       (name, id, memory, vcpus, stat, times)
244     """
245     xm_list = self._GetXMList(instance_name=="Domain-0")
246     result = None
247     for data in xm_list:
248       if data[0] == instance_name:
249         result = data
250         break
251     return result
252
253   def GetAllInstancesInfo(self):
254     """Get properties of all instances.
255
256     Returns:
257       [(name, id, memory, vcpus, stat, times),...]
258     """
259     xm_list = self._GetXMList(False)
260     return xm_list
261
262   def StartInstance(self, instance, block_devices, extra_args):
263     """Start an instance."""
264     self._WriteConfigFile(instance, block_devices, extra_args)
265     result = utils.RunCmd(["xm", "create", instance.name])
266
267     if result.failed:
268       raise HypervisorError("Failed to start instance %s: %s" %
269                             (instance.name, result.fail_reason))
270
271   def StopInstance(self, instance, force=False):
272     """Stop an instance."""
273     self._RemoveConfigFile(instance)
274     if force:
275       command = ["xm", "destroy", instance.name]
276     else:
277       command = ["xm", "shutdown", instance.name]
278     result = utils.RunCmd(command)
279
280     if result.failed:
281       raise HypervisorError("Failed to stop instance %s: %s" %
282                             (instance.name, result.fail_reason))
283
284   def RebootInstance(self, instance):
285     """Reboot an instance."""
286     result = utils.RunCmd(["xm", "reboot", instance.name])
287
288     if result.failed:
289       raise HypervisorError("Failed to reboot instance %s: %s" %
290                             (instance.name, result.fail_reason))
291
292   def GetNodeInfo(self):
293     """Return information about the node.
294
295     The return value is a dict, which has to have the following items:
296       (all values in MiB)
297       - memory_total: the total memory size on the node
298       - memory_free: the available memory on the node for instances
299       - memory_dom0: the memory used by the node itself, if available
300
301     """
302     # note: in xen 3, memory has changed to total_memory
303     result = utils.RunCmd(["xm", "info"])
304     if result.failed:
305       logger.Error("Can't run 'xm info': %s" % result.fail_reason)
306       return None
307
308     xmoutput = result.stdout.splitlines()
309     result = {}
310     for line in xmoutput:
311       splitfields = line.split(":", 1)
312
313       if len(splitfields) > 1:
314         key = splitfields[0].strip()
315         val = splitfields[1].strip()
316         if key == 'memory' or key == 'total_memory':
317           result['memory_total'] = int(val)
318         elif key == 'free_memory':
319           result['memory_free'] = int(val)
320     dom0_info = self.GetInstanceInfo("Domain-0")
321     if dom0_info is not None:
322       result['memory_dom0'] = dom0_info[2]
323
324     return result
325
326   @staticmethod
327   def GetShellCommandForConsole(instance_name):
328     """Return a command for connecting to the console of an instance.
329
330     """
331     return "xm console %s" % instance_name
332
333
334   def Verify(self):
335     """Verify the hypervisor.
336
337     For Xen, this verifies that the xend process is running.
338
339     """
340     if not utils.CheckDaemonAlive('/var/run/xend.pid', 'xend'):
341       return "xend daemon is not running"
342
343
344 class FakeHypervisor(BaseHypervisor):
345   """Fake hypervisor interface.
346
347   This can be used for testing the ganeti code without having to have
348   a real virtualisation software installed.
349
350   """
351   _ROOT_DIR = "/var/run/ganeti-fake-hypervisor"
352
353   def __init__(self):
354     BaseHypervisor.__init__(self)
355     if not os.path.exists(self._ROOT_DIR):
356       os.mkdir(self._ROOT_DIR)
357
358   def ListInstances(self):
359     """Get the list of running instances.
360
361     """
362     return os.listdir(self._ROOT_DIR)
363
364   def GetInstanceInfo(self, instance_name):
365     """Get instance properties.
366
367     Args:
368       instance_name: the instance name
369
370     Returns:
371       (name, id, memory, vcpus, stat, times)
372     """
373     file_name = "%s/%s" % (self._ROOT_DIR, instance_name)
374     if not os.path.exists(file_name):
375       return None
376     try:
377       fh = file(file_name, "r")
378       try:
379         inst_id = fh.readline().strip()
380         memory = fh.readline().strip()
381         vcpus = fh.readline().strip()
382         stat = "---b-"
383         times = "0"
384         return (instance_name, inst_id, memory, vcpus, stat, times)
385       finally:
386         fh.close()
387     except IOError, err:
388       raise HypervisorError("Failed to list instance %s: %s" %
389                             (instance_name, err))
390
391   def GetAllInstancesInfo(self):
392     """Get properties of all instances.
393
394     Returns:
395       [(name, id, memory, vcpus, stat, times),...]
396     """
397     data = []
398     for file_name in os.listdir(self._ROOT_DIR):
399       try:
400         fh = file(self._ROOT_DIR+"/"+file_name, "r")
401         inst_id = "-1"
402         memory = "0"
403         stat = "-----"
404         times = "-1"
405         try:
406           inst_id = fh.readline().strip()
407           memory = fh.readline().strip()
408           vcpus = fh.readline().strip()
409           stat = "---b-"
410           times = "0"
411         finally:
412           fh.close()
413         data.append((file_name, inst_id, memory, vcpus, stat, times))
414       except IOError, err:
415         raise HypervisorError("Failed to list instances: %s" % err)
416     return data
417
418   def StartInstance(self, instance, force, extra_args):
419     """Start an instance.
420
421     For the fake hypervisor, it just creates a file in the base dir,
422     creating an exception if it already exists. We don't actually
423     handle race conditions properly, since these are *FAKE* instances.
424
425     """
426     file_name = self._ROOT_DIR + "/%s" % instance.name
427     if os.path.exists(file_name):
428       raise HypervisorError("Failed to start instance %s: %s" %
429                             (instance.name, "already running"))
430     try:
431       fh = file(file_name, "w")
432       try:
433         fh.write("0\n%d\n%d\n" % (instance.memory, instance.vcpus))
434       finally:
435         fh.close()
436     except IOError, err:
437       raise HypervisorError("Failed to start instance %s: %s" %
438                             (instance.name, err))
439
440   def StopInstance(self, instance, force=False):
441     """Stop an instance.
442
443     For the fake hypervisor, this just removes the file in the base
444     dir, if it exist, otherwise we raise an exception.
445
446     """
447     file_name = self._ROOT_DIR + "/%s" % instance.name
448     if not os.path.exists(file_name):
449       raise HypervisorError("Failed to stop instance %s: %s" %
450                             (instance.name, "not running"))
451     utils.RemoveFile(file_name)
452
453   def RebootInstance(self, instance):
454     """Reboot an instance.
455
456     For the fake hypervisor, this does nothing.
457
458     """
459     return
460
461   def GetNodeInfo(self):
462     """Return information about the node.
463
464     The return value is a dict, which has to have the following items:
465       (all values in MiB)
466       - memory_total: the total memory size on the node
467       - memory_free: the available memory on the node for instances
468       - memory_dom0: the memory used by the node itself, if available
469
470     """
471     # global ram usage from the xm info command
472     # memory                 : 3583
473     # free_memory            : 747
474     # note: in xen 3, memory has changed to total_memory
475     try:
476       fh = file("/proc/meminfo")
477       try:
478         data = fh.readlines()
479       finally:
480         fh.close()
481     except IOError, err:
482       raise HypervisorError("Failed to list node info: %s" % err)
483
484     result = {}
485     sum_free = 0
486     for line in data:
487       splitfields = line.split(":", 1)
488
489       if len(splitfields) > 1:
490         key = splitfields[0].strip()
491         val = splitfields[1].strip()
492         if key == 'MemTotal':
493           result['memory_total'] = int(val.split()[0])/1024
494         elif key in ('MemFree', 'Buffers', 'Cached'):
495           sum_free += int(val.split()[0])/1024
496         elif key == 'Active':
497           result['memory_dom0'] = int(val.split()[0])/1024
498
499     result['memory_free'] = sum_free
500     return result
501
502   @staticmethod
503   def GetShellCommandForConsole(instance_name):
504     """Return a command for connecting to the console of an instance.
505
506     """
507     return "echo Console not available for fake hypervisor"
508
509   def Verify(self):
510     """Verify the hypervisor.
511
512     For the fake hypervisor, it just checks the existence of the base
513     dir.
514
515     """
516     if not os.path.exists(self._ROOT_DIR):
517       return "The required directory '%s' does not exist." % self._ROOT_DIR