Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ ff9c047c

History | View | Annotate | Download (17.9 kB)

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 cPickle
31
from cStringIO import StringIO
32
import ConfigParser
33
import re
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
class ConfigObject(object):
44
  """A generic config object.
45

46
  It has the following properties:
47

48
    - provides somewhat safe recursive unpickling and pickling for its classes
49
    - unset attributes which are defined in slots are always returned
50
      as None instead of raising an error
51

52
  Classes derived from this must always declare __slots__ (we use many
53
  config objects and the memory reduction is useful.
54

55
  """
56
  __slots__ = []
57

    
58
  def __init__(self, **kwargs):
59
    for i in kwargs:
60
      setattr(self, i, kwargs[i])
61

    
62
  def __getattr__(self, name):
63
    if name not in self.__slots__:
64
      raise AttributeError("Invalid object attribute %s.%s" %
65
                           (type(self).__name__, name))
66
    return None
67

    
68
  def __setitem__(self, key, value):
69
    if key not in self.__slots__:
70
      raise KeyError(key)
71
    setattr(self, key, value)
72

    
73
  def __getstate__(self):
74
    state = {}
75
    for name in self.__slots__:
76
      if hasattr(self, name):
77
        state[name] = getattr(self, name)
78
    return state
79

    
80
  def __setstate__(self, state):
81
    for name in state:
82
      if name in self.__slots__:
83
        setattr(self, name, state[name])
84

    
85
  @staticmethod
86
  def FindGlobal(module, name):
87
    """Function filtering the allowed classes to be un-pickled.
88

89
    Currently, we only allow the classes from this module which are
90
    derived from ConfigObject.
91

92
    """
93
    # Also support the old module name (ganeti.config)
94
    cls = None
95
    if module == "ganeti.config" or module == "ganeti.objects":
96
      if name == "ConfigData":
97
        cls = ConfigData
98
      elif name == "NIC":
99
        cls = NIC
100
      elif name == "Disk" or name == "BlockDev":
101
        cls = Disk
102
      elif name == "Instance":
103
        cls = Instance
104
      elif name == "OS":
105
        cls = OS
106
      elif name == "Node":
107
        cls = Node
108
      elif name == "Cluster":
109
        cls = Cluster
110
    elif module == "__builtin__":
111
      if name == "set":
112
        cls = set
113
    if cls is None:
114
      raise cPickle.UnpicklingError("Class %s.%s not allowed due to"
115
                                    " security concerns" % (module, name))
116
    return cls
117

    
118
  def Dump(self, fobj):
119
    """Dump this instance to a file object.
120

121
    Note that we use the HIGHEST_PROTOCOL, as it brings benefits for
122
    the new classes.
123

124
    """
125
    dumper = cPickle.Pickler(fobj, cPickle.HIGHEST_PROTOCOL)
126
    dumper.dump(self)
127

    
128
  @staticmethod
129
  def Load(fobj):
130
    """Unpickle data from the given stream.
131

132
    This uses the `FindGlobal` function to filter the allowed classes.
133

134
    """
135
    loader = cPickle.Unpickler(fobj)
136
    loader.find_global = ConfigObject.FindGlobal
137
    return loader.load()
138

    
139
  def Dumps(self):
140
    """Dump this instance and return the string representation."""
141
    buf = StringIO()
142
    self.Dump(buf)
143
    return buf.getvalue()
144

    
145
  @staticmethod
146
  def Loads(data):
147
    """Load data from a string."""
148
    return ConfigObject.Load(StringIO(data))
149

    
150
  def ToDict(self):
151
    """Convert to a dict holding only standard python types.
152

153
    The generic routine just dumps all of this object's attributes in
154
    a dict. It does not work if the class has children who are
155
    ConfigObjects themselves (e.g. the nics list in an Instance), in
156
    which case the object should subclass the function in order to
157
    make sure all objects returned are only standard python types.
158

159
    """
160
    return dict([(k, getattr(self, k, None)) for k in self.__slots__])
161

    
162
  @classmethod
163
  def FromDict(cls, val):
164
    """Create an object from a dictionary.
165

166
    This generic routine takes a dict, instantiates a new instance of
167
    the given class, and sets attributes based on the dict content.
168

169
    As for `ToDict`, this does not work if the class has children
170
    who are ConfigObjects themselves (e.g. the nics list in an
171
    Instance), in which case the object should subclass the function
172
    and alter the objects.
173

174
    """
175
    if not isinstance(val, dict):
176
      raise errors.ConfigurationError("Invalid object passed to FromDict:"
177
                                      " expected dict, got %s" % type(val))
178
    obj = cls(**val)
179
    return obj
180

    
181
  @staticmethod
182
  def _ContainerToDicts(container):
