Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ d89168ff

History | View | Annotate | Download (60.1 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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
"""Transportable objects for Ganeti.
23

24
This module provides small, mostly data-only objects which are safe to
25
pass to and from external parties.
26

27
"""
28

    
29
# pylint: disable=E0203,W0201,R0902
30

    
31
# E0203: Access to member %r before its definition, since we use
32
# objects.py which doesn't explicitly initialise its members
33

    
34
# W0201: Attribute '%s' defined outside __init__
35

    
36
# R0902: Allow instances of these objects to have more than 20 attributes
37

    
38
import ConfigParser
39
import re
40
import copy
41
import time
42
from cStringIO import StringIO
43

    
44
from ganeti import errors
45
from ganeti import constants
46
from ganeti import netutils
47
from ganeti import objectutils
48
from ganeti import utils
49

    
50
from socket import AF_INET
51

    
52

    
53
__all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
54
           "OS", "Node", "NodeGroup", "Cluster", "FillDict", "Network"]
55

    
56
_TIMESTAMPS = ["ctime", "mtime"]
57
_UUID = ["uuid"]
58

    
59

    
60
def FillDict(defaults_dict, custom_dict, skip_keys=None):
61
  """Basic function to apply settings on top a default dict.
62

63
  @type defaults_dict: dict
64
  @param defaults_dict: dictionary holding the default values
65
  @type custom_dict: dict
66
  @param custom_dict: dictionary holding customized value
67
  @type skip_keys: list
68
  @param skip_keys: which keys not to fill
69
  @rtype: dict
70
  @return: dict with the 'full' values
71

72
  """
73
  ret_dict = copy.deepcopy(defaults_dict)
74
  ret_dict.update(custom_dict)
75
  if skip_keys:
76
    for k in skip_keys:
77
      try:
78
        del ret_dict[k]
79
      except KeyError:
80
        pass
81
  return ret_dict
82

    
83

    
84
def FillIPolicy(default_ipolicy, custom_ipolicy, skip_keys=None):
85
  """Fills an instance policy with defaults.
86

87
  """
88
  assert frozenset(default_ipolicy.keys()) == constants.IPOLICY_ALL_KEYS
89
  ret_dict = {}
90
  for key in constants.IPOLICY_ISPECS:
91
    ret_dict[key] = FillDict(default_ipolicy[key],
92
                             custom_ipolicy.get(key, {}),
93
                             skip_keys=skip_keys)
94
  # list items
95
  for key in [constants.IPOLICY_DTS]:
96
    ret_dict[key] = list(custom_ipolicy.get(key, default_ipolicy[key]))
97
  # other items which we know we can directly copy (immutables)
98
  for key in constants.IPOLICY_PARAMETERS:
99
    ret_dict[key] = custom_ipolicy.get(key, default_ipolicy[key])
100

    
101
  return ret_dict
102

    
103

    
104
def FillDiskParams(default_dparams, custom_dparams, skip_keys=None):
105
  """Fills the disk parameter defaults.
106

107
  @see: L{FillDict} for parameters and return value
108

109
  """
110
  assert frozenset(default_dparams.keys()) == constants.DISK_TEMPLATES
111

    
112
  return dict((dt, FillDict(default_dparams[dt], custom_dparams.get(dt, {}),
113
                             skip_keys=skip_keys))
114
              for dt in constants.DISK_TEMPLATES)
115

    
116

    
117
def UpgradeGroupedParams(target, defaults):
118
  """Update all groups for the target parameter.
119

120
  @type target: dict of dicts
121
  @param target: {group: {parameter: value}}
122
  @type defaults: dict
123
  @param defaults: default parameter values
124

125
  """
126
  if target is None:
127
    target = {constants.PP_DEFAULT: defaults}
128
  else:
129
    for group in target:
130
      target[group] = FillDict(defaults, target[group])
131
  return target
132

    
133

    
134
def UpgradeBeParams(target):
135
  """Update the be parameters dict to the new format.
136

137
  @type target: dict
138
  @param target: "be" parameters dict
139

140
  """
141
  if constants.BE_MEMORY in target:
142
    memory = target[constants.BE_MEMORY]
143
    target[constants.BE_MAXMEM] = memory
144
    target[constants.BE_MINMEM] = memory
145
    del target[constants.BE_MEMORY]
146

    
147

    
148
def UpgradeDiskParams(diskparams):
149
  """Upgrade the disk parameters.
150

151
  @type diskparams: dict
152
  @param diskparams: disk parameters to upgrade
153
  @rtype: dict
154
  @return: the upgraded disk parameters dict
155

156
  """
157
  if not diskparams:
158
    result = {}
159
  else:
160
    result = FillDiskParams(constants.DISK_DT_DEFAULTS, diskparams)
161

    
162
  return result
163

    
164

    
165
def UpgradeNDParams(ndparams):
166
  """Upgrade ndparams structure.
167

168
  @type ndparams: dict
169
  @param ndparams: disk parameters to upgrade
170
  @rtype: dict
171
  @return: the upgraded node parameters dict
172

173
  """
174
  if ndparams is None:
175
    ndparams = {}
176

    
177
  if (constants.ND_OOB_PROGRAM in ndparams and
178
      ndparams[constants.ND_OOB_PROGRAM] is None):
179
    # will be reset by the line below
180
    del ndparams[constants.ND_OOB_PROGRAM]
181
  return FillDict(constants.NDC_DEFAULTS, ndparams)
182

    
183

    
184
def MakeEmptyIPolicy():
185
  """Create empty IPolicy dictionary.
186

187
  """
188
  return dict([
189
    (constants.ISPECS_MIN, {}),
190
    (constants.ISPECS_MAX, {}),
191
    (constants.ISPECS_STD, {}),
192
    ])
193

    
194

    
195
class ConfigObject(objectutils.ValidatedSlots):
196
  """A generic config object.
197

198
  It has the following properties:
199

200
    - provides somewhat safe recursive unpickling and pickling for its classes
201
    - unset attributes which are defined in slots are always returned
202
      as None instead of raising an error
203

204
  Classes derived from this must always declare __slots__ (we use many
205
  config objects and the memory reduction is useful)
206

207
  """
208
  __slots__ = []
209

    
210
  def __getattr__(self, name):
211
    if name not in self.GetAllSlots():
212
      raise AttributeError("Invalid object attribute %s.%s" %
213
                           (type(self).__name__, name))
214
    return None
215

    
216
  def __setstate__(self, state):
217
    slots = self.GetAllSlots()
218
    for name in state:
219
      if name in slots:
220
        setattr(self, name, state[name])
221

    
222
  def Validate(self):
223
    """Validates the slots.
224

225
    """
226

    
227
  def ToDict(self):
228
    """Convert to a dict holding only standard python types.
229

230
    The generic routine just dumps all of this object's attributes in
231
    a dict. It does not work if the class has children who are
232
    ConfigObjects themselves (e.g. the nics list in an Instance), in
233
    which case the object should subclass the function in order to
234
    make sure all objects returned are only standard python types.
235

236
    """
237
    result = {}
238
    for name in self.GetAllSlots():
239
      value = getattr(self, name, None)
240
      if value is not None:
241
        result[name] = value
242
    return result
243

    
244
  __getstate__ = ToDict
245

    
246
  @classmethod
247
  def FromDict(cls, val):
248
    """Create an object from a dictionary.
249

250
    This generic routine takes a dict, instantiates a new instance of
251
    the given class, and sets attributes based on the dict content.
252

253
    As for `ToDict`, this does not work if the class has children
254
    who are ConfigObjects themselves (e.g. the nics list in an
255
    Instance), in which case the object should subclass the function
256
    and alter the objects.
257

258
    """
259
    if not isinstance(val, dict):
260
      raise errors.ConfigurationError("Invalid object passed to FromDict:"
261
                                      " expected dict, got %s" % type(val))
262
    val_str = dict([(str(k), v) for k, v in val.iteritems()])
263
    obj = cls(**val_str) # pylint: disable=W0142
264
    return obj
265

    
266
  @staticmethod
267
  def _ContainerToDicts(container):
268
    """Convert the elements of a container to standard python types.
269

270
    This method converts a container with elements derived from
271
    ConfigData to standard python types. If the container is a dict,
272
    we don't touch the keys, only the values.
273

274
    """
275
    if isinstance(container, dict):
276
      ret = dict([(k, v.ToDict()) for k, v in container.iteritems()])
277
    elif isinstance(container, (list, tuple, set, frozenset)):
278
      ret = [elem.ToDict() for elem in container]
279
    else:
280
      raise TypeError("Invalid type %s passed to _ContainerToDicts" %
281
                      type(container))
282
    return ret
283

    
284
  @staticmethod
285
  def _ContainerFromDicts(source, c_type, e_type):
286
    """Convert a container from standard python types.
287

288
    This method converts a container with standard python types to
289
    ConfigData objects. If the container is a dict, we don't touch the
290
    keys, only the values.
291

292
    """
293
    if not isinstance(c_type, type):
294
      raise TypeError("Container type %s passed to _ContainerFromDicts is"
295
                      " not a type" % type(c_type))
296
    if source is None:
297
      source = c_type()
298
    if c_type is dict:
