Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ 2057f6c7

History | View | Annotate | Download (12.4 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
  __slots__ = []
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(ConfigObject, TaggableObject):
291
  """Config object representing an instance."""
292
  __slots__ = [
293
    "name",
294
    "primary_node",
295
    "os",
296
    "status",
297
    "memory",
298
    "vcpus",
299
    "nics",
300
    "disks",
301
    "disk_template",
302
    "tags",
303
    ]
304

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
382
    return ret
383

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

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

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

    
394
    return None
395

    
396

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

    
409

    
410
class Node(ConfigObject, TaggableObject):
411
  """Config object representing a node."""
412
  __slots__ = ["name", "primary_ip", "secondary_ip", "tags"]
413

    
414

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

    
429

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

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

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

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