183
    """Convert the elements of a container to standard python types.
184

185
    This method converts a container with elements derived from
186
    ConfigData to standard python types. If the container is a dict,
187
    we don't touch the keys, only the values.
188

189
    """
190
    if isinstance(container, dict):
191
      ret = dict([(k, v.ToDict()) for k, v in container.iteritems()])
192
    elif isinstance(container, (list, tuple, set, frozenset)):
193
      ret = [elem.ToDict() for elem in container]
194
    else:
195
      raise TypeError("Invalid type %s passed to _ContainerToDicts" %
196
                      type(container))
197
    return ret
198

    
199
  @staticmethod
200
  def _ContainerFromDicts(source, c_type, e_type):
201
    """Convert a container from standard python types.
202

203
    This method converts a container with standard python types to
204
    ConfigData objects. If the container is a dict, we don't touch the
205
    keys, only the values.
206

207
    """
208
    if not isinstance(c_type, type):
209
      raise TypeError("Container type %s passed to _ContainerFromDicts is"
210
                      " not a type" % type(c_type))
211
    if c_type is dict:
212
      ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
213
    elif c_type in (list, tuple, set, frozenset):
214
      ret = c_type([e_type.FromDict(elem) for elem in source])
215
    else:
216
      raise TypeError("Invalid container type %s passed to"
217
                      " _ContainerFromDicts" % c_type)
218
    return ret
219

    
220
  def __repr__(self):
221
    """Implement __repr__ for ConfigObjects."""
222
    return repr(self.ToDict())
223

    
224

    
225
class TaggableObject(ConfigObject):
226
  """An generic class supporting tags.
227

228
  """
229
  __slots__ = ConfigObject.__slots__ + ["tags"]
230

    
231
  @staticmethod
232
  def ValidateTag(tag):
233
    """Check if a tag is valid.
234

235
    If the tag is invalid, an errors.TagError will be raised. The
236
    function has no return value.
237

238
    """
239
    if not isinstance(tag, basestring):
240
      raise errors.TagError("Invalid tag type (not a string)")
241
    if len(tag) > constants.MAX_TAG_LEN:
242
      raise errors.TagError("Tag too long (>%d)" % constants.MAX_TAG_LEN)
243
    if not tag:
244
      raise errors.TagError("Tags cannot be empty")
245
    if not re.match("^[ \w.+*/:-]+$", tag):
246
      raise errors.TagError("Tag contains invalid characters")
247

    
248
  def GetTags(self):
249
    """Return the tags list.
250

251
    """
252
    tags = getattr(self, "tags", None)
253
    if tags is None:
254
      tags = self.tags = set()
255
    return tags
256

    
257
  def AddTag(self, tag):
258
    """Add a new tag.
259

260
    """
261
    self.ValidateTag(tag)
262
    tags = self.GetTags()
263
    if len(tags) >= constants.MAX_TAGS_PER_OBJ:
264
      raise errors.TagError("Too many tags")
265
    self.GetTags().add(tag)
266

    
267
  def RemoveTag(self, tag):
268
    """Remove a tag.
269

270
    """
271
    self.ValidateTag(tag)
272
    tags = self.GetTags()
273
    try:
274
      tags.remove(tag)
275
    except KeyError:
276
      raise errors.TagError("Tag not found")
277

    
278
  def ToDict(self):
279
    """Taggable-object-specific conversion to standard python types.
280

281
    This replaces the tags set with a list.
282

283
    """
284
    bo = super(TaggableObject, self).ToDict()
285

    
286
    tags = bo.get("tags", None)
287
    if isinstance(tags, set):
288
      bo["tags"] = list(tags)
289
    return bo
290

    
291
  @classmethod
292
  def FromDict(cls, val):
293
    """Custom function for instances.
294

295
    """
296
    obj = super(TaggableObject, cls).FromDict(val)
297
    if hasattr(obj, "tags") and isinstance(obj.tags, list):
298
      obj.tags = set(obj.tags)
299
    return obj
300

    
301

    
302
class ConfigData(ConfigObject):
303
  """Top-level config object."""
304
  __slots__ = ["cluster", "nodes", "instances"]
305

    
306
  def ToDict(self):
307
    """Custom function for top-level config data.
308

309
    This just replaces the list of instances, nodes and the cluster
310
    with standard python types.
311

312
    """
313
    mydict = super(ConfigData, self).ToDict()
314
    mydict["cluster"] = mydict["cluster"].ToDict()
315
    for key in "nodes", "instances":
316
      mydict[key] = self._ContainerToDicts(mydict[key])
317

    
318
    return mydict
319

    
320
  @classmethod
321
  def FromDict(cls, val):
322
    """Custom function for top-level config data
323

324
    """
325
    obj = super(ConfigData, cls).FromDict(val)
326
    obj.cluster = Cluster.FromDict(obj.cluster)
327
    obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
328
    obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
329
    return obj
