Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ 2435f63b

History | View | Annotate | Download (58.1 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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=E0203,W0201,R0902
30

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

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

    
36
# R0902: Allow instances of these objects to have more than 20 attributes
37

    
38
import ConfigParser
39
import re
40
import copy
41
import time
42
from cStringIO import StringIO
43

    
44
from ganeti import errors
45
from ganeti import constants
46
from ganeti import netutils
47
from ganeti import utils
48

    
49
from socket import AF_INET
50

    
51

    
52
__all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
53
           "OS", "Node", "NodeGroup", "Cluster", "FillDict"]
54

    
55
_TIMESTAMPS = ["ctime", "mtime"]
56
_UUID = ["uuid"]
57

    
58

    
59
def FillDict(defaults_dict, custom_dict, skip_keys=None):
60
  """Basic function to apply settings on top a default dict.
61

62
  @type defaults_dict: dict
63
  @param defaults_dict: dictionary holding the default values
64
  @type custom_dict: dict
65
  @param custom_dict: dictionary holding customized value
66
  @type skip_keys: list
67
  @param skip_keys: which keys not to fill
68
  @rtype: dict
69
  @return: dict with the 'full' values
70

71
  """
72
  ret_dict = copy.deepcopy(defaults_dict)
73
  ret_dict.update(custom_dict)
74
  if skip_keys:
75
    for k in skip_keys:
76
      try:
77
        del ret_dict[k]
78
      except KeyError:
79
        pass
80
  return ret_dict
81

    
82

    
83
def FillIPolicy(default_ipolicy, custom_ipolicy, skip_keys=None):
84
  """Fills an instance policy with defaults.
85

86
  """
87
  assert frozenset(default_ipolicy.keys()) == constants.IPOLICY_ALL_KEYS
88
  ret_dict = {}
89
  for key in constants.IPOLICY_ISPECS:
90
    ret_dict[key] = FillDict(default_ipolicy[key],
91
                             custom_ipolicy.get(key, {}),
92
                             skip_keys=skip_keys)
93
  # list items
94
  for key in [constants.IPOLICY_DTS]:
95
    ret_dict[key] = list(custom_ipolicy.get(key, default_ipolicy[key]))
96
  # other items which we know we can directly copy (immutables)
97
  for key in constants.IPOLICY_PARAMETERS:
98
    ret_dict[key] = custom_ipolicy.get(key, default_ipolicy[key])
99

    
100
  return ret_dict
101

    
102

    
103
def FillDiskParams(default_dparams, custom_dparams, skip_keys=None):
104
  """Fills the disk parameter defaults.
105

106
  @see: L{FillDict} for parameters and return value
107

108
  """
109
  assert frozenset(default_dparams.keys()) == constants.DISK_TEMPLATES
110

    
111
  return dict((dt, FillDict(default_dparams[dt], custom_dparams.get(dt, {}),
112
                             skip_keys=skip_keys))
113
              for dt in constants.DISK_TEMPLATES)
114

    
115

    
116
def UpgradeGroupedParams(target, defaults):
117
  """Update all groups for the target parameter.
118

119
  @type target: dict of dicts
120
  @param target: {group: {parameter: value}}
121
  @type defaults: dict
122
  @param defaults: default parameter values
123

124
  """
125
  if target is None:
126
    target = {constants.PP_DEFAULT: defaults}
127
  else:
128
    for group in target:
129
      target[group] = FillDict(defaults, target[group])
130
  return target
131

    
132

    
133
def UpgradeBeParams(target):
134
  """Update the be parameters dict to the new format.
135

136
  @type target: dict
137
  @param target: "be" parameters dict
138

139
  """
140
  if constants.BE_MEMORY in target:
141
    memory = target[constants.BE_MEMORY]
142
    target[constants.BE_MAXMEM] = memory
143
    target[constants.BE_MINMEM] = memory
144
    del target[constants.BE_MEMORY]
145

    
146

    
147
def UpgradeDiskParams(diskparams):
148
  """Upgrade the disk parameters.
149

150
  @type diskparams: dict
151
  @param diskparams: disk parameters to upgrade
152
  @rtype: dict
153
  @return: the upgraded disk parameters dict
154

155
  """
156
  if not diskparams:
157
    result = {}
158
  else:
159
    result = FillDiskParams(constants.DISK_DT_DEFAULTS, diskparams)
160

    
161
  return result
162

    
163

    
164
def UpgradeNDParams(ndparams):
165
  """Upgrade ndparams structure.
166

167
  @type ndparams: dict
168
  @param ndparams: disk parameters to upgrade
169
  @rtype: dict
170
  @return: the upgraded node parameters dict
171

172
  """
173
  if ndparams is None:
174
    ndparams = {}
175

    
176
  return FillDict(constants.NDC_DEFAULTS, ndparams)
177

    
178

    
179
def MakeEmptyIPolicy():
180
  """Create empty IPolicy dictionary.
181

182
  """
183
  return dict([
184
    (constants.ISPECS_MIN, {}),
185
    (constants.ISPECS_MAX, {}),
186
    (constants.ISPECS_STD, {}),
187
    ])
188

    
189

    
190
class ConfigObject(object):
191
  """A generic config object.
192

193
  It has the following properties:
194

195
    - provides somewhat safe recursive unpickling and pickling for its classes
196
    - unset attributes which are defined in slots are always returned
197
      as None instead of raising an error
198

199
  Classes derived from this must always declare __slots__ (we use many
200
  config objects and the memory reduction is useful)
201

202
  """
203
  __slots__ = []
204

    
205
  def __init__(self, **kwargs):
206
    for k, v in kwargs.iteritems():
207
      setattr(self, k, v)
208

    
209
  def __getattr__(self, name):
210
    if name not in self._all_slots():
211
      raise AttributeError("Invalid object attribute %s.%s" %
212
                           (type(self).__name__, name))
213
    return None
214

    
215
  def __setstate__(self, state):
216
    slots = self._all_slots()
217
    for name in state:
218
      if name in slots:
219
        setattr(self, name, state[name])
220

    
221
  @classmethod
222
  def _all_slots(cls):
223
    """Compute the list of all declared slots for a class.
224

225
    """
226
    slots = []
227
    for parent in cls.__mro__:
228
      slots.extend(getattr(parent, "__slots__", []))
229
    return slots
230

    
231
  #: Public getter for the defined slots
232
  GetAllSlots = _all_slots
233

    
234
  def ToDict(self):
235
    """Convert to a dict holding only standard python types.
236

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

243
    """
244
    result = {}
245
    for name in self._all_slots():
246
      value = getattr(self, name, None)
247
      if value is not None:
248
        result[name] = value
249
    return result
250

    
251
  __getstate__ = ToDict
252

    
253
  @classmethod
254
  def FromDict(cls, val):
255
    """Create an object from a dictionary.
256

257
    This generic routine takes a dict, instantiates a new instance of
258
    the given class, and sets attributes based on the dict content.
259

260
    As for `ToDict`, this does not work if the class has children
261
    who are ConfigObjects themselves (e.g. the nics list in an
262
    Instance), in which case the object should subclass the function
263
    and alter the objects.
264

265
    """
266
    if not isinstance(val, dict):
267
      raise errors.ConfigurationError("Invalid object passed to FromDict:"
268
                                      " expected dict, got %s" % type(val))
269
    val_str = dict([(str(k), v) for k, v in val.iteritems()])
270
    obj = cls(**val_str) # pylint: disable=W0142
271
    return obj
272

    
273
  @staticmethod
274
  def _ContainerToDicts(container):
275
    """Convert the elements of a container to standard python types.
276

277
    This method converts a container with elements derived from
278
    ConfigData to standard python types. If the container is a dict,
279
    we don't touch the keys, only the values.
280

281
    """
282
    if isinstance(container, dict):
283
      ret = dict([(k, v.ToDict()) for k, v in container.iteritems()])
284
    elif isinstance(container, (list, tuple, set, frozenset)):
285
      ret = [elem.ToDict() for elem in container]
286
    else:
287
      raise TypeError("Invalid type %s passed to _ContainerToDicts" %
288
                      type(container))
289
    return ret
290

    
291
  @staticmethod
292
  def _ContainerFromDicts(source, c_type, e_type):
293
    """Convert a container from standard python types.
294

295
    This method converts a container with standard python types to
296
    ConfigData objects. If the container is a dict, we don't touch the
297
    keys, only the values.
298

299
    """
300
    if not isinstance(c_type, type):
301
      raise TypeError("Container type %s passed to _ContainerFromDicts is"
302
                      " not a type" % type(c_type))
303
    if source is None:
304
      source = c_type()
305
    if c_type is dict:
306
      ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
307
    elif c_type in (list, tuple, set, frozenset):
