4 # Copyright (C) 2006, 2007 Google Inc.
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.
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.
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
22 """Transportable objects for Ganeti.
24 This module provides small, mostly data-only objects which are safe to
25 pass to and from external parties.
33 from cStringIO import StringIO
35 from ganeti import errors
36 from ganeti import constants
39 __all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
40 "OS", "Node", "Cluster", "FillDict"]
42 def FillDict(defaults_dict, custom_dict):
43 """Basic function to apply settings on top a default dict.
45 @type defaults_dict: dict
46 @param defaults_dict: dictionary holding the default values
47 @type custom_dict: dict
48 @param custom_dict: dictionary holding customized value
50 @return: dict with the 'full' values
53 ret_dict = copy.deepcopy(defaults_dict)
54 ret_dict.update(custom_dict)
58 def UpgradeGroupedParams(target, defaults):
59 """Update all groups for the target parameter.
61 @type target: dict of dicts
62 @param target: {group: {parameter: value}}
64 @param defaults: default parameter values
68 target = {constants.PP_DEFAULT: defaults}
71 target[group] = FillDict(defaults, target[group])
75 class ConfigObject(object):
76 """A generic config object.
78 It has the following properties:
80 - provides somewhat safe recursive unpickling and pickling for its classes
81 - unset attributes which are defined in slots are always returned
82 as None instead of raising an error
84 Classes derived from this must always declare __slots__ (we use many
85 config objects and the memory reduction is useful)
90 def __init__(self, **kwargs):
91 for k, v in kwargs.iteritems():
95 def __getattr__(self, name):
96 if name not in self.__slots__:
97 raise AttributeError("Invalid object attribute %s.%s" %
98 (type(self).__name__, name))
101 def __setitem__(self, key, value):
102 if key not in self.__slots__:
104 setattr(self, key, value)
106 def __getstate__(self):
108 for name in self.__slots__:
109 if hasattr(self, name):
110 state[name] = getattr(self, name)
113 def __setstate__(self, state):
115 if name in self.__slots__:
116 setattr(self, name, state[name])
119 """Convert to a dict holding only standard python types.
121 The generic routine just dumps all of this object's attributes in
122 a dict. It does not work if the class has children who are
123 ConfigObjects themselves (e.g. the nics list in an Instance), in
124 which case the object should subclass the function in order to
125 make sure all objects returned are only standard python types.
128 return dict([(k, getattr(self, k, None)) for k in self.__slots__])
131 def FromDict(cls, val):
132 """Create an object from a dictionary.
134 This generic routine takes a dict, instantiates a new instance of
135 the given class, and sets attributes based on the dict content.
137 As for `ToDict`, this does not work if the class has children
138 who are ConfigObjects themselves (e.g. the nics list in an
139 Instance), in which case the object should subclass the function
140 and alter the objects.
143 if not isinstance(val, dict):
144 raise errors.ConfigurationError("Invalid object passed to FromDict:"
145 " expected dict, got %s" % type(val))
146 val_str = dict([(str(k), v) for k, v in val.iteritems()])
151 def _ContainerToDicts(container):
152 """Convert the elements of a container to standard python types.
154 This method converts a container with elements derived from
155 ConfigData to standard python types. If the container is a dict,
156 we don't touch the keys, only the values.
159 if isinstance(container, dict):
160 ret = dict([(k, v.ToDict()) for k, v in container.iteritems()])
161 elif isinstance(container, (list, tuple, set, frozenset)):
162 ret = [elem.ToDict() for elem in container]
164 raise TypeError("Invalid type %s passed to _ContainerToDicts" %
169 def _ContainerFromDicts(source, c_type, e_type):
170 """Convert a container from standard python types.
172 This method converts a container with standard python types to
173 ConfigData objects. If the container is a dict, we don't touch the
174 keys, only the values.
177 if not isinstance(c_type, type):
178 raise TypeError("Container type %s passed to _ContainerFromDicts is"
179 " not a type" % type(c_type))
181 ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
182 elif c_type in (list, tuple, set, frozenset):
183 ret = c_type([e_type.FromDict(elem) for elem in source])
185 raise TypeError("Invalid container type %s passed to"
186 " _ContainerFromDicts" % c_type)
190 """Implement __repr__ for ConfigObjects."""
191 return repr(self.ToDict())
193 def UpgradeConfig(self):
194 """Fill defaults for missing configuration values.
196 This method will be called at object init time, and its implementation will
203 class TaggableObject(ConfigObject):
204 """An generic class supporting tags.
207 __slots__ = ConfigObject.__slots__ + ["tags"]
210 def ValidateTag(tag):
211 """Check if a tag is valid.
213 If the tag is invalid, an errors.TagError will be raised. The
214 function has no return value.
217 if not isinstance(tag, basestring):
218 raise errors.TagError("Invalid tag type (not a string)")
219 if len(tag) > constants.MAX_TAG_LEN:
220 raise errors.TagError("Tag too long (>%d characters)" %
221 constants.MAX_TAG_LEN)
223 raise errors.TagError("Tags cannot be empty")
224 if not re.match("^[\w.+*/:-]+$", tag):
225 raise errors.TagError("Tag contains invalid characters")
228 """Return the tags list.
231 tags = getattr(self, "tags", None)
233 tags = self.tags = set()
236 def AddTag(self, tag):
240 self.ValidateTag(tag)
241 tags = self.GetTags()
242 if len(tags) >= constants.MAX_TAGS_PER_OBJ:
243 raise errors.TagError("Too many tags")
244 self.GetTags().add(tag)
246 def RemoveTag(self, tag):
250 self.ValidateTag(tag)
251 tags = self.GetTags()
255 raise errors.TagError("Tag not found")
258 """Taggable-object-specific conversion to standard python types.
260 This replaces the tags set with a list.
263 bo = super(TaggableObject, self).ToDict()
265 tags = bo.get("tags", None)
266 if isinstance(tags, set):
267 bo["tags"] = list(tags)
271 def FromDict(cls, val):
272 """Custom function for instances.
275 obj = super(TaggableObject, cls).FromDict(val)
276 if hasattr(obj, "tags") and isinstance(obj.tags, list):
277 obj.tags = set(obj.tags)
281 class ConfigData(ConfigObject):
282 """Top-level config object."""
283 __slots__ = ["version", "cluster", "nodes", "instances", "serial_no"]
286 """Custom function for top-level config data.
288 This just replaces the list of instances, nodes and the cluster
289 with standard python types.
292 mydict = super(ConfigData, self).ToDict()
293 mydict["cluster"] = mydict["cluster"].ToDict()
294 for key in "nodes", "instances":
295 mydict[key] = self._ContainerToDicts(mydict[key])
300 def FromDict(cls, val):
301 """Custom function for top-level config data
304 obj = super(ConfigData, cls).FromDict(val)
305 obj.cluster = Cluster.FromDict(obj.cluster)
306 obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
307 obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
311 class NIC(ConfigObject):
312 """Config object representing a network card."""
313 __slots__ = ["mac", "ip", "bridge", "nicparams"]
316 def CheckParameterSyntax(cls, nicparams):
317 """Check the given parameters for validity.
319 @type nicparams: dict
320 @param nicparams: dictionary with parameter names/value
321 @raise errors.ConfigurationError: when a parameter is not valid
324 if nicparams[constants.NIC_MODE] not in constants.NIC_VALID_MODES:
325 err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
326 raise errors.ConfigurationError(err)
328 if (nicparams[constants.NIC_MODE] is constants.NIC_MODE_BRIDGED and
329 not nicparams[constants.NIC_LINK]):
330 err = "Missing bridged nic link"
331 raise errors.ConfigurationError(err)
333 def UpgradeConfig(self):
334 """Fill defaults for missing configuration values.
337 if self.nicparams is None:
339 if self.bridge is not None:
340 self.nicparams[constants.NIC_MODE] = constants.NIC_MODE_BRIDGED
341 self.nicparams[constants.NIC_LINK] = self.bridge
342 # bridge is no longer used it 2.1. The slot is left there to support
343 # upgrading, but will be removed in 2.2
344 if self.bridge is not None:
348 class Disk(ConfigObject):
349 """Config object representing a block device."""
350 __slots__ = ["dev_type", "logical_id", "physical_id",
351 "children", "iv_name", "size", "mode"]
353 def CreateOnSecondary(self):
354 """Test if this device needs to be created on a secondary node."""
355 return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
357 def AssembleOnSecondary(self):
358 """Test if this device needs to be assembled on a secondary node."""
359 return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
361 def OpenOnSecondary(self):
362 """Test if this device needs to be opened on a secondary node."""
363 return self.dev_type in (constants.LD_LV,)
365 def StaticDevPath(self):
366 """Return the device path if this device type has a static one.
368 Some devices (LVM for example) live always at the same /dev/ path,
369 irrespective of their status. For such devices, we return this
370 path, for others we return None.
373 if self.dev_type == constants.LD_LV:
374 return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
377 def ChildrenNeeded(self):
378 """Compute the needed number of children for activation.
380 This method will return either -1 (all children) or a positive
381 number denoting the minimum number of children needed for
382 activation (only mirrored devices will usually return >=0).
384 Currently, only DRBD8 supports diskless activation (therefore we
385 return 0), for all other we keep the previous semantics and return
389 if self.dev_type == constants.LD_DRBD8:
393 def GetNodes(self, node):
394 """This function returns the nodes this device lives on.
396 Given the node on which the parent of the device lives on (or, in
397 case of a top-level device, the primary node of the devices'
398 instance), this function will return a list of nodes on which this
399 devices needs to (or can) be assembled.
402 if self.dev_type in [constants.LD_LV, constants.LD_FILE]:
404 elif self.dev_type in constants.LDS_DRBD:
405 result = [self.logical_id[0], self.logical_id[1]]
406 if node not in result:
407 raise errors.ConfigurationError("DRBD device passed unknown node")
409 raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
412 def ComputeNodeTree(self, parent_node):
413 """Compute the node/disk tree for this disk and its children.
415 This method, given the node on which the parent disk lives, will
416 return the list of all (node, disk) pairs which describe the disk
417 tree in the most compact way. For example, a drbd/lvm stack
418 will be returned as (primary_node, drbd) and (secondary_node, drbd)
419 which represents all the top-level devices on the nodes.
422 my_nodes = self.GetNodes(parent_node)
423 result = [(node, self) for node in my_nodes]
424 if not self.children:
427 for node in my_nodes:
428 for child in self.children:
429 child_result = child.ComputeNodeTree(node)
430 if len(child_result) == 1:
431 # child (and all its descendants) is simple, doesn't split
432 # over multiple hosts, so we don't need to describe it, our
433 # own entry for this node describes it completely
436 # check if child nodes differ from my nodes; note that
437 # subdisk can differ from the child itself, and be instead
438 # one of its descendants
439 for subnode, subdisk in child_result:
440 if subnode not in my_nodes:
441 result.append((subnode, subdisk))
442 # otherwise child is under our own node, so we ignore this
443 # entry (but probably the other results in the list will
447 def RecordGrow(self, amount):
448 """Update the size of this disk after growth.
450 This method recurses over the disks's children and updates their
451 size correspondigly. The method needs to be kept in sync with the
452 actual algorithms from bdev.
455 if self.dev_type == constants.LD_LV:
457 elif self.dev_type == constants.LD_DRBD8:
459 self.children[0].RecordGrow(amount)
462 raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
463 " disk type %s" % self.dev_type)
465 def SetPhysicalID(self, target_node, nodes_ip):
466 """Convert the logical ID to the physical ID.
468 This is used only for drbd, which needs ip/port configuration.
470 The routine descends down and updates its children also, because
471 this helps when the only the top device is passed to the remote
475 - target_node: the node we wish to configure for
476 - nodes_ip: a mapping of node name to ip
478 The target_node must exist in in nodes_ip, and must be one of the
479 nodes in the logical ID for each of the DRBD devices encountered
484 for child in self.children:
485 child.SetPhysicalID(target_node, nodes_ip)
487 if self.logical_id is None and self.physical_id is not None:
489 if self.dev_type in constants.LDS_DRBD:
490 pnode, snode, port, pminor, sminor, secret = self.logical_id
491 if target_node not in (pnode, snode):
492 raise errors.ConfigurationError("DRBD device not knowing node %s" %
494 pnode_ip = nodes_ip.get(pnode, None)
495 snode_ip = nodes_ip.get(snode, None)
496 if pnode_ip is None or snode_ip is None:
497 raise errors.ConfigurationError("Can't find primary or secondary node"
498 " for %s" % str(self))
499 p_data = (pnode_ip, port)
500 s_data = (snode_ip, port)
501 if pnode == target_node:
502 self.physical_id = p_data + s_data + (pminor, secret)
503 else: # it must be secondary, we tested above
504 self.physical_id = s_data + p_data + (sminor, secret)
506 self.physical_id = self.logical_id
510 """Disk-specific conversion to standard python types.
512 This replaces the children lists of objects with lists of
513 standard python types.
516 bo = super(Disk, self).ToDict()
518 for attr in ("children",):
519 alist = bo.get(attr, None)
521 bo[attr] = self._ContainerToDicts(alist)
525 def FromDict(cls, val):
526 """Custom function for Disks
529 obj = super(Disk, cls).FromDict(val)
531 obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
532 if obj.logical_id and isinstance(obj.logical_id, list):
533 obj.logical_id = tuple(obj.logical_id)
534 if obj.physical_id and isinstance(obj.physical_id, list):
535 obj.physical_id = tuple(obj.physical_id)
536 if obj.dev_type in constants.LDS_DRBD:
537 # we need a tuple of length six here
538 if len(obj.logical_id) < 6:
539 obj.logical_id += (None,) * (6 - len(obj.logical_id))
543 """Custom str() formatter for disks.
546 if self.dev_type == constants.LD_LV:
547 val = "<LogicalVolume(/dev/%s/%s" % self.logical_id
548 elif self.dev_type in constants.LDS_DRBD:
549 node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
551 if self.physical_id is None:
554 phy = ("configured as %s:%s %s:%s" %
555 (self.physical_id[0], self.physical_id[1],
556 self.physical_id[2], self.physical_id[3]))
558 val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
559 (node_a, minor_a, node_b, minor_b, port, phy))
560 if self.children and self.children.count(None) == 0:
561 val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
563 val += "no local storage"
565 val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
566 (self.dev_type, self.logical_id, self.physical_id, self.children))
567 if self.iv_name is None:
568 val += ", not visible"
570 val += ", visible as /dev/%s" % self.iv_name
571 if isinstance(self.size, int):
572 val += ", size=%dm)>" % self.size
574 val += ", size='%s')>" % (self.size,)
578 """Checks that this disk is correctly configured.
582 if self.mode not in constants.DISK_ACCESS_SET:
583 errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
587 class Instance(TaggableObject):
588 """Config object representing an instance."""
589 __slots__ = TaggableObject.__slots__ + [
604 def _ComputeSecondaryNodes(self):
605 """Compute the list of secondary nodes.
607 This is a simple wrapper over _ComputeAllNodes.
610 all_nodes = set(self._ComputeAllNodes())
611 all_nodes.discard(self.primary_node)
612 return tuple(all_nodes)
614 secondary_nodes = property(_ComputeSecondaryNodes, None, None,
615 "List of secondary nodes")
617 def _ComputeAllNodes(self):
618 """Compute the list of all nodes.
620 Since the data is already there (in the drbd disks), keeping it as
621 a separate normal attribute is redundant and if not properly
622 synchronised can cause problems. Thus it's better to compute it
626 def _Helper(nodes, device):
627 """Recursively computes nodes given a top device."""
628 if device.dev_type in constants.LDS_DRBD:
629 nodea, nodeb = device.logical_id[:2]
633 for child in device.children:
634 _Helper(nodes, child)
637 all_nodes.add(self.primary_node)
638 for device in self.disks:
639 _Helper(all_nodes, device)
640 return tuple(all_nodes)
642 all_nodes = property(_ComputeAllNodes, None, None,
643 "List of all nodes of the instance")
645 def MapLVsByNode(self, lvmap=None, devs=None, node=None):
646 """Provide a mapping of nodes to LVs this instance owns.
648 This function figures out what logical volumes should belong on
649 which nodes, recursing through a device tree.
651 @param lvmap: optional dictionary to receive the
652 'node' : ['lv', ...] data.
654 @return: None if lvmap arg is given, otherwise, a dictionary
655 of the form { 'nodename' : ['volume1', 'volume2', ...], ... }
659 node = self.primary_node
662 lvmap = { node : [] }
665 if not node in lvmap:
673 if dev.dev_type == constants.LD_LV:
674 lvmap[node].append(dev.logical_id[1])
676 elif dev.dev_type in constants.LDS_DRBD:
678 self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
679 self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
682 self.MapLVsByNode(lvmap, dev.children, node)
686 def FindDisk(self, idx):
687 """Find a disk given having a specified index.
689 This is just a wrapper that does validation of the index.
692 @param idx: the disk index
694 @return: the corresponding disk
695 @raise errors.OpPrereqError: when the given index is not valid
700 return self.disks[idx]
701 except ValueError, err:
702 raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err))
704 raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
705 " 0 to %d" % (idx, len(self.disks)))
708 """Instance-specific conversion to standard python types.
710 This replaces the children lists of objects with lists of standard
714 bo = super(Instance, self).ToDict()
716 for attr in "nics", "disks":
717 alist = bo.get(attr, None)
719 nlist = self._ContainerToDicts(alist)
726 def FromDict(cls, val):
727 """Custom function for instances.
730 obj = super(Instance, cls).FromDict(val)
731 obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
732 obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
736 class OS(ConfigObject):
737 """Config object representing an operating system."""
750 def FromInvalidOS(cls, err):
751 """Create an OS from an InvalidOS error.
753 This routine knows how to convert an InvalidOS error to an OS
754 object representing the broken OS with a meaningful error message.
757 if not isinstance(err, errors.InvalidOS):
758 raise errors.ProgrammerError("Trying to initialize an OS from an"
759 " invalid object of type %s" % type(err))
761 return cls(name=err.args[0], path=err.args[1], status=err.args[2])
763 def __nonzero__(self):
764 return self.status == constants.OS_VALID_STATUS
766 __bool__ = __nonzero__
769 class Node(TaggableObject):
770 """Config object representing a node."""
771 __slots__ = TaggableObject.__slots__ + [
782 class Cluster(TaggableObject):
783 """Config object representing the cluster."""
784 __slots__ = TaggableObject.__slots__ + [
792 "default_hypervisor",
798 "enabled_hypervisors",
802 "candidate_pool_size",
806 def UpgradeConfig(self):
807 """Fill defaults for missing configuration values.
810 if self.hvparams is None:
811 self.hvparams = constants.HVC_DEFAULTS
813 for hypervisor in self.hvparams:
814 self.hvparams[hypervisor] = FillDict(
815 constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
817 self.beparams = UpgradeGroupedParams(self.beparams,
818 constants.BEC_DEFAULTS)
819 migrate_default_bridge = not self.nicparams
820 self.nicparams = UpgradeGroupedParams(self.nicparams,
821 constants.NICC_DEFAULTS)
822 if migrate_default_bridge:
823 self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
826 if self.modify_etc_hosts is None:
827 self.modify_etc_hosts = True
829 # default_bridge is no longer used it 2.1. The slot is left there to
830 # support auto-upgrading, but will be removed in 2.2
831 if self.default_bridge is not None:
832 self.default_bridge = None
835 """Custom function for cluster.
838 mydict = super(Cluster, self).ToDict()
839 mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
843 def FromDict(cls, val):
844 """Custom function for cluster.
847 obj = super(Cluster, cls).FromDict(val)
848 if not isinstance(obj.tcpudp_port_pool, set):
849 obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
852 def FillHV(self, instance):
853 """Fill an instance's hvparams dict.
855 @type instance: object
856 @param instance: the instance parameter to fill
858 @return: a copy of the instance's hvparams with missing keys filled from
862 return FillDict(self.hvparams.get(instance.hypervisor, {}),
865 def FillBE(self, instance):
866 """Fill an instance's beparams dict.
868 @type instance: object
869 @param instance: the instance parameter to fill
871 @return: a copy of the instance's beparams with missing keys filled from
875 return FillDict(self.beparams.get(constants.PP_DEFAULT, {}),
879 class SerializableConfigParser(ConfigParser.SafeConfigParser):
880 """Simple wrapper over ConfigParse that allows serialization.
882 This class is basically ConfigParser.SafeConfigParser with two
883 additional methods that allow it to serialize/unserialize to/from a
888 """Dump this instance and return the string representation."""
891 return buf.getvalue()
895 """Load data from a string."""
897 cfp = SerializableConfigParser()