Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ f4c9af7a

History | View | Annotate | Download (35.9 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010 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-msg=E0203,W0201
30

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

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

    
36
import ConfigParser
37
import re
38
import copy
39
from cStringIO import StringIO
40

    
41
from ganeti import errors
42
from ganeti import constants
43

    
44
from socket import AF_INET
45

    
46

    
47
__all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
48
           "OS", "Node", "Cluster", "FillDict"]
49

    
50
_TIMESTAMPS = ["ctime", "mtime"]
51
_UUID = ["uuid"]
52

    
53

    
54
def FillDict(defaults_dict, custom_dict, skip_keys=None):
55
  """Basic function to apply settings on top a default dict.
56

57
  @type defaults_dict: dict
58
  @param defaults_dict: dictionary holding the default values
59
  @type custom_dict: dict
60
  @param custom_dict: dictionary holding customized value
61
  @type skip_keys: list
62
  @param skip_keys: which keys not to fill
63
  @rtype: dict
64
  @return: dict with the 'full' values
65

66
  """
67
  ret_dict = copy.deepcopy(defaults_dict)
68
  ret_dict.update(custom_dict)
69
  if skip_keys:
70
    for k in skip_keys:
71
      try:
72
        del ret_dict[k]
73
      except KeyError:
74
        pass
75
  return ret_dict
76

    
77

    
78
def UpgradeGroupedParams(target, defaults):
79
  """Update all groups for the target parameter.
80

81
  @type target: dict of dicts
82
  @param target: {group: {parameter: value}}
83
  @type defaults: dict
84
  @param defaults: default parameter values
85

86
  """
87
  if target is None:
88
    target = {constants.PP_DEFAULT: defaults}
89
  else:
90
    for group in target:
91
      target[group] = FillDict(defaults, target[group])
92
  return target
93

    
94

    
95
class ConfigObject(object):
96
  """A generic config object.
97

98
  It has the following properties:
99

100
    - provides somewhat safe recursive unpickling and pickling for its classes
101
    - unset attributes which are defined in slots are always returned
102
      as None instead of raising an error
103

104
  Classes derived from this must always declare __slots__ (we use many
105
  config objects and the memory reduction is useful)
106

107
  """
108
  __slots__ = []
109

    
110
  def __init__(self, **kwargs):
111
    for k, v in kwargs.iteritems():
112
      setattr(self, k, v)
113

    
114
  def __getattr__(self, name):
115
    if name not in self._all_slots():
116
      raise AttributeError("Invalid object attribute %s.%s" %
117
                           (type(self).__name__, name))
118
    return None
119

    
120
  def __setstate__(self, state):
121
    slots = self._all_slots()
122
    for name in state:
123
      if name in slots:
124
        setattr(self, name, state[name])
125

    
126
  @classmethod
127
  def _all_slots(cls):
128
    """Compute the list of all declared slots for a class.
129

130
    """
131
    slots = []
132
    for parent in cls.__mro__:
133
      slots.extend(getattr(parent, "__slots__", []))
134
    return slots
135

    
136
  def ToDict(self):
137
    """Convert to a dict holding only standard python types.
138

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

145
    """
146
    result = {}
147
    for name in self._all_slots():
148
      value = getattr(self, name, None)
149
      if value is not None:
150
        result[name] = value
151
    return result
152

    
153
  __getstate__ = ToDict
154

    
155
  @classmethod
156
  def FromDict(cls, val):
157
    """Create an object from a dictionary.
158

159
    This generic routine takes a dict, instantiates a new instance of
160
    the given class, and sets attributes based on the dict content.
161

162
    As for `ToDict`, this does not work if the class has children
163
    who are ConfigObjects themselves (e.g. the nics list in an
164
    Instance), in which case the object should subclass the function
165
    and alter the objects.
166

167
    """
168
    if not isinstance(val, dict):
169
      raise errors.ConfigurationError("Invalid object passed to FromDict:"
170
                                      " expected dict, got %s" % type(val))
171
    val_str = dict([(str(k), v) for k, v in val.iteritems()])
172
    obj = cls(**val_str) # pylint: disable-msg=W0142
173
    return obj
174

    
175
  @staticmethod
176
  def _ContainerToDicts(container):
177
    """Convert the elements of a container to standard python types.
178

179
    This method converts a container with elements derived from
180
    ConfigData to standard python types. If the container is a dict,
181
    we don't touch the keys, only the values.
182

183
    """
184
    if isinstance(container, dict):
185
      ret = dict([(k, v.ToDict()) for k, v in container.iteritems()])
186
    elif isinstance(container, (list, tuple, set, frozenset)):
187
      ret = [elem.ToDict() for elem in container]
188
    else:
189
      raise TypeError("Invalid type %s passed to _ContainerToDicts" %
190
                      type(container))
191
    return ret
192

    
193
  @staticmethod
194
  def _ContainerFromDicts(source, c_type, e_type):
