Statistics
| Branch: | Tag: | Revision:

root / lib / hypervisor.py @ cdb08f44

History | View | Annotate | Download (20.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

    
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))