X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/47c28c5b39deb99b19a8a33d6c43e2bddec0ef0a..84b455872adb25569baae3904fec7b3c369ac9db:/lib/objects.py diff --git a/lib/objects.py b/lib/objects.py index 2fb8969..89473fe 100644 --- a/lib/objects.py +++ b/lib/objects.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +# # # Copyright (C) 2006, 2007 Google Inc. @@ -27,11 +27,13 @@ pass to and from external parties. """ -import cPickle -from cStringIO import StringIO import ConfigParser +import re +import copy +from cStringIO import StringIO from ganeti import errors +from ganeti import constants __all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance", @@ -48,24 +50,24 @@ class ConfigObject(object): as None instead of raising an error Classes derived from this must always declare __slots__ (we use many - config objects and the memory reduction is useful. + config objects and the memory reduction is useful) """ __slots__ = [] def __init__(self, **kwargs): - for i in kwargs: - setattr(self, i, kwargs[i]) + for k, v in kwargs.iteritems(): + setattr(self, k, v) def __getattr__(self, name): if name not in self.__slots__: - raise AttributeError, ("Invalid object attribute %s.%s" % - (type(self).__name__, name)) + raise AttributeError("Invalid object attribute %s.%s" % + (type(self).__name__, name)) return None def __setitem__(self, key, value): if key not in self.__slots__: - raise KeyError, key + raise KeyError(key) setattr(self, key, value) def __getstate__(self): @@ -80,75 +82,188 @@ class ConfigObject(object): if name in self.__slots__: setattr(self, name, state[name]) + def ToDict(self): + """Convert to a dict holding only standard python types. + + The generic routine just dumps all of this object's attributes in + a dict. It does not work if the class has children who are + ConfigObjects themselves (e.g. the nics list in an Instance), in + which case the object should subclass the function in order to + make sure all objects returned are only standard python types. + + """ + return dict([(k, getattr(self, k, None)) for k in self.__slots__]) + + @classmethod + def FromDict(cls, val): + """Create an object from a dictionary. + + This generic routine takes a dict, instantiates a new instance of + the given class, and sets attributes based on the dict content. + + As for `ToDict`, this does not work if the class has children + who are ConfigObjects themselves (e.g. the nics list in an + Instance), in which case the object should subclass the function + and alter the objects. + + """ + if not isinstance(val, dict): + raise errors.ConfigurationError("Invalid object passed to FromDict:" + " expected dict, got %s" % type(val)) + val_str = dict([(str(k), v) for k, v in val.iteritems()]) + obj = cls(**val_str) + return obj + @staticmethod - def FindGlobal(module, name): - """Function filtering the allowed classes to be un-pickled. - - Currently, we only allow the classes from this module which are - derived from ConfigObject. - - """ - # Also support the old module name (ganeti.config) - cls = None - if module == "ganeti.config" or module == "ganeti.objects": - if name == "ConfigData": - cls = ConfigData - elif name == "NIC": - cls = NIC - elif name == "Disk" or name == "BlockDev": - cls = Disk - elif name == "Instance": - cls = Instance - elif name == "OS": - cls = OS - elif name == "Node": - cls = Node - elif name == "Cluster": - cls = Cluster - elif module == "__builtin__": - if name == "set": - cls = set - if cls is None: - raise cPickle.UnpicklingError, ("Class %s.%s not allowed due to" - " security concerns" % (module, name)) - return cls - - def Dump(self, fobj): - """Dump this instance to a file object. - - Note that we use the HIGHEST_PROTOCOL, as it brings benefits for - the new classes. - - """ - dumper = cPickle.Pickler(fobj, cPickle.HIGHEST_PROTOCOL) - dumper.dump(self) + def _ContainerToDicts(container): + """Convert the elements of a container to standard python types. + + This method converts a container with elements derived from + ConfigData to standard python types. If the container is a dict, + we don't touch the keys, only the values. + + """ + if isinstance(container, dict): + ret = dict([(k, v.ToDict()) for k, v in container.iteritems()]) + elif isinstance(container, (list, tuple, set, frozenset)): + ret = [elem.ToDict() for elem in container] + else: + raise TypeError("Invalid type %s passed to _ContainerToDicts" % + type(container)) + return ret @staticmethod - def Load(fobj): - """Unpickle data from the given stream. + def _ContainerFromDicts(source, c_type, e_type): + """Convert a container from standard python types. - This uses the `FindGlobal` function to filter the allowed classes. + This method converts a container with standard python types to + ConfigData objects. If the container is a dict, we don't touch the + keys, only the values. """ - loader = cPickle.Unpickler(fobj) - loader.find_global = ConfigObject.FindGlobal - return loader.load() + if not isinstance(c_type, type): + raise TypeError("Container type %s passed to _ContainerFromDicts is" + " not a type" % type(c_type)) + if c_type is dict: + ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()]) + elif c_type in (list, tuple, set, frozenset): + ret = c_type([e_type.FromDict(elem) for elem in source]) + else: + raise TypeError("Invalid container type %s passed to" + " _ContainerFromDicts" % c_type) + return ret + + def __repr__(self): + """Implement __repr__ for ConfigObjects.""" + return repr(self.ToDict()) - def Dumps(self): - """Dump this instance and return the string representation.""" - buf = StringIO() - self.Dump(buf) - return buf.getvalue() + +class TaggableObject(ConfigObject): + """An generic class supporting tags. + + """ + __slots__ = ConfigObject.__slots__ + ["tags"] @staticmethod - def Loads(data): - """Load data from a string.""" - return ConfigObject.Load(StringIO(data)) + def ValidateTag(tag): + """Check if a tag is valid. + + If the tag is invalid, an errors.TagError will be raised. The + function has no return value. + + """ + if not isinstance(tag, basestring): + raise errors.TagError("Invalid tag type (not a string)") + if len(tag) > constants.MAX_TAG_LEN: + raise errors.TagError("Tag too long (>%d characters)" % + constants.MAX_TAG_LEN) + if not tag: + raise errors.TagError("Tags cannot be empty") + if not re.match("^[ \w.+*/:-]+$", tag): + raise errors.TagError("Tag contains invalid characters") + + def GetTags(self): + """Return the tags list. + + """ + tags = getattr(self, "tags", None) + if tags is None: + tags = self.tags = set() + return tags + + def AddTag(self, tag): + """Add a new tag. + + """ + self.ValidateTag(tag) + tags = self.GetTags() + if len(tags) >= constants.MAX_TAGS_PER_OBJ: + raise errors.TagError("Too many tags") + self.GetTags().add(tag) + + def RemoveTag(self, tag): + """Remove a tag. + + """ + self.ValidateTag(tag) + tags = self.GetTags() + try: + tags.remove(tag) + except KeyError: + raise errors.TagError("Tag not found") + + def ToDict(self): + """Taggable-object-specific conversion to standard python types. + + This replaces the tags set with a list. + + """ + bo = super(TaggableObject, self).ToDict() + + tags = bo.get("tags", None) + if isinstance(tags, set): + bo["tags"] = list(tags) + return bo + + @classmethod + def FromDict(cls, val): + """Custom function for instances. + + """ + obj = super(TaggableObject, cls).FromDict(val) + if hasattr(obj, "tags") and isinstance(obj.tags, list): + obj.tags = set(obj.tags) + return obj class ConfigData(ConfigObject): """Top-level config object.""" - __slots__ = ["cluster", "nodes", "instances"] + __slots__ = ["version", "cluster", "nodes", "instances", "serial_no"] + + def ToDict(self): + """Custom function for top-level config data. + + This just replaces the list of instances, nodes and the cluster + with standard python types. + + """ + mydict = super(ConfigData, self).ToDict() + mydict["cluster"] = mydict["cluster"].ToDict() + for key in "nodes", "instances": + mydict[key] = self._ContainerToDicts(mydict[key]) + + return mydict + + @classmethod + def FromDict(cls, val): + """Custom function for top-level config data + + """ + obj = super(ConfigData, cls).FromDict(val) + obj.cluster = Cluster.FromDict(obj.cluster) + obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node) + obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance) + return obj class NIC(ConfigObject): @@ -159,19 +274,47 @@ class NIC(ConfigObject): class Disk(ConfigObject): """Config object representing a block device.""" __slots__ = ["dev_type", "logical_id", "physical_id", - "children", "iv_name", "size"] + "children", "iv_name", "size", "mode"] def CreateOnSecondary(self): """Test if this device needs to be created on a secondary node.""" - return self.dev_type in ("drbd", "lvm") + return self.dev_type in (constants.LD_DRBD8, constants.LD_LV) def AssembleOnSecondary(self): """Test if this device needs to be assembled on a secondary node.""" - return self.dev_type in ("drbd", "lvm") + return self.dev_type in (constants.LD_DRBD8, constants.LD_LV) def OpenOnSecondary(self): """Test if this device needs to be opened on a secondary node.""" - return self.dev_type in ("lvm",) + return self.dev_type in (constants.LD_LV,) + + def StaticDevPath(self): + """Return the device path if this device type has a static one. + + Some devices (LVM for example) live always at the same /dev/ path, + irrespective of their status. For such devices, we return this + path, for others we return None. + + """ + if self.dev_type == constants.LD_LV: + return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1]) + return None + + def ChildrenNeeded(self): + """Compute the needed number of children for activation. + + This method will return either -1 (all children) or a positive + number denoting the minimum number of children needed for + activation (only mirrored devices will usually return >=0). + + Currently, only DRBD8 supports diskless activation (therefore we + return 0), for all other we keep the previous semantics and return + -1. + + """ + if self.dev_type == constants.LD_DRBD8: + return 0 + return -1 def GetNodes(self, node): """This function returns the nodes this device lives on. @@ -182,14 +325,14 @@ class Disk(ConfigObject): devices needs to (or can) be assembled. """ - if self.dev_type == "lvm" or self.dev_type == "md_raid1": + if self.dev_type in [constants.LD_LV, constants.LD_FILE]: result = [node] - elif self.dev_type == "drbd": + elif self.dev_type in constants.LDS_DRBD: result = [self.logical_id[0], self.logical_id[1]] if node not in result: - raise errors.ConfigurationError, ("DRBD device passed unknown node") + raise errors.ConfigurationError("DRBD device passed unknown node") else: - raise errors.ProgrammerError, "Unhandled device type %s" % self.dev_type + raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type) return result def ComputeNodeTree(self, parent_node): @@ -197,12 +340,9 @@ class Disk(ConfigObject): This method, given the node on which the parent disk lives, will return the list of all (node, disk) pairs which describe the disk - tree in the most compact way. For example, a md/drbd/lvm stack - will be returned as (primary_node, md) and (secondary_node, drbd) - which represents all the top-level devices on the nodes. This - means that on the primary node we need to activate the the md (and - recursively all its children) and on the secondary node we need to - activate the drbd device (and its children, the two lvm volumes). + tree in the most compact way. For example, a drbd/lvm stack + will be returned as (primary_node, drbd) and (secondary_node, drbd) + which represents all the top-level devices on the nodes. """ my_nodes = self.GetNodes(parent_node) @@ -230,64 +370,203 @@ class Disk(ConfigObject): # be different) return result + def RecordGrow(self, amount): + """Update the size of this disk after growth. + + This method recurses over the disks's children and updates their + size correspondigly. The method needs to be kept in sync with the + actual algorithms from bdev. + + """ + if self.dev_type == constants.LD_LV: + self.size += amount + elif self.dev_type == constants.LD_DRBD8: + if self.children: + self.children[0].RecordGrow(amount) + self.size += amount + else: + raise errors.ProgrammerError("Disk.RecordGrow called for unsupported" + " disk type %s" % self.dev_type) + + def SetPhysicalID(self, target_node, nodes_ip): + """Convert the logical ID to the physical ID. + + This is used only for drbd, which needs ip/port configuration. + + The routine descends down and updates its children also, because + this helps when the only the top device is passed to the remote + node. + + Arguments: + - target_node: the node we wish to configure for + - nodes_ip: a mapping of node name to ip + + The target_node must exist in in nodes_ip, and must be one of the + nodes in the logical ID for each of the DRBD devices encountered + in the disk tree. + + """ + if self.children: + for child in self.children: + child.SetPhysicalID(target_node, nodes_ip) + + if self.logical_id is None and self.physical_id is not None: + return + if self.dev_type in constants.LDS_DRBD: + pnode, snode, port, pminor, sminor, secret = self.logical_id + if target_node not in (pnode, snode): + raise errors.ConfigurationError("DRBD device not knowing node %s" % + target_node) + pnode_ip = nodes_ip.get(pnode, None) + snode_ip = nodes_ip.get(snode, None) + if pnode_ip is None or snode_ip is None: + raise errors.ConfigurationError("Can't find primary or secondary node" + " for %s" % str(self)) + p_data = (pnode_ip, port) + s_data = (snode_ip, port) + if pnode == target_node: + self.physical_id = p_data + s_data + (pminor, secret) + else: # it must be secondary, we tested above + self.physical_id = s_data + p_data + (sminor, secret) + else: + self.physical_id = self.logical_id + return + + def ToDict(self): + """Disk-specific conversion to standard python types. + + This replaces the children lists of objects with lists of + standard python types. -class Instance(ConfigObject): + """ + bo = super(Disk, self).ToDict() + + for attr in ("children",): + alist = bo.get(attr, None) + if alist: + bo[attr] = self._ContainerToDicts(alist) + return bo + + @classmethod + def FromDict(cls, val): + """Custom function for Disks + + """ + obj = super(Disk, cls).FromDict(val) + if obj.children: + obj.children = cls._ContainerFromDicts(obj.children, list, Disk) + if obj.logical_id and isinstance(obj.logical_id, list): + obj.logical_id = tuple(obj.logical_id) + if obj.physical_id and isinstance(obj.physical_id, list): + obj.physical_id = tuple(obj.physical_id) + if obj.dev_type in constants.LDS_DRBD: + # we need a tuple of length six here + if len(obj.logical_id) < 6: + obj.logical_id += (None,) * (6 - len(obj.logical_id)) + return obj + + def __str__(self): + """Custom str() formatter for disks. + + """ + if self.dev_type == constants.LD_LV: + val = "