195
    """Convert a container from standard python types.
196

197
    This method converts a container with standard python types to
198
    ConfigData objects. If the container is a dict, we don't touch the
199
    keys, only the values.
200

201
    """
202
    if not isinstance(c_type, type):
203
      raise TypeError("Container type %s passed to _ContainerFromDicts is"
204
                      " not a type" % type(c_type))
205
    if c_type is dict:
206
      ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
207
    elif c_type in (list, tuple, set, frozenset):
208
      ret = c_type([e_type.FromDict(elem) for elem in source])
209
    else:
210
      raise TypeError("Invalid container type %s passed to"
211
                      " _ContainerFromDicts" % c_type)
212
    return ret
213

    
214
  def Copy(self):
215
    """Makes a deep copy of the current object and its children.
216

217
    """
218
    dict_form = self.ToDict()
219
    clone_obj = self.__class__.FromDict(dict_form)
220
    return clone_obj
221

    
222
  def __repr__(self):
223
    """Implement __repr__ for ConfigObjects."""
224
    return repr(self.ToDict())
225

    
226
  def UpgradeConfig(self):
227
    """Fill defaults for missing configuration values.
228

229
    This method will be called at configuration load time, and its
230
    implementation will be object dependent.
231

232
    """
233
    pass
234

    
235

    
236
class TaggableObject(ConfigObject):
237
  """An generic class supporting tags.
238

239
  """
240
  __slots__ = ["tags"]
241
  VALID_TAG_RE = re.compile("^[\w.+*/:@-]+$")
242

    
243
  @classmethod
244
  def ValidateTag(cls, tag):
245
    """Check if a tag is valid.
246

247
    If the tag is invalid, an errors.TagError will be raised. The
248
    function has no return value.
249

250
    """
251
    if not isinstance(tag, basestring):
252
      raise errors.TagError("Invalid tag type (not a string)")
253
    if len(tag) > constants.MAX_TAG_LEN:
254
      raise errors.TagError("Tag too long (>%d characters)" %
255
                            constants.MAX_TAG_LEN)
256
    if not tag:
257
      raise errors.TagError("Tags cannot be empty")
258
    if not cls.VALID_TAG_RE.match(tag):
259
      raise errors.TagError("Tag contains invalid characters")
260

    
261
  def GetTags(self):
262
    """Return the tags list.
263

264
    """
265
    tags = getattr(self, "tags", None)
266
    if tags is None:
267
      tags = self.tags = set()
268
    return tags
269

    
270
  def AddTag(self, tag):
271
    """Add a new tag.
272

273
    """
274
    self.ValidateTag(tag)
275
    tags = self.GetTags()
276
    if len(tags) >= constants.MAX_TAGS_PER_OBJ:
277
      raise errors.TagError("Too many tags")
278
    self.GetTags().add(tag)
279

    
280
  def RemoveTag(self, tag):
281
    """Remove a tag.
282

283
    """
284
    self.ValidateTag(tag)
285
    tags = self.GetTags()
286
    try:
287
      tags.remove(tag)
288
    except KeyError:
289
      raise errors.TagError("Tag not found")
290

    
291
  def ToDict(self):
292
    """Taggable-object-specific conversion to standard python types.
293

294
    This replaces the tags set with a list.
295

296
    """
297
    bo = super(TaggableObject, self).ToDict()
298

    
299
    tags = bo.get("tags", None)
300
    if isinstance(tags, set):
301
      bo["tags"] = list(tags)
302
    return bo
303

    
304
  @classmethod
305
  def FromDict(cls, val):
306
    """Custom function for instances.
307

308
    """
309
    obj = super(TaggableObject, cls).FromDict(val)
310
    if hasattr(obj, "tags") and isinstance(obj.tags, list):
311
      obj.tags = set(obj.tags)
312
    return obj
313

    
314

    
315
class ConfigData(ConfigObject):
316
  """Top-level config object."""
317
  __slots__ = (["version", "cluster", "nodes", "instances", "serial_no"] +
318
               _TIMESTAMPS)
319

    
320
  def ToDict(self):
321
    """Custom function for top-level config data.
322

323
    This just replaces the list of instances, nodes and the cluster
324
    with standard python types.
325

326
    """
327
    mydict = super(ConfigData, self).ToDict()
328
    mydict["cluster"] = mydict["cluster"].ToDict()
329
    for key in "nodes", "instances":
330
      mydict[key] = self._ContainerToDicts(mydict[key])
331

    
332
    return mydict
333

    
334
  @classmethod
335
  def FromDict(cls, val):
336
    """Custom function for top-level config data
337

338
    """
339
    obj = super(ConfigData, cls).FromDict(val)
340
    obj.cluster = Cluster.FromDict(obj.cluster)
341
    obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
342
    obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
343
    return obj
344

    
345
  def HasAnyDiskOfType(self, dev_type):
346
    """Check if in there is at disk of the given type in the configuration.
347

348
    @type dev_type: L{constants.LDS_BLOCK}
349
    @param dev_type: the type to look for
350
    @rtype: boolean
351
    @return: boolean indicating if a disk of the given type was found or not
352

353
    """
