Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ 84d7e26b

History | View | Annotate | Download (40.4 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", "bridge", "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
  def UpgradeConfig(self):
413
    """Fill defaults for missing configuration values.
414

415
    """
416
    if self.nicparams is None:
417
      self.nicparams = {}
418
      if self.bridge is not None:
419
        self.nicparams[constants.NIC_MODE] = constants.NIC_MODE_BRIDGED
420
        self.nicparams[constants.NIC_LINK] = self.bridge
421
    # bridge is no longer used it 2.1. The slot is left there to support
422
    # upgrading, but can be removed once upgrades to the current version
423
    # straight from 2.0 are deprecated.
424
    if self.bridge is not None:
425
      self.bridge = None
426

    
427

    
428
class Disk(ConfigObject):
429
  """Config object representing a block device."""
430
  __slots__ = ["dev_type", "logical_id", "physical_id",
431
               "children", "iv_name", "size", "mode"]
432

    
433
  def CreateOnSecondary(self):
434
    """Test if this device needs to be created on a secondary node."""
435
    return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
436

    
437
  def AssembleOnSecondary(self):
438
    """Test if this device needs to be assembled on a secondary node."""
439
    return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
440

    
441
  def OpenOnSecondary(self):
442
    """Test if this device needs to be opened on a secondary node."""
443
    return self.dev_type in (constants.LD_LV,)
444

    
445
  def StaticDevPath(self):
446
    """Return the device path if this device type has a static one.
447

448
    Some devices (LVM for example) live always at the same /dev/ path,
449
    irrespective of their status. For such devices, we return this
450
    path, for others we return None.
451

452
    @warning: The path returned is not a normalized pathname; callers
453
        should check that it is a valid path.
454

455
    """
456
    if self.dev_type == constants.LD_LV:
457
      return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
458
    return None
459

    
460
  def ChildrenNeeded(self):
461
    """Compute the needed number of children for activation.
462

463
    This method will return either -1 (all children) or a positive
464
    number denoting the minimum number of children needed for
465
    activation (only mirrored devices will usually return >=0).
466

467
    Currently, only DRBD8 supports diskless activation (therefore we
468
    return 0), for all other we keep the previous semantics and return
469
    -1.
470

471
    """
472
    if self.dev_type == constants.LD_DRBD8:
473
      return 0
474
    return -1
475

    
476
  def IsBasedOnDiskType(self, dev_type):
477
    """Check if the disk or its children are based on the given type.
478

479
    @type dev_type: L{constants.LDS_BLOCK}
480
    @param dev_type: the type to look for
481
    @rtype: boolean
482
    @return: boolean indicating if a device of the given type was found or not
483

484
    """
485
    if self.children:
486
      for child in self.children:
487
        if child.IsBasedOnDiskType(dev_type):
488
          return True
489
    return self.dev_type == dev_type
490

    
491
  def GetNodes(self, node):
492
    """This function returns the nodes this device lives on.
493

494
    Given the node on which the parent of the device lives on (or, in
495
    case of a top-level device, the primary node of the devices'
496
    instance), this function will return a list of nodes on which this
497
    devices needs to (or can) be assembled.
498

499
    """
500
    if self.dev_type in [constants.LD_LV, constants.LD_FILE]:
501
      result = [node]
502
    elif self.dev_type in constants.LDS_DRBD:
503
      result = [self.logical_id[0], self.logical_id[1]]
504
      if node not in result:
505
        raise errors.ConfigurationError("DRBD device passed unknown node")
506
    else:
507
      raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
508
    return result
509

    
510
  def ComputeNodeTree(self, parent_node):
511
    """Compute the node/disk tree for this disk and its children.
512

513
    This method, given the node on which the parent disk lives, will
514
    return the list of all (node, disk) pairs which describe the disk
515
    tree in the most compact way. For example, a drbd/lvm stack
516
    will be returned as (primary_node, drbd) and (secondary_node, drbd)
517
    which represents all the top-level devices on the nodes.
518

519
    """
520
    my_nodes = self.GetNodes(parent_node)
521
    result = [(node, self) for node in my_nodes]
522
    if not self.children:
523
      # leaf device
524
      return result
525
    for node in my_nodes:
526
      for child in self.children:
527
        child_result = child.ComputeNodeTree(node)
528
        if len(child_result) == 1:
529
          # child (and all its descendants) is simple, doesn't split
530
          # over multiple hosts, so we don't need to describe it, our
531
          # own entry for this node describes it completely
532
          continue
533
        else:
534
          # check if child nodes differ from my nodes; note that
535
          # subdisk can differ from the child itself, and be instead
536
          # one of its descendants
537
          for subnode, subdisk in child_result:
538
            if subnode not in my_nodes:
539
              result.append((subnode, subdisk))
540
            # otherwise child is under our own node, so we ignore this
541
            # entry (but probably the other results in the list will
542
            # be different)
543
    return result
544

    
545
  def RecordGrow(self, amount):
546
    """Update the size of this disk after growth.
547

548
    This method recurses over the disks's children and updates their
549
    size correspondigly. The method needs to be kept in sync with the
550
    actual algorithms from bdev.
551

552
    """
553
    if self.dev_type == constants.LD_LV or self.dev_type == constants.LD_FILE:
554
      self.size += amount
555
    elif self.dev_type == constants.LD_DRBD8:
556
      if self.children:
557
        self.children[0].RecordGrow(amount)
558
      self.size += amount
559
    else:
560
      raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
561
                                   " disk type %s" % self.dev_type)
