Statistics
| Branch: | Tag: | Revision:

root / lib / hypervisor / hv_xen.py @ 637ce7f9

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

    
46
  @classmethod
47
  def _WriteConfigFile(cls, instance, block_devices, extra_args):
48
    """Write the Xen config file for the instance.
49

50
    """
51
    raise NotImplementedError
52

    
53
  @staticmethod
54
  def _WriteConfigFileStatic(instance_name, data):
55
    """Write the Xen config file for the instance.
56

57
    This version of the function just writes the config file from static data.
58

59
    """
60
    utils.WriteFile("/etc/xen/%s" % instance_name, data=data)
61

    
62
  @staticmethod
63
  def _ReadConfigFile(instance_name):
64
    """Returns the contents of the instance config file.
65

66
    """
67
    try:
68
      file_content = utils.ReadFile("/etc/xen/%s" % instance_name)
69
    except EnvironmentError, err:
70
      raise errors.HypervisorError("Failed to load Xen config file: %s" % err)
71
    return file_content
72

    
73
  @staticmethod
74
  def _RemoveConfigFile(instance_name):
75
    """Remove the xen configuration file.
76

77
    """
78
    utils.RemoveFile("/etc/xen/%s" % instance_name)
79

    
80
  @staticmethod
81
  def _GetXMList(include_node):
82
    """Return the list of running instances.
83

84
    If the include_node argument is True, then we return information
85
    for dom0 also, otherwise we filter that from the return value.
86

87
    @return: list of (name, id, memory, vcpus, state, time spent)
88

89
    """
90
    for dummy in range(5):
91
      result = utils.RunCmd(["xm", "list"])
92
      if not result.failed:
93
        break
94
      logging.error("xm list failed (%s): %s", result.fail_reason,
95
                    result.output)
96
      time.sleep(1)
97

    
98
    if result.failed:
99
      raise errors.HypervisorError("xm list failed, retries"
100
                                   " exceeded (%s): %s" %
101
                                   (result.fail_reason, result.stderr))
102

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

    
123
      # skip the Domain-0 (optional)
124
      if include_node or data[0] != 'Domain-0':
125
        result.append(data)
126

    
127
    return result
128

    
129
  def ListInstances(self):
130
    """Get the list of running instances.
131

132
    """
133
    xm_list = self._GetXMList(False)
134
    names = [info[0] for info in xm_list]
135
    return names
136

    
137
  def GetInstanceInfo(self, instance_name):
138
    """Get instance properties.
139

140
    @param instance_name: the instance name
141

142
    @return: tuple (name, id, memory, vcpus, stat, times)
143

144
    """
145
    xm_list = self._GetXMList(instance_name=="Domain-0")
146
    result = None
147
    for data in xm_list:
148
      if data[0] == instance_name:
149
        result = data
150
        break
151
    return result
152

    
153
  def GetAllInstancesInfo(self):
154
    """Get properties of all instances.
155

156
    @return: list of tuples (name, id, memory, vcpus, stat, times)
157

158
    """
159
    xm_list = self._GetXMList(False)
160
    return xm_list
161

    
162
  def StartInstance(self, instance, block_devices, extra_args):
163
    """Start an instance.
164

165
    """
166
    self._WriteConfigFile(instance, block_devices, extra_args)
167
    result = utils.RunCmd(["xm", "create", instance.name])
168

    
169
    if result.failed:
170
      raise errors.HypervisorError("Failed to start instance %s: %s (%s)" %
171
                                   (instance.name, result.fail_reason,
172
                                    result.output))
173

    
174
  def StopInstance(self, instance, force=False):
175
    """Stop an instance.
176

177
    """
178
    self._RemoveConfigFile(instance.name)
179
    if force:
180
      command = ["xm", "destroy", instance.name]
181
    else:
182
      command = ["xm", "shutdown", instance.name]
183
    result = utils.RunCmd(command)
184

    
185
    if result.failed:
186
      raise errors.HypervisorError("Failed to stop instance %s: %s" %
187
                                   (instance.name, result.fail_reason))
188

    
189
  def RebootInstance(self, instance):
190
    """Reboot an instance.
191

192
    """
193
    result = utils.RunCmd(["xm", "reboot", instance.name])
194

    
195
    if result.failed:
196
      raise errors.HypervisorError("Failed to reboot instance %s: %s" %
197
                                   (instance.name, result.fail_reason))
198

    
199
  def GetNodeInfo(self):
