Reduce allowed size and number of tags
[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 ConfigParser
31 import re
32 from cStringIO import StringIO
33
34 from ganeti import errors
35 from ganeti import constants
36
37
38 __all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
39            "OS", "Node", "Cluster"]
40
41
42 class ConfigObject(object):
43   """A generic config object.
44
45   It has the following properties:
46
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
50
51   Classes derived from this must always declare __slots__ (we use many
52   config objects and the memory reduction is useful.
53
54   """
55   __slots__ = []
56
57   def __init__(self, **kwargs):
58     for k, v in kwargs.iteritems():
59       setattr(self, k, v)
60
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))
65     return None
66
67   def __setitem__(self, key, value):
68     if key not in self.__slots__:
69       raise KeyError(key)
70     setattr(self, key, value)
71
72   def __getstate__(self):
73     state = {}
74     for name in self.__slots__:
75       if hasattr(self, name):
76         state[name] = getattr(self, name)
77     return state
78
79   def __setstate__(self, state):
80     for name in state:
81       if name in self.__slots__:
82         setattr(self, name, state[name])
83
84   def ToDict(self):
85     """Convert to a dict holding only standard python types.
86
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.
92
93     """
94     return dict([(k, getattr(self, k, None)) for k in self.__slots__])
95
96   @classmethod
97   def FromDict(cls, val):
98     """Create an object from a dictionary.
99
100     This generic routine takes a dict, instantiates a new instance of
101     the given class, and sets attributes based on the dict content.
102
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.
107
108     """
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()])
113     obj = cls(**val_str)
114     return obj
115
116   @staticmethod
117   def _ContainerToDicts(container):
118     """Convert the elements of a container to standard python types.
119
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.
123
124     """
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]
129     else:
130       raise TypeError("Invalid type %s passed to _ContainerToDicts" %
131                       type(container))
132     return ret
133
134   @staticmethod
135   def _ContainerFromDicts(source, c_type, e_type):
136     """Convert a container from standard python types.
137
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.
141
142     """
143     if not isinstance(c_type, type):
144       raise TypeError("Container type %s passed to _ContainerFromDicts is"
145                       " not a type" % type(c_type))
146     if c_type is dict:
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])
150     else:
151       raise TypeError("Invalid container type %s passed to"
152                       " _ContainerFromDicts" % c_type)
153     return ret
154
155   def __repr__(self):
156     """Implement __repr__ for ConfigObjects."""
157     return repr(self.ToDict())
158
159
160 class TaggableObject(ConfigObject):
161   """An generic class supporting tags.
162
163   """
164   __slots__ = ConfigObject.__slots__ + ["tags"]
165
166   @staticmethod
167   def ValidateTag(tag, removal=False):
168     """Check if a tag is valid.
169
170     If the tag is invalid, an errors.TagError will be raised. The
171     function has no return value.
172
173     Args:
174       tag: Tag value
175       removal: Validating tag for removal?
176
177     """
178     if not isinstance(tag, basestring):
179       raise errors.TagError("Invalid tag type (not a string)")
180     if removal:
181       return
182     if len(tag) > constants.MAX_TAG_LEN:
183       raise errors.TagError("Tag too long (>%d characters)" %
184                             constants.MAX_TAG_LEN)
185     if not tag:
186       raise errors.TagError("Tags cannot be empty")
187     if not re.match("^[ \w.+*/:-]+$", tag):
188       raise errors.TagError("Tag contains invalid characters")
189
190   def GetTags(self):
191     """Return the tags list.
192
193     """
194     tags = getattr(self, "tags", None)
195     if tags is None:
196       tags = self.tags = set()
197     return tags
198
199   def AddTag(self, tag):
200     """Add a new tag.
201
202     """
203     self.ValidateTag(tag)
204     tags = self.GetTags()
205     if len(tags) >= constants.MAX_TAGS_PER_OBJ:
206       raise errors.TagError("Too many tags")
207     self.GetTags().add(tag)
208
209   def RemoveTag(self, tag):
210     """Remove a tag.
211
212     """
213     self.ValidateTag(tag, removal=True)
214     tags = self.GetTags()
215     try:
216       tags.remove(tag)
217     except KeyError:
218       raise errors.TagError("Tag not found")
219
220   def ToDict(self):
221     """Taggable-object-specific conversion to standard python types.
222
223     This replaces the tags set with a list.
224
225     """
226     bo = super(TaggableObject, self).ToDict()
227
228     tags = bo.get("tags", None)
229     if isinstance(tags, set):
230       bo["tags"] = list(tags)
231     return bo
232
233   @classmethod
234   def FromDict(cls, val):
235     """Custom function for instances.
236
237     """
238     obj = super(TaggableObject, cls).FromDict(val)
239     if hasattr(obj, "tags") and isinstance(obj.tags, list):
240       obj.tags = set(obj.tags)
241     return obj
242
243
244 class ConfigData(ConfigObject):
245   """Top-level config object."""
246   __slots__ = ["cluster", "nodes", "instances"]
247
248   def ToDict(self):
249     """Custom function for top-level config data.
250
251     This just replaces the list of instances, nodes and the cluster
252     with standard python types.
253
254     """
255     mydict = super(ConfigData, self).ToDict()
256     mydict["cluster"] = mydict["cluster"].ToDict()
257     for key in "nodes", "instances":
258       mydict[key] = self._ContainerToDicts(mydict[key])
259
260     return mydict
261
262   @classmethod
263   def FromDict(cls, val):
264     """Custom function for top-level config data
265
266     """
267     obj = super(ConfigData, cls).FromDict(val)
268     obj.cluster = Cluster.FromDict(obj.cluster)
269     obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
270     obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
271     return obj
272
273
274 class NIC(ConfigObject):
275   """Config object representing a network card."""
276   __slots__ = ["mac", "ip", "bridge"]
277
278
279 class Disk(ConfigObject):
280   """Config object representing a block device."""
281   __slots__ = ["dev_type", "logical_id", "physical_id",
282                "children", "iv_name", "size"]
283
284   def CreateOnSecondary(self):
285     """Test if this device needs to be created on a secondary node."""
286     return self.dev_type in (constants.LD_DRBD7, constants.LD_DRBD8,
287                              constants.LD_LV)
288
289   def AssembleOnSecondary(self):
290     """Test if this device needs to be assembled on a secondary node."""
291     return self.dev_type in (constants.LD_DRBD7, constants.LD_DRBD8,
292                              constants.LD_LV)
293
294   def OpenOnSecondary(self):
295     """Test if this device needs to be opened on a secondary node."""
296     return self.dev_type in (constants.LD_LV,)
297
298   def StaticDevPath(self):
299     """Return the device path if this device type has a static one.
300
301     Some devices (LVM for example) live always at the same /dev/ path,
302     irrespective of their status. For such devices, we return this
303     path, for others we return None.
304
305     """
306     if self.dev_type == constants.LD_LV:
307       return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
308     return None
309
310   def ChildrenNeeded(self):
311     """Compute the needed number of children for activation.
312
313     This method will return either -1 (all children) or a positive
314     number denoting the minimum number of children needed for
315     activation (only mirrored devices will usually return >=0).
316
317     Currently, only DRBD8 supports diskless activation (therefore we
318     return 0), for all other we keep the previous semantics and return
319     -1.
320
321     """
322     if self.dev_type == constants.LD_DRBD8:
323       return 0
324     return -1
325
326   def GetNodes(self, node):
327     """This function returns the nodes this device lives on.
328
329     Given the node on which the parent of the device lives on (or, in
330     case of a top-level device, the primary node of the devices'
331     instance), this function will return a list of nodes on which this
332     devices needs to (or can) be assembled.
333
334     """
335     if self.dev_type == constants.LD_LV or self.dev_type == constants.LD_MD_R1:
336       result = [node]
337     elif self.dev_type in constants.LDS_DRBD:
338       result = [self.logical_id[0], self.logical_id[1]]
339       if node not in result:
340         raise errors.ConfigurationError("DRBD device passed unknown node")
341     else:
342       raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
343     return result
344
345   def ComputeNodeTree(self, parent_node):
346     """Compute the node/disk tree for this disk and its children.
347
348     This method, given the node on which the parent disk lives, will
349     return the list of all (node, disk) pairs which describe the disk
350     tree in the most compact way. For example, a md/drbd/lvm stack
351     will be returned as (primary_node, md) and (secondary_node, drbd)
352     which represents all the top-level devices on the nodes. This
353     means that on the primary node we need to activate the the md (and
354     recursively all its children) and on the secondary node we need to
355     activate the drbd device (and its children, the two lvm volumes).
356
357     """
358     my_nodes = self.GetNodes(parent_node)
359     result = [(node, self) for node in my_nodes]
360     if not self.children:
361       # leaf device
362       return result
363     for node in my_nodes:
364       for child in self.children:
365         child_result = child.ComputeNodeTree(node)
366         if len(child_result) == 1:
367           # child (and all its descendants) is simple, doesn't split
368           # over multiple hosts, so we don't need to describe it, our
369           # own entry for this node describes it completely
370           continue
371         else:
372           # check if child nodes differ from my nodes; note that
373           # subdisk can differ from the child itself, and be instead
374           # one of its descendants
375           for subnode, subdisk in child_result:
376             if subnode not in my_nodes:
377               result.append((subnode, subdisk))
378             # otherwise child is under our own node, so we ignore this
379             # entry (but probably the other results in the list will
380             # be different)
381     return result
382
383   def RecordGrow(self, amount):
384     """Update the size of this disk after growth.
385
386     This method recurses over the disks's children and updates their
387     size correspondigly. The method needs to be kept in sync with the
388     actual algorithms from bdev.
389
390     """
391     if self.dev_type == constants.LD_LV:
392       self.size += amount
393     elif self.dev_type == constants.LD_DRBD8:
394       if self.children:
395         self.children[0].RecordGrow(amount)
396       self.size += amount
397     else:
398       raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
399                                    " disk type %s" % self.dev_type)
400
401   def SetPhysicalID(self, target_node, nodes_ip):
402     """Convert the logical ID to the physical ID.
403
404     This is used only for drbd, which needs ip/port configuration.
405
406     The routine descends down and updates its children also, because
407     this helps when the only the top device is passed to the remote
408     node.
409
410     Arguments:
411       - target_node: the node we wish to configure for
412       - nodes_ip: a mapping of node name to ip
413
414     The target_node must exist in in nodes_ip, and must be one of the
415     nodes in the logical ID for each of the DRBD devices encountered
416     in the disk tree.
417
418     """
419     if self.children:
420       for child in self.children:
421         child.SetPhysicalID(target_node, nodes_ip)
422
423     if self.logical_id is None and self.physical_id is not None:
424       return
425     if self.dev_type in constants.LDS_DRBD:
426       pnode, snode, port = self.logical_id
427       if target_node not in (pnode, snode):
428         raise errors.ConfigurationError("DRBD device not knowing node %s" %
429                                         target_node)
430       pnode_ip = nodes_ip.get(pnode, None)
431       snode_ip = nodes_ip.get(snode, None)
432       if pnode_ip is None or snode_ip is None:
433         raise errors.ConfigurationError("Can't find primary or secondary node"
434                                         " for %s" % str(self))
435       if pnode == target_node:
436         self.physical_id = (pnode_ip, port,
437                             snode_ip, port)
438       else: # it must be secondary, we tested above
439         self.physical_id = (snode_ip, port,
440                             pnode_ip, port)
441     else:
442       self.physical_id = self.logical_id
443     return
444
445   def ToDict(self):
446     """Disk-specific conversion to standard python types.
447
448     This replaces the children lists of objects with lists of
449     standard python types.
450
451     """
452     bo = super(Disk, self).ToDict()
453
454     for attr in ("children",):
455       alist = bo.get(attr, None)
456       if alist:
457         bo[attr] = self._ContainerToDicts(alist)
458     return bo
459
460   @classmethod
461   def FromDict(cls, val):
462     """Custom function for Disks
463
464     """
465     obj = super(Disk, cls).FromDict(val)
466     if obj.children:
467       obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
468     if obj.logical_id and isinstance(obj.logical_id, list):
469       obj.logical_id = tuple(obj.logical_id)
470     if obj.physical_id and isinstance(obj.physical_id, list):
471       obj.physical_id = tuple(obj.physical_id)
472     return obj
473
474   def __str__(self):
475     """Custom str() formatter for disks.
476
477     """
478     if self.dev_type == constants.LD_LV:
479       val =  "<LogicalVolume(/dev/%s/%s" % self.logical_id
480     elif self.dev_type in constants.LDS_DRBD:
481       if self.dev_type == constants.LD_DRBD7:
482         val = "<DRBD7("
483       else:
484         val = "<DRBD8("
485       if self.physical_id is None:
486         phy = "unconfigured"
487       else:
488         phy = ("configured as %s:%s %s:%s" %
489                (self.physical_id[0], self.physical_id[1],
490                 self.physical_id[2], self.physical_id[3]))
491
492       val += ("hosts=%s-%s, port=%s, %s, " %
493               (self.logical_id[0], self.logical_id[1], self.logical_id[2],
494                phy))
495       if self.children and self.children.count(None) == 0:
496         val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
497       else:
498         val += "no local storage"
499     elif self.dev_type == constants.LD_MD_R1:
500       val = "<MD_R1(uuid=%s, children=%s" % (self.physical_id, self.children)
501     else:
502       val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
503              (self.dev_type, self.logical_id, self.physical_id, self.children))
504     if self.iv_name is None:
505       val += ", not visible"
506     else:
507       val += ", visible as /dev/%s" % self.iv_name
508     val += ", size=%dm)>" % self.size
509     return val
510
511
512 class Instance(TaggableObject):
513   """Config object representing an instance."""
514   __slots__ = TaggableObject.__slots__ + [
515     "name",
516     "primary_node",
517     "os",
518     "status",
519     "memory",
520     "vcpus",
521     "nics",
522     "disks",
523     "disk_template",
524     "network_port",
525     "kernel_path",
526     "initrd_path",
527     "hvm_boot_order",
528     "hvm_acpi",
529     "hvm_pae",
530     "hvm_cdrom_image_path",
531     "vnc_bind_address",
532     ]
533
534   def _ComputeSecondaryNodes(self):
535     """Compute the list of secondary nodes.
536
537     Since the data is already there (in the drbd disks), keeping it as
538     a separate normal attribute is redundant and if not properly
539     synchronised can cause problems. Thus it's better to compute it
540     dynamically.
541
542     """
543     def _Helper(primary, sec_nodes, device):
544       """Recursively computes secondary nodes given a top device."""
545       if device.dev_type in constants.LDS_DRBD:
546         nodea, nodeb, dummy = device.logical_id
547         if nodea == primary:
548           candidate = nodeb
549         else:
550           candidate = nodea
551         if candidate not in sec_nodes:
552           sec_nodes.append(candidate)
553       if device.children:
554         for child in device.children:
555           _Helper(primary, sec_nodes, child)
556
557     secondary_nodes = []
558     for device in self.disks:
559       _Helper(self.primary_node, secondary_nodes, device)
560     return tuple(secondary_nodes)
561
562   secondary_nodes = property(_ComputeSecondaryNodes, None, None,
563                              "List of secondary nodes")
564
565   def MapLVsByNode(self, lvmap=None, devs=None, node=None):
566     """Provide a mapping of nodes to LVs this instance owns.
567
568     This function figures out what logical volumes should belong on which
569     nodes, recursing through a device tree.
570
571     Args:
572       lvmap: (optional) a dictionary to receive the 'node' : ['lv', ...] data.
573
574     Returns:
575       None if lvmap arg is given.
576       Otherwise, { 'nodename' : ['volume1', 'volume2', ...], ... }
577
578     """
579     if node == None:
580       node = self.primary_node
581
582     if lvmap is None:
583       lvmap = { node : [] }
584       ret = lvmap
585     else:
586       if not node in lvmap:
587         lvmap[node] = []
588       ret = None
589
590     if not devs:
591       devs = self.disks
592
593     for dev in devs:
594       if dev.dev_type == constants.LD_LV:
595         lvmap[node].append(dev.logical_id[1])
596
597       elif dev.dev_type in constants.LDS_DRBD:
598         if dev.logical_id[0] not in lvmap:
599           lvmap[dev.logical_id[0]] = []
600
601         if dev.logical_id[1] not in lvmap:
602           lvmap[dev.logical_id[1]] = []
603
604         if dev.children:
605           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
606           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
607
608       elif dev.children:
609         self.MapLVsByNode(lvmap, dev.children, node)
610
611     return ret
612
613   def FindDisk(self, name):
614     """Find a disk given having a specified name.
615
616     This will return the disk which has the given iv_name.
617
618     """
619     for disk in self.disks:
620       if disk.iv_name == name:
621         return disk
622
623     return None
624
625   def ToDict(self):
626     """Instance-specific conversion to standard python types.
627
628     This replaces the children lists of objects with lists of standard
629     python types.
630
631     """
632     bo = super(Instance, self).ToDict()
633
634     for attr in "nics", "disks":
635       alist = bo.get(attr, None)
636       if alist:
637         nlist = self._ContainerToDicts(alist)
638       else:
639         nlist = []
640       bo[attr] = nlist
641     return bo
642
643   @classmethod
644   def FromDict(cls, val):
645     """Custom function for instances.
646
647     """
648     obj = super(Instance, cls).FromDict(val)
649     obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
650     obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
651     return obj
652
653
654 class OS(ConfigObject):
655   """Config object representing an operating system."""
656   __slots__ = [
657     "name",
658     "path",
659     "status",
660     "api_version",
661     "create_script",
662     "export_script",
663     "import_script",
664     "rename_script",
665     ]
666
667   @classmethod
668   def FromInvalidOS(cls, err):
669     """Create an OS from an InvalidOS error.
670
671     This routine knows how to convert an InvalidOS error to an OS
672     object representing the broken OS with a meaningful error message.
673
674     """
675     if not isinstance(err, errors.InvalidOS):
676       raise errors.ProgrammerError("Trying to initialize an OS from an"
677                                    " invalid object of type %s" % type(err))
678
679     return cls(name=err.args[0], path=err.args[1], status=err.args[2])
680
681   def __nonzero__(self):
682     return self.status == constants.OS_VALID_STATUS
683
684   __bool__ = __nonzero__
685
686 class Node(TaggableObject):
687   """Config object representing a node."""
688   __slots__ = TaggableObject.__slots__ + [
689     "name",
690     "primary_ip",
691     "secondary_ip",
692     ]
693
694
695 class Cluster(TaggableObject):
696   """Config object representing the cluster."""
697   __slots__ = TaggableObject.__slots__ + [
698     "config_version",
699     "serial_no",
700     "rsahostkeypub",
701     "highest_used_port",
702     "tcpudp_port_pool",
703     "mac_prefix",
704     "volume_group_name",
705     "default_bridge",
706     ]
707
708   def ToDict(self):
709     """Custom function for cluster.
710
711     """
712     mydict = super(Cluster, self).ToDict()
713     mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
714     return mydict
715
716   @classmethod
717   def FromDict(cls, val):
718     """Custom function for cluster.
719
720     """
721     obj = super(Cluster, cls).FromDict(val)
722     if not isinstance(obj.tcpudp_port_pool, set):
723       obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
724     return obj
725
726
727 class SerializableConfigParser(ConfigParser.SafeConfigParser):
728   """Simple wrapper over ConfigParse that allows serialization.
729
730   This class is basically ConfigParser.SafeConfigParser with two
731   additional methods that allow it to serialize/unserialize to/from a
732   buffer.
733
734   """
735   def Dumps(self):
736     """Dump this instance and return the string representation."""
737     buf = StringIO()
738     self.write(buf)
739     return buf.getvalue()
740
741   @staticmethod
742   def Loads(data):
743     """Load data from a string."""
744     buf = StringIO(data)
745     cfp = SerializableConfigParser()
746     cfp.readfp(buf)
747     return cfp