Statistics
| Branch: | Tag: | Revision:

root / lib / opcodes_base.py @ 580b1fdd

History | View | Annotate | Download (8 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 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
"""OpCodes base module
23

24
This module implements part of the data structures which define the
25
cluster operations - the so-called opcodes.
26

27
Every operation which modifies the cluster state is expressed via
28
opcodes.
29

30
"""
31

    
32
# this are practically structures, so disable the message about too
33
# few public methods:
34
# pylint: disable=R0903
35

    
36
import copy
37
import logging
38
import re
39

    
40
from ganeti import constants
41
from ganeti import errors
42
from ganeti import ht
43
from ganeti import outils
44

    
45

    
46
#: OP_ID conversion regular expression
47
_OPID_RE = re.compile("([a-z])([A-Z])")
48

    
49
SUMMARY_PREFIX = {
50
  "CLUSTER_": "C_",
51
  "GROUP_": "G_",
52
  "NODE_": "N_",
53
  "INSTANCE_": "I_",
54
  }
55

    
56
#: Attribute name for dependencies
57
DEPEND_ATTR = "depends"
58

    
59
#: Attribute name for comment
60
COMMENT_ATTR = "comment"
61

    
62

    
63
def _NameComponents(name):
64
  """Split an opcode class name into its components
65

66
  @type name: string
67
  @param name: the class name, as OpXxxYyy
68
  @rtype: array of strings
69
  @return: the components of the name
70

71
  """
72
  assert name.startswith("Op")
73
  # Note: (?<=[a-z])(?=[A-Z]) would be ideal, since it wouldn't
74
  # consume any input, and hence we would just have all the elements
75
  # in the list, one by one; but it seems that split doesn't work on
76
  # non-consuming input, hence we have to process the input string a
77
  # bit
78
  name = _OPID_RE.sub(r"\1,\2", name)
79
  elems = name.split(",")
80
  return elems
81

    
82

    
83
def _NameToId(name):
84
  """Convert an opcode class name to an OP_ID.
85

86
  @type name: string
87
  @param name: the class name, as OpXxxYyy
88
  @rtype: string
89
  @return: the name in the OP_XXXX_YYYY format
90

91
  """
92
  if not name.startswith("Op"):
93
    return None
94
  return "_".join(n.upper() for n in _NameComponents(name))
95

    
96

    
97
def NameToReasonSrc(name):
98
  """Convert an opcode class name to a source string for the reason trail
99

100
  @type name: string
101
  @param name: the class name, as OpXxxYyy
102
  @rtype: string
103
  @return: the name in the OP_XXXX_YYYY format
104

105
  """
106
  if not name.startswith("Op"):
107
    return None
108
  return "%s:%s" % (constants.OPCODE_REASON_SRC_OPCODE,
109
                    "_".join(n.lower() for n in _NameComponents(name)))
110

    
111

    
112
class _AutoOpParamSlots(outils.AutoSlots):
113
  """Meta class for opcode definitions.
114

115
  """
116
  def __new__(mcs, name, bases, attrs):
117
    """Called when a class should be created.
118

119
    @param mcs: The meta class
120
    @param name: Name of created class
121
    @param bases: Base classes
122
    @type attrs: dict
123
    @param attrs: Class attributes
124

125
    """
126
    assert "OP_ID" not in attrs, "Class '%s' defining OP_ID" % name
127

    
128
    slots = mcs._GetSlots(attrs)
129
    assert "OP_DSC_FIELD" not in attrs or attrs["OP_DSC_FIELD"] in slots, \
130
      "Class '%s' uses unknown field in OP_DSC_FIELD" % name
131
    assert ("OP_DSC_FORMATTER" not in attrs or
132
            callable(attrs["OP_DSC_FORMATTER"])), \
133
      ("Class '%s' uses non-callable in OP_DSC_FORMATTER (%s)" %
134
       (name, type(attrs["OP_DSC_FORMATTER"])))
135

    
136
    attrs["OP_ID"] = _NameToId(name)
137

    
138
    return outils.AutoSlots.__new__(mcs, name, bases, attrs)
139

    
140
  @classmethod
141
  def _GetSlots(mcs, attrs):
142
    """Build the slots out of OP_PARAMS.
143

144
    """
145
    # Always set OP_PARAMS to avoid duplicates in BaseOpCode.GetAllParams
146
    params = attrs.setdefault("OP_PARAMS", [])
147

    
148
    # Use parameter names as slots
149
    return [pname for (pname, _, _, _) in params]
150

    
151

    
152
class BaseOpCode(outils.ValidatedSlots):
153
  """A simple serializable object.
154

155
  This object serves as a parent class for OpCode without any custom
156
  field handling.
157

158
  """
159
  # pylint: disable=E1101
160
  # as OP_ID is dynamically defined
161
  __metaclass__ = _AutoOpParamSlots
162

    
163
  def __getstate__(self):
164
    """Generic serializer.
165

166
    This method just returns the contents of the instance as a
167
    dictionary.
168

169
    @rtype:  C{dict}
170
    @return: the instance attributes and their values
171

172
    """
173
    state = {}
174
    for name in self.GetAllSlots():
175
      if hasattr(self, name):
176
        state[name] = getattr(self, name)
177
    return state
178

    
179
  def __setstate__(self, state):
180
    """Generic unserializer.
181

182
    This method just restores from the serialized state the attributes
183
    of the current instance.
184

185
    @param state: the serialized opcode data
186
    @type state:  C{dict}
187

188
    """
189
    if not isinstance(state, dict):
190
      raise ValueError("Invalid data to __setstate__: expected dict, got %s" %
191
                       type(state))
192

    
193
    for name in self.GetAllSlots():
194
      if name not in state and hasattr(self, name):
195
        delattr(self, name)
196

    
197
    for name in state:
198
      setattr(self, name, state[name])
199

    
200
  @classmethod
201
  def GetAllParams(cls):
202
    """Compute list of all parameters for an opcode.
203

204
    """
205
    slots = []
206
    for parent in cls.__mro__:
207
      slots.extend(getattr(parent, "OP_PARAMS", []))
208
    return slots
209

    
210
  def Validate(self, set_defaults): # pylint: disable=W0221
211
    """Validate opcode parameters, optionally setting default values.
212

213
    @type set_defaults: bool
214
    @param set_defaults: Whether to set default values
215
    @raise errors.OpPrereqError: When a parameter value doesn't match
216
                                 requirements
217

218
    """
219
    for (attr_name, default, test, _) in self.GetAllParams():
220
      assert callable(test)
221

    
222
      if hasattr(self, attr_name):
223
        attr_val = getattr(self, attr_name)
224
      else:
225
        attr_val = copy.deepcopy(default)
226

    
227
      if test(attr_val):
228
        if set_defaults:
229
          setattr(self, attr_name, attr_val)
230
      elif ht.TInt(attr_val) and test(float(attr_val)):
231
        if set_defaults:
232
          setattr(self, attr_name, float(attr_val))
233
      else:
234
        logging.error("OpCode %s, parameter %s, has invalid type %s/value"
235
                      " '%s' expecting type %s",
236
                      self.OP_ID, attr_name, type(attr_val), attr_val, test)
237

    
238
        if attr_val is None:
239
          logging.error("OpCode %s, parameter %s, has default value None which"
240
                        " is does not check against the parameter's type: this"
241
                        " means this parameter is required but no value was"
242
                        " given",
243
                        self.OP_ID, attr_name)
244

    
245
        raise errors.OpPrereqError("Parameter '%s.%s' fails validation" %
246
                                   (self.OP_ID, attr_name),
247
                                   errors.ECODE_INVAL)
248

    
249

    
250
def BuildJobDepCheck(relative):
251
  """Builds check for job dependencies (L{DEPEND_ATTR}).
252

253
  @type relative: bool
254
  @param relative: Whether to accept relative job IDs (negative)
255
  @rtype: callable
256

257
  """
258
  if relative:
259
    job_id = ht.TOr(ht.TJobId, ht.TRelativeJobId)
260
  else:
261
    job_id = ht.TJobId
262

    
263
  job_dep = \
264
    ht.TAnd(ht.TOr(ht.TListOf(ht.TAny), ht.TTuple),
265
            ht.TIsLength(2),
266
            ht.TItems([job_id,
267
                       ht.TListOf(ht.TElemOf(constants.JOBS_FINALIZED))]))
268

    
269
  return ht.TMaybe(ht.TListOf(job_dep))
270

    
271

    
272
TNoRelativeJobDependencies = BuildJobDepCheck(False)
273

    
274

    
275
def RequireSharedFileStorage():
276
  """Checks that shared file storage is enabled.
277

278
  While it doesn't really fit into this module, L{utils} was deemed too large
279
  of a dependency to be imported for just one or two functions.
280

281
  @raise errors.OpPrereqError: when shared file storage is disabled
282

283
  """
284
  if not constants.ENABLE_SHARED_FILE_STORAGE:
285
    raise errors.OpPrereqError("Shared file storage disabled at"
286
                               " configure time", errors.ECODE_INVAL)