562

    
563
  def UnsetSize(self):
564
    """Sets recursively the size to zero for the disk and its children.
565

566
    """
567
    if self.children:
568
      for child in self.children:
569
        child.UnsetSize()
570
    self.size = 0
571

    
572
  def SetPhysicalID(self, target_node, nodes_ip):
573
    """Convert the logical ID to the physical ID.
574

575
    This is used only for drbd, which needs ip/port configuration.
576

577
    The routine descends down and updates its children also, because
578
    this helps when the only the top device is passed to the remote
579
    node.
580

581
    Arguments:
582
      - target_node: the node we wish to configure for
583
      - nodes_ip: a mapping of node name to ip
584

585
    The target_node must exist in in nodes_ip, and must be one of the
586
    nodes in the logical ID for each of the DRBD devices encountered
587
    in the disk tree.
588

589
    """
590
    if self.children:
591
      for child in self.children:
592
        child.SetPhysicalID(target_node, nodes_ip)
593

    
594
    if self.logical_id is None and self.physical_id is not None:
595
      return
596
    if self.dev_type in constants.LDS_DRBD:
597
      pnode, snode, port, pminor, sminor, secret = self.logical_id
598
      if target_node not in (pnode, snode):
599
        raise errors.ConfigurationError("DRBD device not knowing node %s" %
600
                                        target_node)
601
      pnode_ip = nodes_ip.get(pnode, None)
602
      snode_ip = nodes_ip.get(snode, None)
603
      if pnode_ip is None or snode_ip is None:
604
        raise errors.ConfigurationError("Can't find primary or secondary node"
605
                                        " for %s" % str(self))
606
      p_data = (pnode_ip, port)
607
      s_data = (snode_ip, port)
608
      if pnode == target_node:
609
        self.physical_id = p_data + s_data + (pminor, secret)
610
      else: # it must be secondary, we tested above
611
        self.physical_id = s_data + p_data + (sminor, secret)
612
    else:
613
      self.physical_id = self.logical_id
614
    return
615

    
616
  def ToDict(self):
617
    """Disk-specific conversion to standard python types.
618

619
    This replaces the children lists of objects with lists of
620
    standard python types.
621

622
    """
623
    bo = super(Disk, self).ToDict()
624

    
625
    for attr in ("children",):
626
      alist = bo.get(attr, None)
627
      if alist:
628
        bo[attr] = self._ContainerToDicts(alist)
629
    return bo
630

    
631
  @classmethod
632
  def FromDict(cls, val):
633
    """Custom function for Disks
634

635
    """
636
    obj = super(Disk, cls).FromDict(val)
637
    if obj.children:
638
      obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
639
    if obj.logical_id and isinstance(obj.logical_id, list):
640
      obj.logical_id = tuple(obj.logical_id)
641
    if obj.physical_id and isinstance(obj.physical_id, list):
642
      obj.physical_id = tuple(obj.physical_id)
643
    if obj.dev_type in constants.LDS_DRBD:
644
      # we need a tuple of length six here
645
      if len(obj.logical_id) < 6:
646
        obj.logical_id += (None,) * (6 - len(obj.logical_id))
647
    return obj
648

    
649
  def __str__(self):
650
    """Custom str() formatter for disks.
651

652
    """
653
    if self.dev_type == constants.LD_LV:
654
      val =  "<LogicalVolume(/dev/%s/%s" % self.logical_id
655
    elif self.dev_type in constants.LDS_DRBD:
656
      node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
657
      val = "<DRBD8("
658
      if self.physical_id is None:
659
        phy = "unconfigured"
660
      else:
661
        phy = ("configured as %s:%s %s:%s" %
662
               (self.physical_id[0], self.physical_id[1],
663
                self.physical_id[2], self.physical_id[3]))
664

    
665
      val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
666
              (node_a, minor_a, node_b, minor_b, port, phy))
667
      if self.children and self.children.count(None) == 0:
668
        val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
669
      else:
670
        val += "no local storage"
671
    else:
672
      val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
673
             (self.dev_type, self.logical_id, self.physical_id, self.children))
674
    if self.iv_name is None:
675
      val += ", not visible"
676
    else:
677
      val += ", visible as /dev/%s" % self.iv_name
678
    if isinstance(self.size, int):
679
      val += ", size=%dm)>" % self.size
680
    else:
681
      val += ", size='%s')>" % (self.size,)
682
    return val
683

    
684
  def Verify(self):
685
    """Checks that this disk is correctly configured.
686

687
    """
688
    all_errors = []
689
    if self.mode not in constants.DISK_ACCESS_SET:
690
      all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
691
    return all_errors
692

    
693
  def UpgradeConfig(self):
694
    """Fill defaults for missing configuration values.
695

696
    """
697
    if self.children:
698
      for child in self.children:
699
        child.UpgradeConfig()
700
    # add here config upgrade for this disk
701

    
702

    
703
class Instance(TaggableObject):
704
  """Config object representing an instance."""
705
  __slots__ = [
706
    "name",
707
    "primary_node",
708
    "os",
709
    "hypervisor",
710
    "hvparams",
711
    "beparams",
712
    "osparams",
713
    "admin_up",
714
    "nics",
715
    "disks",
716
    "disk_template",
717
    "network_port",
718
    "serial_no",
719
    ] + _TIMESTAMPS + _UUID
720

    
721
  def _ComputeSecondaryNodes(self):
722
    """Compute the list of secondary nodes.
723

724
    This is a simple wrapper over _ComputeAllNodes.
725

726
    """
727
    all_nodes = set(self._ComputeAllNodes())
728
    all_nodes.discard(self.primary_node)
729
    return tuple(all_nodes)
730

    
731
  secondary_nodes = property(_ComputeSecondaryNodes, None, None,
732
                             "List of secondary nodes")
733

    
734
  def _ComputeAllNodes(self):
735
    """Compute the list of all nodes.
736

737
    Since the data is already there (in the drbd disks), keeping it as
738
    a separate normal attribute is redundant and if not properly
739
    synchronised can cause problems. Thus it's better to compute it
740
    dynamically.
741

742
    """
743
    def _Helper(nodes, device):
744
      """Recursively computes nodes given a top device."""
745
      if device.dev_type in constants.LDS_DRBD:
746
        nodea, nodeb = device.logical_id[:2]
747
        nodes.add(nodea)
748
        nodes.add(nodeb)
749
      if device.children:
750
        for child in device.children:
751
          _Helper(nodes, child)
752

    
753
    all_nodes = set()
754
    all_nodes.add(self.primary_node)
755
    for device in self.disks:
756
      _Helper(all_nodes, device)
757
    return tuple(all_nodes)
758

    
759
  all_nodes = property(_ComputeAllNodes, None, None,
760
                       "List of all nodes of the instance")
761

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

765
    This function figures out what logical volumes should belong on
766
    which nodes, recursing through a device tree.
767

768
    @param lvmap: optional dictionary to receive the
769
        'node' : ['lv', ...] data.
770

771
    @return: None if lvmap arg is given, otherwise, a dictionary of
772
        the form { 'nodename' : ['volume1', 'volume2', ...], ... };