308
      ret = c_type([e_type.FromDict(elem) for elem in source])
309
    else:
310
      raise TypeError("Invalid container type %s passed to"
311
                      " _ContainerFromDicts" % c_type)
312
    return ret
313

    
314
  def Copy(self):
315
    """Makes a deep copy of the current object and its children.
316

317
    """
318
    dict_form = self.ToDict()
319
    clone_obj = self.__class__.FromDict(dict_form)
320
    return clone_obj
321

    
322
  def __repr__(self):
323
    """Implement __repr__ for ConfigObjects."""
324
    return repr(self.ToDict())
325

    
326
  def UpgradeConfig(self):
327
    """Fill defaults for missing configuration values.
328

329
    This method will be called at configuration load time, and its
330
    implementation will be object dependent.
331

332
    """
333
    pass
334

    
335

    
336
class TaggableObject(ConfigObject):
337
  """An generic class supporting tags.
338

339
  """
340
  __slots__ = ["tags"]
341
  VALID_TAG_RE = re.compile("^[\w.+*/:@-]+$")
342

    
343
  @classmethod
344
  def ValidateTag(cls, tag):
345
    """Check if a tag is valid.
346

347
    If the tag is invalid, an errors.TagError will be raised. The
348
    function has no return value.
349

350
    """
351
    if not isinstance(tag, basestring):
352
      raise errors.TagError("Invalid tag type (not a string)")
353
    if len(tag) > constants.MAX_TAG_LEN:
354
      raise errors.TagError("Tag too long (>%d characters)" %
355
                            constants.MAX_TAG_LEN)
356
    if not tag:
357
      raise errors.TagError("Tags cannot be empty")
358
    if not cls.VALID_TAG_RE.match(tag):
359
      raise errors.TagError("Tag contains invalid characters")
360

    
361
  def GetTags(self):
362
    """Return the tags list.
363

364
    """
365
    tags = getattr(self, "tags", None)
366
    if tags is None:
367
      tags = self.tags = set()
368
    return tags
369

    
370
  def AddTag(self, tag):
371
    """Add a new tag.
372

373
    """
374
    self.ValidateTag(tag)
375
    tags = self.GetTags()
376
    if len(tags) >= constants.MAX_TAGS_PER_OBJ:
377
      raise errors.TagError("Too many tags")
378
    self.GetTags().add(tag)
379

    
380
  def RemoveTag(self, tag):
381
    """Remove a tag.
382

383
    """
384
    self.ValidateTag(tag)
385
    tags = self.GetTags()
386
    try:
387
      tags.remove(tag)
388
    except KeyError:
389
      raise errors.TagError("Tag not found")
390

    
391
  def ToDict(self):
392
    """Taggable-object-specific conversion to standard python types.
393

394
    This replaces the tags set with a list.
395

396
    """
397
    bo = super(TaggableObject, self).ToDict()
398

    
399
    tags = bo.get("tags", None)
400
    if isinstance(tags, set):
401
      bo["tags"] = list(tags)
402
    return bo
403

    
404
  @classmethod
405
  def FromDict(cls, val):
406
    """Custom function for instances.
407

408
    """
409
    obj = super(TaggableObject, cls).FromDict(val)
410
    if hasattr(obj, "tags") and isinstance(obj.tags, list):
411
      obj.tags = set(obj.tags)
412
    return obj
413

    
414

    
415
class MasterNetworkParameters(ConfigObject):
416
  """Network configuration parameters for the master
417

418
  @ivar name: master name
419
  @ivar ip: master IP
420
  @ivar netmask: master netmask
421
  @ivar netdev: master network device
422
  @ivar ip_family: master IP family
423

424
  """
425
  __slots__ = [
426
    "name",
427
    "ip",
428
    "netmask",
429
    "netdev",
430
    "ip_family"
431
    ]
432

    
433

    
434
class ConfigData(ConfigObject):
435
  """Top-level config object."""
436
  __slots__ = [
437
    "version",
438
    "cluster",
439
    "nodes",
440
    "nodegroups",
441
    "instances",
442
    "serial_no",
443
    ] + _TIMESTAMPS
444

    
445
  def ToDict(self):
446
    """Custom function for top-level config data.
447

448
    This just replaces the list of instances, nodes and the cluster
449
    with standard python types.
450

451
    """
452
    mydict = super(ConfigData, self).ToDict()
453
    mydict["cluster"] = mydict["cluster"].ToDict()
454
    for key in "nodes", "instances", "nodegroups":
455
      mydict[key] = self._ContainerToDicts(mydict[key])
456

    
457
    return mydict
458

    
459
  @classmethod
460
  def FromDict(cls, val):
461
    """Custom function for top-level config data
462

463
    """
464
    obj = super(ConfigData, cls).FromDict(val)
465
    obj.cluster = Cluster.FromDict(obj.cluster)
466
    obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
467
    obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
468
    obj.nodegroups = cls._ContainerFromDicts(obj.nodegroups, dict, NodeGroup)
469
    return obj
470

    
471
  def HasAnyDiskOfType(self, dev_type):
472
    """Check if in there is at disk of the given type in the configuration.
473

474
    @type dev_type: L{constants.LDS_BLOCK}
475
    @param dev_type: the type to look for
476
    @rtype: boolean
477
    @return: boolean indicating if a disk of the given type was found or not
478

479
    """
480
    for instance in self.instances.values():
481
      for disk in instance.disks:
482
        if disk.IsBasedOnDiskType(dev_type):
483
          return True
484
    return False
485

    
486
  def UpgradeConfig(self):
487
    """Fill defaults for missing configuration values.
488

489
    """
490
    self.cluster.UpgradeConfig()
491
    for node in self.nodes.values():
492
      node.UpgradeConfig()
493
    for instance in self.instances.values():
494
      instance.UpgradeConfig()
495
    if self.nodegroups is None:
496
      self.nodegroups = {}
497
    for nodegroup in self.nodegroups.values():
498
      nodegroup.UpgradeConfig()
499
    if self.cluster.drbd_usermode_helper is None:
500
      # To decide if we set an helper let's check if at least one instance has
501
      # a DRBD disk. This does not cover all the possible scenarios but it
502
      # gives a good approximation.
503
      if self.HasAnyDiskOfType(constants.LD_DRBD8):
504
        self.cluster.drbd_usermode_helper = constants.DEFAULT_DRBD_HELPER
505

    
506
class HotplugInfo(ConfigObject):
507
  __slots__ = ["nics", "disks"]
508

    
509

    
510
class NIC(ConfigObject):
511
  """Config object representing a network card."""
512
  __slots__ = ["idx", "pci", "mac", "ip", "nicparams"]
513

    
514
  @classmethod
515
  def CheckParameterSyntax(cls, nicparams):
516
    """Check the given parameters for validity.
517

518
    @type nicparams:  dict
519
    @param nicparams: dictionary with parameter names/value
520
    @raise errors.ConfigurationError: when a parameter is not valid
521

522
    """
523
    if (nicparams[constants.NIC_MODE] not in constants.NIC_VALID_MODES and
524
        nicparams[constants.NIC_MODE] != constants.VALUE_AUTO):
525
      err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
526
      raise errors.ConfigurationError(err)
527

    
528
    if (nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED and
529
        not nicparams[constants.NIC_LINK]):
530
      err = "Missing bridged nic link"
531
      raise errors.ConfigurationError(err)
532

    
533

    
534
class Disk(ConfigObject):
535
  """Config object representing a block device."""
536
  __slots__ = ["idx", "pci", "dev_type", "logical_id", "physical_id",
537
               "children", "iv_name", "size", "mode", "params"]
538

    
539
  def CreateOnSecondary(self):
540
    """Test if this device needs to be created on a secondary node."""
541
    return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
542

    
543
  def AssembleOnSecondary(self):
544
    """Test if this device needs to be assembled on a secondary node."""
545
    return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
546

    
547
  def OpenOnSecondary(self):
548
    """Test if this device needs to be opened on a secondary node."""
549
    return self.dev_type in (constants.LD_LV,)
550

    
551
  def StaticDevPath(self):
552
    """Return the device path if this device type has a static one.
553

554
    Some devices (LVM for example) live always at the same /dev/ path,
555
    irrespective of their status. For such devices, we return this
556
    path, for others we return None.
557

558
    @warning: The path returned is not a normalized pathname; callers
559
        should check that it is a valid path.
560

561
    """
562
    if self.dev_type == constants.LD_LV:
563
      return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
564
    elif self.dev_type == constants.LD_BLOCKDEV:
565
      return self.logical_id[1]
566
    elif self.dev_type == constants.LD_RBD:
567
      return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
568
    return None
569

    
570
  def ChildrenNeeded(self):
