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 _TIMESTAMPS = ["ctime", "mtime"]
45 def FillDict(defaults_dict, custom_dict):
46 """Basic function to apply settings on top a default dict.
48 @type defaults_dict: dict
49 @param defaults_dict: dictionary holding the default values
50 @type custom_dict: dict
51 @param custom_dict: dictionary holding customized value
53 @return: dict with the 'full' values
56 ret_dict = copy.deepcopy(defaults_dict)
57 ret_dict.update(custom_dict)
61 def UpgradeGroupedParams(target, defaults):
62 """Update all groups for the target parameter.
64 @type target: dict of dicts
65 @param target: {group: {parameter: value}}
67 @param defaults: default parameter values
71 target = {constants.PP_DEFAULT: defaults}
74 target[group] = FillDict(defaults, target[group])
78 class ConfigObject(object):
79 """A generic config object.
81 It has the following properties:
83 - provides somewhat safe recursive unpickling and pickling for its classes
84 - unset attributes which are defined in slots are always returned
85 as None instead of raising an error
87 Classes derived from this must always declare __slots__ (we use many
88 config objects and the memory reduction is useful)
93 def __init__(self, **kwargs):
94 for k, v in kwargs.iteritems():
97 def __getattr__(self, name):
98 if name not in self.__slots__:
99 raise AttributeError("Invalid object attribute %s.%s" %
100 (type(self).__name__, name))
103 def __setstate__(self, state):
105 if name in self.__slots__:
106 setattr(self, name, state[name])
109 """Convert to a dict holding only standard python types.
111 The generic routine just dumps all of this object's attributes in
112 a dict. It does not work if the class has children who are
113 ConfigObjects themselves (e.g. the nics list in an Instance), in
114 which case the object should subclass the function in order to
115 make sure all objects returned are only standard python types.
119 for name in self.__slots__:
120 value = getattr(self, name, None)
121 if value is not None:
125 __getstate__ = ToDict
128 def FromDict(cls, val):
129 """Create an object from a dictionary.
131 This generic routine takes a dict, instantiates a new instance of
132 the given class, and sets attributes based on the dict content.
134 As for `ToDict`, this does not work if the class has children
135 who are ConfigObjects themselves (e.g. the nics list in an
136 Instance), in which case the object should subclass the function
137 and alter the objects.
140 if not isinstance(val, dict):
141 raise errors.ConfigurationError("Invalid object passed to FromDict:"
142 " expected dict, got %s" % type(val))
143 val_str = dict([(str(k), v) for k, v in val.iteritems()])
148 def _ContainerToDicts(container):
149 """Convert the elements of a container to standard python types.
151 This method converts a container with elements derived from
152 ConfigData to standard python types. If the container is a dict,
153 we don't touch the keys, only the values.
156 if isinstance(container, dict):
157 ret = dict([(k, v.ToDict()) for k, v in container.iteritems()])
158 elif isinstance(container, (list, tuple, set, frozenset)):
159 ret = [elem.ToDict() for elem in container]
161 raise TypeError("Invalid type %s passed to _ContainerToDicts" %
166 def _ContainerFromDicts(source, c_type, e_type):
167 """Convert a container from standard python types.
169 This method converts a container with standard python types to
170 ConfigData objects. If the container is a dict, we don't touch the
171 keys, only the values.
174 if not isinstance(c_type, type):
175 raise TypeError("Container type %s passed to _ContainerFromDicts is"
176 " not a type" % type(c_type))
178 ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
179 elif c_type in (list, tuple, set, frozenset):
180 ret = c_type([e_type.FromDict(elem) for elem in source])
182 raise TypeError("Invalid container type %s passed to"
183 " _ContainerFromDicts" % c_type)
187 """Makes a deep copy of the current object and its children.
190 dict_form = self.ToDict()
191 clone_obj = self.__class__.FromDict(dict_form)
195 """Implement __repr__ for ConfigObjects."""
196 return repr(self.ToDict())
198 def UpgradeConfig(self):
199 """Fill defaults for missing configuration values.
201 This method will be called at configuration load time, and its
202 implementation will be object dependent.
208 class TaggableObject(ConfigObject):
209 """An generic class supporting tags.
212 __slots__ = ConfigObject.__slots__ + ["tags"]
213 VALID_TAG_RE = re.compile("^[\w.+*/:@-]+$")
216 def ValidateTag(cls, tag):
217 """Check if a tag is valid.
219 If the tag is invalid, an errors.TagError will be raised. The
220 function has no return value.
223 if not isinstance(tag, basestring):
224 raise errors.TagError("Invalid tag type (not a string)")
225 if len(tag) > constants.MAX_TAG_LEN:
226 raise errors.TagError("Tag too long (>%d characters)" %
227 constants.MAX_TAG_LEN)
229 raise errors.TagError("Tags cannot be empty")
230 if not cls.VALID_TAG_RE.match(tag):
231 raise errors.TagError("Tag contains invalid characters")
234 """Return the tags list.
237 tags = getattr(self, "tags", None)
239 tags = self.tags = set()
242 def AddTag(self, tag):
246 self.ValidateTag(tag)
247 tags = self.GetTags()
248 if len(tags) >= constants.MAX_TAGS_PER_OBJ:
249 raise errors.TagError("Too many tags")
250 self.GetTags().add(tag)
252 def RemoveTag(self, tag):
256 self.ValidateTag(tag)
257 tags = self.GetTags()
261 raise errors.TagError("Tag not found")
264 """Taggable-object-specific conversion to standard python types.
266 This replaces the tags set with a list.
269 bo = super(TaggableObject, self).ToDict()
271 tags = bo.get("tags", None)
272 if isinstance(tags, set):
273 bo["tags"] = list(tags)
277 def FromDict(cls, val):
278 """Custom function for instances.
281 obj = super(TaggableObject, cls).FromDict(val)
282 if hasattr(obj, "tags") and isinstance(obj.tags, list):
283 obj.tags = set(obj.tags)
287 class ConfigData(ConfigObject):
288 """Top-level config object."""
289 __slots__ = (["version", "cluster", "nodes", "instances", "serial_no"] +
293 """Custom function for top-level config data.
295 This just replaces the list of instances, nodes and the cluster
296 with standard python types.
299 mydict = super(ConfigData, self).ToDict()
300 mydict["cluster"] = mydict["cluster"].ToDict()
301 for key in "nodes", "instances":
302 mydict[key] = self._ContainerToDicts(mydict[key])
307 def FromDict(cls, val):
308 """Custom function for top-level config data
311 obj = super(ConfigData, cls).FromDict(val)
312 obj.cluster = Cluster.FromDict(obj.cluster)
313 obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
314 obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
317 def UpgradeConfig(self):
318 """Fill defaults for missing configuration values.
321 self.cluster.UpgradeConfig()
322 for node in self.nodes.values():
324 for instance in self.instances.values():
325 instance.UpgradeConfig()
328 class NIC(ConfigObject):
329 """Config object representing a network card."""
330 __slots__ = ["mac", "ip", "bridge", "nicparams"]
333 def CheckParameterSyntax(cls, nicparams):
334 """Check the given parameters for validity.
336 @type nicparams: dict
337 @param nicparams: dictionary with parameter names/value
338 @raise errors.ConfigurationError: when a parameter is not valid
341 if nicparams[constants.NIC_MODE] not in constants.NIC_VALID_MODES:
342 err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
343 raise errors.ConfigurationError(err)
345 if (nicparams[constants.NIC_MODE] is constants.NIC_MODE_BRIDGED and
346 not nicparams[constants.NIC_LINK]):
347 err = "Missing bridged nic link"
348 raise errors.ConfigurationError(err)
350 def UpgradeConfig(self):
351 """Fill defaults for missing configuration values.
354 if self.nicparams is None:
356 if self.bridge is not None:
357 self.nicparams[constants.NIC_MODE] = constants.NIC_MODE_BRIDGED
358 self.nicparams[constants.NIC_LINK] = self.bridge
359 # bridge is no longer used it 2.1. The slot is left there to support
360 # upgrading, but will be removed in 2.2
361 if self.bridge is not None:
365 class Disk(ConfigObject):
366 """Config object representing a block device."""
367 __slots__ = ["dev_type", "logical_id", "physical_id",
368 "children", "iv_name", "size", "mode"]
370 def CreateOnSecondary(self):
371 """Test if this device needs to be created on a secondary node."""
372 return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
374 def AssembleOnSecondary(self):
375 """Test if this device needs to be assembled on a secondary node."""
376 return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
378 def OpenOnSecondary(self):
379 """Test if this device needs to be opened on a secondary node."""
380 return self.dev_type in (constants.LD_LV,)
382 def StaticDevPath(self):
383 """Return the device path if this device type has a static one.
385 Some devices (LVM for example) live always at the same /dev/ path,
386 irrespective of their status. For such devices, we return this
387 path, for others we return None.
390 if self.dev_type == constants.LD_LV:
391 return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
394 def ChildrenNeeded(self):
395 """Compute the needed number of children for activation.
397 This method will return either -1 (all children) or a positive
398 number denoting the minimum number of children needed for
399 activation (only mirrored devices will usually return >=0).
401 Currently, only DRBD8 supports diskless activation (therefore we
402 return 0), for all other we keep the previous semantics and return
406 if self.dev_type == constants.LD_DRBD8:
410 def GetNodes(self, node):
411 """This function returns the nodes this device lives on.
413 Given the node on which the parent of the device lives on (or, in
414 case of a top-level device, the primary node of the devices'
415 instance), this function will return a list of nodes on which this
416 devices needs to (or can) be assembled.
419 if self.dev_type in [constants.LD_LV, constants.LD_FILE]:
421 elif self.dev_type in constants.LDS_DRBD:
422 result = [self.logical_id[0], self.logical_id[1]]
423 if node not in result:
424 raise errors.ConfigurationError("DRBD device passed unknown node")
426 raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
429 def ComputeNodeTree(self, parent_node):
430 """Compute the node/disk tree for this disk and its children.
432 This method, given the node on which the parent disk lives, will
433 return the list of all (node, disk) pairs which describe the disk
434 tree in the most compact way. For example, a drbd/lvm stack
435 will be returned as (primary_node, drbd) and (secondary_node, drbd)
436 which represents all the top-level devices on the nodes.
439 my_nodes = self.GetNodes(parent_node)
440 result = [(node, self) for node in my_nodes]
441 if not self.children:
444 for node in my_nodes:
445 for child in self.children:
446 child_result = child.ComputeNodeTree(node)
447 if len(child_result) == 1:
448 # child (and all its descendants) is simple, doesn't split
449 # over multiple hosts, so we don't need to describe it, our
450 # own entry for this node describes it completely
453 # check if child nodes differ from my nodes; note that
454 # subdisk can differ from the child itself, and be instead
455 # one of its descendants
456 for subnode, subdisk in child_result:
457 if subnode not in my_nodes:
458 result.append((subnode, subdisk))
459 # otherwise child is under our own node, so we ignore this
460 # entry (but probably the other results in the list will
464 def RecordGrow(self, amount):
465 """Update the size of this disk after growth.
467 This method recurses over the disks's children and updates their
468 size correspondigly. The method needs to be kept in sync with the
469 actual algorithms from bdev.
472 if self.dev_type == constants.LD_LV:
474 elif self.dev_type == constants.LD_DRBD8:
476 self.children[0].RecordGrow(amount)
479 raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
480 " disk type %s" % self.dev_type)
483 """Sets recursively the size to zero for the disk and its children.
487 for child in self.children:
491 def SetPhysicalID(self, target_node, nodes_ip):
492 """Convert the logical ID to the physical ID.
494 This is used only for drbd, which needs ip/port configuration.
496 The routine descends down and updates its children also, because
497 this helps when the only the top device is passed to the remote
501 - target_node: the node we wish to configure for
502 - nodes_ip: a mapping of node name to ip
504 The target_node must exist in in nodes_ip, and must be one of the
505 nodes in the logical ID for each of the DRBD devices encountered
510 for child in self.children:
511 child.SetPhysicalID(target_node, nodes_ip)
513 if self.logical_id is None and self.physical_id is not None:
515 if self.dev_type in constants.LDS_DRBD:
516 pnode, snode, port, pminor, sminor, secret = self.logical_id
517 if target_node not in (pnode, snode):
518 raise errors.ConfigurationError("DRBD device not knowing node %s" %
520 pnode_ip = nodes_ip.get(pnode, None)
521 snode_ip = nodes_ip.get(snode, None)
522 if pnode_ip is None or snode_ip is None:
523 raise errors.ConfigurationError("Can't find primary or secondary node"
524 " for %s" % str(self))
525 p_data = (pnode_ip, port)
526 s_data = (snode_ip, port)
527 if pnode == target_node:
528 self.physical_id = p_data + s_data + (pminor, secret)
529 else: # it must be secondary, we tested above
530 self.physical_id = s_data + p_data + (sminor, secret)
532 self.physical_id = self.logical_id
536 """Disk-specific conversion to standard python types.
538 This replaces the children lists of objects with lists of
539 standard python types.
542 bo = super(Disk, self).ToDict()
544 for attr in ("children",):
545 alist = bo.get(attr, None)
547 bo[attr] = self._ContainerToDicts(alist)
551 def FromDict(cls, val):
552 """Custom function for Disks
555 obj = super(Disk, cls).FromDict(val)
557 obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
558 if obj.logical_id and isinstance(obj.logical_id, list):
559 obj.logical_id = tuple(obj.logical_id)
560 if obj.physical_id and isinstance(obj.physical_id, list):
561 obj.physical_id = tuple(obj.physical_id)
562 if obj.dev_type in constants.LDS_DRBD:
563 # we need a tuple of length six here
564 if len(obj.logical_id) < 6:
565 obj.logical_id += (None,) * (6 - len(obj.logical_id))
569 """Custom str() formatter for disks.
572 if self.dev_type == constants.LD_LV:
573 val = "<LogicalVolume(/dev/%s/%s" % self.logical_id
574 elif self.dev_type in constants.LDS_DRBD:
575 node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
577 if self.physical_id is None:
580 phy = ("configured as %s:%s %s:%s" %
581 (self.physical_id[0], self.physical_id[1],
582 self.physical_id[2], self.physical_id[3]))
584 val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
585 (node_a, minor_a, node_b, minor_b, port, phy))
586 if self.children and self.children.count(None) == 0:
587 val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
589 val += "no local storage"
591 val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
592 (self.dev_type, self.logical_id, self.physical_id, self.children))
593 if self.iv_name is None:
594 val += ", not visible"
596 val += ", visible as /dev/%s" % self.iv_name
597 if isinstance(self.size, int):
598 val += ", size=%dm)>" % self.size
600 val += ", size='%s')>" % (self.size,)
604 """Checks that this disk is correctly configured.
608 if self.mode not in constants.DISK_ACCESS_SET:
609 all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
612 def UpgradeConfig(self):
613 """Fill defaults for missing configuration values.
617 for child in self.children:
618 child.UpgradeConfig()
619 # add here config upgrade for this disk
622 class Instance(TaggableObject):
623 """Config object representing an instance."""
624 __slots__ = TaggableObject.__slots__ + [
637 ] + _TIMESTAMPS + _UUID
639 def _ComputeSecondaryNodes(self):
640 """Compute the list of secondary nodes.
642 This is a simple wrapper over _ComputeAllNodes.
645 all_nodes = set(self._ComputeAllNodes())
646 all_nodes.discard(self.primary_node)
647 return tuple(all_nodes)
649 secondary_nodes = property(_ComputeSecondaryNodes, None, None,
650 "List of secondary nodes")
652 def _ComputeAllNodes(self):
653 """Compute the list of all nodes.
655 Since the data is already there (in the drbd disks), keeping it as
656 a separate normal attribute is redundant and if not properly
657 synchronised can cause problems. Thus it's better to compute it
661 def _Helper(nodes, device):
662 """Recursively computes nodes given a top device."""
663 if device.dev_type in constants.LDS_DRBD:
664 nodea, nodeb = device.logical_id[:2]
668 for child in device.children:
669 _Helper(nodes, child)
672 all_nodes.add(self.primary_node)
673 for device in self.disks:
674 _Helper(all_nodes, device)
675 return tuple(all_nodes)
677 all_nodes = property(_ComputeAllNodes, None, None,
678 "List of all nodes of the instance")
680 def MapLVsByNode(self, lvmap=None, devs=None, node=None):
681 """Provide a mapping of nodes to LVs this instance owns.
683 This function figures out what logical volumes should belong on
684 which nodes, recursing through a device tree.
686 @param lvmap: optional dictionary to receive the
687 'node' : ['lv', ...] data.
689 @return: None if lvmap arg is given, otherwise, a dictionary
690 of the form { 'nodename' : ['volume1', 'volume2', ...], ... }
694 node = self.primary_node
697 lvmap = { node : [] }
700 if not node in lvmap:
708 if dev.dev_type == constants.LD_LV:
709 lvmap[node].append(dev.logical_id[1])
711 elif dev.dev_type in constants.LDS_DRBD:
713 self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
714 self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
717 self.MapLVsByNode(lvmap, dev.children, node)
721 def FindDisk(self, idx):
722 """Find a disk given having a specified index.
724 This is just a wrapper that does validation of the index.
727 @param idx: the disk index
729 @return: the corresponding disk
730 @raise errors.OpPrereqError: when the given index is not valid
735 return self.disks[idx]
736 except ValueError, err:
737 raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err),
740 raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
741 " 0 to %d" % (idx, len(self.disks)),
745 """Instance-specific conversion to standard python types.
747 This replaces the children lists of objects with lists of standard
751 bo = super(Instance, self).ToDict()
753 for attr in "nics", "disks":
754 alist = bo.get(attr, None)
756 nlist = self._ContainerToDicts(alist)
763 def FromDict(cls, val):
764 """Custom function for instances.
767 obj = super(Instance, cls).FromDict(val)
768 obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
769 obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
772 def UpgradeConfig(self):
773 """Fill defaults for missing configuration values.
776 for nic in self.nics:
778 for disk in self.disks:
782 class OS(ConfigObject):
783 """Config object representing an operating system."""
792 "supported_variants",
796 class Node(TaggableObject):
797 """Config object representing a node."""
798 __slots__ = TaggableObject.__slots__ + [
806 ] + _TIMESTAMPS + _UUID
809 class Cluster(TaggableObject):
810 """Config object representing the cluster."""
811 __slots__ = TaggableObject.__slots__ + [
819 "default_hypervisor",
825 "enabled_hypervisors",
829 "candidate_pool_size",
832 ] + _TIMESTAMPS + _UUID
834 def UpgradeConfig(self):
835 """Fill defaults for missing configuration values.
838 if self.hvparams is None:
839 self.hvparams = constants.HVC_DEFAULTS
841 for hypervisor in self.hvparams:
842 self.hvparams[hypervisor] = FillDict(
843 constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
845 self.beparams = UpgradeGroupedParams(self.beparams,
846 constants.BEC_DEFAULTS)
847 migrate_default_bridge = not self.nicparams
848 self.nicparams = UpgradeGroupedParams(self.nicparams,
849 constants.NICC_DEFAULTS)
850 if migrate_default_bridge:
851 self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
854 if self.modify_etc_hosts is None:
855 self.modify_etc_hosts = True
857 if self.modify_ssh_setup is None:
858 self.modify_ssh_setup = True
860 # default_bridge is no longer used it 2.1. The slot is left there to
861 # support auto-upgrading, but will be removed in 2.2
862 if self.default_bridge is not None:
863 self.default_bridge = None
865 # default_hypervisor is just the first enabled one in 2.1
866 if self.default_hypervisor is not None:
867 self.enabled_hypervisors = ([self.default_hypervisor] +
868 [hvname for hvname in self.enabled_hypervisors
869 if hvname != self.default_hypervisor])
870 self.default_hypervisor = None
873 """Custom function for cluster.
876 mydict = super(Cluster, self).ToDict()
877 mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
881 def FromDict(cls, val):
882 """Custom function for cluster.
885 obj = super(Cluster, cls).FromDict(val)
886 if not isinstance(obj.tcpudp_port_pool, set):
887 obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
890 def FillHV(self, instance):
891 """Fill an instance's hvparams dict.
893 @type instance: L{objects.Instance}
894 @param instance: the instance parameter to fill
896 @return: a copy of the instance's hvparams with missing keys filled from
900 return FillDict(self.hvparams.get(instance.hypervisor, {}),
903 def FillBE(self, instance):
904 """Fill an instance's beparams dict.
906 @type instance: L{objects.Instance}
907 @param instance: the instance parameter to fill
909 @return: a copy of the instance's beparams with missing keys filled from
913 return FillDict(self.beparams.get(constants.PP_DEFAULT, {}),
917 class BlockDevStatus(ConfigObject):
918 """Config object representing the status of a block device."""
930 class ConfdRequest(ConfigObject):
931 """Object holding a confd request.
933 @ivar protocol: confd protocol version
934 @ivar type: confd query type
935 @ivar query: query request
936 @ivar rsalt: requested reply salt
947 class ConfdReply(ConfigObject):
948 """Object holding a confd reply.
950 @ivar protocol: confd protocol version
951 @ivar status: reply status code (ok, error)
952 @ivar answer: confd query reply
953 @ivar serial: configuration serial number
964 class SerializableConfigParser(ConfigParser.SafeConfigParser):
965 """Simple wrapper over ConfigParse that allows serialization.
967 This class is basically ConfigParser.SafeConfigParser with two
968 additional methods that allow it to serialize/unserialize to/from a
973 """Dump this instance and return the string representation."""
976 return buf.getvalue()
980 """Load data from a string."""
982 cfp = SerializableConfigParser()