330

    
331

    
332
class NIC(ConfigObject):
333
  """Config object representing a network card."""
334
  __slots__ = ["mac", "ip", "bridge"]
335

    
336

    
337
class Disk(ConfigObject):
338
  """Config object representing a block device."""
339
  __slots__ = ["dev_type", "logical_id", "physical_id",
340
               "children", "iv_name", "size"]
341

    
342
  def CreateOnSecondary(self):
343
    """Test if this device needs to be created on a secondary node."""
344
    return self.dev_type in ("drbd", "lvm")
345

    
346
  def AssembleOnSecondary(self):
347
    """Test if this device needs to be assembled on a secondary node."""
348
    return self.dev_type in ("drbd", "lvm")
349

    
350
  def OpenOnSecondary(self):
351
    """Test if this device needs to be opened on a secondary node."""
352
    return self.dev_type in ("lvm",)
353

    
354
  def GetNodes(self, node):
355
    """This function returns the nodes this device lives on.
356

357
    Given the node on which the parent of the device lives on (or, in
358
    case of a top-level device, the primary node of the devices'
359
    instance), this function will return a list of nodes on which this
360
    devices needs to (or can) be assembled.
361

362
    """
363
    if self.dev_type == "lvm" or self.dev_type == "md_raid1":
364
      result = [node]
365
    elif self.dev_type == "drbd":
366
      result = [self.logical_id[0], self.logical_id[1]]
367
      if node not in result:
368
        raise errors.ConfigurationError("DRBD device passed unknown node")
369
    else:
370
      raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
371
    return result
372

    
373
  def ComputeNodeTree(self, parent_node):
374
    """Compute the node/disk tree for this disk and its children.
375

376
    This method, given the node on which the parent disk lives, will
377
    return the list of all (node, disk) pairs which describe the disk
378
    tree in the most compact way. For example, a md/drbd/lvm stack
379
    will be returned as (primary_node, md) and (secondary_node, drbd)
380
    which represents all the top-level devices on the nodes. This
381
    means that on the primary node we need to activate the the md (and
382
    recursively all its children) and on the secondary node we need to
383
    activate the drbd device (and its children, the two lvm volumes).
384

385
    """
386
    my_nodes = self.GetNodes(parent_node)
387
    result = [(node, self) for node in my_nodes]
388
    if not self.children:
389
      # leaf device
390
      return result
391
    for node in my_nodes:
392
      for child in self.children:
393
        child_result = child.ComputeNodeTree(node)
394
        if len(child_result) == 1:
395
          # child (and all its descendants) is simple, doesn't split
396
          # over multiple hosts, so we don't need to describe it, our
397
          # own entry for this node describes it completely
398
          continue
399
        else:
400
          # check if child nodes differ from my nodes; note that
401
          # subdisk can differ from the child itself, and be instead
402
          # one of its descendants
403
          for subnode, subdisk in child_result:
404
            if subnode not in my_nodes:
405
              result.append((subnode, subdisk))
406
            # otherwise child is under our own node, so we ignore this
407
            # entry (but probably the other results in the list will
408
            # be different)
409
    return result
410

    
411
  def ToDict(self):
412
    """Disk-specific conversion to standard python types.
413

414
    This replaces the children lists of objects with lists of
415
    standard python types.
416

417
    """
418
    bo = super(Disk, self).ToDict()
419

    
420
    for attr in ("children",):
421
      alist = bo.get(attr, None)
422
      if alist:
423
        bo[attr] = self._ContainerToDicts(alist)
424
    return bo
425

    
426
  @classmethod
427
  def FromDict(cls, val):
428
    """Custom function for Disks
429

430
    """
431
    obj = super(Disk, cls).FromDict(val)
432
    if obj.children:
433
      obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
434
    if obj.logical_id and isinstance(obj.logical_id, list):
435
      obj.logical_id = tuple(obj.logical_id)
436
    if obj.physical_id and isinstance(obj.physical_id, list):
437
      obj.physical_id = tuple(obj.physical_id)
438
    return obj
439

    
440

    
441
class Instance(TaggableObject):
442
  """Config object representing an instance."""
443
  __slots__ = TaggableObject.__slots__ + [
444
    "name",
445
    "primary_node",
446
    "os",
447
    "status",
448
    "memory",
449
    "vcpus",
450
    "nics",
451
    "disks",
452
    "disk_template",
453
    ]
454

    
455
  def _ComputeSecondaryNodes(self):
456
    """Compute the list of secondary nodes.
457

458
    Since the data is already there (in the drbd disks), keeping it as
459
    a separate normal attribute is redundant and if not properly
460
    synchronised can cause problems. Thus it's better to compute it
461
    dynamically.
462

463
    """
464
    def _Helper(primary, sec_nodes, device):
465
      """Recursively computes secondary nodes given a top device."""
466
      if device.dev_type == 'drbd':