773
        volumeN is of the form "vg_name/lv_name", compatible with
774
        GetVolumeList()
775

776
    """
777
    if node == None:
778
      node = self.primary_node
779

    
780
    if lvmap is None:
781
      lvmap = { node : [] }
782
      ret = lvmap
783
    else:
784
      if not node in lvmap:
785
        lvmap[node] = []
786
      ret = None
787

    
788
    if not devs:
789
      devs = self.disks
790

    
791
    for dev in devs:
792
      if dev.dev_type == constants.LD_LV:
793
        lvmap[node].append(dev.logical_id[0]+"/"+dev.logical_id[1])
794

    
795
      elif dev.dev_type in constants.LDS_DRBD:
796
        if dev.children:
797
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
798
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
799

    
800
      elif dev.children:
801
        self.MapLVsByNode(lvmap, dev.children, node)
802

    
803
    return ret
804

    
805
  def FindDisk(self, idx):
806
    """Find a disk given having a specified index.
807

808
    This is just a wrapper that does validation of the index.
809

810
    @type idx: int
811
    @param idx: the disk index
812
    @rtype: L{Disk}
813
    @return: the corresponding disk
814
    @raise errors.OpPrereqError: when the given index is not valid
815

816
    """
817
    try:
818
      idx = int(idx)
819
      return self.disks[idx]
820
    except (TypeError, ValueError), err:
821
      raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err),
822
                                 errors.ECODE_INVAL)
823
    except IndexError:
824
      raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
825
                                 " 0 to %d" % (idx, len(self.disks)),
826
                                 errors.ECODE_INVAL)
827

    
828
  def ToDict(self):
829
    """Instance-specific conversion to standard python types.
830

831
    This replaces the children lists of objects with lists of standard
832
    python types.
833

834
    """
835
    bo = super(Instance, self).ToDict()
836

    
837
    for attr in "nics", "disks":
838
      alist = bo.get(attr, None)
839
      if alist:
840
        nlist = self._ContainerToDicts(alist)
841
      else:
842
        nlist = []
843
      bo[attr] = nlist
844
    return bo
845

    
846
  @classmethod
847
  def FromDict(cls, val):
848
    """Custom function for instances.
849

850
    """
851
    obj = super(Instance, cls).FromDict(val)
852
    obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
853
    obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
854
    return obj
855

    
856
  def UpgradeConfig(self):
857
    """Fill defaults for missing configuration values.
858

859
    """
860
    for nic in self.nics:
861
      nic.UpgradeConfig()
862
    for disk in self.disks:
863
      disk.UpgradeConfig()
864
    if self.hvparams:
865
      for key in constants.HVC_GLOBALS:
866
        try:
867
          del self.hvparams[key]
868
        except KeyError:
869
          pass
870
    if self.osparams is None:
871
      self.osparams = {}
872

    
873

    
874
class OS(ConfigObject):
875
  """Config object representing an operating system.
876

877
  @type supported_parameters: list
878
  @ivar supported_parameters: a list of tuples, name and description,
879
      containing the supported parameters by this OS
880

881
  @type VARIANT_DELIM: string
882
  @cvar VARIANT_DELIM: the variant delimiter
883

884
  """
885
  __slots__ = [
886
    "name",
887
    "path",
888
    "api_versions",
889
    "create_script",
890
    "export_script",
891
    "import_script",
892
    "rename_script",
893
    "verify_script",
894
    "supported_variants",
895
    "supported_parameters",
896
    ]
897

    
898
  VARIANT_DELIM = "+"
899

    
900
  @classmethod
901
  def SplitNameVariant(cls, name):
902
    """Splits the name into the proper name and variant.
903

904
    @param name: the OS (unprocessed) name
905
    @rtype: list
906
    @return: a list of two elements; if the original name didn't
907
        contain a variant, it's returned as an empty string
908

909
    """
910
    nv = name.split(cls.VARIANT_DELIM, 1)
911
    if len(nv) == 1:
912
      nv.append("")
913
    return nv
914

    
915
  @classmethod
916
  def GetName(cls, name):
917
    """Returns the proper name of the os (without the variant).
918

919
    @param name: the OS (unprocessed) name
920

