Revision 310bbdde

/dev/null
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
174
    lines = result.stdout.splitlines()[1:]
175
    result = []
176
    for line in lines:
177
      # The format of lines is:
178
      # Name      ID Mem(MiB) VCPUs State  Time(s)
179
      # Domain-0   0  3418     4 r-----    266.2
180
      data = line.split()
181
      if len(data) != 6:
182
        raise HypervisorError("Can't parse output of xm list, line: %s" % line)
183
      try:
184
        data[1] = int(data[1])
185
        data[2] = int(data[2])
186
        data[3] = int(data[3])
187
        data[5] = float(data[5])
188
      except ValueError, err:
189
        raise HypervisorError("Can't parse output of xm list,"
190
                              " line: %s, error: %s" % (line, err))
191

  
192
      # skip the Domain-0 (optional)
193
      if include_node or data[0] != 'Domain-0':
194
        result.append(data)
195

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

Also available in: Unified diff