467
        nodea, nodeb, dummy = device.logical_id
468
        if nodea == primary:
469
          candidate = nodeb
470
        else:
471
          candidate = nodea
472
        if candidate not in sec_nodes:
473
          sec_nodes.append(candidate)
474
      if device.children:
475
        for child in device.children:
476
          _Helper(primary, sec_nodes, child)
477

    
478
    secondary_nodes = []
479
    for device in self.disks:
480
      _Helper(self.primary_node, secondary_nodes, device)
481
    return tuple(secondary_nodes)
482

    
483
  secondary_nodes = property(_ComputeSecondaryNodes, None, None,
484
                             "List of secondary nodes")
485

    
486
  def MapLVsByNode(self, lvmap=None, devs=None, node=None):
487
    """Provide a mapping of nodes to LVs this instance owns.
488

489
    This function figures out what logical volumes should belong on which
490
    nodes, recursing through a device tree.
491

492
    Args:
493
      lvmap: (optional) a dictionary to receive the 'node' : ['lv', ...] data.
494

495
    Returns:
496
      None if lvmap arg is given.
497
      Otherwise, { 'nodename' : ['volume1', 'volume2', ...], ... }
498

499
    """
500
    if node == None:
501
      node = self.primary_node
502

    
503
    if lvmap is None:
504
      lvmap = { node : [] }
505
      ret = lvmap
506
    else:
507
      if not node in lvmap:
508
        lvmap[node] = []
509
      ret = None
510

    
511
    if not devs:
512
      devs = self.disks
513

    
514
    for dev in devs:
515
      if dev.dev_type == "lvm":
516
        lvmap[node].append(dev.logical_id[1])
517

    
518
      elif dev.dev_type == "drbd":
519
        if dev.logical_id[0] not in lvmap:
520
          lvmap[dev.logical_id[0]] = []
521

    
522
        if dev.logical_id[1] not in lvmap:
523
          lvmap[dev.logical_id[1]] = []
524

    
525
        if dev.children:
526
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
527
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
528

    
529
      elif dev.children:
530
        self.MapLVsByNode(lvmap, dev.children, node)
531

    
532
    return ret
533

    
534
  def FindDisk(self, name):
535
    """Find a disk given having a specified name.
536

537
    This will return the disk which has the given iv_name.
538

539
    """
540
    for disk in self.disks:
541
      if disk.iv_name == name:
542
        return disk
543

    
544
    return None
545

    
546
  def ToDict(self):
547
    """Instance-specific conversion to standard python types.
548

549
    This replaces the children lists of objects with lists of standard
550
    python types.
551

552
    """
553
    bo = super(Instance, self).ToDict()
554

    
555
    for attr in "nics", "disks":
556
      alist = bo.get(attr, None)
557
      if alist:
558
        nlist = self._ContainerToDicts(alist)
559
      else:
560
        nlist = []
561
      bo[attr] = nlist
562
    return bo
563

    
564
  @classmethod
565
  def FromDict(cls, val):
566
    """Custom function for instances.
567

568
    """
569
    obj = super(Instance, cls).FromDict(val)
570
    obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
571
    obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
572
    return obj
573

    
574

    
575
class OS(ConfigObject):
576
  """Config object representing an operating system."""
577
  __slots__ = [
578
    "name",
579
    "path",
580
    "api_version",
581
    "create_script",
582
    "export_script",
583
    "import_script",
584
    "rename_script",
585
    ]
586

    
587

    
588
class Node(TaggableObject):
589
  """Config object representing a node."""
590
  __slots__ = TaggableObject.__slots__ + [
591
    "name",
592
    "primary_ip",
593
    "secondary_ip",
594
    ]
595

    
596

    
597
class Cluster(TaggableObject):
598
  """Config object representing the cluster."""
599
  __slots__ = TaggableObject.__slots__ + [
600
    "config_version",
601
    "serial_no",
602
    "rsahostkeypub",
603
    "highest_used_port",
604
    "tcpudp_port_pool",
605
    "mac_prefix",
606
    "volume_group_name",
607
    "default_bridge",
608
    ]
609

    
610

    
611
class SerializableConfigParser(ConfigParser.SafeConfigParser):
612
  """Simple wrapper over ConfigParse that allows serialization.
613

614
  This class is basically ConfigParser.SafeConfigParser with two
615
  additional methods that allow it to serialize/unserialize to/from a
616
  buffer.
617

618
  """
619
  def Dumps(self):
620
    """Dump this instance and return the string representation."""
621
    buf = StringIO()
622
    self.write(buf)
623
    return buf.getvalue()
624

    
625
  @staticmethod
626
  def Loads(data):
627
    """Load data from a string."""
628
    buf = StringIO(data)
629
    cfp = SerializableConfigParser()
630
    cfp.readfp(buf)
631
    return cfp