571
    """Compute the needed number of children for activation.
572

573
    This method will return either -1 (all children) or a positive
574
    number denoting the minimum number of children needed for
575
    activation (only mirrored devices will usually return >=0).
576

577
    Currently, only DRBD8 supports diskless activation (therefore we
578
    return 0), for all other we keep the previous semantics and return
579
    -1.
580

581
    """
582
    if self.dev_type == constants.LD_DRBD8:
583
      return 0
584
    return -1
585

    
586
  def IsBasedOnDiskType(self, dev_type):
587
    """Check if the disk or its children are based on the given type.
588

589
    @type dev_type: L{constants.LDS_BLOCK}
590
    @param dev_type: the type to look for
591
    @rtype: boolean
592
    @return: boolean indicating if a device of the given type was found or not
593

594
    """
595
    if self.children:
596
      for child in self.children:
597
        if child.IsBasedOnDiskType(dev_type):
598
          return True
599
    return self.dev_type == dev_type
600

    
601
  def GetNodes(self, node):
602
    """This function returns the nodes this device lives on.
603

604
    Given the node on which the parent of the device lives on (or, in
605
    case of a top-level device, the primary node of the devices'
606
    instance), this function will return a list of nodes on which this
607
    devices needs to (or can) be assembled.
608

609
    """
610
    if self.dev_type in [constants.LD_LV, constants.LD_FILE,
611
                         constants.LD_BLOCKDEV, constants.LD_RBD]:
612
      result = [node]
613
    elif self.dev_type in constants.LDS_DRBD:
614
      result = [self.logical_id[0], self.logical_id[1]]
615
      if node not in result:
616
        raise errors.ConfigurationError("DRBD device passed unknown node")
617
    else:
618
      raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
619
    return result
620

    
621
  def ComputeNodeTree(self, parent_node):
622
    """Compute the node/disk tree for this disk and its children.
623

624
    This method, given the node on which the parent disk lives, will
625
    return the list of all (node, disk) pairs which describe the disk
626
    tree in the most compact way. For example, a drbd/lvm stack
627
    will be returned as (primary_node, drbd) and (secondary_node, drbd)
628
    which represents all the top-level devices on the nodes.
629

630
    """
631
    my_nodes = self.GetNodes(parent_node)
632
    result = [(node, self) for node in my_nodes]
633
    if not self.children:
634
      # leaf device
635
      return result
636
    for node in my_nodes:
637
      for child in self.children:
638
        child_result = child.ComputeNodeTree(node)
639
        if len(child_result) == 1:
640
          # child (and all its descendants) is simple, doesn't split
641
          # over multiple hosts, so we don't need to describe it, our
642
          # own entry for this node describes it completely
643
          continue
644
        else:
645
          # check if child nodes differ from my nodes; note that
646
          # subdisk can differ from the child itself, and be instead
647
          # one of its descendants
648
          for subnode, subdisk in child_result:
649
            if subnode not in my_nodes:
650
              result.append((subnode, subdisk))
651
            # otherwise child is under our own node, so we ignore this
652
            # entry (but probably the other results in the list will
653
            # be different)
654
    return result
655

    
656
  def ComputeGrowth(self, amount):
657
    """Compute the per-VG growth requirements.
658

659
    This only works for VG-based disks.
660

661
    @type amount: integer
662
    @param amount: the desired increase in (user-visible) disk space
663
    @rtype: dict
664
    @return: a dictionary of volume-groups and the required size
665

666
    """
667
    if self.dev_type == constants.LD_LV:
668
      return {self.logical_id[0]: amount}
669
    elif self.dev_type == constants.LD_DRBD8:
670
      if self.children:
671
        return self.children[0].ComputeGrowth(amount)
672
      else:
673
        return {}
674
    else:
675
      # Other disk types do not require VG space
676
      return {}
677

    
678
  def RecordGrow(self, amount):
679
    """Update the size of this disk after growth.
680

681
    This method recurses over the disks's children and updates their
682
    size correspondigly. The method needs to be kept in sync with the
683
    actual algorithms from bdev.
684

685
    """
686
    if self.dev_type in (constants.LD_LV, constants.LD_FILE,
687
                         constants.LD_RBD):
688
      self.size += amount
689
    elif self.dev_type == constants.LD_DRBD8:
690
      if self.children:
691
        self.children[0].RecordGrow(amount)
692
      self.size += amount
693
    else:
694
      raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
695
                                   " disk type %s" % self.dev_type)
696

    
697
  def Update(self, size=None, mode=None):
698
    """Apply changes to size and mode.
699

700
    """
701
    if self.dev_type == constants.LD_DRBD8:
702
      if self.children:
703
        self.children[0].Update(size=size, mode=mode)
704
    else:
705
      assert not self.children
706

    
707
    if size is not None:
708
      self.size = size
709
    if mode is not None:
710
      self.mode = mode
711

    
712
  def UnsetSize(self):
713
    """Sets recursively the size to zero for the disk and its children.
714

715
    """
716
    if self.children:
717
      for child in self.children:
718
        child.UnsetSize()
719
    self.size = 0
720

    
721
  def SetPhysicalID(self, target_node, nodes_ip):
722
    """Convert the logical ID to the physical ID.
723

724
    This is used only for drbd, which needs ip/port configuration.
725

726
    The routine descends down and updates its children also, because
727
    this helps when the only the top device is passed to the remote
728
    node.
729

730
    Arguments:
731
      - target_node: the node we wish to configure for
732
      - nodes_ip: a mapping of node name to ip
733

734
    The target_node must exist in in nodes_ip, and must be one of the
735
    nodes in the logical ID for each of the DRBD devices encountered
736
    in the disk tree.
737

738
    """
739
    if self.children:
740
      for child in self.children:
741
        child.SetPhysicalID(target_node, nodes_ip)
742

    
743
    if self.logical_id is None and self.physical_id is not None:
744
      return
745
    if self.dev_type in constants.LDS_DRBD:
746
      pnode, snode, port, pminor, sminor, secret = self.logical_id
747
      if target_node not in (pnode, snode):
748
        raise errors.ConfigurationError("DRBD device not knowing node %s" %
749
                                        target_node)
750
      pnode_ip = nodes_ip.get(pnode, None)
751
      snode_ip = nodes_ip.get(snode, None)
752
      if pnode_ip is None or snode_ip is None:
753
        raise errors.ConfigurationError("Can't find primary or secondary node"
754
                                        " for %s" % str(self))
755
      p_data = (pnode_ip, port)
756
      s_data = (snode_ip, port)
757
      if pnode == target_node:
758
        self.physical_id = p_data + s_data + (pminor, secret)
759
      else: # it must be secondary, we tested above
760
        self.physical_id = s_data + p_data + (sminor, secret)
761
    else:
762
      self.physical_id = self.logical_id
763
    return
764

    
765
  def ToDict(self):
766
    """Disk-specific conversion to standard python types.
767

768
    This replaces the children lists of objects with lists of
769
    standard python types.
770

771
    """
772
    bo = super(Disk, self).ToDict()
773

    
774
    for attr in ("children",):
775
      alist = bo.get(attr, None)
776
      if alist:
777
        bo[attr] = self._ContainerToDicts(alist)
778
    return bo
779

    
780
  @classmethod
781
  def FromDict(cls, val):
782
    """Custom function for Disks
783

784
    """
785
    obj = super(Disk, cls).FromDict(val)
786
    if obj.children:
787
      obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
788
    if obj.logical_id and isinstance(obj.logical_id, list):
789
      obj.logical_id = tuple(obj.logical_id)
790
    if obj.physical_id and isinstance(obj.physical_id, list):
791
      obj.physical_id = tuple(obj.physical_id)
792
    if obj.dev_type in constants.LDS_DRBD:
793
      # we need a tuple of length six here
794
      if len(obj.logical_id) < 6:
795
        obj.logical_id += (None,) * (6 - len(obj.logical_id))
796
    return obj
797

    
798
  def __str__(self):
799
    """Custom str() formatter for disks.
800

801
    """
802
    if self.dev_type == constants.LD_LV:
803
      val = "<LogicalVolume(/dev/%s/%s" % self.logical_id
804
    elif self.dev_type in constants.LDS_DRBD:
805
      node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
806
      val = "<DRBD8("
807
      if self.physical_id is None:
808
        phy = "unconfigured"
809
      else:
810
        phy = ("configured as %s:%s %s:%s" %
811
               (self.physical_id[0], self.physical_id[1],
812
                self.physical_id[2], self.physical_id[3]))
813

    
814
      val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
815
              (node_a, minor_a, node_b, minor_b, port, phy))
816
      if self.children and self.children.count(None) == 0:
817
        val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
818
      else:
819
        val += "no local storage"