299
      ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
300
    elif c_type in (list, tuple, set, frozenset):
301
      ret = c_type([e_type.FromDict(elem) for elem in source])
302
    else:
303
      raise TypeError("Invalid container type %s passed to"
304
                      " _ContainerFromDicts" % c_type)
305
    return ret
306

    
307
  def Copy(self):
308
    """Makes a deep copy of the current object and its children.
309

310
    """
311
    dict_form = self.ToDict()
312
    clone_obj = self.__class__.FromDict(dict_form)
313
    return clone_obj
314

    
315
  def __repr__(self):
316
    """Implement __repr__ for ConfigObjects."""
317
    return repr(self.ToDict())
318

    
319
  def UpgradeConfig(self):
320
    """Fill defaults for missing configuration values.
321

322
    This method will be called at configuration load time, and its
323
    implementation will be object dependent.
324

325
    """
326
    pass
327

    
328

    
329
class TaggableObject(ConfigObject):
330
  """An generic class supporting tags.
331

332
  """
333
  __slots__ = ["tags"]
334
  VALID_TAG_RE = re.compile("^[\w.+*/:@-]+$")
335

    
336
  @classmethod
337
  def ValidateTag(cls, tag):
338
    """Check if a tag is valid.
339

340
    If the tag is invalid, an errors.TagError will be raised. The
341
    function has no return value.
342

343
    """
344
    if not isinstance(tag, basestring):
345
      raise errors.TagError("Invalid tag type (not a string)")
346
    if len(tag) > constants.MAX_TAG_LEN:
347
      raise errors.TagError("Tag too long (>%d characters)" %
348
                            constants.MAX_TAG_LEN)
349
    if not tag:
350
      raise errors.TagError("Tags cannot be empty")
351
    if not cls.VALID_TAG_RE.match(tag):
352
      raise errors.TagError("Tag contains invalid characters")
353

    
354
  def GetTags(self):
355
    """Return the tags list.
356

357
    """
358
    tags = getattr(self, "tags", None)
359
    if tags is None:
360
      tags = self.tags = set()
361
    return tags
362

    
363
  def AddTag(self, tag):
364
    """Add a new tag.
365

366
    """
367
    self.ValidateTag(tag)
368
    tags = self.GetTags()
369
    if len(tags) >= constants.MAX_TAGS_PER_OBJ:
370
      raise errors.TagError("Too many tags")
371
    self.GetTags().add(tag)
372

    
373
  def RemoveTag(self, tag):
374
    """Remove a tag.
375

376
    """
377
    self.ValidateTag(tag)
378
    tags = self.GetTags()
379
    try:
380
      tags.remove(tag)
381
    except KeyError:
382
      raise errors.TagError("Tag not found")
383

    
384
  def ToDict(self):
385
    """Taggable-object-specific conversion to standard python types.
386

387
    This replaces the tags set with a list.
388

389
    """
390
    bo = super(TaggableObject, self).ToDict()
391

    
392
    tags = bo.get("tags", None)
393
    if isinstance(tags, set):
394
      bo["tags"] = list(tags)
395
    return bo
396

    
397
  @classmethod
398
  def FromDict(cls, val):
399
    """Custom function for instances.
400

401
    """
402
    obj = super(TaggableObject, cls).FromDict(val)
403
    if hasattr(obj, "tags") and isinstance(obj.tags, list):
404
      obj.tags = set(obj.tags)
405
    return obj
406

    
407

    
408
class MasterNetworkParameters(ConfigObject):
409
  """Network configuration parameters for the master
410

411
  @ivar name: master name
412
  @ivar ip: master IP
413
  @ivar netmask: master netmask
414
  @ivar netdev: master network device
415
  @ivar ip_family: master IP family
416

417
  """
418
  __slots__ = [
419
    "name",
420
    "ip",
421
    "netmask",
422
    "netdev",
423
    "ip_family",
424
    ]
425

    
426

    
427
class ConfigData(ConfigObject):
428
  """Top-level config object."""
429
  __slots__ = [
430
    "version",
431
    "cluster",
432
    "nodes",
433
    "nodegroups",
434
    "instances",
435
    "networks",
436
    "serial_no",
437
    ] + _TIMESTAMPS
438

    
439
  def ToDict(self):
440
    """Custom function for top-level config data.
441

442
    This just replaces the list of instances, nodes and the cluster
443
    with standard python types.
444

445
    """
446
    mydict = super(ConfigData, self).ToDict()
447
    mydict["cluster"] = mydict["cluster"].ToDict()
448
    for key in "nodes", "instances", "nodegroups", "networks":
449
      mydict[key] = self._ContainerToDicts(mydict[key])
450

    
451
    return mydict
452

    
453
  @classmethod
454
  def FromDict(cls, val):
455
    """Custom function for top-level config data
456

457
    """
458
    obj = super(ConfigData, cls).FromDict(val)
459
    obj.cluster = Cluster.FromDict(obj.cluster)
460
    obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
461
    obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
462
    obj.nodegroups = cls._ContainerFromDicts(obj.nodegroups, dict, NodeGroup)
463
    obj.networks = cls._ContainerFromDicts(obj.networks, dict, Network)
464
    return obj
465

    
466
  def HasAnyDiskOfType(self, dev_type):
467
    """Check if in there is at disk of the given type in the configuration.
468

469
    @type dev_type: L{constants.LDS_BLOCK}
470
    @param dev_type: the type to look for
471
    @rtype: boolean
472
    @return: boolean indicating if a disk of the given type was found or not
473

474
    """
475
    for instance in self.instances.values():
476
      for disk in instance.disks:
477
        if disk.IsBasedOnDiskType(dev_type):
478
          return True
479
    return False
480

    
481
  def UpgradeConfig(self):
482
    """Fill defaults for missing configuration values.
483

484
    """
485
    self.cluster.UpgradeConfig()
486
    for node in self.nodes.values():
487
      node.UpgradeConfig()
488
    for instance in self.instances.values():
489
      instance.UpgradeConfig()
490
    if self.nodegroups is None:
491
      self.nodegroups = {}
492
    for nodegroup in self.nodegroups.values():
493
      nodegroup.UpgradeConfig()
494
    if self.cluster.drbd_usermode_helper is None:
495
      # To decide if we set an helper let's check if at least one instance has
496
      # a DRBD disk. This does not cover all the possible scenarios but it
497
      # gives a good approximation.
498
      if self.HasAnyDiskOfType(constants.LD_DRBD8):
499
        self.cluster.drbd_usermode_helper = constants.DEFAULT_DRBD_HELPER
500
    if self.networks is None:
501
      self.networks = {}
502
    for network in self.networks.values():
503
      network.UpgradeConfig()
504

    
505

    
506
class NIC(ConfigObject):
507
  """Config object representing a network card."""
508
  __slots__ = ["mac", "ip", "network", "nicparams", "netinfo"]
509

    
510
  @classmethod
511
  def CheckParameterSyntax(cls, nicparams):
512
    """Check the given parameters for validity.
513

514
    @type nicparams:  dict
515
    @param nicparams: dictionary with parameter names/value
516
    @raise errors.ConfigurationError: when a parameter is not valid
517

518
    """
519
    mode = nicparams[constants.NIC_MODE]
520
    if (mode not in constants.NIC_VALID_MODES and
521
        mode != constants.VALUE_AUTO):
522
      raise errors.ConfigurationError("Invalid NIC mode '%s'" % mode)
523

    
524
    if (mode == constants.NIC_MODE_BRIDGED and
525
        not nicparams[constants.NIC_LINK]):
526
      raise errors.ConfigurationError("Missing bridged NIC link")
527

    
528

    
529
class Disk(ConfigObject):
530
  """Config object representing a block device."""
531
  __slots__ = ["dev_type", "logical_id", "physical_id",
532
               "children", "iv_name", "size", "mode", "params"]
533

    
534
  def CreateOnSecondary(self):
535
    """Test if this device needs to be created on a secondary node."""
536
    return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
537

    
538
  def AssembleOnSecondary(self):
539
    """Test if this device needs to be assembled on a secondary node."""
540
    return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
541

    
542
  def OpenOnSecondary(self):
543
    """Test if this device needs to be opened on a secondary node."""
544
    return self.dev_type in (constants.LD_LV,)
545

    
546
  def StaticDevPath(self):
547
    """Return the device path if this device type has a static one.
548

549
    Some devices (LVM for example) live always at the same /dev/ path,
550
    irrespective of their status. For such devices, we return this
551
    path, for others we return None.
552

553
    @warning: The path returned is not a normalized pathname; callers
554
        should check that it is a valid path.
555

556
    """
557
    if self.dev_type == constants.LD_LV:
558
      return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
559
    elif self.dev_type == constants.LD_BLOCKDEV:
560
      return self.logical_id[1]
561
    elif self.dev_type == constants.LD_RBD:
562
      return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
563
    return None
564

    
565
  def ChildrenNeeded(self):
566
    """Compute the needed number of children for activation.
567

568
    This method will return either -1 (all children) or a positive
569
    number denoting the minimum number of children needed for
570
    activation (only mirrored devices will usually return >=0).
571

572
    Currently, only DRBD8 supports diskless activation (therefore we
573
    return 0), for all other we keep the previous semantics and return
574
    -1.
575

576
    """