354
    for instance in self.instances.values():
355
      for disk in instance.disks:
356
        if disk.IsBasedOnDiskType(dev_type):
357
          return True
358
    return False
359

    
360
  def UpgradeConfig(self):
361
    """Fill defaults for missing configuration values.
362

363
    """
364
    self.cluster.UpgradeConfig()
365
    for node in self.nodes.values():
366
      node.UpgradeConfig()
367
    for instance in self.instances.values():
368
      instance.UpgradeConfig()
369
    if self.cluster.drbd_usermode_helper is None:
370
      # To decide if we set an helper let's check if at least one instance has
371
      # a DRBD disk. This does not cover all the possible scenarios but it
372
      # gives a good approximation.
373
      if self.HasAnyDiskOfType(constants.LD_DRBD8):
374
        self.cluster.drbd_usermode_helper = constants.DEFAULT_DRBD_HELPER
375

    
376

    
377
class NIC(ConfigObject):
378
  """Config object representing a network card."""
379
  __slots__ = ["mac", "ip", "bridge", "nicparams"]
380

    
381
  @classmethod
382
  def CheckParameterSyntax(cls, nicparams):
383
    """Check the given parameters for validity.
384

385
    @type nicparams:  dict
386
    @param nicparams: dictionary with parameter names/value
387
    @raise errors.ConfigurationError: when a parameter is not valid
388

389
    """
390
    if nicparams[constants.NIC_MODE] not in constants.NIC_VALID_MODES:
391
      err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
392
      raise errors.ConfigurationError(err)
393

    
394
    if (nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED and
395
        not nicparams[constants.NIC_LINK]):
396
      err = "Missing bridged nic link"
397
      raise errors.ConfigurationError(err)
398

    
399
  def UpgradeConfig(self):
400
    """Fill defaults for missing configuration values.
401

402
    """
403
    if self.nicparams is None:
404
      self.nicparams = {}
405
      if self.bridge is not None:
406
        self.nicparams[constants.NIC_MODE] = constants.NIC_MODE_BRIDGED
407
        self.nicparams[constants.NIC_LINK] = self.bridge
408
    # bridge is no longer used it 2.1. The slot is left there to support
409
    # upgrading, but can be removed once upgrades to the current version
410
    # straight from 2.0 are deprecated.
411
    if self.bridge is not None:
412
      self.bridge = None
413

    
414

    
415
class Disk(ConfigObject):
416
  """Config object representing a block device."""
417
  __slots__ = ["dev_type", "logical_id", "physical_id",
418
               "children", "iv_name", "size", "mode"]
419

    
420
  def CreateOnSecondary(self):
421
    """Test if this device needs to be created on a secondary node."""
422
    return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
423

    
424
  def AssembleOnSecondary(self):
425
    """Test if this device needs to be assembled on a secondary node."""
426
    return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
427

    
428
  def OpenOnSecondary(self):
429
    """Test if this device needs to be opened on a secondary node."""
430
    return self.dev_type in (constants.LD_LV,)
431

    
432
  def StaticDevPath(self):
433
    """Return the device path if this device type has a static one.
434

435
    Some devices (LVM for example) live always at the same /dev/ path,
436
    irrespective of their status. For such devices, we return this
437
    path, for others we return None.
438

439
    @warning: The path returned is not a normalized pathname; callers
440
        should check that it is a valid path.
441

442
    """
443
    if self.dev_type == constants.LD_LV:
444
      return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
445
    return None
446

    
447
  def ChildrenNeeded(self):
448
    """Compute the needed number of children for activation.
449

450
    This method will return either -1 (all children) or a positive
451
    number denoting the minimum number of children needed for
452
    activation (only mirrored devices will usually return >=0).
453

454
    Currently, only DRBD8 supports diskless activation (therefore we
455
    return 0), for all other we keep the previous semantics and return
456
    -1.
457

458
    """
459
    if self.dev_type == constants.LD_DRBD8:
460
      return 0
461
    return -1
462

    
463
  def IsBasedOnDiskType(self, dev_type):
464
    """Check if the disk or its children are based on the given type.
465

466
    @type dev_type: L{constants.LDS_BLOCK}
467
    @param dev_type: the type to look for
468
    @rtype: boolean
469
    @return: boolean indicating if a device of the given type was found or not
470

471
    """
472
    if self.children:
473
      for child in self.children:
474
        if child.IsBasedOnDiskType(dev_type):
475
          return True
476
    return self.dev_type == dev_type
477

    
478
  def GetNodes(self, node):
479
    """This function returns the nodes this device lives on.
480

481
    Given the node on which the parent of the device lives on (or, in
482
    case of a top-level device, the primary node of the devices'
483
    instance), this function will return a list of nodes on which this
484
    devices needs to (or can) be assembled.
485

486
    """
487
    if self.dev_type in [constants.LD_LV, constants.LD_FILE]:
488
      result = [node]
