Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ 5fcdc80d

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
    if node == None:
289
      node = self.primary_node
290

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

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

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

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

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

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

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

    
320
    return ret
321

    
322

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

    
334

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

    
339

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

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

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

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

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