Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ adf385c7

History | View | Annotate | Download (29.7 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007 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

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

    
48
_TIMESTAMPS = ["ctime", "mtime"]
49
_UUID = ["uuid"]
50

    
51
def FillDict(defaults_dict, custom_dict, skip_keys=None):
52
  """Basic function to apply settings on top a default dict.
53

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

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

    
74

    
75
def UpgradeGroupedParams(target, defaults):
76
  """Update all groups for the target parameter.
77

78
  @type target: dict of dicts
79
  @param target: {group: {parameter: value}}
80
  @type defaults: dict
81
  @param defaults: default parameter values
82

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

    
91

    
92
class ConfigObject(object):
93
  """A generic config object.
94

95
  It has the following properties:
96

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

101
  Classes derived from this must always declare __slots__ (we use many
102
  config objects and the memory reduction is useful)
103

104
  """
105
  __slots__ = []
106

    
107
  def __init__(self, **kwargs):
108
    for k, v in kwargs.iteritems():
109
      setattr(self, k, v)
110

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

    
117
  def __setstate__(self, state):
118
    slots = self._all_slots()
119
    for name in state:
120
      if name in slots:
121
        setattr(self, name, state[name])
122

    
123
  @classmethod
124
  def _all_slots(cls):
125
    """Compute the list of all declared slots for a class.
126

127
    """
128
    slots = []
129
    for parent in cls.__mro__:
130
      slots.extend(getattr(parent, "__slots__", []))
131
    return slots
132

    
133
  def ToDict(self):
134
    """Convert to a dict holding only standard python types.
135

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

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

    
150
  __getstate__ = ToDict
151

    
152
  @classmethod
153
  def FromDict(cls, val):
154
    """Create an object from a dictionary.
155

156
    This generic routine takes a dict, instantiates a new instance of
157
    the given class, and sets attributes based on the dict content.
158

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

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

    
172
  @staticmethod
173
  def _ContainerToDicts(container):
174
    """Convert the elements of a container to standard python types.
175

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

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

    
190
  @staticmethod
191
  def _ContainerFromDicts(source, c_type, e_type):
192
    """Convert a container from standard python types.
193

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

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

    
211
  def Copy(self):
212
    """Makes a deep copy of the current object and its children.
213

214
    """
215
    dict_form = self.ToDict()
216
    clone_obj = self.__class__.FromDict(dict_form)
217
    return clone_obj
218

    
219
  def __repr__(self):
220
    """Implement __repr__ for ConfigObjects."""
221
    return repr(self.ToDict())
222

    
223
  def UpgradeConfig(self):
224
    """Fill defaults for missing configuration values.
225

226
    This method will be called at configuration load time, and its
227
    implementation will be object dependent.
228

229
    """
230
    pass
231

    
232

    
233
class TaggableObject(ConfigObject):
234
  """An generic class supporting tags.
235

236
  """
237
  __slots__ = ["tags"]
238
  VALID_TAG_RE = re.compile("^[\w.+*/:@-]+$")
239

    
240
  @classmethod
241
  def ValidateTag(cls, tag):
242
    """Check if a tag is valid.
243

244
    If the tag is invalid, an errors.TagError will be raised. The
245
    function has no return value.
246

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

    
258
  def GetTags(self):
259
    """Return the tags list.
260

261
    """
262
    tags = getattr(self, "tags", None)
263
    if tags is None:
264
      tags = self.tags = set()
265
    return tags
266

    
267
  def AddTag(self, tag):
268
    """Add a new tag.
269

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

    
277
  def RemoveTag(self, tag):
278
    """Remove a tag.
279

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

    
288
  def ToDict(self):
289
    """Taggable-object-specific conversion to standard python types.
290

291
    This replaces the tags set with a list.
292

293
    """
294
    bo = super(TaggableObject, self).ToDict()
295

    
296
    tags = bo.get("tags", None)
297
    if isinstance(tags, set):
298
      bo["tags"] = list(tags)
299
    return bo
300

    
301
  @classmethod
302
  def FromDict(cls, val):
303
    """Custom function for instances.
304

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

    
311

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

    
317
  def ToDict(self):
318
    """Custom function for top-level config data.
319

320
    This just replaces the list of instances, nodes and the cluster
321
    with standard python types.
322

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

    
329
    return mydict
330

    
331
  @classmethod
332
  def FromDict(cls, val):
333
    """Custom function for top-level config data
334

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

    
342
  def UpgradeConfig(self):
343
    """Fill defaults for missing configuration values.
344

345
    """
346
    self.cluster.UpgradeConfig()
347
    for node in self.nodes.values():
348
      node.UpgradeConfig()
349
    for instance in self.instances.values():
350
      instance.UpgradeConfig()
351

    
352

    
353
class NIC(ConfigObject):
354
  """Config object representing a network card."""
355
  __slots__ = ["mac", "ip", "bridge", "nicparams"]
356

    
357
  @classmethod
358
  def CheckParameterSyntax(cls, nicparams):
359
    """Check the given parameters for validity.
360

361
    @type nicparams:  dict
362
    @param nicparams: dictionary with parameter names/value
363
    @raise errors.ConfigurationError: when a parameter is not valid
364

365
    """
366
    if nicparams[constants.NIC_MODE] not in constants.NIC_VALID_MODES:
367
      err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
368
      raise errors.ConfigurationError(err)
369

    
370
    if (nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED and
371
        not nicparams[constants.NIC_LINK]):
372
      err = "Missing bridged nic link"
373
      raise errors.ConfigurationError(err)
374

    
375
  def UpgradeConfig(self):
376
    """Fill defaults for missing configuration values.
377

378
    """
379
    if self.nicparams is None:
380
      self.nicparams = {}
381
      if self.bridge is not None:
382
        self.nicparams[constants.NIC_MODE] = constants.NIC_MODE_BRIDGED
383
        self.nicparams[constants.NIC_LINK] = self.bridge
384
    # bridge is no longer used it 2.1. The slot is left there to support
385
    # upgrading, but will be removed in 2.2
386
    if self.bridge is not None:
387
      self.bridge = None
388

    
389

    
390
class Disk(ConfigObject):
391
  """Config object representing a block device."""
392
  __slots__ = ["dev_type", "logical_id", "physical_id",
393
               "children", "iv_name", "size", "mode"]
394

    
395
  def CreateOnSecondary(self):
396
    """Test if this device needs to be created on a secondary node."""
397
    return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
398

    
399
  def AssembleOnSecondary(self):
400
    """Test if this device needs to be assembled on a secondary node."""
401
    return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
402

    
403
  def OpenOnSecondary(self):
404
    """Test if this device needs to be opened on a secondary node."""
405
    return self.dev_type in (constants.LD_LV,)
406

    
407
  def StaticDevPath(self):
408
    """Return the device path if this device type has a static one.
409

410
    Some devices (LVM for example) live always at the same /dev/ path,
411
    irrespective of their status. For such devices, we return this
412
    path, for others we return None.
413

414
    """
415
    if self.dev_type == constants.LD_LV:
416
      return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
417
    return None
418

    
419
  def ChildrenNeeded(self):
420
    """Compute the needed number of children for activation.
421

422
    This method will return either -1 (all children) or a positive
423
    number denoting the minimum number of children needed for
424
    activation (only mirrored devices will usually return >=0).
425

426
    Currently, only DRBD8 supports diskless activation (therefore we
427
    return 0), for all other we keep the previous semantics and return
428
    -1.
429

430
    """
431
    if self.dev_type == constants.LD_DRBD8:
432
      return 0
433
    return -1
434

    
435
  def GetNodes(self, node):
436
    """This function returns the nodes this device lives on.
437

438
    Given the node on which the parent of the device lives on (or, in
439
    case of a top-level device, the primary node of the devices'
440
    instance), this function will return a list of nodes on which this
441
    devices needs to (or can) be assembled.
442

443
    """
444
    if self.dev_type in [constants.LD_LV, constants.LD_FILE]:
445
      result = [node]
446
    elif self.dev_type in constants.LDS_DRBD:
447
      result = [self.logical_id[0], self.logical_id[1]]
448
      if node not in result:
449
        raise errors.ConfigurationError("DRBD device passed unknown node")
450
    else:
451
      raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
452
    return result
453

    
454
  def ComputeNodeTree(self, parent_node):
455
    """Compute the node/disk tree for this disk and its children.
456

457
    This method, given the node on which the parent disk lives, will
458
    return the list of all (node, disk) pairs which describe the disk
459
    tree in the most compact way. For example, a drbd/lvm stack
460
    will be returned as (primary_node, drbd) and (secondary_node, drbd)
461
    which represents all the top-level devices on the nodes.
462

463
    """
464
    my_nodes = self.GetNodes(parent_node)
465
    result = [(node, self) for node in my_nodes]
466
    if not self.children:
467
      # leaf device
468
      return result
469
    for node in my_nodes:
470
      for child in self.children:
471
        child_result = child.ComputeNodeTree(node)
472
        if len(child_result) == 1:
473
          # child (and all its descendants) is simple, doesn't split
474
          # over multiple hosts, so we don't need to describe it, our
475
          # own entry for this node describes it completely
476
          continue
477
        else:
478
          # check if child nodes differ from my nodes; note that
479
          # subdisk can differ from the child itself, and be instead
480
          # one of its descendants
481
          for subnode, subdisk in child_result:
482
            if subnode not in my_nodes:
483
              result.append((subnode, subdisk))
484
            # otherwise child is under our own node, so we ignore this
485
            # entry (but probably the other results in the list will
486
            # be different)
487
    return result
488

    
489
  def RecordGrow(self, amount):
490
    """Update the size of this disk after growth.
491

492
    This method recurses over the disks's children and updates their
493
    size correspondigly. The method needs to be kept in sync with the
494
    actual algorithms from bdev.
495

496
    """
497
    if self.dev_type == constants.LD_LV:
498
      self.size += amount
499
    elif self.dev_type == constants.LD_DRBD8:
500
      if self.children:
501
        self.children[0].RecordGrow(amount)
502
      self.size += amount
503
    else:
504
      raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
505
                                   " disk type %s" % self.dev_type)
