Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ b2fddf63

History | View | Annotate | Download (10.7 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
    elif module == "__builtin__":
104
      if name == "set":
105
        cls = set
106
    if cls is None:
107
      raise cPickle.UnpicklingError, ("Class %s.%s not allowed due to"
108
                                      " security concerns" % (module, name))
109
    return cls
110

    
111
  def Dump(self, fobj):
112
    """Dump this instance to a file object.
113

114
    Note that we use the HIGHEST_PROTOCOL, as it brings benefits for
115
    the new classes.
116

117
    """
118
    dumper = cPickle.Pickler(fobj, cPickle.HIGHEST_PROTOCOL)
119
    dumper.dump(self)
120

    
121
  @staticmethod
122
  def Load(fobj):
123
    """Unpickle data from the given stream.
124

125
    This uses the `FindGlobal` function to filter the allowed classes.
126

127
    """
128
    loader = cPickle.Unpickler(fobj)
129
    loader.find_global = ConfigObject.FindGlobal
130
    return loader.load()
131

    
132
  def Dumps(self):
133
    """Dump this instance and return the string representation."""
134
    buf = StringIO()
135
    self.Dump(buf)
136
    return buf.getvalue()
137

    
138
  @staticmethod
139
  def Loads(data):
140
    """Load data from a string."""
141
    return ConfigObject.Load(StringIO(data))
142

    
143

    
144
class ConfigData(ConfigObject):
145
  """Top-level config object."""
146
  __slots__ = ["cluster", "nodes", "instances"]
147

    
148

    
149
class NIC(ConfigObject):
150
  """Config object representing a network card."""
151
  __slots__ = ["mac", "ip", "bridge"]
152

    
153

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

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

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

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

    
171
  def GetNodes(self, node):
172
    """This function returns the nodes this device lives on.
173

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

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

    
190
  def ComputeNodeTree(self, parent_node):
191
    """Compute the node/disk tree for this disk and its children.
192

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

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

    
228

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

    
243
  def _ComputeSecondaryNodes(self):
244
    """Compute the list of secondary nodes.
245

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

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

    
266
    secondary_nodes = []
267
    for device in self.disks:
268
      _Helper(self.primary_node, secondary_nodes, device)
269
    return tuple(secondary_nodes)
270

    
271
  secondary_nodes = property(_ComputeSecondaryNodes, None, None,
272
                             "List of secondary nodes")
273

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

277
    This function figures out what logical volumes should belong on which
278
    nodes, recursing through a device tree.
279

280
    Args:
281
      lvmap: (optional) a dictionary to receive the 'node' : ['lv', ...] data.
282

283
    Returns:
284
      None if lvmap arg is given.
285
      Otherwise, { 'nodename' : ['volume1', 'volume2', ...], ... }
286

287
    """
288

    
289
    if node == None:
290
      node = self.primary_node
291

    
292
    if lvmap is None:
293
      lvmap = { node : [] }
294
      ret = lvmap
295
    else:
296
      if not node in lvmap:
297
        lvmap[node] = []
298
      ret = None
299

    
300
    if not devs:
301
      devs = self.disks
302

    
303
    for dev in devs:
304
      if dev.dev_type == "lvm":
305
        lvmap[node].append(dev.logical_id[1])
306

    
307
      elif dev.dev_type == "drbd":
308
        if dev.logical_id[0] not in lvmap:
309
          lvmap[dev.logical_id[0]] = []
310

    
311
        if dev.logical_id[1] not in lvmap:
312
          lvmap[dev.logical_id[1]] = []
313

    
314
        if dev.children:
315
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
316
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
317

    
318
      elif dev.children:
319
        self.MapLVsByNode(lvmap, dev.children, node)
320

    
321
    return ret
322

    
323

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

    
335

    
336
class Node(ConfigObject):
337
  """Config object representing a node."""
338
  __slots__ = ["name", "primary_ip", "secondary_ip"]
339

    
340

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

    
355
class SerializableConfigParser(ConfigParser.SafeConfigParser):
356
  """Simple wrapper over ConfigParse that allows serialization.
357

358
  This class is basically ConfigParser.SafeConfigParser with two
359
  additional methods that allow it to serialize/unserialize to/from a
360
  buffer.
361

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

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