921
    """
922
    return cls.SplitNameVariant(name)[0]
923

    
924
  @classmethod
925
  def GetVariant(cls, name):
926
    """Returns the variant the os (without the base name).
927

928
    @param name: the OS (unprocessed) name
929

930
    """
931
    return cls.SplitNameVariant(name)[1]
932

    
933

    
934
class Node(TaggableObject):
935
  """Config object representing a node."""
936
  __slots__ = [
937
    "name",
938
    "primary_ip",
939
    "secondary_ip",
940
    "serial_no",
941
    "master_candidate",
942
    "offline",
943
    "drained",
944
    "group",
945
    "master_capable",
946
    "vm_capable",
947
    "ndparams",
948
    ] + _TIMESTAMPS + _UUID
949

    
950
  def UpgradeConfig(self):
951
    """Fill defaults for missing configuration values.
952

953
    """
954
    # pylint: disable-msg=E0203
955
    # because these are "defined" via slots, not manually
956
    if self.master_capable is None:
957
      self.master_capable = True
958

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

    
962
    if self.ndparams is None:
963
      self.ndparams = {}
964

    
965

    
966
class NodeGroup(ConfigObject):
967
  """Config object representing a node group."""
968
  __slots__ = [
969
    "name",
970
    "members",
971
    "ndparams",
972
    ] + _TIMESTAMPS + _UUID
973

    
974
  def ToDict(self):
975
    """Custom function for nodegroup.
976

977
    This discards the members object, which gets recalculated and is only kept
978
    in memory.
979

980
    """
981
    mydict = super(NodeGroup, self).ToDict()
982
    del mydict["members"]
983
    return mydict
984

    
985
  @classmethod
986
  def FromDict(cls, val):
987
    """Custom function for nodegroup.
988

989
    The members slot is initialized to an empty list, upon deserialization.
990

991
    """
992
    obj = super(NodeGroup, cls).FromDict(val)
993
    obj.members = []
994
    return obj
995

    
996
  def UpgradeConfig(self):
997
    """Fill defaults for missing configuration values.
998

999
    """
1000
    if self.ndparams is None:
1001
      self.ndparams = {}
1002

    
1003
  def FillND(self, node):
1004
    """Return filled out ndparams for L{object.Node}
1005

1006
    @type node: L{objects.Node}
1007
    @param node: A Node object to fill
1008
    @return a copy of the node's ndparams with defaults filled
1009

1010
    """
1011
    return self.SimpleFillND(node.ndparams)
1012

    
1013
  def SimpleFillND(self, ndparams):
1014
    """Fill a given ndparams dict with defaults.
1015

1016
    @type ndparams: dict
1017
    @param ndparams: the dict to fill
1018
    @rtype: dict
1019
    @return: a copy of the passed in ndparams with missing keys filled
1020
        from the cluster defaults
1021

1022
    """
1023
    return FillDict(self.ndparams, ndparams)
1024

    
1025

    
1026
class Cluster(TaggableObject):
1027
  """Config object representing the cluster."""
1028
  __slots__ = [
1029
    "serial_no",
1030
    "rsahostkeypub",
1031
    "highest_used_port",
1032
    "tcpudp_port_pool",
1033
    "mac_prefix",
1034
    "volume_group_name",
1035
    "reserved_lvs",
1036
    "drbd_usermode_helper",
1037
    "default_bridge",
1038
    "default_hypervisor",
1039
    "master_node",
1040
    "master_ip",
1041
    "master_netdev",
1042
    "cluster_name",
1043
    "file_storage_dir",
1044
    "enabled_hypervisors",
1045
    "hvparams",
1046
    "os_hvp",
1047
    "beparams",
1048
    "osparams",
1049
    "nicparams",
1050
    "ndparams",
1051
    "candidate_pool_size",
1052
    "modify_etc_hosts",
1053
    "modify_ssh_setup",
1054
    "maintain_node_health",
1055
    "uid_pool",
1056
    "default_iallocator",
1057
    "hidden_os",
1058
    "blacklisted_os",
1059
    "primary_ip_family",
1060
    "prealloc_wipe_disks",
1061
    ] + _TIMESTAMPS + _UUID
1062

    
1063
  def UpgradeConfig(self):
1064
    """Fill defaults for missing configuration values.
1065

