Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ 5c947f38

History | View | Annotate | Download (12.2 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(object):
152
  """An generic class supporting tags.
153

154
  """
155
  @staticmethod
156
  def ValidateTag(tag):
157
    """Check if a tag is valid.
158

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

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

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

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

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

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

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

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

    
203

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

    
208

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

    
213

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

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

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

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

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

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

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

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

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

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

    
288

    
289
class Instance(ConfigObject, TaggableObject):
290
  """Config object representing an instance."""
291
  __slots__ = [
292
    "name",
293
    "primary_node",
294
    "os",
295
    "status",
296
    "memory",
297
    "vcpus",
298
    "nics",
299
    "disks",
300
    "disk_template",
301
    "tags",
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

    
384
class OS(ConfigObject):
385
  """Config object representing an operating system."""
386
  __slots__ = [
387
    "name",
388
    "path",
389
    "api_version",
390
    "create_script",
391
    "export_script",
392
    "import_script"
393
    ]
394

    
395

    
396
class Node(ConfigObject, TaggableObject):
397
  """Config object representing a node."""
398
  __slots__ = ["name", "primary_ip", "secondary_ip", "tags"]
399

    
400

    
401
class Cluster(ConfigObject, TaggableObject):
402
  """Config object representing the cluster."""
403
  __slots__ = [
404
    "config_version",
405
    "serial_no",
406
    "rsahostkeypub",
407
    "highest_used_port",
408
    "tcpudp_port_pool",
409
    "mac_prefix",
410
    "volume_group_name",
411
    "default_bridge",
412
    "tags",
413
    ]
414

    
415

    
416
class SerializableConfigParser(ConfigParser.SafeConfigParser):
417
  """Simple wrapper over ConfigParse that allows serialization.
418

419
  This class is basically ConfigParser.SafeConfigParser with two
420
  additional methods that allow it to serialize/unserialize to/from a
421
  buffer.
422

423
  """
424
  def Dumps(self):
425
    """Dump this instance and return the string representation."""
426
    buf = StringIO()
427
    self.write(buf)
428
    return buf.getvalue()
429

    
430
  @staticmethod
431
  def Loads(data):
432
    """Load data from a string."""
433
    buf = StringIO(data)
434
    cfp = SerializableConfigParser()
435
    cfp.readfp(buf)
436
    return cfp