Merge branch 'stable-2.16' into stable-2.17
[ganeti-github.git] / lib / opcodes_base.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 Google Inc.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are
9 # met:
10 #
11 # 1. Redistributions of source code must retain the above copyright notice,
12 # this list of conditions and the following disclaimer.
13 #
14 # 2. Redistributions in binary form must reproduce the above copyright
15 # notice, this list of conditions and the following disclaimer in the
16 # documentation and/or other materials provided with the distribution.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
19 # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20 # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30
31 """OpCodes base module
32
33 This module implements part of the data structures which define the
34 cluster operations - the so-called opcodes.
35
36 Every operation which modifies the cluster state is expressed via
37 opcodes.
38
39 """
40
41 # this are practically structures, so disable the message about too
42 # few public methods:
43 # pylint: disable=R0903
44
45 import copy
46 import logging
47 import re
48
49 from ganeti import constants
50 from ganeti import errors
51 from ganeti import ht
52 from ganeti import outils
53
54
55 #: OP_ID conversion regular expression
56 _OPID_RE = re.compile("([a-z])([A-Z])")
57
58 SUMMARY_PREFIX = {
59 "CLUSTER_": "C_",
60 "GROUP_": "G_",
61 "NODE_": "N_",
62 "INSTANCE_": "I_",
63 }
64
65 #: Attribute name for dependencies
66 DEPEND_ATTR = "depends"
67
68 #: Attribute name for comment
69 COMMENT_ATTR = "comment"
70
71
72 def _NameComponents(name):
73 """Split an opcode class name into its components
74
75 @type name: string
76 @param name: the class name, as OpXxxYyy
77 @rtype: array of strings
78 @return: the components of the name
79
80 """
81 assert name.startswith("Op")
82 # Note: (?<=[a-z])(?=[A-Z]) would be ideal, since it wouldn't
83 # consume any input, and hence we would just have all the elements
84 # in the list, one by one; but it seems that split doesn't work on
85 # non-consuming input, hence we have to process the input string a
86 # bit
87 name = _OPID_RE.sub(r"\1,\2", name)
88 elems = name.split(",")
89 return elems
90
91
92 def _NameToId(name):
93 """Convert an opcode class name to an OP_ID.
94
95 @type name: string
96 @param name: the class name, as OpXxxYyy
97 @rtype: string
98 @return: the name in the OP_XXXX_YYYY format
99
100 """
101 if not name.startswith("Op"):
102 return None
103 return "_".join(n.upper() for n in _NameComponents(name))
104
105
106 def NameToReasonSrc(name, prefix):
107 """Convert an opcode class name to a source string for the reason trail
108
109 @type name: string
110 @param name: the class name, as OpXxxYyy
111 @type prefix: string
112 @param prefix: the prefix that will be prepended to the opcode name
113 @rtype: string
114 @return: the name in the OP_XXXX_YYYY format
115
116 """
117 if not name.startswith("Op"):
118 return None
119 return "%s:%s" % (prefix,
120 "_".join(n.lower() for n in _NameComponents(name)))
121
122
123 class _AutoOpParamSlots(outils.AutoSlots):
124 """Meta class for opcode definitions.
125
126 """
127 def __new__(mcs, name, bases, attrs):
128 """Called when a class should be created.
129
130 @param mcs: The meta class
131 @param name: Name of created class
132 @param bases: Base classes
133 @type attrs: dict
134 @param attrs: Class attributes
135
136 """
137 assert "OP_ID" not in attrs, "Class '%s' defining OP_ID" % name
138
139 slots = mcs._GetSlots(attrs)
140 assert "OP_DSC_FIELD" not in attrs or attrs["OP_DSC_FIELD"] in slots, \
141 "Class '%s' uses unknown field in OP_DSC_FIELD" % name
142 assert ("OP_DSC_FORMATTER" not in attrs or
143 callable(attrs["OP_DSC_FORMATTER"])), \
144 ("Class '%s' uses non-callable in OP_DSC_FORMATTER (%s)" %
145 (name, type(attrs["OP_DSC_FORMATTER"])))
146
147 attrs["OP_ID"] = _NameToId(name)
148
149 return outils.AutoSlots.__new__(mcs, name, bases, attrs)
150
151 @classmethod
152 def _GetSlots(mcs, attrs):
153 """Build the slots out of OP_PARAMS.
154
155 """
156 # Always set OP_PARAMS to avoid duplicates in BaseOpCode.GetAllParams
157 params = attrs.setdefault("OP_PARAMS", [])
158
159 # Use parameter names as slots
160 return [pname for (pname, _, _, _) in params]
161
162
163 class BaseOpCode(outils.ValidatedSlots):
164 """A simple serializable object.
165
166 This object serves as a parent class for OpCode without any custom
167 field handling.
168
169 """
170 # pylint: disable=E1101
171 # as OP_ID is dynamically defined
172 __metaclass__ = _AutoOpParamSlots
173
174 def __init__(self, **kwargs):
175 outils.ValidatedSlots.__init__(self, **kwargs)
176 for key, default, _, _ in self.__class__.GetAllParams():
177 if not hasattr(self, key):
178 setattr(self, key, default)
179
180 def __getstate__(self):
181 """Generic serializer.
182
183 This method just returns the contents of the instance as a
184 dictionary.
185
186 @rtype: C{dict}
187 @return: the instance attributes and their values
188
189 """
190 state = {}
191 for name in self.GetAllSlots():
192 if hasattr(self, name):
193 state[name] = getattr(self, name)
194 return state
195
196 def __setstate__(self, state):
197 """Generic unserializer.
198
199 This method just restores from the serialized state the attributes
200 of the current instance.
201
202 @param state: the serialized opcode data
203 @type state: C{dict}
204
205 """
206 if not isinstance(state, dict):
207 raise ValueError("Invalid data to __setstate__: expected dict, got %s" %
208 type(state))
209
210 for name in self.GetAllSlots():
211 if name not in state and hasattr(self, name):
212 delattr(self, name)
213
214 for name in state:
215 setattr(self, name, state[name])
216
217 @classmethod
218 def GetAllParams(cls):
219 """Compute list of all parameters for an opcode.
220
221 """
222 slots = []
223 for parent in cls.__mro__:
224 slots.extend(getattr(parent, "OP_PARAMS", []))
225 return slots
226
227 def Validate(self, set_defaults): # pylint: disable=W0221
228 """Validate opcode parameters, optionally setting default values.
229
230 @type set_defaults: bool
231 @param set_defaults: whether to set default values
232
233 @rtype: NoneType
234 @return: L{None}, if the validation succeeds
235 @raise errors.OpPrereqError: when a parameter value doesn't match
236 requirements
237
238 """
239 for (attr_name, default, test, _) in self.GetAllParams():
240 assert callable(test)
241
242 if hasattr(self, attr_name):
243 attr_val = getattr(self, attr_name)
244 else:
245 attr_val = copy.deepcopy(default)
246
247 if test(attr_val):
248 if set_defaults:
249 setattr(self, attr_name, attr_val)
250 elif ht.TInt(attr_val) and test(float(attr_val)):
251 if set_defaults:
252 setattr(self, attr_name, float(attr_val))
253 else:
254 logging.error("OpCode %s, parameter %s, has invalid type %s/value"
255 " '%s' expecting type %s",
256 self.OP_ID, attr_name, type(attr_val), attr_val, test)
257
258 if attr_val is None:
259 logging.error("OpCode %s, parameter %s, has default value None which"
260 " is does not check against the parameter's type: this"
261 " means this parameter is required but no value was"
262 " given",
263 self.OP_ID, attr_name)
264
265 raise errors.OpPrereqError("Parameter '%s.%s' fails validation" %
266 (self.OP_ID, attr_name),
267 errors.ECODE_INVAL)
268
269
270 def BuildJobDepCheck(relative):
271 """Builds check for job dependencies (L{DEPEND_ATTR}).
272
273 @type relative: bool
274 @param relative: Whether to accept relative job IDs (negative)
275 @rtype: callable
276
277 """
278 if relative:
279 job_id = ht.TOr(ht.TJobId, ht.TRelativeJobId)
280 else:
281 job_id = ht.TJobId
282
283 job_dep = \
284 ht.TAnd(ht.TOr(ht.TListOf(ht.TAny), ht.TTuple),
285 ht.TIsLength(2),
286 ht.TItems([job_id,
287 ht.TListOf(ht.TElemOf(constants.JOBS_FINALIZED))]))
288
289 return ht.TMaybe(ht.TListOf(job_dep))
290
291
292 TNoRelativeJobDependencies = BuildJobDepCheck(False)