Add 'enabled_storage_types' to the cluster config
[ganeti-local] / lib / objects.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 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 logging
42 import time
43 from cStringIO import StringIO
44
45 from ganeti import errors
46 from ganeti import constants
47 from ganeti import netutils
48 from ganeti import outils
49 from ganeti import utils
50
51 from socket import AF_INET
52
53
54 __all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
55            "OS", "Node", "NodeGroup", "Cluster", "FillDict", "Network"]
56
57 _TIMESTAMPS = ["ctime", "mtime"]
58 _UUID = ["uuid"]
59
60
61 def FillDict(defaults_dict, custom_dict, skip_keys=None):
62   """Basic function to apply settings on top a default dict.
63
64   @type defaults_dict: dict
65   @param defaults_dict: dictionary holding the default values
66   @type custom_dict: dict
67   @param custom_dict: dictionary holding customized value
68   @type skip_keys: list
69   @param skip_keys: which keys not to fill
70   @rtype: dict
71   @return: dict with the 'full' values
72
73   """
74   ret_dict = copy.deepcopy(defaults_dict)
75   ret_dict.update(custom_dict)
76   if skip_keys:
77     for k in skip_keys:
78       try:
79         del ret_dict[k]
80       except KeyError:
81         pass
82   return ret_dict
83
84
85 def FillIPolicy(default_ipolicy, custom_ipolicy, skip_keys=None):
86   """Fills an instance policy with defaults.
87
88   """
89   assert frozenset(default_ipolicy.keys()) == constants.IPOLICY_ALL_KEYS
90   ret_dict = {}
91   for key in constants.IPOLICY_ISPECS:
92     ret_dict[key] = FillDict(default_ipolicy[key],
93                              custom_ipolicy.get(key, {}),
94                              skip_keys=skip_keys)
95   # list items
96   for key in [constants.IPOLICY_DTS]:
97     ret_dict[key] = list(custom_ipolicy.get(key, default_ipolicy[key]))
98   # other items which we know we can directly copy (immutables)
99   for key in constants.IPOLICY_PARAMETERS:
100     ret_dict[key] = custom_ipolicy.get(key, default_ipolicy[key])
101
102   return ret_dict
103
104
105 def FillDiskParams(default_dparams, custom_dparams, skip_keys=None):
106   """Fills the disk parameter defaults.
107
108   @see: L{FillDict} for parameters and return value
109
110   """
111   assert frozenset(default_dparams.keys()) == constants.DISK_TEMPLATES
112
113   return dict((dt, FillDict(default_dparams[dt], custom_dparams.get(dt, {}),
114                              skip_keys=skip_keys))
115               for dt in constants.DISK_TEMPLATES)
116
117
118 def UpgradeGroupedParams(target, defaults):
119   """Update all groups for the target parameter.
120
121   @type target: dict of dicts
122   @param target: {group: {parameter: value}}
123   @type defaults: dict
124   @param defaults: default parameter values
125
126   """
127   if target is None:
128     target = {constants.PP_DEFAULT: defaults}
129   else:
130     for group in target:
131       target[group] = FillDict(defaults, target[group])
132   return target
133
134
135 def UpgradeBeParams(target):
136   """Update the be parameters dict to the new format.
137
138   @type target: dict
139   @param target: "be" parameters dict
140
141   """
142   if constants.BE_MEMORY in target:
143     memory = target[constants.BE_MEMORY]
144     target[constants.BE_MAXMEM] = memory
145     target[constants.BE_MINMEM] = memory
146     del target[constants.BE_MEMORY]
147
148
149 def UpgradeDiskParams(diskparams):
150   """Upgrade the disk parameters.
151
152   @type diskparams: dict
153   @param diskparams: disk parameters to upgrade
154   @rtype: dict
155   @return: the upgraded disk parameters dict
156
157   """
158   if not diskparams:
159     result = {}
160   else:
161     result = FillDiskParams(constants.DISK_DT_DEFAULTS, diskparams)
162
163   return result
164
165
166 def UpgradeNDParams(ndparams):
167   """Upgrade ndparams structure.
168
169   @type ndparams: dict
170   @param ndparams: disk parameters to upgrade
171   @rtype: dict
172   @return: the upgraded node parameters dict
173
174   """
175   if ndparams is None:
176     ndparams = {}
177
178   if (constants.ND_OOB_PROGRAM in ndparams and
179       ndparams[constants.ND_OOB_PROGRAM] is None):
180     # will be reset by the line below
181     del ndparams[constants.ND_OOB_PROGRAM]
182   return FillDict(constants.NDC_DEFAULTS, ndparams)
183
184
185 def MakeEmptyIPolicy():
186   """Create empty IPolicy dictionary.
187
188   """
189   return dict([
190     (constants.ISPECS_MIN, {}),
191     (constants.ISPECS_MAX, {}),
192     (constants.ISPECS_STD, {}),
193     ])
194
195
196 class ConfigObject(outils.ValidatedSlots):
197   """A generic config object.
198
199   It has the following properties:
200
201     - provides somewhat safe recursive unpickling and pickling for its classes
202     - unset attributes which are defined in slots are always returned
203       as None instead of raising an error
204
205   Classes derived from this must always declare __slots__ (we use many
206   config objects and the memory reduction is useful)
207
208   """
209   __slots__ = []
210
211   def __getattr__(self, name):
212     if name not in self.GetAllSlots():
213       raise AttributeError("Invalid object attribute %s.%s" %
214                            (type(self).__name__, name))
215     return None
216
217   def __setstate__(self, state):
218     slots = self.GetAllSlots()
219     for name in state:
220       if name in slots:
221         setattr(self, name, state[name])
222
223   def Validate(self):
224     """Validates the slots.
225
226     """
227
228   def ToDict(self):
229     """Convert to a dict holding only standard python types.
230
231     The generic routine just dumps all of this object's attributes in
232     a dict. It does not work if the class has children who are
233     ConfigObjects themselves (e.g. the nics list in an Instance), in
234     which case the object should subclass the function in order to
235     make sure all objects returned are only standard python types.
236
237     """
238     result = {}
239     for name in self.GetAllSlots():
240       value = getattr(self, name, None)
241       if value is not None:
242         result[name] = value
243     return result
244
245   __getstate__ = ToDict
246
247   @classmethod
248   def FromDict(cls, val):
249     """Create an object from a dictionary.
250
251     This generic routine takes a dict, instantiates a new instance of
252     the given class, and sets attributes based on the dict content.
253
254     As for `ToDict`, this does not work if the class has children
255     who are ConfigObjects themselves (e.g. the nics list in an
256     Instance), in which case the object should subclass the function
257     and alter the objects.
258
259     """
260     if not isinstance(val, dict):
261       raise errors.ConfigurationError("Invalid object passed to FromDict:"
262                                       " expected dict, got %s" % type(val))
263     val_str = dict([(str(k), v) for k, v in val.iteritems()])
264     obj = cls(**val_str) # pylint: disable=W0142
265     return obj
266
267   def Copy(self):
268     """Makes a deep copy of the current object and its children.
269
270     """
271     dict_form = self.ToDict()
272     clone_obj = self.__class__.FromDict(dict_form)
273     return clone_obj
274
275   def __repr__(self):
276     """Implement __repr__ for ConfigObjects."""
277     return repr(self.ToDict())
278
279   def UpgradeConfig(self):
280     """Fill defaults for missing configuration values.
281
282     This method will be called at configuration load time, and its
283     implementation will be object dependent.
284
285     """
286     pass
287
288
289 class TaggableObject(ConfigObject):
290   """An generic class supporting tags.
291
292   """
293   __slots__ = ["tags"]
294   VALID_TAG_RE = re.compile("^[\w.+*/:@-]+$")
295
296   @classmethod
297   def ValidateTag(cls, tag):
298     """Check if a tag is valid.
299
300     If the tag is invalid, an errors.TagError will be raised. The
301     function has no return value.
302
303     """
304     if not isinstance(tag, basestring):
305       raise errors.TagError("Invalid tag type (not a string)")
306     if len(tag) > constants.MAX_TAG_LEN:
307       raise errors.TagError("Tag too long (>%d characters)" %
308                             constants.MAX_TAG_LEN)
309     if not tag:
310       raise errors.TagError("Tags cannot be empty")
311     if not cls.VALID_TAG_RE.match(tag):
312       raise errors.TagError("Tag contains invalid characters")
313
314   def GetTags(self):
315     """Return the tags list.
316
317     """
318     tags = getattr(self, "tags", None)
319     if tags is None:
320       tags = self.tags = set()
321     return tags
322
323   def AddTag(self, tag):
324     """Add a new tag.
325
326     """
327     self.ValidateTag(tag)
328     tags = self.GetTags()
329     if len(tags) >= constants.MAX_TAGS_PER_OBJ:
330       raise errors.TagError("Too many tags")
331     self.GetTags().add(tag)
332
333   def RemoveTag(self, tag):
334     """Remove a tag.
335
336     """
337     self.ValidateTag(tag)
338     tags = self.GetTags()
339     try:
340       tags.remove(tag)
341     except KeyError:
342       raise errors.TagError("Tag not found")
343
344   def ToDict(self):
345     """Taggable-object-specific conversion to standard python types.
346
347     This replaces the tags set with a list.
348
349     """
350     bo = super(TaggableObject, self).ToDict()
351
352     tags = bo.get("tags", None)
353     if isinstance(tags, set):
354       bo["tags"] = list(tags)
355     return bo
356
357   @classmethod
358   def FromDict(cls, val):
359     """Custom function for instances.
360
361     """
362     obj = super(TaggableObject, cls).FromDict(val)
363     if hasattr(obj, "tags") and isinstance(obj.tags, list):
364       obj.tags = set(obj.tags)
365     return obj
366
367
368 class MasterNetworkParameters(ConfigObject):
369   """Network configuration parameters for the master
370
371   @ivar name: master name
372   @ivar ip: master IP
373   @ivar netmask: master netmask
374   @ivar netdev: master network device
375   @ivar ip_family: master IP family
376
377   """
378   __slots__ = [
379     "name",
380     "ip",
381     "netmask",
382     "netdev",
383     "ip_family",
384     ]
385
386
387 class ConfigData(ConfigObject):
388   """Top-level config object."""
389   __slots__ = [
390     "version",
391     "cluster",
392     "nodes",
393     "nodegroups",
394     "instances",
395     "networks",
396     "serial_no",
397     ] + _TIMESTAMPS
398
399   def ToDict(self):
400     """Custom function for top-level config data.
401
402     This just replaces the list of instances, nodes and the cluster
403     with standard python types.
404
405     """
406     mydict = super(ConfigData, self).ToDict()
407     mydict["cluster"] = mydict["cluster"].ToDict()
408     for key in "nodes", "instances", "nodegroups", "networks":
409       mydict[key] = outils.ContainerToDicts(mydict[key])
410
411     return mydict
412
413   @classmethod
414   def FromDict(cls, val):
415     """Custom function for top-level config data
416
417     """
418     obj = super(ConfigData, cls).FromDict(val)
419     obj.cluster = Cluster.FromDict(obj.cluster)
420     obj.nodes = outils.ContainerFromDicts(obj.nodes, dict, Node)
421     obj.instances = \
422       outils.ContainerFromDicts(obj.instances, dict, Instance)
423     obj.nodegroups = \
424       outils.ContainerFromDicts(obj.nodegroups, dict, NodeGroup)
425     obj.networks = outils.ContainerFromDicts(obj.networks, dict, Network)
426     return obj
427
428   def HasAnyDiskOfType(self, dev_type):
429     """Check if in there is at disk of the given type in the configuration.
430
431     @type dev_type: L{constants.LDS_BLOCK}
432     @param dev_type: the type to look for
433     @rtype: boolean
434     @return: boolean indicating if a disk of the given type was found or not
435
436     """
437     for instance in self.instances.values():
438       for disk in instance.disks:
439         if disk.IsBasedOnDiskType(dev_type):
440           return True
441     return False
442
443   def UpgradeConfig(self):
444     """Fill defaults for missing configuration values.
445
446     """
447     self.cluster.UpgradeConfig()
448     for node in self.nodes.values():
449       node.UpgradeConfig()
450     for instance in self.instances.values():
451       instance.UpgradeConfig()
452     if self.nodegroups is None:
453       self.nodegroups = {}
454     for nodegroup in self.nodegroups.values():
455       nodegroup.UpgradeConfig()
456     if self.cluster.drbd_usermode_helper is None:
457       # To decide if we set an helper let's check if at least one instance has
458       # a DRBD disk. This does not cover all the possible scenarios but it
459       # gives a good approximation.
460       if self.HasAnyDiskOfType(constants.LD_DRBD8):
461         self.cluster.drbd_usermode_helper = constants.DEFAULT_DRBD_HELPER
462     if self.networks is None:
463       self.networks = {}
464     for network in self.networks.values():
465       network.UpgradeConfig()
466
467
468 class NIC(ConfigObject):
469   """Config object representing a network card."""
470   __slots__ = ["mac", "ip", "network", "nicparams", "netinfo"]
471
472   @classmethod
473   def CheckParameterSyntax(cls, nicparams):
474     """Check the given parameters for validity.
475
476     @type nicparams:  dict
477     @param nicparams: dictionary with parameter names/value
478     @raise errors.ConfigurationError: when a parameter is not valid
479
480     """
481     mode = nicparams[constants.NIC_MODE]
482     if (mode not in constants.NIC_VALID_MODES and
483         mode != constants.VALUE_AUTO):
484       raise errors.ConfigurationError("Invalid NIC mode '%s'" % mode)
485
486     if (mode == constants.NIC_MODE_BRIDGED and
487         not nicparams[constants.NIC_LINK]):
488       raise errors.ConfigurationError("Missing bridged NIC link")
489
490
491 class Disk(ConfigObject):
492   """Config object representing a block device."""
493   __slots__ = ["dev_type", "logical_id", "physical_id",
494                "children", "iv_name", "size", "mode", "params"]
495
496   def CreateOnSecondary(self):
497     """Test if this device needs to be created on a secondary node."""
498     return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
499
500   def AssembleOnSecondary(self):
501     """Test if this device needs to be assembled on a secondary node."""
502     return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
503
504   def OpenOnSecondary(self):
505     """Test if this device needs to be opened on a secondary node."""
506     return self.dev_type in (constants.LD_LV,)
507
508   def StaticDevPath(self):
509     """Return the device path if this device type has a static one.
510
511     Some devices (LVM for example) live always at the same /dev/ path,
512     irrespective of their status. For such devices, we return this
513     path, for others we return None.
514
515     @warning: The path returned is not a normalized pathname; callers
516         should check that it is a valid path.
517
518     """
519     if self.dev_type == constants.LD_LV:
520       return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
521     elif self.dev_type == constants.LD_BLOCKDEV:
522       return self.logical_id[1]
523     elif self.dev_type == constants.LD_RBD:
524       return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
525     return None
526
527   def ChildrenNeeded(self):
528     """Compute the needed number of children for activation.
529
530     This method will return either -1 (all children) or a positive
531     number denoting the minimum number of children needed for
532     activation (only mirrored devices will usually return >=0).
533
534     Currently, only DRBD8 supports diskless activation (therefore we
535     return 0), for all other we keep the previous semantics and return
536     -1.
537
538     """
539     if self.dev_type == constants.LD_DRBD8:
540       return 0
541     return -1
542
543   def IsBasedOnDiskType(self, dev_type):
544     """Check if the disk or its children are based on the given type.
545
546     @type dev_type: L{constants.LDS_BLOCK}
547     @param dev_type: the type to look for
548     @rtype: boolean
549     @return: boolean indicating if a device of the given type was found or not
550
551     """
552     if self.children:
553       for child in self.children:
554         if child.IsBasedOnDiskType(dev_type):
555           return True
556     return self.dev_type == dev_type
557
558   def GetNodes(self, node):
559     """This function returns the nodes this device lives on.
560
561     Given the node on which the parent of the device lives on (or, in
562     case of a top-level device, the primary node of the devices'
563     instance), this function will return a list of nodes on which this
564     devices needs to (or can) be assembled.
565
566     """
567     if self.dev_type in [constants.LD_LV, constants.LD_FILE,
568                          constants.LD_BLOCKDEV, constants.LD_RBD,
569                          constants.LD_EXT]:
570       result = [node]
571     elif self.dev_type in constants.LDS_DRBD:
572       result = [self.logical_id[0], self.logical_id[1]]
573       if node not in result:
574         raise errors.ConfigurationError("DRBD device passed unknown node")
575     else:
576       raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
577     return result
578
579   def ComputeNodeTree(self, parent_node):
580     """Compute the node/disk tree for this disk and its children.
581
582     This method, given the node on which the parent disk lives, will
583     return the list of all (node, disk) pairs which describe the disk
584     tree in the most compact way. For example, a drbd/lvm stack
585     will be returned as (primary_node, drbd) and (secondary_node, drbd)
586     which represents all the top-level devices on the nodes.
587
588     """
589     my_nodes = self.GetNodes(parent_node)
590     result = [(node, self) for node in my_nodes]
591     if not self.children:
592       # leaf device
593       return result
594     for node in my_nodes:
595       for child in self.children:
596         child_result = child.ComputeNodeTree(node)
597         if len(child_result) == 1:
598           # child (and all its descendants) is simple, doesn't split
599           # over multiple hosts, so we don't need to describe it, our
600           # own entry for this node describes it completely
601           continue
602         else:
603           # check if child nodes differ from my nodes; note that
604           # subdisk can differ from the child itself, and be instead
605           # one of its descendants
606           for subnode, subdisk in child_result:
607             if subnode not in my_nodes:
608               result.append((subnode, subdisk))
609             # otherwise child is under our own node, so we ignore this
610             # entry (but probably the other results in the list will
611             # be different)
612     return result
613
614   def ComputeGrowth(self, amount):
615     """Compute the per-VG growth requirements.
616
617     This only works for VG-based disks.
618
619     @type amount: integer
620     @param amount: the desired increase in (user-visible) disk space
621     @rtype: dict
622     @return: a dictionary of volume-groups and the required size
623
624     """
625     if self.dev_type == constants.LD_LV:
626       return {self.logical_id[0]: amount}
627     elif self.dev_type == constants.LD_DRBD8:
628       if self.children:
629         return self.children[0].ComputeGrowth(amount)
630       else:
631         return {}
632     else:
633       # Other disk types do not require VG space
634       return {}
635
636   def RecordGrow(self, amount):
637     """Update the size of this disk after growth.
638
639     This method recurses over the disks's children and updates their
640     size correspondigly. The method needs to be kept in sync with the
641     actual algorithms from bdev.
642
643     """
644     if self.dev_type in (constants.LD_LV, constants.LD_FILE,
645                          constants.LD_RBD, constants.LD_EXT):
646       self.size += amount
647     elif self.dev_type == constants.LD_DRBD8:
648       if self.children:
649         self.children[0].RecordGrow(amount)
650       self.size += amount
651     else:
652       raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
653                                    " disk type %s" % self.dev_type)
654
655   def Update(self, size=None, mode=None):
656     """Apply changes to size and mode.
657
658     """
659     if self.dev_type == constants.LD_DRBD8:
660       if self.children:
661         self.children[0].Update(size=size, mode=mode)
662     else:
663       assert not self.children
664
665     if size is not None:
666       self.size = size
667     if mode is not None:
668       self.mode = mode
669
670   def UnsetSize(self):
671     """Sets recursively the size to zero for the disk and its children.
672
673     """
674     if self.children:
675       for child in self.children:
676         child.UnsetSize()
677     self.size = 0
678
679   def SetPhysicalID(self, target_node, nodes_ip):
680     """Convert the logical ID to the physical ID.
681
682     This is used only for drbd, which needs ip/port configuration.
683
684     The routine descends down and updates its children also, because
685     this helps when the only the top device is passed to the remote
686     node.
687
688     Arguments:
689       - target_node: the node we wish to configure for
690       - nodes_ip: a mapping of node name to ip
691
692     The target_node must exist in in nodes_ip, and must be one of the
693     nodes in the logical ID for each of the DRBD devices encountered
694     in the disk tree.
695
696     """
697     if self.children:
698       for child in self.children:
699         child.SetPhysicalID(target_node, nodes_ip)
700
701     if self.logical_id is None and self.physical_id is not None:
702       return
703     if self.dev_type in constants.LDS_DRBD:
704       pnode, snode, port, pminor, sminor, secret = self.logical_id
705       if target_node not in (pnode, snode):
706         raise errors.ConfigurationError("DRBD device not knowing node %s" %
707                                         target_node)
708       pnode_ip = nodes_ip.get(pnode, None)
709       snode_ip = nodes_ip.get(snode, None)
710       if pnode_ip is None or snode_ip is None:
711         raise errors.ConfigurationError("Can't find primary or secondary node"
712                                         " for %s" % str(self))
713       p_data = (pnode_ip, port)
714       s_data = (snode_ip, port)
715       if pnode == target_node:
716         self.physical_id = p_data + s_data + (pminor, secret)
717       else: # it must be secondary, we tested above
718         self.physical_id = s_data + p_data + (sminor, secret)
719     else:
720       self.physical_id = self.logical_id
721     return
722
723   def ToDict(self):
724     """Disk-specific conversion to standard python types.
725
726     This replaces the children lists of objects with lists of
727     standard python types.
728
729     """
730     bo = super(Disk, self).ToDict()
731
732     for attr in ("children",):
733       alist = bo.get(attr, None)
734       if alist:
735         bo[attr] = outils.ContainerToDicts(alist)
736     return bo
737
738   @classmethod
739   def FromDict(cls, val):
740     """Custom function for Disks
741
742     """
743     obj = super(Disk, cls).FromDict(val)
744     if obj.children:
745       obj.children = outils.ContainerFromDicts(obj.children, list, Disk)
746     if obj.logical_id and isinstance(obj.logical_id, list):
747       obj.logical_id = tuple(obj.logical_id)
748     if obj.physical_id and isinstance(obj.physical_id, list):
749       obj.physical_id = tuple(obj.physical_id)
750     if obj.dev_type in constants.LDS_DRBD:
751       # we need a tuple of length six here
752       if len(obj.logical_id) < 6:
753         obj.logical_id += (None,) * (6 - len(obj.logical_id))
754     return obj
755
756   def __str__(self):
757     """Custom str() formatter for disks.
758
759     """
760     if self.dev_type == constants.LD_LV:
761       val = "<LogicalVolume(/dev/%s/%s" % self.logical_id
762     elif self.dev_type in constants.LDS_DRBD:
763       node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
764       val = "<DRBD8("
765       if self.physical_id is None:
766         phy = "unconfigured"
767       else:
768         phy = ("configured as %s:%s %s:%s" %
769                (self.physical_id[0], self.physical_id[1],
770                 self.physical_id[2], self.physical_id[3]))
771
772       val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
773               (node_a, minor_a, node_b, minor_b, port, phy))
774       if self.children and self.children.count(None) == 0:
775         val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
776       else:
777         val += "no local storage"
778     else:
779       val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
780              (self.dev_type, self.logical_id, self.physical_id, self.children))
781     if self.iv_name is None:
782       val += ", not visible"
783     else:
784       val += ", visible as /dev/%s" % self.iv_name
785     if isinstance(self.size, int):
786       val += ", size=%dm)>" % self.size
787     else:
788       val += ", size='%s')>" % (self.size,)
789     return val
790
791   def Verify(self):
792     """Checks that this disk is correctly configured.
793
794     """
795     all_errors = []
796     if self.mode not in constants.DISK_ACCESS_SET:
797       all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
798     return all_errors
799
800   def UpgradeConfig(self):
801     """Fill defaults for missing configuration values.
802
803     """
804     if self.children:
805       for child in self.children:
806         child.UpgradeConfig()
807
808     # FIXME: Make this configurable in Ganeti 2.7
809     self.params = {}
810     # add here config upgrade for this disk
811
812   @staticmethod
813   def ComputeLDParams(disk_template, disk_params):
814     """Computes Logical Disk parameters from Disk Template parameters.
815
816     @type disk_template: string
817     @param disk_template: disk template, one of L{constants.DISK_TEMPLATES}
818     @type disk_params: dict
819     @param disk_params: disk template parameters;
820                         dict(template_name -> parameters
821     @rtype: list(dict)
822     @return: a list of dicts, one for each node of the disk hierarchy. Each dict
823       contains the LD parameters of the node. The tree is flattened in-order.
824
825     """
826     if disk_template not in constants.DISK_TEMPLATES:
827       raise errors.ProgrammerError("Unknown disk template %s" % disk_template)
828
829     assert disk_template in disk_params
830
831     result = list()
832     dt_params = disk_params[disk_template]
833     if disk_template == constants.DT_DRBD8:
834       result.append(FillDict(constants.DISK_LD_DEFAULTS[constants.LD_DRBD8], {
835         constants.LDP_RESYNC_RATE: dt_params[constants.DRBD_RESYNC_RATE],
836         constants.LDP_BARRIERS: dt_params[constants.DRBD_DISK_BARRIERS],
837         constants.LDP_NO_META_FLUSH: dt_params[constants.DRBD_META_BARRIERS],
838         constants.LDP_DEFAULT_METAVG: dt_params[constants.DRBD_DEFAULT_METAVG],
839         constants.LDP_DISK_CUSTOM: dt_params[constants.DRBD_DISK_CUSTOM],
840         constants.LDP_NET_CUSTOM: dt_params[constants.DRBD_NET_CUSTOM],
841         constants.LDP_DYNAMIC_RESYNC: dt_params[constants.DRBD_DYNAMIC_RESYNC],
842         constants.LDP_PLAN_AHEAD: dt_params[constants.DRBD_PLAN_AHEAD],
843         constants.LDP_FILL_TARGET: dt_params[constants.DRBD_FILL_TARGET],
844         constants.LDP_DELAY_TARGET: dt_params[constants.DRBD_DELAY_TARGET],
845         constants.LDP_MAX_RATE: dt_params[constants.DRBD_MAX_RATE],
846         constants.LDP_MIN_RATE: dt_params[constants.DRBD_MIN_RATE],
847         }))
848
849       # data LV
850       result.append(FillDict(constants.DISK_LD_DEFAULTS[constants.LD_LV], {
851         constants.LDP_STRIPES: dt_params[constants.DRBD_DATA_STRIPES],
852         }))
853
854       # metadata LV
855       result.append(FillDict(constants.DISK_LD_DEFAULTS[constants.LD_LV], {
856         constants.LDP_STRIPES: dt_params[constants.DRBD_META_STRIPES],
857         }))
858
859     elif disk_template in (constants.DT_FILE, constants.DT_SHARED_FILE):
860       result.append(constants.DISK_LD_DEFAULTS[constants.LD_FILE])
861
862     elif disk_template == constants.DT_PLAIN:
863       result.append(FillDict(constants.DISK_LD_DEFAULTS[constants.LD_LV], {
864         constants.LDP_STRIPES: dt_params[constants.LV_STRIPES],
865         }))
866
867     elif disk_template == constants.DT_BLOCK:
868       result.append(constants.DISK_LD_DEFAULTS[constants.LD_BLOCKDEV])
869
870     elif disk_template == constants.DT_RBD:
871       result.append(FillDict(constants.DISK_LD_DEFAULTS[constants.LD_RBD], {
872         constants.LDP_POOL: dt_params[constants.RBD_POOL],
873         }))
874
875     elif disk_template == constants.DT_EXT:
876       result.append(constants.DISK_LD_DEFAULTS[constants.LD_EXT])
877
878     return result
879
880
881 class InstancePolicy(ConfigObject):
882   """Config object representing instance policy limits dictionary.
883
884
885   Note that this object is not actually used in the config, it's just
886   used as a placeholder for a few functions.
887
888   """
889   @classmethod
890   def CheckParameterSyntax(cls, ipolicy, check_std):
891     """ Check the instance policy for validity.
892
893     """
894     for param in constants.ISPECS_PARAMETERS:
895       InstancePolicy.CheckISpecSyntax(ipolicy, param, check_std)
896     if constants.IPOLICY_DTS in ipolicy:
897       InstancePolicy.CheckDiskTemplates(ipolicy[constants.IPOLICY_DTS])
898     for key in constants.IPOLICY_PARAMETERS:
899       if key in ipolicy:
900         InstancePolicy.CheckParameter(key, ipolicy[key])
901     wrong_keys = frozenset(ipolicy.keys()) - constants.IPOLICY_ALL_KEYS
902     if wrong_keys:
903       raise errors.ConfigurationError("Invalid keys in ipolicy: %s" %
904                                       utils.CommaJoin(wrong_keys))
905
906   @classmethod
907   def CheckISpecSyntax(cls, ipolicy, name, check_std):
908     """Check the instance policy for validity on a given key.
909
910     We check if the instance policy makes sense for a given key, that is
911     if ipolicy[min][name] <= ipolicy[std][name] <= ipolicy[max][name].
912
913     @type ipolicy: dict
914     @param ipolicy: dictionary with min, max, std specs
915     @type name: string
916     @param name: what are the limits for
917     @type check_std: bool
918     @param check_std: Whether to check std value or just assume compliance
919     @raise errors.ConfigureError: when specs for given name are not valid
920
921     """
922     min_v = ipolicy[constants.ISPECS_MIN].get(name, 0)
923
924     if check_std:
925       std_v = ipolicy[constants.ISPECS_STD].get(name, min_v)
926       std_msg = std_v
927     else:
928       std_v = min_v
929       std_msg = "-"
930
931     max_v = ipolicy[constants.ISPECS_MAX].get(name, std_v)
932     err = ("Invalid specification of min/max/std values for %s: %s/%s/%s" %
933            (name,
934             ipolicy[constants.ISPECS_MIN].get(name, "-"),
935             ipolicy[constants.ISPECS_MAX].get(name, "-"),
936             std_msg))
937     if min_v > std_v or std_v > max_v:
938       raise errors.ConfigurationError(err)
939
940   @classmethod
941   def CheckDiskTemplates(cls, disk_templates):
942     """Checks the disk templates for validity.
943
944     """
945     if not disk_templates:
946       raise errors.ConfigurationError("Instance policy must contain" +
947                                       " at least one disk template")
948     wrong = frozenset(disk_templates).difference(constants.DISK_TEMPLATES)
949     if wrong:
950       raise errors.ConfigurationError("Invalid disk template(s) %s" %
951                                       utils.CommaJoin(wrong))
952
953   @classmethod
954   def CheckParameter(cls, key, value):
955     """Checks a parameter.
956
957     Currently we expect all parameters to be float values.
958
959     """
960     try:
961       float(value)
962     except (TypeError, ValueError), err:
963       raise errors.ConfigurationError("Invalid value for key" " '%s':"
964                                       " '%s', error: %s" % (key, value, err))
965
966
967 class Instance(TaggableObject):
968   """Config object representing an instance."""
969   __slots__ = [
970     "name",
971     "primary_node",
972     "os",
973     "hypervisor",
974     "hvparams",
975     "beparams",
976     "osparams",
977     "admin_state",
978     "nics",
979     "disks",
980     "disk_template",
981     "network_port",
982     "serial_no",
983     ] + _TIMESTAMPS + _UUID
984
985   def _ComputeSecondaryNodes(self):
986     """Compute the list of secondary nodes.
987
988     This is a simple wrapper over _ComputeAllNodes.
989
990     """
991     all_nodes = set(self._ComputeAllNodes())
992     all_nodes.discard(self.primary_node)
993     return tuple(all_nodes)
994
995   secondary_nodes = property(_ComputeSecondaryNodes, None, None,
996                              "List of names of secondary nodes")
997
998   def _ComputeAllNodes(self):
999     """Compute the list of all nodes.
1000
1001     Since the data is already there (in the drbd disks), keeping it as
1002     a separate normal attribute is redundant and if not properly
1003     synchronised can cause problems. Thus it's better to compute it
1004     dynamically.
1005
1006     """
1007     def _Helper(nodes, device):
1008       """Recursively computes nodes given a top device."""
1009       if device.dev_type in constants.LDS_DRBD:
1010         nodea, nodeb = device.logical_id[:2]
1011         nodes.add(nodea)
1012         nodes.add(nodeb)
1013       if device.children:
1014         for child in device.children:
1015           _Helper(nodes, child)
1016
1017     all_nodes = set()
1018     all_nodes.add(self.primary_node)
1019     for device in self.disks:
1020       _Helper(all_nodes, device)
1021     return tuple(all_nodes)
1022
1023   all_nodes = property(_ComputeAllNodes, None, None,
1024                        "List of names of all the nodes of the instance")
1025
1026   def MapLVsByNode(self, lvmap=None, devs=None, node=None):
1027     """Provide a mapping of nodes to LVs this instance owns.
1028
1029     This function figures out what logical volumes should belong on
1030     which nodes, recursing through a device tree.
1031
1032     @param lvmap: optional dictionary to receive the
1033         'node' : ['lv', ...] data.
1034
1035     @return: None if lvmap arg is given, otherwise, a dictionary of
1036         the form { 'nodename' : ['volume1', 'volume2', ...], ... };
1037         volumeN is of the form "vg_name/lv_name", compatible with
1038         GetVolumeList()
1039
1040     """
1041     if node is None:
1042       node = self.primary_node
1043
1044     if lvmap is None:
1045       lvmap = {
1046         node: [],
1047         }
1048       ret = lvmap
1049     else:
1050       if not node in lvmap:
1051         lvmap[node] = []
1052       ret = None
1053
1054     if not devs:
1055       devs = self.disks
1056
1057     for dev in devs:
1058       if dev.dev_type == constants.LD_LV:
1059         lvmap[node].append(dev.logical_id[0] + "/" + dev.logical_id[1])
1060
1061       elif dev.dev_type in constants.LDS_DRBD:
1062         if dev.children:
1063           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
1064           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
1065
1066       elif dev.children:
1067         self.MapLVsByNode(lvmap, dev.children, node)
1068
1069     return ret
1070
1071   def FindDisk(self, idx):
1072     """Find a disk given having a specified index.
1073
1074     This is just a wrapper that does validation of the index.
1075
1076     @type idx: int
1077     @param idx: the disk index
1078     @rtype: L{Disk}
1079     @return: the corresponding disk
1080     @raise errors.OpPrereqError: when the given index is not valid
1081
1082     """
1083     try:
1084       idx = int(idx)
1085       return self.disks[idx]
1086     except (TypeError, ValueError), err:
1087       raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err),
1088                                  errors.ECODE_INVAL)
1089     except IndexError:
1090       raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
1091                                  " 0 to %d" % (idx, len(self.disks) - 1),
1092                                  errors.ECODE_INVAL)
1093
1094   def ToDict(self):
1095     """Instance-specific conversion to standard python types.
1096
1097     This replaces the children lists of objects with lists of standard
1098     python types.
1099
1100     """
1101     bo = super(Instance, self).ToDict()
1102
1103     for attr in "nics", "disks":
1104       alist = bo.get(attr, None)
1105       if alist:
1106         nlist = outils.ContainerToDicts(alist)
1107       else:
1108         nlist = []
1109       bo[attr] = nlist
1110     return bo
1111
1112   @classmethod
1113   def FromDict(cls, val):
1114     """Custom function for instances.
1115
1116     """
1117     if "admin_state" not in val:
1118       if val.get("admin_up", False):
1119         val["admin_state"] = constants.ADMINST_UP
1120       else:
1121         val["admin_state"] = constants.ADMINST_DOWN
1122     if "admin_up" in val:
1123       del val["admin_up"]
1124     obj = super(Instance, cls).FromDict(val)
1125     obj.nics = outils.ContainerFromDicts(obj.nics, list, NIC)
1126     obj.disks = outils.ContainerFromDicts(obj.disks, list, Disk)
1127     return obj
1128
1129   def UpgradeConfig(self):
1130     """Fill defaults for missing configuration values.
1131
1132     """
1133     for nic in self.nics:
1134       nic.UpgradeConfig()
1135     for disk in self.disks:
1136       disk.UpgradeConfig()
1137     if self.hvparams:
1138       for key in constants.HVC_GLOBALS:
1139         try:
1140           del self.hvparams[key]
1141         except KeyError:
1142           pass
1143     if self.osparams is None:
1144       self.osparams = {}
1145     UpgradeBeParams(self.beparams)
1146
1147
1148 class OS(ConfigObject):
1149   """Config object representing an operating system.
1150
1151   @type supported_parameters: list
1152   @ivar supported_parameters: a list of tuples, name and description,
1153       containing the supported parameters by this OS
1154
1155   @type VARIANT_DELIM: string
1156   @cvar VARIANT_DELIM: the variant delimiter
1157
1158   """
1159   __slots__ = [
1160     "name",
1161     "path",
1162     "api_versions",
1163     "create_script",
1164     "export_script",
1165     "import_script",
1166     "rename_script",
1167     "verify_script",
1168     "supported_variants",
1169     "supported_parameters",
1170     ]
1171
1172   VARIANT_DELIM = "+"
1173
1174   @classmethod
1175   def SplitNameVariant(cls, name):
1176     """Splits the name into the proper name and variant.
1177
1178     @param name: the OS (unprocessed) name
1179     @rtype: list
1180     @return: a list of two elements; if the original name didn't
1181         contain a variant, it's returned as an empty string
1182
1183     """
1184     nv = name.split(cls.VARIANT_DELIM, 1)
1185     if len(nv) == 1:
1186       nv.append("")
1187     return nv
1188
1189   @classmethod
1190   def GetName(cls, name):
1191     """Returns the proper name of the os (without the variant).
1192
1193     @param name: the OS (unprocessed) name
1194
1195     """
1196     return cls.SplitNameVariant(name)[0]
1197
1198   @classmethod
1199   def GetVariant(cls, name):
1200     """Returns the variant the os (without the base name).
1201
1202     @param name: the OS (unprocessed) name
1203
1204     """
1205     return cls.SplitNameVariant(name)[1]
1206
1207
1208 class ExtStorage(ConfigObject):
1209   """Config object representing an External Storage Provider.
1210
1211   """
1212   __slots__ = [
1213     "name",
1214     "path",
1215     "create_script",
1216     "remove_script",
1217     "grow_script",
1218     "attach_script",
1219     "detach_script",
1220     "setinfo_script",
1221     "verify_script",
1222     "supported_parameters",
1223     ]
1224
1225
1226 class NodeHvState(ConfigObject):
1227   """Hypvervisor state on a node.
1228
1229   @ivar mem_total: Total amount of memory
1230   @ivar mem_node: Memory used by, or reserved for, the node itself (not always
1231     available)
1232   @ivar mem_hv: Memory used by hypervisor or lost due to instance allocation
1233     rounding
1234   @ivar mem_inst: Memory used by instances living on node
1235   @ivar cpu_total: Total node CPU core count
1236   @ivar cpu_node: Number of CPU cores reserved for the node itself
1237
1238   """
1239   __slots__ = [
1240     "mem_total",
1241     "mem_node",
1242     "mem_hv",
1243     "mem_inst",
1244     "cpu_total",
1245     "cpu_node",
1246     ] + _TIMESTAMPS
1247
1248
1249 class NodeDiskState(ConfigObject):
1250   """Disk state on a node.
1251
1252   """
1253   __slots__ = [
1254     "total",
1255     "reserved",
1256     "overhead",
1257     ] + _TIMESTAMPS
1258
1259
1260 class Node(TaggableObject):
1261   """Config object representing a node.
1262
1263   @ivar hv_state: Hypervisor state (e.g. number of CPUs)
1264   @ivar hv_state_static: Hypervisor state overriden by user
1265   @ivar disk_state: Disk state (e.g. free space)
1266   @ivar disk_state_static: Disk state overriden by user
1267
1268   """
1269   __slots__ = [
1270     "name",
1271     "primary_ip",
1272     "secondary_ip",
1273     "serial_no",
1274     "master_candidate",
1275     "offline",
1276     "drained",
1277     "group",
1278     "master_capable",
1279     "vm_capable",
1280     "ndparams",
1281     "powered",
1282     "hv_state",
1283     "hv_state_static",
1284     "disk_state",
1285     "disk_state_static",
1286     ] + _TIMESTAMPS + _UUID
1287
1288   def UpgradeConfig(self):
1289     """Fill defaults for missing configuration values.
1290
1291     """
1292     # pylint: disable=E0203
1293     # because these are "defined" via slots, not manually
1294     if self.master_capable is None:
1295       self.master_capable = True
1296
1297     if self.vm_capable is None:
1298       self.vm_capable = True
1299
1300     if self.ndparams is None:
1301       self.ndparams = {}
1302     # And remove any global parameter
1303     for key in constants.NDC_GLOBALS:
1304       if key in self.ndparams:
1305         logging.warning("Ignoring %s node parameter for node %s",
1306                         key, self.name)
1307         del self.ndparams[key]
1308
1309     if self.powered is None:
1310       self.powered = True
1311
1312   def ToDict(self):
1313     """Custom function for serializing.
1314
1315     """
1316     data = super(Node, self).ToDict()
1317
1318     hv_state = data.get("hv_state", None)
1319     if hv_state is not None:
1320       data["hv_state"] = outils.ContainerToDicts(hv_state)
1321
1322     disk_state = data.get("disk_state", None)
1323     if disk_state is not None:
1324       data["disk_state"] = \
1325         dict((key, outils.ContainerToDicts(value))
1326              for (key, value) in disk_state.items())
1327
1328     return data
1329
1330   @classmethod
1331   def FromDict(cls, val):
1332     """Custom function for deserializing.
1333
1334     """
1335     obj = super(Node, cls).FromDict(val)
1336
1337     if obj.hv_state is not None:
1338       obj.hv_state = \
1339         outils.ContainerFromDicts(obj.hv_state, dict, NodeHvState)
1340
1341     if obj.disk_state is not None:
1342       obj.disk_state = \
1343         dict((key, outils.ContainerFromDicts(value, dict, NodeDiskState))
1344              for (key, value) in obj.disk_state.items())
1345
1346     return obj
1347
1348
1349 class NodeGroup(TaggableObject):
1350   """Config object representing a node group."""
1351   __slots__ = [
1352     "name",
1353     "members",
1354     "ndparams",
1355     "diskparams",
1356     "ipolicy",
1357     "serial_no",
1358     "hv_state_static",
1359     "disk_state_static",
1360     "alloc_policy",
1361     "networks",
1362     ] + _TIMESTAMPS + _UUID
1363
1364   def ToDict(self):
1365     """Custom function for nodegroup.
1366
1367     This discards the members object, which gets recalculated and is only kept
1368     in memory.
1369
1370     """
1371     mydict = super(NodeGroup, self).ToDict()
1372     del mydict["members"]
1373     return mydict
1374
1375   @classmethod
1376   def FromDict(cls, val):
1377     """Custom function for nodegroup.
1378
1379     The members slot is initialized to an empty list, upon deserialization.
1380
1381     """
1382     obj = super(NodeGroup, cls).FromDict(val)
1383     obj.members = []
1384     return obj
1385
1386   def UpgradeConfig(self):
1387     """Fill defaults for missing configuration values.
1388
1389     """
1390     if self.ndparams is None:
1391       self.ndparams = {}
1392
1393     if self.serial_no is None:
1394       self.serial_no = 1
1395
1396     if self.alloc_policy is None:
1397       self.alloc_policy = constants.ALLOC_POLICY_PREFERRED
1398
1399     # We only update mtime, and not ctime, since we would not be able
1400     # to provide a correct value for creation time.
1401     if self.mtime is None:
1402       self.mtime = time.time()
1403
1404     if self.diskparams is None:
1405       self.diskparams = {}
1406     if self.ipolicy is None:
1407       self.ipolicy = MakeEmptyIPolicy()
1408
1409     if self.networks is None:
1410       self.networks = {}
1411
1412   def FillND(self, node):
1413     """Return filled out ndparams for L{objects.Node}
1414
1415     @type node: L{objects.Node}
1416     @param node: A Node object to fill
1417     @return a copy of the node's ndparams with defaults filled
1418
1419     """
1420     return self.SimpleFillND(node.ndparams)
1421
1422   def SimpleFillND(self, ndparams):
1423     """Fill a given ndparams dict with defaults.
1424
1425     @type ndparams: dict
1426     @param ndparams: the dict to fill
1427     @rtype: dict
1428     @return: a copy of the passed in ndparams with missing keys filled
1429         from the node group defaults
1430
1431     """
1432     return FillDict(self.ndparams, ndparams)
1433
1434
1435 class Cluster(TaggableObject):
1436   """Config object representing the cluster."""
1437   __slots__ = [
1438     "serial_no",
1439     "rsahostkeypub",
1440     "highest_used_port",
1441     "tcpudp_port_pool",
1442     "mac_prefix",
1443     "volume_group_name",
1444     "reserved_lvs",
1445     "drbd_usermode_helper",
1446     "default_bridge",
1447     "default_hypervisor",
1448     "master_node",
1449     "master_ip",
1450     "master_netdev",
1451     "master_netmask",
1452     "use_external_mip_script",
1453     "cluster_name",
1454     "file_storage_dir",
1455     "shared_file_storage_dir",
1456     "enabled_hypervisors",
1457     "hvparams",
1458     "ipolicy",
1459     "os_hvp",
1460     "beparams",
1461     "osparams",
1462     "nicparams",
1463     "ndparams",
1464     "diskparams",
1465     "candidate_pool_size",
1466     "modify_etc_hosts",
1467     "modify_ssh_setup",
1468     "maintain_node_health",
1469     "uid_pool",
1470     "default_iallocator",
1471     "hidden_os",
1472     "blacklisted_os",
1473     "primary_ip_family",
1474     "prealloc_wipe_disks",
1475     "hv_state_static",
1476     "disk_state_static",
1477     "enabled_storage_types",
1478     ] + _TIMESTAMPS + _UUID
1479
1480   def UpgradeConfig(self):
1481     """Fill defaults for missing configuration values.
1482
1483     """
1484     # pylint: disable=E0203
1485     # because these are "defined" via slots, not manually
1486     if self.hvparams is None:
1487       self.hvparams = constants.HVC_DEFAULTS
1488     else:
1489       for hypervisor in self.hvparams:
1490         self.hvparams[hypervisor] = FillDict(
1491             constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
1492
1493     if self.os_hvp is None:
1494       self.os_hvp = {}
1495
1496     # osparams added before 2.2
1497     if self.osparams is None:
1498       self.osparams = {}
1499
1500     self.ndparams = UpgradeNDParams(self.ndparams)
1501
1502     self.beparams = UpgradeGroupedParams(self.beparams,
1503                                          constants.BEC_DEFAULTS)
1504     for beparams_group in self.beparams:
1505       UpgradeBeParams(self.beparams[beparams_group])
1506
1507     migrate_default_bridge = not self.nicparams
1508     self.nicparams = UpgradeGroupedParams(self.nicparams,
1509                                           constants.NICC_DEFAULTS)
1510     if migrate_default_bridge:
1511       self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
1512         self.default_bridge
1513
1514     if self.modify_etc_hosts is None:
1515       self.modify_etc_hosts = True
1516
1517     if self.modify_ssh_setup is None:
1518       self.modify_ssh_setup = True
1519
1520     # default_bridge is no longer used in 2.1. The slot is left there to
1521     # support auto-upgrading. It can be removed once we decide to deprecate
1522     # upgrading straight from 2.0.
1523     if self.default_bridge is not None:
1524       self.default_bridge = None
1525
1526     # default_hypervisor is just the first enabled one in 2.1. This slot and
1527     # code can be removed once upgrading straight from 2.0 is deprecated.
1528     if self.default_hypervisor is not None:
1529       self.enabled_hypervisors = ([self.default_hypervisor] +
1530                                   [hvname for hvname in self.enabled_hypervisors
1531                                    if hvname != self.default_hypervisor])
1532       self.default_hypervisor = None
1533
1534     # maintain_node_health added after 2.1.1
1535     if self.maintain_node_health is None:
1536       self.maintain_node_health = False
1537
1538     if self.uid_pool is None:
1539       self.uid_pool = []
1540
1541     if self.default_iallocator is None:
1542       self.default_iallocator = ""
1543
1544     # reserved_lvs added before 2.2
1545     if self.reserved_lvs is None:
1546       self.reserved_lvs = []
1547
1548     # hidden and blacklisted operating systems added before 2.2.1
1549     if self.hidden_os is None:
1550       self.hidden_os = []
1551
1552     if self.blacklisted_os is None:
1553       self.blacklisted_os = []
1554
1555     # primary_ip_family added before 2.3
1556     if self.primary_ip_family is None:
1557       self.primary_ip_family = AF_INET
1558
1559     if self.master_netmask is None:
1560       ipcls = netutils.IPAddress.GetClassFromIpFamily(self.primary_ip_family)
1561       self.master_netmask = ipcls.iplen
1562
1563     if self.prealloc_wipe_disks is None:
1564       self.prealloc_wipe_disks = False
1565
1566     # shared_file_storage_dir added before 2.5
1567     if self.shared_file_storage_dir is None:
1568       self.shared_file_storage_dir = ""
1569
1570     if self.use_external_mip_script is None:
1571       self.use_external_mip_script = False
1572
1573     if self.diskparams:
1574       self.diskparams = UpgradeDiskParams(self.diskparams)
1575     else:
1576       self.diskparams = constants.DISK_DT_DEFAULTS.copy()
1577
1578     # instance policy added before 2.6
1579     if self.ipolicy is None:
1580       self.ipolicy = FillIPolicy(constants.IPOLICY_DEFAULTS, {})
1581     else:
1582       # we can either make sure to upgrade the ipolicy always, or only
1583       # do it in some corner cases (e.g. missing keys); note that this
1584       # will break any removal of keys from the ipolicy dict
1585       wrongkeys = frozenset(self.ipolicy.keys()) - constants.IPOLICY_ALL_KEYS
1586       if wrongkeys:
1587         # These keys would be silently removed by FillIPolicy()
1588         msg = ("Cluster instance policy contains spourious keys: %s" %
1589                utils.CommaJoin(wrongkeys))
1590         raise errors.ConfigurationError(msg)
1591       self.ipolicy = FillIPolicy(constants.IPOLICY_DEFAULTS, self.ipolicy)
1592
1593   @property
1594   def primary_hypervisor(self):
1595     """The first hypervisor is the primary.
1596
1597     Useful, for example, for L{Node}'s hv/disk state.
1598
1599     """
1600     return self.enabled_hypervisors[0]
1601
1602   def ToDict(self):
1603     """Custom function for cluster.
1604
1605     """
1606     mydict = super(Cluster, self).ToDict()
1607
1608     if self.tcpudp_port_pool is None:
1609       tcpudp_port_pool = []
1610     else:
1611       tcpudp_port_pool = list(self.tcpudp_port_pool)
1612
1613     mydict["tcpudp_port_pool"] = tcpudp_port_pool
1614
1615     return mydict
1616
1617   @classmethod
1618   def FromDict(cls, val):
1619     """Custom function for cluster.
1620
1621     """
1622     obj = super(Cluster, cls).FromDict(val)
1623
1624     if obj.tcpudp_port_pool is None:
1625       obj.tcpudp_port_pool = set()
1626     elif not isinstance(obj.tcpudp_port_pool, set):
1627       obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
1628
1629     return obj
1630
1631   def SimpleFillDP(self, diskparams):
1632     """Fill a given diskparams dict with cluster defaults.
1633
1634     @param diskparams: The diskparams
1635     @return: The defaults dict
1636
1637     """
1638     return FillDiskParams(self.diskparams, diskparams)
1639
1640   def GetHVDefaults(self, hypervisor, os_name=None, skip_keys=None):
1641     """Get the default hypervisor parameters for the cluster.
1642
1643     @param hypervisor: the hypervisor name
1644     @param os_name: if specified, we'll also update the defaults for this OS
1645     @param skip_keys: if passed, list of keys not to use
1646     @return: the defaults dict
1647
1648     """
1649     if skip_keys is None:
1650       skip_keys = []
1651
1652     fill_stack = [self.hvparams.get(hypervisor, {})]
1653     if os_name is not None:
1654       os_hvp = self.os_hvp.get(os_name, {}).get(hypervisor, {})
1655       fill_stack.append(os_hvp)
1656
1657     ret_dict = {}
1658     for o_dict in fill_stack:
1659       ret_dict = FillDict(ret_dict, o_dict, skip_keys=skip_keys)
1660
1661     return ret_dict
1662
1663   def SimpleFillHV(self, hv_name, os_name, hvparams, skip_globals=False):
1664     """Fill a given hvparams dict with cluster defaults.
1665
1666     @type hv_name: string
1667     @param hv_name: the hypervisor to use
1668     @type os_name: string
1669     @param os_name: the OS to use for overriding the hypervisor defaults
1670     @type skip_globals: boolean
1671     @param skip_globals: if True, the global hypervisor parameters will
1672         not be filled
1673     @rtype: dict
1674     @return: a copy of the given hvparams with missing keys filled from
1675         the cluster defaults
1676
1677     """
1678     if skip_globals:
1679       skip_keys = constants.HVC_GLOBALS
1680     else:
1681       skip_keys = []
1682
1683     def_dict = self.GetHVDefaults(hv_name, os_name, skip_keys=skip_keys)
1684     return FillDict(def_dict, hvparams, skip_keys=skip_keys)
1685
1686   def FillHV(self, instance, skip_globals=False):
1687     """Fill an instance's hvparams dict with cluster defaults.
1688
1689     @type instance: L{objects.Instance}
1690     @param instance: the instance parameter to fill
1691     @type skip_globals: boolean
1692     @param skip_globals: if True, the global hypervisor parameters will
1693         not be filled
1694     @rtype: dict
1695     @return: a copy of the instance's hvparams with missing keys filled from
1696         the cluster defaults
1697
1698     """
1699     return self.SimpleFillHV(instance.hypervisor, instance.os,
1700                              instance.hvparams, skip_globals)
1701
1702   def SimpleFillBE(self, beparams):
1703     """Fill a given beparams dict with cluster defaults.
1704
1705     @type beparams: dict
1706     @param beparams: the dict to fill
1707     @rtype: dict
1708     @return: a copy of the passed in beparams with missing keys filled
1709         from the cluster defaults
1710
1711     """
1712     return FillDict(self.beparams.get(constants.PP_DEFAULT, {}), beparams)
1713
1714   def FillBE(self, instance):
1715     """Fill an instance's beparams dict with cluster defaults.
1716
1717     @type instance: L{objects.Instance}
1718     @param instance: the instance parameter to fill
1719     @rtype: dict
1720     @return: a copy of the instance's beparams with missing keys filled from
1721         the cluster defaults
1722
1723     """
1724     return self.SimpleFillBE(instance.beparams)
1725
1726   def SimpleFillNIC(self, nicparams):
1727     """Fill a given nicparams dict with cluster defaults.
1728
1729     @type nicparams: dict
1730     @param nicparams: the dict to fill
1731     @rtype: dict
1732     @return: a copy of the passed in nicparams with missing keys filled
1733         from the cluster defaults
1734
1735     """
1736     return FillDict(self.nicparams.get(constants.PP_DEFAULT, {}), nicparams)
1737
1738   def SimpleFillOS(self, os_name, os_params):
1739     """Fill an instance's osparams dict with cluster defaults.
1740
1741     @type os_name: string
1742     @param os_name: the OS name to use
1743     @type os_params: dict
1744     @param os_params: the dict to fill with default values
1745     @rtype: dict
1746     @return: a copy of the instance's osparams with missing keys filled from
1747         the cluster defaults
1748
1749     """
1750     name_only = os_name.split("+", 1)[0]
1751     # base OS
1752     result = self.osparams.get(name_only, {})
1753     # OS with variant
1754     result = FillDict(result, self.osparams.get(os_name, {}))
1755     # specified params
1756     return FillDict(result, os_params)
1757
1758   @staticmethod
1759   def SimpleFillHvState(hv_state):
1760     """Fill an hv_state sub dict with cluster defaults.
1761
1762     """
1763     return FillDict(constants.HVST_DEFAULTS, hv_state)
1764
1765   @staticmethod
1766   def SimpleFillDiskState(disk_state):
1767     """Fill an disk_state sub dict with cluster defaults.
1768
1769     """
1770     return FillDict(constants.DS_DEFAULTS, disk_state)
1771
1772   def FillND(self, node, nodegroup):
1773     """Return filled out ndparams for L{objects.NodeGroup} and L{objects.Node}
1774
1775     @type node: L{objects.Node}
1776     @param node: A Node object to fill
1777     @type nodegroup: L{objects.NodeGroup}
1778     @param nodegroup: A Node object to fill
1779     @return a copy of the node's ndparams with defaults filled
1780
1781     """
1782     return self.SimpleFillND(nodegroup.FillND(node))
1783
1784   def SimpleFillND(self, ndparams):
1785     """Fill a given ndparams dict with defaults.
1786
1787     @type ndparams: dict
1788     @param ndparams: the dict to fill
1789     @rtype: dict
1790     @return: a copy of the passed in ndparams with missing keys filled
1791         from the cluster defaults
1792
1793     """
1794     return FillDict(self.ndparams, ndparams)
1795
1796   def SimpleFillIPolicy(self, ipolicy):
1797     """ Fill instance policy dict with defaults.
1798
1799     @type ipolicy: dict
1800     @param ipolicy: the dict to fill
1801     @rtype: dict
1802     @return: a copy of passed ipolicy with missing keys filled from
1803       the cluster defaults
1804
1805     """
1806     return FillIPolicy(self.ipolicy, ipolicy)
1807
1808
1809 class BlockDevStatus(ConfigObject):
1810   """Config object representing the status of a block device."""
1811   __slots__ = [
1812     "dev_path",
1813     "major",
1814     "minor",
1815     "sync_percent",
1816     "estimated_time",
1817     "is_degraded",
1818     "ldisk_status",
1819     ]
1820
1821
1822 class ImportExportStatus(ConfigObject):
1823   """Config object representing the status of an import or export."""
1824   __slots__ = [
1825     "recent_output",
1826     "listen_port",
1827     "connected",
1828     "progress_mbytes",
1829     "progress_throughput",
1830     "progress_eta",
1831     "progress_percent",
1832     "exit_status",
1833     "error_message",
1834     ] + _TIMESTAMPS
1835
1836
1837 class ImportExportOptions(ConfigObject):
1838   """Options for import/export daemon
1839
1840   @ivar key_name: X509 key name (None for cluster certificate)
1841   @ivar ca_pem: Remote peer CA in PEM format (None for cluster certificate)
1842   @ivar compress: Compression method (one of L{constants.IEC_ALL})
1843   @ivar magic: Used to ensure the connection goes to the right disk
1844   @ivar ipv6: Whether to use IPv6
1845   @ivar connect_timeout: Number of seconds for establishing connection
1846
1847   """
1848   __slots__ = [
1849     "key_name",
1850     "ca_pem",
1851     "compress",
1852     "magic",
1853     "ipv6",
1854     "connect_timeout",
1855     ]
1856
1857
1858 class ConfdRequest(ConfigObject):
1859   """Object holding a confd request.
1860
1861   @ivar protocol: confd protocol version
1862   @ivar type: confd query type
1863   @ivar query: query request
1864   @ivar rsalt: requested reply salt
1865
1866   """
1867   __slots__ = [
1868     "protocol",
1869     "type",
1870     "query",
1871     "rsalt",
1872     ]
1873
1874
1875 class ConfdReply(ConfigObject):
1876   """Object holding a confd reply.
1877
1878   @ivar protocol: confd protocol version
1879   @ivar status: reply status code (ok, error)
1880   @ivar answer: confd query reply
1881   @ivar serial: configuration serial number
1882
1883   """
1884   __slots__ = [
1885     "protocol",
1886     "status",
1887     "answer",
1888     "serial",
1889     ]
1890
1891
1892 class QueryFieldDefinition(ConfigObject):
1893   """Object holding a query field definition.
1894
1895   @ivar name: Field name
1896   @ivar title: Human-readable title
1897   @ivar kind: Field type
1898   @ivar doc: Human-readable description
1899
1900   """
1901   __slots__ = [
1902     "name",
1903     "title",
1904     "kind",
1905     "doc",
1906     ]
1907
1908
1909 class _QueryResponseBase(ConfigObject):
1910   __slots__ = [
1911     "fields",
1912     ]
1913
1914   def ToDict(self):
1915     """Custom function for serializing.
1916
1917     """
1918     mydict = super(_QueryResponseBase, self).ToDict()
1919     mydict["fields"] = outils.ContainerToDicts(mydict["fields"])
1920     return mydict
1921
1922   @classmethod
1923   def FromDict(cls, val):
1924     """Custom function for de-serializing.
1925
1926     """
1927     obj = super(_QueryResponseBase, cls).FromDict(val)
1928     obj.fields = \
1929       outils.ContainerFromDicts(obj.fields, list, QueryFieldDefinition)
1930     return obj
1931
1932
1933 class QueryResponse(_QueryResponseBase):
1934   """Object holding the response to a query.
1935
1936   @ivar fields: List of L{QueryFieldDefinition} objects
1937   @ivar data: Requested data
1938
1939   """
1940   __slots__ = [
1941     "data",
1942     ]
1943
1944
1945 class QueryFieldsRequest(ConfigObject):
1946   """Object holding a request for querying available fields.
1947
1948   """
1949   __slots__ = [
1950     "what",
1951     "fields",
1952     ]
1953
1954
1955 class QueryFieldsResponse(_QueryResponseBase):
1956   """Object holding the response to a query for fields.
1957
1958   @ivar fields: List of L{QueryFieldDefinition} objects
1959
1960   """
1961   __slots__ = []
1962
1963
1964 class MigrationStatus(ConfigObject):
1965   """Object holding the status of a migration.
1966
1967   """
1968   __slots__ = [
1969     "status",
1970     "transferred_ram",
1971     "total_ram",
1972     ]
1973
1974
1975 class InstanceConsole(ConfigObject):
1976   """Object describing how to access the console of an instance.
1977
1978   """
1979   __slots__ = [
1980     "instance",
1981     "kind",
1982     "message",
1983     "host",
1984     "port",
1985     "user",
1986     "command",
1987     "display",
1988     ]
1989
1990   def Validate(self):
1991     """Validates contents of this object.
1992
1993     """
1994     assert self.kind in constants.CONS_ALL, "Unknown console type"
1995     assert self.instance, "Missing instance name"
1996     assert self.message or self.kind in [constants.CONS_SSH,
1997                                          constants.CONS_SPICE,
1998                                          constants.CONS_VNC]
1999     assert self.host or self.kind == constants.CONS_MESSAGE
2000     assert self.port or self.kind in [constants.CONS_MESSAGE,
2001                                       constants.CONS_SSH]
2002     assert self.user or self.kind in [constants.CONS_MESSAGE,
2003                                       constants.CONS_SPICE,
2004                                       constants.CONS_VNC]
2005     assert self.command or self.kind in [constants.CONS_MESSAGE,
2006                                          constants.CONS_SPICE,
2007                                          constants.CONS_VNC]
2008     assert self.display or self.kind in [constants.CONS_MESSAGE,
2009                                          constants.CONS_SPICE,
2010                                          constants.CONS_SSH]
2011     return True
2012
2013
2014 class Network(TaggableObject):
2015   """Object representing a network definition for ganeti.
2016
2017   """
2018   __slots__ = [
2019     "name",
2020     "serial_no",
2021     "mac_prefix",
2022     "network",
2023     "network6",
2024     "gateway",
2025     "gateway6",
2026     "reservations",
2027     "ext_reservations",
2028     ] + _TIMESTAMPS + _UUID
2029
2030   def HooksDict(self, prefix=""):
2031     """Export a dictionary used by hooks with a network's information.
2032
2033     @type prefix: String
2034     @param prefix: Prefix to prepend to the dict entries
2035
2036     """
2037     result = {
2038       "%sNETWORK_NAME" % prefix: self.name,
2039       "%sNETWORK_UUID" % prefix: self.uuid,
2040       "%sNETWORK_TAGS" % prefix: " ".join(self.GetTags()),
2041     }
2042     if self.network:
2043       result["%sNETWORK_SUBNET" % prefix] = self.network
2044     if self.gateway:
2045       result["%sNETWORK_GATEWAY" % prefix] = self.gateway
2046     if self.network6:
2047       result["%sNETWORK_SUBNET6" % prefix] = self.network6
2048     if self.gateway6:
2049       result["%sNETWORK_GATEWAY6" % prefix] = self.gateway6
2050     if self.mac_prefix:
2051       result["%sNETWORK_MAC_PREFIX" % prefix] = self.mac_prefix
2052
2053     return result
2054
2055   @classmethod
2056   def FromDict(cls, val):
2057     """Custom function for networks.
2058
2059     Remove deprecated network_type and family.
2060
2061     """
2062     if "network_type" in val:
2063       del val["network_type"]
2064     if "family" in val:
2065       del val["family"]
2066     obj = super(Network, cls).FromDict(val)
2067     return obj
2068
2069
2070 class SerializableConfigParser(ConfigParser.SafeConfigParser):
2071   """Simple wrapper over ConfigParse that allows serialization.
2072
2073   This class is basically ConfigParser.SafeConfigParser with two
2074   additional methods that allow it to serialize/unserialize to/from a
2075   buffer.
2076
2077   """
2078   def Dumps(self):
2079     """Dump this instance and return the string representation."""
2080     buf = StringIO()
2081     self.write(buf)
2082     return buf.getvalue()
2083
2084   @classmethod
2085   def Loads(cls, data):
2086     """Load data from a string."""
2087     buf = StringIO(data)
2088     cfp = cls()
2089     cfp.readfp(buf)
2090     return cfp
2091
2092
2093 class LvmPvInfo(ConfigObject):
2094   """Information about an LVM physical volume (PV).
2095
2096   @type name: string
2097   @ivar name: name of the PV
2098   @type vg_name: string
2099   @ivar vg_name: name of the volume group containing the PV
2100   @type size: float
2101   @ivar size: size of the PV in MiB
2102   @type free: float
2103   @ivar free: free space in the PV, in MiB
2104   @type attributes: string
2105   @ivar attributes: PV attributes
2106   @type lv_list: list of strings
2107   @ivar lv_list: names of the LVs hosted on the PV
2108   """
2109   __slots__ = [
2110     "name",
2111     "vg_name",
2112     "size",
2113     "free",
2114     "attributes",
2115     "lv_list"
2116     ]
2117
2118   def IsEmpty(self):
2119     """Is this PV empty?
2120
2121     """
2122     return self.size <= (self.free + 1)
2123
2124   def IsAllocatable(self):
2125     """Is this PV allocatable?
2126
2127     """
2128     return ("a" in self.attributes)