Move iallocator script execution to ganeti-noded
[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_DRBD7, constants.LD_DRBD8,
281                              constants.LD_LV)
282
283   def AssembleOnSecondary(self):
284     """Test if this device needs to be assembled on a secondary node."""
285     return self.dev_type in (constants.LD_DRBD7, constants.LD_DRBD8,
286                              constants.LD_LV)
287
288   def OpenOnSecondary(self):
289     """Test if this device needs to be opened on a secondary node."""
290     return self.dev_type in (constants.LD_LV,)
291
292   def StaticDevPath(self):
293     """Return the device path if this device type has a static one.
294
295     Some devices (LVM for example) live always at the same /dev/ path,
296     irrespective of their status. For such devices, we return this
297     path, for others we return None.
298
299     """
300     if self.dev_type == constants.LD_LV:
301       return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
302     return None
303
304   def ChildrenNeeded(self):
305     """Compute the needed number of children for activation.
306
307     This method will return either -1 (all children) or a positive
308     number denoting the minimum number of children needed for
309     activation (only mirrored devices will usually return >=0).
310
311     Currently, only DRBD8 supports diskless activation (therefore we
312     return 0), for all other we keep the previous semantics and return
313     -1.
314
315     """
316     if self.dev_type == constants.LD_DRBD8:
317       return 0
318     return -1
319
320   def GetNodes(self, node):
321     """This function returns the nodes this device lives on.
322
323     Given the node on which the parent of the device lives on (or, in
324     case of a top-level device, the primary node of the devices'
325     instance), this function will return a list of nodes on which this
326     devices needs to (or can) be assembled.
327
328     """
329     if self.dev_type in [constants.LD_LV, constants.LD_MD_R1,
330                          constants.LD_FILE]:
331       result = [node]
332     elif self.dev_type in constants.LDS_DRBD:
333       result = [self.logical_id[0], self.logical_id[1]]
334       if node not in result:
335         raise errors.ConfigurationError("DRBD device passed unknown node")
336     else:
337       raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
338     return result
339
340   def ComputeNodeTree(self, parent_node):
341     """Compute the node/disk tree for this disk and its children.
342
343     This method, given the node on which the parent disk lives, will
344     return the list of all (node, disk) pairs which describe the disk
345     tree in the most compact way. For example, a md/drbd/lvm stack
346     will be returned as (primary_node, md) and (secondary_node, drbd)
347     which represents all the top-level devices on the nodes. This
348     means that on the primary node we need to activate the the md (and
349     recursively all its children) and on the secondary node we need to
350     activate the drbd device (and its children, the two lvm volumes).
351
352     """
353     my_nodes = self.GetNodes(parent_node)
354     result = [(node, self) for node in my_nodes]
355     if not self.children:
356       # leaf device
357       return result
358     for node in my_nodes:
359       for child in self.children:
360         child_result = child.ComputeNodeTree(node)
361         if len(child_result) == 1:
362           # child (and all its descendants) is simple, doesn't split
363           # over multiple hosts, so we don't need to describe it, our
364           # own entry for this node describes it completely
365           continue
366         else:
367           # check if child nodes differ from my nodes; note that
368           # subdisk can differ from the child itself, and be instead
369           # one of its descendants
370           for subnode, subdisk in child_result:
371             if subnode not in my_nodes:
372               result.append((subnode, subdisk))
373             # otherwise child is under our own node, so we ignore this
374             # entry (but probably the other results in the list will
375             # be different)
376     return result
377
378   def ToDict(self):
379     """Disk-specific conversion to standard python types.
380
381     This replaces the children lists of objects with lists of
382     standard python types.
383
384     """
385     bo = super(Disk, self).ToDict()
386
387     for attr in ("children",):
388       alist = bo.get(attr, None)
389       if alist:
390         bo[attr] = self._ContainerToDicts(alist)
391     return bo
392
393   @classmethod
394   def FromDict(cls, val):
395     """Custom function for Disks
396
397     """
398     obj = super(Disk, cls).FromDict(val)
399     if obj.children:
400       obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
401     if obj.logical_id and isinstance(obj.logical_id, list):
402       obj.logical_id = tuple(obj.logical_id)
403     if obj.physical_id and isinstance(obj.physical_id, list):
404       obj.physical_id = tuple(obj.physical_id)
405     return obj
406
407   def __str__(self):
408     """Custom str() formatter for disks.
409
410     """
411     if self.dev_type == constants.LD_LV:
412       val =  "<LogicalVolume(/dev/%s/%s" % self.logical_id
413     elif self.dev_type in constants.LDS_DRBD:
414       if self.dev_type == constants.LD_DRBD7:
415         val = "<DRBD7("
416       else:
417         val = "<DRBD8("
418       if self.physical_id is None:
419         phy = "unconfigured"
420       else:
421         phy = ("configured as %s:%s %s:%s" %
422                (self.physical_id[0], self.physical_id[1],
423                 self.physical_id[2], self.physical_id[3]))
424
425       val += ("hosts=%s-%s, port=%s, %s, " %
426               (self.logical_id[0], self.logical_id[1], self.logical_id[2],
427                phy))
428       if self.children and self.children.count(None) == 0:
429         val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
430       else:
431         val += "no local storage"
432     elif self.dev_type == constants.LD_MD_R1:
433       val = "<MD_R1(uuid=%s, children=%s" % (self.physical_id, self.children)
434     else:
435       val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
436              (self.dev_type, self.logical_id, self.physical_id, self.children))
437     if self.iv_name is None:
438       val += ", not visible"
439     else:
440       val += ", visible as /dev/%s" % self.iv_name
441     val += ", size=%dm)>" % self.size
442     return val
443
444
445 class Instance(TaggableObject):
446   """Config object representing an instance."""
447   __slots__ = TaggableObject.__slots__ + [
448     "name",
449     "primary_node",
450     "os",
451     "status",
452     "memory",
453     "vcpus",
454     "nics",
455     "disks",
456     "disk_template",
457     "network_port",
458     "kernel_path",
459     "initrd_path",
460     "hvm_boot_order",
461     "hvm_acpi",
462     "hvm_pae",
463     "hvm_cdrom_image_path",
464     "vnc_bind_address",
465     ]
466
467   def _ComputeSecondaryNodes(self):
468     """Compute the list of secondary nodes.
469
470     Since the data is already there (in the drbd disks), keeping it as
471     a separate normal attribute is redundant and if not properly
472     synchronised can cause problems. Thus it's better to compute it
473     dynamically.
474
475     """
476     def _Helper(primary, sec_nodes, device):
477       """Recursively computes secondary nodes given a top device."""
478       if device.dev_type in constants.LDS_DRBD:
479         nodea, nodeb, dummy = device.logical_id
480         if nodea == primary:
481           candidate = nodeb
482         else:
483           candidate = nodea
484         if candidate not in sec_nodes:
485           sec_nodes.append(candidate)
486       if device.children:
487         for child in device.children:
488           _Helper(primary, sec_nodes, child)
489
490     secondary_nodes = []
491     for device in self.disks:
492       _Helper(self.primary_node, secondary_nodes, device)
493     return tuple(secondary_nodes)
494
495   secondary_nodes = property(_ComputeSecondaryNodes, None, None,
496                              "List of secondary nodes")
497
498   def MapLVsByNode(self, lvmap=None, devs=None, node=None):
499     """Provide a mapping of nodes to LVs this instance owns.
500
501     This function figures out what logical volumes should belong on which
502     nodes, recursing through a device tree.
503
504     Args:
505       lvmap: (optional) a dictionary to receive the 'node' : ['lv', ...] data.
506
507     Returns:
508       None if lvmap arg is given.
509       Otherwise, { 'nodename' : ['volume1', 'volume2', ...], ... }
510
511     """
512     if node == None:
513       node = self.primary_node
514
515     if lvmap is None:
516       lvmap = { node : [] }
517       ret = lvmap
518     else:
519       if not node in lvmap:
520         lvmap[node] = []
521       ret = None
522
523     if not devs:
524       devs = self.disks
525
526     for dev in devs:
527       if dev.dev_type == constants.LD_LV:
528         lvmap[node].append(dev.logical_id[1])
529
530       elif dev.dev_type in constants.LDS_DRBD:
531         if dev.logical_id[0] not in lvmap:
532           lvmap[dev.logical_id[0]] = []
533
534         if dev.logical_id[1] not in lvmap:
535           lvmap[dev.logical_id[1]] = []
536
537         if dev.children:
538           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
539           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
540
541       elif dev.children:
542         self.MapLVsByNode(lvmap, dev.children, node)
543
544     return ret
545
546   def FindDisk(self, name):
547     """Find a disk given having a specified name.
548
549     This will return the disk which has the given iv_name.
550
551     """
552     for disk in self.disks:
553       if disk.iv_name == name:
554         return disk
555
556     return None
557
558   def ToDict(self):
559     """Instance-specific conversion to standard python types.
560
561     This replaces the children lists of objects with lists of standard
562     python types.
563
564     """
565     bo = super(Instance, self).ToDict()
566
567     for attr in "nics", "disks":
568       alist = bo.get(attr, None)
569       if alist:
570         nlist = self._ContainerToDicts(alist)
571       else:
572         nlist = []
573       bo[attr] = nlist
574     return bo
575
576   @classmethod
577   def FromDict(cls, val):
578     """Custom function for instances.
579
580     """
581     obj = super(Instance, cls).FromDict(val)
582     obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
583     obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
584     return obj
585
586
587 class OS(ConfigObject):
588   """Config object representing an operating system."""
589   __slots__ = [
590     "name",
591     "path",
592     "status",
593     "api_version",
594     "create_script",
595     "export_script",
596     "import_script",
597     "rename_script",
598     ]
599
600   @classmethod
601   def FromInvalidOS(cls, err):
602     """Create an OS from an InvalidOS error.
603
604     This routine knows how to convert an InvalidOS error to an OS
605     object representing the broken OS with a meaningful error message.
606
607     """
608     if not isinstance(err, errors.InvalidOS):
609       raise errors.ProgrammerError("Trying to initialize an OS from an"
610                                    " invalid object of type %s" % type(err))
611
612     return cls(name=err.args[0], path=err.args[1], status=err.args[2])
613
614   def __nonzero__(self):
615     return self.status == constants.OS_VALID_STATUS
616
617   __bool__ = __nonzero__
618
619
620 class Node(TaggableObject):
621   """Config object representing a node."""
622   __slots__ = TaggableObject.__slots__ + [
623     "name",
624     "primary_ip",
625     "secondary_ip",
626     ]
627
628
629 class Cluster(TaggableObject):
630   """Config object representing the cluster."""
631   __slots__ = TaggableObject.__slots__ + [
632     "config_version",
633     "serial_no",
634     "rsahostkeypub",
635     "highest_used_port",
636     "tcpudp_port_pool",
637     "mac_prefix",
638     "volume_group_name",
639     "default_bridge",
640     ]
641
642   def ToDict(self):
643     """Custom function for cluster.
644
645     """
646     mydict = super(Cluster, self).ToDict()
647     mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
648     return mydict
649
650   @classmethod
651   def FromDict(cls, val):
652     """Custom function for cluster.
653
654     """
655     obj = super(Cluster, cls).FromDict(val)
656     if not isinstance(obj.tcpudp_port_pool, set):
657       obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
658     return obj
659
660
661 class SerializableConfigParser(ConfigParser.SafeConfigParser):
662   """Simple wrapper over ConfigParse that allows serialization.
663
664   This class is basically ConfigParser.SafeConfigParser with two
665   additional methods that allow it to serialize/unserialize to/from a
666   buffer.
667
668   """
669   def Dumps(self):
670     """Dump this instance and return the string representation."""
671     buf = StringIO()
672     self.write(buf)
673     return buf.getvalue()
674
675   @staticmethod
676   def Loads(data):
677     """Load data from a string."""
678     buf = StringIO(data)
679     cfp = SerializableConfigParser()
680     cfp.readfp(buf)
681     return cfp