-
Notifications
You must be signed in to change notification settings - Fork 3
/
step.py
299 lines (225 loc) · 12 KB
/
step.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# ex:ts=4:sw=4:sts=4:et
# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
r"""
The base class for QuickRelease steps, along with error reporting classes,
custom subclasses of steps, and classes to run the individual parts of the
steps.
L{Step}s are usually searched for in the C{quickrelease/steps} directory. This behavior can be modified by setting the B{C{QUICKRELEASE_DEFINITIONS_PATH}} to the name of a directory containing both a "processes" and "steps" directory.
To exclude the inclusion of any L{Process}es or L{Step<quickrelease.step.Step>}s in the standard QuickRelease directories, set B{C{QUICKRELEASE_OVERRIDE_DEFAULT_DEFINITIONS}} in the environment.
"""
import os
from quickrelease.exception import ReleaseFrameworkError, ReleaseFrameworkErrorCollection
from quickrelease.utils import GetActivePartners, PrintReleaseFrameworkError
class StepError(ReleaseFrameworkError):
"""
An exception subclassed from L{ReleaseFrameworkError<quickrelease.exception.ReleaseFrameworkError>} which provides a more useful error message about the specific L{Step} the error occured in.
"""
def __init__(self, stepObj, errStr, *args, **kwargs):
ReleaseFrameworkError.__init__(self, errStr, stepObj)
assert isinstance(stepObj, Step), "StepErrors require a Step object"
self._partnerStr = ""
if isinstance(stepObj, PartnerStep):
self._partnerStr = " (partner: %s)" % (stepObj.activePartner)
def _GetErroredStep(self): return self.details
erroredStep = property(_GetErroredStep)
def __str__(self):
return "Error in step %s%s: %s" % (self.erroredStep, self._partnerStr,
ReleaseFrameworkError.__str__(self))
class _StandardStepRunner(object):
def __init__(self, *args, **kwargs):
object.__init__(self)
def DoPreflight(self, stepObj):
stepObj.Preflight()
def DoExecute(self, stepObj):
stepObj.Execute()
def DoVerify(self, stepObj):
stepObj.Verify()
def DoNotify(self, stepObj):
stepObj.Notify()
class Step(object):
"""
An object representing a single step of a defined L{Process<quickrelease.process.Process>}
"""
def __init__(self, *args, **kwargs):
"""
Construct a L{Step} object.
@param process: The parent-process this L{Step} belongs to.
@type process: L{Process<quickrelease.process.Process>}
@param runner: The L{Step}-runner to use for this L{Step}. This allows different types of L{Step}'s to modify the logic of what it means to "run" a step if they so choose (e.g. a L{PartnerStep}).
@type runner: object
"""
object.__init__(self)
self._parentProcess = None
self._runner = _StandardStepRunner()
if kwargs.has_key('process'):
self._parentProcess = kwargs['process']
if kwargs.has_key('runner'):
self._runner = kwargs['runner']
def __str__(self):
"""The L{Step}'s name."""
return self.__class__.__name__
def _GetName(self): return str(self)
def _GetRunner(self): return self._runner
def _GetParentProcess(self): return self._parentProcess
def _GetConfig(self):
if self.process is None:
raise self.SimpleStepError("%s has no associated process to "
"obtain a ConfigSpec." % (self))
elif self.process.config is None:
raise self.SimpleStepError("Process %s has no associated "
"ConfigSpec." % (self.process))
else:
return self.process.config
def _GetLogger(self):
if self.process is None:
raise self.SimpleStepError("%s has no associated process to "
"obtain a Logger." % (self))
elif self.process.logger is None:
raise self.SimpleStepError("Process %s has no associated Logger."
% (self.process))
else:
return self.process.logger
def Log(self, msg):
return self.logger.Log(msg, step=self)
def LogErr(self, msg):
return self.logger.LogErr(msg, step=self)
def LogDebug(self, msg):
return self.logger.LogDebug(msg, step=self)
def ShellCommandLog(self, combined=True):
# TODO: handle to a named file for a log command
pass
name = property(_GetName)
runner = property(_GetRunner)
"""Return the runner object responsible for running the parts of the step. Read-only.
@type: Runner object"""
config = property(_GetConfig)
"""The config associated with the L{Step}'s parent process, if any. Read-only.
@type: L{ConfigSpec<quickrelease.config.ConfigSpec>} or C{None}."""
process = property(_GetParentProcess)
"""The process this step is a part of, if any. Read-only.
@type: L{Process<quickrelease.process.Process>} or C{None}"""
logger = property(_GetLogger)
"""The logger associated with the L{Step}'s parent process, if any. Read-only.
@type: L{Logger<quickrelease.logger.Logger>} or C{None}."""
def Preflight(self):
"""
A method intended for L{Step}s to override with any activities which
must be executed before I{either} the L{Execute<quickrelease.step.Step.Execute>} or L{Verify<quickrelease.step.Step.Verify>} methods need to be executed, if any such activities exist.
"""
pass
def Execute(self):
"""
A method intended for dervied L{Step}s to override with the execution logic of the particular L{Process} step.
B{Note}: This method B{must} be redefined by the dervied L{Step}.
@raise NotImplementedError: If the derived L{Step} does not define an C{Execute} method.
"""
raise NotImplementedError("Need implementation for %s::Execute()" %
(str(self)))
def Verify(self):
"""
A method intended for dervied L{Step}s to override with the unit test-like verification logic of the particular L{Process} step.
B{Note}: This method B{must} be redefined by the dervied L{Step}.
@raise NotImplementedError: If the derived L{Step} does not define an C{Verify} method.
"""
raise NotImplementedError("Need implementation for %s::Verify()" %
(str(self)))
def Notify(self):
"""
A method intended for L{Step}s to override with any notifications that should occur after a step has successful been executed and/or verified.
B{Note}: Currently, these notifications will fire even if only the verification-steps are run.
"""
pass
# We're kinda cheating here; when using it, it looks like SimpleStepError
# is an exception type, not a function; it's mostly a convenience function
# for creating a StepError Exception with a simple message, so we don't
# have to pass the step object the StepError expects explicitly.
def SimpleStepError(self, errStr, details=None):
"""
A convenience method for creating a L{StepError} with a simple message,
so users don't have to pass the L{Step} object the L{StepError} expects
explicitly.
@param errStr: the error string
@type errStr: C{str}
@param details: Extra details about the error condition.
@type details: Variable
@return: An initialized L{StepError} with the current step associated to it.
@rtype: L{StepError}
"""
return StepError(self, errStr, details=details)
class _PartnerStepRunner(object):
def __init__(self, *args, **kwargs):
object.__init__(self)
def _RunPartnerStepMethod(self, stepObj, methodName):
conf = stepObj.config
rootDir = conf.rootDir
errors = []
for p in GetActivePartners(conf):
try:
os.chdir(rootDir)
stepObj.activePartner = p
stepMethod = getattr(stepObj, methodName)
stepMethod()
except ReleaseFrameworkError, ex:
if stepObj._haltOnFirstError:
raise ex
else:
# Unless we're in quiet mode...
PrintReleaseFrameworkError(ex)
errors.append(ex)
if len(errors) != 0:
raise ReleaseFrameworkErrorCollection(errors)
def DoPreflight(self, stepObj):
self._RunPartnerStepMethod(stepObj, "Preflight")
def DoExecute(self, stepObj):
self._RunPartnerStepMethod(stepObj, "Execute")
def DoVerify(self, stepObj):
self._RunPartnerStepMethod(stepObj, "Verify")
def DoNotify(self, stepObj):
self._RunPartnerStepMethod(stepObj, "Notify")
class PartnerStep(Step):
"""
A special type of L{Step} which will perform the requested C{Execute} and
C{Verify} methods for all active partners (as determined by L{GetActivePartners<quickrelease.utils.GetActivePartners>}).
Subclasses may call the the constructor of C{PartnerStep} with the following
keywords to modify its behavior:
1. C{auto_set_partner_config}: By default, when the L{PartnerStep} sets the next partner to execute or verify the portion of the current step, it will also set the section of the associated L{ConfigSpec<quickrelease.config.ConfigSpec>} to the active partner section (via a call to L{SetPartnerSection<quickrelease.config.ConfigSpec.SetPartnerSection>}. Setting this to C{False} will disable that bahavior and make the subclassed L{PartnerStep}s responsible for managing the state of their L{ConfigSpec<quickrelease.config.ConfigSpec>}.
2. C{halt_on_first_error}: By default, if an error is encountered during the execution or verification portion of a L{PartnerStep}, the error will be reported and noted, but the L{Step} will continue for each active partner. Once each active partner's step has been called, I{then} the L{PartnerStep} will halt. For example, say there exist two partners, "Acme" and "Biffco" and a three-step process, consisting of L{PartnerStep}s named C{WillBeOK}, C{WillFailForAcme}, and C{WillNotRun}. By default, C{WillBeOK} will run for Acme and Biffco; C{WillFailForAcme} will run for Acme and fail, and will then run for Biffco, and succeed. At this point, the L{PartnerStep} will halt with the errors, and the last step will not run. If this is set to C{True}, the L{PartnerStep} would immediately halt when it encountered the Acme-error.
"""
def __init__(self, *args, **kwargs):
Step.__init__(self, *args, **kwargs)
self._runner = _PartnerStepRunner()
self._activePartner = None
self._partnerData = {}
self._autoSetPartnerConfig = True
self._haltOnFirstError = False
if kwargs.has_key('auto_set_partner_config'):
self._autoSetPartnerConfig = kwargs['auto_set_partner_config']
if kwargs.has_key('halt_on_first_error'):
self._haltOnFirstError = kwargs['halt_on_first_error']
def _GetActivePartner(self): return self._activePartner
def _SetActivePartner(self, partner):
if partner not in GetActivePartners(self.config):
raise self.SimpleStepError("Unknown partner '%s'" % (partner))
self._activePartner = partner
if self._autoSetPartnerConfig:
self.config.SetPartnerSection(partner)
if partner not in self._partnerData.keys():
self._partnerData[partner] = {}
activePartner = property(_GetActivePartner, _SetActivePartner)
def Save(self, key, data):
"""
Store partner-specific data that may need to persist across a set of C{Execute}/C{Verify} calls.
@param key: Key to retrieve the data.
@type key: C{str}
@param data: The data to store.
@type data: Variable
"""
self._partnerData[self.activePartner][key] = data
def Load(self, key):
"""
Retrieve partner-specific data that may need to persist across a set of C{Execute}/C{Verify} calls.
@param key: Key of data to retrieve
@type key: C{str}
@raise KeyError: If the data described by the specified key does not exist.
"""
return self._partnerData[self.activePartner][key]