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_DRBD8, constants.LD_LV)
282 def AssembleOnSecondary(self):
283 """Test if this device needs to be assembled on a secondary node."""
284 return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
286 def OpenOnSecondary(self):
287 """Test if this device needs to be opened on a secondary node."""
288 return self.dev_type in (constants.LD_LV,)
290 def StaticDevPath(self):
291 """Return the device path if this device type has a static one.
293 Some devices (LVM for example) live always at the same /dev/ path,
294 irrespective of their status. For such devices, we return this
295 path, for others we return None.
298 if self.dev_type == constants.LD_LV:
299 return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
302 def ChildrenNeeded(self):
303 """Compute the needed number of children for activation.
305 This method will return either -1 (all children) or a positive
306 number denoting the minimum number of children needed for
307 activation (only mirrored devices will usually return >=0).
309 Currently, only DRBD8 supports diskless activation (therefore we
310 return 0), for all other we keep the previous semantics and return
314 if self.dev_type == constants.LD_DRBD8:
318 def GetNodes(self, node):
319 """This function returns the nodes this device lives on.
321 Given the node on which the parent of the device lives on (or, in
322 case of a top-level device, the primary node of the devices'
323 instance), this function will return a list of nodes on which this
324 devices needs to (or can) be assembled.
327 if self.dev_type in [constants.LD_LV, constants.LD_FILE]:
329 elif self.dev_type in constants.LDS_DRBD:
330 result = [self.logical_id[0], self.logical_id[1]]
331 if node not in result:
332 raise errors.ConfigurationError("DRBD device passed unknown node")
334 raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
337 def ComputeNodeTree(self, parent_node):
338 """Compute the node/disk tree for this disk and its children.
340 This method, given the node on which the parent disk lives, will
341 return the list of all (node, disk) pairs which describe the disk
342 tree in the most compact way. For example, a drbd/lvm stack
343 will be returned as (primary_node, drbd) and (secondary_node, drbd)
344 which represents all the top-level devices on the nodes.
347 my_nodes = self.GetNodes(parent_node)
348 result = [(node, self) for node in my_nodes]
349 if not self.children:
352 for node in my_nodes:
353 for child in self.children:
354 child_result = child.ComputeNodeTree(node)
355 if len(child_result) == 1:
356 # child (and all its descendants) is simple, doesn't split
357 # over multiple hosts, so we don't need to describe it, our
358 # own entry for this node describes it completely
361 # check if child nodes differ from my nodes; note that
362 # subdisk can differ from the child itself, and be instead
363 # one of its descendants
364 for subnode, subdisk in child_result:
365 if subnode not in my_nodes:
366 result.append((subnode, subdisk))
367 # otherwise child is under our own node, so we ignore this
368 # entry (but probably the other results in the list will
372 def RecordGrow(self, amount):
373 """Update the size of this disk after growth.
375 This method recurses over the disks's children and updates their
376 size correspondigly. The method needs to be kept in sync with the
377 actual algorithms from bdev.
380 if self.dev_type == constants.LD_LV:
382 elif self.dev_type == constants.LD_DRBD8:
384 self.children[0].RecordGrow(amount)
387 raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
388 " disk type %s" % self.dev_type)
390 def SetPhysicalID(self, target_node, nodes_ip):
391 """Convert the logical ID to the physical ID.
393 This is used only for drbd, which needs ip/port configuration.
395 The routine descends down and updates its children also, because
396 this helps when the only the top device is passed to the remote
400 - target_node: the node we wish to configure for
401 - nodes_ip: a mapping of node name to ip
403 The target_node must exist in in nodes_ip, and must be one of the
404 nodes in the logical ID for each of the DRBD devices encountered
409 for child in self.children:
410 child.SetPhysicalID(target_node, nodes_ip)
412 if self.logical_id is None and self.physical_id is not None:
414 if self.dev_type in constants.LDS_DRBD:
415 pnode, snode, port, pminor, sminor = self.logical_id
416 if target_node not in (pnode, snode):
417 raise errors.ConfigurationError("DRBD device not knowing node %s" %
419 pnode_ip = nodes_ip.get(pnode, None)
420 snode_ip = nodes_ip.get(snode, None)
421 if pnode_ip is None or snode_ip is None:
422 raise errors.ConfigurationError("Can't find primary or secondary node"
423 " for %s" % str(self))
424 p_data = (pnode_ip, port)
425 s_data = (snode_ip, port)
426 if pnode == target_node:
427 self.physical_id = p_data + s_data + (pminor,)
428 else: # it must be secondary, we tested above
429 self.physical_id = s_data + p_data + (sminor,)
431 self.physical_id = self.logical_id
435 """Disk-specific conversion to standard python types.
437 This replaces the children lists of objects with lists of
438 standard python types.
441 bo = super(Disk, self).ToDict()
443 for attr in ("children",):
444 alist = bo.get(attr, None)
446 bo[attr] = self._ContainerToDicts(alist)
450 def FromDict(cls, val):
451 """Custom function for Disks
454 obj = super(Disk, cls).FromDict(val)
456 obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
457 if obj.logical_id and isinstance(obj.logical_id, list):
458 obj.logical_id = tuple(obj.logical_id)
459 if obj.physical_id and isinstance(obj.physical_id, list):
460 obj.physical_id = tuple(obj.physical_id)
461 if obj.dev_type in constants.LDS_DRBD and len(obj.logical_id) == 3:
462 # old non-minor based disk type
463 obj.logical_id += (None, None)
467 """Custom str() formatter for disks.
470 if self.dev_type == constants.LD_LV:
471 val = "<LogicalVolume(/dev/%s/%s" % self.logical_id
472 elif self.dev_type in constants.LDS_DRBD:
474 if self.physical_id is None:
477 phy = ("configured as %s:%s %s:%s" %
478 (self.physical_id[0], self.physical_id[1],
479 self.physical_id[2], self.physical_id[3]))
481 val += ("hosts=%s-%s, port=%s, %s, " %
482 (self.logical_id[0], self.logical_id[1], self.logical_id[2],
484 if self.children and self.children.count(None) == 0:
485 val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
487 val += "no local storage"
489 val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
490 (self.dev_type, self.logical_id, self.physical_id, self.children))
491 if self.iv_name is None:
492 val += ", not visible"
494 val += ", visible as /dev/%s" % self.iv_name
495 val += ", size=%dm)>" % self.size
499 class Instance(TaggableObject):
500 """Config object representing an instance."""
501 __slots__ = TaggableObject.__slots__ + [
517 "hvm_cdrom_image_path",
523 def _ComputeSecondaryNodes(self):
524 """Compute the list of secondary nodes.
526 Since the data is already there (in the drbd disks), keeping it as
527 a separate normal attribute is redundant and if not properly
528 synchronised can cause problems. Thus it's better to compute it
532 def _Helper(primary, sec_nodes, device):
533 """Recursively computes secondary nodes given a top device."""
534 if device.dev_type in constants.LDS_DRBD:
535 nodea, nodeb, dummy = device.logical_id[:3]
540 if candidate not in sec_nodes:
541 sec_nodes.append(candidate)
543 for child in device.children:
544 _Helper(primary, sec_nodes, child)
547 for device in self.disks:
548 _Helper(self.primary_node, secondary_nodes, device)
549 return tuple(secondary_nodes)
551 secondary_nodes = property(_ComputeSecondaryNodes, None, None,
552 "List of secondary nodes")
554 def MapLVsByNode(self, lvmap=None, devs=None, node=None):
555 """Provide a mapping of nodes to LVs this instance owns.
557 This function figures out what logical volumes should belong on which
558 nodes, recursing through a device tree.
561 lvmap: (optional) a dictionary to receive the 'node' : ['lv', ...] data.
564 None if lvmap arg is given.
565 Otherwise, { 'nodename' : ['volume1', 'volume2', ...], ... }
569 node = self.primary_node
572 lvmap = { node : [] }
575 if not node in lvmap:
583 if dev.dev_type == constants.LD_LV:
584 lvmap[node].append(dev.logical_id[1])
586 elif dev.dev_type in constants.LDS_DRBD:
587 if dev.logical_id[0] not in lvmap:
588 lvmap[dev.logical_id[0]] = []
590 if dev.logical_id[1] not in lvmap:
591 lvmap[dev.logical_id[1]] = []
594 self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
595 self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
598 self.MapLVsByNode(lvmap, dev.children, node)
602 def FindDisk(self, name):
603 """Find a disk given having a specified name.
605 This will return the disk which has the given iv_name.
608 for disk in self.disks:
609 if disk.iv_name == name:
615 """Instance-specific conversion to standard python types.
617 This replaces the children lists of objects with lists of standard
621 bo = super(Instance, self).ToDict()
623 for attr in "nics", "disks":
624 alist = bo.get(attr, None)
626 nlist = self._ContainerToDicts(alist)
633 def FromDict(cls, val):
634 """Custom function for instances.
637 obj = super(Instance, cls).FromDict(val)
638 obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
639 obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
643 class OS(ConfigObject):
644 """Config object representing an operating system."""
657 def FromInvalidOS(cls, err):
658 """Create an OS from an InvalidOS error.
660 This routine knows how to convert an InvalidOS error to an OS
661 object representing the broken OS with a meaningful error message.
664 if not isinstance(err, errors.InvalidOS):
665 raise errors.ProgrammerError("Trying to initialize an OS from an"
666 " invalid object of type %s" % type(err))
668 return cls(name=err.args[0], path=err.args[1], status=err.args[2])
670 def __nonzero__(self):
671 return self.status == constants.OS_VALID_STATUS
673 __bool__ = __nonzero__
676 class Node(TaggableObject):
677 """Config object representing a node."""
678 __slots__ = TaggableObject.__slots__ + [
685 class Cluster(TaggableObject):
686 """Config object representing the cluster."""
687 __slots__ = TaggableObject.__slots__ + [
698 """Custom function for cluster.
701 mydict = super(Cluster, self).ToDict()
702 mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
706 def FromDict(cls, val):
707 """Custom function for cluster.
710 obj = super(Cluster, cls).FromDict(val)
711 if not isinstance(obj.tcpudp_port_pool, set):
712 obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
716 class SerializableConfigParser(ConfigParser.SafeConfigParser):
717 """Simple wrapper over ConfigParse that allows serialization.
719 This class is basically ConfigParser.SafeConfigParser with two
720 additional methods that allow it to serialize/unserialize to/from a
725 """Dump this instance and return the string representation."""
728 return buf.getvalue()
732 """Load data from a string."""
734 cfp = SerializableConfigParser()