1066
    """
1067
    # pylint: disable-msg=E0203
1068
    # because these are "defined" via slots, not manually
1069
    if self.hvparams is None:
1070
      self.hvparams = constants.HVC_DEFAULTS
1071
    else:
1072
      for hypervisor in self.hvparams:
1073
        self.hvparams[hypervisor] = FillDict(
1074
            constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
1075

    
1076
    if self.os_hvp is None:
1077
      self.os_hvp = {}
1078

    
1079
    # osparams added before 2.2
1080
    if self.osparams is None:
1081
      self.osparams = {}
1082

    
1083
    if self.ndparams is None:
1084
      self.ndparams = constants.NDC_DEFAULTS
1085

    
1086
    self.beparams = UpgradeGroupedParams(self.beparams,
1087
                                         constants.BEC_DEFAULTS)
1088
    migrate_default_bridge = not self.nicparams
1089
    self.nicparams = UpgradeGroupedParams(self.nicparams,
1090
                                          constants.NICC_DEFAULTS)
1091
    if migrate_default_bridge:
1092
      self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
1093
        self.default_bridge
1094

    
1095
    if self.modify_etc_hosts is None:
1096
      self.modify_etc_hosts = True
1097

    
1098
    if self.modify_ssh_setup is None:
1099
      self.modify_ssh_setup = True
1100

    
1101
    # default_bridge is no longer used it 2.1. The slot is left there to
1102
    # support auto-upgrading. It can be removed once we decide to deprecate
1103
    # upgrading straight from 2.0.
1104
    if self.default_bridge is not None:
1105
      self.default_bridge = None
1106

    
1107
    # default_hypervisor is just the first enabled one in 2.1. This slot and
1108
    # code can be removed once upgrading straight from 2.0 is deprecated.
1109
    if self.default_hypervisor is not None:
1110
      self.enabled_hypervisors = ([self.default_hypervisor] +
1111
        [hvname for hvname in self.enabled_hypervisors
1112
         if hvname != self.default_hypervisor])
1113
      self.default_hypervisor = None
1114

    
1115
    # maintain_node_health added after 2.1.1
1116
    if self.maintain_node_health is None:
1117
      self.maintain_node_health = False
1118

    
1119
    if self.uid_pool is None:
1120
      self.uid_pool = []
1121

    
1122
    if self.default_iallocator is None:
1123
      self.default_iallocator = ""
1124

    
1125
    # reserved_lvs added before 2.2
1126
    if self.reserved_lvs is None:
1127
      self.reserved_lvs = []
1128

    
1129
    # hidden and blacklisted operating systems added before 2.2.1
1130
    if self.hidden_os is None:
1131
      self.hidden_os = []
1132

    
1133
    if self.blacklisted_os is None:
1134
      self.blacklisted_os = []
1135

    
1136
    # primary_ip_family added before 2.3
1137
    if self.primary_ip_family is None:
1138
      self.primary_ip_family = AF_INET
1139

    
1140
    if self.prealloc_wipe_disks is None:
1141
      self.prealloc_wipe_disks = False
1142

    
1143
  def ToDict(self):
1144
    """Custom function for cluster.
1145

1146
    """
1147
    mydict = super(Cluster, self).ToDict()
1148
    mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
1149
    return mydict
1150

    
1151
  @classmethod
1152
  def FromDict(cls, val):
1153
    """Custom function for cluster.
1154

1155
    """
1156
    obj = super(Cluster, cls).FromDict(val)
1157
    if not isinstance(obj.tcpudp_port_pool, set):
1158
      obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
1159
    return obj
1160

    
1161
  def GetHVDefaults(self, hypervisor, os_name=None, skip_keys=None):
1162
    """Get the default hypervisor parameters for the cluster.
1163

1164
    @param hypervisor: the hypervisor name
1165
    @param os_name: if specified, we'll also update the defaults for this OS
1166
    @param skip_keys: if passed, list of keys not to use
1167
    @return: the defaults dict
1168