489
    elif self.dev_type in constants.LDS_DRBD:
490
      result = [self.logical_id[0], self.logical_id[1]]
491
      if node not in result:
492
        raise errors.ConfigurationError("DRBD device passed unknown node")
493
    else:
494
      raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
495
    return result
496

    
497
  def ComputeNodeTree(self, parent_node):
498
    """Compute the node/disk tree for this disk and its children.
499

500
    This method, given the node on which the parent disk lives, will
501
    return the list of all (node, disk) pairs which describe the disk
502
    tree in the most compact way. For example, a drbd/lvm stack
503
    will be returned as (primary_node, drbd) and (secondary_node, drbd)
504
    which represents all the top-level devices on the nodes.
505

506
    """
507
    my_nodes = self.GetNodes(parent_node)
508
    result = [(node, self) for node in my_nodes]
509
    if not self.children:
510
      # leaf device
511
      return result
512
    for node in my_nodes:
513
      for child in self.children:
514
        child_result = child.ComputeNodeTree(node)
515
        if len(child_result) == 1:
516
          # child (and all its descendants) is simple, doesn't split
517
          # over multiple hosts, so we don't need to describe it, our
518
          # own entry for this node describes it completely
519
          continue
520
        else:
521
          # check if child nodes differ from my nodes; note that
522
          # subdisk can differ from the child itself, and be instead
523
          # one of its descendants
524
          for subnode, subdisk in child_result:
525
            if subnode not in my_nodes:
526
              result.append((subnode, subdisk))
527
            # otherwise child is under our own node, so we ignore this
528
            # entry (but probably the other results in the list will
529
            # be different)
530
    return result
531

    
532
  def RecordGrow(self, amount):
533
    """Update the size of this disk after growth.
534

535
    This method recurses over the disks's children and updates their
536
    size correspondigly. The method needs to be kept in sync with the
537
    actual algorithms from bdev.
538

539
    """
540
    if self.dev_type == constants.LD_LV or self.dev_type == constants.LD_FILE:
541
      self.size += amount
542
    elif self.dev_type == constants.LD_DRBD8:
543
      if self.children:
544
        self.children[0].RecordGrow(amount)
545
      self.size += amount
546
    else:
547
      raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
548
                                   " disk type %s" % self.dev_type)
549

    
550
  def UnsetSize(self):
551
    """Sets recursively the size to zero for the disk and its children.
552

553
    """
554
    if self.children:
555
      for child in self.children:
556
        child.UnsetSize()
557
    self.size = 0
558

    
559
  def SetPhysicalID(self, target_node, nodes_ip):
560
    """Convert the logical ID to the physical ID.
561

562
    This is used only for drbd, which needs ip/port configuration.
563

564
    The routine descends down and updates its children also, because
565
    this helps when the only the top device is passed to the remote
566
    node.
567

568
    Arguments:
569
      - target_node: the node we wish to configure for
570
      - nodes_ip: a mapping of node name to ip
571

572
    The target_node must exist in in nodes_ip, and must be one of the
573
    nodes in the logical ID for each of the DRBD devices encountered
574
    in the disk tree.
575

576
    """
577
    if self.children:
578
      for child in self.children:
579
        child.SetPhysicalID(target_node, nodes_ip)
580

    
581
    if self.logical_id is None and self.physical_id is not None:
582
      return
583
    if self.dev_type in constants.LDS_DRBD:
584
      pnode, snode, port, pminor, sminor, secret = self.logical_id
585
      if target_node not in (pnode, snode):
586
        raise errors.ConfigurationError("DRBD device not knowing node %s" %
587
                                        target_node)
588
      pnode_ip = nodes_ip.get(pnode, None)
589
      snode_ip = nodes_ip.get(snode, None)
590
      if pnode_ip is None or snode_ip is None:
591
        raise errors.ConfigurationError("Can't find primary or secondary node"
592
                                        " for %s" % str(self))
593
      p_data = (pnode_ip, port)
594
      s_data = (snode_ip, port)
595
      if pnode == target_node:
596
        self.physical_id = p_data + s_data + (pminor, secret)
597
      else: # it must be secondary, we tested above
598
        self.physical_id = s_data + p_data + (sminor, secret)
599
    else:
600
      self.physical_id = self.logical_id
601
    return
602

    
603
  def ToDict(self):
604
    """Disk-specific conversion to standard python types.
605

606
    This replaces the children lists of objects with lists of
607
    standard python types.
608

609
    """
610
    bo = super(Disk, self).ToDict()
611

    
612
    for attr in ("children",):
613
      alist = bo.get(attr, None)
614
      if alist:
615
        bo[attr] = self._ContainerToDicts(alist)
616
    return bo
617

    
618
  @classmethod
619
  def FromDict(cls, val):
620
    """Custom function for Disks
621

622
    """
623
    obj = super(Disk, cls).FromDict(val)
624
    if obj.children:
625
      obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
626
    if obj.logical_id and isinstance(obj.logical_id, list):