577
    if self.dev_type == constants.LD_DRBD8:
578
      return 0
579
    return -1
580

    
581
  def IsBasedOnDiskType(self, dev_type):
582
    """Check if the disk or its children are based on the given type.
583

584
    @type dev_type: L{constants.LDS_BLOCK}
585
    @param dev_type: the type to look for
586
    @rtype: boolean
587
    @return: boolean indicating if a device of the given type was found or not
588

589
    """
590
    if self.children:
591
      for child in self.children:
592
        if child.IsBasedOnDiskType(dev_type):
593
          return True
594
    return self.dev_type == dev_type
595

    
596
  def GetNodes(self, node):
597
    """This function returns the nodes this device lives on.
598

599
    Given the node on which the parent of the device lives on (or, in
600
    case of a top-level device, the primary node of the devices'
601
    instance), this function will return a list of nodes on which this
602
    devices needs to (or can) be assembled.
603

604
    """
605
    if self.dev_type in [constants.LD_LV, constants.LD_FILE,
606
                         constants.LD_BLOCKDEV, constants.LD_RBD,
607
                         constants.LD_EXT]:
608
      result = [node]
609
    elif self.dev_type in constants.LDS_DRBD:
610
      result = [self.logical_id[0], self.logical_id[1]]
611
      if node not in result:
612
        raise errors.ConfigurationError("DRBD device passed unknown node")
613
    else:
614
      raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
615
    return result
616

    
617
  def ComputeNodeTree(self, parent_node):
618
    """Compute the node/disk tree for this disk and its children.
619

620
    This method, given the node on which the parent disk lives, will
621
    return the list of all (node, disk) pairs which describe the disk
622
    tree in the most compact way. For example, a drbd/lvm stack
623
    will be returned as (primary_node, drbd) and (secondary_node, drbd)
624
    which represents all the top-level devices on the nodes.
625

626
    """
627
    my_nodes = self.GetNodes(parent_node)
628
    result = [(node, self) for node in my_nodes]
629
    if not self.children:
630
      # leaf device
631
      return result
632
    for node in my_nodes:
633
      for child in self.children:
634
        child_result = child.ComputeNodeTree(node)
635
        if len(child_result) == 1:
636
          # child (and all its descendants) is simple, doesn't split
637
          # over multiple hosts, so we don't need to describe it, our
638
          # own entry for this node describes it completely
639
          continue
640
        else:
641
          # check if child nodes differ from my nodes; note that
642
          # subdisk can differ from the child itself, and be instead
643
          # one of its descendants
644
          for subnode, subdisk in child_result:
645
            if subnode not in my_nodes:
646
              result.append((subnode, subdisk))
647
            # otherwise child is under our own node, so we ignore this
648
            # entry (but probably the other results in the list will
649
            # be different)
650
    return result
651

    
652
  def ComputeGrowth(self, amount):
653
    """Compute the per-VG growth requirements.
654

655
    This only works for VG-based disks.
656

657
    @type amount: integer
658
    @param amount: the desired increase in (user-visible) disk space
659
    @rtype: dict
660
    @return: a dictionary of volume-groups and the required size
661

662
    """
663
    if self.dev_type == constants.LD_LV:
664
      return {self.logical_id[0]: amount}
665
    elif self.dev_type == constants.LD_DRBD8:
666
      if self.children:
667
        return self.children[0].ComputeGrowth(amount)
668
      else:
669
        return {}
670
    else:
671
      # Other disk types do not require VG space
672
      return {}
673

    
674
  def RecordGrow(self, amount):
675
    """Update the size of this disk after growth.
676

677
    This method recurses over the disks's children and updates their
678
    size correspondigly. The method needs to be kept in sync with the
679
    actual algorithms from bdev.
680

681
    """
682
    if self.dev_type in (constants.LD_LV, constants.LD_FILE,
683
                         constants.LD_RBD, constants.LD_EXT):
684
      self.size += amount
685
    elif self.dev_type == constants.LD_DRBD8:
686
      if self.children:
687
        self.children[0].RecordGrow(amount)
688
      self.size += amount
689
    else:
690
      raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
691
                                   " disk type %s" % self.dev_type)
692

    
693
  def Update(self, size=None, mode=None):
694
    """Apply changes to size and mode.
695

696
    """
697
    if self.dev_type == constants.LD_DRBD8:
698
      if self.children:
699
        self.children[0].Update(size=size, mode=mode)
700
    else:
701
      assert not self.children
702

    
703
    if size is not None:
704
      self.size = size
705
    if mode is not None:
706
      self.mode = mode
707

    
708
  def UnsetSize(self):
709
    """Sets recursively the size to zero for the disk and its children.
710

711
    """
712
    if self.children:
713
      for child in self.children:
714
        child.UnsetSize()
715
    self.size = 0
716

    
717
  def SetPhysicalID(self, target_node, nodes_ip):
718
    """Convert the logical ID to the physical ID.
719

720
    This is used only for drbd, which needs ip/port configuration.
721

722
    The routine descends down and updates its children also, because
723
    this helps when the only the top device is passed to the remote
724
    node.
725

726
    Arguments:
727
      - target_node: the node we wish to configure for
728
      - nodes_ip: a mapping of node name to ip
729

730
    The target_node must exist in in nodes_ip, and must be one of the
731
    nodes in the logical ID for each of the DRBD devices encountered
732
    in the disk tree.
733

734
    """
735
    if self.children:
736
      for child in self.children:
737
        child.SetPhysicalID(target_node, nodes_ip)
738

    
739
    if self.logical_id is None and self.physical_id is not None:
740
      return
741
    if self.dev_type in constants.LDS_DRBD:
742
      pnode, snode, port, pminor, sminor, secret = self.logical_id
743
      if target_node not in (pnode, snode):
744
        raise errors.ConfigurationError("DRBD device not knowing node %s" %
745
                                        target_node)
746
      pnode_ip = nodes_ip.get(pnode, None)
747
      snode_ip = nodes_ip.get(snode, None)
748
      if pnode_ip is None or snode_ip is None:
749
        raise errors.ConfigurationError("Can't find primary or secondary node"
750
                                        " for %s" % str(self))
751
      p_data = (pnode_ip, port)
752
      s_data = (snode_ip, port)
753
      if pnode == target_node:
754
        self.physical_id = p_data + s_data + (pminor, secret)
755
      else: # it must be secondary, we tested above
756
        self.physical_id = s_data + p_data + (sminor, secret)
757
    else:
758
      self.physical_id = self.logical_id
759
    return
760

    
761
  def ToDict(self):
762
    """Disk-specific conversion to standard python types.
763

764
    This replaces the children lists of objects with lists of
765
    standard python types.
766

767
    """
768
    bo = super(Disk, self).ToDict()
769

    
770
    for attr in ("children",):
771
      alist = bo.get(attr, None)
772
      if alist:
773
        bo[attr] = self._ContainerToDicts(alist)
774
    return bo
775

    
776
  @classmethod
777
  def FromDict(cls, val):
778
    """Custom function for Disks
779

780
    """
781
    obj = super(Disk, cls).FromDict(val)
782
    if obj.children:
783
      obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
784
    if obj.logical_id and isinstance(obj.logical_id, list):
785
      obj.logical_id = tuple(obj.logical_id)
786
    if obj.physical_id and isinstance(obj.physical_id, list):
787
      obj.physical_id = tuple(obj.physical_id)
788
    if obj.dev_type in constants.LDS_DRBD:
789
      # we need a tuple of length six here
790
      if len(obj.logical_id) < 6:
791
        obj.logical_id += (None,) * (6 - len(obj.logical_id))
792
    return obj
793

    
794
  def __str__(self):
795
    """Custom str() formatter for disks.
796

797
    """
798
    if self.dev_type == constants.LD_LV:
799
      val = "<LogicalVolume(/dev/%s/%s" % self.logical_id
800
    elif self.dev_type in constants.LDS_DRBD:
801
      node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
802
      val = "<DRBD8("
803
      if self.physical_id is None:
804
        phy = "unconfigured"
805
      else:
806
        phy = ("configured as %s:%s %s:%s" %
807
               (self.physical_id[0], self.physical_id[1],
808
                self.physical_id[2], self.physical_id[3]))
809

    
810
      val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
811
              (node_a, minor_a, node_b, minor_b, port, phy))
812
      if self.children and self.children.count(None) == 0:
813
        val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
814
      else:
815
        val += "no local storage"
816
    else:
817
      val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
818
             (self.dev_type, self.logical_id, self.physical_id, self.children))
819
    if self.iv_name is None:
820
      val += ", not visible"
821
    else:
822
      val += ", visible as /dev/%s" % self.iv_name
823
    if isinstance(self.size, int):
824
      val += ", size=%dm)>" % self.size
825
    else:
826
      val += ", size='%s')>" % (self.size,)
827
    return val
828

    
829
  def Verify(self):
830
    """Checks that this disk is correctly configured.
831

832
    """
833
    all_errors = []
834
    if self.mode not in constants.DISK_ACCESS_SET:
835
      all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
836
    return all_errors
837

    
838
  def UpgradeConfig(self):
