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 ("drbd", "lvm")
319 def AssembleOnSecondary(self):
320 """Test if this device needs to be assembled on a secondary node."""
321 return self.dev_type in ("drbd", "lvm")
323 def OpenOnSecondary(self):
324 """Test if this device needs to be opened on a secondary node."""
325 return self.dev_type in ("lvm",)
327 def GetNodes(self, node):
328 """This function returns the nodes this device lives on.
330 Given the node on which the parent of the device lives on (or, in
331 case of a top-level device, the primary node of the devices'
332 instance), this function will return a list of nodes on which this
333 devices needs to (or can) be assembled.
336 if self.dev_type == "lvm" or self.dev_type == "md_raid1":
338 elif self.dev_type == "drbd":
339 result = [self.logical_id[0], self.logical_id[1]]
340 if node not in result:
341 raise errors.ConfigurationError("DRBD device passed unknown node")
343 raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
346 def ComputeNodeTree(self, parent_node):
347 """Compute the node/disk tree for this disk and its children.
349 This method, given the node on which the parent disk lives, will
350 return the list of all (node, disk) pairs which describe the disk
351 tree in the most compact way. For example, a md/drbd/lvm stack
352 will be returned as (primary_node, md) and (secondary_node, drbd)
353 which represents all the top-level devices on the nodes. This
354 means that on the primary node we need to activate the the md (and
355 recursively all its children) and on the secondary node we need to
356 activate the drbd device (and its children, the two lvm volumes).
359 my_nodes = self.GetNodes(parent_node)
360 result = [(node, self) for node in my_nodes]
361 if not self.children:
364 for node in my_nodes:
365 for child in self.children:
366 child_result = child.ComputeNodeTree(node)
367 if len(child_result) == 1:
368 # child (and all its descendants) is simple, doesn't split
369 # over multiple hosts, so we don't need to describe it, our
370 # own entry for this node describes it completely
373 # check if child nodes differ from my nodes; note that
374 # subdisk can differ from the child itself, and be instead
375 # one of its descendants
376 for subnode, subdisk in child_result:
377 if subnode not in my_nodes:
378 result.append((subnode, subdisk))
379 # otherwise child is under our own node, so we ignore this
380 # entry (but probably the other results in the list will
385 """Disk-specific conversion to standard python types.
387 This replaces the children lists of objects with lists of
388 standard python types.
391 bo = super(Disk, self).ToDict()
393 for attr in ("children",):
394 alist = bo.get(attr, None)
396 bo[attr] = self._ContainerToDicts(alist)
400 def FromDict(cls, val):
401 """Custom function for Disks
404 obj = super(Disk, cls).FromDict(val)
406 obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
407 if obj.logical_id and isinstance(obj.logical_id, list):
408 obj.logical_id = tuple(obj.logical_id)
409 if obj.physical_id and isinstance(obj.physical_id, list):
410 obj.physical_id = tuple(obj.physical_id)
414 class Instance(TaggableObject):
415 """Config object representing an instance."""
416 __slots__ = TaggableObject.__slots__ + [
428 def _ComputeSecondaryNodes(self):
429 """Compute the list of secondary nodes.
431 Since the data is already there (in the drbd disks), keeping it as
432 a separate normal attribute is redundant and if not properly
433 synchronised can cause problems. Thus it's better to compute it
437 def _Helper(primary, sec_nodes, device):
438 """Recursively computes secondary nodes given a top device."""
439 if device.dev_type == 'drbd':
440 nodea, nodeb, dummy = device.logical_id
445 if candidate not in sec_nodes:
446 sec_nodes.append(candidate)
448 for child in device.children:
449 _Helper(primary, sec_nodes, child)
452 for device in self.disks:
453 _Helper(self.primary_node, secondary_nodes, device)
454 return tuple(secondary_nodes)
456 secondary_nodes = property(_ComputeSecondaryNodes, None, None,
457 "List of secondary nodes")
459 def MapLVsByNode(self, lvmap=None, devs=None, node=None):
460 """Provide a mapping of nodes to LVs this instance owns.
462 This function figures out what logical volumes should belong on which
463 nodes, recursing through a device tree.
466 lvmap: (optional) a dictionary to receive the 'node' : ['lv', ...] data.
469 None if lvmap arg is given.
470 Otherwise, { 'nodename' : ['volume1', 'volume2', ...], ... }
474 node = self.primary_node
477 lvmap = { node : [] }
480 if not node in lvmap:
488 if dev.dev_type == "lvm":
489 lvmap[node].append(dev.logical_id[1])
491 elif dev.dev_type == "drbd":
492 if dev.logical_id[0] not in lvmap:
493 lvmap[dev.logical_id[0]] = []
495 if dev.logical_id[1] not in lvmap:
496 lvmap[dev.logical_id[1]] = []
499 self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
500 self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
503 self.MapLVsByNode(lvmap, dev.children, node)
507 def FindDisk(self, name):
508 """Find a disk given having a specified name.
510 This will return the disk which has the given iv_name.
513 for disk in self.disks:
514 if disk.iv_name == name:
520 """Instance-specific conversion to standard python types.
522 This replaces the children lists of objects with lists of standard
526 bo = super(Instance, self).ToDict()
528 for attr in "nics", "disks":
529 alist = bo.get(attr, None)
531 nlist = self._ContainerToDicts(alist)
538 def FromDict(cls, val):
539 """Custom function for instances.
542 obj = super(Instance, cls).FromDict(val)
543 obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
544 obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
548 class OS(ConfigObject):
549 """Config object representing an operating system."""
561 class Node(TaggableObject):
562 """Config object representing a node."""
563 __slots__ = TaggableObject.__slots__ + [
570 class Cluster(TaggableObject):
571 """Config object representing the cluster."""
572 __slots__ = TaggableObject.__slots__ + [
584 """Custom function for cluster.
587 mydict = super(Cluster, self).ToDict()
588 mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
592 def FromDict(cls, val):
593 """Custom function for cluster.
596 obj = super(Cluster, cls).FromDict(val)
597 if not isinstance(obj.tcpudp_port_pool, set):
598 obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
602 class SerializableConfigParser(ConfigParser.SafeConfigParser):
603 """Simple wrapper over ConfigParse that allows serialization.
605 This class is basically ConfigParser.SafeConfigParser with two
606 additional methods that allow it to serialize/unserialize to/from a
611 """Dump this instance and return the string representation."""
614 return buf.getvalue()
618 """Load data from a string."""
620 cfp = SerializableConfigParser()