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"]
44 def FillDict(defaults_dict, custom_dict):
45 """Basic function to apply settings on top a default dict.
47 @type defaults_dict: dict
48 @param defaults_dict: dictionary holding the default values
49 @type custom_dict: dict
50 @param custom_dict: dictionary holding customized value
52 @return: dict with the 'full' values
55 ret_dict = copy.deepcopy(defaults_dict)
56 ret_dict.update(custom_dict)
60 def UpgradeGroupedParams(target, defaults):
61 """Update all groups for the target parameter.
63 @type target: dict of dicts
64 @param target: {group: {parameter: value}}
66 @param defaults: default parameter values
70 target = {constants.PP_DEFAULT: defaults}
73 target[group] = FillDict(defaults, target[group])
77 class ConfigObject(object):
78 """A generic config object.
80 It has the following properties:
82 - provides somewhat safe recursive unpickling and pickling for its classes
83 - unset attributes which are defined in slots are always returned
84 as None instead of raising an error
86 Classes derived from this must always declare __slots__ (we use many
87 config objects and the memory reduction is useful)
92 def __init__(self, **kwargs):
93 for k, v in kwargs.iteritems():
96 def __getattr__(self, name):
97 if name not in self.__slots__:
98 raise AttributeError("Invalid object attribute %s.%s" %
99 (type(self).__name__, name))
102 def __setstate__(self, state):
104 if name in self.__slots__:
105 setattr(self, name, state[name])
108 """Convert to a dict holding only standard python types.
110 The generic routine just dumps all of this object's attributes in
111 a dict. It does not work if the class has children who are
112 ConfigObjects themselves (e.g. the nics list in an Instance), in
113 which case the object should subclass the function in order to
114 make sure all objects returned are only standard python types.
118 for name in self.__slots__:
119 value = getattr(self, name, None)
120 if value is not None:
124 __getstate__ = ToDict
127 def FromDict(cls, val):
128 """Create an object from a dictionary.
130 This generic routine takes a dict, instantiates a new instance of
131 the given class, and sets attributes based on the dict content.
133 As for `ToDict`, this does not work if the class has children
134 who are ConfigObjects themselves (e.g. the nics list in an
135 Instance), in which case the object should subclass the function
136 and alter the objects.
139 if not isinstance(val, dict):
140 raise errors.ConfigurationError("Invalid object passed to FromDict:"
141 " expected dict, got %s" % type(val))
142 val_str = dict([(str(k), v) for k, v in val.iteritems()])
147 def _ContainerToDicts(container):
148 """Convert the elements of a container to standard python types.
150 This method converts a container with elements derived from
151 ConfigData to standard python types. If the container is a dict,
152 we don't touch the keys, only the values.
155 if isinstance(container, dict):
156 ret = dict([(k, v.ToDict()) for k, v in container.iteritems()])
157 elif isinstance(container, (list, tuple, set, frozenset)):
158 ret = [elem.ToDict() for elem in container]
160 raise TypeError("Invalid type %s passed to _ContainerToDicts" %
165 def _ContainerFromDicts(source, c_type, e_type):
166 """Convert a container from standard python types.
168 This method converts a container with standard python types to
169 ConfigData objects. If the container is a dict, we don't touch the
170 keys, only the values.
173 if not isinstance(c_type, type):
174 raise TypeError("Container type %s passed to _ContainerFromDicts is"
175 " not a type" % type(c_type))
177 ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
178 elif c_type in (list, tuple, set, frozenset):
179 ret = c_type([e_type.FromDict(elem) for elem in source])
181 raise TypeError("Invalid container type %s passed to"
182 " _ContainerFromDicts" % c_type)
186 """Makes a deep copy of the current object and its children.
189 dict_form = self.ToDict()
190 clone_obj = self.__class__.FromDict(dict_form)
194 """Implement __repr__ for ConfigObjects."""
195 return repr(self.ToDict())
197 def UpgradeConfig(self):
198 """Fill defaults for missing configuration values.
200 This method will be called at configuration load time, and its
201 implementation will be object dependent.
207 class TaggableObject(ConfigObject):
208 """An generic class supporting tags.
211 __slots__ = ConfigObject.__slots__ + ["tags"]
214 def ValidateTag(tag):
215 """Check if a tag is valid.
217 If the tag is invalid, an errors.TagError will be raised. The
218 function has no return value.
221 if not isinstance(tag, basestring):
222 raise errors.TagError("Invalid tag type (not a string)")
223 if len(tag) > constants.MAX_TAG_LEN:
224 raise errors.TagError("Tag too long (>%d characters)" %
225 constants.MAX_TAG_LEN)
227 raise errors.TagError("Tags cannot be empty")
228 if not re.match("^[\w.+*/:-]+$", tag):
229 raise errors.TagError("Tag contains invalid characters")
232 """Return the tags list.
235 tags = getattr(self, "tags", None)
237 tags = self.tags = set()
240 def AddTag(self, tag):
244 self.ValidateTag(tag)
245 tags = self.GetTags()
246 if len(tags) >= constants.MAX_TAGS_PER_OBJ:
247 raise errors.TagError("Too many tags")
248 self.GetTags().add(tag)
250 def RemoveTag(self, tag):
254 self.ValidateTag(tag)
255 tags = self.GetTags()
259 raise errors.TagError("Tag not found")
262 """Taggable-object-specific conversion to standard python types.
264 This replaces the tags set with a list.
267 bo = super(TaggableObject, self).ToDict()
269 tags = bo.get("tags", None)
270 if isinstance(tags, set):
271 bo["tags"] = list(tags)
275 def FromDict(cls, val):
276 """Custom function for instances.
279 obj = super(TaggableObject, cls).FromDict(val)
280 if hasattr(obj, "tags") and isinstance(obj.tags, list):
281 obj.tags = set(obj.tags)
285 class ConfigData(ConfigObject):
286 """Top-level config object."""
287 __slots__ = (["version", "cluster", "nodes", "instances", "serial_no"] +
291 """Custom function for top-level config data.
293 This just replaces the list of instances, nodes and the cluster
294 with standard python types.
297 mydict = super(ConfigData, self).ToDict()
298 mydict["cluster"] = mydict["cluster"].ToDict()
299 for key in "nodes", "instances":
300 mydict[key] = self._ContainerToDicts(mydict[key])
305 def FromDict(cls, val):
306 """Custom function for top-level config data
309 obj = super(ConfigData, cls).FromDict(val)
310 obj.cluster = Cluster.FromDict(obj.cluster)
311 obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
312 obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
315 def UpgradeConfig(self):
316 """Fill defaults for missing configuration values.
319 self.cluster.UpgradeConfig()
320 for node in self.nodes.values():
322 for instance in self.instances.values():
323 instance.UpgradeConfig()
326 class NIC(ConfigObject):
327 """Config object representing a network card."""
328 __slots__ = ["mac", "ip", "bridge", "nicparams"]
331 def CheckParameterSyntax(cls, nicparams):
332 """Check the given parameters for validity.
334 @type nicparams: dict
335 @param nicparams: dictionary with parameter names/value
336 @raise errors.ConfigurationError: when a parameter is not valid
339 if nicparams[constants.NIC_MODE] not in constants.NIC_VALID_MODES:
340 err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
341 raise errors.ConfigurationError(err)
343 if (nicparams[constants.NIC_MODE] is constants.NIC_MODE_BRIDGED and
344 not nicparams[constants.NIC_LINK]):
345 err = "Missing bridged nic link"
346 raise errors.ConfigurationError(err)
348 def UpgradeConfig(self):
349 """Fill defaults for missing configuration values.
352 if self.nicparams is None:
354 if self.bridge is not None:
355 self.nicparams[constants.NIC_MODE] = constants.NIC_MODE_BRIDGED
356 self.nicparams[constants.NIC_LINK] = self.bridge
357 # bridge is no longer used it 2.1. The slot is left there to support
358 # upgrading, but will be removed in 2.2
359 if self.bridge is not None:
363 class Disk(ConfigObject):
364 """Config object representing a block device."""
365 __slots__ = ["dev_type", "logical_id", "physical_id",
366 "children", "iv_name", "size", "mode"]
368 def CreateOnSecondary(self):
369 """Test if this device needs to be created on a secondary node."""
370 return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
372 def AssembleOnSecondary(self):
373 """Test if this device needs to be assembled on a secondary node."""
374 return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
376 def OpenOnSecondary(self):
377 """Test if this device needs to be opened on a secondary node."""
378 return self.dev_type in (constants.LD_LV,)
380 def StaticDevPath(self):
381 """Return the device path if this device type has a static one.
383 Some devices (LVM for example) live always at the same /dev/ path,
384 irrespective of their status. For such devices, we return this
385 path, for others we return None.
388 if self.dev_type == constants.LD_LV:
389 return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
392 def ChildrenNeeded(self):
393 """Compute the needed number of children for activation.
395 This method will return either -1 (all children) or a positive
396 number denoting the minimum number of children needed for
397 activation (only mirrored devices will usually return >=0).
399 Currently, only DRBD8 supports diskless activation (therefore we
400 return 0), for all other we keep the previous semantics and return
404 if self.dev_type == constants.LD_DRBD8:
408 def GetNodes(self, node):
409 """This function returns the nodes this device lives on.
411 Given the node on which the parent of the device lives on (or, in
412 case of a top-level device, the primary node of the devices'
413 instance), this function will return a list of nodes on which this
414 devices needs to (or can) be assembled.
417 if self.dev_type in [constants.LD_LV, constants.LD_FILE]:
419 elif self.dev_type in constants.LDS_DRBD:
420 result = [self.logical_id[0], self.logical_id[1]]
421 if node not in result:
422 raise errors.ConfigurationError("DRBD device passed unknown node")
424 raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
427 def ComputeNodeTree(self, parent_node):
428 """Compute the node/disk tree for this disk and its children.
430 This method, given the node on which the parent disk lives, will
431 return the list of all (node, disk) pairs which describe the disk
432 tree in the most compact way. For example, a drbd/lvm stack
433 will be returned as (primary_node, drbd) and (secondary_node, drbd)
434 which represents all the top-level devices on the nodes.
437 my_nodes = self.GetNodes(parent_node)
438 result = [(node, self) for node in my_nodes]
439 if not self.children:
442 for node in my_nodes:
443 for child in self.children:
444 child_result = child.ComputeNodeTree(node)
445 if len(child_result) == 1:
446 # child (and all its descendants) is simple, doesn't split
447 # over multiple hosts, so we don't need to describe it, our
448 # own entry for this node describes it completely
451 # check if child nodes differ from my nodes; note that
452 # subdisk can differ from the child itself, and be instead
453 # one of its descendants
454 for subnode, subdisk in child_result:
455 if subnode not in my_nodes:
456 result.append((subnode, subdisk))
457 # otherwise child is under our own node, so we ignore this
458 # entry (but probably the other results in the list will
462 def RecordGrow(self, amount):
463 """Update the size of this disk after growth.
465 This method recurses over the disks's children and updates their
466 size correspondigly. The method needs to be kept in sync with the
467 actual algorithms from bdev.
470 if self.dev_type == constants.LD_LV:
472 elif self.dev_type == constants.LD_DRBD8:
474 self.children[0].RecordGrow(amount)
477 raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
478 " disk type %s" % self.dev_type)
481 """Sets recursively the size to zero for the disk and its children.
485 for child in self.children:
489 def SetPhysicalID(self, target_node, nodes_ip):
490 """Convert the logical ID to the physical ID.
492 This is used only for drbd, which needs ip/port configuration.
494 The routine descends down and updates its children also, because
495 this helps when the only the top device is passed to the remote
499 - target_node: the node we wish to configure for
500 - nodes_ip: a mapping of node name to ip
502 The target_node must exist in in nodes_ip, and must be one of the
503 nodes in the logical ID for each of the DRBD devices encountered
508 for child in self.children:
509 child.SetPhysicalID(target_node, nodes_ip)
511 if self.logical_id is None and self.physical_id is not None:
513 if self.dev_type in constants.LDS_DRBD:
514 pnode, snode, port, pminor, sminor, secret = self.logical_id
515 if target_node not in (pnode, snode):
516 raise errors.ConfigurationError("DRBD device not knowing node %s" %
518 pnode_ip = nodes_ip.get(pnode, None)
519 snode_ip = nodes_ip.get(snode, None)
520 if pnode_ip is None or snode_ip is None:
521 raise errors.ConfigurationError("Can't find primary or secondary node"
522 " for %s" % str(self))
523 p_data = (pnode_ip, port)
524 s_data = (snode_ip, port)
525 if pnode == target_node:
526 self.physical_id = p_data + s_data + (pminor, secret)
527 else: # it must be secondary, we tested above
528 self.physical_id = s_data + p_data + (sminor, secret)
530 self.physical_id = self.logical_id
534 """Disk-specific conversion to standard python types.
536 This replaces the children lists of objects with lists of
537 standard python types.
540 bo = super(Disk, self).ToDict()
542 for attr in ("children",):
543 alist = bo.get(attr, None)
545 bo[attr] = self._ContainerToDicts(alist)
549 def FromDict(cls, val):
550 """Custom function for Disks
553 obj = super(Disk, cls).FromDict(val)
555 obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
556 if obj.logical_id and isinstance(obj.logical_id, list):
557 obj.logical_id = tuple(obj.logical_id)
558 if obj.physical_id and isinstance(obj.physical_id, list):
559 obj.physical_id = tuple(obj.physical_id)
560 if obj.dev_type in constants.LDS_DRBD:
561 # we need a tuple of length six here
562 if len(obj.logical_id) < 6:
563 obj.logical_id += (None,) * (6 - len(obj.logical_id))
567 """Custom str() formatter for disks.
570 if self.dev_type == constants.LD_LV:
571 val = "<LogicalVolume(/dev/%s/%s" % self.logical_id
572 elif self.dev_type in constants.LDS_DRBD:
573 node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
575 if self.physical_id is None:
578 phy = ("configured as %s:%s %s:%s" %
579 (self.physical_id[0], self.physical_id[1],
580 self.physical_id[2], self.physical_id[3]))
582 val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
583 (node_a, minor_a, node_b, minor_b, port, phy))
584 if self.children and self.children.count(None) == 0:
585 val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
587 val += "no local storage"
589 val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
590 (self.dev_type, self.logical_id, self.physical_id, self.children))
591 if self.iv_name is None:
592 val += ", not visible"
594 val += ", visible as /dev/%s" % self.iv_name
595 if isinstance(self.size, int):
596 val += ", size=%dm)>" % self.size
598 val += ", size='%s')>" % (self.size,)
602 """Checks that this disk is correctly configured.
606 if self.mode not in constants.DISK_ACCESS_SET:
607 all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
610 def UpgradeConfig(self):
611 """Fill defaults for missing configuration values.
615 for child in self.children:
616 child.UpgradeConfig()
617 # add here config upgrade for this disk
620 class Instance(TaggableObject):
621 """Config object representing an instance."""
622 __slots__ = TaggableObject.__slots__ + [
637 def _ComputeSecondaryNodes(self):
638 """Compute the list of secondary nodes.
640 This is a simple wrapper over _ComputeAllNodes.
643 all_nodes = set(self._ComputeAllNodes())
644 all_nodes.discard(self.primary_node)
645 return tuple(all_nodes)
647 secondary_nodes = property(_ComputeSecondaryNodes, None, None,
648 "List of secondary nodes")
650 def _ComputeAllNodes(self):
651 """Compute the list of all nodes.
653 Since the data is already there (in the drbd disks), keeping it as
654 a separate normal attribute is redundant and if not properly
655 synchronised can cause problems. Thus it's better to compute it
659 def _Helper(nodes, device):
660 """Recursively computes nodes given a top device."""
661 if device.dev_type in constants.LDS_DRBD:
662 nodea, nodeb = device.logical_id[:2]
666 for child in device.children:
667 _Helper(nodes, child)
670 all_nodes.add(self.primary_node)
671 for device in self.disks:
672 _Helper(all_nodes, device)
673 return tuple(all_nodes)
675 all_nodes = property(_ComputeAllNodes, None, None,
676 "List of all nodes of the instance")
678 def MapLVsByNode(self, lvmap=None, devs=None, node=None):
679 """Provide a mapping of nodes to LVs this instance owns.
681 This function figures out what logical volumes should belong on
682 which nodes, recursing through a device tree.
684 @param lvmap: optional dictionary to receive the
685 'node' : ['lv', ...] data.
687 @return: None if lvmap arg is given, otherwise, a dictionary
688 of the form { 'nodename' : ['volume1', 'volume2', ...], ... }
692 node = self.primary_node
695 lvmap = { node : [] }
698 if not node in lvmap:
706 if dev.dev_type == constants.LD_LV:
707 lvmap[node].append(dev.logical_id[1])
709 elif dev.dev_type in constants.LDS_DRBD:
711 self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
712 self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
715 self.MapLVsByNode(lvmap, dev.children, node)
719 def FindDisk(self, idx):
720 """Find a disk given having a specified index.
722 This is just a wrapper that does validation of the index.
725 @param idx: the disk index
727 @return: the corresponding disk
728 @raise errors.OpPrereqError: when the given index is not valid
733 return self.disks[idx]
734 except ValueError, err:
735 raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err))
737 raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
738 " 0 to %d" % (idx, len(self.disks)))
741 """Instance-specific conversion to standard python types.
743 This replaces the children lists of objects with lists of standard
747 bo = super(Instance, self).ToDict()
749 for attr in "nics", "disks":
750 alist = bo.get(attr, None)
752 nlist = self._ContainerToDicts(alist)
759 def FromDict(cls, val):
760 """Custom function for instances.
763 obj = super(Instance, cls).FromDict(val)
764 obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
765 obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
768 def UpgradeConfig(self):
769 """Fill defaults for missing configuration values.
772 for nic in self.nics:
774 for disk in self.disks:
778 class OS(ConfigObject):
779 """Config object representing an operating system."""
791 class Node(TaggableObject):
792 """Config object representing a node."""
793 __slots__ = TaggableObject.__slots__ + [
804 class Cluster(TaggableObject):
805 """Config object representing the cluster."""
806 __slots__ = TaggableObject.__slots__ + [
814 "default_hypervisor",
820 "enabled_hypervisors",
824 "candidate_pool_size",
828 def UpgradeConfig(self):
829 """Fill defaults for missing configuration values.
832 if self.hvparams is None:
833 self.hvparams = constants.HVC_DEFAULTS
835 for hypervisor in self.hvparams:
836 self.hvparams[hypervisor] = FillDict(
837 constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
839 self.beparams = UpgradeGroupedParams(self.beparams,
840 constants.BEC_DEFAULTS)
841 migrate_default_bridge = not self.nicparams
842 self.nicparams = UpgradeGroupedParams(self.nicparams,
843 constants.NICC_DEFAULTS)
844 if migrate_default_bridge:
845 self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
848 if self.modify_etc_hosts is None:
849 self.modify_etc_hosts = True
851 # default_bridge is no longer used it 2.1. The slot is left there to
852 # support auto-upgrading, but will be removed in 2.2
853 if self.default_bridge is not None:
854 self.default_bridge = None
856 # default_hypervisor is just the first enabled one in 2.1
857 if self.default_hypervisor is not None:
858 self.enabled_hypervisors = ([self.default_hypervisor] +
859 [hvname for hvname in self.enabled_hypervisors
860 if hvname != self.default_hypervisor])
861 self.default_hypervisor = None
864 """Custom function for cluster.
867 mydict = super(Cluster, self).ToDict()
868 mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
872 def FromDict(cls, val):
873 """Custom function for cluster.
876 obj = super(Cluster, cls).FromDict(val)
877 if not isinstance(obj.tcpudp_port_pool, set):
878 obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
881 def FillHV(self, instance):
882 """Fill an instance's hvparams dict.
884 @type instance: L{objects.Instance}
885 @param instance: the instance parameter to fill
887 @return: a copy of the instance's hvparams with missing keys filled from
891 return FillDict(self.hvparams.get(instance.hypervisor, {}),
894 def FillBE(self, instance):
895 """Fill an instance's beparams dict.
897 @type instance: L{objects.Instance}
898 @param instance: the instance parameter to fill
900 @return: a copy of the instance's beparams with missing keys filled from
904 return FillDict(self.beparams.get(constants.PP_DEFAULT, {}),
908 class BlockDevStatus(ConfigObject):
909 """Config object representing the status of a block device."""
921 class ConfdRequest(ConfigObject):
922 """Object holding a confd request.
924 @ivar protocol: confd protocol version
925 @ivar type: confd query type
926 @ivar query: query request
927 @ivar rsalt: requested reply salt
938 class ConfdReply(ConfigObject):
939 """Object holding a confd reply.
941 @ivar protocol: confd protocol version
942 @ivar status: reply status code (ok, error)
943 @ivar answer: confd query reply
944 @ivar serial: configuration serial number
955 class SerializableConfigParser(ConfigParser.SafeConfigParser):
956 """Simple wrapper over ConfigParse that allows serialization.
958 This class is basically ConfigParser.SafeConfigParser with two
959 additional methods that allow it to serialize/unserialize to/from a
964 """Dump this instance and return the string representation."""
967 return buf.getvalue()
971 """Load data from a string."""
973 cfp = SerializableConfigParser()