Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ 1177d70e

History | View | Annotate | Download (39.9 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 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", "NodeGroup", "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 source is None:
206
      source = c_type()
207
    if c_type is dict:
208
      ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
209
    elif c_type in (list, tuple, set, frozenset):
210
      ret = c_type([e_type.FromDict(elem) for elem in source])
211
    else:
212
      raise TypeError("Invalid container type %s passed to"
213
                      " _ContainerFromDicts" % c_type)
214
    return ret
215

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

219
    """
220
    dict_form = self.ToDict()
221
    clone_obj = self.__class__.FromDict(dict_form)
222
    return clone_obj
223

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

    
228
  def UpgradeConfig(self):
229
    """Fill defaults for missing configuration values.
230

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

234
    """
235
    pass
236

    
237

    
238
class TaggableObject(ConfigObject):
239
  """An generic class supporting tags.
240

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

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

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

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

    
263
  def GetTags(self):
264
    """Return the tags list.
265

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

    
272
  def AddTag(self, tag):
273
    """Add a new tag.
274

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

    
282
  def RemoveTag(self, tag):
283
    """Remove a tag.
284

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

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

296
    This replaces the tags set with a list.
297

298
    """
299
    bo = super(TaggableObject, self).ToDict()
300

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

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

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

    
316

    
317
class ConfigData(ConfigObject):
318
  """Top-level config object."""
319
  __slots__ = [
320
    "version",
321
    "cluster",
322
    "nodes",
323
    "nodegroups",
324
    "instances",
325
    "serial_no",
326
    ] + _TIMESTAMPS
327

    
328
  def ToDict(self):
329
    """Custom function for top-level config data.
330

331
    This just replaces the list of instances, nodes and the cluster
332
    with standard python types.
333

334
    """
335
    mydict = super(ConfigData, self).ToDict()
336
    mydict["cluster"] = mydict["cluster"].ToDict()
337
    for key in "nodes", "instances", "nodegroups":
338
      mydict[key] = self._ContainerToDicts(mydict[key])
339

    
340
    return mydict
341

    
342
  @classmethod
343
  def FromDict(cls, val):
344
    """Custom function for top-level config data
345

346
    """
347
    obj = super(ConfigData, cls).FromDict(val)
348
    obj.cluster = Cluster.FromDict(obj.cluster)
349
    obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
350
    obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
351
    obj.nodegroups = cls._ContainerFromDicts(obj.nodegroups, dict, NodeGroup)
352
    return obj
353

    
354
  def HasAnyDiskOfType(self, dev_type):
355
    """Check if in there is at disk of the given type in the configuration.
356

357
    @type dev_type: L{constants.LDS_BLOCK}
358
    @param dev_type: the type to look for
359
    @rtype: boolean
360
    @return: boolean indicating if a disk of the given type was found or not
361

362
    """
363
    for instance in self.instances.values():
364
      for disk in instance.disks:
365
        if disk.IsBasedOnDiskType(dev_type):
366
          return True
367
    return False
368

    
369
  def UpgradeConfig(self):
370
    """Fill defaults for missing configuration values.
371

372
    """
373
    self.cluster.UpgradeConfig()
374
    for node in self.nodes.values():
375
      node.UpgradeConfig()
376
    for instance in self.instances.values():
377
      instance.UpgradeConfig()
378
    if self.nodegroups is None:
379
      self.nodegroups = {}
380
    for nodegroup in self.nodegroups.values():
381
      nodegroup.UpgradeConfig()
382
    if self.cluster.drbd_usermode_helper is None:
383
      # To decide if we set an helper let's check if at least one instance has
384
      # a DRBD disk. This does not cover all the possible scenarios but it
385
      # gives a good approximation.
386
      if self.HasAnyDiskOfType(constants.LD_DRBD8):
387
        self.cluster.drbd_usermode_helper = constants.DEFAULT_DRBD_HELPER
388

    
389

    
390
class NIC(ConfigObject):
391
  """Config object representing a network card."""
392
  __slots__ = ["mac", "ip", "nicparams"]
393

    
394
  @classmethod
395
  def CheckParameterSyntax(cls, nicparams):
396
    """Check the given parameters for validity.
397

398
    @type nicparams:  dict
399
    @param nicparams: dictionary with parameter names/value
400
    @raise errors.ConfigurationError: when a parameter is not valid
401

402
    """
403
    if nicparams[constants.NIC_MODE] not in constants.NIC_VALID_MODES:
404
      err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
405
      raise errors.ConfigurationError(err)
406

    
407
    if (nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED and
408
        not nicparams[constants.NIC_LINK]):
409
      err = "Missing bridged nic link"
410
      raise errors.ConfigurationError(err)
411

    
412

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

560
    This is used only for drbd, which needs ip/port configuration.
561

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

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

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

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

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

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

604
    This replaces the children lists of objects with lists of
605
    standard python types.
606

607
    """
608
    bo = super(Disk, self).ToDict()
609

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

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

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

    
634
  def __str__(self):
635
    """Custom str() formatter for disks.
636

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

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

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

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

    
678
  def UpgradeConfig(self):
679
    """Fill defaults for missing configuration values.
680

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

    
687

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

    
706
  def _ComputeSecondaryNodes(self):
707
    """Compute the list of secondary nodes.
708

709
    This is a simple wrapper over _ComputeAllNodes.
710

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

    
716
  secondary_nodes = property(_ComputeSecondaryNodes, None, None,
717
                             "List of secondary nodes")
718

    
719
  def _ComputeAllNodes(self):
720
    """Compute the list of all nodes.
721

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

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

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

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

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

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

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

756
    @return: None if lvmap arg is given, otherwise, a dictionary of
757
        the form { 'nodename' : ['volume1', 'volume2', ...], ... };
758
        volumeN is of the form "vg_name/lv_name", compatible with
759
        GetVolumeList()
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[0]+"/"+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
  @type VARIANT_DELIM: string
867
  @cvar VARIANT_DELIM: the variant delimiter
868

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

    
883
  VARIANT_DELIM = "+"
884

    
885
  @classmethod
886
  def SplitNameVariant(cls, name):
887
    """Splits the name into the proper name and variant.
888

889
    @param name: the OS (unprocessed) name
890
    @rtype: list
891
    @return: a list of two elements; if the original name didn't
892
        contain a variant, it's returned as an empty string
893

894
    """
895
    nv = name.split(cls.VARIANT_DELIM, 1)
896
    if len(nv) == 1:
897
      nv.append("")
898
    return nv
899

    
900
  @classmethod
901
  def GetName(cls, name):
902
    """Returns the proper name of the os (without the variant).
903

904
    @param name: the OS (unprocessed) name
905

906
    """
907
    return cls.SplitNameVariant(name)[0]
908

    
909
  @classmethod
910
  def GetVariant(cls, name):
911
    """Returns the variant the os (without the base name).
912

913
    @param name: the OS (unprocessed) name
914

915
    """
916
    return cls.SplitNameVariant(name)[1]
917

    
918

    
919
class Node(TaggableObject):
920
  """Config object representing a node."""
921
  __slots__ = [
922
    "name",
923
    "primary_ip",
924
    "secondary_ip",
925
    "serial_no",
926
    "master_candidate",
927
    "offline",
928
    "drained",
929
    "group",
930
    "master_capable",
931
    "vm_capable",
932
    "ndparams",
933
    ] + _TIMESTAMPS + _UUID
934

    
935
  def UpgradeConfig(self):
936
    """Fill defaults for missing configuration values.
937

938
    """
939
    # pylint: disable-msg=E0203
940
    # because these are "defined" via slots, not manually
941
    if self.master_capable is None:
942
      self.master_capable = True
943

    
944
    if self.vm_capable is None:
945
      self.vm_capable = True
946

    
947
    if self.ndparams is None:
948
      self.ndparams = {}
949

    
950

    
951
class NodeGroup(ConfigObject):
952
  """Config object representing a node group."""
953
  __slots__ = [
954
    "name",
955
    "members",
956
    "ndparams",
957
    ] + _TIMESTAMPS + _UUID
958

    
959
  def ToDict(self):
960
    """Custom function for nodegroup.
961

962
    This discards the members object, which gets recalculated and is only kept
963
    in memory.
964

965
    """
966
    mydict = super(NodeGroup, self).ToDict()
967
    del mydict["members"]
968
    return mydict
969

    
970
  @classmethod
971
  def FromDict(cls, val):
972
    """Custom function for nodegroup.
973

974
    The members slot is initialized to an empty list, upon deserialization.
975

976
    """
977
    obj = super(NodeGroup, cls).FromDict(val)
978
    obj.members = []
979
    return obj
980

    
981
  def UpgradeConfig(self):
982
    """Fill defaults for missing configuration values.
983

984
    """
985
    if self.ndparams is None:
986
      self.ndparams = {}
987

    
988
  def FillND(self, node):
989
    """Return filled out ndparams for L{object.Node}
990

991
    @type node: L{objects.Node}
992
    @param node: A Node object to fill
993
    @return a copy of the node's ndparams with defaults filled
994

995
    """
996
    return self.SimpleFillND(node.ndparams)
997

    
998
  def SimpleFillND(self, ndparams):
999
    """Fill a given ndparams dict with defaults.
1000

1001
    @type ndparams: dict
1002
    @param ndparams: the dict to fill
1003
    @rtype: dict
1004
    @return: a copy of the passed in ndparams with missing keys filled
1005
        from the cluster defaults
1006

1007
    """
1008
    return FillDict(self.ndparams, ndparams)
1009

    
1010

    
1011
class Cluster(TaggableObject):
1012
  """Config object representing the cluster."""
1013
  __slots__ = [
1014
    "serial_no",
1015
    "rsahostkeypub",
1016
    "highest_used_port",
1017
    "tcpudp_port_pool",
1018
    "mac_prefix",
1019
    "volume_group_name",
1020
    "reserved_lvs",
1021
    "drbd_usermode_helper",
1022
    "default_bridge",
1023
    "default_hypervisor",
1024
    "master_node",
1025
    "master_ip",
1026
    "master_netdev",
1027
    "cluster_name",
1028
    "file_storage_dir",
1029
    "enabled_hypervisors",
1030
    "hvparams",
1031
    "os_hvp",
1032
    "beparams",
1033
    "osparams",
1034
    "nicparams",
1035
    "ndparams",
1036
    "candidate_pool_size",
1037
    "modify_etc_hosts",
1038
    "modify_ssh_setup",
1039
    "maintain_node_health",
1040
    "uid_pool",
1041
    "default_iallocator",
1042
    "hidden_os",
1043
    "blacklisted_os",
1044
    "primary_ip_family",
1045
    "prealloc_wipe_disks",
1046
    ] + _TIMESTAMPS + _UUID
1047

    
1048
  def UpgradeConfig(self):
1049
    """Fill defaults for missing configuration values.
1050

1051
    """
1052
    # pylint: disable-msg=E0203
1053
    # because these are "defined" via slots, not manually
1054
    if self.hvparams is None:
1055
      self.hvparams = constants.HVC_DEFAULTS
1056
    else:
1057
      for hypervisor in self.hvparams:
1058
        self.hvparams[hypervisor] = FillDict(
1059
            constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
1060

    
1061
    if self.os_hvp is None:
1062
      self.os_hvp = {}
1063

    
1064
    # osparams added before 2.2
1065
    if self.osparams is None:
1066
      self.osparams = {}
1067

    
1068
    if self.ndparams is None:
1069
      self.ndparams = constants.NDC_DEFAULTS
1070

    
1071
    self.beparams = UpgradeGroupedParams(self.beparams,
1072
                                         constants.BEC_DEFAULTS)
1073
    migrate_default_bridge = not self.nicparams
1074
    self.nicparams = UpgradeGroupedParams(self.nicparams,
1075
                                          constants.NICC_DEFAULTS)
1076
    if migrate_default_bridge:
1077
      self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
1078
        self.default_bridge
1079

    
1080
    if self.modify_etc_hosts is None:
1081
      self.modify_etc_hosts = True
1082

    
1083
    if self.modify_ssh_setup is None:
1084
      self.modify_ssh_setup = True
1085

    
1086
    # default_bridge is no longer used it 2.1. The slot is left there to
1087
    # support auto-upgrading. It can be removed once we decide to deprecate
1088
    # upgrading straight from 2.0.
1089
    if self.default_bridge is not None:
1090
      self.default_bridge = None
1091

    
1092
    # default_hypervisor is just the first enabled one in 2.1. This slot and
1093
    # code can be removed once upgrading straight from 2.0 is deprecated.
1094
    if self.default_hypervisor is not None:
1095
      self.enabled_hypervisors = ([self.default_hypervisor] +
1096
        [hvname for hvname in self.enabled_hypervisors
1097
         if hvname != self.default_hypervisor])
1098
      self.default_hypervisor = None
1099

    
1100
    # maintain_node_health added after 2.1.1
1101
    if self.maintain_node_health is None:
1102
      self.maintain_node_health = False
1103

    
1104
    if self.uid_pool is None:
1105
      self.uid_pool = []
1106

    
1107
    if self.default_iallocator is None:
1108
      self.default_iallocator = ""
1109

    
1110
    # reserved_lvs added before 2.2
1111
    if self.reserved_lvs is None:
1112
      self.reserved_lvs = []
1113

    
1114
    # hidden and blacklisted operating systems added before 2.2.1
1115
    if self.hidden_os is None:
1116
      self.hidden_os = []
1117

    
1118
    if self.blacklisted_os is None:
1119
      self.blacklisted_os = []
1120

    
1121
    # primary_ip_family added before 2.3
1122
    if self.primary_ip_family is None:
1123
      self.primary_ip_family = AF_INET
1124

    
1125
    if self.prealloc_wipe_disks is None:
1126
      self.prealloc_wipe_disks = False
1127

    
1128
  def ToDict(self):
1129
    """Custom function for cluster.
1130

1131
    """
1132
    mydict = super(Cluster, self).ToDict()
1133
    mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
1134
    return mydict
1135

    
1136
  @classmethod
1137
  def FromDict(cls, val):
1138
    """Custom function for cluster.
1139

1140
    """
1141
    obj = super(Cluster, cls).FromDict(val)
1142
    if not isinstance(obj.tcpudp_port_pool, set):
1143
      obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
1144
    return obj
1145

    
1146
  def GetHVDefaults(self, hypervisor, os_name=None, skip_keys=None):
1147
    """Get the default hypervisor parameters for the cluster.
1148

1149
    @param hypervisor: the hypervisor name
1150
    @param os_name: if specified, we'll also update the defaults for this OS
1151
    @param skip_keys: if passed, list of keys not to use
1152
    @return: the defaults dict
1153

1154
    """
1155
    if skip_keys is None:
1156
      skip_keys = []
1157

    
1158
    fill_stack = [self.hvparams.get(hypervisor, {})]
1159
    if os_name is not None:
1160
      os_hvp = self.os_hvp.get(os_name, {}).get(hypervisor, {})
1161
      fill_stack.append(os_hvp)
1162

    
1163
    ret_dict = {}
1164
    for o_dict in fill_stack:
1165
      ret_dict = FillDict(ret_dict, o_dict, skip_keys=skip_keys)
1166

    
1167
    return ret_dict
1168

    
1169
  def SimpleFillHV(self, hv_name, os_name, hvparams, skip_globals=False):
1170
    """Fill a given hvparams dict with cluster defaults.
1171

1172
    @type hv_name: string
1173
    @param hv_name: the hypervisor to use
1174
    @type os_name: string
1175
    @param os_name: the OS to use for overriding the hypervisor defaults
1176
    @type skip_globals: boolean
1177
    @param skip_globals: if True, the global hypervisor parameters will
1178
        not be filled
1179
    @rtype: dict
1180
    @return: a copy of the given hvparams with missing keys filled from
1181
        the cluster defaults
1182

1183
    """
1184
    if skip_globals:
1185
      skip_keys = constants.HVC_GLOBALS
1186
    else:
1187
      skip_keys = []
1188

    
1189
    def_dict = self.GetHVDefaults(hv_name, os_name, skip_keys=skip_keys)
1190
    return FillDict(def_dict, hvparams, skip_keys=skip_keys)
1191

    
1192
  def FillHV(self, instance, skip_globals=False):
1193
    """Fill an instance's hvparams dict with cluster defaults.
1194

1195
    @type instance: L{objects.Instance}
1196
    @param instance: the instance parameter to fill
1197
    @type skip_globals: boolean
1198
    @param skip_globals: if True, the global hypervisor parameters will
1199
        not be filled
1200
    @rtype: dict
1201
    @return: a copy of the instance's hvparams with missing keys filled from
1202
        the cluster defaults
1203

1204
    """
1205
    return self.SimpleFillHV(instance.hypervisor, instance.os,
1206
                             instance.hvparams, skip_globals)
1207

    
1208
  def SimpleFillBE(self, beparams):
1209
    """Fill a given beparams dict with cluster defaults.
1210

1211
    @type beparams: dict
1212
    @param beparams: the dict to fill
1213
    @rtype: dict
1214
    @return: a copy of the passed in beparams with missing keys filled
1215
        from the cluster defaults
1216

1217
    """
1218
    return FillDict(self.beparams.get(constants.PP_DEFAULT, {}), beparams)
1219

    
1220
  def FillBE(self, instance):
1221
    """Fill an instance's beparams dict with cluster defaults.
1222

1223
    @type instance: L{objects.Instance}
1224
    @param instance: the instance parameter to fill
1225
    @rtype: dict
1226
    @return: a copy of the instance's beparams with missing keys filled from
1227
        the cluster defaults
1228

1229
    """
1230
    return self.SimpleFillBE(instance.beparams)
1231

    
1232
  def SimpleFillNIC(self, nicparams):
1233
    """Fill a given nicparams dict with cluster defaults.
1234

1235
    @type nicparams: dict
1236
    @param nicparams: the dict to fill
1237
    @rtype: dict
1238
    @return: a copy of the passed in nicparams with missing keys filled
1239
        from the cluster defaults
1240

1241
    """
1242
    return FillDict(self.nicparams.get(constants.PP_DEFAULT, {}), nicparams)
1243

    
1244
  def SimpleFillOS(self, os_name, os_params):
1245
    """Fill an instance's osparams dict with cluster defaults.
1246

1247
    @type os_name: string
1248
    @param os_name: the OS name to use
1249
    @type os_params: dict
1250
    @param os_params: the dict to fill with default values
1251
    @rtype: dict
1252
    @return: a copy of the instance's osparams with missing keys filled from
1253
        the cluster defaults
1254

1255
    """
1256
    name_only = os_name.split("+", 1)[0]
1257
    # base OS
1258
    result = self.osparams.get(name_only, {})
1259
    # OS with variant
1260
    result = FillDict(result, self.osparams.get(os_name, {}))
1261
    # specified params
1262
    return FillDict(result, os_params)
1263

    
1264
  def FillND(self, node, nodegroup):
1265
    """Return filled out ndparams for L{objects.NodeGroup} and L{object.Node}
1266

1267
    @type node: L{objects.Node}
1268
    @param node: A Node object to fill
1269
    @type nodegroup: L{objects.NodeGroup}
1270
    @param nodegroup: A Node object to fill
1271
    @return a copy of the node's ndparams with defaults filled
1272

1273
    """
1274
    return self.SimpleFillND(nodegroup.FillND(node))
1275

    
1276
  def SimpleFillND(self, ndparams):
1277
    """Fill a given ndparams dict with defaults.
1278

1279
    @type ndparams: dict
1280
    @param ndparams: the dict to fill
1281
    @rtype: dict
1282
    @return: a copy of the passed in ndparams with missing keys filled
1283
        from the cluster defaults
1284

1285
    """
1286
    return FillDict(self.ndparams, ndparams)
1287

    
1288

    
1289
class BlockDevStatus(ConfigObject):
1290
  """Config object representing the status of a block device."""
1291
  __slots__ = [
1292
    "dev_path",
1293
    "major",
1294
    "minor",
1295
    "sync_percent",
1296
    "estimated_time",
1297
    "is_degraded",
1298
    "ldisk_status",
1299
    ]
1300

    
1301

    
1302
class ImportExportStatus(ConfigObject):
1303
  """Config object representing the status of an import or export."""
1304
  __slots__ = [
1305
    "recent_output",
1306
    "listen_port",
1307
    "connected",
1308
    "progress_mbytes",
1309
    "progress_throughput",
1310
    "progress_eta",
1311
    "progress_percent",
1312
    "exit_status",
1313
    "error_message",
1314
    ] + _TIMESTAMPS
1315

    
1316

    
1317
class ImportExportOptions(ConfigObject):
1318
  """Options for import/export daemon
1319

1320
  @ivar key_name: X509 key name (None for cluster certificate)
1321
  @ivar ca_pem: Remote peer CA in PEM format (None for cluster certificate)
1322
  @ivar compress: Compression method (one of L{constants.IEC_ALL})
1323
  @ivar magic: Used to ensure the connection goes to the right disk
1324
  @ivar ipv6: Whether to use IPv6
1325

1326
  """
1327
  __slots__ = [
1328
    "key_name",
1329
    "ca_pem",
1330
    "compress",
1331
    "magic",
1332
    "ipv6",
1333
    ]
1334

    
1335

    
1336
class ConfdRequest(ConfigObject):
1337
  """Object holding a confd request.
1338

1339
  @ivar protocol: confd protocol version
1340
  @ivar type: confd query type
1341
  @ivar query: query request
1342
  @ivar rsalt: requested reply salt
1343

1344
  """
1345
  __slots__ = [
1346
    "protocol",
1347
    "type",
1348
    "query",
1349
    "rsalt",
1350
    ]
1351

    
1352

    
1353
class ConfdReply(ConfigObject):
1354
  """Object holding a confd reply.
1355

1356
  @ivar protocol: confd protocol version
1357
  @ivar status: reply status code (ok, error)
1358
  @ivar answer: confd query reply
1359
  @ivar serial: configuration serial number
1360

1361
  """
1362
  __slots__ = [
1363
    "protocol",
1364
    "status",
1365
    "answer",
1366
    "serial",
1367
    ]
1368

    
1369

    
1370
class QueryFieldDefinition(ConfigObject):
1371
  """Object holding a query field definition.
1372

1373
  @ivar name: Field name as a regular expression
1374
  @ivar title: Human-readable title
1375
  @ivar kind: Field type
1376

1377
  """
1378
  __slots__ = [
1379
    "name",
1380
    "title",
1381
    "kind",
1382
    ]
1383

    
1384

    
1385
class SerializableConfigParser(ConfigParser.SafeConfigParser):
1386
  """Simple wrapper over ConfigParse that allows serialization.
1387

1388
  This class is basically ConfigParser.SafeConfigParser with two
1389
  additional methods that allow it to serialize/unserialize to/from a
1390
  buffer.
1391

1392
  """
1393
  def Dumps(self):
1394
    """Dump this instance and return the string representation."""
1395
    buf = StringIO()
1396
    self.write(buf)
1397
    return buf.getvalue()
1398

    
1399
  @classmethod
1400
  def Loads(cls, data):
1401
    """Load data from a string."""
1402
    buf = StringIO(data)
1403
    cfp = cls()
1404
    cfp.readfp(buf)
1405
    return cfp