1169
    """
1170
    if skip_keys is None:
1171
      skip_keys = []
1172

    
1173
    fill_stack = [self.hvparams.get(hypervisor, {})]
1174
    if os_name is not None:
1175
      os_hvp = self.os_hvp.get(os_name, {}).get(hypervisor, {})
1176
      fill_stack.append(os_hvp)
1177

    
1178
    ret_dict = {}
1179
    for o_dict in fill_stack:
1180
      ret_dict = FillDict(ret_dict, o_dict, skip_keys=skip_keys)
1181

    
1182
    return ret_dict
1183

    
1184
  def SimpleFillHV(self, hv_name, os_name, hvparams, skip_globals=False):
1185
    """Fill a given hvparams dict with cluster defaults.
1186

1187
    @type hv_name: string
1188
    @param hv_name: the hypervisor to use
1189
    @type os_name: string
1190
    @param os_name: the OS to use for overriding the hypervisor defaults
1191
    @type skip_globals: boolean
1192
    @param skip_globals: if True, the global hypervisor parameters will
1193
        not be filled
1194
    @rtype: dict
1195
    @return: a copy of the given hvparams with missing keys filled from
1196
        the cluster defaults
1197

1198
    """
1199
    if skip_globals:
1200
      skip_keys = constants.HVC_GLOBALS
1201
    else:
1202
      skip_keys = []
1203

    
1204
    def_dict = self.GetHVDefaults(hv_name, os_name, skip_keys=skip_keys)
1205
    return FillDict(def_dict, hvparams, skip_keys=skip_keys)
1206

    
1207
  def FillHV(self, instance, skip_globals=False):
1208
    """Fill an instance's hvparams dict with cluster defaults.
1209

1210
    @type instance: L{objects.Instance}
1211
    @param instance: the instance parameter to fill
1212
    @type skip_globals: boolean
1213
    @param skip_globals: if True, the global hypervisor parameters will
1214
        not be filled
1215
    @rtype: dict
1216
    @return: a copy of the instance's hvparams with missing keys filled from
1217
        the cluster defaults
1218

1219
    """
1220
    return self.SimpleFillHV(instance.hypervisor, instance.os,
1221
                             instance.hvparams, skip_globals)
1222

    
1223
  def SimpleFillBE(self, beparams):
1224
    """Fill a given beparams dict with cluster defaults.
1225

1226
    @type beparams: dict
1227
    @param beparams: the dict to fill
1228
    @rtype: dict
1229
    @return: a copy of the passed in beparams with missing keys filled
1230
        from the cluster defaults
1231

1232
    """
1233
    return FillDict(self.beparams.get(constants.PP_DEFAULT, {}), beparams)
1234

    
1235
  def FillBE(self, instance):
1236
    """Fill an instance's beparams dict with cluster defaults.
1237

1238
    @type instance: L{objects.Instance}
1239
    @param instance: the instance parameter to fill
1240
    @rtype: dict
1241
    @return: a copy of the instance's beparams with missing keys filled from
1242
        the cluster defaults
1243

1244
    """
1245
    return self.SimpleFillBE(instance.beparams)
1246

    
1247
  def SimpleFillNIC(self, nicparams):
1248
    """Fill a given nicparams dict with cluster defaults.
1249

1250
    @type nicparams: dict
1251
    @param nicparams: the dict to fill
1252
    @rtype: dict
1253
    @return: a copy of the passed in nicparams with missing keys filled
1254
        from the cluster defaults
1255

1256
    """
1257
    return FillDict(self.nicparams.get(constants.PP_DEFAULT, {}), nicparams)
1258

    
1259
  def SimpleFillOS(self, os_name, os_params):
1260
    """Fill an instance's osparams dict with cluster defaults.
1261

1262
    @type os_name: string
1263
    @param os_name: the OS name to use
1264
    @type os_params: dict
1265
    @param os_params: the dict to fill with default values
1266
    @rtype: dict
1267
    @return: a copy of the instance's osparams with missing keys filled from
1268
        the cluster defaults
1269

1270
    """
1271
    name_only = os_name.split("+", 1)[0]
1272
    # base OS
1273
    result = self.osparams.get(name_only, {})
1274
    # OS with variant
1275
    result = FillDict(result, self.osparams.get(os_name, {}))
1276
    # specified params
1277
    return FillDict(result, os_params)
1278

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

1282
    @type node: L{objects.Node}
1283
    @param node: A Node object to fill
1284
    @type nodegroup: L{objects.NodeGroup}
1285
    @param nodegroup: A Node object to fill
1286
    @return a copy of the node's ndparams with defaults filled
1287

1288
    """
