Statistics
| Branch: | Tag: | Revision:

root / lib / hypervisor / hv_xen.py @ 3680f662

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

    
406
class XenPvmHypervisor(XenHypervisor):
407
  """Xen PVM hypervisor interface"""
408

    
409
  PARAMETERS = {
410
    constants.HV_KERNEL_PATH: hv_base.REQ_FILE_CHECK,
411
    constants.HV_INITRD_PATH: hv_base.OPT_FILE_CHECK,
412
    constants.HV_ROOT_PATH: hv_base.REQUIRED_CHECK,
413
    constants.HV_KERNEL_ARGS: hv_base.NO_CHECK,
414
    }
415

    
416
  @classmethod
417
  def _WriteConfigFile(cls, instance, block_devices):
418
    """Write the Xen config file for the instance.
419

420
    """
421
    hvp = instance.hvparams
422
    config = StringIO()
423
    config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
424

    
425
    # kernel handling
426
    kpath = hvp[constants.HV_KERNEL_PATH]
427
    config.write("kernel = '%s'\n" % kpath)
428

    
429
    # initrd handling
430
    initrd_path = hvp[constants.HV_INITRD_PATH]
431
    if initrd_path:
432
      config.write("ramdisk = '%s'\n" % initrd_path)
433

    
434
    # rest of the settings
435
    config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
436
    config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
437
    config.write("name = '%s'\n" % instance.name)
438

    
439
    vif_data = []
440
    for nic in instance.nics:
441
      nic_str = "mac=%s, bridge=%s" % (nic.mac, nic.bridge)
442
      ip = getattr(nic, "ip", None)
443
      if ip is not None:
444
        nic_str += ", ip=%s" % ip
445
      vif_data.append("'%s'" % nic_str)
446

    
447
    config.write("vif = [%s]\n" % ",".join(vif_data))
448
    config.write("disk = [%s]\n" % ",".join(
449
                 cls._GetConfigFileDiskData(instance.disk_template,
450
                                            block_devices)))
451

    
452
    config.write("root = '%s'\n" % hvp[constants.HV_ROOT_PATH])
453
    config.write("on_poweroff = 'destroy'\n")
454
    config.write("on_reboot = 'restart'\n")
455
    config.write("on_crash = 'restart'\n")
456
    config.write("extra = '%s'\n" % hvp[constants.HV_KERNEL_ARGS])
457
    # just in case it exists
458
    utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
459
    try:
460
      utils.WriteFile("/etc/xen/%s" % instance.name, data=config.getvalue())
461
    except EnvironmentError, err:
462
      raise errors.HypervisorError("Cannot write Xen instance confile"
463
                                   " file /etc/xen/%s: %s" %
464
                                   (instance.name, err))
465

    
466
    return True
467

    
468

    
469
class XenHvmHypervisor(XenHypervisor):
470
  """Xen HVM hypervisor interface"""
471

    
472
  ANCILLARY_FILES = XenHypervisor.ANCILLARY_FILES + \
473
    [constants.VNC_PASSWORD_FILE]
474

    
475
  PARAMETERS = {
476
    constants.HV_ACPI: hv_base.NO_CHECK,
477
    constants.HV_BOOT_ORDER: (True, ) + \
478
    (lambda x: x and len(x.strip("acdn")) == 0,
479
     "Invalid boot order specified, must be one or more of [acdn]",
480
     None, None),
481
    constants.HV_CDROM_IMAGE_PATH: hv_base.OPT_FILE_CHECK,
482
    constants.HV_DISK_TYPE: \
483
    hv_base.ParamInSet(True, constants.HT_HVM_VALID_DISK_TYPES),
484
    constants.HV_NIC_TYPE: \
485
    hv_base.ParamInSet(True, constants.HT_HVM_VALID_NIC_TYPES),
486
    constants.HV_PAE: hv_base.NO_CHECK,
487
    constants.HV_VNC_BIND_ADDRESS: \
488
    (False, utils.IsValidIP,
489
     "VNC bind address is not a valid IP address", None, None),
490
    constants.HV_KERNEL_PATH: hv_base.REQ_FILE_CHECK,
491
    constants.HV_DEVICE_MODEL: hv_base.REQ_FILE_CHECK,
492
    }
493

    
494
  @classmethod
495
  def _WriteConfigFile(cls, instance, block_devices):
496
    """Create a Xen 3.1 HVM config file.
497

498
    """