506

    
507
  def UnsetSize(self):
508
    """Sets recursively the size to zero for the disk and its children.
509

510
    """
511
    if self.children:
512
      for child in self.children:
513
        child.UnsetSize()
514
    self.size = 0
515

    
516
  def SetPhysicalID(self, target_node, nodes_ip):
517
    """Convert the logical ID to the physical ID.
518

519
    This is used only for drbd, which needs ip/port configuration.
520

521
    The routine descends down and updates its children also, because
522
    this helps when the only the top device is passed to the remote
523
    node.
524

525
    Arguments:
526
      - target_node: the node we wish to configure for
527
      - nodes_ip: a mapping of node name to ip
528

529
    The target_node must exist in in nodes_ip, and must be one of the
530
    nodes in the logical ID for each of the DRBD devices encountered
531
    in the disk tree.
532

533
    """
534
    if self.children:
535
      for child in self.children:
536
        child.SetPhysicalID(target_node, nodes_ip)
537

    
538
    if self.logical_id is None and self.physical_id is not None:
539
      return
540
    if self.dev_type in constants.LDS_DRBD:
541
      pnode, snode, port, pminor, sminor, secret = self.logical_id
542
      if target_node not in (pnode, snode):
543
        raise errors.ConfigurationError("DRBD device not knowing node %s" %
544
                                        target_node)
