Statistics
| Branch: | Tag: | Revision:

root / lib / hypervisor.py @ 94285814

History | View | Annotate | Download (21.1 kB)

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
  @staticmethod
314
  def _GetConfigFileDiskData(disk_template, block_devices):
315
    """Get disk directive for xen config file.
316

317
    This method builds the xen config disk directive according to the
318
    given disk_template and block_devices.
319

320
    Args:
321
      disk_template: String containing instance disk template
322
      block_devices: List[tuple1,tuple2,...]
323
        tuple: (cfdev, rldev)
324
          cfdev: dict containing ganeti config disk part
325
          rldev: ganeti.bdev.BlockDev object
326

327
    Returns:
328
      String containing disk directive for xen instance config file
329

330
    """
331
    FILE_DRIVER_MAP = {
332
      constants.FD_LOOP: "file",
333
      constants.FD_BLKTAP: "tap:aio",
334
      }
335
    disk_data = []
336
    for cfdev, rldev in block_devices:
337
      if cfdev.dev_type == constants.LD_FILE:
338
        line = "'%s:%s,%s,w'" % (FILE_DRIVER_MAP[cfdev.physical_id[0]],
339
                                 rldev.dev_path, cfdev.iv_name)
340
      else:
341
        line = "'phy:%s,%s,w'" % (rldev.dev_path, cfdev.iv_name)
342
      disk_data.append(line)
343

    
344
    return disk_data
345

    
346

    
347
class XenPvmHypervisor(XenHypervisor):
348
  """Xen PVM hypervisor interface"""
349

    
350
  @classmethod
351
  def _WriteConfigFile(cls, instance, block_devices, extra_args):
352
    """Write the Xen config file for the instance.
353

354
    """
355
    config = StringIO()
356
    config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
357

    
358
    # kernel handling
359
    if instance.kernel_path in (None, constants.VALUE_DEFAULT):
360
      kpath = constants.XEN_KERNEL
361
    else:
362
      if not os.path.exists(instance.kernel_path):
363
        raise errors.HypervisorError("The kernel %s for instance %s is"
364
                                     " missing" % (instance.kernel_path,
365
                                                   instance.name))
366
      kpath = instance.kernel_path
367
    config.write("kernel = '%s'\n" % kpath)
368

    
369
    # initrd handling
370
    if instance.initrd_path in (None, constants.VALUE_DEFAULT):
371
      if os.path.exists(constants.XEN_INITRD):
372
        initrd_path = constants.XEN_INITRD
373
      else:
374
        initrd_path = None
375
    elif instance.initrd_path == constants.VALUE_NONE:
376
      initrd_path = None
377
    else:
378
      if not os.path.exists(instance.initrd_path):
379
        raise errors.HypervisorError("The initrd %s for instance %s is"
380
                                     " missing" % (instance.initrd_path,
381
                                                   instance.name))
382
      initrd_path = instance.initrd_path
383

    
384
    if initrd_path:
385
      config.write("ramdisk = '%s'\n" % initrd_path)
386

    
387
    # rest of the settings
388
    config.write("memory = %d\n" % instance.memory)
389
    config.write("vcpus = %d\n" % instance.vcpus)
390
    config.write("name = '%s'\n" % instance.name)
391

    
392
    vif_data = []
393
    for nic in instance.nics:
394
      nic_str = "mac=%s, bridge=%s" % (nic.mac, nic.bridge)
395
      ip = getattr(nic, "ip", None)
396
      if ip is not None:
397
        nic_str += ", ip=%s" % ip
398
      vif_data.append("'%s'" % nic_str)
399

    
400
    config.write("vif = [%s]\n" % ",".join(vif_data))
401
    config.write("disk = [%s]\n" % ",".join(
402
                 cls._GetConfigFileDiskData(instance.disk_template,
403
                                            block_devices)))
404
    config.write("root = '/dev/sda ro'\n")
405
    config.write("on_poweroff = 'destroy'\n")
406
    config.write("on_reboot = 'restart'\n")
407
    config.write("on_crash = 'restart'\n")
408
    if extra_args:
409
      config.write("extra = '%s'\n" % extra_args)
410
    # just in case it exists
411
    utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
412
    try:
413
      f = open("/etc/xen/%s" % instance.name, "w")
414
      try:
415
        f.write(config.getvalue())
416
      finally:
417
        f.close()
418
    except IOError, err:
419
      raise errors.OpExecError("Cannot write Xen instance confile"
420
                               " file /etc/xen/%s: %s" % (instance.name, err))
421
    return True
422

    
423
  @staticmethod
424
  def GetShellCommandForConsole(instance):
425
    """Return a command for connecting to the console of an instance.
426

427
    """
428
    return "xm console %s" % instance.name
429

    
430

    
431
class FakeHypervisor(BaseHypervisor):
432
  """Fake hypervisor interface.
433

434
  This can be used for testing the ganeti code without having to have
435
  a real virtualisation software installed.
436

437
  """
