Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ a8083063

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

    
34
from ganeti import errors
35

    
36

    
37
__all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
38
           "OS", "Node", "Cluster"]
39

    
40

    
41
class ConfigObject(object):
42
  """A generic config object.
43

44
  It has the following properties:
45

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

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

53
  """
54
  __slots__ = []
55

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

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

    
66
  def __getstate__(self):
67
    state = {}
68
    for name in self.__slots__:
69
      if hasattr(self, name):
70
        state[name] = getattr(self, name)
71
    return state
72

    
73
  def __setstate__(self, state):
74
    for name in state:
75
      if name in self.__slots__:
76
        setattr(self, name, state[name])
77

    
78
  @staticmethod
79
  def FindGlobal(module, name):
80
    """Function filtering the allowed classes to be un-pickled.
81

82
    Currently, we only allow the classes from this module which are
83
    derived from ConfigObject.
84

85
    """
86
    # Also support the old module name (ganeti.config)
87
    cls = None
88
    if module == "ganeti.config" or module == "ganeti.objects":
89
      if name == "ConfigData":
90
        cls = ConfigData
91
      elif name == "NIC":
92
        cls = NIC
93
      elif name == "Disk" or name == "BlockDev":
94
        cls = Disk
95
      elif name == "Instance":
96
        cls = Instance
97
      elif name == "OS":
98
        cls = OS
99
      elif name == "Node":
100
        cls = Node
101
      elif name == "Cluster":
102
        cls = Cluster
103
    if cls is None:
104
      raise cPickle.UnpicklingError, ("Class %s.%s not allowed due to"
105
                                      " security concerns" % (module, name))
106
    return cls
107

    
108
  def Dump(self, fobj):
109
    """Dump this instance to a file object.
110

111
    Note that we use the HIGHEST_PROTOCOL, as it brings benefits for
112
    the new classes.
113

114
    """
115
    dumper = cPickle.Pickler(fobj, cPickle.HIGHEST_PROTOCOL)
116
    dumper.dump(self)
117

    
118
  @staticmethod
119
  def Load(fobj):
120
    """Unpickle data from the given stream.
121

122
    This uses the `FindGlobal` function to filter the allowed classes.
123

124
    """
125
    loader = cPickle.Unpickler(fobj)
126
    loader.find_global = ConfigObject.FindGlobal
127
    return loader.load()
128

    
129
  def Dumps(self):
130
    """Dump this instance and return the string representation."""
131
    buf = StringIO()
132
    self.Dump(buf)
133
    return buf.getvalue()
134

    
135
  @staticmethod
136
  def Loads(data):
137
    """Load data from a string."""
138
    return ConfigObject.Load(StringIO(data))
139

    
140

    
141
class ConfigData(ConfigObject):
142
  """Top-level config object."""
143
  __slots__ = ["cluster", "nodes", "instances"]
144

    
145

    
146
class NIC(ConfigObject):
147
  """Config object representing a network card."""
148
  __slots__ = ["mac", "ip", "bridge"]
149

    
150

    
151
class Disk(ConfigObject):
152
  """Config object representing a block device."""
153
  __slots__ = ["dev_type", "logical_id", "physical_id",
154
               "children", "iv_name", "size"]
155

    
156
  def CreateOnSecondary(self):
157
    """Test if this device needs to be created on a secondary node."""
158
    return self.dev_type in ("drbd", "lvm")
159

    
160
  def AssembleOnSecondary(self):
161
    """Test if this device needs to be assembled on a secondary node."""
162
    return self.dev_type in ("drbd", "lvm")
163

    
164
  def OpenOnSecondary(self):
165
    """Test if this device needs to be opened on a secondary node."""
166
    return self.dev_type in ("lvm",)
167

    
168
  def GetNodes(self, node):
169
    """This function returns the nodes this device lives on.
170

171
    Given the node on which the parent of the device lives on (or, in
172
    case of a top-level device, the primary node of the devices'
173
    instance), this function will return a list of nodes on which this
174
    devices needs to (or can) be assembled.
175

176
    """
177
    if self.dev_type == "lvm" or self.dev_type == "md_raid1":
178
      result = [node]
179
    elif self.dev_type == "drbd":
180
      result = [self.logical_id[0], self.logical_id[1]]
181
      if node not in result:
182
        raise errors.ConfigurationError, ("DRBD device passed unknown node")
183
    else:
184
      raise errors.ProgrammerError, "Unhandled device type %s" % self.dev_type
185
    return result
186

    
187
  def ComputeNodeTree(self, parent_node):
188
    """Compute the node/disk tree for this disk and its children.
189

190
    This method, given the node on which the parent disk lives, will
191
    return the list of all (node, disk) pairs which describe the disk
192
    tree in the most compact way. For example, a md/drbd/lvm stack
193
    will be returned as (primary_node, md) and (secondary_node, drbd)
194
    which represents all the top-level devices on the nodes. This
195
    means that on the primary node we need to activate the the md (and
196
    recursively all its children) and on the secondary node we need to
197
    activate the drbd device (and its children, the two lvm volumes).
198

199
    """
200
    my_nodes = self.GetNodes(parent_node)
201
    result = [(node, self) for node in my_nodes]