545
      pnode_ip = nodes_ip.get(pnode, None)
546
      snode_ip = nodes_ip.get(snode, None)
547
      if pnode_ip is None or snode_ip is None:
548
        raise errors.ConfigurationError("Can't find primary or secondary node"
549
                                        " for %s" % str(self))
550
      p_data = (pnode_ip, port)
551
      s_data = (snode_ip, port)
552
      if pnode == target_node:
553
        self.physical_id = p_data + s_data + (pminor, secret)
554
      else: # it must be secondary, we tested above
555
        self.physical_id = s_data + p_data + (sminor, secret)
556
    else:
557
      self.physical_id = self.logical_id
558
    return
559

    
560
  def ToDict(self):
561
    """Disk-specific conversion to standard python types.
562

563
    This replaces the children lists of objects with lists of
564
    standard python types.
565

566
    """
567
    bo = super(Disk, self).ToDict()
568

    
569
    for attr in ("children",):
570
      alist = bo.get(attr, None)
571
      if alist:
572
        bo[attr] = self._ContainerToDicts(alist)
573
    return bo
574

    
575
  @classmethod
576
  def FromDict(cls, val):
577
    """Custom function for Disks
578

579
    """
580
    obj = super(Disk, cls).FromDict(val)
581
    if obj.children:
582
      obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
583
    if obj.logical_id and isinstance(obj.logical_id, list):
584
      obj.logical_id = tuple(obj.logical_id)
585
    if obj.physical_id and isinstance(obj.physical_id, list):
586
      obj.physical_id = tuple(obj.physical_id)
587
    if obj.dev_type in constants.LDS_DRBD:
588
      # we need a tuple of length six here
