blob: 3b5a1a5cf553357b4d3cfb7488bbecab02f1f8d1 [file] [log] [blame]
James E. Blairee743612012-05-29 14:49:32 -07001# Copyright 2012 Hewlett-Packard Development Company, L.P.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15import re
James E. Blairff986a12012-05-30 14:56:51 -070016import time
James E. Blair4886cc12012-07-18 15:39:41 -070017from uuid import uuid4
James E. Blair5a9918a2013-08-27 10:06:27 -070018import extras
19
20OrderedDict = extras.try_imports(['collections.OrderedDict',
21 'ordereddict.OrderedDict'])
James E. Blair4886cc12012-07-18 15:39:41 -070022
23
James E. Blair19deff22013-08-25 13:17:35 -070024MERGER_MERGE = 1 # "git merge"
25MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
26MERGER_CHERRY_PICK = 3 # "git cherry-pick"
27
28MERGER_MAP = {
29 'merge': MERGER_MERGE,
30 'merge-resolve': MERGER_MERGE_RESOLVE,
31 'cherry-pick': MERGER_CHERRY_PICK,
32}
James E. Blairee743612012-05-29 14:49:32 -070033
James E. Blair64ed6f22013-07-10 14:07:23 -070034PRECEDENCE_NORMAL = 0
35PRECEDENCE_LOW = 1
36PRECEDENCE_HIGH = 2
37
38PRECEDENCE_MAP = {
39 None: PRECEDENCE_NORMAL,
40 'low': PRECEDENCE_LOW,
41 'normal': PRECEDENCE_NORMAL,
42 'high': PRECEDENCE_HIGH,
43}
44
James E. Blair1e8dd892012-05-30 09:15:05 -070045
James E. Blair4aea70c2012-07-26 14:23:24 -070046class Pipeline(object):
47 """A top-level pipeline such as check, gate, post, etc."""
48 def __init__(self, name):
49 self.name = name
James E. Blair8dbd56a2012-12-22 10:55:10 -080050 self.description = None
James E. Blair56370192013-01-14 15:47:28 -080051 self.failure_message = None
52 self.success_message = None
James E. Blair2fa50962013-01-30 21:50:41 -080053 self.dequeue_on_new_patchset = True
James E. Blair4aea70c2012-07-26 14:23:24 -070054 self.job_trees = {} # project -> JobTree
55 self.manager = None
James E. Blaire0487072012-08-29 17:38:31 -070056 self.queues = []
James E. Blair64ed6f22013-07-10 14:07:23 -070057 self.precedence = PRECEDENCE_NORMAL
James E. Blair6c358e72013-07-29 17:06:47 -070058 self.trigger = None
Joshua Hesketh1879cf72013-08-19 14:13:15 +100059 self.start_actions = None
60 self.success_actions = None
61 self.failure_actions = None
James E. Blair4aea70c2012-07-26 14:23:24 -070062
James E. Blaird09c17a2012-08-07 09:23:14 -070063 def __repr__(self):
64 return '<Pipeline %s>' % self.name
65
James E. Blair4aea70c2012-07-26 14:23:24 -070066 def setManager(self, manager):
67 self.manager = manager
68
69 def addProject(self, project):
70 job_tree = JobTree(None) # Null job == job tree root
71 self.job_trees[project] = job_tree
72 return job_tree
73
74 def getProjects(self):
75 return self.job_trees.keys()
76
James E. Blaire0487072012-08-29 17:38:31 -070077 def addQueue(self, queue):
78 self.queues.append(queue)
79
80 def getQueue(self, project):
81 for queue in self.queues:
82 if project in queue.projects:
83 return queue
84 return None
85
James E. Blair4aea70c2012-07-26 14:23:24 -070086 def getJobTree(self, project):
87 tree = self.job_trees.get(project)
88 return tree
89
90 def getJobs(self, changeish):
James E. Blaird09c17a2012-08-07 09:23:14 -070091 tree = self.getJobTree(changeish.project)
James E. Blair4aea70c2012-07-26 14:23:24 -070092 if not tree:
93 return []
94 return changeish.filterJobs(tree.getJobs())
95
James E. Blairfee8d652013-06-07 08:57:52 -070096 def _findJobsToRun(self, job_trees, item):
James E. Blair4aea70c2012-07-26 14:23:24 -070097 torun = []
James E. Blairfee8d652013-06-07 08:57:52 -070098 if item.item_ahead:
James E. Blair4aea70c2012-07-26 14:23:24 -070099 # Only run jobs if any 'hold' jobs on the change ahead
100 # have completed successfully.
James E. Blairfee8d652013-06-07 08:57:52 -0700101 if self.isHoldingFollowingChanges(item.item_ahead):
James E. Blair4aea70c2012-07-26 14:23:24 -0700102 return []
103 for tree in job_trees:
104 job = tree.job
105 result = None
106 if job:
James E. Blairfee8d652013-06-07 08:57:52 -0700107 if not job.changeMatches(item.change):
James E. Blair4aea70c2012-07-26 14:23:24 -0700108 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700109 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700110 if build:
111 result = build.result
112 else:
113 # There is no build for the root of this job tree,
114 # so we should run it.
115 torun.append(job)
116 # If there is no job, this is a null job tree, and we should
117 # run all of its jobs.
118 if result == 'SUCCESS' or not job:
James E. Blairfee8d652013-06-07 08:57:52 -0700119 torun.extend(self._findJobsToRun(tree.job_trees, item))
James E. Blair4aea70c2012-07-26 14:23:24 -0700120 return torun
121
James E. Blairfee8d652013-06-07 08:57:52 -0700122 def findJobsToRun(self, item):
123 tree = self.getJobTree(item.change.project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700124 if not tree:
125 return []
James E. Blairfee8d652013-06-07 08:57:52 -0700126 return self._findJobsToRun(tree.job_trees, item)
James E. Blair4aea70c2012-07-26 14:23:24 -0700127
James E. Blairbea9ef12013-07-15 11:52:23 -0700128 def haveAllJobsStarted(self, item):
129 for job in self.getJobs(item.change):
130 build = item.current_build_set.getBuild(job.name)
131 if not build or not build.start_time:
132 return False
133 return True
134
James E. Blairfee8d652013-06-07 08:57:52 -0700135 def areAllJobsComplete(self, item):
136 for job in self.getJobs(item.change):
137 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700138 if not build or not build.result:
139 return False
140 return True
141
James E. Blairfee8d652013-06-07 08:57:52 -0700142 def didAllJobsSucceed(self, item):
143 for job in self.getJobs(item.change):
James E. Blair4ec821f2012-08-23 15:28:28 -0700144 if not job.voting:
145 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700146 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700147 if not build:
148 return False
149 if build.result != 'SUCCESS':
150 return False
151 return True
152
James E. Blairfee8d652013-06-07 08:57:52 -0700153 def didAnyJobFail(self, item):
154 for job in self.getJobs(item.change):
James E. Blair4ec821f2012-08-23 15:28:28 -0700155 if not job.voting:
156 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700157 build = item.current_build_set.getBuild(job.name)
James E. Blair0018a6c2013-02-27 14:11:45 -0800158 if build and build.result and (build.result != 'SUCCESS'):
James E. Blair4aea70c2012-07-26 14:23:24 -0700159 return True
160 return False
161
James E. Blairfee8d652013-06-07 08:57:52 -0700162 def isHoldingFollowingChanges(self, item):
163 for job in self.getJobs(item.change):
James E. Blair4aea70c2012-07-26 14:23:24 -0700164 if not job.hold_following_changes:
165 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700166 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700167 if not build:
168 return True
169 if build.result != 'SUCCESS':
170 return True
James E. Blair972e3c72013-08-29 12:04:55 -0700171
James E. Blairfee8d652013-06-07 08:57:52 -0700172 if not item.item_ahead:
James E. Blair4aea70c2012-07-26 14:23:24 -0700173 return False
James E. Blairfee8d652013-06-07 08:57:52 -0700174 return self.isHoldingFollowingChanges(item.item_ahead)
James E. Blair4aea70c2012-07-26 14:23:24 -0700175
James E. Blairfee8d652013-06-07 08:57:52 -0700176 def setResult(self, item, build):
James E. Blair4a28a882013-08-23 15:17:33 -0700177 if build.retry:
178 item.removeBuild(build)
179 elif build.result != 'SUCCESS':
James E. Blair4aea70c2012-07-26 14:23:24 -0700180 # Get a JobTree from a Job so we can find only its dependent jobs
James E. Blairfee8d652013-06-07 08:57:52 -0700181 root = self.getJobTree(item.change.project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700182 tree = root.getJobTreeForJob(build.job)
183 for job in tree.getJobs():
184 fakebuild = Build(job, None)
185 fakebuild.result = 'SKIPPED'
James E. Blairfee8d652013-06-07 08:57:52 -0700186 item.addBuild(fakebuild)
James E. Blair4aea70c2012-07-26 14:23:24 -0700187
James E. Blair6736beb2013-07-11 15:18:15 -0700188 def setUnableToMerge(self, item, msg):
James E. Blairfee8d652013-06-07 08:57:52 -0700189 item.current_build_set.unable_to_merge = True
James E. Blair6736beb2013-07-11 15:18:15 -0700190 item.current_build_set.unable_to_merge_message = msg
James E. Blairfee8d652013-06-07 08:57:52 -0700191 root = self.getJobTree(item.change.project)
James E. Blair973721f2012-08-15 10:19:43 -0700192 for job in root.getJobs():
193 fakebuild = Build(job, None)
194 fakebuild.result = 'SKIPPED'
James E. Blairfee8d652013-06-07 08:57:52 -0700195 item.addBuild(fakebuild)
James E. Blair973721f2012-08-15 10:19:43 -0700196
James E. Blairfee8d652013-06-07 08:57:52 -0700197 def setDequeuedNeedingChange(self, item):
198 item.dequeued_needing_change = True
199 root = self.getJobTree(item.change.project)
James E. Blaircaec0c52012-08-22 14:52:22 -0700200 for job in root.getJobs():
201 fakebuild = Build(job, None)
202 fakebuild.result = 'SKIPPED'
James E. Blairfee8d652013-06-07 08:57:52 -0700203 item.addBuild(fakebuild)
James E. Blaircaec0c52012-08-22 14:52:22 -0700204
James E. Blaire0487072012-08-29 17:38:31 -0700205 def getChangesInQueue(self):
206 changes = []
207 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700208 changes.extend([x.change for x in shared_queue.queue])
James E. Blaire0487072012-08-29 17:38:31 -0700209 return changes
210
James E. Blairfee8d652013-06-07 08:57:52 -0700211 def getAllItems(self):
212 items = []
James E. Blaire0487072012-08-29 17:38:31 -0700213 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700214 items.extend(shared_queue.queue)
James E. Blairfee8d652013-06-07 08:57:52 -0700215 return items
James E. Blaire0487072012-08-29 17:38:31 -0700216
217 def formatStatusHTML(self):
218 ret = ''
219 for queue in self.queues:
220 if len(self.queues) > 1:
221 s = 'Change queue: %s' % queue.name
222 ret += s + '\n'
223 ret += '-' * len(s) + '\n'
James E. Blair972e3c72013-08-29 12:04:55 -0700224 for item in queue.queue:
225 ret += self.formatStatus(item, html=True)
James E. Blaire0487072012-08-29 17:38:31 -0700226 return ret
227
James E. Blair8dbd56a2012-12-22 10:55:10 -0800228 def formatStatusJSON(self):
229 j_pipeline = dict(name=self.name,
230 description=self.description)
231 j_queues = []
232 j_pipeline['change_queues'] = j_queues
233 for queue in self.queues:
234 j_queue = dict(name=queue.name)
235 j_queues.append(j_queue)
236 j_queue['heads'] = []
James E. Blair972e3c72013-08-29 12:04:55 -0700237
238 j_changes = []
239 for e in queue.queue:
240 if not e.item_ahead:
241 if j_changes:
242 j_queue['heads'].append(j_changes)
243 j_changes = []
244 j_changes.append(self.formatItemJSON(e))
245 if (len(j_changes) > 1 and
246 (j_changes[-2]['remaining_time'] is not None) and
247 (j_changes[-1]['remaining_time'] is not None)):
248 j_changes[-1]['remaining_time'] = max(
249 j_changes[-2]['remaining_time'],
250 j_changes[-1]['remaining_time'])
251 if j_changes:
James E. Blair8dbd56a2012-12-22 10:55:10 -0800252 j_queue['heads'].append(j_changes)
253 return j_pipeline
254
James E. Blairfee8d652013-06-07 08:57:52 -0700255 def formatStatus(self, item, indent=0, html=False):
256 changeish = item.change
James E. Blaire0487072012-08-29 17:38:31 -0700257 indent_str = ' ' * indent
258 ret = ''
259 if html and hasattr(changeish, 'url') and changeish.url is not None:
260 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
261 indent_str,
262 changeish.project.name,
263 changeish.url,
264 changeish._id())
265 else:
James E. Blair972e3c72013-08-29 12:04:55 -0700266 ret += '%sProject %s change %s based on %s\n' % (
267 indent_str,
268 changeish.project.name,
269 changeish._id(),
270 item.item_ahead)
James E. Blaire0487072012-08-29 17:38:31 -0700271 for job in self.getJobs(changeish):
James E. Blairfee8d652013-06-07 08:57:52 -0700272 build = item.current_build_set.getBuild(job.name)
James E. Blaire0487072012-08-29 17:38:31 -0700273 if build:
274 result = build.result
275 else:
276 result = None
277 job_name = job.name
278 if not job.voting:
279 voting = ' (non-voting)'
280 else:
281 voting = ''
282 if html:
283 if build:
284 url = build.url
285 else:
286 url = None
287 if url is not None:
288 job_name = '<a href="%s">%s</a>' % (url, job_name)
289 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
290 ret += '\n'
James E. Blaire0487072012-08-29 17:38:31 -0700291 return ret
292
James E. Blairfee8d652013-06-07 08:57:52 -0700293 def formatItemJSON(self, item):
294 changeish = item.change
James E. Blair8dbd56a2012-12-22 10:55:10 -0800295 ret = {}
296 if hasattr(changeish, 'url') and changeish.url is not None:
297 ret['url'] = changeish.url
James E. Blairc44b1382012-12-23 09:39:55 -0800298 else:
299 ret['url'] = None
James E. Blair8dbd56a2012-12-22 10:55:10 -0800300 ret['id'] = changeish._id()
James E. Blair2feda2d2013-09-13 11:48:19 -0700301 if item.item_ahead:
302 ret['item_ahead'] = item.item_ahead.change._id()
303 else:
304 ret['item_ahead'] = None
305 ret['items_behind'] = [i.change._id() for i in item.items_behind]
306 ret['failing_reasons'] = item.current_build_set.failing_reasons
James E. Blair062c4fb2013-09-26 07:46:00 -0700307 ret['zuul_ref'] = item.current_build_set.ref
James E. Blair8dbd56a2012-12-22 10:55:10 -0800308 ret['project'] = changeish.project.name
James E. Blairfee8d652013-06-07 08:57:52 -0700309 ret['enqueue_time'] = int(item.enqueue_time * 1000)
James E. Blair8dbd56a2012-12-22 10:55:10 -0800310 ret['jobs'] = []
James E. Blairbea9ef12013-07-15 11:52:23 -0700311 max_remaining = 0
James E. Blair8dbd56a2012-12-22 10:55:10 -0800312 for job in self.getJobs(changeish):
James E. Blairbea9ef12013-07-15 11:52:23 -0700313 now = time.time()
James E. Blairfee8d652013-06-07 08:57:52 -0700314 build = item.current_build_set.getBuild(job.name)
James E. Blairbea9ef12013-07-15 11:52:23 -0700315 elapsed = None
316 remaining = None
317 result = None
318 url = None
James E. Blair8dbd56a2012-12-22 10:55:10 -0800319 if build:
320 result = build.result
321 url = build.url
James E. Blairbea9ef12013-07-15 11:52:23 -0700322 if build.start_time:
323 if build.end_time:
324 elapsed = int((build.end_time -
325 build.start_time) * 1000)
326 remaining = 0
327 else:
328 elapsed = int((now - build.start_time) * 1000)
329 if build.estimated_time:
330 remaining = max(
331 int(build.estimated_time * 1000) - elapsed,
332 0)
333 if remaining and remaining > max_remaining:
334 max_remaining = remaining
James E. Blair8dbd56a2012-12-22 10:55:10 -0800335 ret['jobs'].append(
336 dict(
337 name=job.name,
James E. Blairbea9ef12013-07-15 11:52:23 -0700338 elapsed_time=elapsed,
339 remaining_time=remaining,
James E. Blair8dbd56a2012-12-22 10:55:10 -0800340 url=url,
341 result=result,
342 voting=job.voting))
James E. Blairbea9ef12013-07-15 11:52:23 -0700343 if self.haveAllJobsStarted(item):
James E. Blair972e3c72013-08-29 12:04:55 -0700344 ret['remaining_time'] = max_remaining
James E. Blairbea9ef12013-07-15 11:52:23 -0700345 else:
346 ret['remaining_time'] = None
James E. Blair8dbd56a2012-12-22 10:55:10 -0800347 return ret
348
James E. Blair4aea70c2012-07-26 14:23:24 -0700349
Joshua Hesketh1879cf72013-08-19 14:13:15 +1000350class ActionReporter(object):
351 """An ActionReporter has a reporter and its configured paramaters"""
352
353 def __repr__(self):
354 return '<ActionReporter %s, %s>' % (self.reporter, self.params)
355
356 def __init__(self, reporter, params):
357 self.reporter = reporter
358 self.params = params
359
360 def report(self, change, message):
361 """Sends the built message off to the configured reporter.
362 Takes the change and message and adds the configured parameters.
363 """
364 return self.reporter.report(change, message, self.params)
365
366 def getSubmitAllowNeeds(self):
367 """Gets the submit allow needs from the reporter based off the
368 parameters."""
369 return self.reporter.getSubmitAllowNeeds(self.params)
370
371
James E. Blairee743612012-05-29 14:49:32 -0700372class ChangeQueue(object):
James E. Blair4aea70c2012-07-26 14:23:24 -0700373 """DependentPipelines have multiple parallel queues shared by
374 different projects; this is one of them. For instance, there may
375 a queue shared by interrelated projects foo and bar, and a second
376 queue for independent project baz. Pipelines have one or more
377 PipelineQueues."""
James E. Blaire0487072012-08-29 17:38:31 -0700378 def __init__(self, pipeline, dependent=True):
James E. Blair4aea70c2012-07-26 14:23:24 -0700379 self.pipeline = pipeline
James E. Blairee743612012-05-29 14:49:32 -0700380 self.name = ''
James E. Blairee743612012-05-29 14:49:32 -0700381 self.projects = []
382 self._jobs = set()
383 self.queue = []
James E. Blaire0487072012-08-29 17:38:31 -0700384 self.dependent = dependent
James E. Blairee743612012-05-29 14:49:32 -0700385
James E. Blair9f9667e2012-06-12 17:51:08 -0700386 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700387 return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
James E. Blairee743612012-05-29 14:49:32 -0700388
389 def getJobs(self):
390 return self._jobs
391
392 def addProject(self, project):
393 if project not in self.projects:
394 self.projects.append(project)
395 names = [x.name for x in self.projects]
396 names.sort()
397 self.name = ', '.join(names)
James E. Blair4aea70c2012-07-26 14:23:24 -0700398 self._jobs |= set(self.pipeline.getJobTree(project).getJobs())
James E. Blairee743612012-05-29 14:49:32 -0700399
400 def enqueueChange(self, change):
James E. Blairfee8d652013-06-07 08:57:52 -0700401 item = QueueItem(self.pipeline, change)
James E. Blaircdccd972013-07-01 12:10:22 -0700402 self.enqueueItem(item)
403 item.enqueue_time = time.time()
404 return item
405
406 def enqueueItem(self, item):
James E. Blair75241582012-08-31 12:16:55 -0700407 if self.dependent and self.queue:
James E. Blairfee8d652013-06-07 08:57:52 -0700408 item.item_ahead = self.queue[-1]
James E. Blair972e3c72013-08-29 12:04:55 -0700409 item.item_ahead.items_behind.append(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700410 self.queue.append(item)
James E. Blairee743612012-05-29 14:49:32 -0700411
James E. Blairfee8d652013-06-07 08:57:52 -0700412 def dequeueItem(self, item):
413 if item in self.queue:
414 self.queue.remove(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700415 if item.item_ahead:
James E. Blair972e3c72013-08-29 12:04:55 -0700416 item.item_ahead.items_behind.remove(item)
417 for item_behind in item.items_behind:
418 if item.item_ahead:
419 item.item_ahead.items_behind.append(item_behind)
420 item_behind.item_ahead = item.item_ahead
James E. Blairfee8d652013-06-07 08:57:52 -0700421 item.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700422 item.items_behind = []
James E. Blairfee8d652013-06-07 08:57:52 -0700423 item.dequeue_time = time.time()
James E. Blaire0487072012-08-29 17:38:31 -0700424
James E. Blair972e3c72013-08-29 12:04:55 -0700425 def moveItem(self, item, item_ahead):
426 if not self.dependent:
427 return False
428 if item.item_ahead == item_ahead:
429 return False
430 # Remove from current location
431 if item.item_ahead:
432 item.item_ahead.items_behind.remove(item)
433 for item_behind in item.items_behind:
434 if item.item_ahead:
435 item.item_ahead.items_behind.append(item_behind)
436 item_behind.item_ahead = item.item_ahead
437 # Add to new location
438 item.item_ahead = item_ahead
James E. Blair00451262013-09-20 11:40:17 -0700439 item.items_behind = []
James E. Blair972e3c72013-08-29 12:04:55 -0700440 if item.item_ahead:
441 item.item_ahead.items_behind.append(item)
442 return True
James E. Blairee743612012-05-29 14:49:32 -0700443
444 def mergeChangeQueue(self, other):
445 for project in other.projects:
446 self.addProject(project)
447
James E. Blair1e8dd892012-05-30 09:15:05 -0700448
James E. Blair4aea70c2012-07-26 14:23:24 -0700449class Project(object):
450 def __init__(self, name):
451 self.name = name
James E. Blair19deff22013-08-25 13:17:35 -0700452 self.merge_mode = MERGER_MERGE_RESOLVE
James E. Blair4aea70c2012-07-26 14:23:24 -0700453
454 def __str__(self):
455 return self.name
456
457 def __repr__(self):
458 return '<Project %s>' % (self.name)
459
460
James E. Blairee743612012-05-29 14:49:32 -0700461class Job(object):
462 def __init__(self, name):
James E. Blair222d4982012-07-16 09:31:19 -0700463 # If you add attributes here, be sure to add them to the copy method.
James E. Blairee743612012-05-29 14:49:32 -0700464 self.name = name
465 self.failure_message = None
466 self.success_message = None
James E. Blair6aea36d2012-12-17 13:03:24 -0800467 self.failure_pattern = None
468 self.success_pattern = None
James E. Blaire5a847f2012-07-10 15:29:14 -0700469 self.parameter_function = None
James E. Blair222d4982012-07-16 09:31:19 -0700470 self.hold_following_changes = False
James E. Blair4ec821f2012-08-23 15:28:28 -0700471 self.voting = True
James E. Blaire421a232012-07-25 16:59:21 -0700472 self.branches = []
473 self._branches = []
James E. Blair70c71582013-03-06 08:50:50 -0800474 self.files = []
475 self._files = []
James E. Blairee743612012-05-29 14:49:32 -0700476
477 def __str__(self):
478 return self.name
479
480 def __repr__(self):
481 return '<Job %s>' % (self.name)
482
James E. Blairb0954652012-06-01 11:32:01 -0700483 def copy(self, other):
James E. Blairc28d1b02013-07-19 11:37:06 -0700484 if other.failure_message:
485 self.failure_message = other.failure_message
486 if other.success_message:
487 self.success_message = other.success_message
488 if other.failure_pattern:
489 self.failure_pattern = other.failure_pattern
490 if other.success_pattern:
491 self.success_pattern = other.success_pattern
492 if other.parameter_function:
493 self.parameter_function = other.parameter_function
494 if other.branches:
495 self.branches = other.branches[:]
496 self._branches = other._branches[:]
497 if other.files:
498 self.files = other.files[:]
499 self._files = other._files[:]
James E. Blair222d4982012-07-16 09:31:19 -0700500 self.hold_following_changes = other.hold_following_changes
James E. Blair4ec821f2012-08-23 15:28:28 -0700501 self.voting = other.voting
James E. Blairb0954652012-06-01 11:32:01 -0700502
James E. Blaire421a232012-07-25 16:59:21 -0700503 def changeMatches(self, change):
James E. Blair70c71582013-03-06 08:50:50 -0800504 matches_branch = False
James E. Blaire421a232012-07-25 16:59:21 -0700505 for branch in self.branches:
James E. Blair45865f32012-10-05 09:39:46 -0700506 if hasattr(change, 'branch') and branch.match(change.branch):
James E. Blair70c71582013-03-06 08:50:50 -0800507 matches_branch = True
James E. Blair45865f32012-10-05 09:39:46 -0700508 if hasattr(change, 'ref') and branch.match(change.ref):
James E. Blair70c71582013-03-06 08:50:50 -0800509 matches_branch = True
510 if self.branches and not matches_branch:
511 return False
512
513 matches_file = False
514 for f in self.files:
515 if hasattr(change, 'files'):
516 for cf in change.files:
517 if f.match(cf):
518 matches_file = True
519 if self.files and not matches_file:
520 return False
521
522 return True
James E. Blaire5a847f2012-07-10 15:29:14 -0700523
James E. Blair1e8dd892012-05-30 09:15:05 -0700524
James E. Blairee743612012-05-29 14:49:32 -0700525class JobTree(object):
526 """ A JobTree represents an instance of one Job, and holds JobTrees
527 whose jobs should be run if that Job succeeds. A root node of a
528 JobTree will have no associated Job. """
529
530 def __init__(self, job):
531 self.job = job
532 self.job_trees = []
533
534 def addJob(self, job):
535 if job not in [x.job for x in self.job_trees]:
536 t = JobTree(job)
537 self.job_trees.append(t)
538 return t
539
540 def getJobs(self):
541 jobs = []
542 for x in self.job_trees:
543 jobs.append(x.job)
544 jobs.extend(x.getJobs())
545 return jobs
546
547 def getJobTreeForJob(self, job):
548 if self.job == job:
549 return self
550 for tree in self.job_trees:
551 ret = tree.getJobTreeForJob(job)
552 if ret:
553 return ret
554 return None
555
James E. Blair1e8dd892012-05-30 09:15:05 -0700556
James E. Blair4aea70c2012-07-26 14:23:24 -0700557class Build(object):
558 def __init__(self, job, uuid):
559 self.job = job
560 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -0700561 self.url = None
562 self.number = None
563 self.result = None
564 self.build_set = None
565 self.launch_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -0800566 self.start_time = None
567 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -0700568 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -0700569 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -0700570 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -0700571 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -0700572 self.parameters = {}
James E. Blairee743612012-05-29 14:49:32 -0700573
574 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700575 return '<Build %s of %s>' % (self.uuid, self.job.name)
James E. Blairee743612012-05-29 14:49:32 -0700576
James E. Blair1e8dd892012-05-30 09:15:05 -0700577
James E. Blair7e530ad2012-07-03 16:12:28 -0700578class BuildSet(object):
James E. Blairfee8d652013-06-07 08:57:52 -0700579 def __init__(self, item):
580 self.item = item
James E. Blair11700c32012-07-05 17:50:05 -0700581 self.other_changes = []
James E. Blair7e530ad2012-07-03 16:12:28 -0700582 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -0700583 self.result = None
584 self.next_build_set = None
585 self.previous_build_set = None
James E. Blair4886cc12012-07-18 15:39:41 -0700586 self.ref = None
James E. Blair81515ad2012-10-01 18:29:08 -0700587 self.commit = None
James E. Blair973721f2012-08-15 10:19:43 -0700588 self.unable_to_merge = False
James E. Blair6736beb2013-07-11 15:18:15 -0700589 self.unable_to_merge_message = None
James E. Blair972e3c72013-08-29 12:04:55 -0700590 self.failing_reasons = []
James E. Blair7e530ad2012-07-03 16:12:28 -0700591
James E. Blair4886cc12012-07-18 15:39:41 -0700592 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -0700593 # The change isn't enqueued until after it's created
594 # so we don't know what the other changes ahead will be
595 # until jobs start.
596 if not self.other_changes:
James E. Blairfee8d652013-06-07 08:57:52 -0700597 next_item = self.item.item_ahead
598 while next_item:
599 self.other_changes.append(next_item.change)
600 next_item = next_item.item_ahead
James E. Blair4886cc12012-07-18 15:39:41 -0700601 if not self.ref:
602 self.ref = 'Z' + uuid4().hex
603
James E. Blair4886cc12012-07-18 15:39:41 -0700604 def addBuild(self, build):
605 self.builds[build.job.name] = build
606 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -0700607
James E. Blair4a28a882013-08-23 15:17:33 -0700608 def removeBuild(self, build):
609 del self.builds[build.job.name]
610
James E. Blair7e530ad2012-07-03 16:12:28 -0700611 def getBuild(self, job_name):
612 return self.builds.get(job_name)
613
James E. Blair11700c32012-07-05 17:50:05 -0700614 def getBuilds(self):
615 keys = self.builds.keys()
616 keys.sort()
617 return [self.builds.get(x) for x in keys]
618
James E. Blair7e530ad2012-07-03 16:12:28 -0700619
James E. Blairfee8d652013-06-07 08:57:52 -0700620class QueueItem(object):
621 """A changish inside of a Pipeline queue"""
James E. Blair32663402012-06-01 10:04:18 -0700622
James E. Blairfee8d652013-06-07 08:57:52 -0700623 def __init__(self, pipeline, change):
624 self.pipeline = pipeline
625 self.change = change # a changeish
James E. Blair7e530ad2012-07-03 16:12:28 -0700626 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -0700627 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -0700628 self.current_build_set = BuildSet(self)
629 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -0700630 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700631 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -0800632 self.enqueue_time = None
633 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -0700634 self.reported = False
James E. Blaire5a847f2012-07-10 15:29:14 -0700635
James E. Blair972e3c72013-08-29 12:04:55 -0700636 def __repr__(self):
637 if self.pipeline:
638 pipeline = self.pipeline.name
639 else:
640 pipeline = None
641 return '<QueueItem 0x%x for %s in %s>' % (
642 id(self), self.change, pipeline)
643
James E. Blairee743612012-05-29 14:49:32 -0700644 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -0700645 old = self.current_build_set
646 self.current_build_set.result = 'CANCELED'
647 self.current_build_set = BuildSet(self)
648 old.next_build_set = self.current_build_set
649 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -0700650 self.build_sets.append(self.current_build_set)
James E. Blairee743612012-05-29 14:49:32 -0700651
652 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -0700653 self.current_build_set.addBuild(build)
James E. Blair66eeebf2013-07-27 17:44:32 -0700654 build.pipeline = self.pipeline
James E. Blairee743612012-05-29 14:49:32 -0700655
James E. Blair4a28a882013-08-23 15:17:33 -0700656 def removeBuild(self, build):
657 self.current_build_set.removeBuild(build)
658
James E. Blairfee8d652013-06-07 08:57:52 -0700659 def setReportedResult(self, result):
660 self.current_build_set.result = result
661
662
663class Changeish(object):
664 """Something like a change; either a change or a ref"""
665 is_reportable = False
666
667 def __init__(self, project):
668 self.project = project
669
670 def equals(self, other):
671 raise NotImplementedError()
672
673 def isUpdateOf(self, other):
674 raise NotImplementedError()
675
676 def filterJobs(self, jobs):
677 return filter(lambda job: job.changeMatches(self), jobs)
678
679 def getRelatedChanges(self):
680 return set()
681
James E. Blair1e8dd892012-05-30 09:15:05 -0700682
James E. Blair4aea70c2012-07-26 14:23:24 -0700683class Change(Changeish):
684 is_reportable = True
685
686 def __init__(self, project):
687 super(Change, self).__init__(project)
688 self.branch = None
689 self.number = None
690 self.url = None
691 self.patchset = None
692 self.refspec = None
693
James E. Blair70c71582013-03-06 08:50:50 -0800694 self.files = []
James E. Blair4aea70c2012-07-26 14:23:24 -0700695 self.needs_change = None
696 self.needed_by_changes = []
697 self.is_current_patchset = True
698 self.can_merge = False
699 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -0700700 self.failed_to_merge = False
James E. Blair4aea70c2012-07-26 14:23:24 -0700701
702 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -0700703 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -0700704
705 def __repr__(self):
706 return '<Change 0x%x %s>' % (id(self), self._id())
707
708 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +0800709 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -0700710 return True
711 return False
712
James E. Blair2fa50962013-01-30 21:50:41 -0800713 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -0800714 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -0700715 (hasattr(other, 'patchset') and
716 self.patchset is not None and
717 other.patchset is not None and
718 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -0800719 return True
720 return False
721
James E. Blairfee8d652013-06-07 08:57:52 -0700722 def getRelatedChanges(self):
723 related = set()
724 if self.needs_change:
725 related.add(self.needs_change)
726 for c in self.needed_by_changes:
727 related.add(c)
728 related.update(c.getRelatedChanges())
729 return related
James E. Blair4aea70c2012-07-26 14:23:24 -0700730
731
732class Ref(Changeish):
733 is_reportable = False
734
735 def __init__(self, project):
James E. Blairbe765db2012-08-07 08:36:20 -0700736 super(Ref, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700737 self.ref = None
738 self.oldrev = None
739 self.newrev = None
740
James E. Blairbe765db2012-08-07 08:36:20 -0700741 def _id(self):
742 return self.newrev
743
Antoine Musso68bdcd72013-01-17 12:31:28 +0100744 def __repr__(self):
745 rep = None
746 if self.newrev == '0000000000000000000000000000000000000000':
747 rep = '<Ref 0x%x deletes %s from %s' % (
748 id(self), self.ref, self.oldrev)
749 elif self.oldrev == '0000000000000000000000000000000000000000':
750 rep = '<Ref 0x%x creates %s on %s>' % (
751 id(self), self.ref, self.newrev)
752 else:
753 # Catch all
754 rep = '<Ref 0x%x %s updated %s..%s>' % (
755 id(self), self.ref, self.oldrev, self.newrev)
756
757 return rep
758
James E. Blair4aea70c2012-07-26 14:23:24 -0700759 def equals(self, other):
James E. Blair9358c612012-09-28 08:29:39 -0700760 if (self.project == other.project
761 and self.ref == other.ref
762 and self.newrev == other.newrev):
James E. Blair4aea70c2012-07-26 14:23:24 -0700763 return True
764 return False
765
James E. Blair2fa50962013-01-30 21:50:41 -0800766 def isUpdateOf(self, other):
767 return False
768
James E. Blair4aea70c2012-07-26 14:23:24 -0700769
James E. Blair63bb0ef2013-07-29 17:14:51 -0700770class NullChange(Changeish):
771 is_reportable = False
772
773 def __init__(self, project):
774 super(NullChange, self).__init__(project)
775
776 def _id(self):
777 return 'None'
778
779 def equals(self, other):
780 return False
781
782 def isUpdateOf(self, other):
783 return False
784
785
James E. Blairee743612012-05-29 14:49:32 -0700786class TriggerEvent(object):
787 def __init__(self):
788 self.data = None
James E. Blair32663402012-06-01 10:04:18 -0700789 # common
James E. Blairee743612012-05-29 14:49:32 -0700790 self.type = None
791 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -0700792 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +0100793 # Representation of the user account that performed the event.
794 self.account = None
James E. Blair32663402012-06-01 10:04:18 -0700795 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -0700796 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -0700797 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -0700798 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -0700799 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -0700800 self.approvals = []
801 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -0700802 self.comment = None
James E. Blair32663402012-06-01 10:04:18 -0700803 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -0700804 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -0700805 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -0700806 self.newrev = None
James E. Blair63bb0ef2013-07-29 17:14:51 -0700807 # timer
808 self.timespec = None
James E. Blairee743612012-05-29 14:49:32 -0700809
James E. Blair9f9667e2012-06-12 17:51:08 -0700810 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -0700811 ret = '<TriggerEvent %s %s' % (self.type, self.project_name)
James E. Blair1e8dd892012-05-30 09:15:05 -0700812
James E. Blairee743612012-05-29 14:49:32 -0700813 if self.branch:
814 ret += " %s" % self.branch
815 if self.change_number:
816 ret += " %s,%s" % (self.change_number, self.patch_number)
817 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -0700818 ret += ' ' + ', '.join(
819 ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
James E. Blairee743612012-05-29 14:49:32 -0700820 ret += '>'
821
822 return ret
823
James E. Blair4aea70c2012-07-26 14:23:24 -0700824 def getChange(self, project, trigger):
James E. Blaire421a232012-07-25 16:59:21 -0700825 if self.change_number:
James E. Blair4aea70c2012-07-26 14:23:24 -0700826 change = trigger.getChange(self.change_number, self.patch_number)
James E. Blair63bb0ef2013-07-29 17:14:51 -0700827 elif self.ref:
James E. Blair4aea70c2012-07-26 14:23:24 -0700828 change = Ref(project)
James E. Blaire421a232012-07-25 16:59:21 -0700829 change.ref = self.ref
830 change.oldrev = self.oldrev
831 change.newrev = self.newrev
James E. Blairc44b1382012-12-23 09:39:55 -0800832 change.url = trigger.getGitwebUrl(project, sha=self.newrev)
James E. Blair63bb0ef2013-07-29 17:14:51 -0700833 else:
834 change = NullChange(project)
James E. Blaire421a232012-07-25 16:59:21 -0700835
836 return change
837
James E. Blair1e8dd892012-05-30 09:15:05 -0700838
James E. Blairee743612012-05-29 14:49:32 -0700839class EventFilter(object):
James E. Blaire5a847f2012-07-10 15:29:14 -0700840 def __init__(self, types=[], branches=[], refs=[], approvals={},
James E. Blair63bb0ef2013-07-29 17:14:51 -0700841 comment_filters=[], email_filters=[], timespecs=[]):
James E. Blairee743612012-05-29 14:49:32 -0700842 self._types = types
843 self._branches = branches
844 self._refs = refs
James E. Blaircf429f32012-12-20 14:28:24 -0800845 self._comment_filters = comment_filters
846 self._email_filters = email_filters
James E. Blairee743612012-05-29 14:49:32 -0700847 self.types = [re.compile(x) for x in types]
848 self.branches = [re.compile(x) for x in branches]
849 self.refs = [re.compile(x) for x in refs]
Clark Boylanb9bcb402012-06-29 17:44:05 -0700850 self.comment_filters = [re.compile(x) for x in comment_filters]
Antoine Mussob4e809e2012-12-06 16:58:06 +0100851 self.email_filters = [re.compile(x) for x in email_filters]
James E. Blairee743612012-05-29 14:49:32 -0700852 self.approvals = approvals
James E. Blair63bb0ef2013-07-29 17:14:51 -0700853 self.timespecs = timespecs
James E. Blairee743612012-05-29 14:49:32 -0700854
James E. Blair9f9667e2012-06-12 17:51:08 -0700855 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -0700856 ret = '<EventFilter'
James E. Blair1e8dd892012-05-30 09:15:05 -0700857
James E. Blairee743612012-05-29 14:49:32 -0700858 if self._types:
859 ret += ' types: %s' % ', '.join(self._types)
860 if self._branches:
861 ret += ' branches: %s' % ', '.join(self._branches)
862 if self._refs:
863 ret += ' refs: %s' % ', '.join(self._refs)
864 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -0700865 ret += ' approvals: %s' % ', '.join(
866 ['%s:%s' % a for a in self.approvals.items()])
James E. Blaircf429f32012-12-20 14:28:24 -0800867 if self._comment_filters:
868 ret += ' comment_filters: %s' % ', '.join(self._comment_filters)
869 if self._email_filters:
870 ret += ' email_filters: %s' % ', '.join(self._email_filters)
James E. Blair63bb0ef2013-07-29 17:14:51 -0700871 if self.timespecs:
872 ret += ' timespecs: %s' % ', '.join(self.timespecs)
James E. Blairee743612012-05-29 14:49:32 -0700873 ret += '>'
874
875 return ret
876
877 def matches(self, event):
878 def normalizeCategory(name):
879 name = name.lower()
880 return re.sub(' ', '-', name)
881
882 # event types are ORed
883 matches_type = False
884 for etype in self.types:
885 if etype.match(event.type):
886 matches_type = True
887 if self.types and not matches_type:
888 return False
889
890 # branches are ORed
891 matches_branch = False
892 for branch in self.branches:
893 if branch.match(event.branch):
894 matches_branch = True
895 if self.branches and not matches_branch:
896 return False
897
898 # refs are ORed
899 matches_ref = False
900 for ref in self.refs:
901 if ref.match(event.ref):
902 matches_ref = True
903 if self.refs and not matches_ref:
904 return False
905
Clark Boylanb9bcb402012-06-29 17:44:05 -0700906 # comment_filters are ORed
907 matches_comment_filter = False
908 for comment_filter in self.comment_filters:
909 if (event.comment is not None and
James E. Blaircf429f32012-12-20 14:28:24 -0800910 comment_filter.search(event.comment)):
Clark Boylanb9bcb402012-06-29 17:44:05 -0700911 matches_comment_filter = True
912 if self.comment_filters and not matches_comment_filter:
913 return False
914
Antoine Mussob4e809e2012-12-06 16:58:06 +0100915 # We better have an account provided by Gerrit to do
916 # email filtering.
917 if event.account is not None:
James E. Blaircf429f32012-12-20 14:28:24 -0800918 account_email = event.account.get('email')
Antoine Mussob4e809e2012-12-06 16:58:06 +0100919 # email_filters are ORed
920 matches_email_filter = False
921 for email_filter in self.email_filters:
Antoine Mussob4e809e2012-12-06 16:58:06 +0100922 if (account_email is not None and
James E. Blaircf429f32012-12-20 14:28:24 -0800923 email_filter.search(account_email)):
Antoine Mussob4e809e2012-12-06 16:58:06 +0100924 matches_email_filter = True
925 if self.email_filters and not matches_email_filter:
926 return False
927
James E. Blairee743612012-05-29 14:49:32 -0700928 # approvals are ANDed
929 for category, value in self.approvals.items():
930 matches_approval = False
931 for eapproval in event.approvals:
932 if (normalizeCategory(eapproval['description']) == category and
933 int(eapproval['value']) == int(value)):
934 matches_approval = True
James E. Blair1e8dd892012-05-30 09:15:05 -0700935 if not matches_approval:
936 return False
James E. Blair63bb0ef2013-07-29 17:14:51 -0700937
938 # timespecs are ORed
939 matches_timespec = False
940 for timespec in self.timespecs:
941 if (event.timespec == timespec):
942 matches_timespec = True
943 if self.timespecs and not matches_timespec:
944 return False
945
James E. Blairee743612012-05-29 14:49:32 -0700946 return True
James E. Blaireff88162013-07-01 12:44:14 -0400947
948
949class Layout(object):
950 def __init__(self):
951 self.projects = {}
James E. Blair5a9918a2013-08-27 10:06:27 -0700952 self.pipelines = OrderedDict()
James E. Blaireff88162013-07-01 12:44:14 -0400953 self.jobs = {}
James E. Blairc28d1b02013-07-19 11:37:06 -0700954 self.metajobs = []
James E. Blaireff88162013-07-01 12:44:14 -0400955
956 def getJob(self, name):
957 if name in self.jobs:
958 return self.jobs[name]
959 job = Job(name)
960 if name.startswith('^'):
961 # This is a meta-job
962 regex = re.compile(name)
James E. Blairc28d1b02013-07-19 11:37:06 -0700963 self.metajobs.append((regex, job))
James E. Blaireff88162013-07-01 12:44:14 -0400964 else:
965 # Apply attributes from matching meta-jobs
James E. Blairc28d1b02013-07-19 11:37:06 -0700966 for regex, metajob in self.metajobs:
James E. Blaireff88162013-07-01 12:44:14 -0400967 if regex.match(name):
968 job.copy(metajob)
969 self.jobs[name] = job
970 return job