499
    hvp = instance.hvparams
500

    
501
    config = StringIO()
502
    config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
503

    
504
    # kernel handling
505
    kpath = hvp[constants.HV_KERNEL_PATH]
506
    config.write("kernel = '%s'\n" % kpath)
507

    
508
    config.write("builder = 'hvm'\n")
509
    config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
510
    config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
511
    config.write("name = '%s'\n" % instance.name)
512
    if hvp[constants.HV_PAE]:
513
      config.write("pae = 1\n")
514
    else:
515
      config.write("pae = 0\n")
516
    if hvp[constants.HV_ACPI]:
517
      config.write("acpi = 1\n")
518
    else:
519
      config.write("acpi = 0\n")
520
    config.write("apic = 1\n")
521
    config.write("device_model = '%s'\n" % hvp[constants.HV_DEVICE_MODEL])
522
    config.write("boot = '%s'\n" % hvp[constants.HV_BOOT_ORDER])
523
    config.write("sdl = 0\n")
524
    config.write("usb = 1\n")
525
    config.write("usbdevice = 'tablet'\n")
526
    config.write("vnc = 1\n")
527
    if hvp[constants.HV_VNC_BIND_ADDRESS] is None:
528
      config.write("vnclisten = '%s'\n" % constants.VNC_DEFAULT_BIND_ADDRESS)
529
    else:
530
      config.write("vnclisten = '%s'\n" % hvp[constants.HV_VNC_BIND_ADDRESS])
531

    
532
    if instance.network_port > constants.VNC_BASE_PORT:
533
      display = instance.network_port - constants.VNC_BASE_PORT
534
      config.write("vncdisplay = %s\n" % display)
535
      config.write("vncunused = 0\n")
536
    else:
537
      config.write("# vncdisplay = 1\n")
538
      config.write("vncunused = 1\n")
539

    
540
    try:
541
      password = utils.ReadFile(constants.VNC_PASSWORD_FILE)
542
    except EnvironmentError, err:
543
      raise errors.HypervisorError("Failed to open VNC password file %s: %s" %
544
                                   (constants.VNC_PASSWORD_FILE, err))
545

    
546
    config.write("vncpasswd = '%s'\n" % password.rstrip())
547

    
548
    config.write("serial = 'pty'\n")
549
    config.write("localtime = 1\n")
550

    
551
    vif_data = []
552
    nic_type = hvp[constants.HV_NIC_TYPE]
553
    if nic_type is None:
554
      # ensure old instances don't change
555
      nic_type_str = ", type=ioemu"
556
    elif nic_type == constants.HT_NIC_PARAVIRTUAL:
557
      nic_type_str = ", type=paravirtualized"
558
    else:
559
      nic_type_str = ", model=%s, type=ioemu" % nic_type
560
    for nic in instance.nics:
561
      nic_str = "mac=%s, bridge=%s%s" % (nic.mac, nic.bridge, nic_type_str)
562
      ip = getattr(nic, "ip", None)
563
      if ip is not None:
564
        nic_str += ", ip=%s" % ip
565
      vif_data.append("'%s'" % nic_str)
566

    
567
    config.write("vif = [%s]\n" % ",".join(vif_data))
568
    disk_data = cls._GetConfigFileDiskData(instance.disk_template,
569
                                            block_devices)
570
    disk_type = hvp[constants.HV_DISK_TYPE]
571
    if disk_type in (None, constants.HT_DISK_IOEMU):
572
      replacement = ",ioemu:hd"
573
    else:
574
      replacement = ",hd"
575
    disk_data = [line.replace(",sd", replacement) for line in disk_data]
576
    iso_path = hvp[constants.HV_CDROM_IMAGE_PATH]
577
    if iso_path:
578
      iso = "'file:%s,hdc:cdrom,r'" % iso_path
579
      disk_data.append(iso)
580

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

    
583
    config.write("on_poweroff = 'destroy'\n")
584
    config.write("on_reboot = 'restart'\n")
585
    config.write("on_crash = 'restart'\n")
586
    # just in case it exists
587
    utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
588
    try:
589
      utils.WriteFile("/etc/xen/%s" % instance.name,
590
                      data=config.getvalue())
591
    except EnvironmentError, err:
592
      raise errors.HypervisorError("Cannot write Xen instance confile"
593
                                   " file /etc/xen/%s: %s" %
594
                                   (instance.name, err))
595

    
596
    return True