820
    else:
821
      val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
822
             (self.dev_type, self.logical_id, self.physical_id, self.children))
823
    if self.iv_name is None:
824
      val += ", not visible"
825
    else:
826
      val += ", visible as /dev/%s" % self.iv_name
827
    if isinstance(self.size, int):
828
      val += ", size=%dm)>" % self.size
829
    else:
830
      val += ", size='%s')>" % (self.size,)
831
    return val
832

    
833
  def Verify(self):
834
    """Checks that this disk is correctly configured.
835

836
    """
837
    all_errors = []
838
    if self.mode not in constants.DISK_ACCESS_SET:
839
      all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
840
    return all_errors
841

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

845
    """
846
    if self.children:
847
      for child in self.children:
848
        child.UpgradeConfig()
849

    
850
    # FIXME: Make this configurable in Ganeti 2.7
851
    self.params = {}
852
    # add here config upgrade for this disk
853

    
854
  @staticmethod
855
  def ComputeLDParams(disk_template, disk_params):
856
    """Computes Logical Disk parameters from Disk Template parameters.
857

858
    @type disk_template: string
859
    @param disk_template: disk template, one of L{constants.DISK_TEMPLATES}
860
    @type disk_params: dict
861
    @param disk_params: disk template parameters;
862
                        dict(template_name -> parameters
863
    @rtype: list(dict)
864
    @return: a list of dicts, one for each node of the disk hierarchy. Each dict
865
      contains the LD parameters of the node. The tree is flattened in-order.
866

867
    """
868
    if disk_template not in constants.DISK_TEMPLATES:
869
      raise errors.ProgrammerError("Unknown disk template %s" % disk_template)
870

    
871
    assert disk_template in disk_params
872

    
873
    result = list()
874
    dt_params = disk_params[disk_template]
875
    if disk_template == constants.DT_DRBD8:
876
      drbd_params = {
877
        constants.LDP_RESYNC_RATE: dt_params[constants.DRBD_RESYNC_RATE],
878
        constants.LDP_BARRIERS: dt_params[constants.DRBD_DISK_BARRIERS],
879
        constants.LDP_NO_META_FLUSH: dt_params[constants.DRBD_META_BARRIERS],
880
        constants.LDP_DEFAULT_METAVG: dt_params[constants.DRBD_DEFAULT_METAVG],
881
        constants.LDP_DISK_CUSTOM: dt_params[constants.DRBD_DISK_CUSTOM],
882
        constants.LDP_NET_CUSTOM: dt_params[constants.DRBD_NET_CUSTOM],
883
        constants.LDP_DYNAMIC_RESYNC: dt_params[constants.DRBD_DYNAMIC_RESYNC],
884
        constants.LDP_PLAN_AHEAD: dt_params[constants.DRBD_PLAN_AHEAD],
885
        constants.LDP_FILL_TARGET: dt_params[constants.DRBD_FILL_TARGET],
886
        constants.LDP_DELAY_TARGET: dt_params[constants.DRBD_DELAY_TARGET],
887
        constants.LDP_MAX_RATE: dt_params[constants.DRBD_MAX_RATE],
888
        constants.LDP_MIN_RATE: dt_params[constants.DRBD_MIN_RATE],
889
        }
890

    
891
      drbd_params = \
892
        FillDict(constants.DISK_LD_DEFAULTS[constants.LD_DRBD8],
893
                 drbd_params)
894

    
895
      result.append(drbd_params)
896

    
897
      # data LV
898
      data_params = {
899
        constants.LDP_STRIPES: dt_params[constants.DRBD_DATA_STRIPES],
900
        }
901
      data_params = \
902
        FillDict(constants.DISK_LD_DEFAULTS[constants.LD_LV],
903
                 data_params)
904
      result.append(data_params)
905

    
906
      # metadata LV
907
      meta_params = {
908
        constants.LDP_STRIPES: dt_params[constants.DRBD_META_STRIPES],
909
        }
910
      meta_params = \
911
        FillDict(constants.DISK_LD_DEFAULTS[constants.LD_LV],
912
                 meta_params)
913
      result.append(meta_params)
914

    
915
    elif (disk_template == constants.DT_FILE or
916
          disk_template == constants.DT_SHARED_FILE):
917
      result.append(constants.DISK_LD_DEFAULTS[constants.LD_FILE])
918

    
919
    elif disk_template == constants.DT_PLAIN:
920
      params = {
921
        constants.LDP_STRIPES: dt_params[constants.LV_STRIPES],
922
        }
923
      params = \
924
        FillDict(constants.DISK_LD_DEFAULTS[constants.LD_LV],
925
                 params)
926
      result.append(params)
927

    
928
    elif disk_template == constants.DT_BLOCK:
929
      result.append(constants.DISK_LD_DEFAULTS[constants.LD_BLOCKDEV])
930

    
931
    elif disk_template == constants.DT_RBD:
932
      params = {
933
        constants.LDP_POOL: dt_params[constants.RBD_POOL]
934
        }
935
      params = \
936
        FillDict(constants.DISK_LD_DEFAULTS[constants.LD_RBD],
937
                 params)
938
      result.append(params)
939

    
940
    return result
941

    
942

    
943
class InstancePolicy(ConfigObject):
944
  """Config object representing instance policy limits dictionary.
945

946

947
  Note that this object is not actually used in the config, it's just
948
  used as a placeholder for a few functions.
949

950
  """
951
  @classmethod
952
  def CheckParameterSyntax(cls, ipolicy, check_std):
953
    """ Check the instance policy for validity.
954

955
    """
956
    for param in constants.ISPECS_PARAMETERS:
957
      InstancePolicy.CheckISpecSyntax(ipolicy, param, check_std)
958
    if constants.IPOLICY_DTS in ipolicy:
959
      InstancePolicy.CheckDiskTemplates(ipolicy[constants.IPOLICY_DTS])
960
    for key in constants.IPOLICY_PARAMETERS:
961
      if key in ipolicy:
962
        InstancePolicy.CheckParameter(key, ipolicy[key])
963
    wrong_keys = frozenset(ipolicy.keys()) - constants.IPOLICY_ALL_KEYS
964
    if wrong_keys:
965
      raise errors.ConfigurationError("Invalid keys in ipolicy: %s" %
966
                                      utils.CommaJoin(wrong_keys))
967

    
968
  @classmethod
969
  def CheckISpecSyntax(cls, ipolicy, name, check_std):
970
    """Check the instance policy for validity on a given key.
971

972
    We check if the instance policy makes sense for a given key, that is
973
    if ipolicy[min][name] <= ipolicy[std][name] <= ipolicy[max][name].
974

975
    @type ipolicy: dict
976
    @param ipolicy: dictionary with min, max, std specs
977
    @type name: string
978
    @param name: what are the limits for
979
    @type check_std: bool
980
    @param check_std: Whether to check std value or just assume compliance
981
    @raise errors.ConfigureError: when specs for given name are not valid
982

983
    """
984
    min_v = ipolicy[constants.ISPECS_MIN].get(name, 0)
985

    
986
    if check_std:
987
      std_v = ipolicy[constants.ISPECS_STD].get(name, min_v)
988
      std_msg = std_v
989
    else:
990
      std_v = min_v
991
      std_msg = "-"
992

    
993
    max_v = ipolicy[constants.ISPECS_MAX].get(name, std_v)
994
    err = ("Invalid specification of min/max/std values for %s: %s/%s/%s" %
995
           (name,
996
            ipolicy[constants.ISPECS_MIN].get(name, "-"),
997
            ipolicy[constants.ISPECS_MAX].get(name, "-"),
998
            std_msg))
999
    if min_v > std_v or std_v > max_v:
1000
      raise errors.ConfigurationError(err)
1001

    
1002
  @classmethod
1003
  def CheckDiskTemplates(cls, disk_templates):
1004
    """Checks the disk templates for validity.
1005

1006
    """
1007
    wrong = frozenset(disk_templates).difference(constants.DISK_TEMPLATES)
1008
    if wrong:
1009
      raise errors.ConfigurationError("Invalid disk template(s) %s" %
1010
                                      utils.CommaJoin(wrong))
1011

    
1012
  @classmethod
1013
  def CheckParameter(cls, key, value):
1014
    """Checks a parameter.
1015

1016
    Currently we expect all parameters to be float values.
1017

1018
    """
1019
    try:
1020
      float(value)
1021
    except (TypeError, ValueError), err:
1022
      raise errors.ConfigurationError("Invalid value for key" " '%s':"
1023
                                      " '%s', error: %s" % (key, value, err))
1024

    
1025

    
1026
class Instance(TaggableObject):
1027
  """Config object representing an instance."""
1028
  __slots__ = [
1029
    "name",
1030
    "primary_node",
1031
    "os",
1032
    "hypervisor",
1033
    "hvparams",
1034
    "beparams",
1035
    "osparams",
1036
    "admin_state",
1037
    "nics",
1038
    "disks",
1039
    "hotplug_info",
1040
    "disk_template",
1041
    "network_port",
1042
    "serial_no",
1043
    ] + _TIMESTAMPS + _UUID
1044

    
1045
  def _ComputeSecondaryNodes(self):
1046
    """Compute the list of secondary nodes.
1047

1048
    This is a simple wrapper over _ComputeAllNodes.
1049

1050
    """
1051
    all_nodes = set(self._ComputeAllNodes())
1052
    all_nodes.discard(self.primary_node)
1053
    return tuple(all_nodes)
1054

    
1055
  secondary_nodes = property(_ComputeSecondaryNodes, None, None,
1056
                             "List of secondary nodes")
1057

    
1058
  def _ComputeAllNodes(self):
1059
    """Compute the list of all nodes.
1060

1061
    Since the data is already there (in the drbd disks), keeping it as
1062
    a separate normal attribute is redundant and if not properly
1063
    synchronised can cause problems. Thus it's better to compute it
1064
    dynamically.
1065

1066
    """
1067
    def _Helper(nodes, device):
1068
      """Recursively computes nodes given a top device."""
1069
      if device.dev_type in constants.LDS_DRBD:
1070
        nodea, nodeb = device.logical_id[:2]
1071
        nodes.add(nodea)
1072
        nodes.add(nodeb)
1073
      if device.children:
1074
        for child in device.children:
1075
          _Helper(nodes, child)
1076

    
1077
    all_nodes = set()
1078
    all_nodes.add(self.primary_node)
1079
    for device in self.disks:
1080
      _Helper(all_nodes, device)
1081
    return tuple(all_nodes)
1082

    
1083
  all_nodes = property(_ComputeAllNodes, None, None,
1084
                       "List of all nodes of the instance")
1085

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

1089
    This function figures out what logical volumes should belong on
1090
    which nodes, recursing through a device tree.
1091

1092
    @param lvmap: optional dictionary to receive the
1093
        'node' : ['lv', ...] data.
1094

1095
    @return: None if lvmap arg is given, otherwise, a dictionary of
1096
        the form { 'nodename' : ['volume1', 'volume2', ...], ... };
1097
        volumeN is of the form "vg_name/lv_name", compatible with
1098
        GetVolumeList()
1099

1100
    """
1101
    if node == None:
1102
      node = self.primary_node
1103

    
1104
    if lvmap is None:
1105
      lvmap = {
1106
        node: [],
1107
        }
1108
      ret = lvmap
1109
    else:
1110
      if not node in lvmap:
1111
        lvmap[node] = []
1112
      ret = None
1113

    
1114
    if not devs:
1115
      devs = self.disks
1116

    
1117
    for dev in devs:
1118
      if dev.dev_type == constants.LD_LV:
1119
        lvmap[node].append(dev.logical_id[0] + "/" + dev.logical_id[1])
1120

    
1121
      elif dev.dev_type in constants.LDS_DRBD:
1122
        if dev.children:
1123
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
1124
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
1125

    
1126
      elif dev.children:
1127
        self.MapLVsByNode(lvmap, dev.children, node)
1128

    
1129
    return ret
1130

    
1131
  def FindDisk(self, idx):
1132
    """Find a disk given having a specified index.
1133

1134
    This is just a wrapper that does validation of the index.
1135

1136
    @type idx: int
1137
    @param idx: the disk index
1138
    @rtype: L{Disk}
1139
    @return: the corresponding disk
1140
    @raise errors.OpPrereqError: when the given index is not valid
1141

1142
    """
1143
    try:
1144
      idx = int(idx)
1145
      return self.disks[idx]
1146
    except (TypeError, ValueError), err:
1147
      raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err),
