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