839
    """Fill defaults for missing configuration values.
840

841
    """
842
    if self.children:
843
      for child in self.children:
844
        child.UpgradeConfig()
845

    
846
    # FIXME: Make this configurable in Ganeti 2.7
847
    self.params = {}
848
    # add here config upgrade for this disk
849

    
850
  @staticmethod
851
  def ComputeLDParams(disk_template, disk_params):
852
    """Computes Logical Disk parameters from Disk Template parameters.
853

854
    @type disk_template: string
855
    @param disk_template: disk template, one of L{constants.DISK_TEMPLATES}
856
    @type disk_params: dict
857
    @param disk_params: disk template parameters;
858
                        dict(template_name -> parameters
859
    @rtype: list(dict)
860
    @return: a list of dicts, one for each node of the disk hierarchy. Each dict
861
      contains the LD parameters of the node. The tree is flattened in-order.
862

863
    """
864
    if disk_template not in constants.DISK_TEMPLATES:
865
      raise errors.ProgrammerError("Unknown disk template %s" % disk_template)
866

    
867
    assert disk_template in disk_params
868

    
869
    result = list()
870
    dt_params = disk_params[disk_template]
871
    if disk_template == constants.DT_DRBD8:
872
      result.append(FillDict(constants.DISK_LD_DEFAULTS[constants.LD_DRBD8], {
873
        constants.LDP_RESYNC_RATE: dt_params[constants.DRBD_RESYNC_RATE],
874
        constants.LDP_BARRIERS: dt_params[constants.DRBD_DISK_BARRIERS],
875
        constants.LDP_NO_META_FLUSH: dt_params[constants.DRBD_META_BARRIERS],
876
        constants.LDP_DEFAULT_METAVG: dt_params[constants.DRBD_DEFAULT_METAVG],
877
        constants.LDP_DISK_CUSTOM: dt_params[constants.DRBD_DISK_CUSTOM],
878
        constants.LDP_NET_CUSTOM: dt_params[constants.DRBD_NET_CUSTOM],
879
        constants.LDP_DYNAMIC_RESYNC: dt_params[constants.DRBD_DYNAMIC_RESYNC],
880
        constants.LDP_PLAN_AHEAD: dt_params[constants.DRBD_PLAN_AHEAD],
881
        constants.LDP_FILL_TARGET: dt_params[constants.DRBD_FILL_TARGET],
882
        constants.LDP_DELAY_TARGET: dt_params[constants.DRBD_DELAY_TARGET],
883
        constants.LDP_MAX_RATE: dt_params[constants.DRBD_MAX_RATE],
884
        constants.LDP_MIN_RATE: dt_params[constants.DRBD_MIN_RATE],
885
        }))
886

    
887
      # data LV
888
      result.append(FillDict(constants.DISK_LD_DEFAULTS[constants.LD_LV], {
889
        constants.LDP_STRIPES: dt_params[constants.DRBD_DATA_STRIPES],
890
        }))
891

    
892
      # metadata LV
893
      result.append(FillDict(constants.DISK_LD_DEFAULTS[constants.LD_LV], {
894
        constants.LDP_STRIPES: dt_params[constants.DRBD_META_STRIPES],
895
        }))
896

    
897
    elif disk_template in (constants.DT_FILE, constants.DT_SHARED_FILE):
898
      result.append(constants.DISK_LD_DEFAULTS[constants.LD_FILE])
899

    
900
    elif disk_template == constants.DT_PLAIN:
901
      result.append(FillDict(constants.DISK_LD_DEFAULTS[constants.LD_LV], {
902
        constants.LDP_STRIPES: dt_params[constants.LV_STRIPES],
903
        }))
904

    
905
    elif disk_template == constants.DT_BLOCK:
906
      result.append(constants.DISK_LD_DEFAULTS[constants.LD_BLOCKDEV])
907

    
908
    elif disk_template == constants.DT_RBD:
909
      result.append(FillDict(constants.DISK_LD_DEFAULTS[constants.LD_RBD], {
910
        constants.LDP_POOL: dt_params[constants.RBD_POOL],
911
        }))
912

    
913
    elif disk_template == constants.DT_EXT:
914
      result.append(constants.DISK_LD_DEFAULTS[constants.LD_EXT])
915

    
916
    return result
917

    
918

    
919
class InstancePolicy(ConfigObject):
920
  """Config object representing instance policy limits dictionary.
921

922

923
  Note that this object is not actually used in the config, it's just
924
  used as a placeholder for a few functions.
925

926
  """
927
  @classmethod
928
  def CheckParameterSyntax(cls, ipolicy, check_std):
929
    """ Check the instance policy for validity.
930

931
    """
932
    for param in constants.ISPECS_PARAMETERS:
933
      InstancePolicy.CheckISpecSyntax(ipolicy, param, check_std)
934
    if constants.IPOLICY_DTS in ipolicy:
935
      InstancePolicy.CheckDiskTemplates(ipolicy[constants.IPOLICY_DTS])
936
    for key in constants.IPOLICY_PARAMETERS:
937
      if key in ipolicy:
938
        InstancePolicy.CheckParameter(key, ipolicy[key])
939
    wrong_keys = frozenset(ipolicy.keys()) - constants.IPOLICY_ALL_KEYS
940
    if wrong_keys:
941
      raise errors.ConfigurationError("Invalid keys in ipolicy: %s" %
942
                                      utils.CommaJoin(wrong_keys))
943

    
944
  @classmethod
945
  def CheckISpecSyntax(cls, ipolicy, name, check_std):
946
    """Check the instance policy for validity on a given key.
947

948
    We check if the instance policy makes sense for a given key, that is
949
    if ipolicy[min][name] <= ipolicy[std][name] <= ipolicy[max][name].
950

951
    @type ipolicy: dict
952
    @param ipolicy: dictionary with min, max, std specs
953
    @type name: string
954
    @param name: what are the limits for
955
    @type check_std: bool
956
    @param check_std: Whether to check std value or just assume compliance
957
    @raise errors.ConfigureError: when specs for given name are not valid
958

959
    """
960
    min_v = ipolicy[constants.ISPECS_MIN].get(name, 0)
961

    
962
    if check_std:
963
      std_v = ipolicy[constants.ISPECS_STD].get(name, min_v)
964
      std_msg = std_v
965
    else:
966
      std_v = min_v
967
      std_msg = "-"
968

    
969
    max_v = ipolicy[constants.ISPECS_MAX].get(name, std_v)
970
    err = ("Invalid specification of min/max/std values for %s: %s/%s/%s" %
971
           (name,
972
            ipolicy[constants.ISPECS_MIN].get(name, "-"),
973
            ipolicy[constants.ISPECS_MAX].get(name, "-"),
974
            std_msg))
975
    if min_v > std_v or std_v > max_v:
976
      raise errors.ConfigurationError(err)
977

    
978
  @classmethod
979
  def CheckDiskTemplates(cls, disk_templates):
980
    """Checks the disk templates for validity.
981

982
    """
983
    wrong = frozenset(disk_templates).difference(constants.DISK_TEMPLATES)
984
    if wrong:
985
      raise errors.ConfigurationError("Invalid disk template(s) %s" %
986
                                      utils.CommaJoin(wrong))
987

    
988
  @classmethod
989
  def CheckParameter(cls, key, value):
990
    """Checks a parameter.
991

992
    Currently we expect all parameters to be float values.
993

994
    """
995
    try:
996
      float(value)
997
    except (TypeError, ValueError), err:
998
      raise errors.ConfigurationError("Invalid value for key" " '%s':"
999
                                      " '%s', error: %s" % (key, value, err))
1000

    
1001

    
1002
class Instance(TaggableObject):
1003
  """Config object representing an instance."""
1004
  __slots__ = [
1005
    "name",
1006
    "primary_node",
1007
    "os",
1008
    "hypervisor",
1009
    "hvparams",
1010
    "beparams",
1011
    "osparams",
1012
    "admin_state",
1013
    "nics",
1014
    "disks",
1015
    "disk_template",
1016
    "network_port",
1017
    "serial_no",
1018
    ] + _TIMESTAMPS + _UUID
1019

    
1020
  def _ComputeSecondaryNodes(self):
1021
    """Compute the list of secondary nodes.
1022

1023
    This is a simple wrapper over _ComputeAllNodes.
1024

1025
    """
1026
    all_nodes = set(self._ComputeAllNodes())
1027
    all_nodes.discard(self.primary_node)
1028
    return tuple(all_nodes)
1029

    
1030
  secondary_nodes = property(_ComputeSecondaryNodes, None, None,
1031
                             "List of names of secondary nodes")
1032

    
1033
  def _ComputeAllNodes(self):
1034
    """Compute the list of all nodes.
1035

1036
    Since the data is already there (in the drbd disks), keeping it as
1037
    a separate normal attribute is redundant and if not properly
1038
    synchronised can cause problems. Thus it's better to compute it
1039
    dynamically.
1040

1041
    """
1042
    def _Helper(nodes, device):
1043
      """Recursively computes nodes given a top device."""
1044
      if device.dev_type in constants.LDS_DRBD:
1045
        nodea, nodeb = device.logical_id[:2]
