Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ d63479b5

History | View | Annotate | Download (30.9 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
    @warning: The path returned is not a normalized pathname; callers
415
        should check that it is a valid path.
416

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

    
422
  def ChildrenNeeded(self):
423
    """Compute the needed number of children for activation.
424

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

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

433
    """
434
    if self.dev_type == constants.LD_DRBD8:
435
      return 0
436
    return -1
437

    
438
  def GetNodes(self, node):
439
    """This function returns the nodes this device lives on.
440

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

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

    
457
  def ComputeNodeTree(self, parent_node):
458
    """Compute the node/disk tree for this disk and its children.
459

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

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

    
492
  def RecordGrow(self, amount):
493
    """Update the size of this disk after growth.
494

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

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

    
510
  def UnsetSize(self):
511
    """Sets recursively the size to zero for the disk and its children.
512

513
    """
514
    if self.children:
515
      for child in self.children:
516
        child.UnsetSize()
517
    self.size = 0
518

    
519
  def SetPhysicalID(self, target_node, nodes_ip):
520
    """Convert the logical ID to the physical ID.
521

522
    This is used only for drbd, which needs ip/port configuration.
523

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

528
    Arguments:
529
      - target_node: the node we wish to configure for
530
      - nodes_ip: a mapping of node name to ip
531

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

536
    """
537
    if self.children:
538
      for child in self.children:
539
        child.SetPhysicalID(target_node, nodes_ip)
540

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

    
563
  def ToDict(self):
564
    """Disk-specific conversion to standard python types.
565

566
    This replaces the children lists of objects with lists of
567
    standard python types.
568

569
    """
570
    bo = super(Disk, self).ToDict()
571

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

    
578
  @classmethod
579
  def FromDict(cls, val):
580
    """Custom function for Disks
581

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

    
596
  def __str__(self):
597
    """Custom str() formatter for disks.
598

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

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

    
631
  def Verify(self):
632
    """Checks that this disk is correctly configured.
633

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

    
640
  def UpgradeConfig(self):
641
    """Fill defaults for missing configuration values.
642

643
    """
644
    if self.children:
645
      for child in self.children:
646
        child.UpgradeConfig()
647
    # add here config upgrade for this disk
648

    
649

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

    
667
  def _ComputeSecondaryNodes(self):
668
    """Compute the list of secondary nodes.
669

670
    This is a simple wrapper over _ComputeAllNodes.
671

672
    """
673
    all_nodes = set(self._ComputeAllNodes())
674
    all_nodes.discard(self.primary_node)
675
    return tuple(all_nodes)
676

    
677
  secondary_nodes = property(_ComputeSecondaryNodes, None, None,
678
                             "List of secondary nodes")
679

    
680
  def _ComputeAllNodes(self):
681
    """Compute the list of all nodes.
682

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

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

    
699
    all_nodes = set()
700
    all_nodes.add(self.primary_node)
701
    for device in self.disks:
702
      _Helper(all_nodes, device)
703
    return tuple(all_nodes)
704

    
705
  all_nodes = property(_ComputeAllNodes, None, None,
706
                       "List of all nodes of the instance")
707

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

711
    This function figures out what logical volumes should belong on
712
    which nodes, recursing through a device tree.
713

714
    @param lvmap: optional dictionary to receive the
715
        'node' : ['lv', ...] data.
716

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

720
    """
721
    if node == None:
722
      node = self.primary_node
723

    
724
    if lvmap is None:
725
      lvmap = { node : [] }
726
      ret = lvmap
727
    else:
728
      if not node in lvmap:
729
        lvmap[node] = []
730
      ret = None
731

    
732
    if not devs:
733
      devs = self.disks
734

    
735
    for dev in devs:
736
      if dev.dev_type == constants.LD_LV:
737
        lvmap[node].append(dev.logical_id[1])
738

    
739
      elif dev.dev_type in constants.LDS_DRBD:
740
        if dev.children:
741
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
742
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
743

    
744
      elif dev.children:
745
        self.MapLVsByNode(lvmap, dev.children, node)
746

    
747
    return ret
748

    
749
  def FindDisk(self, idx):
750
    """Find a disk given having a specified index.
751

752
    This is just a wrapper that does validation of the index.
753

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

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

    
772
  def ToDict(self):
773
    """Instance-specific conversion to standard python types.
774

775
    This replaces the children lists of objects with lists of standard
776
    python types.
777

778
    """
779
    bo = super(Instance, self).ToDict()
780

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

    
790
  @classmethod
791
  def FromDict(cls, val):
792
    """Custom function for instances.
793

794
    """
795
    obj = super(Instance, cls).FromDict(val)
796
    obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
797
    obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
798
    return obj
799

    
800
  def UpgradeConfig(self):
801
    """Fill defaults for missing configuration values.
802

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

    
815

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

    
829

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

    
842

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

    
870
  def UpgradeConfig(self):
871
    """Fill defaults for missing configuration values.
872

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

    
883
    # TODO: Figure out if it's better to put this into OS than Cluster
884
    if self.os_hvp is None:
885
      self.os_hvp = {}
886

    
887
    self.beparams = UpgradeGroupedParams(self.beparams,
888
                                         constants.BEC_DEFAULTS)
889
    migrate_default_bridge = not self.nicparams
890
    self.nicparams = UpgradeGroupedParams(self.nicparams,
891
                                          constants.NICC_DEFAULTS)
892
    if migrate_default_bridge:
893
      self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
894
        self.default_bridge
895

    
896
    if self.modify_etc_hosts is None:
897
      self.modify_etc_hosts = True
898

    
899
    if self.modify_ssh_setup is None:
900
      self.modify_ssh_setup = True
901

    
902
    # default_bridge is no longer used it 2.1. The slot is left there to
903
    # support auto-upgrading, but will be removed in 2.2
904
    if self.default_bridge is not None:
905
      self.default_bridge = None
906

    
907
    # default_hypervisor is just the first enabled one in 2.1
908
    if self.default_hypervisor is not None:
909
      self.enabled_hypervisors = ([self.default_hypervisor] +
910
        [hvname for hvname in self.enabled_hypervisors
911
         if hvname != self.default_hypervisor])
912
      self.default_hypervisor = None
913

    
914
    # maintain_node_health added after 2.1.1
915
    if self.maintain_node_health is None:
916
      self.maintain_node_health = False
917

    
918
  def ToDict(self):
919
    """Custom function for cluster.
920

921
    """
922
    mydict = super(Cluster, self).ToDict()
923
    mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
924
    return mydict
925

    
926
  @classmethod
927
  def FromDict(cls, val):
928
    """Custom function for cluster.
929

930
    """
931
    obj = super(Cluster, cls).FromDict(val)
932
    if not isinstance(obj.tcpudp_port_pool, set):
933
      obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
934
    return obj
935

    
936
  def GetHVDefaults(self, hypervisor, os_name=None, skip_keys=None):
937
    """Get the default hypervisor parameters for the cluster.
938

939
    @param hypervisor: the hypervisor name
940
    @param os_name: if specified, we'll also update the defaults for this OS
941
    @param skip_keys: if passed, list of keys not to use
942
    @return: the defaults dict
943

944
    """
945
    if skip_keys is None:
946
      skip_keys = []
947

    
948
    fill_stack = [self.hvparams.get(hypervisor, {})]
949
    if os_name is not None:
950
      os_hvp = self.os_hvp.get(os_name, {}).get(hypervisor, {})
951
      fill_stack.append(os_hvp)
952

    
953
    ret_dict = {}
954
    for o_dict in fill_stack:
955
      ret_dict = FillDict(ret_dict, o_dict, skip_keys=skip_keys)
956

    
957
    return ret_dict
958

    
959

    
960
  def FillHV(self, instance, skip_globals=False):
961
    """Fill an instance's hvparams dict.
962

963
    @type instance: L{objects.Instance}
964
    @param instance: the instance parameter to fill
965
    @type skip_globals: boolean
966
    @param skip_globals: if True, the global hypervisor parameters will
967
        not be filled
968
    @rtype: dict
969
    @return: a copy of the instance's hvparams with missing keys filled from
970
        the cluster defaults
971

972
    """
973
    if skip_globals:
974
      skip_keys = constants.HVC_GLOBALS
975
    else:
976
      skip_keys = []
977

    
978
    def_dict = self.GetHVDefaults(instance.hypervisor, instance.os,
979
                                  skip_keys=skip_keys)
980
    return FillDict(def_dict, instance.hvparams, skip_keys=skip_keys)
981

    
982
  def FillBE(self, instance):
983
    """Fill an instance's beparams dict.
984

985
    @type instance: L{objects.Instance}
986
    @param instance: the instance parameter to fill
987
    @rtype: dict
988
    @return: a copy of the instance's beparams with missing keys filled from
989
        the cluster defaults
990

991
    """
992
    return FillDict(self.beparams.get(constants.PP_DEFAULT, {}),
993
                    instance.beparams)
994

    
995

    
996
class BlockDevStatus(ConfigObject):
997
  """Config object representing the status of a block device."""
998
  __slots__ = [
999
    "dev_path",
1000
    "major",
1001
    "minor",
1002
    "sync_percent",
1003
    "estimated_time",
1004
    "is_degraded",
1005
    "ldisk_status",
1006
    ]
1007

    
1008

    
1009
class ConfdRequest(ConfigObject):
1010
  """Object holding a confd request.
1011

1012
  @ivar protocol: confd protocol version
1013
  @ivar type: confd query type
1014
  @ivar query: query request
1015
  @ivar rsalt: requested reply salt
1016

1017
  """
1018
  __slots__ = [
1019
    "protocol",
1020
    "type",
1021
    "query",
1022
    "rsalt",
1023
    ]
1024

    
1025

    
1026
class ConfdReply(ConfigObject):
1027
  """Object holding a confd reply.
1028

1029
  @ivar protocol: confd protocol version
1030
  @ivar status: reply status code (ok, error)
1031
  @ivar answer: confd query reply
1032
  @ivar serial: configuration serial number
1033

1034
  """
1035
  __slots__ = [
1036
    "protocol",
1037
    "status",
1038
    "answer",
1039
    "serial",
1040
    ]
1041

    
1042

    
1043
class SerializableConfigParser(ConfigParser.SafeConfigParser):
1044
  """Simple wrapper over ConfigParse that allows serialization.
1045

1046
  This class is basically ConfigParser.SafeConfigParser with two
1047
  additional methods that allow it to serialize/unserialize to/from a
1048
  buffer.
1049

1050
  """
1051
  def Dumps(self):
1052
    """Dump this instance and return the string representation."""
1053
    buf = StringIO()
1054
    self.write(buf)
1055
    return buf.getvalue()
1056

    
1057
  @classmethod
1058
  def Loads(cls, data):
1059
    """Load data from a string."""
1060
    buf = StringIO(data)
1061
    cfp = cls()
1062
    cfp.readfp(buf)
1063
    return cfp