PEP8 style fixes
[ganeti-local] / lib / objects.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Transportable objects for Ganeti.
23
24 This module provides small, mostly data-only objects which are safe to
25 pass to and from external parties.
26
27 """
28
29 # pylint: disable-msg=E0203,W0201
30
31 # E0203: Access to member %r before its definition, since we use
32 # objects.py which doesn't explicitely initialise its members
33
34 # W0201: Attribute '%s' defined outside __init__
35
36 import ConfigParser
37 import re
38 import copy
39 import time
40 from cStringIO import StringIO
41
42 from ganeti import errors
43 from ganeti import constants
44
45 from socket import AF_INET
46
47
48 __all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
49            "OS", "Node", "NodeGroup", "Cluster", "FillDict"]
50
51 _TIMESTAMPS = ["ctime", "mtime"]
52 _UUID = ["uuid"]
53
54
55 def FillDict(defaults_dict, custom_dict, skip_keys=None):
56   """Basic function to apply settings on top a default dict.
57
58   @type defaults_dict: dict
59   @param defaults_dict: dictionary holding the default values
60   @type custom_dict: dict
61   @param custom_dict: dictionary holding customized value
62   @type skip_keys: list
63   @param skip_keys: which keys not to fill
64   @rtype: dict
65   @return: dict with the 'full' values
66
67   """
68   ret_dict = copy.deepcopy(defaults_dict)
69   ret_dict.update(custom_dict)
70   if skip_keys:
71     for k in skip_keys:
72       try:
73         del ret_dict[k]
74       except KeyError:
75         pass
76   return ret_dict
77
78
79 def UpgradeGroupedParams(target, defaults):
80   """Update all groups for the target parameter.
81
82   @type target: dict of dicts
83   @param target: {group: {parameter: value}}
84   @type defaults: dict
85   @param defaults: default parameter values
86
87   """
88   if target is None:
89     target = {constants.PP_DEFAULT: defaults}
90   else:
91     for group in target:
92       target[group] = FillDict(defaults, target[group])
93   return target
94
95
96 class ConfigObject(object):
97   """A generic config object.
98
99   It has the following properties:
100
101     - provides somewhat safe recursive unpickling and pickling for its classes
102     - unset attributes which are defined in slots are always returned
103       as None instead of raising an error
104
105   Classes derived from this must always declare __slots__ (we use many
106   config objects and the memory reduction is useful)
107
108   """
109   __slots__ = []
110
111   def __init__(self, **kwargs):
112     for k, v in kwargs.iteritems():
113       setattr(self, k, v)
114
115   def __getattr__(self, name):
116     if name not in self._all_slots():
117       raise AttributeError("Invalid object attribute %s.%s" %
118                            (type(self).__name__, name))
119     return None
120
121   def __setstate__(self, state):
122     slots = self._all_slots()
123     for name in state:
124       if name in slots:
125         setattr(self, name, state[name])
126
127   @classmethod
128   def _all_slots(cls):
129     """Compute the list of all declared slots for a class.
130
131     """
132     slots = []
133     for parent in cls.__mro__:
134       slots.extend(getattr(parent, "__slots__", []))
135     return slots
136
137   def ToDict(self):
138     """Convert to a dict holding only standard python types.
139
140     The generic routine just dumps all of this object's attributes in
141     a dict. It does not work if the class has children who are
142     ConfigObjects themselves (e.g. the nics list in an Instance), in
143     which case the object should subclass the function in order to
144     make sure all objects returned are only standard python types.
145
146     """
147     result = {}
148     for name in self._all_slots():
149       value = getattr(self, name, None)
150       if value is not None:
151         result[name] = value
152     return result
153
154   __getstate__ = ToDict
155
156   @classmethod
157   def FromDict(cls, val):
158     """Create an object from a dictionary.
159
160     This generic routine takes a dict, instantiates a new instance of
161     the given class, and sets attributes based on the dict content.
162
163     As for `ToDict`, this does not work if the class has children
164     who are ConfigObjects themselves (e.g. the nics list in an
165     Instance), in which case the object should subclass the function
166     and alter the objects.
167
168     """
169     if not isinstance(val, dict):
170       raise errors.ConfigurationError("Invalid object passed to FromDict:"
171                                       " expected dict, got %s" % type(val))
172     val_str = dict([(str(k), v) for k, v in val.iteritems()])
173     obj = cls(**val_str) # pylint: disable-msg=W0142
174     return obj
175
176   @staticmethod
177   def _ContainerToDicts(container):
178     """Convert the elements of a container to standard python types.
179
180     This method converts a container with elements derived from
181     ConfigData to standard python types. If the container is a dict,
182     we don't touch the keys, only the values.
183
184     """
185     if isinstance(container, dict):
186       ret = dict([(k, v.ToDict()) for k, v in container.iteritems()])
187     elif isinstance(container, (list, tuple, set, frozenset)):
188       ret = [elem.ToDict() for elem in container]
189     else:
190       raise TypeError("Invalid type %s passed to _ContainerToDicts" %
191                       type(container))
192     return ret
193
194   @staticmethod
195   def _ContainerFromDicts(source, c_type, e_type):
196     """Convert a container from standard python types.
197
198     This method converts a container with standard python types to
199     ConfigData objects. If the container is a dict, we don't touch the
200     keys, only the values.
201
202     """
203     if not isinstance(c_type, type):
204       raise TypeError("Container type %s passed to _ContainerFromDicts is"
205                       " not a type" % type(c_type))
206     if source is None:
207       source = c_type()
208     if c_type is dict:
209       ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
210     elif c_type in (list, tuple, set, frozenset):
211       ret = c_type([e_type.FromDict(elem) for elem in source])
212     else:
213       raise TypeError("Invalid container type %s passed to"
214                       " _ContainerFromDicts" % c_type)
215     return ret
216
217   def Copy(self):
218     """Makes a deep copy of the current object and its children.
219
220     """
221     dict_form = self.ToDict()
222     clone_obj = self.__class__.FromDict(dict_form)
223     return clone_obj
224
225   def __repr__(self):
226     """Implement __repr__ for ConfigObjects."""
227     return repr(self.ToDict())
228
229   def UpgradeConfig(self):
230     """Fill defaults for missing configuration values.
231
232     This method will be called at configuration load time, and its
233     implementation will be object dependent.
234
235     """
236     pass
237
238
239 class TaggableObject(ConfigObject):
240   """An generic class supporting tags.
241
242   """
243   __slots__ = ["tags"]
244   VALID_TAG_RE = re.compile("^[\w.+*/:@-]+$")
245
246   @classmethod
247   def ValidateTag(cls, tag):
248     """Check if a tag is valid.
249
250     If the tag is invalid, an errors.TagError will be raised. The
251     function has no return value.
252
253     """
254     if not isinstance(tag, basestring):
255       raise errors.TagError("Invalid tag type (not a string)")
256     if len(tag) > constants.MAX_TAG_LEN:
257       raise errors.TagError("Tag too long (>%d characters)" %
258                             constants.MAX_TAG_LEN)
259     if not tag:
260       raise errors.TagError("Tags cannot be empty")
261     if not cls.VALID_TAG_RE.match(tag):
262       raise errors.TagError("Tag contains invalid characters")
263
264   def GetTags(self):
265     """Return the tags list.
266
267     """
268     tags = getattr(self, "tags", None)
269     if tags is None:
270       tags = self.tags = set()
271     return tags
272
273   def AddTag(self, tag):
274     """Add a new tag.
275
276     """
277     self.ValidateTag(tag)
278     tags = self.GetTags()
279     if len(tags) >= constants.MAX_TAGS_PER_OBJ:
280       raise errors.TagError("Too many tags")
281     self.GetTags().add(tag)
282
283   def RemoveTag(self, tag):
284     """Remove a tag.
285
286     """
287     self.ValidateTag(tag)
288     tags = self.GetTags()
289     try:
290       tags.remove(tag)
291     except KeyError:
292       raise errors.TagError("Tag not found")
293
294   def ToDict(self):
295     """Taggable-object-specific conversion to standard python types.
296
297     This replaces the tags set with a list.
298
299     """
300     bo = super(TaggableObject, self).ToDict()
301
302     tags = bo.get("tags", None)
303     if isinstance(tags, set):
304       bo["tags"] = list(tags)
305     return bo
306
307   @classmethod
308   def FromDict(cls, val):
309     """Custom function for instances.
310
311     """
312     obj = super(TaggableObject, cls).FromDict(val)
313     if hasattr(obj, "tags") and isinstance(obj.tags, list):
314       obj.tags = set(obj.tags)
315     return obj
316
317
318 class ConfigData(ConfigObject):
319   """Top-level config object."""
320   __slots__ = [
321     "version",
322     "cluster",
323     "nodes",
324     "nodegroups",
325     "instances",
326     "serial_no",
327     ] + _TIMESTAMPS
328
329   def ToDict(self):
330     """Custom function for top-level config data.
331
332     This just replaces the list of instances, nodes and the cluster
333     with standard python types.
334
335     """
336     mydict = super(ConfigData, self).ToDict()
337     mydict["cluster"] = mydict["cluster"].ToDict()
338     for key in "nodes", "instances", "nodegroups":
339       mydict[key] = self._ContainerToDicts(mydict[key])
340
341     return mydict
342
343   @classmethod
344   def FromDict(cls, val):
345     """Custom function for top-level config data
346
347     """
348     obj = super(ConfigData, cls).FromDict(val)
349     obj.cluster = Cluster.FromDict(obj.cluster)
350     obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
351     obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
352     obj.nodegroups = cls._ContainerFromDicts(obj.nodegroups, dict, NodeGroup)
353     return obj
354
355   def HasAnyDiskOfType(self, dev_type):
356     """Check if in there is at disk of the given type in the configuration.
357
358     @type dev_type: L{constants.LDS_BLOCK}
359     @param dev_type: the type to look for
360     @rtype: boolean
361     @return: boolean indicating if a disk of the given type was found or not
362
363     """
364     for instance in self.instances.values():
365       for disk in instance.disks:
366         if disk.IsBasedOnDiskType(dev_type):
367           return True
368     return False
369
370   def UpgradeConfig(self):
371     """Fill defaults for missing configuration values.
372
373     """
374     self.cluster.UpgradeConfig()
375     for node in self.nodes.values():
376       node.UpgradeConfig()
377     for instance in self.instances.values():
378       instance.UpgradeConfig()
379     if self.nodegroups is None:
380       self.nodegroups = {}
381     for nodegroup in self.nodegroups.values():
382       nodegroup.UpgradeConfig()
383     if self.cluster.drbd_usermode_helper is None:
384       # To decide if we set an helper let's check if at least one instance has
385       # a DRBD disk. This does not cover all the possible scenarios but it
386       # gives a good approximation.
387       if self.HasAnyDiskOfType(constants.LD_DRBD8):
388         self.cluster.drbd_usermode_helper = constants.DEFAULT_DRBD_HELPER
389
390
391 class NIC(ConfigObject):
392   """Config object representing a network card."""
393   __slots__ = ["mac", "ip", "nicparams"]
394
395   @classmethod
396   def CheckParameterSyntax(cls, nicparams):
397     """Check the given parameters for validity.
398
399     @type nicparams:  dict
400     @param nicparams: dictionary with parameter names/value
401     @raise errors.ConfigurationError: when a parameter is not valid
402
403     """
404     if nicparams[constants.NIC_MODE] not in constants.NIC_VALID_MODES:
405       err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
406       raise errors.ConfigurationError(err)
407
408     if (nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED and
409         not nicparams[constants.NIC_LINK]):
410       err = "Missing bridged nic link"
411       raise errors.ConfigurationError(err)
412
413
414 class Disk(ConfigObject):
415   """Config object representing a block device."""
416   __slots__ = ["dev_type", "logical_id", "physical_id",
417                "children", "iv_name", "size", "mode"]
418
419   def CreateOnSecondary(self):
420     """Test if this device needs to be created on a secondary node."""
421     return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
422
423   def AssembleOnSecondary(self):
424     """Test if this device needs to be assembled on a secondary node."""
425     return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
426
427   def OpenOnSecondary(self):
428     """Test if this device needs to be opened on a secondary node."""
429     return self.dev_type in (constants.LD_LV,)
430
431   def StaticDevPath(self):
432     """Return the device path if this device type has a static one.
433
434     Some devices (LVM for example) live always at the same /dev/ path,
435     irrespective of their status. For such devices, we return this
436     path, for others we return None.
437
438     @warning: The path returned is not a normalized pathname; callers
439         should check that it is a valid path.
440
441     """
442     if self.dev_type == constants.LD_LV:
443       return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
444     elif self.dev_type == constants.LD_BLOCKDEV:
445       return self.logical_id[1]
446     return None
447
448   def ChildrenNeeded(self):
449     """Compute the needed number of children for activation.
450
451     This method will return either -1 (all children) or a positive
452     number denoting the minimum number of children needed for
453     activation (only mirrored devices will usually return >=0).
454
455     Currently, only DRBD8 supports diskless activation (therefore we
456     return 0), for all other we keep the previous semantics and return
457     -1.
458
459     """
460     if self.dev_type == constants.LD_DRBD8:
461       return 0
462     return -1
463
464   def IsBasedOnDiskType(self, dev_type):
465     """Check if the disk or its children are based on the given type.
466
467     @type dev_type: L{constants.LDS_BLOCK}
468     @param dev_type: the type to look for
469     @rtype: boolean
470     @return: boolean indicating if a device of the given type was found or not
471
472     """
473     if self.children:
474       for child in self.children:
475         if child.IsBasedOnDiskType(dev_type):
476           return True
477     return self.dev_type == dev_type
478
479   def GetNodes(self, node):
480     """This function returns the nodes this device lives on.
481
482     Given the node on which the parent of the device lives on (or, in
483     case of a top-level device, the primary node of the devices'
484     instance), this function will return a list of nodes on which this
485     devices needs to (or can) be assembled.
486
487     """
488     if self.dev_type in [constants.LD_LV, constants.LD_FILE,
489                          constants.LD_BLOCKDEV]:
490       result = [node]
491     elif self.dev_type in constants.LDS_DRBD:
492       result = [self.logical_id[0], self.logical_id[1]]
493       if node not in result:
494         raise errors.ConfigurationError("DRBD device passed unknown node")
495     else:
496       raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
497     return result
498
499   def ComputeNodeTree(self, parent_node):
500     """Compute the node/disk tree for this disk and its children.
501
502     This method, given the node on which the parent disk lives, will
503     return the list of all (node, disk) pairs which describe the disk
504     tree in the most compact way. For example, a drbd/lvm stack
505     will be returned as (primary_node, drbd) and (secondary_node, drbd)
506     which represents all the top-level devices on the nodes.
507
508     """
509     my_nodes = self.GetNodes(parent_node)
510     result = [(node, self) for node in my_nodes]
511     if not self.children:
512       # leaf device
513       return result
514     for node in my_nodes:
515       for child in self.children:
516         child_result = child.ComputeNodeTree(node)
517         if len(child_result) == 1:
518           # child (and all its descendants) is simple, doesn't split
519           # over multiple hosts, so we don't need to describe it, our
520           # own entry for this node describes it completely
521           continue
522         else:
523           # check if child nodes differ from my nodes; note that
524           # subdisk can differ from the child itself, and be instead
525           # one of its descendants
526           for subnode, subdisk in child_result:
527             if subnode not in my_nodes:
528               result.append((subnode, subdisk))
529             # otherwise child is under our own node, so we ignore this
530             # entry (but probably the other results in the list will
531             # be different)
532     return result
533
534   def ComputeGrowth(self, amount):
535     """Compute the per-VG growth requirements.
536
537     This only works for VG-based disks.
538
539     @type amount: integer
540     @param amount: the desired increase in (user-visible) disk space
541     @rtype: dict
542     @return: a dictionary of volume-groups and the required size
543
544     """
545     if self.dev_type == constants.LD_LV:
546       return {self.logical_id[0]: amount}
547     elif self.dev_type == constants.LD_DRBD8:
548       if self.children:
549         return self.children[0].ComputeGrowth(amount)
550       else:
551         return {}
552     else:
553       # Other disk types do not require VG space
554       return {}
555
556   def RecordGrow(self, amount):
557     """Update the size of this disk after growth.
558
559     This method recurses over the disks's children and updates their
560     size correspondigly. The method needs to be kept in sync with the
561     actual algorithms from bdev.
562
563     """
564     if self.dev_type in (constants.LD_LV, constants.LD_FILE):
565       self.size += amount
566     elif self.dev_type == constants.LD_DRBD8:
567       if self.children:
568         self.children[0].RecordGrow(amount)
569       self.size += amount
570     else:
571       raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
572                                    " disk type %s" % self.dev_type)
573
574   def UnsetSize(self):
575     """Sets recursively the size to zero for the disk and its children.
576
577     """
578     if self.children:
579       for child in self.children:
580         child.UnsetSize()
581     self.size = 0
582
583   def SetPhysicalID(self, target_node, nodes_ip):
584     """Convert the logical ID to the physical ID.
585
586     This is used only for drbd, which needs ip/port configuration.
587
588     The routine descends down and updates its children also, because
589     this helps when the only the top device is passed to the remote
590     node.
591
592     Arguments:
593       - target_node: the node we wish to configure for
594       - nodes_ip: a mapping of node name to ip
595
596     The target_node must exist in in nodes_ip, and must be one of the
597     nodes in the logical ID for each of the DRBD devices encountered
598     in the disk tree.
599
600     """
601     if self.children:
602       for child in self.children:
603         child.SetPhysicalID(target_node, nodes_ip)
604
605     if self.logical_id is None and self.physical_id is not None:
606       return
607     if self.dev_type in constants.LDS_DRBD:
608       pnode, snode, port, pminor, sminor, secret = self.logical_id
609       if target_node not in (pnode, snode):
610         raise errors.ConfigurationError("DRBD device not knowing node %s" %
611                                         target_node)
612       pnode_ip = nodes_ip.get(pnode, None)
613       snode_ip = nodes_ip.get(snode, None)
614       if pnode_ip is None or snode_ip is None:
615         raise errors.ConfigurationError("Can't find primary or secondary node"
616                                         " for %s" % str(self))
617       p_data = (pnode_ip, port)
618       s_data = (snode_ip, port)
619       if pnode == target_node:
620         self.physical_id = p_data + s_data + (pminor, secret)
621       else: # it must be secondary, we tested above
622         self.physical_id = s_data + p_data + (sminor, secret)
623     else:
624       self.physical_id = self.logical_id
625     return
626
627   def ToDict(self):
628     """Disk-specific conversion to standard python types.
629
630     This replaces the children lists of objects with lists of
631     standard python types.
632
633     """
634     bo = super(Disk, self).ToDict()
635
636     for attr in ("children",):
637       alist = bo.get(attr, None)
638       if alist:
639         bo[attr] = self._ContainerToDicts(alist)
640     return bo
641
642   @classmethod
643   def FromDict(cls, val):
644     """Custom function for Disks
645
646     """
647     obj = super(Disk, cls).FromDict(val)
648     if obj.children:
649       obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
650     if obj.logical_id and isinstance(obj.logical_id, list):
651       obj.logical_id = tuple(obj.logical_id)
652     if obj.physical_id and isinstance(obj.physical_id, list):
653       obj.physical_id = tuple(obj.physical_id)
654     if obj.dev_type in constants.LDS_DRBD:
655       # we need a tuple of length six here
656       if len(obj.logical_id) < 6:
657         obj.logical_id += (None,) * (6 - len(obj.logical_id))
658     return obj
659
660   def __str__(self):
661     """Custom str() formatter for disks.
662
663     """
664     if self.dev_type == constants.LD_LV:
665       val = "<LogicalVolume(/dev/%s/%s" % self.logical_id
666     elif self.dev_type in constants.LDS_DRBD:
667       node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
668       val = "<DRBD8("
669       if self.physical_id is None:
670         phy = "unconfigured"
671       else:
672         phy = ("configured as %s:%s %s:%s" %
673                (self.physical_id[0], self.physical_id[1],
674                 self.physical_id[2], self.physical_id[3]))
675
676       val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
677               (node_a, minor_a, node_b, minor_b, port, phy))
678       if self.children and self.children.count(None) == 0:
679         val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
680       else:
681         val += "no local storage"
682     else:
683       val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
684              (self.dev_type, self.logical_id, self.physical_id, self.children))
685     if self.iv_name is None:
686       val += ", not visible"
687     else:
688       val += ", visible as /dev/%s" % self.iv_name
689     if isinstance(self.size, int):
690       val += ", size=%dm)>" % self.size
691     else:
692       val += ", size='%s')>" % (self.size,)
693     return val
694
695   def Verify(self):
696     """Checks that this disk is correctly configured.
697
698     """
699     all_errors = []
700     if self.mode not in constants.DISK_ACCESS_SET:
701       all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
702     return all_errors
703
704   def UpgradeConfig(self):
705     """Fill defaults for missing configuration values.
706
707     """
708     if self.children:
709       for child in self.children:
710         child.UpgradeConfig()
711     # add here config upgrade for this disk
712
713
714 class Instance(TaggableObject):
715   """Config object representing an instance."""
716   __slots__ = [
717     "name",
718     "primary_node",
719     "os",
720     "hypervisor",
721     "hvparams",
722     "beparams",
723     "osparams",
724     "admin_up",
725     "nics",
726     "disks",
727     "disk_template",
728     "network_port",
729     "serial_no",
730     ] + _TIMESTAMPS + _UUID
731
732   def _ComputeSecondaryNodes(self):
733     """Compute the list of secondary nodes.
734
735     This is a simple wrapper over _ComputeAllNodes.
736
737     """
738     all_nodes = set(self._ComputeAllNodes())
739     all_nodes.discard(self.primary_node)
740     return tuple(all_nodes)
741
742   secondary_nodes = property(_ComputeSecondaryNodes, None, None,
743                              "List of secondary nodes")
744
745   def _ComputeAllNodes(self):
746     """Compute the list of all nodes.
747
748     Since the data is already there (in the drbd disks), keeping it as
749     a separate normal attribute is redundant and if not properly
750     synchronised can cause problems. Thus it's better to compute it
751     dynamically.
752
753     """
754     def _Helper(nodes, device):
755       """Recursively computes nodes given a top device."""
756       if device.dev_type in constants.LDS_DRBD:
757         nodea, nodeb = device.logical_id[:2]
758         nodes.add(nodea)
759         nodes.add(nodeb)
760       if device.children:
761         for child in device.children:
762           _Helper(nodes, child)
763
764     all_nodes = set()
765     all_nodes.add(self.primary_node)
766     for device in self.disks:
767       _Helper(all_nodes, device)
768     return tuple(all_nodes)
769
770   all_nodes = property(_ComputeAllNodes, None, None,
771                        "List of all nodes of the instance")
772
773   def MapLVsByNode(self, lvmap=None, devs=None, node=None):
774     """Provide a mapping of nodes to LVs this instance owns.
775
776     This function figures out what logical volumes should belong on
777     which nodes, recursing through a device tree.
778
779     @param lvmap: optional dictionary to receive the
780         'node' : ['lv', ...] data.
781
782     @return: None if lvmap arg is given, otherwise, a dictionary of
783         the form { 'nodename' : ['volume1', 'volume2', ...], ... };
784         volumeN is of the form "vg_name/lv_name", compatible with
785         GetVolumeList()
786
787     """
788     if node == None:
789       node = self.primary_node
790
791     if lvmap is None:
792       lvmap = {
793         node: [],
794         }
795       ret = lvmap
796     else:
797       if not node in lvmap:
798         lvmap[node] = []
799       ret = None
800
801     if not devs:
802       devs = self.disks
803
804     for dev in devs:
805       if dev.dev_type == constants.LD_LV:
806         lvmap[node].append(dev.logical_id[0] + "/" + dev.logical_id[1])
807
808       elif dev.dev_type in constants.LDS_DRBD:
809         if dev.children:
810           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
811           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
812
813       elif dev.children:
814         self.MapLVsByNode(lvmap, dev.children, node)
815
816     return ret
817
818   def FindDisk(self, idx):
819     """Find a disk given having a specified index.
820
821     This is just a wrapper that does validation of the index.
822
823     @type idx: int
824     @param idx: the disk index
825     @rtype: L{Disk}
826     @return: the corresponding disk
827     @raise errors.OpPrereqError: when the given index is not valid
828
829     """
830     try:
831       idx = int(idx)
832       return self.disks[idx]
833     except (TypeError, ValueError), err:
834       raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err),
835                                  errors.ECODE_INVAL)
836     except IndexError:
837       raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
838                                  " 0 to %d" % (idx, len(self.disks) - 1),
839                                  errors.ECODE_INVAL)
840
841   def ToDict(self):
842     """Instance-specific conversion to standard python types.
843
844     This replaces the children lists of objects with lists of standard
845     python types.
846
847     """
848     bo = super(Instance, self).ToDict()
849
850     for attr in "nics", "disks":
851       alist = bo.get(attr, None)
852       if alist:
853         nlist = self._ContainerToDicts(alist)
854       else:
855         nlist = []
856       bo[attr] = nlist
857     return bo
858
859   @classmethod
860   def FromDict(cls, val):
861     """Custom function for instances.
862
863     """
864     obj = super(Instance, cls).FromDict(val)
865     obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
866     obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
867     return obj
868
869   def UpgradeConfig(self):
870     """Fill defaults for missing configuration values.
871
872     """
873     for nic in self.nics:
874       nic.UpgradeConfig()
875     for disk in self.disks:
876       disk.UpgradeConfig()
877     if self.hvparams:
878       for key in constants.HVC_GLOBALS:
879         try:
880           del self.hvparams[key]
881         except KeyError:
882           pass
883     if self.osparams is None:
884       self.osparams = {}
885
886
887 class OS(ConfigObject):
888   """Config object representing an operating system.
889
890   @type supported_parameters: list
891   @ivar supported_parameters: a list of tuples, name and description,
892       containing the supported parameters by this OS
893
894   @type VARIANT_DELIM: string
895   @cvar VARIANT_DELIM: the variant delimiter
896
897   """
898   __slots__ = [
899     "name",
900     "path",
901     "api_versions",
902     "create_script",
903     "export_script",
904     "import_script",
905     "rename_script",
906     "verify_script",
907     "supported_variants",
908     "supported_parameters",
909     ]
910
911   VARIANT_DELIM = "+"
912
913   @classmethod
914   def SplitNameVariant(cls, name):
915     """Splits the name into the proper name and variant.
916
917     @param name: the OS (unprocessed) name
918     @rtype: list
919     @return: a list of two elements; if the original name didn't
920         contain a variant, it's returned as an empty string
921
922     """
923     nv = name.split(cls.VARIANT_DELIM, 1)
924     if len(nv) == 1:
925       nv.append("")
926     return nv
927
928   @classmethod
929   def GetName(cls, name):
930     """Returns the proper name of the os (without the variant).
931
932     @param name: the OS (unprocessed) name
933
934     """
935     return cls.SplitNameVariant(name)[0]
936
937   @classmethod
938   def GetVariant(cls, name):
939     """Returns the variant the os (without the base name).
940
941     @param name: the OS (unprocessed) name
942
943     """
944     return cls.SplitNameVariant(name)[1]
945
946
947 class Node(TaggableObject):
948   """Config object representing a node."""
949   __slots__ = [
950     "name",
951     "primary_ip",
952     "secondary_ip",
953     "serial_no",
954     "master_candidate",
955     "offline",
956     "drained",
957     "group",
958     "master_capable",
959     "vm_capable",
960     "ndparams",
961     "powered",
962     ] + _TIMESTAMPS + _UUID
963
964   def UpgradeConfig(self):
965     """Fill defaults for missing configuration values.
966
967     """
968     # pylint: disable-msg=E0203
969     # because these are "defined" via slots, not manually
970     if self.master_capable is None:
971       self.master_capable = True
972
973     if self.vm_capable is None:
974       self.vm_capable = True
975
976     if self.ndparams is None:
977       self.ndparams = {}
978
979     if self.powered is None:
980       self.powered = True
981
982
983 class NodeGroup(TaggableObject):
984   """Config object representing a node group."""
985   __slots__ = [
986     "name",
987     "members",
988     "ndparams",
989     "serial_no",
990     "alloc_policy",
991     ] + _TIMESTAMPS + _UUID
992
993   def ToDict(self):
994     """Custom function for nodegroup.
995
996     This discards the members object, which gets recalculated and is only kept
997     in memory.
998
999     """
1000     mydict = super(NodeGroup, self).ToDict()
1001     del mydict["members"]
1002     return mydict
1003
1004   @classmethod
1005   def FromDict(cls, val):
1006     """Custom function for nodegroup.
1007
1008     The members slot is initialized to an empty list, upon deserialization.
1009
1010     """
1011     obj = super(NodeGroup, cls).FromDict(val)
1012     obj.members = []
1013     return obj
1014
1015   def UpgradeConfig(self):
1016     """Fill defaults for missing configuration values.
1017
1018     """
1019     if self.ndparams is None:
1020       self.ndparams = {}
1021
1022     if self.serial_no is None:
1023       self.serial_no = 1
1024
1025     if self.alloc_policy is None:
1026       self.alloc_policy = constants.ALLOC_POLICY_PREFERRED
1027
1028     # We only update mtime, and not ctime, since we would not be able to provide
1029     # a correct value for creation time.
1030     if self.mtime is None:
1031       self.mtime = time.time()
1032
1033   def FillND(self, node):
1034     """Return filled out ndparams for L{objects.Node}
1035
1036     @type node: L{objects.Node}
1037     @param node: A Node object to fill
1038     @return a copy of the node's ndparams with defaults filled
1039
1040     """
1041     return self.SimpleFillND(node.ndparams)
1042
1043   def SimpleFillND(self, ndparams):
1044     """Fill a given ndparams dict with defaults.
1045
1046     @type ndparams: dict
1047     @param ndparams: the dict to fill
1048     @rtype: dict
1049     @return: a copy of the passed in ndparams with missing keys filled
1050         from the node group defaults
1051
1052     """
1053     return FillDict(self.ndparams, ndparams)
1054
1055
1056 class Cluster(TaggableObject):
1057   """Config object representing the cluster."""
1058   __slots__ = [
1059     "serial_no",
1060     "rsahostkeypub",
1061     "highest_used_port",
1062     "tcpudp_port_pool",
1063     "mac_prefix",
1064     "volume_group_name",
1065     "reserved_lvs",
1066     "drbd_usermode_helper",
1067     "default_bridge",
1068     "default_hypervisor",
1069     "master_node",
1070     "master_ip",
1071     "master_netdev",
1072     "cluster_name",
1073     "file_storage_dir",
1074     "shared_file_storage_dir",
1075     "enabled_hypervisors",
1076     "hvparams",
1077     "os_hvp",
1078     "beparams",
1079     "osparams",
1080     "nicparams",
1081     "ndparams",
1082     "candidate_pool_size",
1083     "modify_etc_hosts",
1084     "modify_ssh_setup",
1085     "maintain_node_health",
1086     "uid_pool",
1087     "default_iallocator",
1088     "hidden_os",
1089     "blacklisted_os",
1090     "primary_ip_family",
1091     "prealloc_wipe_disks",
1092     ] + _TIMESTAMPS + _UUID
1093
1094   def UpgradeConfig(self):
1095     """Fill defaults for missing configuration values.
1096
1097     """
1098     # pylint: disable-msg=E0203
1099     # because these are "defined" via slots, not manually
1100     if self.hvparams is None:
1101       self.hvparams = constants.HVC_DEFAULTS
1102     else:
1103       for hypervisor in self.hvparams:
1104         self.hvparams[hypervisor] = FillDict(
1105             constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
1106
1107     if self.os_hvp is None:
1108       self.os_hvp = {}
1109
1110     # osparams added before 2.2
1111     if self.osparams is None:
1112       self.osparams = {}
1113
1114     if self.ndparams is None:
1115       self.ndparams = constants.NDC_DEFAULTS
1116
1117     self.beparams = UpgradeGroupedParams(self.beparams,
1118                                          constants.BEC_DEFAULTS)
1119     migrate_default_bridge = not self.nicparams
1120     self.nicparams = UpgradeGroupedParams(self.nicparams,
1121                                           constants.NICC_DEFAULTS)
1122     if migrate_default_bridge:
1123       self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
1124         self.default_bridge
1125
1126     if self.modify_etc_hosts is None:
1127       self.modify_etc_hosts = True
1128
1129     if self.modify_ssh_setup is None:
1130       self.modify_ssh_setup = True
1131
1132     # default_bridge is no longer used in 2.1. The slot is left there to
1133     # support auto-upgrading. It can be removed once we decide to deprecate
1134     # upgrading straight from 2.0.
1135     if self.default_bridge is not None:
1136       self.default_bridge = None
1137
1138     # default_hypervisor is just the first enabled one in 2.1. This slot and
1139     # code can be removed once upgrading straight from 2.0 is deprecated.
1140     if self.default_hypervisor is not None:
1141       self.enabled_hypervisors = ([self.default_hypervisor] +
1142         [hvname for hvname in self.enabled_hypervisors
1143          if hvname != self.default_hypervisor])
1144       self.default_hypervisor = None
1145
1146     # maintain_node_health added after 2.1.1
1147     if self.maintain_node_health is None:
1148       self.maintain_node_health = False
1149
1150     if self.uid_pool is None:
1151       self.uid_pool = []
1152
1153     if self.default_iallocator is None:
1154       self.default_iallocator = ""
1155
1156     # reserved_lvs added before 2.2
1157     if self.reserved_lvs is None:
1158       self.reserved_lvs = []
1159
1160     # hidden and blacklisted operating systems added before 2.2.1
1161     if self.hidden_os is None:
1162       self.hidden_os = []
1163
1164     if self.blacklisted_os is None:
1165       self.blacklisted_os = []
1166
1167     # primary_ip_family added before 2.3
1168     if self.primary_ip_family is None:
1169       self.primary_ip_family = AF_INET
1170
1171     if self.prealloc_wipe_disks is None:
1172       self.prealloc_wipe_disks = False
1173
1174     # shared_file_storage_dir added before 2.5
1175     if self.shared_file_storage_dir is None:
1176       self.shared_file_storage_dir = ""
1177
1178   def ToDict(self):
1179     """Custom function for cluster.
1180
1181     """
1182     mydict = super(Cluster, self).ToDict()
1183     mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
1184     return mydict
1185
1186   @classmethod
1187   def FromDict(cls, val):
1188     """Custom function for cluster.
1189
1190     """
1191     obj = super(Cluster, cls).FromDict(val)
1192     if not isinstance(obj.tcpudp_port_pool, set):
1193       obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
1194     return obj
1195
1196   def GetHVDefaults(self, hypervisor, os_name=None, skip_keys=None):
1197     """Get the default hypervisor parameters for the cluster.
1198
1199     @param hypervisor: the hypervisor name
1200     @param os_name: if specified, we'll also update the defaults for this OS
1201     @param skip_keys: if passed, list of keys not to use
1202     @return: the defaults dict
1203
1204     """
1205     if skip_keys is None:
1206       skip_keys = []
1207
1208     fill_stack = [self.hvparams.get(hypervisor, {})]
1209     if os_name is not None:
1210       os_hvp = self.os_hvp.get(os_name, {}).get(hypervisor, {})
1211       fill_stack.append(os_hvp)
1212
1213     ret_dict = {}
1214     for o_dict in fill_stack:
1215       ret_dict = FillDict(ret_dict, o_dict, skip_keys=skip_keys)
1216
1217     return ret_dict
1218
1219   def SimpleFillHV(self, hv_name, os_name, hvparams, skip_globals=False):
1220     """Fill a given hvparams dict with cluster defaults.
1221
1222     @type hv_name: string
1223     @param hv_name: the hypervisor to use
1224     @type os_name: string
1225     @param os_name: the OS to use for overriding the hypervisor defaults
1226     @type skip_globals: boolean
1227     @param skip_globals: if True, the global hypervisor parameters will
1228         not be filled
1229     @rtype: dict
1230     @return: a copy of the given hvparams with missing keys filled from
1231         the cluster defaults
1232
1233     """
1234     if skip_globals:
1235       skip_keys = constants.HVC_GLOBALS
1236     else:
1237       skip_keys = []
1238
1239     def_dict = self.GetHVDefaults(hv_name, os_name, skip_keys=skip_keys)
1240     return FillDict(def_dict, hvparams, skip_keys=skip_keys)
1241
1242   def FillHV(self, instance, skip_globals=False):
1243     """Fill an instance's hvparams dict with cluster defaults.
1244
1245     @type instance: L{objects.Instance}
1246     @param instance: the instance parameter to fill
1247     @type skip_globals: boolean
1248     @param skip_globals: if True, the global hypervisor parameters will
1249         not be filled
1250     @rtype: dict
1251     @return: a copy of the instance's hvparams with missing keys filled from
1252         the cluster defaults
1253
1254     """
1255     return self.SimpleFillHV(instance.hypervisor, instance.os,
1256                              instance.hvparams, skip_globals)
1257
1258   def SimpleFillBE(self, beparams):
1259     """Fill a given beparams dict with cluster defaults.
1260
1261     @type beparams: dict
1262     @param beparams: the dict to fill
1263     @rtype: dict
1264     @return: a copy of the passed in beparams with missing keys filled
1265         from the cluster defaults
1266
1267     """
1268     return FillDict(self.beparams.get(constants.PP_DEFAULT, {}), beparams)
1269
1270   def FillBE(self, instance):
1271     """Fill an instance's beparams dict with cluster defaults.
1272
1273     @type instance: L{objects.Instance}
1274     @param instance: the instance parameter to fill
1275     @rtype: dict
1276     @return: a copy of the instance's beparams with missing keys filled from
1277         the cluster defaults
1278
1279     """
1280     return self.SimpleFillBE(instance.beparams)
1281
1282   def SimpleFillNIC(self, nicparams):
1283     """Fill a given nicparams dict with cluster defaults.
1284
1285     @type nicparams: dict
1286     @param nicparams: the dict to fill
1287     @rtype: dict
1288     @return: a copy of the passed in nicparams with missing keys filled
1289         from the cluster defaults
1290
1291     """
1292     return FillDict(self.nicparams.get(constants.PP_DEFAULT, {}), nicparams)
1293
1294   def SimpleFillOS(self, os_name, os_params):
1295     """Fill an instance's osparams dict with cluster defaults.
1296
1297     @type os_name: string
1298     @param os_name: the OS name to use
1299     @type os_params: dict
1300     @param os_params: the dict to fill with default values
1301     @rtype: dict
1302     @return: a copy of the instance's osparams with missing keys filled from
1303         the cluster defaults
1304
1305     """
1306     name_only = os_name.split("+", 1)[0]
1307     # base OS
1308     result = self.osparams.get(name_only, {})
1309     # OS with variant
1310     result = FillDict(result, self.osparams.get(os_name, {}))
1311     # specified params
1312     return FillDict(result, os_params)
1313
1314   def FillND(self, node, nodegroup):
1315     """Return filled out ndparams for L{objects.NodeGroup} and L{objects.Node}
1316
1317     @type node: L{objects.Node}
1318     @param node: A Node object to fill
1319     @type nodegroup: L{objects.NodeGroup}
1320     @param nodegroup: A Node object to fill
1321     @return a copy of the node's ndparams with defaults filled
1322
1323     """
1324     return self.SimpleFillND(nodegroup.FillND(node))
1325
1326   def SimpleFillND(self, ndparams):
1327     """Fill a given ndparams dict with defaults.
1328
1329     @type ndparams: dict
1330     @param ndparams: the dict to fill
1331     @rtype: dict
1332     @return: a copy of the passed in ndparams with missing keys filled
1333         from the cluster defaults
1334
1335     """
1336     return FillDict(self.ndparams, ndparams)
1337
1338
1339 class BlockDevStatus(ConfigObject):
1340   """Config object representing the status of a block device."""
1341   __slots__ = [
1342     "dev_path",
1343     "major",
1344     "minor",
1345     "sync_percent",
1346     "estimated_time",
1347     "is_degraded",
1348     "ldisk_status",
1349     ]
1350
1351
1352 class ImportExportStatus(ConfigObject):
1353   """Config object representing the status of an import or export."""
1354   __slots__ = [
1355     "recent_output",
1356     "listen_port",
1357     "connected",
1358     "progress_mbytes",
1359     "progress_throughput",
1360     "progress_eta",
1361     "progress_percent",
1362     "exit_status",
1363     "error_message",
1364     ] + _TIMESTAMPS
1365
1366
1367 class ImportExportOptions(ConfigObject):
1368   """Options for import/export daemon
1369
1370   @ivar key_name: X509 key name (None for cluster certificate)
1371   @ivar ca_pem: Remote peer CA in PEM format (None for cluster certificate)
1372   @ivar compress: Compression method (one of L{constants.IEC_ALL})
1373   @ivar magic: Used to ensure the connection goes to the right disk
1374   @ivar ipv6: Whether to use IPv6
1375   @ivar connect_timeout: Number of seconds for establishing connection
1376
1377   """
1378   __slots__ = [
1379     "key_name",
1380     "ca_pem",
1381     "compress",
1382     "magic",
1383     "ipv6",
1384     "connect_timeout",
1385     ]
1386
1387
1388 class ConfdRequest(ConfigObject):
1389   """Object holding a confd request.
1390
1391   @ivar protocol: confd protocol version
1392   @ivar type: confd query type
1393   @ivar query: query request
1394   @ivar rsalt: requested reply salt
1395
1396   """
1397   __slots__ = [
1398     "protocol",
1399     "type",
1400     "query",
1401     "rsalt",
1402     ]
1403
1404
1405 class ConfdReply(ConfigObject):
1406   """Object holding a confd reply.
1407
1408   @ivar protocol: confd protocol version
1409   @ivar status: reply status code (ok, error)
1410   @ivar answer: confd query reply
1411   @ivar serial: configuration serial number
1412
1413   """
1414   __slots__ = [
1415     "protocol",
1416     "status",
1417     "answer",
1418     "serial",
1419     ]
1420
1421
1422 class QueryFieldDefinition(ConfigObject):
1423   """Object holding a query field definition.
1424
1425   @ivar name: Field name
1426   @ivar title: Human-readable title
1427   @ivar kind: Field type
1428   @ivar doc: Human-readable description
1429
1430   """
1431   __slots__ = [
1432     "name",
1433     "title",
1434     "kind",
1435     "doc",
1436     ]
1437
1438
1439 class _QueryResponseBase(ConfigObject):
1440   __slots__ = [
1441     "fields",
1442     ]
1443
1444   def ToDict(self):
1445     """Custom function for serializing.
1446
1447     """
1448     mydict = super(_QueryResponseBase, self).ToDict()
1449     mydict["fields"] = self._ContainerToDicts(mydict["fields"])
1450     return mydict
1451
1452   @classmethod
1453   def FromDict(cls, val):
1454     """Custom function for de-serializing.
1455
1456     """
1457     obj = super(_QueryResponseBase, cls).FromDict(val)
1458     obj.fields = cls._ContainerFromDicts(obj.fields, list, QueryFieldDefinition)
1459     return obj
1460
1461
1462 class QueryRequest(ConfigObject):
1463   """Object holding a query request.
1464
1465   """
1466   __slots__ = [
1467     "what",
1468     "fields",
1469     "filter",
1470     ]
1471
1472
1473 class QueryResponse(_QueryResponseBase):
1474   """Object holding the response to a query.
1475
1476   @ivar fields: List of L{QueryFieldDefinition} objects
1477   @ivar data: Requested data
1478
1479   """
1480   __slots__ = [
1481     "data",
1482     ]
1483
1484
1485 class QueryFieldsRequest(ConfigObject):
1486   """Object holding a request for querying available fields.
1487
1488   """
1489   __slots__ = [
1490     "what",
1491     "fields",
1492     ]
1493
1494
1495 class QueryFieldsResponse(_QueryResponseBase):
1496   """Object holding the response to a query for fields.
1497
1498   @ivar fields: List of L{QueryFieldDefinition} objects
1499
1500   """
1501   __slots__ = [
1502     ]
1503
1504
1505 class InstanceConsole(ConfigObject):
1506   """Object describing how to access the console of an instance.
1507
1508   """
1509   __slots__ = [
1510     "instance",
1511     "kind",
1512     "message",
1513     "host",
1514     "port",
1515     "user",
1516     "command",
1517     "display",
1518     ]
1519
1520   def Validate(self):
1521     """Validates contents of this object.
1522
1523     """
1524     assert self.kind in constants.CONS_ALL, "Unknown console type"
1525     assert self.instance, "Missing instance name"
1526     assert self.message or self.kind in [constants.CONS_SSH, constants.CONS_VNC]
1527     assert self.host or self.kind == constants.CONS_MESSAGE
1528     assert self.port or self.kind in [constants.CONS_MESSAGE,
1529                                       constants.CONS_SSH]
1530     assert self.user or self.kind in [constants.CONS_MESSAGE,
1531                                       constants.CONS_VNC]
1532     assert self.command or self.kind in [constants.CONS_MESSAGE,
1533                                          constants.CONS_VNC]
1534     assert self.display or self.kind in [constants.CONS_MESSAGE,
1535                                          constants.CONS_SSH]
1536     return True
1537
1538
1539 class SerializableConfigParser(ConfigParser.SafeConfigParser):
1540   """Simple wrapper over ConfigParse that allows serialization.
1541
1542   This class is basically ConfigParser.SafeConfigParser with two
1543   additional methods that allow it to serialize/unserialize to/from a
1544   buffer.
1545
1546   """
1547   def Dumps(self):
1548     """Dump this instance and return the string representation."""
1549     buf = StringIO()
1550     self.write(buf)
1551     return buf.getvalue()
1552
1553   @classmethod
1554   def Loads(cls, data):
1555     """Load data from a string."""
1556     buf = StringIO(data)
1557     cfp = cls()
1558     cfp.readfp(buf)
1559     return cfp