Statistics
| Branch: | Tag: | Revision:

root / lib / hypervisor / hv_xen.py @ 503b97a9

History | View | Annotate | Download (19.9 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008 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
"""Xen hypervisors
23

24
"""
25

    
26
import os
27
import os.path
28
import time
29
import logging
30
from cStringIO import StringIO
31

    
32
from ganeti import constants
33
from ganeti import errors
34
from ganeti import utils
35
from ganeti.hypervisor import hv_base
36

    
37

    
38
class XenHypervisor(hv_base.BaseHypervisor):
39
  """Xen generic hypervisor interface
40

41
  This is the Xen base class used for both Xen PVM and HVM. It contains
42
  all the functionality that is identical for both.
43

44
  """
45
  REBOOT_RETRY_COUNT = 60
46
  REBOOT_RETRY_INTERVAL = 10
47

    
48
  ANCILLARY_FILES = [
49
    '/etc/xen/xend-config.sxp',
50
    '/etc/xen/scripts/vif-bridge',
51
    ]
52

    
53
  @classmethod
54
  def _WriteConfigFile(cls, instance, block_devices):
55
    """Write the Xen config file for the instance.
56

57
    """
58
    raise NotImplementedError
59

    
60
  @staticmethod
61
  def _WriteConfigFileStatic(instance_name, data):
62
    """Write the Xen config file for the instance.
63

64
    This version of the function just writes the config file from static data.
65

66
    """
67
    utils.WriteFile("/etc/xen/%s" % instance_name, data=data)
68

    
69
  @staticmethod
70
  def _ReadConfigFile(instance_name):
71
    """Returns the contents of the instance config file.
72

73
    """
74
    try:
75
      file_content = utils.ReadFile("/etc/xen/%s" % instance_name)
76
    except EnvironmentError, err:
77
      raise errors.HypervisorError("Failed to load Xen config file: %s" % err)
78
    return file_content
79

    
80
  @staticmethod
81
  def _RemoveConfigFile(instance_name):
82
    """Remove the xen configuration file.
83

84
    """
85
    utils.RemoveFile("/etc/xen/%s" % instance_name)
86

    
87
  @staticmethod
88
  def _GetXMList(include_node):
89
    """Return the list of running instances.
90

91
    If the include_node argument is True, then we return information
92
    for dom0 also, otherwise we filter that from the return value.
93

94
    @return: list of (name, id, memory, vcpus, state, time spent)
95

96
    """
97
    for dummy in range(5):
98
      result = utils.RunCmd(["xm", "list"])
99
      if not result.failed:
100
        break
101
      logging.error("xm list failed (%s): %s", result.fail_reason,
102
                    result.output)
103
      time.sleep(1)
104

    
105
    if result.failed:
106
      raise errors.HypervisorError("xm list failed, retries"
107
                                   " exceeded (%s): %s" %
108
                                   (result.fail_reason, result.output))
109

    
110
    # skip over the heading
111
    lines = result.stdout.splitlines()[1:]
112
    result = []
113
    for line in lines:
114
      # The format of lines is:
115
      # Name      ID Mem(MiB) VCPUs State  Time(s)
116
      # Domain-0   0  3418     4 r-----    266.2
117
      data = line.split()
118
      if len(data) != 6:
119
        raise errors.HypervisorError("Can't parse output of xm list,"
120
                                     " line: %s" % line)
121
      try:
122
        data[1] = int(data[1])
123
        data[2] = int(data[2])
124
        data[3] = int(data[3])
125
        data[5] = float(data[5])
126
      except ValueError, err:
127
        raise errors.HypervisorError("Can't parse output of xm list,"
128
                                     " line: %s, error: %s" % (line, err))
129

    
130
      # skip the Domain-0 (optional)
131
      if include_node or data[0] != 'Domain-0':
132
        result.append(data)
133

    
134
    return result
135

    
136
  def ListInstances(self):
137
    """Get the list of running instances.
138

139
    """
140
    xm_list = self._GetXMList(False)
141
    names = [info[0] for info in xm_list]
142
    return names
143

    
144
  def GetInstanceInfo(self, instance_name):
145
    """Get instance properties.
146

147
    @param instance_name: the instance name
148

149
    @return: tuple (name, id, memory, vcpus, stat, times)
150

151
    """
152
    xm_list = self._GetXMList(instance_name=="Domain-0")
153
    result = None
154
    for data in xm_list:
155
      if data[0] == instance_name:
156
        result = data
157
        break
158
    return result
159

    
160
  def GetAllInstancesInfo(self):
161
    """Get properties of all instances.
162

163
    @return: list of tuples (name, id, memory, vcpus, stat, times)
164

165
    """
166
    xm_list = self._GetXMList(False)
167
    return xm_list
168

    
169
  def StartInstance(self, instance, block_devices):
170
    """Start an instance.
171

172
    """
173
    self._WriteConfigFile(instance, block_devices)
174
    result = utils.RunCmd(["xm", "create", instance.name])
175

    
176
    if result.failed:
177
      raise errors.HypervisorError("Failed to start instance %s: %s (%s)" %
178
                                   (instance.name, result.fail_reason,
179
                                    result.output))
180

    
181
  def StopInstance(self, instance, force=False):
182
    """Stop an instance.
183

184
    """
185
    self._RemoveConfigFile(instance.name)
186
    if force:
187
      command = ["xm", "destroy", instance.name]
188
    else:
189
      command = ["xm", "shutdown", instance.name]
190
    result = utils.RunCmd(command)
191

    
192
    if result.failed:
193
      raise errors.HypervisorError("Failed to stop instance %s: %s, %s" %
194
                                   (instance.name, result.fail_reason,
195
                                    result.output))
196

    
197
  def RebootInstance(self, instance):
198
    """Reboot an instance.
199

200
    """
201
    ini_info = self.GetInstanceInfo(instance.name)
202
    result = utils.RunCmd(["xm", "reboot", instance.name])
203

    
204
    if result.failed:
205
      raise errors.HypervisorError("Failed to reboot instance %s: %s, %s" %
206
                                   (instance.name, result.fail_reason,
207
                                    result.output))
208
    done = False
209
    retries = self.REBOOT_RETRY_COUNT
210
    while retries > 0:
211
      new_info = self.GetInstanceInfo(instance.name)
212
      # check if the domain ID has changed or the run time has
213
      # decreased
214
      if new_info[1] != ini_info[1] or new_info[5] < ini_info[5]:
215
        done = True
216
        break
217
      time.sleep(self.REBOOT_RETRY_INTERVAL)
218
      retries -= 1
219

    
220
    if not done:
221
      raise errors.HypervisorError("Failed to reboot instance %s: instance"
222
                                   " did not reboot in the expected interval" %
223
                                   (instance.name, ))
224

    
225
  def GetNodeInfo(self):
226
    """Return information about the node.
227

228
    @return: a dict with the following keys (memory values in MiB):
229
          - memory_total: the total memory size on the node
230
          - memory_free: the available memory on the node for instances
231
          - memory_dom0: the memory used by the node itself, if available
232
          - nr_cpus: total number of CPUs
233
          - nr_nodes: in a NUMA system, the number of domains
234
          - nr_sockets: the number of physical CPU sockets in the node
235

236
    """
237
    # note: in xen 3, memory has changed to total_memory
238
    result = utils.RunCmd(["xm", "info"])
239
    if result.failed:
240
      logging.error("Can't run 'xm info' (%s): %s", result.fail_reason,
241
                    result.output)
242
      return None
243

    
244
    xmoutput = result.stdout.splitlines()
245
    result = {}
246
    cores_per_socket = threads_per_core = nr_cpus = None
247
    for line in xmoutput:
248
      splitfields = line.split(":", 1)
249

    
250
      if len(splitfields) > 1:
251
        key = splitfields[0].strip()
252
        val = splitfields[1].strip()
253
        if key == 'memory' or key == 'total_memory':
254
          result['memory_total'] = int(val)
255
        elif key == 'free_memory':
256
          result['memory_free'] = int(val)
257
        elif key == 'nr_cpus':
258
          nr_cpus = result['cpu_total'] = int(val)
259
        elif key == 'nr_nodes':
260
          result['cpu_nodes'] = int(val)
261
        elif key == 'cores_per_socket':
262
          cores_per_socket = int(val)
263
        elif key == 'threads_per_core':
264
          threads_per_core = int(val)
265

    
266
    if (cores_per_socket is not None and
267
        threads_per_core is not None and nr_cpus is not None):
268
      result['cpu_sockets'] = nr_cpus / (cores_per_socket * threads_per_core)
269

    
270
    dom0_info = self.GetInstanceInfo("Domain-0")
271
    if dom0_info is not None:
272
      result['memory_dom0'] = dom0_info[2]
273

    
274
    return result
275

    
276
  @classmethod
277
  def GetShellCommandForConsole(cls, instance, hvparams, beparams):
278
    """Return a command for connecting to the console of an instance.
279

280
    """
281
    return "xm console %s" % instance.name
282

    
283

    
284
  def Verify(self):
285
    """Verify the hypervisor.
286

287
    For Xen, this verifies that the xend process is running.
288

289
    """
290
    result = utils.RunCmd(["xm", "info"])
291
    if result.failed:
292
      return "'xm info' failed: %s, %s" % (result.fail_reason, result.output)
293

    
294
  @staticmethod
295
  def _GetConfigFileDiskData(disk_template, block_devices):
296
    """Get disk directive for xen config file.
297

298
    This method builds the xen config disk directive according to the
299
    given disk_template and block_devices.
300

301
    @param disk_template: string containing instance disk template
302
    @param block_devices: list of tuples (cfdev, rldev):
303
        - cfdev: dict containing ganeti config disk part
304
        - rldev: ganeti.bdev.BlockDev object
305

306
    @return: string containing disk directive for xen instance config file
307

308
    """
309
    FILE_DRIVER_MAP = {
310
      constants.FD_LOOP: "file",
311
      constants.FD_BLKTAP: "tap:aio",
312
      }
313
    disk_data = []
314
    if len(block_devices) > 24:
315
      # 'z' - 'a' = 24
316
      raise errors.HypervisorError("Too many disks")
317
    # FIXME: instead of this hardcoding here, each of PVM/HVM should
318
    # directly export their info (currently HVM will just sed this info)
319
    namespace = ["sd" + chr(i + ord('a')) for i in range(24)]
320
    for sd_name, (cfdev, dev_path) in zip(namespace, block_devices):
321
      if cfdev.mode == constants.DISK_RDWR:
322
        mode = "w"
323
      else:
324
        mode = "r"
325
      if cfdev.dev_type == constants.LD_FILE:
326
        line = "'%s:%s,%s,%s'" % (FILE_DRIVER_MAP[cfdev.physical_id[0]],
327
                                  dev_path, sd_name, mode)
328
      else:
329
        line = "'phy:%s,%s,%s'" % (dev_path, sd_name, mode)
330
      disk_data.append(line)
331

    
332
    return disk_data
333

    
334
  def MigrationInfo(self, instance):
335
    """Get instance information to perform a migration.
336

337
    @type instance: L{objects.Instance}
338
    @param instance: instance to be migrated
339
    @rtype: string
340
    @return: content of the xen config file
341

342
    """
343
    return self._ReadConfigFile(instance.name)
344

    
345
  def AcceptInstance(self, instance, info, target):
346
    """Prepare to accept an instance.
347

348
    @type instance: L{objects.Instance}
349
    @param instance: instance to be accepted
350
    @type info: string
351
    @param info: content of the xen config file on the source node
352
    @type target: string
353
    @param target: target host (usually ip), on this node
354

355
    """
356
    pass
357

    
358
  def FinalizeMigration(self, instance, info, success):
359
    """Finalize an instance migration.
360

361
    After a successful migration we write the xen config file.
362
    We do nothing on a failure, as we did not change anything at accept time.
363

364
    @type instance: L{objects.Instance}
365
    @param instance: instance whose migration is being aborted
366
    @type info: string
367
    @param info: content of the xen config file on the source node
368
    @type success: boolean
369
    @param success: whether the migration was a success or a failure
370

371
    """
372
    if success:
373
      self._WriteConfigFileStatic(instance.name, info)
374

    
375
  def MigrateInstance(self, instance, target, live):
376
    """Migrate an instance to a target node.
377

378
    The migration will not be attempted if the instance is not
379
    currently running.
380

381
    @type instance: string
382
    @param instance: instance name
383
    @type target: string
384
    @param target: ip address of the target node
385
    @type live: boolean
386
    @param live: perform a live migration
387

388
    """
389
    if self.GetInstanceInfo(instance) is None:
390
      raise errors.HypervisorError("Instance not running, cannot migrate")
391
    args = ["xm", "migrate"]
392
    if live:
393
      args.append("-l")
394
    args.extend([instance, target])
395
    result = utils.RunCmd(args)
396
    if result.failed:
397
      raise errors.HypervisorError("Failed to migrate instance %s: %s" %
398
                                   (instance, result.output))
399
    # remove old xen file after migration succeeded
400
    try:
401
      self._RemoveConfigFile(instance)
402
    except EnvironmentError:
403
      logging.exception("Failure while removing instance config file")
404

    
405
  @classmethod
406
  def PowercycleNode(cls):
407
    """Xen-specific powercycle.
408

409
    This first does a Linux reboot (which triggers automatically a Xen
410
    reboot), and if that fails it tries to do a Xen reboot. The reason
411
    we don't try a Xen reboot first is that the xen reboot launches an
412
    external command which connects to the Xen hypervisor, and that
413
    won't work in case the root filesystem is broken and/or the xend
414
    daemon is not working.
415

416
    """
417
    try:
418
      cls.LinuxPowercycle()
419
    finally:
420
      utils.RunCmd(["xm", "debug", "R"])
421

    
422

    
423
class XenPvmHypervisor(XenHypervisor):
424
  """Xen PVM hypervisor interface"""
425

    
426
  PARAMETERS = {
427
    constants.HV_KERNEL_PATH: hv_base.REQ_FILE_CHECK,
428
    constants.HV_INITRD_PATH: hv_base.OPT_FILE_CHECK,
429
    constants.HV_ROOT_PATH: hv_base.REQUIRED_CHECK,
430
    constants.HV_KERNEL_ARGS: hv_base.NO_CHECK,
431
    }
432

    
433
  @classmethod
434
  def _WriteConfigFile(cls, instance, block_devices):
435
    """Write the Xen config file for the instance.
436

437
    """
438
    hvp = instance.hvparams
439
    config = StringIO()
440
    config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
441

    
442
    # kernel handling
443
    kpath = hvp[constants.HV_KERNEL_PATH]
444
    config.write("kernel = '%s'\n" % kpath)
445

    
446
    # initrd handling
447
    initrd_path = hvp[constants.HV_INITRD_PATH]
448
    if initrd_path:
449
      config.write("ramdisk = '%s'\n" % initrd_path)
450

    
451
    # rest of the settings
452
    config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
453
    config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
454
    config.write("name = '%s'\n" % instance.name)
455

    
456
    vif_data = []
457
    for nic in instance.nics:
458
      nic_str = "mac=%s" % (nic.mac)
459
      ip = getattr(nic, "ip", None)
460
      if ip is not None:
461
        nic_str += ", ip=%s" % ip
462
      vif_data.append("'%s'" % nic_str)
463
      if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
464
        nic_str += ", bridge=%s" % nic.nicparams[constants.NIC_LINK]
465

    
466
    config.write("vif = [%s]\n" % ",".join(vif_data))
467
    config.write("disk = [%s]\n" % ",".join(
468
                 cls._GetConfigFileDiskData(instance.disk_template,
469
                                            block_devices)))
470

    
471
    config.write("root = '%s'\n" % hvp[constants.HV_ROOT_PATH])
472
    config.write("on_poweroff = 'destroy'\n")
473
    config.write("on_reboot = 'restart'\n")
474
    config.write("on_crash = 'restart'\n")
475
    config.write("extra = '%s'\n" % hvp[constants.HV_KERNEL_ARGS])
476
    # just in case it exists
477
    utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
478
    try:
479
      utils.WriteFile("/etc/xen/%s" % instance.name, data=config.getvalue())
480
    except EnvironmentError, err:
481
      raise errors.HypervisorError("Cannot write Xen instance confile"
482
                                   " file /etc/xen/%s: %s" %
483
                                   (instance.name, err))
484

    
485
    return True
486

    
487

    
488
class XenHvmHypervisor(XenHypervisor):
489
  """Xen HVM hypervisor interface"""
490

    
491
  ANCILLARY_FILES = XenHypervisor.ANCILLARY_FILES + \
492
    [constants.VNC_PASSWORD_FILE]
493

    
494
  PARAMETERS = {
495
    constants.HV_ACPI: hv_base.NO_CHECK,
496
    constants.HV_BOOT_ORDER: (True, ) + \
497
    (lambda x: x and len(x.strip("acdn")) == 0,
498
     "Invalid boot order specified, must be one or more of [acdn]",
499
     None, None),
500
    constants.HV_CDROM_IMAGE_PATH: hv_base.OPT_FILE_CHECK,
501
    constants.HV_DISK_TYPE: \
502
    hv_base.ParamInSet(True, constants.HT_HVM_VALID_DISK_TYPES),
503
    constants.HV_NIC_TYPE: \
504
    hv_base.ParamInSet(True, constants.HT_HVM_VALID_NIC_TYPES),
505
    constants.HV_PAE: hv_base.NO_CHECK,
506
    constants.HV_VNC_BIND_ADDRESS: \
507
    (False, utils.IsValidIP,
508
     "VNC bind address is not a valid IP address", None, None),
509
    constants.HV_KERNEL_PATH: hv_base.REQ_FILE_CHECK,
510
    constants.HV_DEVICE_MODEL: hv_base.REQ_FILE_CHECK,
511
    }
512

    
513
  @classmethod
514
  def _WriteConfigFile(cls, instance, block_devices):
515
    """Create a Xen 3.1 HVM config file.
516

517
    """
518
    hvp = instance.hvparams
519

    
520
    config = StringIO()
521
    config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
522

    
523
    # kernel handling
524
    kpath = hvp[constants.HV_KERNEL_PATH]
525
    config.write("kernel = '%s'\n" % kpath)
526

    
527
    config.write("builder = 'hvm'\n")
528
    config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
529
    config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
530
    config.write("name = '%s'\n" % instance.name)
531
    if hvp[constants.HV_PAE]:
532
      config.write("pae = 1\n")
533
    else:
534
      config.write("pae = 0\n")
535
    if hvp[constants.HV_ACPI]:
536
      config.write("acpi = 1\n")
537
    else:
538
      config.write("acpi = 0\n")
539
    config.write("apic = 1\n")
540
    config.write("device_model = '%s'\n" % hvp[constants.HV_DEVICE_MODEL])
541
    config.write("boot = '%s'\n" % hvp[constants.HV_BOOT_ORDER])
542
    config.write("sdl = 0\n")
543
    config.write("usb = 1\n")
544
    config.write("usbdevice = 'tablet'\n")
545
    config.write("vnc = 1\n")
546
    if hvp[constants.HV_VNC_BIND_ADDRESS] is None:
547
      config.write("vnclisten = '%s'\n" % constants.VNC_DEFAULT_BIND_ADDRESS)
548
    else:
549
      config.write("vnclisten = '%s'\n" % hvp[constants.HV_VNC_BIND_ADDRESS])
550

    
551
    if instance.network_port > constants.VNC_BASE_PORT:
552
      display = instance.network_port - constants.VNC_BASE_PORT
553
      config.write("vncdisplay = %s\n" % display)
554
      config.write("vncunused = 0\n")
555
    else:
556
      config.write("# vncdisplay = 1\n")
557
      config.write("vncunused = 1\n")
558

    
559
    try:
560
      password = utils.ReadFile(constants.VNC_PASSWORD_FILE)
561
    except EnvironmentError, err:
562
      raise errors.HypervisorError("Failed to open VNC password file %s: %s" %
563
                                   (constants.VNC_PASSWORD_FILE, err))
564

    
565
    config.write("vncpasswd = '%s'\n" % password.rstrip())
566

    
567
    config.write("serial = 'pty'\n")
568
    config.write("localtime = 1\n")
569

    
570
    vif_data = []
571
    nic_type = hvp[constants.HV_NIC_TYPE]
572
    if nic_type is None:
573
      # ensure old instances don't change
574
      nic_type_str = ", type=ioemu"
575
    elif nic_type == constants.HT_NIC_PARAVIRTUAL:
576
      nic_type_str = ", type=paravirtualized"
577
    else:
578
      nic_type_str = ", model=%s, type=ioemu" % nic_type
579
    for nic in instance.nics:
580
      nic_str = "mac=%s%s" % (nic.mac, nic_type_str)
581
      ip = getattr(nic, "ip", None)
582
      if ip is not None:
583
        nic_str += ", ip=%s" % ip
584
      vif_data.append("'%s'" % nic_str)
585
      if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
586
        nic_str += ", bridge=%s" % nic.nicparams[constants.NIC_LINK]
587

    
588
    config.write("vif = [%s]\n" % ",".join(vif_data))
589
    disk_data = cls._GetConfigFileDiskData(instance.disk_template,
590
                                            block_devices)
591
    disk_type = hvp[constants.HV_DISK_TYPE]
592
    if disk_type in (None, constants.HT_DISK_IOEMU):
593
      replacement = ",ioemu:hd"
594
    else:
595
      replacement = ",hd"
596
    disk_data = [line.replace(",sd", replacement) for line in disk_data]
597
    iso_path = hvp[constants.HV_CDROM_IMAGE_PATH]
598
    if iso_path:
599
      iso = "'file:%s,hdc:cdrom,r'" % iso_path
600
      disk_data.append(iso)
601

    
602
    config.write("disk = [%s]\n" % (",".join(disk_data)))
603

    
604
    config.write("on_poweroff = 'destroy'\n")
605
    config.write("on_reboot = 'restart'\n")
606
    config.write("on_crash = 'restart'\n")
607
    # just in case it exists
608
    utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
609
    try:
610
      utils.WriteFile("/etc/xen/%s" % instance.name,
611
                      data=config.getvalue())
612
    except EnvironmentError, err:
613
      raise errors.HypervisorError("Cannot write Xen instance confile"
614
                                   " file /etc/xen/%s: %s" %
615
                                   (instance.name, err))
616

    
617
    return True