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"]
43 # Check whether the simplejson module supports indentation
46 simplejson.dumps(1, indent=_JSON_INDENT)
51 class ConfigObject(object):
52 """A generic config object.
54 It has the following properties:
56 - provides somewhat safe recursive unpickling and pickling for its classes
57 - unset attributes which are defined in slots are always returned
58 as None instead of raising an error
60 Classes derived from this must always declare __slots__ (we use many
61 config objects and the memory reduction is useful.
66 def __init__(self, **kwargs):
67 for k, v in kwargs.iteritems():
70 def __getattr__(self, name):
71 if name not in self.__slots__:
72 raise AttributeError("Invalid object attribute %s.%s" %
73 (type(self).__name__, name))
76 def __setitem__(self, key, value):
77 if key not in self.__slots__:
79 setattr(self, key, value)
81 def __getstate__(self):
83 for name in self.__slots__:
84 if hasattr(self, name):
85 state[name] = getattr(self, name)
88 def __setstate__(self, state):
90 if name in self.__slots__:
91 setattr(self, name, state[name])
94 """Dump to a file object.
98 if _JSON_INDENT is None:
99 simplejson.dump(data, fobj)
101 simplejson.dump(data, fobj, indent=_JSON_INDENT)
105 """Load data from the given stream.
108 return cls.FromDict(simplejson.load(fobj))
111 """Dump and return the string representation."""
114 return buf.getvalue()
117 def Loads(cls, data):
118 """Load data from a string."""
119 return cls.Load(StringIO(data))
122 """Convert to a dict holding only standard python types.
124 The generic routine just dumps all of this object's attributes in
125 a dict. It does not work if the class has children who are
126 ConfigObjects themselves (e.g. the nics list in an Instance), in
127 which case the object should subclass the function in order to
128 make sure all objects returned are only standard python types.
131 return dict([(k, getattr(self, k, None)) for k in self.__slots__])
134 def FromDict(cls, val):
135 """Create an object from a dictionary.
137 This generic routine takes a dict, instantiates a new instance of
138 the given class, and sets attributes based on the dict content.
140 As for `ToDict`, this does not work if the class has children
141 who are ConfigObjects themselves (e.g. the nics list in an
142 Instance), in which case the object should subclass the function
143 and alter the objects.
146 if not isinstance(val, dict):
147 raise errors.ConfigurationError("Invalid object passed to FromDict:"
148 " expected dict, got %s" % type(val))
149 val_str = dict([(str(k), v) for k, v in val.iteritems()])
154 def _ContainerToDicts(container):
155 """Convert the elements of a container to standard python types.
157 This method converts a container with elements derived from
158 ConfigData to standard python types. If the container is a dict,
159 we don't touch the keys, only the values.
162 if isinstance(container, dict):
163 ret = dict([(k, v.ToDict()) for k, v in container.iteritems()])
164 elif isinstance(container, (list, tuple, set, frozenset)):
165 ret = [elem.ToDict() for elem in container]
167 raise TypeError("Invalid type %s passed to _ContainerToDicts" %
172 def _ContainerFromDicts(source, c_type, e_type):
173 """Convert a container from standard python types.
175 This method converts a container with standard python types to
176 ConfigData objects. If the container is a dict, we don't touch the
177 keys, only the values.
180 if not isinstance(c_type, type):
181 raise TypeError("Container type %s passed to _ContainerFromDicts is"
182 " not a type" % type(c_type))
184 ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
185 elif c_type in (list, tuple, set, frozenset):
186 ret = c_type([e_type.FromDict(elem) for elem in source])
188 raise TypeError("Invalid container type %s passed to"
189 " _ContainerFromDicts" % c_type)
193 """Implement __repr__ for ConfigObjects."""
194 return repr(self.ToDict())
197 class TaggableObject(ConfigObject):
198 """An generic class supporting tags.
201 __slots__ = ConfigObject.__slots__ + ["tags"]
204 def ValidateTag(tag):
205 """Check if a tag is valid.
207 If the tag is invalid, an errors.TagError will be raised. The
208 function has no return value.
211 if not isinstance(tag, basestring):
212 raise errors.TagError("Invalid tag type (not a string)")
213 if len(tag) > constants.MAX_TAG_LEN:
214 raise errors.TagError("Tag too long (>%d characters)" %
215 constants.MAX_TAG_LEN)
217 raise errors.TagError("Tags cannot be empty")
218 if not re.match("^[ \w.+*/:-]+$", tag):
219 raise errors.TagError("Tag contains invalid characters")
222 """Return the tags list.
225 tags = getattr(self, "tags", None)
227 tags = self.tags = set()
230 def AddTag(self, tag):
234 self.ValidateTag(tag)
235 tags = self.GetTags()
236 if len(tags) >= constants.MAX_TAGS_PER_OBJ:
237 raise errors.TagError("Too many tags")
238 self.GetTags().add(tag)
240 def RemoveTag(self, tag):
244 self.ValidateTag(tag)
245 tags = self.GetTags()
249 raise errors.TagError("Tag not found")
252 """Taggable-object-specific conversion to standard python types.
254 This replaces the tags set with a list.
257 bo = super(TaggableObject, self).ToDict()
259 tags = bo.get("tags", None)
260 if isinstance(tags, set):
261 bo["tags"] = list(tags)
265 def FromDict(cls, val):
266 """Custom function for instances.
269 obj = super(TaggableObject, cls).FromDict(val)
270 if hasattr(obj, "tags") and isinstance(obj.tags, list):
271 obj.tags = set(obj.tags)
275 class ConfigData(ConfigObject):
276 """Top-level config object."""
277 __slots__ = ["cluster", "nodes", "instances"]
280 """Custom function for top-level config data.
282 This just replaces the list of instances, nodes and the cluster
283 with standard python types.
286 mydict = super(ConfigData, self).ToDict()
287 mydict["cluster"] = mydict["cluster"].ToDict()
288 for key in "nodes", "instances":
289 mydict[key] = self._ContainerToDicts(mydict[key])
294 def FromDict(cls, val):
295 """Custom function for top-level config data
298 obj = super(ConfigData, cls).FromDict(val)
299 obj.cluster = Cluster.FromDict(obj.cluster)
300 obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
301 obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
305 class NIC(ConfigObject):
306 """Config object representing a network card."""
307 __slots__ = ["mac", "ip", "bridge"]
310 class Disk(ConfigObject):
311 """Config object representing a block device."""
312 __slots__ = ["dev_type", "logical_id", "physical_id",
313 "children", "iv_name", "size"]
315 def CreateOnSecondary(self):
316 """Test if this device needs to be created on a secondary node."""
317 return self.dev_type in (constants.LD_DRBD7, constants.LD_DRBD8,
320 def AssembleOnSecondary(self):
321 """Test if this device needs to be assembled on a secondary node."""
322 return self.dev_type in (constants.LD_DRBD7, constants.LD_DRBD8,
325 def OpenOnSecondary(self):
326 """Test if this device needs to be opened on a secondary node."""
327 return self.dev_type in (constants.LD_LV,)
329 def StaticDevPath(self):
330 """Return the device path if this device type has a static one.
332 Some devices (LVM for example) live always at the same /dev/ path,
333 irrespective of their status. For such devices, we return this
334 path, for others we return None.
337 if self.dev_type == constants.LD_LV:
338 return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
341 def ChildrenNeeded(self):
342 """Compute the needed number of children for activation.
344 This method will return either -1 (all children) or a positive
345 number denoting the minimum number of children needed for
346 activation (only mirrored devices will usually return >=0).
348 Currently, only DRBD8 supports diskless activation (therefore we
349 return 0), for all other we keep the previous semantics and return
353 if self.dev_type == constants.LD_DRBD8:
357 def GetNodes(self, node):
358 """This function returns the nodes this device lives on.
360 Given the node on which the parent of the device lives on (or, in
361 case of a top-level device, the primary node of the devices'
362 instance), this function will return a list of nodes on which this
363 devices needs to (or can) be assembled.
366 if self.dev_type == constants.LD_LV or self.dev_type == constants.LD_MD_R1:
368 elif self.dev_type in constants.LDS_DRBD:
369 result = [self.logical_id[0], self.logical_id[1]]
370 if node not in result:
371 raise errors.ConfigurationError("DRBD device passed unknown node")
373 raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
376 def ComputeNodeTree(self, parent_node):
377 """Compute the node/disk tree for this disk and its children.
379 This method, given the node on which the parent disk lives, will
380 return the list of all (node, disk) pairs which describe the disk
381 tree in the most compact way. For example, a md/drbd/lvm stack
382 will be returned as (primary_node, md) and (secondary_node, drbd)
383 which represents all the top-level devices on the nodes. This
384 means that on the primary node we need to activate the the md (and
385 recursively all its children) and on the secondary node we need to
386 activate the drbd device (and its children, the two lvm volumes).
389 my_nodes = self.GetNodes(parent_node)
390 result = [(node, self) for node in my_nodes]
391 if not self.children:
394 for node in my_nodes:
395 for child in self.children:
396 child_result = child.ComputeNodeTree(node)
397 if len(child_result) == 1:
398 # child (and all its descendants) is simple, doesn't split
399 # over multiple hosts, so we don't need to describe it, our
400 # own entry for this node describes it completely
403 # check if child nodes differ from my nodes; note that
404 # subdisk can differ from the child itself, and be instead
405 # one of its descendants
406 for subnode, subdisk in child_result:
407 if subnode not in my_nodes:
408 result.append((subnode, subdisk))
409 # otherwise child is under our own node, so we ignore this
410 # entry (but probably the other results in the list will
415 """Disk-specific conversion to standard python types.
417 This replaces the children lists of objects with lists of
418 standard python types.
421 bo = super(Disk, self).ToDict()
423 for attr in ("children",):
424 alist = bo.get(attr, None)
426 bo[attr] = self._ContainerToDicts(alist)
430 def FromDict(cls, val):
431 """Custom function for Disks
434 obj = super(Disk, cls).FromDict(val)
436 obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
437 if obj.logical_id and isinstance(obj.logical_id, list):
438 obj.logical_id = tuple(obj.logical_id)
439 if obj.physical_id and isinstance(obj.physical_id, list):
440 obj.physical_id = tuple(obj.physical_id)
444 """Custom str() formatter for disks.
447 if self.dev_type == constants.LD_LV:
448 val = "<LogicalVolume(/dev/%s/%s" % self.logical_id
449 elif self.dev_type in constants.LDS_DRBD:
450 if self.dev_type == constants.LD_DRBD7:
454 if self.physical_id is None:
457 phy = ("configured as %s:%s %s:%s" %
458 (self.physical_id[0], self.physical_id[1],
459 self.physical_id[2], self.physical_id[3]))
461 val += ("hosts=%s-%s, port=%s, %s, " %
462 (self.logical_id[0], self.logical_id[1], self.logical_id[2],
464 if self.children and self.children.count(None) == 0:
465 val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
467 val += "no local storage"
468 elif self.dev_type == constants.LD_MD_R1:
469 val = "<MD_R1(uuid=%s, children=%s" % (self.physical_id, self.children)
471 val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
472 (self.dev_type, self.logical_id, self.physical_id, self.children))
473 if self.iv_name is None:
474 val += ", not visible"
476 val += ", visible as /dev/%s" % self.iv_name
477 val += ", size=%dm)>" % self.size
481 class Instance(TaggableObject):
482 """Config object representing an instance."""
483 __slots__ = TaggableObject.__slots__ + [
499 def _ComputeSecondaryNodes(self):
500 """Compute the list of secondary nodes.
502 Since the data is already there (in the drbd disks), keeping it as
503 a separate normal attribute is redundant and if not properly
504 synchronised can cause problems. Thus it's better to compute it
508 def _Helper(primary, sec_nodes, device):
509 """Recursively computes secondary nodes given a top device."""
510 if device.dev_type in constants.LDS_DRBD:
511 nodea, nodeb, dummy = device.logical_id
516 if candidate not in sec_nodes:
517 sec_nodes.append(candidate)
519 for child in device.children:
520 _Helper(primary, sec_nodes, child)
523 for device in self.disks:
524 _Helper(self.primary_node, secondary_nodes, device)
525 return tuple(secondary_nodes)
527 secondary_nodes = property(_ComputeSecondaryNodes, None, None,
528 "List of secondary nodes")
530 def MapLVsByNode(self, lvmap=None, devs=None, node=None):
531 """Provide a mapping of nodes to LVs this instance owns.
533 This function figures out what logical volumes should belong on which
534 nodes, recursing through a device tree.
537 lvmap: (optional) a dictionary to receive the 'node' : ['lv', ...] data.
540 None if lvmap arg is given.
541 Otherwise, { 'nodename' : ['volume1', 'volume2', ...], ... }
545 node = self.primary_node
548 lvmap = { node : [] }
551 if not node in lvmap:
559 if dev.dev_type == constants.LD_LV:
560 lvmap[node].append(dev.logical_id[1])
562 elif dev.dev_type in constants.LDS_DRBD:
563 if dev.logical_id[0] not in lvmap:
564 lvmap[dev.logical_id[0]] = []
566 if dev.logical_id[1] not in lvmap:
567 lvmap[dev.logical_id[1]] = []
570 self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
571 self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
574 self.MapLVsByNode(lvmap, dev.children, node)
578 def FindDisk(self, name):
579 """Find a disk given having a specified name.
581 This will return the disk which has the given iv_name.
584 for disk in self.disks:
585 if disk.iv_name == name:
591 """Instance-specific conversion to standard python types.
593 This replaces the children lists of objects with lists of standard
597 bo = super(Instance, self).ToDict()
599 for attr in "nics", "disks":
600 alist = bo.get(attr, None)
602 nlist = self._ContainerToDicts(alist)
609 def FromDict(cls, val):
610 """Custom function for instances.
613 obj = super(Instance, cls).FromDict(val)
614 obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
615 obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
619 class OS(ConfigObject):
620 """Config object representing an operating system."""
633 def FromInvalidOS(cls, err):
634 """Create an OS from an InvalidOS error.
636 This routine knows how to convert an InvalidOS error to an OS
637 object representing the broken OS with a meaningful error message.
640 if not isinstance(err, errors.InvalidOS):
641 raise errors.ProgrammerError("Trying to initialize an OS from an"
642 " invalid object of type %s" % type(err))
644 return cls(name=err.args[0], path=err.args[1], status=err.args[2])
646 def __nonzero__(self):
647 return self.status == constants.OS_VALID_STATUS
649 __bool__ = __nonzero__
651 class Node(TaggableObject):
652 """Config object representing a node."""
653 __slots__ = TaggableObject.__slots__ + [
660 class Cluster(TaggableObject):
661 """Config object representing the cluster."""
662 __slots__ = TaggableObject.__slots__ + [
674 """Custom function for cluster.
677 mydict = super(Cluster, self).ToDict()
678 mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
682 def FromDict(cls, val):
683 """Custom function for cluster.
686 obj = super(Cluster, cls).FromDict(val)
687 if not isinstance(obj.tcpudp_port_pool, set):
688 obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
692 class SerializableConfigParser(ConfigParser.SafeConfigParser):
693 """Simple wrapper over ConfigParse that allows serialization.
695 This class is basically ConfigParser.SafeConfigParser with two
696 additional methods that allow it to serialize/unserialize to/from a
701 """Dump this instance and return the string representation."""
704 return buf.getvalue()
708 """Load data from a string."""
710 cfp = SerializableConfigParser()