Do not install init script in PREFIX/bin.
[ganeti-local] / lib / objects.py
1 #!/usr/bin/python
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 ("drbd", "lvm")
318
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")
322
323   def OpenOnSecondary(self):
324     """Test if this device needs to be opened on a secondary node."""
325     return self.dev_type in ("lvm",)
326
327   def GetNodes(self, node):
328     """This function returns the nodes this device lives on.
329
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.
334
335     """
336     if self.dev_type == "lvm" or self.dev_type == "md_raid1":
337       result = [node]
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")
342     else:
343       raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
344     return result
345
346   def ComputeNodeTree(self, parent_node):
347     """Compute the node/disk tree for this disk and its children.
348
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).
357
358     """
359     my_nodes = self.GetNodes(parent_node)
360     result = [(node, self) for node in my_nodes]
361     if not self.children:
362       # leaf device
363       return result
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
371           continue
372         else:
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
381             # be different)
382     return result
383
384   def ToDict(self):
385     """Disk-specific conversion to standard python types.
386
387     This replaces the children lists of objects with lists of
388     standard python types.
389
390     """
391     bo = super(Disk, self).ToDict()
392
393     for attr in ("children",):
394       alist = bo.get(attr, None)
395       if alist:
396         bo[attr] = self._ContainerToDicts(alist)
397     return bo
398
399   @classmethod
400   def FromDict(cls, val):
401     """Custom function for Disks
402
403     """
404     obj = super(Disk, cls).FromDict(val)
405     if obj.children:
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)
411     return obj
412
413
414 class Instance(TaggableObject):
415   """Config object representing an instance."""
416   __slots__ = TaggableObject.__slots__ + [
417     "name",
418     "primary_node",
419     "os",
420     "status",
421     "memory",
422     "vcpus",
423     "nics",
424     "disks",
425     "disk_template",
426     ]
427
428   def _ComputeSecondaryNodes(self):
429     """Compute the list of secondary nodes.
430
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
434     dynamically.
435
436     """
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
441         if nodea == primary:
442           candidate = nodeb
443         else:
444           candidate = nodea
445         if candidate not in sec_nodes:
446           sec_nodes.append(candidate)
447       if device.children:
448         for child in device.children:
449           _Helper(primary, sec_nodes, child)
450
451     secondary_nodes = []
452     for device in self.disks:
453       _Helper(self.primary_node, secondary_nodes, device)
454     return tuple(secondary_nodes)
455
456   secondary_nodes = property(_ComputeSecondaryNodes, None, None,
457                              "List of secondary nodes")
458
459   def MapLVsByNode(self, lvmap=None, devs=None, node=None):
460     """Provide a mapping of nodes to LVs this instance owns.
461
462     This function figures out what logical volumes should belong on which
463     nodes, recursing through a device tree.
464
465     Args:
466       lvmap: (optional) a dictionary to receive the 'node' : ['lv', ...] data.
467
468     Returns:
469       None if lvmap arg is given.
470       Otherwise, { 'nodename' : ['volume1', 'volume2', ...], ... }
471
472     """
473     if node == None:
474       node = self.primary_node
475
476     if lvmap is None:
477       lvmap = { node : [] }
478       ret = lvmap
479     else:
480       if not node in lvmap:
481         lvmap[node] = []
482       ret = None
483
484     if not devs:
485       devs = self.disks
486
487     for dev in devs:
488       if dev.dev_type == "lvm":
489         lvmap[node].append(dev.logical_id[1])
490
491       elif dev.dev_type == "drbd":
492         if dev.logical_id[0] not in lvmap:
493           lvmap[dev.logical_id[0]] = []
494
495         if dev.logical_id[1] not in lvmap:
496           lvmap[dev.logical_id[1]] = []
497
498         if dev.children:
499           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
500           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
501
502       elif dev.children:
503         self.MapLVsByNode(lvmap, dev.children, node)
504
505     return ret
506
507   def FindDisk(self, name):
508     """Find a disk given having a specified name.
509
510     This will return the disk which has the given iv_name.
511
512     """
513     for disk in self.disks:
514       if disk.iv_name == name:
515         return disk
516
517     return None
518
519   def ToDict(self):
520     """Instance-specific conversion to standard python types.
521
522     This replaces the children lists of objects with lists of standard
523     python types.
524
525     """
526     bo = super(Instance, self).ToDict()
527
528     for attr in "nics", "disks":
529       alist = bo.get(attr, None)
530       if alist:
531         nlist = self._ContainerToDicts(alist)
532       else:
533         nlist = []
534       bo[attr] = nlist
535     return bo
536
537   @classmethod
538   def FromDict(cls, val):
539     """Custom function for instances.
540
541     """
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)
545     return obj
546
547
548 class OS(ConfigObject):
549   """Config object representing an operating system."""
550   __slots__ = [
551     "name",
552     "path",
553     "api_version",
554     "create_script",
555     "export_script",
556     "import_script",
557     "rename_script",
558     ]
559
560
561 class Node(TaggableObject):
562   """Config object representing a node."""
563   __slots__ = TaggableObject.__slots__ + [
564     "name",
565     "primary_ip",
566     "secondary_ip",
567     ]
568
569
570 class Cluster(TaggableObject):
571   """Config object representing the cluster."""
572   __slots__ = TaggableObject.__slots__ + [
573     "config_version",
574     "serial_no",
575     "rsahostkeypub",
576     "highest_used_port",
577     "tcpudp_port_pool",
578     "mac_prefix",
579     "volume_group_name",
580     "default_bridge",
581     ]
582
583   def ToDict(self):
584     """Custom function for cluster.
585
586     """
587     mydict = super(Cluster, self).ToDict()
588     mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
589     return mydict
590
591   @classmethod
592   def FromDict(cls, val):
593     """Custom function for cluster.
594
595     """
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)
599     return obj
600
601
602 class SerializableConfigParser(ConfigParser.SafeConfigParser):
603   """Simple wrapper over ConfigParse that allows serialization.
604
605   This class is basically ConfigParser.SafeConfigParser with two
606   additional methods that allow it to serialize/unserialize to/from a
607   buffer.
608
609   """
610   def Dumps(self):
611     """Dump this instance and return the string representation."""
612     buf = StringIO()
613     self.write(buf)
614     return buf.getvalue()
615
616   @staticmethod
617   def Loads(data):
618     """Load data from a string."""
619     buf = StringIO(data)
620     cfp = SerializableConfigParser()
621     cfp.readfp(buf)
622     return cfp