202
    if not self.children:
203
      # leaf device
204
      return result
205
    for node in my_nodes:
206
      for child in self.children:
207
        child_result = child.ComputeNodeTree(node)
208
        if len(child_result) == 1:
209
          # child (and all its descendants) is simple, doesn't split
210
          # over multiple hosts, so we don't need to describe it, our
211
          # own entry for this node describes it completely
212
          continue
213
        else:
214
          # check if child nodes differ from my nodes; note that
215
          # subdisk can differ from the child itself, and be instead
216
          # one of its descendants
217
          for subnode, subdisk in child_result:
218
            if subnode not in my_nodes:
219
              result.append((subnode, subdisk))
220
            # otherwise child is under our own node, so we ignore this
221
            # entry (but probably the other results in the list will
222
            # be different)
223
    return result
224

    
225

    
226
class Instance(ConfigObject):
227
  """Config object representing an instance."""
228
  __slots__ = [
229
    "name",
230
    "primary_node",
231
    "os",
232
    "status",
233
    "memory",
234
    "vcpus",
235
    "nics",
236
    "disks",
237
    "disk_template",
238
    ]
239

    
240
  def _ComputeSecondaryNodes(self):
241
    """Compute the list of secondary nodes.
242

243
    Since the data is already there (in the drbd disks), keeping it as
244
    a separate normal attribute is redundant and if not properly
245
    synchronised can cause problems. Thus it's better to compute it
246
    dynamically.
247

248
    """
249
    def _Helper(primary, sec_nodes, device):
250
      """Recursively computes secondary nodes given a top device."""
251
      if device.dev_type == 'drbd':
252
        nodea, nodeb, dummy = device.logical_id
253
        if nodea == primary:
254
          candidate = nodeb
255
        else:
256
          candidate = nodea
257
        if candidate not in sec_nodes:
258
          sec_nodes.append(candidate)
259
      if device.children:
260
        for child in device.children:
261
          _Helper(primary, sec_nodes, child)
262

    
263
    secondary_nodes = []
264
    for device in self.disks:
265
      _Helper(self.primary_node, secondary_nodes, device)
266
    return tuple(secondary_nodes)
267

    
268
  secondary_nodes = property(_ComputeSecondaryNodes, None, None,
269
                             "List of secondary nodes")
270

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

274
    This function figures out what logical volumes should belong on which
275
    nodes, recursing through a device tree.
276

277
    Args:
278
      lvmap: (optional) a dictionary to receive the 'node' : ['lv', ...] data.
279

280
    Returns:
281
      None if lvmap arg is given.
282
      Otherwise, { 'nodename' : ['volume1', 'volume2', ...], ... }
283

284
    """
285

    
286
    if node == None:
287
      node = self.primary_node
288

    
289
    if lvmap is None:
290
      lvmap = { node : [] }
291
      ret = lvmap
292
    else:
293
      if not node in lvmap:
294
        lvmap[node] = []
295
      ret = None
296

    
297
    if not devs:
298
      devs = self.disks
299

    
300
    for dev in devs:
301
      if dev.dev_type == "lvm":
302
        lvmap[node].append(dev.logical_id[1])
303

    
304
      elif dev.dev_type == "drbd":
305
        if dev.logical_id[0] not in lvmap:
306
          lvmap[dev.logical_id[0]] = []
307

    
308
        if dev.logical_id[1] not in lvmap:
309
          lvmap[dev.logical_id[1]] = []
310

    
311
        if dev.children:
312
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
313
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
314

    
315
      elif dev.children:
316
        self.MapLVsByNode(lvmap, dev.children, node)
317

    
318
    return ret
319

    
320

    
321
class OS(ConfigObject):
322
  """Config object representing an operating system."""
323
  __slots__ = [
324
    "name",
325
    "path",
326
    "api_version",
327
    "create_script",
328
    "export_script",
329
    "import_script"
330
    ]
331

    
332

    
333
class Node(ConfigObject):
334
  """Config object representing a node."""
335
  __slots__ = ["name", "primary_ip", "secondary_ip"]
336

    
337

    
338
class Cluster(ConfigObject):
339
  """Config object representing the cluster."""
340
  __slots__ = [
341
    "config_version",
342
    "serial_no",
343
    "master_node",
344
    "name",
345
    "rsahostkeypub",
346
    "highest_used_port",
347
    "mac_prefix",
348
    "volume_group_name",
349
    "default_bridge",
350
    ]
351

    
352
class SerializableConfigParser(ConfigParser.SafeConfigParser):
353
  """Simple wrapper over ConfigParse that allows serialization.
354

355
  This class is basically ConfigParser.SafeConfigParser with two
356
  additional methods that allow it to serialize/unserialize to/from a
357
  buffer.
358

359
  """
360
  def Dumps(self):
361
    """Dump this instance and return the string representation."""
362
    buf = StringIO()
363
    self.write(buf)
364
    return buf.getvalue()
365

    
366
  @staticmethod
367
  def Loads(data):
368
    """Load data from a string."""
369
    buf = StringIO(data)
370
    cfp = SerializableConfigParser()
371
    cfp.readfp(buf)
372
    return cfp