Statistics
| Branch: | Tag: | Revision:

root / lib / hypervisor / hv_xen.py @ f5118ade

History | View | Annotate | Download (19.7 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, bridge=%s" % (nic.mac, nic.bridge)
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

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

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

    
483
    return True
484

    
485

    
486
class XenHvmHypervisor(XenHypervisor):
487
  """Xen HVM hypervisor interface"""
488

    
489
  ANCILLARY_FILES = XenHypervisor.ANCILLARY_FILES + \
490
    [constants.VNC_PASSWORD_FILE]
491

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

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

515
    """
516
    hvp = instance.hvparams
517

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

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

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

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

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

    
563
    config.write("vncpasswd = '%s'\n" % password.rstrip())
564

    
565
    config.write("serial = 'pty'\n")
566
    config.write("localtime = 1\n")
567

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

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

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

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

    
613
    return True