200
    """Return information about the node.
201

202
    @return: a dict with the following keys (values in MiB):
203
          - memory_total: the total memory size on the node
204
          - memory_free: the available memory on the node for instances
205
          - memory_dom0: the memory used by the node itself, if available
206

207
    """
208
    # note: in xen 3, memory has changed to total_memory
209
    result = utils.RunCmd(["xm", "info"])
210
    if result.failed:
211
      logging.error("Can't run 'xm info' (%s): %s", result.fail_reason,
212
                    result.output)
213
      return None
214

    
215
    xmoutput = result.stdout.splitlines()
216
    result = {}
217
    for line in xmoutput:
218
      splitfields = line.split(":", 1)
219

    
220
      if len(splitfields) > 1:
221
        key = splitfields[0].strip()
222
        val = splitfields[1].strip()
223
        if key == 'memory' or key == 'total_memory':
224
          result['memory_total'] = int(val)
225
        elif key == 'free_memory':
226
          result['memory_free'] = int(val)
227
        elif key == 'nr_cpus':
228
          result['cpu_total'] = int(val)
229
    dom0_info = self.GetInstanceInfo("Domain-0")
230
    if dom0_info is not None:
231
      result['memory_dom0'] = dom0_info[2]
232

    
233
    return result
234

    
235
  @classmethod
236
  def GetShellCommandForConsole(cls, instance):
237
    """Return a command for connecting to the console of an instance.
238

239
    """
240
    return "xm console %s" % instance.name
241

    
242

    
243
  def Verify(self):
244
    """Verify the hypervisor.
245

246
    For Xen, this verifies that the xend process is running.
247

248
    """
249
    result = utils.RunCmd(["xm", "info"])
250
    if result.failed:
251
      return "'xm info' failed: %s" % result.fail_reason
252

    
253
  @staticmethod
254
  def _GetConfigFileDiskData(disk_template, block_devices):
255
    """Get disk directive for xen config file.
256

257
    This method builds the xen config disk directive according to the
258
    given disk_template and block_devices.
259

260
    @param disk_template: string containing instance disk template
261
    @param block_devices: list of tuples (cfdev, rldev):
262
        - cfdev: dict containing ganeti config disk part
263
        - rldev: ganeti.bdev.BlockDev object
264

265
    @return: string containing disk directive for xen instance config file
266

267
    """
268
    FILE_DRIVER_MAP = {
269
      constants.FD_LOOP: "file",
270
      constants.FD_BLKTAP: "tap:aio",
271
      }
272
    disk_data = []
273
    if len(block_devices) > 24:
274
      # 'z' - 'a' = 24
275
      raise errors.HypervisorError("Too many disks")
276
    # FIXME: instead of this hardcoding here, each of PVM/HVM should
277
    # directly export their info (currently HVM will just sed this info)
278
    namespace = ["sd" + chr(i + ord('a')) for i in range(24)]
279
    for sd_name, (cfdev, dev_path) in zip(namespace, block_devices):
280
      if cfdev.dev_type == constants.LD_FILE:
281
        line = "'%s:%s,%s,w'" % (FILE_DRIVER_MAP[cfdev.physical_id[0]],
282
                                 dev_path, sd_name)
283
      else:
284
        line = "'phy:%s,%s,w'" % (dev_path, sd_name)
285
      disk_data.append(line)
286

    
287
    return disk_data
288

    
289
  def MigrationInfo(self, instance):
290
    """Get instance information to perform a migration.
291

292
    @type instance: L{objects.Instance}
293
    @param instance: instance to be migrated
294
    @rtype: string
295
    @return: content of the xen config file
296

297
    """
298
    return self._ReadConfigFile(instance.name)
299

    
300
  def AcceptInstance(self, instance, info, target):
301
    """Prepare to accept an instance.
302

303
    @type instance: L{objects.Instance}
304
    @param instance: instance to be accepted
305
    @type info: string
306
    @param info: content of the xen config file on the source node
307
    @type target: string
308
    @param target: target host (usually ip), on this node
309

310
    """
311
    pass
312

    
313
  def FinalizeMigration(self, instance, info, success):
314
    """Finalize an instance migration.
315

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

319
    @type instance: L{objects.Instance}
320
    @param instance: instance whose migration is being aborted
321
    @type info: string
322
    @param info: content of the xen config file on the source node
323
    @type success: boolean
324
    @param success: whether the migration was a success or a failure
325

326
    """
327
    if success:
328
      self._WriteConfigFileStatic(instance.name, info)
329

    
330
  def MigrateInstance(self, instance, target, live):