438
  _ROOT_DIR = constants.RUN_DIR + "/ganeti-fake-hypervisor"
439

    
440
  def __init__(self):
441
    BaseHypervisor.__init__(self)
442
    if not os.path.exists(self._ROOT_DIR):
443
      os.mkdir(self._ROOT_DIR)
444

    
445
  def ListInstances(self):
446
    """Get the list of running instances.
447

448
    """
449
    return os.listdir(self._ROOT_DIR)
450

    
451
  def GetInstanceInfo(self, instance_name):
452
    """Get instance properties.
453

454
    Args:
455
      instance_name: the instance name
456

457
    Returns:
458
      (name, id, memory, vcpus, stat, times)
459
    """
460
    file_name = "%s/%s" % (self._ROOT_DIR, instance_name)
461
    if not os.path.exists(file_name):
462
      return None
463
    try:
464
      fh = file(file_name, "r")
465
      try:
466
        inst_id = fh.readline().strip()
467
        memory = fh.readline().strip()
468
        vcpus = fh.readline().strip()
469
        stat = "---b-"
470
        times = "0"
471
        return (instance_name, inst_id, memory, vcpus, stat, times)
472
      finally:
473
        fh.close()
474
    except IOError, err:
475
      raise HypervisorError("Failed to list instance %s: %s" %
476
                            (instance_name, err))
477

    
478
  def GetAllInstancesInfo(self):
479
    """Get properties of all instances.
480

481
    Returns:
482
      [(name, id, memory, vcpus, stat, times),...]
483
    """
484
    data = []
485
    for file_name in os.listdir(self._ROOT_DIR):
486
      try:
487
        fh = file(self._ROOT_DIR+"/"+file_name, "r")
488
        inst_id = "-1"
489
        memory = "0"
490
        stat = "-----"
491
        times = "-1"
492
        try:
493
          inst_id = fh.readline().strip()
494
          memory = fh.readline().strip()
495
          vcpus = fh.readline().strip()
496
          stat = "---b-"
497
          times = "0"
498
        finally:
499
          fh.close()
500
        data.append((file_name, inst_id, memory, vcpus, stat, times))
501
      except IOError, err:
502
        raise HypervisorError("Failed to list instances: %s" % err)
503
    return data
504

    
505
  def StartInstance(self, instance, force, extra_args):
506
    """Start an instance.
507

508
    For the fake hypervisor, it just creates a file in the base dir,
509
    creating an exception if it already exists. We don't actually
510
    handle race conditions properly, since these are *FAKE* instances.
511

512
    """
513
    file_name = self._ROOT_DIR + "/%s" % instance.name
514
    if os.path.exists(file_name):
515
      raise HypervisorError("Failed to start instance %s: %s" %
516
                            (instance.name, "already running"))
517
    try:
518
      fh = file(file_name, "w")
519
      try:
520
        fh.write("0\n%d\n%d\n" % (instance.memory, instance.vcpus))
521
      finally:
522
        fh.close()
523
    except IOError, err:
524
      raise HypervisorError("Failed to start instance %s: %s" %
525
                            (instance.name, err))
526

    
527
  def StopInstance(self, instance, force=False):
528
    """Stop an instance.
529

530
    For the fake hypervisor, this just removes the file in the base
531
    dir, if it exist, otherwise we raise an exception.
532

533
    """
534
    file_name = self._ROOT_DIR + "/%s" % instance.name
535
    if not os.path.exists(file_name):
536
      raise HypervisorError("Failed to stop instance %s: %s" %
537
                            (instance.name, "not running"))
538
    utils.RemoveFile(file_name)
539

    
540
  def RebootInstance(self, instance):
541
    """Reboot an instance.
542

543
    For the fake hypervisor, this does nothing.
544

545
    """
546
    return
547

    
548
  def GetNodeInfo(self):
549
    """Return information about the node.
550

551
    The return value is a dict, which has to have the following items:
552
      (all values in MiB)
553
      - memory_total: the total memory size on the node
554
      - memory_free: the available memory on the node for instances
555
      - memory_dom0: the memory used by the node itself, if available
556

557
    """
558
    # global ram usage from the xm info command
559
    # memory                 : 3583
560
    # free_memory            : 747
561
    # note: in xen 3, memory has changed to total_memory
562
    try:
563
      fh = file("/proc/meminfo")
564
      try:
565
        data = fh.readlines()
566
      finally:
567
        fh.close()
568
    except IOError, err:
569
      raise HypervisorError("Failed to list node info: %s" % err)
570

    
571
    result = {}
572
    sum_free = 0
573
    for line in data:
574
      splitfields = line.split(":", 1)
575

    
576
      if len(splitfields) > 1:
577
        key = splitfields[0].strip()
578
        val = splitfields[1].strip()
579
        if key == 'MemTotal':
580
          result['memory_total'] = int(val.split()[0])/1024
581
        elif key in ('MemFree', 'Buffers', 'Cached'):
582
          sum_free += int(val.split()[0])/1024
583
        elif key == 'Active':