589
      if len(obj.logical_id) < 6:
590
        obj.logical_id += (None,) * (6 - len(obj.logical_id))
591
    return obj
592

    
593
  def __str__(self):
594
    """Custom str() formatter for disks.
595

596
    """
597
    if self.dev_type == constants.LD_LV:
598
      val =  "<LogicalVolume(/dev/%s/%s" % self.logical_id
599
    elif self.dev_type in constants.LDS_DRBD:
600
      node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
601
      val = "<DRBD8("
602
      if self.physical_id is None:
603
        phy = "unconfigured"
604
      else:
605
        phy = ("configured as %s:%s %s:%s" %
606
               (self.physical_id[0], self.physical_id[1],
607
                self.physical_id[2], self.physical_id[3]))
608

    
609
      val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
610
              (node_a, minor_a, node_b, minor_b, port, phy))
611
      if self.children and self.children.count(None) == 0:
612
        val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
613
      else:
614
        val += "no local storage"
615
    else:
616
      val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
617
             (self.dev_type, self.logical_id, self.physical_id, self.children))
618
    if self.iv_name is None:
619
      val += ", not visible"
620
    else:
621
      val += ", visible as /dev/%s" % self.iv_name
622
    if isinstance(self.size, int):
623
      val += ", size=%dm)>" % self.size
624
    else:
625
      val += ", size='%s')>" % (self.size,)
626
    return val
627

    
628
  def Verify(self):
629
    """Checks that this disk is correctly configured.
630

631
    """
632
    all_errors = []
633
    if self.mode not in constants.DISK_ACCESS_SET:
634
      all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
635
    return all_errors
636

    
637
  def UpgradeConfig(self):
638
    """Fill defaults for missing configuration values.
639

640
    """
641
    if self.children:
642
      for child in self.children:
643
        child.UpgradeConfig()
644
    # add here config upgrade for this disk
645

    
646

    
647
class Instance(TaggableObject):
648
  """Config object representing an instance."""
649
  __slots__ = [
650
    "name",
651
    "primary_node",
652
    "os",
653
    "hypervisor",
654
    "hvparams",
655
    "beparams",
656
    "admin_up",
657
    "nics",
658
    "disks",
659
    "disk_template",
660
    "network_port",
661
    "serial_no",
662
    ] + _TIMESTAMPS + _UUID
663

    
664
  def _ComputeSecondaryNodes(self):
665
    """Compute the list of secondary nodes.
666

667
    This is a simple wrapper over _ComputeAllNodes.
668

669
    """
670
    all_nodes = set(self._ComputeAllNodes())
671
    all_nodes.discard(self.primary_node)
672
    return tuple(all_nodes)
673

    
674
  secondary_nodes = property(_ComputeSecondaryNodes, None, None,
675
                             "List of secondary nodes")
676

    
677
  def _ComputeAllNodes(self):
678
    """Compute the list of all nodes.
679

680
    Since the data is already there (in the drbd disks), keeping it as
681
    a separate normal attribute is redundant and if not properly
682
    synchronised can cause problems. Thus it's better to compute it
683
    dynamically.
684

685
    """
686
    def _Helper(nodes, device):
687
      """Recursively computes nodes given a top device."""
688
      if device.dev_type in constants.LDS_DRBD:
689
        nodea, nodeb = device.logical_id[:2]
690
        nodes.add(nodea)
691
        nodes.add(nodeb)
692
      if device.children:
693
        for child in device.children:
694
          _Helper(nodes, child)
695

    
696
    all_nodes = set()
697
    all_nodes.add(self.primary_node)
698
    for device in self.disks:
699
      _Helper(all_nodes, device)
700
    return tuple(all_nodes)
701

    
702
  all_nodes = property(_ComputeAllNodes, None, None,
703
                       "List of all nodes of the instance")
704

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

708
    This function figures out what logical volumes should belong on
709
    which nodes, recursing through a device tree.
710

711
    @param lvmap: optional dictionary to receive the
712
        'node' : ['lv', ...] data.
713

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

717
    """
718
    if node == None:
719
      node = self.primary_node
720

    
721
    if lvmap is None:
722
      lvmap = { node : [] }
723
      ret = lvmap
724
    else:
725
      if not node in lvmap:
726
        lvmap[node] = []
727
      ret = None
728

    
729
    if not devs:
730
      devs = self.disks
731

    
732
    for dev in devs:
733
      if dev.dev_type == constants.LD_LV:
734
        lvmap[node].append(dev.logical_id[1])
735

    
736
      elif dev.dev_type in constants.LDS_DRBD:
737
        if dev.children:
738
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
739
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
740

    
741
      elif dev.children:
742
        self.MapLVsByNode(lvmap, dev.children, node)
743

    
744
    return ret
745

    
746
  def FindDisk(self, idx):
747
    """Find a disk given having a specified index.
