cluster modify: deprecate --no-drbd-storage
[ganeti-local] / lib / opcodes_base.py
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)