627
      obj.logical_id = tuple(obj.logical_id)
628
    if obj.physical_id and isinstance(obj.physical_id, list):
629
      obj.physical_id = tuple(obj.physical_id)
630
    if obj.dev_type in constants.LDS_DRBD:
631
      # we need a tuple of length six here
632
      if len(obj.logical_id) < 6:
633
        obj.logical_id += (None,) * (6 - len(obj.logical_id))
634
    return obj
635

    
636
  def __str__(self):
637
    """Custom str() formatter for disks.
638

639
    """
640
    if self.dev_type == constants.LD_LV:
641
      val =  "<LogicalVolume(/dev/%s/%s" % self.logical_id
642
    elif self.dev_type in constants.LDS_DRBD:
643
      node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
644
      val = "<DRBD8("
645
      if self.physical_id is None:
646
        phy = "unconfigured"
647
      else:
648
        phy = ("configured as %s:%s %s:%s" %
649
               (self.physical_id[0], self.physical_id[1],
650
                self.physical_id[2], self.physical_id[3]))
651

    
652
      val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
653
              (node_a, minor_a, node_b, minor_b, port, phy))
654
      if self.children and self.children.count(None) == 0:
655
        val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
656
      else:
657
        val += "no local storage"
658
    else:
659
      val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
660
             (self.dev_type, self.logical_id, self.physical_id, self.children))
661
    if self.iv_name is None:
662
      val += ", not visible"
663
    else:
664
      val += ", visible as /dev/%s" % self.iv_name
665
    if isinstance(self.size, int):
666
      val += ", size=%dm)>" % self.size
667
    else:
668
      val += ", size='%s')>" % (self.size,)
669
    return val
670

    
671
  def Verify(self):
672
    """Checks that this disk is correctly configured.
673

674
    """
675
    all_errors = []
676
    if self.mode not in constants.DISK_ACCESS_SET:
677
      all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
678
    return all_errors
679

    
680
  def UpgradeConfig(self):
681
    """Fill defaults for missing configuration values.
682

683
    """
684
    if self.children:
685
      for child in self.children:
686
        child.UpgradeConfig()
687
    # add here config upgrade for this disk
688

    
689

    
690
class Instance(TaggableObject):
691
  """Config object representing an instance."""
692
  __slots__ = [
693
    "name",
694
    "primary_node",
695
    "os",
696
    "hypervisor",
697
    "hvparams",
698
    "beparams",
699
    "osparams",
700
    "admin_up",
701
    "nics",
702
    "disks",
703
    "disk_template",
704
    "network_port",
705
    "serial_no",
706
    ] + _TIMESTAMPS + _UUID
707

    
708
  def _ComputeSecondaryNodes(self):
709
    """Compute the list of secondary nodes.
710

711
    This is a simple wrapper over _ComputeAllNodes.
712

713
    """
714
    all_nodes = set(self._ComputeAllNodes())
715
    all_nodes.discard(self.primary_node)
716
    return tuple(all_nodes)
717

    
718
  secondary_nodes = property(_ComputeSecondaryNodes, None, None,
719
                             "List of secondary nodes")
720

    
721
  def _ComputeAllNodes(self):
722
    """Compute the list of all nodes.
723

724
    Since the data is already there (in the drbd disks), keeping it as
725
    a separate normal attribute is redundant and if not properly
726
    synchronised can cause problems. Thus it's better to compute it
727
    dynamically.
728

729
    """
730
    def _Helper(nodes, device):
731
      """Recursively computes nodes given a top device."""
732
      if device.dev_type in constants.LDS_DRBD:
733
        nodea, nodeb = device.logical_id[:2]
734
        nodes.add(nodea)
735
        nodes.add(nodeb)
736
      if device.children:
737
        for child in device.children:
738
          _Helper(nodes, child)
739

    
740
    all_nodes = set()
741
    all_nodes.add(self.primary_node)
742
    for device in self.disks:
743
      _Helper(all_nodes, device)
744
    return tuple(all_nodes)
745

    
746
  all_nodes = property(_ComputeAllNodes, None, None,
747
                       "List of all nodes of the instance")
748

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

752
    This function figures out what logical volumes should belong on
753
    which nodes, recursing through a device tree.
754

755
    @param lvmap: optional dictionary to receive the
756
        'node' : ['lv', ...] data.
757

758
    @return: None if lvmap arg is given, otherwise, a dictionary
759
        of the form { 'nodename' : ['volume1', 'volume2', ...], ... }
760

761
    """
762
    if node == None:
763
      node = self.primary_node
764

    
765
    if lvmap is None:
766
      lvmap = { node : [] }
767
      ret = lvmap
768
    else:
769
      if not node in lvmap:
770
        lvmap[node] = []
771
      ret = None
772

    
773
    if not devs:
774
      devs = self.disks
775

    
776
    for dev in devs:
777
      if dev.dev_type == constants.LD_LV:
778
        lvmap[node].append(dev.logical_id[1])
779

    
780
      elif dev.dev_type in constants.LDS_DRBD:
781
        if dev.children:
782
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
783
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
784

    
785
      elif dev.children:
786
        self.MapLVsByNode(lvmap, dev.children, node)
787

    
788
    return ret
789

    
790
  def FindDisk(self, idx):
791
    """Find a disk given having a specified index.