1289
    return self.SimpleFillND(nodegroup.FillND(node))
1290

    
1291
  def SimpleFillND(self, ndparams):
1292
    """Fill a given ndparams dict with defaults.
1293

1294
    @type ndparams: dict
1295
    @param ndparams: the dict to fill
1296
    @rtype: dict
1297
    @return: a copy of the passed in ndparams with missing keys filled
1298
        from the cluster defaults
1299

1300
    """
1301
    return FillDict(self.ndparams, ndparams)
1302

    
1303

    
1304
class BlockDevStatus(ConfigObject):
1305
  """Config object representing the status of a block device."""
1306
  __slots__ = [
1307
    "dev_path",
1308
    "major",
1309
    "minor",
1310
    "sync_percent",
1311
    "estimated_time",
1312
    "is_degraded",
1313
    "ldisk_status",
1314
    ]
1315

    
1316

    
1317
class ImportExportStatus(ConfigObject):
1318
  """Config object representing the status of an import or export."""
1319
  __slots__ = [
1320
    "recent_output",
1321
    "listen_port",
1322
    "connected",
1323
    "progress_mbytes",
1324
    "progress_throughput",
1325
    "progress_eta",
1326
    "progress_percent",
1327
    "exit_status",
1328
    "error_message",
1329
    ] + _TIMESTAMPS
1330

    
1331

    
1332
class ImportExportOptions(ConfigObject):
1333
  """Options for import/export daemon
1334

1335
  @ivar key_name: X509 key name (None for cluster certificate)
1336
  @ivar ca_pem: Remote peer CA in PEM format (None for cluster certificate)
1337
  @ivar compress: Compression method (one of L{constants.IEC_ALL})
1338
  @ivar magic: Used to ensure the connection goes to the right disk
1339
  @ivar ipv6: Whether to use IPv6
1340

1341
  """
1342
  __slots__ = [
1343
    "key_name",
1344
    "ca_pem",
1345
    "compress",
1346
    "magic",
1347
    "ipv6",
1348
    ]
1349

    
1350

    
1351
class ConfdRequest(ConfigObject):
1352
  """Object holding a confd request.
1353

1354
  @ivar protocol: confd protocol version
1355
  @ivar type: confd query type
1356
  @ivar query: query request
1357
  @ivar rsalt: requested reply salt
1358

1359
  """
1360
  __slots__ = [
1361
    "protocol",
1362
    "type",
1363
    "query",
1364
    "rsalt",
1365
    ]
1366

    
1367

    
1368
class ConfdReply(ConfigObject):
1369
  """Object holding a confd reply.
1370

1371
  @ivar protocol: confd protocol version
1372
  @ivar status: reply status code (ok, error)
1373
  @ivar answer: confd query reply
1374
  @ivar serial: configuration serial number
1375

1376
  """
1377
  __slots__ = [
1378
    "protocol",
1379
    "status",
1380
    "answer",
1381
    "serial",
1382
    ]
1383

    
1384

    
1385
class QueryFieldDefinition(ConfigObject):
1386
  """Object holding a query field definition.
1387

1388
  @ivar name: Field name as a regular expression
1389
  @ivar title: Human-readable title
1390
  @ivar kind: Field type
1391

1392
  """
1393
  __slots__ = [
1394
    "name",
1395
    "title",
1396
    "kind",
1397
    ]
1398

    
1399

    
1400
class SerializableConfigParser(ConfigParser.SafeConfigParser):
1401
  """Simple wrapper over ConfigParse that allows serialization.
1402

1403
  This class is basically ConfigParser.SafeConfigParser with two
1404
  additional methods that allow it to serialize/unserialize to/from a
1405
  buffer.
1406

1407
  """
1408
  def Dumps(self):
1409
    """Dump this instance and return the string representation."""
1410
    buf = StringIO()
1411
    self.write(buf)
1412
    return buf.getvalue()
1413

    
1414
  @classmethod
1415
  def Loads(cls, data):
1416
    """Load data from a string."""
1417
    buf = StringIO(data)
1418
    cfp = cls()
1419
    cfp.readfp(buf)
1420
    return cfp