331
    """Migrate an instance to a target node.
332

333
    The migration will not be attempted if the instance is not
334
    currently running.
335

336
    @type instance: string
337
    @param instance: instance name
338
    @type target: string
339
    @param target: ip address of the target node
340
    @type live: boolean
341
    @param live: perform a live migration
342

343
    """
344
    if self.GetInstanceInfo(instance) is None:
345
      raise errors.HypervisorError("Instance not running, cannot migrate")
346
    args = ["xm", "migrate"]
347
    if live:
348
      args.append("-l")
349
    args.extend([instance, target])
350
    result = utils.RunCmd(args)
351
    if result.failed:
352
      raise errors.HypervisorError("Failed to migrate instance %s: %s" %
353
                                   (instance, result.output))
354
    # remove old xen file after migration succeeded
355
    try:
356
      self._RemoveConfigFile(instance)
357
    except EnvironmentError:
358
      logging.exception("Failure while removing instance config file")
359

    
360

    
361
class XenPvmHypervisor(XenHypervisor):
362
  """Xen PVM hypervisor interface"""
363

    
364
  PARAMETERS = [
365
    constants.HV_KERNEL_PATH,
366
    constants.HV_INITRD_PATH,
367
    ]
368

    
369
  @classmethod
370
  def CheckParameterSyntax(cls, hvparams):
371
    """Check the given parameters for validity.
372

373
    For the PVM hypervisor, this only check the existence of the
374
    kernel.
375

376
    @type hvparams:  dict
377
    @param hvparams: dictionary with parameter names/value
378
    @raise errors.HypervisorError: when a parameter is not valid
379

380
    """
381
    super(XenPvmHypervisor, cls).CheckParameterSyntax(hvparams)
382

    
383
    if not hvparams[constants.HV_KERNEL_PATH]:
384
      raise errors.HypervisorError("Need a kernel for the instance")
385

    
386
    if not os.path.isabs(hvparams[constants.HV_KERNEL_PATH]):
387
      raise errors.HypervisorError("The kernel path must be an absolute path")
388

    
389
    if hvparams[constants.HV_INITRD_PATH]:
390
      if not os.path.isabs(hvparams[constants.HV_INITRD_PATH]):
391
        raise errors.HypervisorError("The initrd path must be an absolute path"
392
                                     ", if defined")
393

    
394
  def ValidateParameters(self, hvparams):
395
    """Check the given parameters for validity.
396

397
    For the PVM hypervisor, this only check the existence of the
398
    kernel.
399

400
    """
401
    super(XenPvmHypervisor, self).ValidateParameters(hvparams)
402

    
403
    kernel_path = hvparams[constants.HV_KERNEL_PATH]
404
    if not os.path.isfile(kernel_path):
405
      raise errors.HypervisorError("Instance kernel '%s' not found or"
406
                                   " not a file" % kernel_path)
407
    initrd_path = hvparams[constants.HV_INITRD_PATH]
408
    if initrd_path and not os.path.isfile(initrd_path):
409
      raise errors.HypervisorError("Instance initrd '%s' not found or"
410
                                   " not a file" % initrd_path)
411

    
412
  @classmethod
413
  def _WriteConfigFile(cls, instance, block_devices, extra_args):
414
    """Write the Xen config file for the instance.
415

416
    """
417
    config = StringIO()
418
    config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
419

    
420
    # kernel handling
421
    kpath = instance.hvparams[constants.HV_KERNEL_PATH]
422
    config.write("kernel = '%s'\n" % kpath)
423

    
424
    # initrd handling
425
    initrd_path = instance.hvparams[constants.HV_INITRD_PATH]
426
    if initrd_path:
427
      config.write("ramdisk = '%s'\n" % initrd_path)
428

    
429
    # rest of the settings
430
    config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
431
    config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
432
    config.write("name = '%s'\n" % instance.name)
433

    
434
    vif_data = []
435
    for nic in instance.nics:
436
      nic_str = "mac=%s, bridge=%s" % (nic.mac, nic.bridge)
437
      ip = getattr(nic, "ip", None)
438
      if ip is not None:
439
        nic_str += ", ip=%s" % ip
440
      vif_data.append("'%s'" % nic_str)
441

    
442
    config.write("vif = [%s]\n" % ",".join(vif_data))
443
    config.write("disk = [%s]\n" % ",".join(
444
                 cls._GetConfigFileDiskData(instance.disk_template,
445
                                            block_devices)))
446
    config.write("root = '/dev/sda ro'\n")