792

793
    This is just a wrapper that does validation of the index.
794

795
    @type idx: int
796
    @param idx: the disk index
797
    @rtype: L{Disk}
798
    @return: the corresponding disk
799
    @raise errors.OpPrereqError: when the given index is not valid
800

801
    """
802
    try:
803
      idx = int(idx)
804
      return self.disks[idx]
805
    except (TypeError, ValueError), err:
806
      raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err),
807
                                 errors.ECODE_INVAL)
808
    except IndexError:
809
      raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
810
                                 " 0 to %d" % (idx, len(self.disks)),
811
                                 errors.ECODE_INVAL)
812

    
813
  def ToDict(self):
814
    """Instance-specific conversion to standard python types.
815

816
    This replaces the children lists of objects with lists of standard
817
    python types.
818

819
    """
820
    bo = super(Instance, self).ToDict()
821

    
822
    for attr in "nics", "disks":
823
      alist = bo.get(attr, None)
824
      if alist:
825
        nlist = self._ContainerToDicts(alist)
826
      else:
827
        nlist = []
828
      bo[attr] = nlist
829
    return bo
830

    
831
  @classmethod
832
  def FromDict(cls, val):
833
    """Custom function for instances.
834

835
    """
836
    obj = super(Instance, cls).FromDict(val)
837
    obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
838
    obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
839
    return obj
840

    
841
  def UpgradeConfig(self):
842
    """Fill defaults for missing configuration values.
843

844
    """
845
    for nic in self.nics:
846
      nic.UpgradeConfig()
847
    for disk in self.disks:
848
      disk.UpgradeConfig()
849
    if self.hvparams:
850
      for key in constants.HVC_GLOBALS:
851
        try:
852
          del self.hvparams[key]
853
        except KeyError:
854
          pass
855
    if self.osparams is None:
856
      self.osparams = {}
857

    
858

    
859
class OS(ConfigObject):
860
  """Config object representing an operating system.
861

862
  @type supported_parameters: list
863
  @ivar supported_parameters: a list of tuples, name and description,
864
      containing the supported parameters by this OS
865

866
  """
867
  __slots__ = [
868
    "name",
869
    "path",
870
    "api_versions",
871
    "create_script",
872
    "export_script",
873
    "import_script",
874
    "rename_script",
875
    "verify_script",
876
    "supported_variants",
877
    "supported_parameters",
878
    ]
879

    
880

    
881
class Node(TaggableObject):
882
  """Config object representing a node."""
883
  __slots__ = [
884
    "name",
885
    "primary_ip",
886
    "secondary_ip",
887
    "serial_no",
888
    "master_candidate",
889
    "offline",
890
    "drained",
891
    ] + _TIMESTAMPS + _UUID
892

    
893

    
894
class Cluster(TaggableObject):
895
  """Config object representing the cluster."""
896
  __slots__ = [
897
    "serial_no",
898
    "rsahostkeypub",
899
    "highest_used_port",
900
    "tcpudp_port_pool",
901
    "mac_prefix",
902
    "volume_group_name",
903
    "reserved_lvs",
904
    "drbd_usermode_helper",
905
    "default_bridge",
906
    "default_hypervisor",
907
    "master_node",
908
    "master_ip",
909
    "master_netdev",
910
    "cluster_name",
911
    "file_storage_dir",
912
    "enabled_hypervisors",
913
    "hvparams",
914
    "os_hvp",
915
    "beparams",
916
    "osparams",
917
    "nicparams",
918
    "candidate_pool_size",
919
    "modify_etc_hosts",
920
    "modify_ssh_setup",
921
    "maintain_node_health",
922
    "uid_pool",
923
    "default_iallocator",
924
    "primary_ip_family",
925
    ] + _TIMESTAMPS + _UUID
926

    
927
  def UpgradeConfig(self):
928
    """Fill defaults for missing configuration values.
929