748

749
    This is just a wrapper that does validation of the index.
750

751
    @type idx: int
752
    @param idx: the disk index
753
    @rtype: L{Disk}
754
    @return: the corresponding disk
755
    @raise errors.OpPrereqError: when the given index is not valid
756

757
    """
758
    try:
759
      idx = int(idx)
760
      return self.disks[idx]
761
    except (TypeError, ValueError), err:
762
      raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err),
763
                                 errors.ECODE_INVAL)
764
    except IndexError:
765
      raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
766
                                 " 0 to %d" % (idx, len(self.disks)),
767
                                 errors.ECODE_INVAL)
768

    
769
  def ToDict(self):
770
    """Instance-specific conversion to standard python types.
771

772
    This replaces the children lists of objects with lists of standard
773
    python types.
774

775
    """
776
    bo = super(Instance, self).ToDict()
777

    
778
    for attr in "nics", "disks":
779
      alist = bo.get(attr, None)
780
      if alist:
781
        nlist = self._ContainerToDicts(alist)
782
      else:
783
        nlist = []
784
      bo[attr] = nlist
785
    return bo
786

    
787
  @classmethod
788
  def FromDict(cls, val):
789
    """Custom function for instances.
790

791
    """
792
    obj = super(Instance, cls).FromDict(val)
793
    obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
794
    obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
795
    return obj
796

    
797
  def UpgradeConfig(self):
798
    """Fill defaults for missing configuration values.
799

800
    """
801
    for nic in self.nics:
802
      nic.UpgradeConfig()
803
    for disk in self.disks:
804
      disk.UpgradeConfig()
805
    if self.hvparams:
806
      for key in constants.HVC_GLOBALS:
807
        try:
808
          del self.hvparams[key]
809
        except KeyError:
810
          pass
811

    
812

    
813
class OS(ConfigObject):
814
  """Config object representing an operating system."""
815
  __slots__ = [
816
    "name",
817
    "path",
818
    "api_versions",
819
    "create_script",
820
    "export_script",
821
    "import_script",
822
    "rename_script",
823
    "supported_variants",
824
    ]
825

    
826

    
827
class Node(TaggableObject):
828
  """Config object representing a node."""
829
  __slots__ = [
830
    "name",
831
    "primary_ip",
832
    "secondary_ip",
833
    "serial_no",
834
    "master_candidate",
835
    "offline",
836
    "drained",
837
    ] + _TIMESTAMPS + _UUID
838

    
839

    
840
class Cluster(TaggableObject):
841
  """Config object representing the cluster."""
842
  __slots__ = [
843
    "serial_no",
844
    "rsahostkeypub",
845
    "highest_used_port",
846
    "tcpudp_port_pool",
847
    "mac_prefix",
848
    "volume_group_name",
849
    "default_bridge",
850
    "default_hypervisor",
851
    "master_node",
852
    "master_ip",
853
    "master_netdev",
854
    "cluster_name",
855
    "file_storage_dir",
856
    "enabled_hypervisors",
857
    "hvparams",
858
    "beparams",
859
    "nicparams",
860
    "candidate_pool_size",
861
    "modify_etc_hosts",
862
    "modify_ssh_setup",
863
    ] + _TIMESTAMPS + _UUID
864

    
865
  def UpgradeConfig(self):
866
    """Fill defaults for missing configuration values.
867

