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.
32 from cStringIO import StringIO
34 from ganeti import errors
35 from ganeti import constants
38 __all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
39 "OS", "Node", "Cluster"]
42 class ConfigObject(object):
43 """A generic config object.
45 It has the following properties:
47 - provides somewhat safe recursive unpickling and pickling for its classes
48 - unset attributes which are defined in slots are always returned
49 as None instead of raising an error
51 Classes derived from this must always declare __slots__ (we use many
52 config objects and the memory reduction is useful.
57 def __init__(self, **kwargs):
58 for k, v in kwargs.iteritems():
61 def __getattr__(self, name):
62 if name not in self.__slots__:
63 raise AttributeError("Invalid object attribute %s.%s" %
64 (type(self).__name__, name))
67 def __setitem__(self, key, value):
68 if key not in self.__slots__:
70 setattr(self, key, value)
72 def __getstate__(self):
74 for name in self.__slots__:
75 if hasattr(self, name):
76 state[name] = getattr(self, name)
79 def __setstate__(self, state):
81 if name in self.__slots__:
82 setattr(self, name, state[name])
85 """Convert to a dict holding only standard python types.
87 The generic routine just dumps all of this object's attributes in
88 a dict. It does not work if the class has children who are
89 ConfigObjects themselves (e.g. the nics list in an Instance), in
90 which case the object should subclass the function in order to
91 make sure all objects returned are only standard python types.
94 return dict([(k, getattr(self, k, None)) for k in self.__slots__])
97 def FromDict(cls, val):
98 """Create an object from a dictionary.
100 This generic routine takes a dict, instantiates a new instance of
101 the given class, and sets attributes based on the dict content.
103 As for `ToDict`, this does not work if the class has children
104 who are ConfigObjects themselves (e.g. the nics list in an
105 Instance), in which case the object should subclass the function
106 and alter the objects.
109 if not isinstance(val, dict):
110 raise errors.ConfigurationError("Invalid object passed to FromDict:"
111 " expected dict, got %s" % type(val))
112 val_str = dict([(str(k), v) for k, v in val.iteritems()])
117 def _ContainerToDicts(container):
118 """Convert the elements of a container to standard python types.
120 This method converts a container with elements derived from
121 ConfigData to standard python types. If the container is a dict,
122 we don't touch the keys, only the values.
125 if isinstance(container, dict):
126 ret = dict([(k, v.ToDict()) for k, v in container.iteritems()])
127 elif isinstance(container, (list, tuple, set, frozenset)):
128 ret = [elem.ToDict() for elem in container]
130 raise TypeError("Invalid type %s passed to _ContainerToDicts" %
135 def _ContainerFromDicts(source, c_type, e_type):
136 """Convert a container from standard python types.
138 This method converts a container with standard python types to
139 ConfigData objects. If the container is a dict, we don't touch the
140 keys, only the values.
143 if not isinstance(c_type, type):
144 raise TypeError("Container type %s passed to _ContainerFromDicts is"
145 " not a type" % type(c_type))
147 ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
148 elif c_type in (list, tuple, set, frozenset):
149 ret = c_type([e_type.FromDict(elem) for elem in source])
151 raise TypeError("Invalid container type %s passed to"
152 " _ContainerFromDicts" % c_type)
156 """Implement __repr__ for ConfigObjects."""
157 return repr(self.ToDict())
160 class TaggableObject(ConfigObject):
161 """An generic class supporting tags.
164 __slots__ = ConfigObject.__slots__ + ["tags"]
167 def ValidateTag(tag):
168 """Check if a tag is valid.
170 If the tag is invalid, an errors.TagError will be raised. The
171 function has no return value.
174 if not isinstance(tag, basestring):
175 raise errors.TagError("Invalid tag type (not a string)")
176 if len(tag) > constants.MAX_TAG_LEN:
177 raise errors.TagError("Tag too long (>%d characters)" %
178 constants.MAX_TAG_LEN)
180 raise errors.TagError("Tags cannot be empty")
181 if not re.match("^[ \w.+*/:-]+$", tag):
182 raise errors.TagError("Tag contains invalid characters")
185 """Return the tags list.
188 tags = getattr(self, "tags", None)
190 tags = self.tags = set()
193 def AddTag(self, tag):
197 self.ValidateTag(tag)
198 tags = self.GetTags()
199 if len(tags) >= constants.MAX_TAGS_PER_OBJ:
200 raise errors.TagError("Too many tags")
201 self.GetTags().add(tag)
203 def RemoveTag(self, tag):
207 self.ValidateTag(tag)
208 tags = self.GetTags()
212 raise errors.TagError("Tag not found")
215 """Taggable-object-specific conversion to standard python types.
217 This replaces the tags set with a list.
220 bo = super(TaggableObject, self).ToDict()
222 tags = bo.get("tags", None)
223 if isinstance(tags, set):
224 bo["tags"] = list(tags)
228 def FromDict(cls, val):
229 """Custom function for instances.
232 obj = super(TaggableObject, cls).FromDict(val)
233 if hasattr(obj, "tags") and isinstance(obj.tags, list):
234 obj.tags = set(obj.tags)
238 class ConfigData(ConfigObject):
239 """Top-level config object."""
240 __slots__ = ["cluster", "nodes", "instances"]
243 """Custom function for top-level config data.
245 This just replaces the list of instances, nodes and the cluster
246 with standard python types.
249 mydict = super(ConfigData, self).ToDict()
250 mydict["cluster"] = mydict["cluster"].ToDict()
251 for key in "nodes", "instances":
252 mydict[key] = self._ContainerToDicts(mydict[key])
257 def FromDict(cls, val):
258 """Custom function for top-level config data
261 obj = super(ConfigData, cls).FromDict(val)
262 obj.cluster = Cluster.FromDict(obj.cluster)
263 obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
264 obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
268 class NIC(ConfigObject):
269 """Config object representing a network card."""
270 __slots__ = ["mac", "ip", "bridge"]
273 class Disk(ConfigObject):
274 """Config object representing a block device."""
275 __slots__ = ["dev_type", "logical_id", "physical_id",
276 "children", "iv_name", "size"]
278 def CreateOnSecondary(self):
279 """Test if this device needs to be created on a secondary node."""
280 return self.dev_type in (constants.LD_DRBD7, constants.LD_DRBD8,
283 def AssembleOnSecondary(self):
284 """Test if this device needs to be assembled on a secondary node."""
285 return self.dev_type in (constants.LD_DRBD7, constants.LD_DRBD8,
288 def OpenOnSecondary(self):
289 """Test if this device needs to be opened on a secondary node."""
290 return self.dev_type in (constants.LD_LV,)
292 def StaticDevPath(self):
293 """Return the device path if this device type has a static one.
295 Some devices (LVM for example) live always at the same /dev/ path,
296 irrespective of their status. For such devices, we return this
297 path, for others we return None.
300 if self.dev_type == constants.LD_LV:
301 return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
304 def ChildrenNeeded(self):
305 """Compute the needed number of children for activation.
307 This method will return either -1 (all children) or a positive
308 number denoting the minimum number of children needed for
309 activation (only mirrored devices will usually return >=0).
311 Currently, only DRBD8 supports diskless activation (therefore we
312 return 0), for all other we keep the previous semantics and return
316 if self.dev_type == constants.LD_DRBD8:
320 def GetNodes(self, node):
321 """This function returns the nodes this device lives on.
323 Given the node on which the parent of the device lives on (or, in
324 case of a top-level device, the primary node of the devices'
325 instance), this function will return a list of nodes on which this
326 devices needs to (or can) be assembled.
329 if self.dev_type in [constants.LD_LV, constants.LD_MD_R1,
332 elif self.dev_type in constants.LDS_DRBD:
333 result = [self.logical_id[0], self.logical_id[1]]
334 if node not in result:
335 raise errors.ConfigurationError("DRBD device passed unknown node")
337 raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
340 def ComputeNodeTree(self, parent_node):
341 """Compute the node/disk tree for this disk and its children.
343 This method, given the node on which the parent disk lives, will
344 return the list of all (node, disk) pairs which describe the disk
345 tree in the most compact way. For example, a md/drbd/lvm stack
346 will be returned as (primary_node, md) and (secondary_node, drbd)
347 which represents all the top-level devices on the nodes. This
348 means that on the primary node we need to activate the the md (and
349 recursively all its children) and on the secondary node we need to
350 activate the drbd device (and its children, the two lvm volumes).
353 my_nodes = self.GetNodes(parent_node)
354 result = [(node, self) for node in my_nodes]
355 if not self.children:
358 for node in my_nodes:
359 for child in self.children:
360 child_result = child.ComputeNodeTree(node)
361 if len(child_result) == 1:
362 # child (and all its descendants) is simple, doesn't split
363 # over multiple hosts, so we don't need to describe it, our
364 # own entry for this node describes it completely
367 # check if child nodes differ from my nodes; note that
368 # subdisk can differ from the child itself, and be instead
369 # one of its descendants
370 for subnode, subdisk in child_result:
371 if subnode not in my_nodes:
372 result.append((subnode, subdisk))
373 # otherwise child is under our own node, so we ignore this
374 # entry (but probably the other results in the list will
379 """Disk-specific conversion to standard python types.
381 This replaces the children lists of objects with lists of
382 standard python types.
385 bo = super(Disk, self).ToDict()
387 for attr in ("children",):
388 alist = bo.get(attr, None)
390 bo[attr] = self._ContainerToDicts(alist)
394 def FromDict(cls, val):
395 """Custom function for Disks
398 obj = super(Disk, cls).FromDict(val)
400 obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
401 if obj.logical_id and isinstance(obj.logical_id, list):
402 obj.logical_id = tuple(obj.logical_id)
403 if obj.physical_id and isinstance(obj.physical_id, list):
404 obj.physical_id = tuple(obj.physical_id)
408 """Custom str() formatter for disks.
411 if self.dev_type == constants.LD_LV:
412 val = "<LogicalVolume(/dev/%s/%s" % self.logical_id
413 elif self.dev_type in constants.LDS_DRBD:
414 if self.dev_type == constants.LD_DRBD7:
418 if self.physical_id is None:
421 phy = ("configured as %s:%s %s:%s" %
422 (self.physical_id[0], self.physical_id[1],
423 self.physical_id[2], self.physical_id[3]))
425 val += ("hosts=%s-%s, port=%s, %s, " %
426 (self.logical_id[0], self.logical_id[1], self.logical_id[2],
428 if self.children and self.children.count(None) == 0:
429 val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
431 val += "no local storage"
432 elif self.dev_type == constants.LD_MD_R1:
433 val = "<MD_R1(uuid=%s, children=%s" % (self.physical_id, self.children)
435 val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
436 (self.dev_type, self.logical_id, self.physical_id, self.children))
437 if self.iv_name is None:
438 val += ", not visible"
440 val += ", visible as /dev/%s" % self.iv_name
441 val += ", size=%dm)>" % self.size
445 class Instance(TaggableObject):
446 """Config object representing an instance."""
447 __slots__ = TaggableObject.__slots__ + [
463 "hvm_cdrom_image_path",
467 def _ComputeSecondaryNodes(self):
468 """Compute the list of secondary nodes.
470 Since the data is already there (in the drbd disks), keeping it as
471 a separate normal attribute is redundant and if not properly
472 synchronised can cause problems. Thus it's better to compute it
476 def _Helper(primary, sec_nodes, device):
477 """Recursively computes secondary nodes given a top device."""
478 if device.dev_type in constants.LDS_DRBD:
479 nodea, nodeb, dummy = device.logical_id
484 if candidate not in sec_nodes:
485 sec_nodes.append(candidate)
487 for child in device.children:
488 _Helper(primary, sec_nodes, child)
491 for device in self.disks:
492 _Helper(self.primary_node, secondary_nodes, device)
493 return tuple(secondary_nodes)
495 secondary_nodes = property(_ComputeSecondaryNodes, None, None,
496 "List of secondary nodes")
498 def MapLVsByNode(self, lvmap=None, devs=None, node=None):
499 """Provide a mapping of nodes to LVs this instance owns.
501 This function figures out what logical volumes should belong on which
502 nodes, recursing through a device tree.
505 lvmap: (optional) a dictionary to receive the 'node' : ['lv', ...] data.
508 None if lvmap arg is given.
509 Otherwise, { 'nodename' : ['volume1', 'volume2', ...], ... }
513 node = self.primary_node
516 lvmap = { node : [] }
519 if not node in lvmap:
527 if dev.dev_type == constants.LD_LV:
528 lvmap[node].append(dev.logical_id[1])
530 elif dev.dev_type in constants.LDS_DRBD:
531 if dev.logical_id[0] not in lvmap:
532 lvmap[dev.logical_id[0]] = []
534 if dev.logical_id[1] not in lvmap:
535 lvmap[dev.logical_id[1]] = []
538 self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
539 self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
542 self.MapLVsByNode(lvmap, dev.children, node)
546 def FindDisk(self, name):
547 """Find a disk given having a specified name.
549 This will return the disk which has the given iv_name.
552 for disk in self.disks:
553 if disk.iv_name == name:
559 """Instance-specific conversion to standard python types.
561 This replaces the children lists of objects with lists of standard
565 bo = super(Instance, self).ToDict()
567 for attr in "nics", "disks":
568 alist = bo.get(attr, None)
570 nlist = self._ContainerToDicts(alist)
577 def FromDict(cls, val):
578 """Custom function for instances.
581 obj = super(Instance, cls).FromDict(val)
582 obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
583 obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
587 class OS(ConfigObject):
588 """Config object representing an operating system."""
601 def FromInvalidOS(cls, err):
602 """Create an OS from an InvalidOS error.
604 This routine knows how to convert an InvalidOS error to an OS
605 object representing the broken OS with a meaningful error message.
608 if not isinstance(err, errors.InvalidOS):
609 raise errors.ProgrammerError("Trying to initialize an OS from an"
610 " invalid object of type %s" % type(err))
612 return cls(name=err.args[0], path=err.args[1], status=err.args[2])
614 def __nonzero__(self):
615 return self.status == constants.OS_VALID_STATUS
617 __bool__ = __nonzero__
620 class Node(TaggableObject):
621 """Config object representing a node."""
622 __slots__ = TaggableObject.__slots__ + [
629 class Cluster(TaggableObject):
630 """Config object representing the cluster."""
631 __slots__ = TaggableObject.__slots__ + [
643 """Custom function for cluster.
646 mydict = super(Cluster, self).ToDict()
647 mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
651 def FromDict(cls, val):
652 """Custom function for cluster.
655 obj = super(Cluster, cls).FromDict(val)
656 if not isinstance(obj.tcpudp_port_pool, set):
657 obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
661 class SerializableConfigParser(ConfigParser.SafeConfigParser):
662 """Simple wrapper over ConfigParse that allows serialization.
664 This class is basically ConfigParser.SafeConfigParser with two
665 additional methods that allow it to serialize/unserialize to/from a
670 """Dump this instance and return the string representation."""
673 return buf.getvalue()
677 """Load data from a string."""
679 cfp = SerializableConfigParser()