Two small style fixes
[ganeti-local] / lib / objects.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007 Google Inc.
5 #
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.
10 #
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.
15 #
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
19 # 02110-1301, USA.
20
21
22 """Transportable objects for Ganeti.
23
24 This module provides small, mostly data-only objects which are safe to
25 pass to and from external parties.
26
27 """
28
29
30 import simplejson
31 import ConfigParser
32 import re
33 from cStringIO import StringIO
34
35 from ganeti import errors
36 from ganeti import constants
37
38
39 __all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
40            "OS", "Node", "Cluster"]
41
42
43 # Check whether the simplejson module supports indentation
44 _JSON_INDENT = 2
45 try:
46   simplejson.dumps(1, indent=_JSON_INDENT)
47 except TypeError:
48   _JSON_INDENT = None
49
50
51 class ConfigObject(object):
52   """A generic config object.
53
54   It has the following properties:
55
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
59
60   Classes derived from this must always declare __slots__ (we use many
61   config objects and the memory reduction is useful.
62
63   """
64   __slots__ = []
65
66   def __init__(self, **kwargs):
67     for k, v in kwargs.iteritems():
68       setattr(self, k, v)
69
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))
74     return None
75
76   def __setitem__(self, key, value):
77     if key not in self.__slots__:
78       raise KeyError(key)
79     setattr(self, key, value)
80
81   def __getstate__(self):
82     state = {}
83     for name in self.__slots__:
84       if hasattr(self, name):
85         state[name] = getattr(self, name)
86     return state
87
88   def __setstate__(self, state):
89     for name in state:
90       if name in self.__slots__:
91         setattr(self, name, state[name])
92
93   def Dump(self, fobj):
94     """Dump to a file object.
95
96     """
97     data = self.ToDict()
98     if _JSON_INDENT is None:
99       simplejson.dump(data, fobj)
100     else:
101       simplejson.dump(data, fobj, indent=_JSON_INDENT)
102
103   @classmethod
104   def Load(cls, fobj):
105     """Load data from the given stream.
106
107     """
108     return cls.FromDict(simplejson.load(fobj))
109
110   def Dumps(self):
111     """Dump and return the string representation."""
112     buf = StringIO()
113     self.Dump(buf)
114     return buf.getvalue()
115
116   @classmethod
117   def Loads(cls, data):
118     """Load data from a string."""
119     return cls.Load(StringIO(data))
120
121   def ToDict(self):
122     """Convert to a dict holding only standard python types.
123
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.
129
130     """
131     return dict([(k, getattr(self, k, None)) for k in self.__slots__])
132
133   @classmethod
134   def FromDict(cls, val):
135     """Create an object from a dictionary.
136
137     This generic routine takes a dict, instantiates a new instance of
138     the given class, and sets attributes based on the dict content.
139
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.
144
145     """
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()])
150     obj = cls(**val_str)
151     return obj
152
153   @staticmethod
154   def _ContainerToDicts(container):
155     """Convert the elements of a container to standard python types.
156
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.
160
161     """
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]
166     else:
167       raise TypeError("Invalid type %s passed to _ContainerToDicts" %
168                       type(container))
169     return ret
170
171   @staticmethod
172   def _ContainerFromDicts(source, c_type, e_type):
173     """Convert a container from standard python types.
174
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.
178
179     """
180     if not isinstance(c_type, type):
181       raise TypeError("Container type %s passed to _ContainerFromDicts is"
182                       " not a type" % type(c_type))
183     if c_type is dict:
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])
187     else:
188       raise TypeError("Invalid container type %s passed to"
189                       " _ContainerFromDicts" % c_type)
190     return ret
191
192   def __repr__(self):
193     """Implement __repr__ for ConfigObjects."""
194     return repr(self.ToDict())
195
196
197 class TaggableObject(ConfigObject):
198   """An generic class supporting tags.
199
200   """
201   __slots__ = ConfigObject.__slots__ + ["tags"]
202
203   @staticmethod
204   def ValidateTag(tag):
205     """Check if a tag is valid.
206
207     If the tag is invalid, an errors.TagError will be raised. The
208     function has no return value.
209
210     """
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)
216     if not tag:
217       raise errors.TagError("Tags cannot be empty")
218     if not re.match("^[ \w.+*/:-]+$", tag):
219       raise errors.TagError("Tag contains invalid characters")
220
221   def GetTags(self):
222     """Return the tags list.
223
224     """
225     tags = getattr(self, "tags", None)
226     if tags is None:
227       tags = self.tags = set()
228     return tags
229
230   def AddTag(self, tag):
231     """Add a new tag.
232
233     """
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)
239
240   def RemoveTag(self, tag):
241     """Remove a tag.
242
243     """
244     self.ValidateTag(tag)
245     tags = self.GetTags()
246     try:
247       tags.remove(tag)
248     except KeyError:
249       raise errors.TagError("Tag not found")
250
251   def ToDict(self):
252     """Taggable-object-specific conversion to standard python types.
253
254     This replaces the tags set with a list.
255
256     """
257     bo = super(TaggableObject, self).ToDict()
258
259     tags = bo.get("tags", None)
260     if isinstance(tags, set):
261       bo["tags"] = list(tags)
262     return bo
263
264   @classmethod
265   def FromDict(cls, val):
266     """Custom function for instances.
267
268     """
269     obj = super(TaggableObject, cls).FromDict(val)
270     if hasattr(obj, "tags") and isinstance(obj.tags, list):
271       obj.tags = set(obj.tags)
272     return obj
273
274
275 class ConfigData(ConfigObject):
276   """Top-level config object."""
277   __slots__ = ["cluster", "nodes", "instances"]
278
279   def ToDict(self):
280     """Custom function for top-level config data.
281
282     This just replaces the list of instances, nodes and the cluster
283     with standard python types.
284
285     """
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])
290
291     return mydict
292
293   @classmethod
294   def FromDict(cls, val):
295     """Custom function for top-level config data
296
297     """
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)
302     return obj
303
304
305 class NIC(ConfigObject):
306   """Config object representing a network card."""
307   __slots__ = ["mac", "ip", "bridge"]
308
309
310 class Disk(ConfigObject):
311   """Config object representing a block device."""
312   __slots__ = ["dev_type", "logical_id", "physical_id",
313                "children", "iv_name", "size"]
314
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,
318                              constants.LD_LV)
319
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,
323                              constants.LD_LV)
324
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,)
328
329   def StaticDevPath(self):
330     """Return the device path if this device type has a static one.
331
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.
335
336     """
337     if self.dev_type == constants.LD_LV:
338       return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
339     return None
340
341   def ChildrenNeeded(self):
342     """Compute the needed number of children for activation.
343
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).
347
348     Currently, only DRBD8 supports diskless activation (therefore we
349     return 0), for all other we keep the previous semantics and return
350     -1.
351
352     """
353     if self.dev_type == constants.LD_DRBD8:
354       return 0
355     return -1
356
357   def GetNodes(self, node):
358     """This function returns the nodes this device lives on.
359
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.
364
365     """
366     if self.dev_type == constants.LD_LV or self.dev_type == constants.LD_MD_R1:
367       result = [node]
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")
372     else:
373       raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
374     return result
375
376   def ComputeNodeTree(self, parent_node):
377     """Compute the node/disk tree for this disk and its children.
378
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).
387
388     """
389     my_nodes = self.GetNodes(parent_node)
390     result = [(node, self) for node in my_nodes]
391     if not self.children:
392       # leaf device
393       return result
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
401           continue
402         else:
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
411             # be different)
412     return result
413
414   def ToDict(self):
415     """Disk-specific conversion to standard python types.
416
417     This replaces the children lists of objects with lists of
418     standard python types.
419
420     """
421     bo = super(Disk, self).ToDict()
422
423     for attr in ("children",):
424       alist = bo.get(attr, None)
425       if alist:
426         bo[attr] = self._ContainerToDicts(alist)
427     return bo
428
429   @classmethod
430   def FromDict(cls, val):
431     """Custom function for Disks
432
433     """
434     obj = super(Disk, cls).FromDict(val)
435     if obj.children:
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)
441     return obj
442
443   def __str__(self):
444     """Custom str() formatter for disks.
445
446     """
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:
451         val = "<DRBD7("
452       else:
453         val = "<DRBD8("
454       if self.physical_id is None:
455         phy = "unconfigured"
456       else:
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]))
460
461       val += ("hosts=%s-%s, port=%s, %s, " %
462               (self.logical_id[0], self.logical_id[1], self.logical_id[2],
463                phy))
464       if self.children and self.children.count(None) == 0:
465         val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
466       else:
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)
470     else:
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"
475     else:
476       val += ", visible as /dev/%s" % self.iv_name
477     val += ", size=%dm)>" % self.size
478     return val
479
480
481 class Instance(TaggableObject):
482   """Config object representing an instance."""
483   __slots__ = TaggableObject.__slots__ + [
484     "name",
485     "primary_node",
486     "os",
487     "status",
488     "memory",
489     "vcpus",
490     "nics",
491     "disks",
492     "disk_template",
493     "network_port",
494     "kernel_path",
495     "initrd_path",
496     "hvm_boot_order",
497     ]
498
499   def _ComputeSecondaryNodes(self):
500     """Compute the list of secondary nodes.
501
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
505     dynamically.
506
507     """
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
512         if nodea == primary:
513           candidate = nodeb
514         else:
515           candidate = nodea
516         if candidate not in sec_nodes:
517           sec_nodes.append(candidate)
518       if device.children:
519         for child in device.children:
520           _Helper(primary, sec_nodes, child)
521
522     secondary_nodes = []
523     for device in self.disks:
524       _Helper(self.primary_node, secondary_nodes, device)
525     return tuple(secondary_nodes)
526
527   secondary_nodes = property(_ComputeSecondaryNodes, None, None,
528                              "List of secondary nodes")
529
530   def MapLVsByNode(self, lvmap=None, devs=None, node=None):
531     """Provide a mapping of nodes to LVs this instance owns.
532
533     This function figures out what logical volumes should belong on which
534     nodes, recursing through a device tree.
535
536     Args:
537       lvmap: (optional) a dictionary to receive the 'node' : ['lv', ...] data.
538
539     Returns:
540       None if lvmap arg is given.
541       Otherwise, { 'nodename' : ['volume1', 'volume2', ...], ... }
542
543     """
544     if node == None:
545       node = self.primary_node
546
547     if lvmap is None:
548       lvmap = { node : [] }
549       ret = lvmap
550     else:
551       if not node in lvmap:
552         lvmap[node] = []
553       ret = None
554
555     if not devs:
556       devs = self.disks
557
558     for dev in devs:
559       if dev.dev_type == constants.LD_LV:
560         lvmap[node].append(dev.logical_id[1])
561
562       elif dev.dev_type in constants.LDS_DRBD:
563         if dev.logical_id[0] not in lvmap:
564           lvmap[dev.logical_id[0]] = []
565
566         if dev.logical_id[1] not in lvmap:
567           lvmap[dev.logical_id[1]] = []
568
569         if dev.children:
570           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
571           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
572
573       elif dev.children:
574         self.MapLVsByNode(lvmap, dev.children, node)
575
576     return ret
577
578   def FindDisk(self, name):
579     """Find a disk given having a specified name.
580
581     This will return the disk which has the given iv_name.
582
583     """
584     for disk in self.disks:
585       if disk.iv_name == name:
586         return disk
587
588     return None
589
590   def ToDict(self):
591     """Instance-specific conversion to standard python types.
592
593     This replaces the children lists of objects with lists of standard
594     python types.
595
596     """
597     bo = super(Instance, self).ToDict()
598
599     for attr in "nics", "disks":
600       alist = bo.get(attr, None)
601       if alist:
602         nlist = self._ContainerToDicts(alist)
603       else:
604         nlist = []
605       bo[attr] = nlist
606     return bo
607
608   @classmethod
609   def FromDict(cls, val):
610     """Custom function for instances.
611
612     """
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)
616     return obj
617
618
619 class OS(ConfigObject):
620   """Config object representing an operating system."""
621   __slots__ = [
622     "name",
623     "path",
624     "status",
625     "api_version",
626     "create_script",
627     "export_script",
628     "import_script",
629     "rename_script",
630     ]
631
632   @classmethod
633   def FromInvalidOS(cls, err):
634     """Create an OS from an InvalidOS error.
635
636     This routine knows how to convert an InvalidOS error to an OS
637     object representing the broken OS with a meaningful error message.
638
639     """
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))
643
644     return cls(name=err.args[0], path=err.args[1], status=err.args[2])
645
646   def __nonzero__(self):
647     return self.status == constants.OS_VALID_STATUS
648
649   __bool__ = __nonzero__
650
651 class Node(TaggableObject):
652   """Config object representing a node."""
653   __slots__ = TaggableObject.__slots__ + [
654     "name",
655     "primary_ip",
656     "secondary_ip",
657     ]
658
659
660 class Cluster(TaggableObject):
661   """Config object representing the cluster."""
662   __slots__ = TaggableObject.__slots__ + [
663     "config_version",
664     "serial_no",
665     "rsahostkeypub",
666     "highest_used_port",
667     "tcpudp_port_pool",
668     "mac_prefix",
669     "volume_group_name",
670     "default_bridge",
671     ]
672
673   def ToDict(self):
674     """Custom function for cluster.
675
676     """
677     mydict = super(Cluster, self).ToDict()
678     mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
679     return mydict
680
681   @classmethod
682   def FromDict(cls, val):
683     """Custom function for cluster.
684
685     """
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)
689     return obj
690
691
692 class SerializableConfigParser(ConfigParser.SafeConfigParser):
693   """Simple wrapper over ConfigParse that allows serialization.
694
695   This class is basically ConfigParser.SafeConfigParser with two
696   additional methods that allow it to serialize/unserialize to/from a
697   buffer.
698
699   """
700   def Dumps(self):
701     """Dump this instance and return the string representation."""
702     buf = StringIO()
703     self.write(buf)
704     return buf.getvalue()
705
706   @staticmethod
707   def Loads(data):
708     """Load data from a string."""
709     buf = StringIO(data)
710     cfp = SerializableConfigParser()
711     cfp.readfp(buf)
712     return cfp