868
    """
869
    # pylint: disable-msg=E0203
870
    # because these are "defined" via slots, not manually
871
    if self.hvparams is None:
872
      self.hvparams = constants.HVC_DEFAULTS
873
    else:
874
      for hypervisor in self.hvparams:
875
        self.hvparams[hypervisor] = FillDict(
876
            constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
877

    
878
    self.beparams = UpgradeGroupedParams(self.beparams,
879
                                         constants.BEC_DEFAULTS)
880
    migrate_default_bridge = not self.nicparams
881
    self.nicparams = UpgradeGroupedParams(self.nicparams,
882
                                          constants.NICC_DEFAULTS)
883
    if migrate_default_bridge:
884
      self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
885
        self.default_bridge
886

    
887
    if self.modify_etc_hosts is None:
888
      self.modify_etc_hosts = True
889

    
890
    if self.modify_ssh_setup is None:
891
      self.modify_ssh_setup = True
892

    
893
    # default_bridge is no longer used it 2.1. The slot is left there to
894
    # support auto-upgrading, but will be removed in 2.2
895
    if self.default_bridge is not None:
896
      self.default_bridge = None
897

    
898
    # default_hypervisor is just the first enabled one in 2.1
899
    if self.default_hypervisor is not None:
900
      self.enabled_hypervisors = ([self.default_hypervisor] +
901
        [hvname for hvname in self.enabled_hypervisors
902
         if hvname != self.default_hypervisor])
903
      self.default_hypervisor = None
904

    
905
  def ToDict(self):
906
    """Custom function for cluster.
907

908
    """
909
    mydict = super(Cluster, self).ToDict()
910
    mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
911
    return mydict
912

    
913
  @classmethod
914
  def FromDict(cls, val):
915
    """Custom function for cluster.
916

917
    """
918
    obj = super(Cluster, cls).FromDict(val)
919
    if not isinstance(obj.tcpudp_port_pool, set):
920
      obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
921
    return obj
922

    
923
  def FillHV(self, instance, skip_globals=False):
924
    """Fill an instance's hvparams dict.
925

926
    @type instance: L{objects.Instance}
927
    @param instance: the instance parameter to fill
928
    @type skip_globals: boolean
929
    @param skip_globals: if True, the global hypervisor parameters will
930
        not be filled
931
    @rtype: dict
932
    @return: a copy of the instance's hvparams with missing keys filled from
933
        the cluster defaults
934

935
    """
936
    if skip_globals:
937
      skip_keys = constants.HVC_GLOBALS
938
    else:
939
      skip_keys = []
940
    return FillDict(self.hvparams.get(instance.hypervisor, {}),
941
                    instance.hvparams, skip_keys=skip_keys)
942

    
943
  def FillBE(self, instance):
944
    """Fill an instance's beparams dict.
945

946
    @type instance: L{objects.Instance}
947
    @param instance: the instance parameter to fill
948
    @rtype: dict
949
    @return: a copy of the instance's beparams with missing keys filled from
950
        the cluster defaults
951

952
    """
953
    return FillDict(self.beparams.get(constants.PP_DEFAULT, {}),
954
                    instance.beparams)
955

    
956

    
957
class BlockDevStatus(ConfigObject):
958
  """Config object representing the status of a block device."""
959
  __slots__ = [
960
    "dev_path",
961
    "major",
962
    "minor",
963
    "sync_percent",
964
    "estimated_time",
965
    "is_degraded",
966
    "ldisk_status",
967
    ]
968

    
969

    
970
class ConfdRequest(ConfigObject):
971
  """Object holding a confd request.
972

973
  @ivar protocol: confd protocol version
974
  @ivar type: confd query type
975
  @ivar query: query request
976
  @ivar rsalt: requested reply salt
977

978
  """
979
  __slots__ = [
980
    "protocol",
981
    "type",
982
    "query",
983
    "rsalt",
984
    ]
985

    
986

    
987
class ConfdReply(ConfigObject):
988
  """Object holding a confd reply.
989

990
  @ivar protocol: confd protocol version
991
  @ivar status: reply status code (ok, error)
992
  @ivar answer: confd query reply
993
  @ivar serial: configuration serial number
994

995
  """
996
  __slots__ = [
997
    "protocol",
998
    "status",
999
    "answer",
1000
    "serial",
1001
    ]
1002

    
1003

    
1004
class SerializableConfigParser(ConfigParser.SafeConfigParser):
1005
  """Simple wrapper over ConfigParse that allows serialization.
1006

1007
  This class is basically ConfigParser.SafeConfigParser with two
1008
  additional methods that allow it to serialize/unserialize to/from a
1009
  buffer.
1010

1011
  """
1012
  def Dumps(self):
1013
    """Dump this instance and return the string representation."""
1014
    buf = StringIO()
1015
    self.write(buf)
1016
    return buf.getvalue()
1017

    
1018
  @staticmethod
1019
  def Loads(data):
1020
    """Load data from a string."""
1021
    buf = StringIO(data)
1022
    cfp = SerializableConfigParser()
1023
    cfp.readfp(buf)
1024
    return cfp