447
    config.write("on_poweroff = 'destroy'\n")
448
    config.write("on_reboot = 'restart'\n")
449
    config.write("on_crash = 'restart'\n")
450
    if extra_args:
451
      config.write("extra = '%s'\n" % extra_args)
452
    # just in case it exists
453
    utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
454
    try:
455
      f = open("/etc/xen/%s" % instance.name, "w")
456
      try:
457
        f.write(config.getvalue())
458
      finally:
459
        f.close()
460
    except IOError, err:
461
      raise errors.OpExecError("Cannot write Xen instance confile"
462
                               " file /etc/xen/%s: %s" % (instance.name, err))
463
    return True
464

    
465

    
466
class XenHvmHypervisor(XenHypervisor):
467
  """Xen HVM hypervisor interface"""
468

    
469
  PARAMETERS = [
470
    constants.HV_ACPI,
471
    constants.HV_BOOT_ORDER,
472
    constants.HV_CDROM_IMAGE_PATH,
473
    constants.HV_DISK_TYPE,
474
    constants.HV_NIC_TYPE,
475
    constants.HV_PAE,
476
    constants.HV_VNC_BIND_ADDRESS,
477
    ]
478

    
479
  @classmethod
480
  def CheckParameterSyntax(cls, hvparams):
481
    """Check the given parameter syntax.
482

483
    """
484
    super(XenHvmHypervisor, cls).CheckParameterSyntax(hvparams)
485
    # boot order verification
486
    boot_order = hvparams[constants.HV_BOOT_ORDER]
487
    if len(boot_order.strip("acdn")) != 0:
488
      raise errors.HypervisorError("Invalid boot order '%s' specified,"
489
                                   " must be one or more of [acdn]" %
490
                                   boot_order)
491
    # device type checks
492
    nic_type = hvparams[constants.HV_NIC_TYPE]
493
    if nic_type not in constants.HT_HVM_VALID_NIC_TYPES:
494
      raise errors.HypervisorError("Invalid NIC type %s specified for Xen HVM"
495
                                   " hypervisor" % nic_type)
496
    disk_type = hvparams[constants.HV_DISK_TYPE]
497
    if disk_type not in constants.HT_HVM_VALID_DISK_TYPES:
498
      raise errors.HypervisorError("Invalid disk type %s specified for Xen HVM"
499
                                   " hypervisor" % disk_type)
500
    # vnc_bind_address verification
501
    vnc_bind_address = hvparams[constants.HV_VNC_BIND_ADDRESS]
502
    if vnc_bind_address is not None:
503
      if not utils.IsValidIP(vnc_bind_address):
504
        raise errors.OpPrereqError("given VNC bind address '%s' doesn't look"
505
                                   " like a valid IP address" %
506
                                   vnc_bind_address)
507

    
508
    iso_path = hvparams[constants.HV_CDROM_IMAGE_PATH]
509
    if iso_path and not os.path.isabs(iso_path):
510
      raise errors.HypervisorError("The path to the HVM CDROM image must"
511
                                   " be an absolute path or None, not %s" %
512
                                   iso_path)
513

    
514
  def ValidateParameters(self, hvparams):
515
    """Check the given parameters for validity.
516

517
    For the PVM hypervisor, this only check the existence of the
518
    kernel.
519

520
    @type hvparams:  dict
521
    @param hvparams: dictionary with parameter names/value
522
    @raise errors.HypervisorError: when a parameter is not valid
523

524
    """
525
    super(XenHvmHypervisor, self).ValidateParameters(hvparams)
526

    
527
    # hvm_cdrom_image_path verification
528
    iso_path = hvparams[constants.HV_CDROM_IMAGE_PATH]
529
    if iso_path and not os.path.isfile(iso_path):
530
      raise errors.HypervisorError("The HVM CDROM image must either be a"
531
                                   " regular file or a symlink pointing to"
532
                                   " an existing regular file, not %s" %
533
                                   iso_path)
534

    
535
  @classmethod
536
  def _WriteConfigFile(cls, instance, block_devices, extra_args):
537
    """Create a Xen 3.1 HVM config file.
538

539
    """
540
    config = StringIO()
541
    config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
542
    config.write("kernel = '/usr/lib/xen/boot/hvmloader'\n")
543
    config.write("builder = 'hvm'\n")
544
    config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
545
    config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
546
    config.write("name = '%s'\n" % instance.name)
547
    if instance.hvparams[constants.HV_PAE]:
548
      config.write("pae = 1\n")
549
    else:
550
      config.write("pae = 0\n")
551
    if instance.hvparams[constants.HV_ACPI]:
552
      config.write("acpi = 1\n")
553
    else:
554
      config.write("acpi = 0\n")
555
    config.write("apic = 1\n")
556
    arch = os.uname()[4]
557
    if '64' in arch:
558
      config.write("device_model = '/usr/lib64/xen/bin/qemu-dm'\n")
559
    else:
560
      config.write("device_model = '/usr/lib/xen/bin/qemu-dm'\n")
561
    if instance.hvparams[constants.HV_BOOT_ORDER] is None:
562
      config.write("boot = '%s'\n" % constants.HT_HVM_DEFAULT_BOOT_ORDER)
563
    else:
564
      config.write("boot = '%s'\n" % instance.hvparams["boot_order"])
565
    config.write("sdl = 0\n")
566
    config.write("usb = 1\n")
567
    config.write("usbdevice = 'tablet'\n")
568
    config.write("vnc = 1\n")
569
    if instance.hvparams[constants.HV_VNC_BIND_ADDRESS] is None:
570
      config.write("vnclisten = '%s'\n" % constants.VNC_DEFAULT_BIND_ADDRESS)
571
    else:
572
      config.write("vnclisten = '%s'\n" %
573
                   instance.hvparams["vnc_bind_address"])
574

    
575
    if instance.network_port > constants.HT_HVM_VNC_BASE_PORT:
576
      display = instance.network_port - constants.HT_HVM_VNC_BASE_PORT
577
      config.write("vncdisplay = %s\n" % display)
578
      config.write("vncunused = 0\n")
579
    else:
580
      config.write("# vncdisplay = 1\n")
581
      config.write("vncunused = 1\n")
582

    
583
    try:
584
      password_file = open(constants.VNC_PASSWORD_FILE, "r")
585
      try:
586
        password = password_file.readline()
587
      finally:
588
        password_file.close()
589
    except IOError:
590
      raise errors.OpExecError("failed to open VNC password file %s " %
591
                               constants.VNC_PASSWORD_FILE)
592

    
593
    config.write("vncpasswd = '%s'\n" % password.rstrip())
594

    
595
    config.write("serial = 'pty'\n")
596
    config.write("localtime = 1\n")
597

    
598
    vif_data = []
599
    nic_type = instance.hvparams[constants.HV_NIC_TYPE]
600
    if nic_type is None:
601
      # ensure old instances don't change
602
      nic_type_str = ", type=ioemu"
603
    elif nic_type == constants.HT_HVM_DEV_PARAVIRTUAL:
604
      nic_type_str = ", type=paravirtualized"
605
    else:
606
      nic_type_str = ", model=%s, type=ioemu" % nic_type
607
    for nic in instance.nics:
608
      nic_str = "mac=%s, bridge=%s%s" % (nic.mac, nic.bridge, nic_type_str)
609
      ip = getattr(nic, "ip", None)
610
      if ip is not None:
611
        nic_str += ", ip=%s" % ip
612
      vif_data.append("'%s'" % nic_str)
613

    
614
    config.write("vif = [%s]\n" % ",".join(vif_data))
615
    disk_data = cls._GetConfigFileDiskData(instance.disk_template,
616
                                            block_devices)
617
    disk_type = instance.hvparams[constants.HV_DISK_TYPE]
618
    if disk_type in (None, constants.HT_HVM_DEV_IOEMU):
619
      replacement = ",ioemu:hd"
620
    else:
621
      replacement = ",hd"
622
    disk_data = [line.replace(",sd", replacement) for line in disk_data]
623
    iso_path = instance.hvparams[constants.HV_CDROM_IMAGE_PATH]
624
    if iso_path:
625
      iso = "'file:%s,hdc:cdrom,r'" % iso_path
626
      disk_data.append(iso)
627

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

    
630
    config.write("on_poweroff = 'destroy'\n")
631
    config.write("on_reboot = 'restart'\n")
632
    config.write("on_crash = 'restart'\n")
633
    if extra_args:
634
      config.write("extra = '%s'\n" % extra_args)
635
    # just in case it exists
636
    utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
637
    try:
638
      f = open("/etc/xen/%s" % instance.name, "w")
639
      try:
640
        f.write(config.getvalue())
641
      finally:
642
        f.close()
643
    except IOError, err:
644
      raise errors.OpExecError("Cannot write Xen instance confile"
645
                               " file /etc/xen/%s: %s" % (instance.name, err))
646
    return True