930
    """
931
    # pylint: disable-msg=E0203
932
    # because these are "defined" via slots, not manually
933
    if self.hvparams is None:
934
      self.hvparams = constants.HVC_DEFAULTS
935
    else:
936
      for hypervisor in self.hvparams:
937
        self.hvparams[hypervisor] = FillDict(
938
            constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
939

    
940
    if self.os_hvp is None:
941
      self.os_hvp = {}
942

    
943
    # osparams added before 2.2
944
    if self.osparams is None:
945
      self.osparams = {}
946

    
947
    self.beparams = UpgradeGroupedParams(self.beparams,
948
                                         constants.BEC_DEFAULTS)
949
    migrate_default_bridge = not self.nicparams
950
    self.nicparams = UpgradeGroupedParams(self.nicparams,
951
                                          constants.NICC_DEFAULTS)
952
    if migrate_default_bridge:
953
      self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
954
        self.default_bridge
955

    
956
    if self.modify_etc_hosts is None:
957
      self.modify_etc_hosts = True
958

    
959
    if self.modify_ssh_setup is None:
960
      self.modify_ssh_setup = True
961

    
962
    # default_bridge is no longer used it 2.1. The slot is left there to
963
    # support auto-upgrading. It can be removed once we decide to deprecate
964
    # upgrading straight from 2.0.
965
    if self.default_bridge is not None:
966
      self.default_bridge = None
967

    
968
    # default_hypervisor is just the first enabled one in 2.1. This slot and
969
    # code can be removed once upgrading straight from 2.0 is deprecated.
970
    if self.default_hypervisor is not None:
971
      self.enabled_hypervisors = ([self.default_hypervisor] +
972
        [hvname for hvname in self.enabled_hypervisors
973
         if hvname != self.default_hypervisor])
974
      self.default_hypervisor = None
975

    
976
    # maintain_node_health added after 2.1.1
977
    if self.maintain_node_health is None:
978
      self.maintain_node_health = False
979

    
980
    if self.uid_pool is None:
981
      self.uid_pool = []
982

    
983
    if self.default_iallocator is None:
984
      self.default_iallocator = ""
985

    
986
    # reserved_lvs added before 2.2
987
    if self.reserved_lvs is None:
988
      self.reserved_lvs = []
989

    
990
    # primary_ip_family added before 2.3
991
    if self.primary_ip_family is None:
992
      self.primary_ip_family = AF_INET
993

    
994
  def ToDict(self):
995
    """Custom function for cluster.
996

997
    """
998
    mydict = super(Cluster, self).ToDict()
999
    mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
1000
    return mydict
1001

    
1002
  @classmethod
1003
  def FromDict(cls, val):
1004
    """Custom function for cluster.
1005

1006
    """
1007
    obj = super(Cluster, cls).FromDict(val)
1008
    if not isinstance(obj.tcpudp_port_pool, set):
1009
      obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
1010
    return obj
1011

    
1012
  def GetHVDefaults(self, hypervisor, os_name=None, skip_keys=None):
1013
    """Get the default hypervisor parameters for the cluster.
1014

1015
    @param hypervisor: the hypervisor name
1016
    @param os_name: if specified, we'll also update the defaults for this OS
1017
    @param skip_keys: if passed, list of keys not to use
1018
    @return: the defaults dict
1019

1020
    """
1021
    if skip_keys is None:
1022
      skip_keys = []
1023

    
1024
    fill_stack = [self.hvparams.get(hypervisor, {})]
1025
    if os_name is not None:
1026
      os_hvp = self.os_hvp.get(os_name, {}).get(hypervisor, {})
1027
      fill_stack.append(os_hvp)
1028

    
1029
    ret_dict = {}
1030
    for o_dict in fill_stack:
1031
      ret_dict = FillDict(ret_dict, o_dict, skip_keys=skip_keys)
1032

    
1033
    return ret_dict
1034

    
1035
  def SimpleFillHV(self, hv_name, os_name, hvparams, skip_globals=False):
1036
    """Fill a given hvparams dict with cluster defaults.
1037

1038
    @type hv_name: string
1039
    @param hv_name: the hypervisor to use
1040
    @type os_name: string
1041
    @param os_name: the OS to use for overriding the hypervisor defaults
1042
    @type skip_globals: boolean
1043
    @param skip_globals: if True, the global hypervisor parameters will
1044
        not be filled
1045
    @rtype: dict
1046
    @return: a copy of the given hvparams with missing keys filled from
1047
        the cluster defaults
1048

1049
    """
1050
    if skip_globals:
1051
      skip_keys = constants.HVC_GLOBALS
1052
    else:
1053
      skip_keys = []
1054

    
1055
    def_dict = self.GetHVDefaults(hv_name, os_name, skip_keys=skip_keys)
1056
    return FillDict(def_dict, hvparams, skip_keys=skip_keys)
1057

    
1058
  def FillHV(self, instance, skip_globals=False):
1059
    """Fill an instance's hvparams dict with cluster defaults.
1060

1061
    @type instance: L{objects.Instance}
1062
    @param instance: the instance parameter to fill
1063
    @type skip_globals: boolean
1064
    @param skip_globals: if True, the global hypervisor parameters will
1065
        not be filled
1066
    @rtype: dict
1067
    @return: a copy of the instance's hvparams with missing keys filled from
1068
        the cluster defaults
1069

1070
    """
1071
    return self.SimpleFillHV(instance.hypervisor, instance.os,
1072
                             instance.hvparams, skip_globals)
1073

    
1074
  def SimpleFillBE(self, beparams):
1075
    """Fill a given beparams dict with cluster defaults.
1076

1077
    @type beparams: dict
1078
    @param beparams: the dict to fill
1079
    @rtype: dict
1080
    @return: a copy of the passed in beparams with missing keys filled