1046
        nodes.add(nodea)
1047
        nodes.add(nodeb)
1048
      if device.children:
1049
        for child in device.children:
1050
          _Helper(nodes, child)
1051

    
1052
    all_nodes = set()
1053
    all_nodes.add(self.primary_node)
1054
    for device in self.disks:
1055
      _Helper(all_nodes, device)
1056
    return tuple(all_nodes)
1057

    
1058
  all_nodes = property(_ComputeAllNodes, None, None,
1059
                       "List of names of all the nodes of the instance")
1060

    
1061
  def MapLVsByNode(self, lvmap=None, devs=None, node=None):
1062
    """Provide a mapping of nodes to LVs this instance owns.
1063

1064
    This function figures out what logical volumes should belong on
1065
    which nodes, recursing through a device tree.
1066

1067
    @param lvmap: optional dictionary to receive the
1068
        'node' : ['lv', ...] data.
1069

1070
    @return: None if lvmap arg is given, otherwise, a dictionary of
1071
        the form { 'nodename' : ['volume1', 'volume2', ...], ... };
1072
        volumeN is of the form "vg_name/lv_name", compatible with
1073
        GetVolumeList()
1074

1075
    """
1076
    if node is None:
1077
      node = self.primary_node
1078

    
1079
    if lvmap is None:
1080
      lvmap = {
1081
        node: [],
1082
        }
1083
      ret = lvmap
1084
    else:
1085
      if not node in lvmap:
1086
        lvmap[node] = []
1087
      ret = None
1088

    
1089
    if not devs:
1090
      devs = self.disks
1091

    
1092
    for dev in devs:
1093
      if dev.dev_type == constants.LD_LV:
1094
        lvmap[node].append(dev.logical_id[0] + "/" + dev.logical_id[1])
1095

    
1096
      elif dev.dev_type in constants.LDS_DRBD:
1097
        if dev.children:
1098
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
1099
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
1100

    
1101
      elif dev.children:
1102
        self.MapLVsByNode(lvmap, dev.children, node)
1103

    
1104
    return ret
1105

    
1106
  def FindDisk(self, idx):
1107
    """Find a disk given having a specified index.
1108

1109
    This is just a wrapper that does validation of the index.
1110

1111
    @type idx: int
1112
    @param idx: the disk index
1113
    @rtype: L{Disk}
1114
    @return: the corresponding disk
1115
    @raise errors.OpPrereqError: when the given index is not valid
1116

1117
    """
1118
    try:
1119
      idx = int(idx)
1120
      return self.disks[idx]
1121
    except (TypeError, ValueError), err:
1122
      raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err),
1123
                                 errors.ECODE_INVAL)
1124
    except IndexError:
1125
      raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
1126
                                 " 0 to %d" % (idx, len(self.disks) - 1),
1127
                                 errors.ECODE_INVAL)
1128

    
1129
  def ToDict(self):
1130
    """Instance-specific conversion to standard python types.
1131

1132
    This replaces the children lists of objects with lists of standard
1133
    python types.
1134

1135
    """
1136
    bo = super(Instance, self).ToDict()
1137

    
1138
    for attr in "nics", "disks":
1139
      alist = bo.get(attr, None)
1140
      if alist:
1141
        nlist = self._ContainerToDicts(alist)
1142
      else:
1143
        nlist = []
1144
      bo[attr] = nlist
1145
    return bo
1146

    
1147
  @classmethod
1148
  def FromDict(cls, val):
1149
    """Custom function for instances.
1150

1151
    """
1152
    if "admin_state" not in val:
1153
      if val.get("admin_up", False):
1154
        val["admin_state"] = constants.ADMINST_UP
1155
      else:
1156
        val["admin_state"] = constants.ADMINST_DOWN
1157
    if "admin_up" in val:
1158
      del val["admin_up"]
1159
    obj = super(Instance, cls).FromDict(val)
1160
    obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
1161
    obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
1162
    return obj
1163

    
1164
  def UpgradeConfig(self):
1165
    """Fill defaults for missing configuration values.
1166

1167
    """
1168
    for nic in self.nics:
1169
      nic.UpgradeConfig()
1170
    for disk in self.disks:
1171
      disk.UpgradeConfig()
1172
    if self.hvparams:
1173
      for key in constants.HVC_GLOBALS:
1174
        try:
1175
          del self.hvparams[key]
1176
        except KeyError:
1177
          pass
1178
    if self.osparams is None:
1179
      self.osparams = {}
1180
    UpgradeBeParams(self.beparams)
1181

    
1182

    
1183
class OS(ConfigObject):
1184
  """Config object representing an operating system.
1185

1186
  @type supported_parameters: list
1187
  @ivar supported_parameters: a list of tuples, name and description,
1188
      containing the supported parameters by this OS
1189

1190
  @type VARIANT_DELIM: string
1191
  @cvar VARIANT_DELIM: the variant delimiter
1192

1193
  """
1194
  __slots__ = [
1195
    "name",
1196
    "path",
1197
    "api_versions",
1198
    "create_script",
1199
    "export_script",
1200
    "import_script",
1201
    "rename_script",
1202
    "verify_script",
1203
    "supported_variants",
1204
    "supported_parameters",
1205
    ]
1206

    
1207
  VARIANT_DELIM = "+"
1208

    
1209
  @classmethod
1210
  def SplitNameVariant(cls, name):
1211
    """Splits the name into the proper name and variant.
1212

1213
    @param name: the OS (unprocessed) name
1214
    @rtype: list
1215
    @return: a list of two elements; if the original name didn't
1216
        contain a variant, it's returned as an empty string
1217

1218
    """
1219
    nv = name.split(cls.VARIANT_DELIM, 1)
1220
    if len(nv) == 1:
1221
      nv.append("")
1222
    return nv
1223

    
1224
  @classmethod
1225
  def GetName(cls, name):
1226
    """Returns the proper name of the os (without the variant).
1227

1228
    @param name: the OS (unprocessed) name
1229

1230
    """
1231
    return cls.SplitNameVariant(name)[0]
1232

    
1233
  @classmethod
1234
  def GetVariant(cls, name):
1235
    """Returns the variant the os (without the base name).
1236

1237
    @param name: the OS (unprocessed) name
1238

1239
    """
1240
    return cls.SplitNameVariant(name)[1]
1241

    
1242

    
1243
class ExtStorage(ConfigObject):
1244
  """Config object representing an External Storage Provider.
1245

1246
  """
1247
  __slots__ = [
1248
    "name",
1249
    "path",
1250
    "create_script",
1251
    "remove_script",
1252
    "grow_script",
1253
    "attach_script",
1254
    "detach_script",
1255
    "setinfo_script",
1256
    "verify_script",
1257
    "supported_parameters",
1258
    ]
1259

    
1260

    
1261
class NodeHvState(ConfigObject):
1262
  """Hypvervisor state on a node.
1263

1264
  @ivar mem_total: Total amount of memory
1265
  @ivar mem_node: Memory used by, or reserved for, the node itself (not always
1266
    available)
1267
  @ivar mem_hv: Memory used by hypervisor or lost due to instance allocation
1268
    rounding
1269
  @ivar mem_inst: Memory used by instances living on node
1270
  @ivar cpu_total: Total node CPU core count
1271
  @ivar cpu_node: Number of CPU cores reserved for the node itself
1272

1273
  """
1274
  __slots__ = [
1275
    "mem_total",
1276
    "mem_node",
1277
    "mem_hv",
1278
    "mem_inst",
1279
    "cpu_total",
1280
    "cpu_node",
1281
    ] + _TIMESTAMPS
1282

    
1283

    
1284
class NodeDiskState(ConfigObject):
1285
  """Disk state on a node.
1286

1287
  """
1288
  __slots__ = [
1289
    "total",
1290
    "reserved",
1291
    "overhead",
1292
    ] + _TIMESTAMPS
1293

    
1294

    
1295
class Node(TaggableObject):
1296
  """Config object representing a node.
1297

1298
  @ivar hv_state: Hypervisor state (e.g. number of CPUs)
1299
  @ivar hv_state_static: Hypervisor state overriden by user
1300
  @ivar disk_state: Disk state (e.g. free space)
1301
  @ivar disk_state_static: Disk state overriden by user
1302

1303
  """
1304
  __slots__ = [
1305
    "name",
1306
    "primary_ip",
1307
    "secondary_ip",
1308
    "serial_no",
1309
    "master_candidate",
1310
    "offline",
1311
    "drained",
1312
    "group",
1313
    "master_capable",
1314
    "vm_capable",
1315
    "ndparams",
1316
    "powered",
1317
    "hv_state",
1318
    "hv_state_static",
1319
    "disk_state",
1320
    "disk_state_static",
1321
    ] + _TIMESTAMPS + _UUID
1322

    
1323
  def UpgradeConfig(self):
1324
    """Fill defaults for missing configuration values.
1325

1326
    """
1327
    # pylint: disable=E0203
1328
    # because these are "defined" via slots, not manually
1329
    if self.master_capable is None:
1330
      self.master_capable = True
1331

    
1332
    if self.vm_capable is None:
1333
      self.vm_capable = True
1334

    
1335
    if self.ndparams is None:
1336
      self.ndparams = {}
1337

    
1338
    if self.powered is None:
1339
      self.powered = True
1340

    
1341
  def ToDict(self):
1342
    """Custom function for serializing.
1343

1344
    """
1345
    data = super(Node, self).ToDict()
1346

    
1347
    hv_state = data.get("hv_state", None)
1348
    if hv_state is not None:
1349
      data["hv_state"] = self._ContainerToDicts(hv_state)
1350

    
1351
    disk_state = data.get("disk_state", None)
1352
    if disk_state is not None:
1353
      data["disk_state"] = \
1354
        dict((key, self._ContainerToDicts(value))
1355
             for (key, value) in disk_state.items())
1356

    
1357
    return data
1358

    
1359
  @classmethod
1360
  def FromDict(cls, val):
1361
    """Custom function for deserializing.
1362

1363
    """
1364
    obj = super(Node, cls).FromDict(val)
1365

    
1366
    if obj.hv_state is not None:
1367
      obj.hv_state = cls._ContainerFromDicts(obj.hv_state, dict, NodeHvState)
1368

    
1369
    if obj.disk_state is not None:
1370
      obj.disk_state = \
1371
        dict((key, cls._ContainerFromDicts(value, dict, NodeDiskState))
1372
             for (key, value) in obj.disk_state.items())
1373

    
1374
    return obj
1375

    
1376

    
1377
class NodeGroup(TaggableObject):
1378
  """Config object representing a node group."""
1379
  __slots__ = [
1380
    "name",
1381
    "members",
1382
    "ndparams",
1383
    "diskparams",
1384
    "ipolicy",
1385
    "serial_no",
1386
    "hv_state_static",
1387
    "disk_state_static",
1388
    "alloc_policy",
1389
    "networks",
1390
    ] + _TIMESTAMPS + _UUID
1391

    
1392
  def ToDict(self):
1393
    """Custom function for nodegroup.
1394

1395
    This discards the members object, which gets recalculated and is only kept
1396
    in memory.
1397

1398
    """
1399
    mydict = super(NodeGroup, self).ToDict()
1400
    del mydict["members"]
1401
    return mydict
1402

    
1403
  @classmethod
1404
  def FromDict(cls, val):
1405
    """Custom function for nodegroup.
1406

1407
    The members slot is initialized to an empty list, upon deserialization.
1408

1409
    """
1410
    obj = super(NodeGroup, cls).FromDict(val)
1411
    obj.members = []
1412
    return obj
1413

    
1414
  def UpgradeConfig(self):
1415
    """Fill defaults for missing configuration values.
1416

1417
    """
1418
    if self.ndparams is None:
1419
      self.ndparams = {}
1420

    
1421
    if self.serial_no is None:
1422
      self.serial_no = 1
1423

    
1424
    if self.alloc_policy is None:
1425
      self.alloc_policy = constants.ALLOC_POLICY_PREFERRED
1426

    
1427
    # We only update mtime, and not ctime, since we would not be able
1428
    # to provide a correct value for creation time.
1429
    if self.mtime is None:
1430
      self.mtime = time.time()
1431

    
1432
    if self.diskparams is None:
1433
      self.diskparams = {}
1434
    if self.ipolicy is None:
1435
      self.ipolicy = MakeEmptyIPolicy()
1436

    
1437
    if self.networks is None:
1438
      self.networks = {}
1439

    
1440
  def FillND(self, node):
1441
    """Return filled out ndparams for L{objects.Node}
1442

1443
    @type node: L{objects.Node}
1444
    @param node: A Node object to fill
1445
    @return a copy of the node's ndparams with defaults filled
1446

1447
    """
1448
    return self.SimpleFillND(node.ndparams)
1449

    
1450
  def SimpleFillND(self, ndparams):
1451
    """Fill a given ndparams dict with defaults.
1452

1453
    @type ndparams: dict
1454
    @param ndparams: the dict to fill
1455
    @rtype: dict
1456
    @return: a copy of the passed in ndparams with missing keys filled
1457
        from the node group defaults
1458

1459
    """
1460
    return FillDict(self.ndparams, ndparams)
1461

    
1462

    
1463
class Cluster(TaggableObject):
1464
  """Config object representing the cluster."""
1465
  __slots__ = [
1466
    "serial_no",
1467
    "rsahostkeypub",
1468
    "highest_used_port",
1469
    "tcpudp_port_pool",
1470
    "mac_prefix",
1471
    "volume_group_name",
1472
    "reserved_lvs",
1473
    "drbd_usermode_helper",
1474
    "default_bridge",
1475
    "default_hypervisor",
1476
    "master_node",
1477
    "master_ip",
1478
    "master_netdev",
1479
    "master_netmask",
1480
    "use_external_mip_script",
1481
    "cluster_name",
1482
    "file_storage_dir",
1483
    "shared_file_storage_dir",
1484
    "enabled_hypervisors",
1485
    "hvparams",
1486
    "ipolicy",
1487
    "os_hvp",
1488
    "beparams",
1489
    "osparams",
1490
    "nicparams",
1491
    "ndparams",
1492
    "diskparams",
1493
    "candidate_pool_size",
1494
    "modify_etc_hosts",
1495
    "modify_ssh_setup",
1496
    "maintain_node_health",
1497
    "uid_pool",
1498
    "default_iallocator",
1499
    "hidden_os",
1500
    "blacklisted_os",
1501
    "primary_ip_family",
1502
    "prealloc_wipe_disks",
1503
    "hv_state_static",
1504
    "disk_state_static",
1505
    ] + _TIMESTAMPS + _UUID
1506

    
1507
  def UpgradeConfig(self):
1508
    """Fill defaults for missing configuration values.
1509

1510
    """
1511
    # pylint: disable=E0203
1512
    # because these are "defined" via slots, not manually
1513
    if self.hvparams is None:
1514
      self.hvparams = constants.HVC_DEFAULTS
1515
    else:
1516
      for hypervisor in self.hvparams:
1517
        self.hvparams[hypervisor] = FillDict(
1518
            constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
1519

    
1520
    if self.os_hvp is None:
1521
      self.os_hvp = {}
1522

    
1523
    # osparams added before 2.2
1524
    if self.osparams is None:
1525
      self.osparams = {}
1526

    
1527
    self.ndparams = UpgradeNDParams(self.ndparams)
1528

    
1529
    self.beparams = UpgradeGroupedParams(self.beparams,
1530
                                         constants.BEC_DEFAULTS)
1531
    for beparams_group in self.beparams:
1532
      UpgradeBeParams(self.beparams[beparams_group])
1533

    
1534
    migrate_default_bridge = not self.nicparams
1535
    self.nicparams = UpgradeGroupedParams(self.nicparams,
1536
                                          constants.NICC_DEFAULTS)
1537
    if migrate_default_bridge:
1538
      self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
1539
        self.default_bridge
1540

    
1541
    if self.modify_etc_hosts is None:
1542
      self.modify_etc_hosts = True
1543

    
1544
    if self.modify_ssh_setup is None:
1545
      self.modify_ssh_setup = True
1546

    
1547
    # default_bridge is no longer used in 2.1. The slot is left there to
1548
    # support auto-upgrading. It can be removed once we decide to deprecate
1549
    # upgrading straight from 2.0.
1550
    if self.default_bridge is not None:
1551
      self.default_bridge = None
1552

    
1553
    # default_hypervisor is just the first enabled one in 2.1. This slot and
1554
    # code can be removed once upgrading straight from 2.0 is deprecated.
1555
    if self.default_hypervisor is not None:
1556
      self.enabled_hypervisors = ([self.default_hypervisor] +
1557
                                  [hvname for hvname in self.enabled_hypervisors
1558
                                   if hvname != self.default_hypervisor])
1559
      self.default_hypervisor = None
1560

    
1561
    # maintain_node_health added after 2.1.1
1562
    if self.maintain_node_health is None:
1563
      self.maintain_node_health = False
1564

    
1565
    if self.uid_pool is None:
1566
      self.uid_pool = []
1567

    
1568
    if self.default_iallocator is None:
1569
      self.default_iallocator = ""
1570

    
1571
    # reserved_lvs added before 2.2
1572
    if self.reserved_lvs is None:
1573
      self.reserved_lvs = []
1574

    
1575
    # hidden and blacklisted operating systems added before 2.2.1
1576
    if self.hidden_os is None:
1577
      self.hidden_os = []
1578

    
1579
    if self.blacklisted_os is None:
1580
      self.blacklisted_os = []
1581

    
1582
    # primary_ip_family added before 2.3
1583
    if self.primary_ip_family is None:
1584
      self.primary_ip_family = AF_INET
1585

    
1586
    if self.master_netmask is None:
1587
      ipcls = netutils.IPAddress.GetClassFromIpFamily(self.primary_ip_family)
1588
      self.master_netmask = ipcls.iplen
1589

    
1590
    if self.prealloc_wipe_disks is None:
1591
      self.prealloc_wipe_disks = False
1592

    
1593
    # shared_file_storage_dir added before 2.5
1594
    if self.shared_file_storage_dir is None:
1595
      self.shared_file_storage_dir = ""
1596

    
1597
    if self.use_external_mip_script is None:
1598
      self.use_external_mip_script = False
1599

    
1600
    if self.diskparams:
1601
      self.diskparams = UpgradeDiskParams(self.diskparams)
1602
    else:
1603
      self.diskparams = constants.DISK_DT_DEFAULTS.copy()
1604

    
1605
    # instance policy added before 2.6
1606
    if self.ipolicy is None:
1607
      self.ipolicy = FillIPolicy(constants.IPOLICY_DEFAULTS, {})
1608
    else:
1609
      # we can either make sure to upgrade the ipolicy always, or only
1610
      # do it in some corner cases (e.g. missing keys); note that this
1611
      # will break any removal of keys from the ipolicy dict
1612
      self.ipolicy = FillIPolicy(constants.IPOLICY_DEFAULTS, self.ipolicy)
1613

    
1614
  @property
1615
  def primary_hypervisor(self):
1616
    """The first hypervisor is the primary.
1617

1618
    Useful, for example, for L{Node}'s hv/disk state.
1619

1620
    """
1621
    return self.enabled_hypervisors[0]
1622

    
1623
  def ToDict(self):
1624
    """Custom function for cluster.
1625

1626
    """
1627
    mydict = super(Cluster, self).ToDict()
1628
    mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
1629
    return mydict
1630

    
1631
  @classmethod
1632
  def FromDict(cls, val):
1633
    """Custom function for cluster.
1634

1635
    """
1636
    obj = super(Cluster, cls).FromDict(val)
1637
    if not isinstance(obj.tcpudp_port_pool, set):
1638
      obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
1639
    return obj
1640

    
1641
  def SimpleFillDP(self, diskparams):
1642
    """Fill a given diskparams dict with cluster defaults.
1643

1644
    @param diskparams: The diskparams
1645
    @return: The defaults dict
1646

1647
    """
1648
    return FillDiskParams(self.diskparams, diskparams)
1649

    
1650
  def GetHVDefaults(self, hypervisor, os_name=None, skip_keys=None):
1651
    """Get the default hypervisor parameters for the cluster.
1652

1653
    @param hypervisor: the hypervisor name
1654
    @param os_name: if specified, we'll also update the defaults for this OS
1655
    @param skip_keys: if passed, list of keys not to use
1656
    @return: the defaults dict
1657

1658
    """
1659
    if skip_keys is None:
1660
      skip_keys = []
1661

    
1662
    fill_stack = [self.hvparams.get(hypervisor, {})]
1663
    if os_name is not None:
1664
      os_hvp = self.os_hvp.get(os_name, {}).get(hypervisor, {})
1665
      fill_stack.append(os_hvp)
1666

    
1667
    ret_dict = {}
1668
    for o_dict in fill_stack:
1669
      ret_dict = FillDict(ret_dict, o_dict, skip_keys=skip_keys)
1670

    
1671
    return ret_dict
1672

    
1673
  def SimpleFillHV(self, hv_name, os_name, hvparams, skip_globals=False):
1674
    """Fill a given hvparams dict with cluster defaults.
1675

1676
    @type hv_name: string
1677
    @param hv_name: the hypervisor to use
1678
    @type os_name: string
1679
    @param os_name: the OS to use for overriding the hypervisor defaults
1680
    @type skip_globals: boolean
1681
    @param skip_globals: if True, the global hypervisor parameters will
1682
        not be filled
1683
    @rtype: dict
1684
    @return: a copy of the given hvparams with missing keys filled from
1685
        the cluster defaults
1686

1687
    """
1688
    if skip_globals:
1689
      skip_keys = constants.HVC_GLOBALS
1690
    else:
1691
      skip_keys = []
1692

    
1693
    def_dict = self.GetHVDefaults(hv_name, os_name, skip_keys=skip_keys)
1694
    return FillDict(def_dict, hvparams, skip_keys=skip_keys)
1695

    
1696
  def FillHV(self, instance, skip_globals=False):
1697
    """Fill an instance's hvparams dict with cluster defaults.
1698

1699
    @type instance: L{objects.Instance}
1700
    @param instance: the instance parameter to fill
1701
    @type skip_globals: boolean
1702
    @param skip_globals: if True, the global hypervisor parameters will
1703
        not be filled
1704
    @rtype: dict
1705
    @return: a copy of the instance's hvparams with missing keys filled from
1706
        the cluster defaults
1707

1708
    """
1709
    return self.SimpleFillHV(instance.hypervisor, instance.os,
1710
                             instance.hvparams, skip_globals)
1711

    
1712
  def SimpleFillBE(self, beparams):
1713
    """Fill a given beparams dict with cluster defaults.
1714

1715
    @type beparams: dict
1716
    @param beparams: the dict to fill
1717
    @rtype: dict
1718
    @return: a copy of the passed in beparams with missing keys filled
1719
        from the cluster defaults
1720

1721
    """
1722
    return FillDict(self.beparams.get(constants.PP_DEFAULT, {}), beparams)
1723

    
1724
  def FillBE(self, instance):
1725
    """Fill an instance's beparams dict with cluster defaults.
1726

1727
    @type instance: L{objects.Instance}
1728
    @param instance: the instance parameter to fill
1729
    @rtype: dict
1730
    @return: a copy of the instance's beparams with missing keys filled from
1731
        the cluster defaults
1732

1733
    """
1734
    return self.SimpleFillBE(instance.beparams)
1735

    
1736
  def SimpleFillNIC(self, nicparams):
1737
    """Fill a given nicparams dict with cluster defaults.
1738

1739
    @type nicparams: dict
1740
    @param nicparams: the dict to fill
1741
    @rtype: dict
1742
    @return: a copy of the passed in nicparams with missing keys filled
1743
        from the cluster defaults
1744

1745
    """
1746
    return FillDict(self.nicparams.get(constants.PP_DEFAULT, {}), nicparams)
1747

    
1748
  def SimpleFillOS(self, os_name, os_params):
1749
    """Fill an instance's osparams dict with cluster defaults.
1750

1751
    @type os_name: string
1752
    @param os_name: the OS name to use
1753
    @type os_params: dict
1754
    @param os_params: the dict to fill with default values
1755
    @rtype: dict
1756
    @return: a copy of the instance's osparams with missing keys filled from
1757
        the cluster defaults
1758

1759
    """
1760
    name_only = os_name.split("+", 1)[0]
1761
    # base OS
1762
    result = self.osparams.get(name_only, {})
1763
    # OS with variant
1764
    result = FillDict(result, self.osparams.get(os_name, {}))
1765
    # specified params
1766
    return FillDict(result, os_params)
1767

    
1768
  @staticmethod
1769
  def SimpleFillHvState(hv_state):
1770
    """Fill an hv_state sub dict with cluster defaults.
1771

1772
    """
1773
    return FillDict(constants.HVST_DEFAULTS, hv_state)
1774

    
1775
  @staticmethod
1776
  def SimpleFillDiskState(disk_state):
1777
    """Fill an disk_state sub dict with cluster defaults.
1778

1779
    """
1780
    return FillDict(constants.DS_DEFAULTS, disk_state)
1781

    
1782
  def FillND(self, node, nodegroup):
1783
    """Return filled out ndparams for L{objects.NodeGroup} and L{objects.Node}
1784

1785
    @type node: L{objects.Node}
1786
    @param node: A Node object to fill
1787
    @type nodegroup: L{objects.NodeGroup}
1788
    @param nodegroup: A Node object to fill
1789
    @return a copy of the node's ndparams with defaults filled
1790

1791
    """
1792
    return self.SimpleFillND(nodegroup.FillND(node))
1793

    
1794
  def SimpleFillND(self, ndparams):
1795
    """Fill a given ndparams dict with defaults.
1796

1797
    @type ndparams: dict
1798
    @param ndparams: the dict to fill
1799
    @rtype: dict
1800
    @return: a copy of the passed in ndparams with missing keys filled
1801
        from the cluster defaults
1802

1803
    """
1804
    return FillDict(self.ndparams, ndparams)
1805

    
1806
  def SimpleFillIPolicy(self, ipolicy):
1807
    """ Fill instance policy dict with defaults.
1808

1809
    @type ipolicy: dict
1810
    @param ipolicy: the dict to fill
1811
    @rtype: dict
1812
    @return: a copy of passed ipolicy with missing keys filled from
1813
      the cluster defaults
1814

1815
    """
1816
    return FillIPolicy(self.ipolicy, ipolicy)
1817

    
1818

    
1819
class BlockDevStatus(ConfigObject):
1820
  """Config object representing the status of a block device."""
1821
  __slots__ = [
1822
    "dev_path",
1823
    "major",
1824
    "minor",
1825
    "sync_percent",
1826
    "estimated_time",
1827
    "is_degraded",
1828
    "ldisk_status",
1829
    ]
1830

    
1831

    
1832
class ImportExportStatus(ConfigObject):
1833
  """Config object representing the status of an import or export."""
1834
  __slots__ = [
1835
    "recent_output",
1836
    "listen_port",
1837
    "connected",
1838
    "progress_mbytes",
1839
    "progress_throughput",
1840
    "progress_eta",
1841
    "progress_percent",
1842
    "exit_status",
1843
    "error_message",
1844
    ] + _TIMESTAMPS
1845

    
1846

    
1847
class ImportExportOptions(ConfigObject):
1848
  """Options for import/export daemon
1849

1850
  @ivar key_name: X509 key name (None for cluster certificate)
1851
  @ivar ca_pem: Remote peer CA in PEM format (None for cluster certificate)
1852
  @ivar compress: Compression method (one of L{constants.IEC_ALL})
1853
  @ivar magic: Used to ensure the connection goes to the right disk
1854
  @ivar ipv6: Whether to use IPv6
1855
  @ivar connect_timeout: Number of seconds for establishing connection
1856

1857
  """
1858
  __slots__ = [
1859
    "key_name",
1860
    "ca_pem",
1861
    "compress",
1862
    "magic",
1863
    "ipv6",
1864
    "connect_timeout",
1865
    ]
1866

    
1867

    
1868
class ConfdRequest(ConfigObject):
1869
  """Object holding a confd request.
1870

1871
  @ivar protocol: confd protocol version
1872
  @ivar type: confd query type
1873
  @ivar query: query request
1874
  @ivar rsalt: requested reply salt
1875

1876
  """
1877
  __slots__ = [
1878
    "protocol",
1879
    "type",
1880
    "query",
1881
    "rsalt",
1882
    ]
1883

    
1884

    
1885
class ConfdReply(ConfigObject):
1886
  """Object holding a confd reply.
1887

1888
  @ivar protocol: confd protocol version
1889
  @ivar status: reply status code (ok, error)
1890
  @ivar answer: confd query reply
1891
  @ivar serial: configuration serial number
1892

1893
  """
1894
  __slots__ = [
1895
    "protocol",
1896
    "status",
1897
    "answer",
1898
    "serial",
1899
    ]
1900

    
1901

    
1902
class QueryFieldDefinition(ConfigObject):
1903
  """Object holding a query field definition.
1904

1905
  @ivar name: Field name
1906
  @ivar title: Human-readable title
1907
  @ivar kind: Field type
1908
  @ivar doc: Human-readable description
1909

1910
  """
1911
  __slots__ = [
1912
    "name",
1913
    "title",
1914
    "kind",
1915
    "doc",
1916
    ]
1917

    
1918

    
1919
class _QueryResponseBase(ConfigObject):
1920
  __slots__ = [
1921
    "fields",
1922
    ]
1923

    
1924
  def ToDict(self):
1925
    """Custom function for serializing.
1926

1927
    """
1928
    mydict = super(_QueryResponseBase, self).ToDict()
1929
    mydict["fields"] = self._ContainerToDicts(mydict["fields"])
1930
    return mydict
1931

    
1932
  @classmethod
1933
  def FromDict(cls, val):
1934
    """Custom function for de-serializing.
1935

1936
    """
1937
    obj = super(_QueryResponseBase, cls).FromDict(val)
1938
    obj.fields = cls._ContainerFromDicts(obj.fields, list, QueryFieldDefinition)
1939
    return obj
1940

    
1941

    
1942
class QueryResponse(_QueryResponseBase):
1943
  """Object holding the response to a query.
1944

1945
  @ivar fields: List of L{QueryFieldDefinition} objects
1946
  @ivar data: Requested data
1947

1948
  """
1949
  __slots__ = [
1950
    "data",
1951
    ]
1952

    
1953

    
1954
class QueryFieldsRequest(ConfigObject):
1955
  """Object holding a request for querying available fields.
1956

1957
  """
1958
  __slots__ = [
1959
    "what",
1960
    "fields",
1961
    ]
1962

    
1963

    
1964
class QueryFieldsResponse(_QueryResponseBase):
1965
  """Object holding the response to a query for fields.
1966

1967
  @ivar fields: List of L{QueryFieldDefinition} objects
1968

1969
  """
1970
  __slots__ = []
1971

    
1972

    
1973
class MigrationStatus(ConfigObject):
1974
  """Object holding the status of a migration.
1975

1976
  """
1977
  __slots__ = [
1978
    "status",
1979
    "transferred_ram",
1980
    "total_ram",
1981
    ]
1982

    
1983

    
1984
class InstanceConsole(ConfigObject):
1985
  """Object describing how to access the console of an instance.
1986

1987
  """
1988
  __slots__ = [
1989
    "instance",
1990
    "kind",
1991
    "message",
1992
    "host",
1993
    "port",
1994
    "user",
1995
    "command",
1996
    "display",
1997
    ]
1998

    
1999
  def Validate(self):
2000
    """Validates contents of this object.
2001

2002
    """
2003
    assert self.kind in constants.CONS_ALL, "Unknown console type"
2004
    assert self.instance, "Missing instance name"
2005
    assert self.message or self.kind in [constants.CONS_SSH,
2006
                                         constants.CONS_SPICE,
2007
                                         constants.CONS_VNC]
2008
    assert self.host or self.kind == constants.CONS_MESSAGE
2009
    assert self.port or self.kind in [constants.CONS_MESSAGE,
2010
                                      constants.CONS_SSH]
2011
    assert self.user or self.kind in [constants.CONS_MESSAGE,
2012
                                      constants.CONS_SPICE,
2013
                                      constants.CONS_VNC]
2014
    assert self.command or self.kind in [constants.CONS_MESSAGE,
2015
                                         constants.CONS_SPICE,
2016
                                         constants.CONS_VNC]
2017
    assert self.display or self.kind in [constants.CONS_MESSAGE,
2018
                                         constants.CONS_SPICE,
2019
                                         constants.CONS_SSH]
2020
    return True
2021

    
2022

    
2023
class Network(TaggableObject):
2024
  """Object representing a network definition for ganeti.
2025

2026
  """
2027
  __slots__ = [
2028
    "name",
2029
    "serial_no",
2030
    "network_type",
2031
    "mac_prefix",
2032
    "family",
2033
    "network",
2034
    "network6",
2035
    "gateway",
2036
    "gateway6",
2037
    "size",
2038
    "reservations",
2039
    "ext_reservations",
2040
    ] + _TIMESTAMPS + _UUID
2041

    
2042
  def HooksDict(self, prefix):
2043
    """Export a dictionary used by hooks with a network's information.
2044

2045
    @type prefix: String
2046
    @param prefix: Prefix to prepend to the dict entries
2047

2048
    """
2049
    result = {
2050
      "%sNETWORK" % prefix: self.name,
2051
      "%sNETWORK_UUID" % prefix: self.uuid,
2052
      "%sNETWORK_FAMILY" % prefix: str(self.family),
2053
      "%sNETWORK_TAGS" % prefix: " ".join(self.tags),
2054
    }
2055
    if self.network:
2056
      result["%sNETWORK_SUBNET" % prefix] = self.network
2057
    if self.gateway:
2058
      result["%sNETWORK_GATEWAY" % prefix] = self.gateway
2059
    if self.network6:
2060
      result["%sNETWORK_SUBNET6" % prefix] = self.network6
2061
    if self.gateway6:
2062
      result["%sNETWORK_GATEWAY6" % prefix] = self.gateway6
2063
    if self.mac_prefix:
2064
      result["%sNETWORK_MAC_PREFIX" % prefix] = self.mac_prefix
2065
    if self.network_type:
2066
      result["%sNETWORK_TYPE" % prefix] = self.network_type
2067

    
2068
    return result
2069

    
2070

    
2071
class SerializableConfigParser(ConfigParser.SafeConfigParser):
2072
  """Simple wrapper over ConfigParse that allows serialization.
2073

2074
  This class is basically ConfigParser.SafeConfigParser with two
2075
  additional methods that allow it to serialize/unserialize to/from a
2076
  buffer.
2077

2078
  """
2079
  def Dumps(self):
2080
    """Dump this instance and return the string representation."""
2081
    buf = StringIO()
2082
    self.write(buf)
2083
    return buf.getvalue()
2084

    
2085
  @classmethod
2086
  def Loads(cls, data):
2087
    """Load data from a string."""
2088
    buf = StringIO(data)
2089
    cfp = cls()
2090
    cfp.readfp(buf)
2091
    return cfp
2092

    
2093

    
2094
class LvmPvInfo(ConfigObject):
2095
  """Information about an LVM physical volume (PV).
2096

2097
  @type name: string
2098
  @ivar name: name of the PV
2099
  @type vg_name: string
2100
  @ivar vg_name: name of the volume group containing the PV
2101
  @type size: float
2102
  @ivar size: size of the PV in MiB
2103
  @type free: float
2104
  @ivar free: free space in the PV, in MiB
2105
  @type attributes: string
2106
  @ivar attributes: PV attributes
2107
  @type lv_list: list of strings
2108
  @ivar lv_list: names of the LVs hosted on the PV
2109
  """
2110
  __slots__ = [
2111
    "name",
2112
    "vg_name",
2113
    "size",
2114
    "free",
2115
    "attributes",
2116
    "lv_list"
2117
    ]
2118

    
2119
  def IsEmpty(self):
2120
    """Is this PV empty?
2121

2122
    """
2123
    return self.size <= (self.free + 1)
2124

    
2125
  def IsAllocatable(self):
2126
    """Is this PV allocatable?
2127

2128
    """
2129
    return ("a" in self.attributes)