1148
                                 errors.ECODE_INVAL)
1149
    except IndexError:
1150
      raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
1151
                                 " 0 to %d" % (idx, len(self.disks) - 1),
1152
                                 errors.ECODE_INVAL)
1153

    
1154
  def ToDict(self):
1155
    """Instance-specific conversion to standard python types.
1156

1157
    This replaces the children lists of objects with lists of standard
1158
    python types.
1159

1160
    """
1161
    bo = super(Instance, self).ToDict()
1162

    
1163
    for attr in "nics", "disks":
1164
      alist = bo.get(attr, None)
1165
      if alist:
1166
        nlist = self._ContainerToDicts(alist)
1167
      else:
1168
        nlist = []
1169
      bo[attr] = nlist
1170
    if self.hotplug_info:
1171
      bo['hotplug_info'] = self.hotplug_info.ToDict()
1172
    return bo
1173

    
1174
  @classmethod
1175
  def FromDict(cls, val):
1176
    """Custom function for instances.
1177

1178
    """
1179
    if "admin_state" not in val:
1180
      if val.get("admin_up", False):
1181
        val["admin_state"] = constants.ADMINST_UP
1182
      else:
1183
        val["admin_state"] = constants.ADMINST_DOWN
1184
    if "admin_up" in val:
1185
      del val["admin_up"]
1186
    obj = super(Instance, cls).FromDict(val)
1187
    obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
1188
    obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
1189
    if "hotplug_info" in val:
1190
      obj.hotplug_info = HotplugInfo.FromDict(val["hotplug_info"])
1191
    return obj
1192

    
1193
  def UpgradeConfig(self):
1194
    """Fill defaults for missing configuration values.
1195

1196
    """
1197
    for nic in self.nics:
1198
      nic.UpgradeConfig()
1199
    for disk in self.disks:
1200
      disk.UpgradeConfig()
1201
    if self.hvparams:
1202
      for key in constants.HVC_GLOBALS:
1203
        try:
1204
          del self.hvparams[key]
1205
        except KeyError:
1206
          pass
1207
    if self.osparams is None:
1208
      self.osparams = {}
1209
    UpgradeBeParams(self.beparams)
1210

    
1211

    
1212
class OS(ConfigObject):
1213
  """Config object representing an operating system.
1214

1215
  @type supported_parameters: list
1216
  @ivar supported_parameters: a list of tuples, name and description,
1217
      containing the supported parameters by this OS
1218

1219
  @type VARIANT_DELIM: string
1220
  @cvar VARIANT_DELIM: the variant delimiter
1221

1222
  """
1223
  __slots__ = [
1224
    "name",
1225
    "path",
1226
    "api_versions",
1227
    "create_script",
1228
    "export_script",
1229
    "import_script",
1230
    "rename_script",
1231
    "verify_script",
1232
    "supported_variants",
1233
    "supported_parameters",
1234
    ]
1235

    
1236
  VARIANT_DELIM = "+"
1237

    
1238
  @classmethod
1239
  def SplitNameVariant(cls, name):
1240
    """Splits the name into the proper name and variant.
1241

1242
    @param name: the OS (unprocessed) name
1243
    @rtype: list
1244
    @return: a list of two elements; if the original name didn't
1245
        contain a variant, it's returned as an empty string
1246

1247
    """
1248
    nv = name.split(cls.VARIANT_DELIM, 1)
1249
    if len(nv) == 1:
1250
      nv.append("")
1251
    return nv
1252

    
1253
  @classmethod
1254
  def GetName(cls, name):
1255
    """Returns the proper name of the os (without the variant).
1256

1257
    @param name: the OS (unprocessed) name
1258

1259
    """
1260
    return cls.SplitNameVariant(name)[0]
1261

    
1262
  @classmethod
1263
  def GetVariant(cls, name):
1264
    """Returns the variant the os (without the base name).
1265

1266
    @param name: the OS (unprocessed) name
1267

1268
    """
1269
    return cls.SplitNameVariant(name)[1]
1270

    
1271

    
1272
class NodeHvState(ConfigObject):
1273
  """Hypvervisor state on a node.
1274

1275
  @ivar mem_total: Total amount of memory
1276
  @ivar mem_node: Memory used by, or reserved for, the node itself (not always
1277
    available)
1278
  @ivar mem_hv: Memory used by hypervisor or lost due to instance allocation
1279
    rounding
1280
  @ivar mem_inst: Memory used by instances living on node
1281
  @ivar cpu_total: Total node CPU core count
1282
  @ivar cpu_node: Number of CPU cores reserved for the node itself
1283

1284
  """
1285
  __slots__ = [
1286
    "mem_total",
1287
    "mem_node",
1288
    "mem_hv",
1289
    "mem_inst",
1290
    "cpu_total",
1291
    "cpu_node",
1292
    ] + _TIMESTAMPS