1081
        from the cluster defaults
1082

1083
    """
1084
    return FillDict(self.beparams.get(constants.PP_DEFAULT, {}), beparams)
1085

    
1086
  def FillBE(self, instance):
1087
    """Fill an instance's beparams dict with cluster defaults.
1088

1089
    @type instance: L{objects.Instance}
1090
    @param instance: the instance parameter to fill
1091
    @rtype: dict
1092
    @return: a copy of the instance's beparams with missing keys filled from
1093
        the cluster defaults
1094

1095
    """
1096
    return self.SimpleFillBE(instance.beparams)
1097

    
1098
  def SimpleFillNIC(self, nicparams):
1099
    """Fill a given nicparams dict with cluster defaults.
1100

1101
    @type nicparams: dict
1102
    @param nicparams: the dict to fill
1103
    @rtype: dict
1104
    @return: a copy of the passed in nicparams with missing keys filled
1105
        from the cluster defaults
1106

1107
    """
1108
    return FillDict(self.nicparams.get(constants.PP_DEFAULT, {}), nicparams)
1109

    
1110
  def SimpleFillOS(self, os_name, os_params):
1111
    """Fill an instance's osparams dict with cluster defaults.
1112

1113
    @type os_name: string
1114
    @param os_name: the OS name to use
1115
    @type os_params: dict
1116
    @param os_params: the dict to fill with default values
1117
    @rtype: dict
1118
    @return: a copy of the instance's osparams with missing keys filled from
1119
        the cluster defaults
1120

1121
    """
1122
    name_only = os_name.split("+", 1)[0]
1123
    # base OS
1124
    result = self.osparams.get(name_only, {})
1125
    # OS with variant
1126
    result = FillDict(result, self.osparams.get(os_name, {}))
1127
    # specified params
1128
    return FillDict(result, os_params)
1129

    
1130

    
1131
class BlockDevStatus(ConfigObject):
1132
  """Config object representing the status of a block device."""
1133
  __slots__ = [
1134
    "dev_path",
1135
    "major",
1136
    "minor",
1137
    "sync_percent",
1138
    "estimated_time",
1139
    "is_degraded",
1140
    "ldisk_status",
1141
    ]
1142

    
1143

    
1144
class ImportExportStatus(ConfigObject):
1145
  """Config object representing the status of an import or export."""
1146
  __slots__ = [
1147
    "recent_output",
1148
    "listen_port",
1149
    "connected",
1150
    "progress_mbytes",
1151
    "progress_throughput",
1152
    "progress_eta",
1153
    "progress_percent",
1154
    "exit_status",
1155
    "error_message",
1156
    ] + _TIMESTAMPS
1157

    
1158

    
1159
class ImportExportOptions(ConfigObject):
1160
  """Options for import/export daemon
1161

1162
  @ivar key_name: X509 key name (None for cluster certificate)
1163
  @ivar ca_pem: Remote peer CA in PEM format (None for cluster certificate)
1164
  @ivar compress: Compression method (one of L{constants.IEC_ALL})
1165
  @ivar magic: Used to ensure the connection goes to the right disk
1166

1167
  """
1168
  __slots__ = [
1169
    "key_name",
1170
    "ca_pem",
1171
    "compress",
1172
    "magic",
1173
    ]
1174

    
1175

    
1176
class ConfdRequest(ConfigObject):
1177
  """Object holding a confd request.
1178

1179
  @ivar protocol: confd protocol version
1180
  @ivar type: confd query type
1181
  @ivar query: query request
1182
  @ivar rsalt: requested reply salt
1183

1184
  """
1185
  __slots__ = [
1186
    "protocol",
1187
    "type",
1188
    "query",
1189
    "rsalt",
1190
    ]
1191

    
1192

    
1193
class ConfdReply(ConfigObject):
1194
  """Object holding a confd reply.
1195

1196
  @ivar protocol: confd protocol version
1197
  @ivar status: reply status code (ok, error)
1198
  @ivar answer: confd query reply
1199
  @ivar serial: configuration serial number
1200

1201
  """
1202
  __slots__ = [
1203
    "protocol",
1204
    "status",
1205
    "answer",
1206
    "serial",
1207
    ]
1208

    
1209

    
1210
class SerializableConfigParser(ConfigParser.SafeConfigParser):
1211
  """Simple wrapper over ConfigParse that allows serialization.
1212

1213
  This class is basically ConfigParser.SafeConfigParser with two
1214
  additional methods that allow it to serialize/unserialize to/from a
1215
  buffer.
1216

1217
  """
1218
  def Dumps(self):
1219
    """Dump this instance and return the string representation."""
1220
    buf = StringIO()
1221
    self.write(buf)
1222
    return buf.getvalue()
1223

    
1224
  @classmethod
1225
  def Loads(cls, data):
1226
    """Load data from a string."""
1227
    buf = StringIO(data)
1228
    cfp = cls()
1229
    cfp.readfp(buf)
1230
    return cfp