Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ ec29fe40

History | View | Annotate | Download (12.5 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

    
151
class TaggableObject(ConfigObject):
152
  """An generic class supporting tags.
153

154
  """
155
  __slots__ = ConfigObject.__slots__ + ["tags"]
156

    
157
  @staticmethod
158
  def ValidateTag(tag):
159
    """Check if a tag is valid.
160

161
    If the tag is invalid, an errors.TagError will be raised. The
162
    function has no return value.
163

164
    """
165
    if not isinstance(tag, basestring):
166
      raise errors.TagError("Invalid tag type (not a string)")
167
    if len(tag) > constants.MAX_TAG_LEN:
168
      raise errors.TagError("Tag too long (>%d)" % constants.MAX_TAG_LEN)
169
    if not tag:
170
      raise errors.TagError("Tags cannot be empty")
171
    if not re.match("^[ \w.+*/:-]+$", tag):
172
      raise errors.TagError("Tag contains invalid characters")
173

    
174
  def GetTags(self):
175
    """Return the tags list.
176

177
    """
178
    tags = getattr(self, "tags", None)
179
    if tags is None:
180
      tags = self.tags = set()
181
    return tags
182

    
183
  def AddTag(self, tag):
184
    """Add a new tag.
185

186
    """
187
    self.ValidateTag(tag)
188
    tags = self.GetTags()
189
    if len(tags) >= constants.MAX_TAGS_PER_OBJ:
190
      raise errors.TagError("Too many tags")
191
    self.GetTags().add(tag)
192

    
193
  def RemoveTag(self, tag):
194
    """Remove a tag.
195

196
    """
197
    self.ValidateTag(tag)
198
    tags = self.GetTags()
199
    try:
200
      tags.remove(tag)
201
    except KeyError:
202
      raise errors.TagError("Tag not found")
203

    
204

    
205
class ConfigData(ConfigObject):
206
  """Top-level config object."""
207
  __slots__ = ["cluster", "nodes", "instances"]
208

    
209

    
210
class NIC(ConfigObject):
211
  """Config object representing a network card."""
212
  __slots__ = ["mac", "ip", "bridge"]
213

    
214

    
215
class Disk(ConfigObject):
216
  """Config object representing a block device."""
217
  __slots__ = ["dev_type", "logical_id", "physical_id",
218
               "children", "iv_name", "size"]
219

    
220
  def CreateOnSecondary(self):
221
    """Test if this device needs to be created on a secondary node."""
222
    return self.dev_type in ("drbd", "lvm")
223

    
224
  def AssembleOnSecondary(self):
225
    """Test if this device needs to be assembled on a secondary node."""
226
    return self.dev_type in ("drbd", "lvm")
227

    
228
  def OpenOnSecondary(self):
229
    """Test if this device needs to be opened on a secondary node."""
230
    return self.dev_type in ("lvm",)
231

    
232
  def GetNodes(self, node):
233
    """This function returns the nodes this device lives on.
234

235
    Given the node on which the parent of the device lives on (or, in
236
    case of a top-level device, the primary node of the devices'
237
    instance), this function will return a list of nodes on which this
238
    devices needs to (or can) be assembled.
239

240
    """
241
    if self.dev_type == "lvm" or self.dev_type == "md_raid1":
242
      result = [node]
243
    elif self.dev_type == "drbd":
244
      result = [self.logical_id[0], self.logical_id[1]]
245
      if node not in result:
246
        raise errors.ConfigurationError("DRBD device passed unknown node")
247
    else:
248
      raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
249
    return result
250

    
251
  def ComputeNodeTree(self, parent_node):
252
    """Compute the node/disk tree for this disk and its children.
253

254
    This method, given the node on which the parent disk lives, will
255
    return the list of all (node, disk) pairs which describe the disk
256
    tree in the most compact way. For example, a md/drbd/lvm stack
257
    will be returned as (primary_node, md) and (secondary_node, drbd)
258
    which represents all the top-level devices on the nodes. This
259
    means that on the primary node we need to activate the the md (and
260
    recursively all its children) and on the secondary node we need to
261
    activate the drbd device (and its children, the two lvm volumes).
262

263
    """
264
    my_nodes = self.GetNodes(parent_node)
265
    result = [(node, self) for node in my_nodes]
266
    if not self.children:
267
      # leaf device
268
      return result
269
    for node in my_nodes:
270
      for child in self.children:
271
        child_result = child.ComputeNodeTree(node)
272
        if len(child_result) == 1:
273
          # child (and all its descendants) is simple, doesn't split
274
          # over multiple hosts, so we don't need to describe it, our
275
          # own entry for this node describes it completely
276
          continue
277
        else:
278
          # check if child nodes differ from my nodes; note that
279
          # subdisk can differ from the child itself, and be instead
280
          # one of its descendants
281
          for subnode, subdisk in child_result:
282
            if subnode not in my_nodes:
283
              result.append((subnode, subdisk))
284
            # otherwise child is under our own node, so we ignore this
285
            # entry (but probably the other results in the list will
286
            # be different)
287
    return result
288

    
289

    
290
class Instance(TaggableObject):
291
  """Config object representing an instance."""
292
  __slots__ = TaggableObject.__slots__ + [
293
    "name",
294
    "primary_node",
295
    "os",
296
    "status",
297
    "memory",
298
    "vcpus",
299
    "nics",
300
    "disks",
301
    "disk_template",
302
    ]
303

    
304
  def _ComputeSecondaryNodes(self):
305
    """Compute the list of secondary nodes.
306

307
    Since the data is already there (in the drbd disks), keeping it as
308
    a separate normal attribute is redundant and if not properly
309
    synchronised can cause problems. Thus it's better to compute it
310
    dynamically.
311

312
    """
313
    def _Helper(primary, sec_nodes, device):
314
      """Recursively computes secondary nodes given a top device."""
315
      if device.dev_type == 'drbd':
316
        nodea, nodeb, dummy = device.logical_id
317
        if nodea == primary:
318
          candidate = nodeb
319
        else:
320
          candidate = nodea
321
        if candidate not in sec_nodes:
322
          sec_nodes.append(candidate)
323
      if device.children:
324
        for child in device.children:
325
          _Helper(primary, sec_nodes, child)
326

    
327
    secondary_nodes = []
328
    for device in self.disks:
329
      _Helper(self.primary_node, secondary_nodes, device)
330
    return tuple(secondary_nodes)
331

    
332
  secondary_nodes = property(_ComputeSecondaryNodes, None, None,
333
                             "List of secondary nodes")
334

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

338
    This function figures out what logical volumes should belong on which
339
    nodes, recursing through a device tree.
340

341
    Args:
342
      lvmap: (optional) a dictionary to receive the 'node' : ['lv', ...] data.
343

344
    Returns:
345
      None if lvmap arg is given.
346
      Otherwise, { 'nodename' : ['volume1', 'volume2', ...], ... }
347

348
    """
349
    if node == None:
350
      node = self.primary_node
351

    
352
    if lvmap is None:
353
      lvmap = { node : [] }
354
      ret = lvmap
355
    else:
356
      if not node in lvmap:
357
        lvmap[node] = []
358
      ret = None
359

    
360
    if not devs:
361
      devs = self.disks
362

    
363
    for dev in devs:
364
      if dev.dev_type == "lvm":
365
        lvmap[node].append(dev.logical_id[1])
366

    
367
      elif dev.dev_type == "drbd":
368
        if dev.logical_id[0] not in lvmap:
369
          lvmap[dev.logical_id[0]] = []
370

    
371
        if dev.logical_id[1] not in lvmap:
372
          lvmap[dev.logical_id[1]] = []
373

    
374
        if dev.children:
375
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
376
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
377

    
378
      elif dev.children:
379
        self.MapLVsByNode(lvmap, dev.children, node)
380

    
381
    return ret
382

    
383
  def FindDisk(self, name):
384
    """Find a disk given having a specified name.
385

386
    This will return the disk which has the given iv_name.
387

388
    """
389
    for disk in self.disks:
390
      if disk.iv_name == name:
391
        return disk
392

    
393
    return None
394

    
395

    
396
class OS(ConfigObject):
397
  """Config object representing an operating system."""
398
  __slots__ = [
399
    "name",
400
    "path",
401
    "api_version",
402
    "create_script",
403
    "export_script",
404
    "import_script",
405
    "rename_script",
406
    ]
407

    
408

    
409
class Node(TaggableObject):
410
  """Config object representing a node."""
411
  __slots__ = TaggableObject.__slots__ + [
412
    "name",
413
    "primary_ip",
414
    "secondary_ip",
415
    ]
416

    
417

    
418
class Cluster(TaggableObject):
419
  """Config object representing the cluster."""
420
  __slots__ = TaggableObject.__slots__ + [
421
    "config_version",
422
    "serial_no",
423
    "rsahostkeypub",
424
    "highest_used_port",
425
    "tcpudp_port_pool",
426
    "mac_prefix",
427
    "volume_group_name",
428
    "default_bridge",
429
    ]
430

    
431

    
432
class SerializableConfigParser(ConfigParser.SafeConfigParser):
433
  """Simple wrapper over ConfigParse that allows serialization.
434

435
  This class is basically ConfigParser.SafeConfigParser with two
436
  additional methods that allow it to serialize/unserialize to/from a
437
  buffer.
438

439
  """
440
  def Dumps(self):
441
    """Dump this instance and return the string representation."""
442
    buf = StringIO()
443
    self.write(buf)
444
    return buf.getvalue()
445

    
446
  @staticmethod
447
  def Loads(data):
448
    """Load data from a string."""
449
    buf = StringIO(data)
450
    cfp = SerializableConfigParser()
451
    cfp.readfp(buf)
452
    return cfp