584
          result['memory_dom0'] = int(val.split()[0])/1024
585

    
586
    result['memory_free'] = sum_free
587
    return result
588

    
589
  @staticmethod
590
  def GetShellCommandForConsole(instance):
591
    """Return a command for connecting to the console of an instance.
592

593
    """
594
    return "echo Console not available for fake hypervisor"
595

    
596
  def Verify(self):
597
    """Verify the hypervisor.
598

599
    For the fake hypervisor, it just checks the existence of the base
600
    dir.
601

602
    """
603
    if not os.path.exists(self._ROOT_DIR):
604
      return "The required directory '%s' does not exist." % self._ROOT_DIR
605

    
606

    
607
class XenHvmHypervisor(XenHypervisor):
608
  """Xen HVM hypervisor interface"""
609

    
610
  @classmethod
611
  def _WriteConfigFile(cls, instance, block_devices, extra_args):
612
    """Create a Xen 3.1 HVM config file.
613

614
    """
615
    config = StringIO()
616
    config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
617
    config.write("kernel = '/usr/lib/xen/boot/hvmloader'\n")
618
    config.write("builder = 'hvm'\n")
619
    config.write("memory = %d\n" % instance.memory)
620
    config.write("vcpus = %d\n" % instance.vcpus)
621
    config.write("name = '%s'\n" % instance.name)
622
    config.write("pae = 1\n")
623
    config.write("acpi = 1\n")
624
    config.write("apic = 1\n")
625
    arch = os.uname()[4]
626
    if '64' in arch:
627
      config.write("device_model = '/usr/lib64/xen/bin/qemu-dm'\n")
628
    else:
629
      config.write("device_model = '/usr/lib/xen/bin/qemu-dm'\n")
630
    if instance.hvm_boot_order is None:
631
      config.write("boot = '%s'\n" % constants.HT_HVM_DEFAULT_BOOT_ORDER)
632
    else:
633
      config.write("boot = '%s'\n" % instance.hvm_boot_order)
634
    config.write("sdl = 0\n")
635
    config.write("usb = 1\n");
636
    config.write("usbdevice = 'tablet'\n");
637
    config.write("vnc = 1\n")
638
    config.write("vnclisten = '0.0.0.0'\n")
639

    
640
    if instance.network_port > constants.HT_HVM_VNC_BASE_PORT:
641
      display = instance.network_port - constants.HT_HVM_VNC_BASE_PORT
642
      config.write("vncdisplay = %s\n" % display)
643
      config.write("vncunused = 0\n")
644
    else:
645
      config.write("# vncdisplay = 1\n")
646
      config.write("vncunused = 1\n")
647

    
648
    try:
649
      password_file = open(constants.VNC_PASSWORD_FILE, "r")
650
      try:
651
        password = password_file.readline()
652
      finally:
653
        password_file.close()
654
    except IOError:
655
      raise errors.OpExecError("failed to open VNC password file %s " %
656
                               constants.VNC_PASSWORD_FILE)
657

    
658
    config.write("vncpasswd = '%s'\n" % password.rstrip())
659

    
660
    config.write("serial = 'pty'\n")
661
    config.write("localtime = 1\n")
662

    
663
    vif_data = []
664
    for nic in instance.nics:
665
      nic_str = "mac=%s, bridge=%s, type=ioemu" % (nic.mac, nic.bridge)
666
      ip = getattr(nic, "ip", None)
667
      if ip is not None:
668
        nic_str += ", ip=%s" % ip
669
      vif_data.append("'%s'" % nic_str)
670

    
671
    config.write("vif = [%s]\n" % ",".join(vif_data))
672
    iso = "'file:/srv/ganeti/iso/hvm-install.iso,hdc:cdrom,r'"
673
    config.write("disk = [%s, %s]\n" % (",".join(
674
                 cls._GetConfigFileDiskData(instance.disk_template,
675
                                            block_devices)), iso))
676
    config.write("on_poweroff = 'destroy'\n")
677
    config.write("on_reboot = 'restart'\n")
678
    config.write("on_crash = 'restart'\n")
679
    if extra_args:
680
      config.write("extra = '%s'\n" % extra_args)
681
    # just in case it exists
682
    utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
683
    try:
684
      f = open("/etc/xen/%s" % instance.name, "w")
685
      try:
686
        f.write(config.getvalue())
687
      finally:
688
        f.close()
689
    except IOError, err:
690
      raise errors.OpExecError("Cannot write Xen instance confile"
691
                               " file /etc/xen/%s: %s" % (instance.name, err))
692
    return True
693

    
694
  @staticmethod
695
  def GetShellCommandForConsole(instance):
696
    """Return a command for connecting to the console of an instance.
697

698
    """
699
    if instance.network_port is None:
700
      raise errors.OpExecError("no console port defined for %s"
701
                               % instance.name)
702
    else:
703
      raise errors.OpExecError("no PTY console, connect to %s:%s via VNC"
704
                               % (instance.primary_node,
705
                                  instance.network_port))