a26907d8341aa3a8468e2123e0d4079992e9857c
[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):
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     """
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)
179     if not tag:
180       raise errors.TagError("Tags cannot be empty")
181     if not re.match("^[ \w.+*/:-]+$", tag):
182       raise errors.TagError("Tag contains invalid characters")
183
184   def GetTags(self):
185     """Return the tags list.
186
187     """
188     tags = getattr(self, "tags", None)
189     if tags is None:
190       tags = self.tags = set()
191     return tags
192
193   def AddTag(self, tag):
194     """Add a new tag.
195
196     """
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)
202
203   def RemoveTag(self, tag):
204     """Remove a tag.
205
206     """
207     self.ValidateTag(tag)
208     tags = self.GetTags()
209     try:
210       tags.remove(tag)
211     except KeyError:
212       raise errors.TagError("Tag not found")
213
214   def ToDict(self):
215     """Taggable-object-specific conversion to standard python types.
216
217     This replaces the tags set with a list.
218
219     """
220     bo = super(TaggableObject, self).ToDict()
221
222     tags = bo.get("tags", None)
223     if isinstance(tags, set):
224       bo["tags"] = list(tags)
225     return bo
226
227   @classmethod
228   def FromDict(cls, val):
229     """Custom function for instances.
230
231     """
232     obj = super(TaggableObject, cls).FromDict(val)
233     if hasattr(obj, "tags") and isinstance(obj.tags, list):
234       obj.tags = set(obj.tags)
235     return obj
236
237
238 class ConfigData(ConfigObject):
239   """Top-level config object."""
240   __slots__ = ["cluster", "nodes", "instances"]
241
242   def ToDict(self):
243     """Custom function for top-level config data.
244
245     This just replaces the list of instances, nodes and the cluster
246     with standard python types.
247
248     """
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])
253
254     return mydict
255
256   @classmethod
257   def FromDict(cls, val):
258     """Custom function for top-level config data
259
260     """
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)
265     return obj
266
267
268 class NIC(ConfigObject):
269   """Config object representing a network card."""
270   __slots__ = ["mac", "ip", "bridge"]
271
272
273 class Disk(ConfigObject):
274   """Config object representing a block device."""
275   __slots__ = ["dev_type", "logical_id", "physical_id",
276                "children", "iv_name", "size"]
277
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)
281
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)
285
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,)
289
290   def StaticDevPath(self):
291     """Return the device path if this device type has a static one.
292
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.
296
297     """
298     if self.dev_type == constants.LD_LV:
299       return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
300     return None
301
302   def ChildrenNeeded(self):
303     """Compute the needed number of children for activation.
304
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).
308
309     Currently, only DRBD8 supports diskless activation (therefore we
310     return 0), for all other we keep the previous semantics and return
311     -1.
312
313     """
314     if self.dev_type == constants.LD_DRBD8:
315       return 0
316     return -1
317
318   def GetNodes(self, node):
319     """This function returns the nodes this device lives on.
320
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.
325
326     """
327     if self.dev_type in [constants.LD_LV, constants.LD_FILE]:
328       result = [node]
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")
333     else:
334       raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
335     return result
336
337   def ComputeNodeTree(self, parent_node):
338     """Compute the node/disk tree for this disk and its children.
339
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.
345
346     """
347     my_nodes = self.GetNodes(parent_node)
348     result = [(node, self) for node in my_nodes]
349     if not self.children:
350       # leaf device
351       return result
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
359           continue
360         else:
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
369             # be different)
370     return result
371
372   def RecordGrow(self, amount):
373     """Update the size of this disk after growth.
374
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.
378
379     """
380     if self.dev_type == constants.LD_LV:
381       self.size += amount
382     elif self.dev_type == constants.LD_DRBD8:
383       if self.children:
384         self.children[0].RecordGrow(amount)
385       self.size += amount
386     else:
387       raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
388                                    " disk type %s" % self.dev_type)
389
390   def SetPhysicalID(self, target_node, nodes_ip):
391     """Convert the logical ID to the physical ID.
392
393     This is used only for drbd, which needs ip/port configuration.
394
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
397     node.
398
399     Arguments:
400       - target_node: the node we wish to configure for
401       - nodes_ip: a mapping of node name to ip
402
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
405     in the disk tree.
406
407     """
408     if self.children:
409       for child in self.children:
410         child.SetPhysicalID(target_node, nodes_ip)
411
412     if self.logical_id is None and self.physical_id is not None:
413       return
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" %
418                                         target_node)
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,)
430     else:
431       self.physical_id = self.logical_id
432     return
433
434   def ToDict(self):
435     """Disk-specific conversion to standard python types.
436
437     This replaces the children lists of objects with lists of
438     standard python types.
439
440     """
441     bo = super(Disk, self).ToDict()
442
443     for attr in ("children",):
444       alist = bo.get(attr, None)
445       if alist:
446         bo[attr] = self._ContainerToDicts(alist)
447     return bo
448
449   @classmethod
450   def FromDict(cls, val):
451     """Custom function for Disks
452
453     """
454     obj = super(Disk, cls).FromDict(val)
455     if obj.children:
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)
464     return obj
465
466   def __str__(self):
467     """Custom str() formatter for disks.
468
469     """
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:
473       val = "<DRBD8("
474       if self.physical_id is None:
475         phy = "unconfigured"
476       else:
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]))
480
481       val += ("hosts=%s-%s, port=%s, %s, " %
482               (self.logical_id[0], self.logical_id[1], self.logical_id[2],
483                phy))
484       if self.children and self.children.count(None) == 0:
485         val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
486       else:
487         val += "no local storage"
488     else:
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"
493     else:
494       val += ", visible as /dev/%s" % self.iv_name
495     val += ", size=%dm)>" % self.size
496     return val
497
498
499 class Instance(TaggableObject):
500   """Config object representing an instance."""
501   __slots__ = TaggableObject.__slots__ + [
502     "name",
503     "primary_node",
504     "os",
505     "status",
506     "memory",
507     "vcpus",
508     "nics",
509     "disks",
510     "disk_template",
511     "network_port",
512     "kernel_path",
513     "initrd_path",
514     "hvm_boot_order",
515     "hvm_acpi",
516     "hvm_pae",
517     "hvm_cdrom_image_path",
518     "hvm_nic_type",
519     "hvm_disk_type",
520     "vnc_bind_address",
521     ]
522
523   def _ComputeSecondaryNodes(self):
524     """Compute the list of secondary nodes.
525
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
529     dynamically.
530
531     """
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]
536         if nodea == primary:
537           candidate = nodeb
538         else:
539           candidate = nodea
540         if candidate not in sec_nodes:
541           sec_nodes.append(candidate)
542       if device.children:
543         for child in device.children:
544           _Helper(primary, sec_nodes, child)
545
546     secondary_nodes = []
547     for device in self.disks:
548       _Helper(self.primary_node, secondary_nodes, device)
549     return tuple(secondary_nodes)
550
551   secondary_nodes = property(_ComputeSecondaryNodes, None, None,
552                              "List of secondary nodes")
553
554   def MapLVsByNode(self, lvmap=None, devs=None, node=None):
555     """Provide a mapping of nodes to LVs this instance owns.
556
557     This function figures out what logical volumes should belong on which
558     nodes, recursing through a device tree.
559
560     Args:
561       lvmap: (optional) a dictionary to receive the 'node' : ['lv', ...] data.
562
563     Returns:
564       None if lvmap arg is given.
565       Otherwise, { 'nodename' : ['volume1', 'volume2', ...], ... }
566
567     """
568     if node == None:
569       node = self.primary_node
570
571     if lvmap is None:
572       lvmap = { node : [] }
573       ret = lvmap
574     else:
575       if not node in lvmap:
576         lvmap[node] = []
577       ret = None
578
579     if not devs:
580       devs = self.disks
581
582     for dev in devs:
583       if dev.dev_type == constants.LD_LV:
584         lvmap[node].append(dev.logical_id[1])
585
586       elif dev.dev_type in constants.LDS_DRBD:
587         if dev.logical_id[0] not in lvmap:
588           lvmap[dev.logical_id[0]] = []
589
590         if dev.logical_id[1] not in lvmap:
591           lvmap[dev.logical_id[1]] = []
592
593         if dev.children:
594           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
595           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
596
597       elif dev.children:
598         self.MapLVsByNode(lvmap, dev.children, node)
599
600     return ret
601
602   def FindDisk(self, name):
603     """Find a disk given having a specified name.
604
605     This will return the disk which has the given iv_name.
606
607     """
608     for disk in self.disks:
609       if disk.iv_name == name:
610         return disk
611
612     return None
613
614   def ToDict(self):
615     """Instance-specific conversion to standard python types.
616
617     This replaces the children lists of objects with lists of standard
618     python types.
619
620     """
621     bo = super(Instance, self).ToDict()
622
623     for attr in "nics", "disks":
624       alist = bo.get(attr, None)
625       if alist:
626         nlist = self._ContainerToDicts(alist)
627       else:
628         nlist = []
629       bo[attr] = nlist
630     return bo
631
632   @classmethod
633   def FromDict(cls, val):
634     """Custom function for instances.
635
636     """
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)
640     return obj
641
642
643 class OS(ConfigObject):
644   """Config object representing an operating system."""
645   __slots__ = [
646     "name",
647     "path",
648     "status",
649     "api_version",
650     "create_script",
651     "export_script",
652     "import_script",
653     "rename_script",
654     ]
655
656   @classmethod
657   def FromInvalidOS(cls, err):
658     """Create an OS from an InvalidOS error.
659
660     This routine knows how to convert an InvalidOS error to an OS
661     object representing the broken OS with a meaningful error message.
662
663     """
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))
667
668     return cls(name=err.args[0], path=err.args[1], status=err.args[2])
669
670   def __nonzero__(self):
671     return self.status == constants.OS_VALID_STATUS
672
673   __bool__ = __nonzero__
674
675
676 class Node(TaggableObject):
677   """Config object representing a node."""
678   __slots__ = TaggableObject.__slots__ + [
679     "name",
680     "primary_ip",
681     "secondary_ip",
682     ]
683
684
685 class Cluster(TaggableObject):
686   """Config object representing the cluster."""
687   __slots__ = TaggableObject.__slots__ + [
688     "serial_no",
689     "rsahostkeypub",
690     "highest_used_port",
691     "tcpudp_port_pool",
692     "mac_prefix",
693     "volume_group_name",
694     "default_bridge",
695     ]
696
697   def ToDict(self):
698     """Custom function for cluster.
699
700     """
701     mydict = super(Cluster, self).ToDict()
702     mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
703     return mydict
704
705   @classmethod
706   def FromDict(cls, val):
707     """Custom function for cluster.
708
709     """
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)
713     return obj
714
715
716 class SerializableConfigParser(ConfigParser.SafeConfigParser):
717   """Simple wrapper over ConfigParse that allows serialization.
718
719   This class is basically ConfigParser.SafeConfigParser with two
720   additional methods that allow it to serialize/unserialize to/from a
721   buffer.
722
723   """
724   def Dumps(self):
725     """Dump this instance and return the string representation."""
726     buf = StringIO()
727     self.write(buf)
728     return buf.getvalue()
729
730   @staticmethod
731   def Loads(data):
732     """Load data from a string."""
733     buf = StringIO(data)
734     cfp = SerializableConfigParser()
735     cfp.readfp(buf)
736     return cfp