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 == constants.LD_LV or self.dev_type == constants.LD_MD_R1:
331 elif self.dev_type in constants.LDS_DRBD:
332 result = [self.logical_id[0], self.logical_id[1]]
333 if node not in result:
334 raise errors.ConfigurationError("DRBD device passed unknown node")
336 raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
339 def ComputeNodeTree(self, parent_node):
340 """Compute the node/disk tree for this disk and its children.
342 This method, given the node on which the parent disk lives, will
343 return the list of all (node, disk) pairs which describe the disk
344 tree in the most compact way. For example, a md/drbd/lvm stack
345 will be returned as (primary_node, md) and (secondary_node, drbd)
346 which represents all the top-level devices on the nodes. This
347 means that on the primary node we need to activate the the md (and
348 recursively all its children) and on the secondary node we need to
349 activate the drbd device (and its children, the two lvm volumes).
352 my_nodes = self.GetNodes(parent_node)
353 result = [(node, self) for node in my_nodes]
354 if not self.children:
357 for node in my_nodes:
358 for child in self.children:
359 child_result = child.ComputeNodeTree(node)
360 if len(child_result) == 1:
361 # child (and all its descendants) is simple, doesn't split
362 # over multiple hosts, so we don't need to describe it, our
363 # own entry for this node describes it completely
366 # check if child nodes differ from my nodes; note that
367 # subdisk can differ from the child itself, and be instead
368 # one of its descendants
369 for subnode, subdisk in child_result:
370 if subnode not in my_nodes:
371 result.append((subnode, subdisk))
372 # otherwise child is under our own node, so we ignore this
373 # entry (but probably the other results in the list will
377 def RecordGrow(self, amount):
378 """Update the size of this disk after growth.
380 This method recurses over the disks's children and updates their
381 size correspondigly. The method needs to be kept in sync with the
382 actual algorithms from bdev.
385 if self.dev_type == constants.LD_LV:
387 elif self.dev_type == constants.LD_DRBD8:
389 self.children[0].RecordGrow(amount)
392 raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
393 " disk type %s" % self.dev_type)
396 """Disk-specific conversion to standard python types.
398 This replaces the children lists of objects with lists of
399 standard python types.
402 bo = super(Disk, self).ToDict()
404 for attr in ("children",):
405 alist = bo.get(attr, None)
407 bo[attr] = self._ContainerToDicts(alist)
411 def FromDict(cls, val):
412 """Custom function for Disks
415 obj = super(Disk, cls).FromDict(val)
417 obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
418 if obj.logical_id and isinstance(obj.logical_id, list):
419 obj.logical_id = tuple(obj.logical_id)
420 if obj.physical_id and isinstance(obj.physical_id, list):
421 obj.physical_id = tuple(obj.physical_id)
425 """Custom str() formatter for disks.
428 if self.dev_type == constants.LD_LV:
429 val = "<LogicalVolume(/dev/%s/%s" % self.logical_id
430 elif self.dev_type in constants.LDS_DRBD:
431 if self.dev_type == constants.LD_DRBD7:
435 if self.physical_id is None:
438 phy = ("configured as %s:%s %s:%s" %
439 (self.physical_id[0], self.physical_id[1],
440 self.physical_id[2], self.physical_id[3]))
442 val += ("hosts=%s-%s, port=%s, %s, " %
443 (self.logical_id[0], self.logical_id[1], self.logical_id[2],
445 if self.children and self.children.count(None) == 0:
446 val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
448 val += "no local storage"
449 elif self.dev_type == constants.LD_MD_R1:
450 val = "<MD_R1(uuid=%s, children=%s" % (self.physical_id, self.children)
452 val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
453 (self.dev_type, self.logical_id, self.physical_id, self.children))
454 if self.iv_name is None:
455 val += ", not visible"
457 val += ", visible as /dev/%s" % self.iv_name
458 val += ", size=%dm)>" % self.size
462 class Instance(TaggableObject):
463 """Config object representing an instance."""
464 __slots__ = TaggableObject.__slots__ + [
480 "hvm_cdrom_image_path",
484 def _ComputeSecondaryNodes(self):
485 """Compute the list of secondary nodes.
487 Since the data is already there (in the drbd disks), keeping it as
488 a separate normal attribute is redundant and if not properly
489 synchronised can cause problems. Thus it's better to compute it
493 def _Helper(primary, sec_nodes, device):
494 """Recursively computes secondary nodes given a top device."""
495 if device.dev_type in constants.LDS_DRBD:
496 nodea, nodeb, dummy = device.logical_id
501 if candidate not in sec_nodes:
502 sec_nodes.append(candidate)
504 for child in device.children:
505 _Helper(primary, sec_nodes, child)
508 for device in self.disks:
509 _Helper(self.primary_node, secondary_nodes, device)
510 return tuple(secondary_nodes)
512 secondary_nodes = property(_ComputeSecondaryNodes, None, None,
513 "List of secondary nodes")
515 def MapLVsByNode(self, lvmap=None, devs=None, node=None):
516 """Provide a mapping of nodes to LVs this instance owns.
518 This function figures out what logical volumes should belong on which
519 nodes, recursing through a device tree.
522 lvmap: (optional) a dictionary to receive the 'node' : ['lv', ...] data.
525 None if lvmap arg is given.
526 Otherwise, { 'nodename' : ['volume1', 'volume2', ...], ... }
530 node = self.primary_node
533 lvmap = { node : [] }
536 if not node in lvmap:
544 if dev.dev_type == constants.LD_LV:
545 lvmap[node].append(dev.logical_id[1])
547 elif dev.dev_type in constants.LDS_DRBD:
548 if dev.logical_id[0] not in lvmap:
549 lvmap[dev.logical_id[0]] = []
551 if dev.logical_id[1] not in lvmap:
552 lvmap[dev.logical_id[1]] = []
555 self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
556 self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
559 self.MapLVsByNode(lvmap, dev.children, node)
563 def FindDisk(self, name):
564 """Find a disk given having a specified name.
566 This will return the disk which has the given iv_name.
569 for disk in self.disks:
570 if disk.iv_name == name:
576 """Instance-specific conversion to standard python types.
578 This replaces the children lists of objects with lists of standard
582 bo = super(Instance, self).ToDict()
584 for attr in "nics", "disks":
585 alist = bo.get(attr, None)
587 nlist = self._ContainerToDicts(alist)
594 def FromDict(cls, val):
595 """Custom function for instances.
598 obj = super(Instance, cls).FromDict(val)
599 obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
600 obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
604 class OS(ConfigObject):
605 """Config object representing an operating system."""
618 def FromInvalidOS(cls, err):
619 """Create an OS from an InvalidOS error.
621 This routine knows how to convert an InvalidOS error to an OS
622 object representing the broken OS with a meaningful error message.
625 if not isinstance(err, errors.InvalidOS):
626 raise errors.ProgrammerError("Trying to initialize an OS from an"
627 " invalid object of type %s" % type(err))
629 return cls(name=err.args[0], path=err.args[1], status=err.args[2])
631 def __nonzero__(self):
632 return self.status == constants.OS_VALID_STATUS
634 __bool__ = __nonzero__
636 class Node(TaggableObject):
637 """Config object representing a node."""
638 __slots__ = TaggableObject.__slots__ + [
645 class Cluster(TaggableObject):
646 """Config object representing the cluster."""
647 __slots__ = TaggableObject.__slots__ + [
659 """Custom function for cluster.
662 mydict = super(Cluster, self).ToDict()
663 mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
667 def FromDict(cls, val):
668 """Custom function for cluster.
671 obj = super(Cluster, cls).FromDict(val)
672 if not isinstance(obj.tcpudp_port_pool, set):
673 obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
677 class SerializableConfigParser(ConfigParser.SafeConfigParser):
678 """Simple wrapper over ConfigParse that allows serialization.
680 This class is basically ConfigParser.SafeConfigParser with two
681 additional methods that allow it to serialize/unserialize to/from a
686 """Dump this instance and return the string representation."""
689 return buf.getvalue()
693 """Load data from a string."""
695 cfp = SerializableConfigParser()