1293

    
1294

    
1295
class NodeDiskState(ConfigObject):
1296
  """Disk state on a node.
1297

1298
  """
1299
  __slots__ = [
1300
    "total",
1301
    "reserved",
1302
    "overhead",
1303
    ] + _TIMESTAMPS
1304

    
1305

    
1306
class Node(TaggableObject):
1307
  """Config object representing a node.
1308

1309
  @ivar hv_state: Hypervisor state (e.g. number of CPUs)
1310
  @ivar hv_state_static: Hypervisor state overriden by user
1311
  @ivar disk_state: Disk state (e.g. free space)
1312
  @ivar disk_state_static: Disk state overriden by user
1313

1314
  """
1315
  __slots__ = [
1316
    "name",
1317
    "primary_ip",
1318
    "secondary_ip",
1319
    "serial_no",
1320
    "master_candidate",
1321
    "offline",
1322
    "drained",
1323
    "group",
1324
    "master_capable",
1325
    "vm_capable",
1326
    "ndparams",
1327
    "powered",
1328
    "hv_state",
1329
    "hv_state_static",
1330
    "disk_state",
1331
    "disk_state_static",
1332
    ] + _TIMESTAMPS + _UUID
1333

    
1334
  def UpgradeConfig(self):
1335
    """Fill defaults for missing configuration values.
1336

1337
    """
1338
    # pylint: disable=E0203
1339
    # because these are "defined" via slots, not manually
1340
    if self.master_capable is None:
1341
      self.master_capable = True
1342

    
1343
    if self.vm_capable is None:
1344
      self.vm_capable = True
1345

    
1346
    if self.ndparams is None:
1347
      self.ndparams = {}
1348

    
1349
    if self.powered is None:
1350
      self.powered = True
1351

    
1352
  def ToDict(self):
1353
    """Custom function for serializing.
1354

1355
    """
1356
    data = super(Node, self).ToDict()
1357

    
1358
    hv_state = data.get("hv_state", None)
1359
    if hv_state is not None:
1360
      data["hv_state"] = self._ContainerToDicts(hv_state)
1361

    
1362
    disk_state = data.get("disk_state", None)
1363
    if disk_state is not None:
1364
      data["disk_state"] = \
1365
        dict((key, self._ContainerToDicts(value))
1366
             for (key, value) in disk_state.items())
1367

    
1368
    return data
1369

    
1370
  @classmethod
1371
  def FromDict(cls, val):
1372
    """Custom function for deserializing.
1373

1374
    """
1375
    obj = super(Node, cls).FromDict(val)
1376

    
1377
    if obj.hv_state is not None:
1378
      obj.hv_state = cls._ContainerFromDicts(obj.hv_state, dict, NodeHvState)
1379

    
1380
    if obj.disk_state is not None:
1381
      obj.disk_state = \
1382
        dict((key, cls._ContainerFromDicts(value, dict, NodeDiskState))
1383
             for (key, value) in obj.disk_state.items())
1384

    
1385
    return obj
1386

    
1387

    
1388
class NodeGroup(TaggableObject):
1389
  """Config object representing a node group."""
1390
  __slots__ = [
1391
    "name",
1392
    "members",
1393
    "ndparams",
1394
    "diskparams",
1395
    "ipolicy",
1396
    "serial_no",
1397
    "hv_state_static",
1398
    "disk_state_static",
1399
    "alloc_policy",
1400
    ] + _TIMESTAMPS + _UUID
1401

    
1402
  def ToDict(self):
1403
    """Custom function for nodegroup.
1404

1405
    This discards the members object, which gets recalculated and is only kept
1406
    in memory.
1407

1408
    """
1409
    mydict = super(NodeGroup, self).ToDict()
1410
    del mydict["members"]
1411
    return mydict
1412

    
1413
  @classmethod
1414
  def FromDict(cls, val):
1415
    """Custom function for nodegroup.
1416

1417
    The members slot is initialized to an empty list, upon deserialization.
1418

1419
    """
1420
    obj = super(NodeGroup, cls).FromDict(val)
1421
    obj.members = []
1422
    return obj
1423

    
1424
  def UpgradeConfig(self):
1425
    """Fill defaults for missing configuration values.
1426

1427
    """
1428
    if self.ndparams is None:
1429
      self.ndparams = {}
1430

    
1431
    if self.serial_no is None:
1432
      self.serial_no = 1
1433

    
1434
    if self.alloc_policy is None:
1435
      self.alloc_policy = constants.ALLOC_POLICY_PREFERRED
1436

    
1437
    # We only update mtime, and not ctime, since we would not be able
1438
    # to provide a correct value for creation time.
1439
    if self.mtime is None:
1440
      self.mtime = time.time()
1441

    
1442
    if self.diskparams is None:
1443
      self.diskparams = {}
1444
    if self.ipolicy is None:
1445
      self.ipolicy = MakeEmptyIPolicy()
1446

    
1447
  def FillND(self, node):
1448
    """Return filled out ndparams for L{objects.Node}
1449

1450
    @type node: L{objects.Node}
1451
    @param node: A Node object to fill
1452
    @return a copy of the node's ndparams with defaults filled
1453

1454
    """
1455
    return self.SimpleFillND(node.ndparams)
1456

    
1457
  def SimpleFillND(self, ndparams):
1458
    """Fill a given ndparams dict with defaults.
1459

1460
    @type ndparams: dict
1461
    @param ndparams: the dict to fill
1462
    @rtype: dict
1463
    @return: a copy of the passed in ndparams with missing keys filled
1464
        from the node group defaults
1465

1466
    """
1467
    return FillDict(self.ndparams, ndparams)
1468

    
1469

    
1470
class Cluster(TaggableObject):
1471
  """Config object representing the cluster."""
1472
  __slots__ = [
1473
    "serial_no",
1474
    "rsahostkeypub",
1475
    "highest_used_port",
1476
    "tcpudp_port_pool",
1477
    "mac_prefix",
1478
    "volume_group_name",
1479
    "reserved_lvs",
1480
    "drbd_usermode_helper",
1481
    "default_bridge",
1482
    "default_hypervisor",
1483
    "master_node",
1484
    "master_ip",
1485
    "master_netdev",
1486
    "master_netmask",
1487
    "use_external_mip_script",
1488
    "cluster_name",
1489
    "file_storage_dir",
1490
    "shared_file_storage_dir",
1491
    "enabled_hypervisors",
1492
    "hvparams",
1493
    "ipolicy",
1494
    "os_hvp",
1495
    "beparams",
1496
    "osparams",
1497
    "nicparams",
1498
    "ndparams",
1499
    "diskparams",
1500
    "candidate_pool_size",
1501
    "modify_etc_hosts",
1502
    "modify_ssh_setup",
1503
    "maintain_node_health",
1504
    "uid_pool",
1505
    "default_iallocator",
1506
    "hidden_os",
1507
    "blacklisted_os",
1508
    "primary_ip_family",
1509
    "prealloc_wipe_disks",
1510
    "hv_state_static",
1511
    "disk_state_static",
1512
    ] + _TIMESTAMPS + _UUID
1513

    
1514
  def UpgradeConfig(self):
1515
    """Fill defaults for missing configuration values.
1516

1517
    """
1518
    # pylint: disable=E0203
1519
    # because these are "defined" via slots, not manually
1520
    if self.hvparams is None:
1521
      self.hvparams = constants.HVC_DEFAULTS
1522
    else:
1523
      for hypervisor in self.hvparams:
1524
        self.hvparams[hypervisor] = FillDict(
1525
            constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
1526

    
1527
    if self.os_hvp is None:
1528
      self.os_hvp = {}
1529

    
1530
    # osparams added before 2.2
1531
    if self.osparams is None:
1532
      self.osparams = {}
1533

    
1534
    self.ndparams = UpgradeNDParams(self.ndparams)
1535

    
1536
    self.beparams = UpgradeGroupedParams(self.beparams,
1537
                                         constants.BEC_DEFAULTS)
1538
    for beparams_group in self.beparams:
1539
      UpgradeBeParams(self.beparams[beparams_group])
1540

    
1541
    migrate_default_bridge = not self.nicparams
1542
    self.nicparams = UpgradeGroupedParams(self.nicparams,
1543
                                          constants.NICC_DEFAULTS)
1544
    if migrate_default_bridge:
1545
      self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
1546
        self.default_bridge
1547

    
1548
    if self.modify_etc_hosts is None:
1549
      self.modify_etc_hosts = True
1550

    
1551
    if self.modify_ssh_setup is None:
1552
      self.modify_ssh_setup = True
1553

    
1554
    # default_bridge is no longer used in 2.1. The slot is left there to
1555
    # support auto-upgrading. It can be removed once we decide to deprecate
1556
    # upgrading straight from 2.0.
1557
    if self.default_bridge is not None:
1558
      self.default_bridge = None
1559

    
1560
    # default_hypervisor is just the first enabled one in 2.1. This slot and
1561
    # code can be removed once upgrading straight from 2.0 is deprecated.
1562
    if self.default_hypervisor is not None:
1563
      self.enabled_hypervisors = ([self.default_hypervisor] +
1564
        [hvname for hvname in self.enabled_hypervisors
1565
         if hvname != self.default_hypervisor])
1566
      self.default_hypervisor = None
1567

    
1568
    # maintain_node_health added after 2.1.1
1569
    if self.maintain_node_health is None:
1570
      self.maintain_node_health = False
1571

    
1572
    if self.uid_pool is None:
1573
      self.uid_pool = []
1574

    
1575
    if self.default_iallocator is None:
1576
      self.default_iallocator = ""
1577

    
1578
    # reserved_lvs added before 2.2
1579
    if self.reserved_lvs is None:
1580
      self.reserved_lvs = []
1581

    
1582
    # hidden and blacklisted operating systems added before 2.2.1
1583
    if self.hidden_os is None:
1584
      self.hidden_os = []
1585

    
1586
    if self.blacklisted_os is None:
1587
      self.blacklisted_os = []
1588

    
1589
    # primary_ip_family added before 2.3
1590
    if self.primary_ip_family is None:
1591
      self.primary_ip_family = AF_INET
1592

    
1593
    if self.master_netmask is None:
1594
      ipcls = netutils.IPAddress.GetClassFromIpFamily(self.primary_ip_family)
1595
      self.master_netmask = ipcls.iplen
1596

    
1597
    if self.prealloc_wipe_disks is None:
1598
      self.prealloc_wipe_disks = False
1599

    
1600
    # shared_file_storage_dir added before 2.5
1601
    if self.shared_file_storage_dir is None:
1602
      self.shared_file_storage_dir = ""
1603

    
1604
    if self.use_external_mip_script is None:
1605
      self.use_external_mip_script = False
1606

    
1607
    if self.diskparams:
1608
      self.diskparams = UpgradeDiskParams(self.diskparams)
1609
    else:
1610
      self.diskparams = constants.DISK_DT_DEFAULTS.copy()
1611

    
1612
    # instance policy added before 2.6
1613
    if self.ipolicy is None:
1614
      self.ipolicy = FillIPolicy(constants.IPOLICY_DEFAULTS, {})
1615
    else:
1616
      # we can either make sure to upgrade the ipolicy always, or only
1617
      # do it in some corner cases (e.g. missing keys); note that this
1618
      # will break any removal of keys from the ipolicy dict
1619
      self.ipolicy = FillIPolicy(constants.IPOLICY_DEFAULTS, self.ipolicy)
1620

    
1621
  @property
1622
  def primary_hypervisor(self):
1623
    """The first hypervisor is the primary.
1624

1625
    Useful, for example, for L{Node}'s hv/disk state.
1626

1627
    """
1628
    return self.enabled_hypervisors[0]
1629

    
1630
  def ToDict(self):
1631
    """Custom function for cluster.
1632

1633
    """
1634
    mydict = super(Cluster, self).ToDict()
1635
    mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
1636
    return mydict
1637

    
1638
  @classmethod
1639
  def FromDict(cls, val):
1640
    """Custom function for cluster.
1641

1642
    """
1643
    obj = super(Cluster, cls).FromDict(val)
1644
    if not isinstance(obj.tcpudp_port_pool, set):
1645
      obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
1646
    return obj
1647

    
1648
  def SimpleFillDP(self, diskparams):
1649
    """Fill a given diskparams dict with cluster defaults.
1650

1651
    @param diskparams: The diskparams
1652
    @return: The defaults dict
1653

1654
    """
1655
    return FillDiskParams(self.diskparams, diskparams)
1656

    
1657
  def GetHVDefaults(self, hypervisor, os_name=None, skip_keys=None):
1658
    """Get the default hypervisor parameters for the cluster.
1659

1660
    @param hypervisor: the hypervisor name
1661
    @param os_name: if specified, we'll also update the defaults for this OS
1662
    @param skip_keys: if passed, list of keys not to use
1663
    @return: the defaults dict
1664

1665
    """
1666
    if skip_keys is None:
1667
      skip_keys = []
1668

    
1669
    fill_stack = [self.hvparams.get(hypervisor, {})]
1670
    if os_name is not None:
1671
      os_hvp = self.os_hvp.get(os_name, {}).get(hypervisor, {})
1672
      fill_stack.append(os_hvp)
1673

    
1674
    ret_dict = {}
1675
    for o_dict in fill_stack:
1676
      ret_dict = FillDict(ret_dict, o_dict, skip_keys=skip_keys)
1677

    
1678
    return ret_dict
1679

    
1680
  def SimpleFillHV(self, hv_name, os_name, hvparams, skip_globals=False):
1681
    """Fill a given hvparams dict with cluster defaults.
1682

1683
    @type hv_name: string
1684
    @param hv_name: the hypervisor to use
1685
    @type os_name: string
1686
    @param os_name: the OS to use for overriding the hypervisor defaults
1687
    @type skip_globals: boolean
1688
    @param skip_globals: if True, the global hypervisor parameters will
1689
        not be filled
1690
    @rtype: dict
1691
    @return: a copy of the given hvparams with missing keys filled from
1692
        the cluster defaults
1693

1694
    """
1695
    if skip_globals:
1696
      skip_keys = constants.HVC_GLOBALS
1697
    else:
1698
      skip_keys = []
1699

    
1700
    def_dict = self.GetHVDefaults(hv_name, os_name, skip_keys=skip_keys)
1701
    return FillDict(def_dict, hvparams, skip_keys=skip_keys)
1702

    
1703
  def FillHV(self, instance, skip_globals=False):
1704
    """Fill an instance's hvparams dict with cluster defaults.
1705

1706
    @type instance: L{objects.Instance}
1707
    @param instance: the instance parameter to fill
1708
    @type skip_globals: boolean
1709
    @param skip_globals: if True, the global hypervisor parameters will
1710
        not be filled
1711
    @rtype: dict
1712
    @return: a copy of the instance's hvparams with missing keys filled from
1713
        the cluster defaults
1714

1715
    """
1716
    return self.SimpleFillHV(instance.hypervisor, instance.os,
1717
                             instance.hvparams, skip_globals)
1718

    
1719
  def SimpleFillBE(self, beparams):
1720
    """Fill a given beparams dict with cluster defaults.
1721

1722
    @type beparams: dict
1723
    @param beparams: the dict to fill
1724
    @rtype: dict
1725
    @return: a copy of the passed in beparams with missing keys filled
1726
        from the cluster defaults
1727

1728
    """
1729
    return FillDict(self.beparams.get(constants.PP_DEFAULT, {}), beparams)
1730

    
1731
  def FillBE(self, instance):
1732
    """Fill an instance's beparams dict with cluster defaults.
1733

1734
    @type instance: L{objects.Instance}
1735
    @param instance: the instance parameter to fill
1736
    @rtype: dict
1737
    @return: a copy of the instance's beparams with missing keys filled from
1738
        the cluster defaults
1739

1740
    """
1741
    return self.SimpleFillBE(instance.beparams)
1742

    
1743
  def SimpleFillNIC(self, nicparams):
1744
    """Fill a given nicparams dict with cluster defaults.
1745

1746
    @type nicparams: dict
1747
    @param nicparams: the dict to fill
1748
    @rtype: dict
1749
    @return: a copy of the passed in nicparams with missing keys filled
1750
        from the cluster defaults
1751

1752
    """
1753
    return FillDict(self.nicparams.get(constants.PP_DEFAULT, {}), nicparams)
1754

    
1755
  def SimpleFillOS(self, os_name, os_params):
1756
    """Fill an instance's osparams dict with cluster defaults.
1757

1758
    @type os_name: string
1759
    @param os_name: the OS name to use
1760
    @type os_params: dict
1761
    @param os_params: the dict to fill with default values
1762
    @rtype: dict
1763
    @return: a copy of the instance's osparams with missing keys filled from
1764
        the cluster defaults
1765

1766
    """
1767
    name_only = os_name.split("+", 1)[0]
1768
    # base OS
1769
    result = self.osparams.get(name_only, {})
1770
    # OS with variant
1771
    result = FillDict(result, self.osparams.get(os_name, {}))
1772
    # specified params
1773
    return FillDict(result, os_params)
1774

    
1775
  @staticmethod
1776
  def SimpleFillHvState(hv_state):
1777
    """Fill an hv_state sub dict with cluster defaults.
1778

1779
    """
1780
    return FillDict(constants.HVST_DEFAULTS, hv_state)
1781

    
1782
  @staticmethod
1783
  def SimpleFillDiskState(disk_state):
1784
    """Fill an disk_state sub dict with cluster defaults.
1785

1786
    """
1787
    return FillDict(constants.DS_DEFAULTS, disk_state)
1788

    
1789
  def FillND(self, node, nodegroup):
1790
    """Return filled out ndparams for L{objects.NodeGroup} and L{objects.Node}
1791

1792
    @type node: L{objects.Node}
1793
    @param node: A Node object to fill
1794
    @type nodegroup: L{objects.NodeGroup}
1795
    @param nodegroup: A Node object to fill
1796
    @return a copy of the node's ndparams with defaults filled
1797

1798
    """
1799
    return self.SimpleFillND(nodegroup.FillND(node))
1800

    
1801
  def SimpleFillND(self, ndparams):
1802
    """Fill a given ndparams dict with defaults.
1803

1804
    @type ndparams: dict
1805
    @param ndparams: the dict to fill
1806
    @rtype: dict
1807
    @return: a copy of the passed in ndparams with missing keys filled
1808
        from the cluster defaults
1809

1810
    """
1811
    return FillDict(self.ndparams, ndparams)
1812

    
1813
  def SimpleFillIPolicy(self, ipolicy):
1814
    """ Fill instance policy dict with defaults.
1815

1816
    @type ipolicy: dict
1817
    @param ipolicy: the dict to fill
1818
    @rtype: dict
1819
    @return: a copy of passed ipolicy with missing keys filled from
1820
      the cluster defaults
1821

1822
    """
1823
    return FillIPolicy(self.ipolicy, ipolicy)
1824

    
1825

    
1826
class BlockDevStatus(ConfigObject):
1827
  """Config object representing the status of a block device."""
1828
  __slots__ = [
1829
    "dev_path",
1830
    "major",
1831
    "minor",
1832
    "sync_percent",
1833
    "estimated_time",
1834
    "is_degraded",
1835
    "ldisk_status",
1836
    ]
1837

    
1838

    
1839
class ImportExportStatus(ConfigObject):
1840
  """Config object representing the status of an import or export."""
1841
  __slots__ = [
1842
    "recent_output",
1843
    "listen_port",
1844
    "connected",
1845
    "progress_mbytes",
1846
    "progress_throughput",
1847
    "progress_eta",
1848
    "progress_percent",
1849
    "exit_status",
1850
    "error_message",
1851
    ] + _TIMESTAMPS
1852

    
1853

    
1854
class ImportExportOptions(ConfigObject):
1855
  """Options for import/export daemon
1856

1857
  @ivar key_name: X509 key name (None for cluster certificate)
1858
  @ivar ca_pem: Remote peer CA in PEM format (None for cluster certificate)
1859
  @ivar compress: Compression method (one of L{constants.IEC_ALL})
1860
  @ivar magic: Used to ensure the connection goes to the right disk
1861
  @ivar ipv6: Whether to use IPv6
1862
  @ivar connect_timeout: Number of seconds for establishing connection
1863

1864
  """
1865
  __slots__ = [
1866
    "key_name",
1867
    "ca_pem",
1868
    "compress",
1869
    "magic",
1870
    "ipv6",
1871
    "connect_timeout",
1872
    ]
1873

    
1874

    
1875
class ConfdRequest(ConfigObject):
1876
  """Object holding a confd request.
1877

1878
  @ivar protocol: confd protocol version
1879
  @ivar type: confd query type
1880
  @ivar query: query request
1881
  @ivar rsalt: requested reply salt
1882

1883
  """
1884
  __slots__ = [
1885
    "protocol",
1886
    "type",
1887
    "query",
1888
    "rsalt",
1889
    ]
1890

    
1891

    
1892
class ConfdReply(ConfigObject):
1893
  """Object holding a confd reply.
1894

1895
  @ivar protocol: confd protocol version
1896
  @ivar status: reply status code (ok, error)
1897
  @ivar answer: confd query reply
1898
  @ivar serial: configuration serial number
1899

1900
  """
1901
  __slots__ = [
1902
    "protocol",
1903
    "status",
1904
    "answer",
1905
    "serial",
1906
    ]
1907

    
1908

    
1909
class QueryFieldDefinition(ConfigObject):
1910
  """Object holding a query field definition.
1911

1912
  @ivar name: Field name
1913
  @ivar title: Human-readable title
1914
  @ivar kind: Field type
1915
  @ivar doc: Human-readable description
1916

1917
  """
1918
  __slots__ = [
1919
    "name",
1920
    "title",
1921
    "kind",
1922
    "doc",
1923
    ]
1924

    
1925

    
1926
class _QueryResponseBase(ConfigObject):
1927
  __slots__ = [
1928
    "fields",
1929
    ]
1930

    
1931
  def ToDict(self):
1932
    """Custom function for serializing.
1933

1934
    """
1935
    mydict = super(_QueryResponseBase, self).ToDict()
1936
    mydict["fields"] = self._ContainerToDicts(mydict["fields"])
1937
    return mydict
1938

    
1939
  @classmethod
1940
  def FromDict(cls, val):
1941
    """Custom function for de-serializing.
1942

1943
    """
1944
    obj = super(_QueryResponseBase, cls).FromDict(val)
1945
    obj.fields = cls._ContainerFromDicts(obj.fields, list, QueryFieldDefinition)
1946
    return obj
1947

    
1948

    
1949
class QueryResponse(_QueryResponseBase):
1950
  """Object holding the response to a query.
1951

1952
  @ivar fields: List of L{QueryFieldDefinition} objects
1953
  @ivar data: Requested data
1954

1955
  """
1956
  __slots__ = [
1957
    "data",
1958
    ]
1959

    
1960

    
1961
class QueryFieldsRequest(ConfigObject):
1962
  """Object holding a request for querying available fields.
1963

1964
  """
1965
  __slots__ = [
1966
    "what",
1967
    "fields",
1968
    ]
1969

    
1970

    
1971
class QueryFieldsResponse(_QueryResponseBase):
1972
  """Object holding the response to a query for fields.
1973

1974
  @ivar fields: List of L{QueryFieldDefinition} objects
1975

1976
  """
1977
  __slots__ = [
1978
    ]
1979

    
1980

    
1981
class MigrationStatus(ConfigObject):
1982
  """Object holding the status of a migration.
1983

1984
  """
1985
  __slots__ = [
1986
    "status",
1987
    "transferred_ram",
1988
    "total_ram",
1989
    ]
1990

    
1991

    
1992
class InstanceConsole(ConfigObject):
1993
  """Object describing how to access the console of an instance.
1994

1995
  """
1996
  __slots__ = [
1997
    "instance",
1998
    "kind",
1999
    "message",
2000
    "host",
2001
    "port",
2002
    "user",
2003
    "command",
2004
    "display",
2005
    ]
2006

    
2007
  def Validate(self):
2008
    """Validates contents of this object.
2009

2010
    """
2011
    assert self.kind in constants.CONS_ALL, "Unknown console type"
2012
    assert self.instance, "Missing instance name"
2013
    assert self.message or self.kind in [constants.CONS_SSH,
2014
                                         constants.CONS_SPICE,
2015
                                         constants.CONS_VNC]
2016
    assert self.host or self.kind == constants.CONS_MESSAGE
2017
    assert self.port or self.kind in [constants.CONS_MESSAGE,
2018
                                      constants.CONS_SSH]
2019
    assert self.user or self.kind in [constants.CONS_MESSAGE,
2020
                                      constants.CONS_SPICE,
2021
                                      constants.CONS_VNC]
2022
    assert self.command or self.kind in [constants.CONS_MESSAGE,
2023
                                         constants.CONS_SPICE,
2024
                                         constants.CONS_VNC]
2025
    assert self.display or self.kind in [constants.CONS_MESSAGE,
2026
                                         constants.CONS_SPICE,
2027
                                         constants.CONS_SSH]
2028
    return True
2029

    
2030

    
2031
class SerializableConfigParser(ConfigParser.SafeConfigParser):
2032
  """Simple wrapper over ConfigParse that allows serialization.
2033

2034
  This class is basically ConfigParser.SafeConfigParser with two
2035
  additional methods that allow it to serialize/unserialize to/from a
2036
  buffer.
2037

2038
  """
2039
  def Dumps(self):
2040
    """Dump this instance and return the string representation."""
2041
    buf = StringIO()
2042
    self.write(buf)
2043
    return buf.getvalue()
2044

    
2045
  @classmethod
2046
  def Loads(cls, data):
2047
    """Load data from a string."""
2048
    buf = StringIO(data)
2049
    cfp = cls()
2050
    cfp.readfp(buf)
2051
    return cfp