blob: b94295d824b333c64a36f0a31f3046de1c6fefe5 [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
James E. Blair1b265312014-06-24 09:35:21 -070015import copy
James E. Blairee743612012-05-29 14:49:32 -070016import re
James E. Blairff986a12012-05-30 14:56:51 -070017import time
James E. Blair4886cc12012-07-18 15:39:41 -070018from uuid import uuid4
James E. Blair5a9918a2013-08-27 10:06:27 -070019import extras
20
21OrderedDict = extras.try_imports(['collections.OrderedDict',
22 'ordereddict.OrderedDict'])
James E. Blair4886cc12012-07-18 15:39:41 -070023
24
James E. Blair19deff22013-08-25 13:17:35 -070025MERGER_MERGE = 1 # "git merge"
26MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
27MERGER_CHERRY_PICK = 3 # "git cherry-pick"
28
29MERGER_MAP = {
30 'merge': MERGER_MERGE,
31 'merge-resolve': MERGER_MERGE_RESOLVE,
32 'cherry-pick': MERGER_CHERRY_PICK,
33}
James E. Blairee743612012-05-29 14:49:32 -070034
James E. Blair64ed6f22013-07-10 14:07:23 -070035PRECEDENCE_NORMAL = 0
36PRECEDENCE_LOW = 1
37PRECEDENCE_HIGH = 2
38
39PRECEDENCE_MAP = {
40 None: PRECEDENCE_NORMAL,
41 'low': PRECEDENCE_LOW,
42 'normal': PRECEDENCE_NORMAL,
43 'high': PRECEDENCE_HIGH,
44}
45
James E. Blair1e8dd892012-05-30 09:15:05 -070046
James E. Blairc053d022014-01-22 14:57:33 -080047def time_to_seconds(s):
48 if s.endswith('s'):
49 return int(s[:-1])
50 if s.endswith('m'):
51 return int(s[:-1]) * 60
52 if s.endswith('h'):
53 return int(s[:-1]) * 60 * 60
54 if s.endswith('d'):
55 return int(s[:-1]) * 24 * 60 * 60
56 if s.endswith('w'):
57 return int(s[:-1]) * 7 * 24 * 60 * 60
58 raise Exception("Unable to parse time value: %s" % s)
59
60
James E. Blair11041d22014-05-02 14:49:53 -070061def normalizeCategory(name):
62 name = name.lower()
63 return re.sub(' ', '-', name)
64
65
James E. Blair4aea70c2012-07-26 14:23:24 -070066class Pipeline(object):
67 """A top-level pipeline such as check, gate, post, etc."""
68 def __init__(self, name):
69 self.name = name
James E. Blair8dbd56a2012-12-22 10:55:10 -080070 self.description = None
James E. Blair56370192013-01-14 15:47:28 -080071 self.failure_message = None
Joshua Heskethb7179772014-01-30 23:30:46 +110072 self.merge_failure_message = None
James E. Blair56370192013-01-14 15:47:28 -080073 self.success_message = None
Joshua Hesketh3979e3e2014-03-04 11:21:10 +110074 self.footer_message = None
James E. Blair2fa50962013-01-30 21:50:41 -080075 self.dequeue_on_new_patchset = True
James E. Blair4aea70c2012-07-26 14:23:24 -070076 self.job_trees = {} # project -> JobTree
77 self.manager = None
James E. Blaire0487072012-08-29 17:38:31 -070078 self.queues = []
James E. Blair64ed6f22013-07-10 14:07:23 -070079 self.precedence = PRECEDENCE_NORMAL
James E. Blairc0dedf82014-08-06 09:37:52 -070080 self.source = None
Joshua Hesketh1879cf72013-08-19 14:13:15 +100081 self.start_actions = None
82 self.success_actions = None
83 self.failure_actions = None
Clark Boylan7603a372014-01-21 11:43:20 -080084 self.window = None
85 self.window_floor = None
86 self.window_increase_type = None
87 self.window_increase_factor = None
88 self.window_decrease_type = None
89 self.window_decrease_factor = None
James E. Blair4aea70c2012-07-26 14:23:24 -070090
James E. Blaird09c17a2012-08-07 09:23:14 -070091 def __repr__(self):
92 return '<Pipeline %s>' % self.name
93
James E. Blair4aea70c2012-07-26 14:23:24 -070094 def setManager(self, manager):
95 self.manager = manager
96
97 def addProject(self, project):
98 job_tree = JobTree(None) # Null job == job tree root
99 self.job_trees[project] = job_tree
100 return job_tree
101
102 def getProjects(self):
James E. Blairc3d428e2013-12-03 15:06:48 -0800103 return sorted(self.job_trees.keys(), lambda a, b: cmp(a.name, b.name))
James E. Blair4aea70c2012-07-26 14:23:24 -0700104
James E. Blaire0487072012-08-29 17:38:31 -0700105 def addQueue(self, queue):
106 self.queues.append(queue)
107
108 def getQueue(self, project):
109 for queue in self.queues:
110 if project in queue.projects:
111 return queue
112 return None
113
James E. Blairbfb8e042014-12-30 17:01:44 -0800114 def removeQueue(self, queue):
115 self.queues.remove(queue)
116
James E. Blair4aea70c2012-07-26 14:23:24 -0700117 def getJobTree(self, project):
118 tree = self.job_trees.get(project)
119 return tree
120
James E. Blair107c3852015-02-07 08:23:10 -0800121 def getJobs(self, item):
122 if not item.live:
123 return []
124 tree = self.getJobTree(item.change.project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700125 if not tree:
126 return []
James E. Blair107c3852015-02-07 08:23:10 -0800127 return item.change.filterJobs(tree.getJobs())
James E. Blair4aea70c2012-07-26 14:23:24 -0700128
James E. Blairfee8d652013-06-07 08:57:52 -0700129 def _findJobsToRun(self, job_trees, item):
James E. Blair4aea70c2012-07-26 14:23:24 -0700130 torun = []
James E. Blairfee8d652013-06-07 08:57:52 -0700131 if item.item_ahead:
James E. Blair4aea70c2012-07-26 14:23:24 -0700132 # Only run jobs if any 'hold' jobs on the change ahead
133 # have completed successfully.
James E. Blairfee8d652013-06-07 08:57:52 -0700134 if self.isHoldingFollowingChanges(item.item_ahead):
James E. Blair4aea70c2012-07-26 14:23:24 -0700135 return []
136 for tree in job_trees:
137 job = tree.job
138 result = None
139 if job:
James E. Blairfee8d652013-06-07 08:57:52 -0700140 if not job.changeMatches(item.change):
James E. Blair4aea70c2012-07-26 14:23:24 -0700141 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700142 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700143 if build:
144 result = build.result
145 else:
146 # There is no build for the root of this job tree,
147 # so we should run it.
148 torun.append(job)
149 # If there is no job, this is a null job tree, and we should
150 # run all of its jobs.
151 if result == 'SUCCESS' or not job:
James E. Blairfee8d652013-06-07 08:57:52 -0700152 torun.extend(self._findJobsToRun(tree.job_trees, item))
James E. Blair4aea70c2012-07-26 14:23:24 -0700153 return torun
154
James E. Blairfee8d652013-06-07 08:57:52 -0700155 def findJobsToRun(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800156 if not item.live:
157 return []
James E. Blairfee8d652013-06-07 08:57:52 -0700158 tree = self.getJobTree(item.change.project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700159 if not tree:
160 return []
James E. Blairfee8d652013-06-07 08:57:52 -0700161 return self._findJobsToRun(tree.job_trees, item)
James E. Blair4aea70c2012-07-26 14:23:24 -0700162
James E. Blairbea9ef12013-07-15 11:52:23 -0700163 def haveAllJobsStarted(self, item):
James E. Blair107c3852015-02-07 08:23:10 -0800164 for job in self.getJobs(item):
James E. Blairbea9ef12013-07-15 11:52:23 -0700165 build = item.current_build_set.getBuild(job.name)
166 if not build or not build.start_time:
167 return False
168 return True
169
James E. Blairfee8d652013-06-07 08:57:52 -0700170 def areAllJobsComplete(self, item):
James E. Blair107c3852015-02-07 08:23:10 -0800171 for job in self.getJobs(item):
James E. Blairfee8d652013-06-07 08:57:52 -0700172 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700173 if not build or not build.result:
174 return False
175 return True
176
James E. Blairfee8d652013-06-07 08:57:52 -0700177 def didAllJobsSucceed(self, item):
James E. Blair107c3852015-02-07 08:23:10 -0800178 for job in self.getJobs(item):
James E. Blair4ec821f2012-08-23 15:28:28 -0700179 if not job.voting:
180 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700181 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700182 if not build:
183 return False
184 if build.result != 'SUCCESS':
185 return False
186 return True
187
Joshua Heskethb7179772014-01-30 23:30:46 +1100188 def didMergerSucceed(self, item):
189 if item.current_build_set.unable_to_merge:
190 return False
191 return True
192
James E. Blairfee8d652013-06-07 08:57:52 -0700193 def didAnyJobFail(self, item):
James E. Blair107c3852015-02-07 08:23:10 -0800194 for job in self.getJobs(item):
James E. Blair4ec821f2012-08-23 15:28:28 -0700195 if not job.voting:
196 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700197 build = item.current_build_set.getBuild(job.name)
James E. Blair0018a6c2013-02-27 14:11:45 -0800198 if build and build.result and (build.result != 'SUCCESS'):
James E. Blair4aea70c2012-07-26 14:23:24 -0700199 return True
200 return False
201
James E. Blairfee8d652013-06-07 08:57:52 -0700202 def isHoldingFollowingChanges(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800203 if not item.live:
204 return False
James E. Blair107c3852015-02-07 08:23:10 -0800205 for job in self.getJobs(item):
James E. Blair4aea70c2012-07-26 14:23:24 -0700206 if not job.hold_following_changes:
207 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700208 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700209 if not build:
210 return True
211 if build.result != 'SUCCESS':
212 return True
James E. Blair972e3c72013-08-29 12:04:55 -0700213
James E. Blairfee8d652013-06-07 08:57:52 -0700214 if not item.item_ahead:
James E. Blair4aea70c2012-07-26 14:23:24 -0700215 return False
James E. Blairfee8d652013-06-07 08:57:52 -0700216 return self.isHoldingFollowingChanges(item.item_ahead)
James E. Blair4aea70c2012-07-26 14:23:24 -0700217
James E. Blairfee8d652013-06-07 08:57:52 -0700218 def setResult(self, item, build):
James E. Blair4a28a882013-08-23 15:17:33 -0700219 if build.retry:
220 item.removeBuild(build)
221 elif build.result != 'SUCCESS':
James E. Blair4aea70c2012-07-26 14:23:24 -0700222 # Get a JobTree from a Job so we can find only its dependent jobs
James E. Blairfee8d652013-06-07 08:57:52 -0700223 root = self.getJobTree(item.change.project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700224 tree = root.getJobTreeForJob(build.job)
225 for job in tree.getJobs():
226 fakebuild = Build(job, None)
227 fakebuild.result = 'SKIPPED'
James E. Blairfee8d652013-06-07 08:57:52 -0700228 item.addBuild(fakebuild)
James E. Blair4aea70c2012-07-26 14:23:24 -0700229
Joshua Heskethb7179772014-01-30 23:30:46 +1100230 def setUnableToMerge(self, item):
James E. Blairfee8d652013-06-07 08:57:52 -0700231 item.current_build_set.unable_to_merge = True
232 root = self.getJobTree(item.change.project)
James E. Blair973721f2012-08-15 10:19:43 -0700233 for job in root.getJobs():
234 fakebuild = Build(job, None)
235 fakebuild.result = 'SKIPPED'
James E. Blairfee8d652013-06-07 08:57:52 -0700236 item.addBuild(fakebuild)
James E. Blair973721f2012-08-15 10:19:43 -0700237
James E. Blairfee8d652013-06-07 08:57:52 -0700238 def setDequeuedNeedingChange(self, item):
239 item.dequeued_needing_change = True
240 root = self.getJobTree(item.change.project)
James E. Blaircaec0c52012-08-22 14:52:22 -0700241 for job in root.getJobs():
242 fakebuild = Build(job, None)
243 fakebuild.result = 'SKIPPED'
James E. Blairfee8d652013-06-07 08:57:52 -0700244 item.addBuild(fakebuild)
James E. Blaircaec0c52012-08-22 14:52:22 -0700245
James E. Blaire0487072012-08-29 17:38:31 -0700246 def getChangesInQueue(self):
247 changes = []
248 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700249 changes.extend([x.change for x in shared_queue.queue])
James E. Blaire0487072012-08-29 17:38:31 -0700250 return changes
251
James E. Blairfee8d652013-06-07 08:57:52 -0700252 def getAllItems(self):
253 items = []
James E. Blaire0487072012-08-29 17:38:31 -0700254 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700255 items.extend(shared_queue.queue)
James E. Blairfee8d652013-06-07 08:57:52 -0700256 return items
James E. Blaire0487072012-08-29 17:38:31 -0700257
James E. Blair8dbd56a2012-12-22 10:55:10 -0800258 def formatStatusJSON(self):
259 j_pipeline = dict(name=self.name,
260 description=self.description)
261 j_queues = []
262 j_pipeline['change_queues'] = j_queues
263 for queue in self.queues:
264 j_queue = dict(name=queue.name)
265 j_queues.append(j_queue)
266 j_queue['heads'] = []
Clark Boylanaf2476f2014-01-23 14:47:36 -0800267 j_queue['window'] = queue.window
James E. Blair972e3c72013-08-29 12:04:55 -0700268
269 j_changes = []
270 for e in queue.queue:
271 if not e.item_ahead:
272 if j_changes:
273 j_queue['heads'].append(j_changes)
274 j_changes = []
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800275 j_changes.append(e.formatJSON())
James E. Blair972e3c72013-08-29 12:04:55 -0700276 if (len(j_changes) > 1 and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000277 (j_changes[-2]['remaining_time'] is not None) and
278 (j_changes[-1]['remaining_time'] is not None)):
James E. Blair972e3c72013-08-29 12:04:55 -0700279 j_changes[-1]['remaining_time'] = max(
280 j_changes[-2]['remaining_time'],
281 j_changes[-1]['remaining_time'])
282 if j_changes:
James E. Blair8dbd56a2012-12-22 10:55:10 -0800283 j_queue['heads'].append(j_changes)
284 return j_pipeline
285
James E. Blair4aea70c2012-07-26 14:23:24 -0700286
Joshua Hesketh1879cf72013-08-19 14:13:15 +1000287class ActionReporter(object):
Alex Gaynor813d39b2014-05-17 16:17:16 -0700288 """An ActionReporter has a reporter and its configured parameters"""
Joshua Hesketh1879cf72013-08-19 14:13:15 +1000289
290 def __repr__(self):
291 return '<ActionReporter %s, %s>' % (self.reporter, self.params)
292
293 def __init__(self, reporter, params):
294 self.reporter = reporter
295 self.params = params
296
297 def report(self, change, message):
298 """Sends the built message off to the configured reporter.
299 Takes the change and message and adds the configured parameters.
300 """
301 return self.reporter.report(change, message, self.params)
302
303 def getSubmitAllowNeeds(self):
304 """Gets the submit allow needs from the reporter based off the
305 parameters."""
306 return self.reporter.getSubmitAllowNeeds(self.params)
307
308
James E. Blairee743612012-05-29 14:49:32 -0700309class ChangeQueue(object):
James E. Blair4aea70c2012-07-26 14:23:24 -0700310 """DependentPipelines have multiple parallel queues shared by
311 different projects; this is one of them. For instance, there may
312 a queue shared by interrelated projects foo and bar, and a second
313 queue for independent project baz. Pipelines have one or more
James E. Blairbfb8e042014-12-30 17:01:44 -0800314 ChangeQueues."""
315 def __init__(self, pipeline, window=0, window_floor=1,
Clark Boylan7603a372014-01-21 11:43:20 -0800316 window_increase_type='linear', window_increase_factor=1,
317 window_decrease_type='exponential', window_decrease_factor=2):
James E. Blair4aea70c2012-07-26 14:23:24 -0700318 self.pipeline = pipeline
James E. Blairee743612012-05-29 14:49:32 -0700319 self.name = ''
James E. Blairc8a1e052014-02-25 09:29:26 -0800320 self.assigned_name = None
321 self.generated_name = None
James E. Blairee743612012-05-29 14:49:32 -0700322 self.projects = []
323 self._jobs = set()
324 self.queue = []
Clark Boylan7603a372014-01-21 11:43:20 -0800325 self.window = window
326 self.window_floor = window_floor
327 self.window_increase_type = window_increase_type
328 self.window_increase_factor = window_increase_factor
329 self.window_decrease_type = window_decrease_type
330 self.window_decrease_factor = window_decrease_factor
James E. Blairee743612012-05-29 14:49:32 -0700331
James E. Blair9f9667e2012-06-12 17:51:08 -0700332 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700333 return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
James E. Blairee743612012-05-29 14:49:32 -0700334
335 def getJobs(self):
336 return self._jobs
337
338 def addProject(self, project):
339 if project not in self.projects:
340 self.projects.append(project)
James E. Blairc8a1e052014-02-25 09:29:26 -0800341 self._jobs |= set(self.pipeline.getJobTree(project).getJobs())
342
James E. Blairee743612012-05-29 14:49:32 -0700343 names = [x.name for x in self.projects]
344 names.sort()
James E. Blairc8a1e052014-02-25 09:29:26 -0800345 self.generated_name = ', '.join(names)
346
347 for job in self._jobs:
348 if job.queue_name:
349 if (self.assigned_name and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000350 job.queue_name != self.assigned_name):
James E. Blairc8a1e052014-02-25 09:29:26 -0800351 raise Exception("More than one name assigned to "
352 "change queue: %s != %s" %
353 (self.assigned_name, job.queue_name))
354 self.assigned_name = job.queue_name
355 self.name = self.assigned_name or self.generated_name
James E. Blairee743612012-05-29 14:49:32 -0700356
357 def enqueueChange(self, change):
James E. Blairbfb8e042014-12-30 17:01:44 -0800358 item = QueueItem(self, change)
James E. Blaircdccd972013-07-01 12:10:22 -0700359 self.enqueueItem(item)
360 item.enqueue_time = time.time()
361 return item
362
363 def enqueueItem(self, item):
James E. Blair4a035d92014-01-23 13:10:48 -0800364 item.pipeline = self.pipeline
James E. Blairbfb8e042014-12-30 17:01:44 -0800365 item.queue = self
366 if self.queue:
James E. Blairfee8d652013-06-07 08:57:52 -0700367 item.item_ahead = self.queue[-1]
James E. Blair972e3c72013-08-29 12:04:55 -0700368 item.item_ahead.items_behind.append(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700369 self.queue.append(item)
James E. Blairee743612012-05-29 14:49:32 -0700370
James E. Blairfee8d652013-06-07 08:57:52 -0700371 def dequeueItem(self, item):
372 if item in self.queue:
373 self.queue.remove(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700374 if item.item_ahead:
James E. Blair972e3c72013-08-29 12:04:55 -0700375 item.item_ahead.items_behind.remove(item)
376 for item_behind in item.items_behind:
377 if item.item_ahead:
378 item.item_ahead.items_behind.append(item_behind)
379 item_behind.item_ahead = item.item_ahead
James E. Blairfee8d652013-06-07 08:57:52 -0700380 item.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700381 item.items_behind = []
James E. Blairfee8d652013-06-07 08:57:52 -0700382 item.dequeue_time = time.time()
James E. Blaire0487072012-08-29 17:38:31 -0700383
James E. Blair972e3c72013-08-29 12:04:55 -0700384 def moveItem(self, item, item_ahead):
James E. Blair972e3c72013-08-29 12:04:55 -0700385 if item.item_ahead == item_ahead:
386 return False
387 # Remove from current location
388 if item.item_ahead:
389 item.item_ahead.items_behind.remove(item)
390 for item_behind in item.items_behind:
391 if item.item_ahead:
392 item.item_ahead.items_behind.append(item_behind)
393 item_behind.item_ahead = item.item_ahead
394 # Add to new location
395 item.item_ahead = item_ahead
James E. Blair00451262013-09-20 11:40:17 -0700396 item.items_behind = []
James E. Blair972e3c72013-08-29 12:04:55 -0700397 if item.item_ahead:
398 item.item_ahead.items_behind.append(item)
399 return True
James E. Blairee743612012-05-29 14:49:32 -0700400
401 def mergeChangeQueue(self, other):
402 for project in other.projects:
403 self.addProject(project)
Clark Boylan7603a372014-01-21 11:43:20 -0800404 self.window = min(self.window, other.window)
405 # TODO merge semantics
406
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800407 def isActionable(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800408 if self.window:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800409 return item in self.queue[:self.window]
Clark Boylan7603a372014-01-21 11:43:20 -0800410 else:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800411 return True
Clark Boylan7603a372014-01-21 11:43:20 -0800412
413 def increaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800414 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800415 if self.window_increase_type == 'linear':
416 self.window += self.window_increase_factor
417 elif self.window_increase_type == 'exponential':
418 self.window *= self.window_increase_factor
419
420 def decreaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800421 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800422 if self.window_decrease_type == 'linear':
423 self.window = max(
424 self.window_floor,
425 self.window - self.window_decrease_factor)
426 elif self.window_decrease_type == 'exponential':
427 self.window = max(
428 self.window_floor,
429 self.window / self.window_decrease_factor)
James E. Blairee743612012-05-29 14:49:32 -0700430
James E. Blair1e8dd892012-05-30 09:15:05 -0700431
James E. Blair4aea70c2012-07-26 14:23:24 -0700432class Project(object):
433 def __init__(self, name):
434 self.name = name
James E. Blair19deff22013-08-25 13:17:35 -0700435 self.merge_mode = MERGER_MERGE_RESOLVE
James E. Blair4aea70c2012-07-26 14:23:24 -0700436
437 def __str__(self):
438 return self.name
439
440 def __repr__(self):
441 return '<Project %s>' % (self.name)
442
443
James E. Blairee743612012-05-29 14:49:32 -0700444class Job(object):
445 def __init__(self, name):
James E. Blair222d4982012-07-16 09:31:19 -0700446 # If you add attributes here, be sure to add them to the copy method.
James E. Blairee743612012-05-29 14:49:32 -0700447 self.name = name
James E. Blairc8a1e052014-02-25 09:29:26 -0800448 self.queue_name = None
James E. Blairee743612012-05-29 14:49:32 -0700449 self.failure_message = None
450 self.success_message = None
James E. Blair6aea36d2012-12-17 13:03:24 -0800451 self.failure_pattern = None
452 self.success_pattern = None
James E. Blaire5a847f2012-07-10 15:29:14 -0700453 self.parameter_function = None
James E. Blair222d4982012-07-16 09:31:19 -0700454 self.hold_following_changes = False
James E. Blair4ec821f2012-08-23 15:28:28 -0700455 self.voting = True
James E. Blaire421a232012-07-25 16:59:21 -0700456 self.branches = []
457 self._branches = []
James E. Blair70c71582013-03-06 08:50:50 -0800458 self.files = []
459 self._files = []
Joshua Hesketh36c3fa52014-01-22 11:40:52 +1100460 self.swift = {}
James E. Blairee743612012-05-29 14:49:32 -0700461
462 def __str__(self):
463 return self.name
464
465 def __repr__(self):
466 return '<Job %s>' % (self.name)
467
James E. Blairb0954652012-06-01 11:32:01 -0700468 def copy(self, other):
James E. Blairc28d1b02013-07-19 11:37:06 -0700469 if other.failure_message:
470 self.failure_message = other.failure_message
471 if other.success_message:
472 self.success_message = other.success_message
473 if other.failure_pattern:
474 self.failure_pattern = other.failure_pattern
475 if other.success_pattern:
476 self.success_pattern = other.success_pattern
477 if other.parameter_function:
478 self.parameter_function = other.parameter_function
479 if other.branches:
480 self.branches = other.branches[:]
481 self._branches = other._branches[:]
482 if other.files:
483 self.files = other.files[:]
484 self._files = other._files[:]
Joshua Hesketh36c3fa52014-01-22 11:40:52 +1100485 if other.swift:
486 self.swift.update(other.swift)
James E. Blair222d4982012-07-16 09:31:19 -0700487 self.hold_following_changes = other.hold_following_changes
James E. Blair4ec821f2012-08-23 15:28:28 -0700488 self.voting = other.voting
James E. Blairb0954652012-06-01 11:32:01 -0700489
James E. Blaire421a232012-07-25 16:59:21 -0700490 def changeMatches(self, change):
James E. Blair70c71582013-03-06 08:50:50 -0800491 matches_branch = False
James E. Blaire421a232012-07-25 16:59:21 -0700492 for branch in self.branches:
James E. Blair45865f32012-10-05 09:39:46 -0700493 if hasattr(change, 'branch') and branch.match(change.branch):
James E. Blair70c71582013-03-06 08:50:50 -0800494 matches_branch = True
James E. Blair45865f32012-10-05 09:39:46 -0700495 if hasattr(change, 'ref') and branch.match(change.ref):
James E. Blair70c71582013-03-06 08:50:50 -0800496 matches_branch = True
497 if self.branches and not matches_branch:
498 return False
499
500 matches_file = False
501 for f in self.files:
502 if hasattr(change, 'files'):
503 for cf in change.files:
504 if f.match(cf):
505 matches_file = True
506 if self.files and not matches_file:
507 return False
508
509 return True
James E. Blaire5a847f2012-07-10 15:29:14 -0700510
James E. Blair1e8dd892012-05-30 09:15:05 -0700511
James E. Blairee743612012-05-29 14:49:32 -0700512class JobTree(object):
513 """ A JobTree represents an instance of one Job, and holds JobTrees
514 whose jobs should be run if that Job succeeds. A root node of a
515 JobTree will have no associated Job. """
516
517 def __init__(self, job):
518 self.job = job
519 self.job_trees = []
520
521 def addJob(self, job):
James E. Blair12a92b12014-03-26 11:54:53 -0700522 if job not in [x.job for x in self.job_trees]:
523 t = JobTree(job)
524 self.job_trees.append(t)
525 return t
James E. Blairee743612012-05-29 14:49:32 -0700526
527 def getJobs(self):
528 jobs = []
529 for x in self.job_trees:
530 jobs.append(x.job)
531 jobs.extend(x.getJobs())
532 return jobs
533
534 def getJobTreeForJob(self, job):
535 if self.job == job:
536 return self
537 for tree in self.job_trees:
538 ret = tree.getJobTreeForJob(job)
539 if ret:
540 return ret
541 return None
542
James E. Blair1e8dd892012-05-30 09:15:05 -0700543
James E. Blair4aea70c2012-07-26 14:23:24 -0700544class Build(object):
545 def __init__(self, job, uuid):
546 self.job = job
547 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -0700548 self.url = None
549 self.number = None
550 self.result = None
551 self.build_set = None
552 self.launch_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -0800553 self.start_time = None
554 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -0700555 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -0700556 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -0700557 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -0700558 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -0700559 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +0800560 self.worker = Worker()
James E. Blairee743612012-05-29 14:49:32 -0700561
562 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +0800563 return ('<Build %s of %s on %s>' %
564 (self.uuid, self.job.name, self.worker))
565
566
567class Worker(object):
568 """A model of the worker running a job"""
569 def __init__(self):
570 self.name = "Unknown"
571 self.hostname = None
572 self.ips = []
573 self.fqdn = None
574 self.program = None
575 self.version = None
576 self.extra = {}
577
578 def updateFromData(self, data):
579 """Update worker information if contained in the WORK_DATA response."""
580 self.name = data.get('worker_name', self.name)
581 self.hostname = data.get('worker_hostname', self.hostname)
582 self.ips = data.get('worker_ips', self.ips)
583 self.fqdn = data.get('worker_fqdn', self.fqdn)
584 self.program = data.get('worker_program', self.program)
585 self.version = data.get('worker_version', self.version)
586 self.extra = data.get('worker_extra', self.extra)
587
588 def __repr__(self):
589 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -0700590
James E. Blair1e8dd892012-05-30 09:15:05 -0700591
James E. Blair7e530ad2012-07-03 16:12:28 -0700592class BuildSet(object):
James E. Blair4076e2b2014-01-28 12:42:20 -0800593 # Merge states:
594 NEW = 1
595 PENDING = 2
596 COMPLETE = 3
597
Antoine Musso9b229282014-08-18 23:45:43 +0200598 states_map = {
599 1: 'NEW',
600 2: 'PENDING',
601 3: 'COMPLETE',
602 }
603
James E. Blairfee8d652013-06-07 08:57:52 -0700604 def __init__(self, item):
605 self.item = item
James E. Blair11700c32012-07-05 17:50:05 -0700606 self.other_changes = []
James E. Blair7e530ad2012-07-03 16:12:28 -0700607 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -0700608 self.result = None
609 self.next_build_set = None
610 self.previous_build_set = None
James E. Blair4886cc12012-07-18 15:39:41 -0700611 self.ref = None
James E. Blair81515ad2012-10-01 18:29:08 -0700612 self.commit = None
James E. Blair4076e2b2014-01-28 12:42:20 -0800613 self.zuul_url = None
James E. Blair973721f2012-08-15 10:19:43 -0700614 self.unable_to_merge = False
James E. Blair972e3c72013-08-29 12:04:55 -0700615 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -0800616 self.merge_state = self.NEW
James E. Blair7e530ad2012-07-03 16:12:28 -0700617
Antoine Musso9b229282014-08-18 23:45:43 +0200618 def __repr__(self):
619 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
620 self.item,
621 len(self.builds),
622 self.getStateName(self.merge_state))
623
James E. Blair4886cc12012-07-18 15:39:41 -0700624 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -0700625 # The change isn't enqueued until after it's created
626 # so we don't know what the other changes ahead will be
627 # until jobs start.
628 if not self.other_changes:
James E. Blairfee8d652013-06-07 08:57:52 -0700629 next_item = self.item.item_ahead
630 while next_item:
631 self.other_changes.append(next_item.change)
632 next_item = next_item.item_ahead
James E. Blair4886cc12012-07-18 15:39:41 -0700633 if not self.ref:
634 self.ref = 'Z' + uuid4().hex
635
Antoine Musso9b229282014-08-18 23:45:43 +0200636 def getStateName(self, state_num):
637 return self.states_map.get(
638 state_num, 'UNKNOWN (%s)' % state_num)
639
James E. Blair4886cc12012-07-18 15:39:41 -0700640 def addBuild(self, build):
641 self.builds[build.job.name] = build
642 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -0700643
James E. Blair4a28a882013-08-23 15:17:33 -0700644 def removeBuild(self, build):
645 del self.builds[build.job.name]
646
James E. Blair7e530ad2012-07-03 16:12:28 -0700647 def getBuild(self, job_name):
648 return self.builds.get(job_name)
649
James E. Blair11700c32012-07-05 17:50:05 -0700650 def getBuilds(self):
651 keys = self.builds.keys()
652 keys.sort()
653 return [self.builds.get(x) for x in keys]
654
James E. Blair7e530ad2012-07-03 16:12:28 -0700655
James E. Blairfee8d652013-06-07 08:57:52 -0700656class QueueItem(object):
657 """A changish inside of a Pipeline queue"""
James E. Blair32663402012-06-01 10:04:18 -0700658
James E. Blairbfb8e042014-12-30 17:01:44 -0800659 def __init__(self, queue, change):
660 self.pipeline = queue.pipeline
661 self.queue = queue
James E. Blairfee8d652013-06-07 08:57:52 -0700662 self.change = change # a changeish
James E. Blair7e530ad2012-07-03 16:12:28 -0700663 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -0700664 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -0700665 self.current_build_set = BuildSet(self)
666 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -0700667 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700668 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -0800669 self.enqueue_time = None
670 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -0700671 self.reported = False
James E. Blairbfb8e042014-12-30 17:01:44 -0800672 self.active = False # Whether an item is within an active window
673 self.live = True # Whether an item is intended to be processed at all
James E. Blaire5a847f2012-07-10 15:29:14 -0700674
James E. Blair972e3c72013-08-29 12:04:55 -0700675 def __repr__(self):
676 if self.pipeline:
677 pipeline = self.pipeline.name
678 else:
679 pipeline = None
680 return '<QueueItem 0x%x for %s in %s>' % (
681 id(self), self.change, pipeline)
682
James E. Blairee743612012-05-29 14:49:32 -0700683 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -0700684 old = self.current_build_set
685 self.current_build_set.result = 'CANCELED'
686 self.current_build_set = BuildSet(self)
687 old.next_build_set = self.current_build_set
688 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -0700689 self.build_sets.append(self.current_build_set)
James E. Blairee743612012-05-29 14:49:32 -0700690
691 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -0700692 self.current_build_set.addBuild(build)
James E. Blair66eeebf2013-07-27 17:44:32 -0700693 build.pipeline = self.pipeline
James E. Blairee743612012-05-29 14:49:32 -0700694
James E. Blair4a28a882013-08-23 15:17:33 -0700695 def removeBuild(self, build):
696 self.current_build_set.removeBuild(build)
697
James E. Blairfee8d652013-06-07 08:57:52 -0700698 def setReportedResult(self, result):
699 self.current_build_set.result = result
700
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800701 def formatJSON(self):
702 changeish = self.change
703 ret = {}
704 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -0800705 ret['live'] = self.live
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800706 if hasattr(changeish, 'url') and changeish.url is not None:
707 ret['url'] = changeish.url
708 else:
709 ret['url'] = None
710 ret['id'] = changeish._id()
711 if self.item_ahead:
712 ret['item_ahead'] = self.item_ahead.change._id()
713 else:
714 ret['item_ahead'] = None
715 ret['items_behind'] = [i.change._id() for i in self.items_behind]
716 ret['failing_reasons'] = self.current_build_set.failing_reasons
717 ret['zuul_ref'] = self.current_build_set.ref
718 ret['project'] = changeish.project.name
719 ret['enqueue_time'] = int(self.enqueue_time * 1000)
720 ret['jobs'] = []
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -0500721 if hasattr(changeish, 'owner'):
722 ret['owner'] = changeish.owner
723 else:
724 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800725 max_remaining = 0
James E. Blair107c3852015-02-07 08:23:10 -0800726 for job in self.pipeline.getJobs(self):
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800727 now = time.time()
728 build = self.current_build_set.getBuild(job.name)
729 elapsed = None
730 remaining = None
731 result = None
732 url = None
733 worker = None
734 if build:
735 result = build.result
736 url = build.url
737 if build.start_time:
738 if build.end_time:
739 elapsed = int((build.end_time -
740 build.start_time) * 1000)
741 remaining = 0
742 else:
743 elapsed = int((now - build.start_time) * 1000)
744 if build.estimated_time:
745 remaining = max(
746 int(build.estimated_time * 1000) - elapsed,
747 0)
748 worker = {
749 'name': build.worker.name,
750 'hostname': build.worker.hostname,
751 'ips': build.worker.ips,
752 'fqdn': build.worker.fqdn,
753 'program': build.worker.program,
754 'version': build.worker.version,
755 'extra': build.worker.extra
756 }
757 if remaining and remaining > max_remaining:
758 max_remaining = remaining
759
760 ret['jobs'].append({
761 'name': job.name,
762 'elapsed_time': elapsed,
763 'remaining_time': remaining,
764 'url': url,
765 'result': result,
766 'voting': job.voting,
767 'uuid': build.uuid if build else None,
768 'launch_time': build.launch_time if build else None,
769 'start_time': build.start_time if build else None,
770 'end_time': build.end_time if build else None,
771 'estimated_time': build.estimated_time if build else None,
772 'pipeline': build.pipeline.name if build else None,
773 'canceled': build.canceled if build else None,
774 'retry': build.retry if build else None,
775 'number': build.number if build else None,
776 'parameters': build.parameters if build else None,
777 'worker': worker
778 })
779
780 if self.pipeline.haveAllJobsStarted(self):
781 ret['remaining_time'] = max_remaining
782 else:
783 ret['remaining_time'] = None
784 return ret
785
786 def formatStatus(self, indent=0, html=False):
787 changeish = self.change
788 indent_str = ' ' * indent
789 ret = ''
790 if html and hasattr(changeish, 'url') and changeish.url is not None:
791 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
792 indent_str,
793 changeish.project.name,
794 changeish.url,
795 changeish._id())
796 else:
797 ret += '%sProject %s change %s based on %s\n' % (
798 indent_str,
799 changeish.project.name,
800 changeish._id(),
801 self.item_ahead)
James E. Blair107c3852015-02-07 08:23:10 -0800802 for job in self.pipeline.getJobs(self):
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800803 build = self.current_build_set.getBuild(job.name)
804 if build:
805 result = build.result
806 else:
807 result = None
808 job_name = job.name
809 if not job.voting:
810 voting = ' (non-voting)'
811 else:
812 voting = ''
813 if html:
814 if build:
815 url = build.url
816 else:
817 url = None
818 if url is not None:
819 job_name = '<a href="%s">%s</a>' % (url, job_name)
820 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
821 ret += '\n'
822 return ret
823
James E. Blairfee8d652013-06-07 08:57:52 -0700824
825class Changeish(object):
826 """Something like a change; either a change or a ref"""
James E. Blairfee8d652013-06-07 08:57:52 -0700827
828 def __init__(self, project):
829 self.project = project
830
Joshua Hesketh36c3fa52014-01-22 11:40:52 +1100831 def getBasePath(self):
832 base_path = ''
833 if hasattr(self, 'refspec'):
834 base_path = "%s/%s/%s" % (
835 self.number[-2:], self.number, self.patchset)
836 elif hasattr(self, 'ref'):
837 base_path = "%s/%s" % (self.newrev[:2], self.newrev)
838
839 return base_path
840
James E. Blairfee8d652013-06-07 08:57:52 -0700841 def equals(self, other):
842 raise NotImplementedError()
843
844 def isUpdateOf(self, other):
845 raise NotImplementedError()
846
847 def filterJobs(self, jobs):
848 return filter(lambda job: job.changeMatches(self), jobs)
849
850 def getRelatedChanges(self):
851 return set()
852
James E. Blair1e8dd892012-05-30 09:15:05 -0700853
James E. Blair4aea70c2012-07-26 14:23:24 -0700854class Change(Changeish):
James E. Blair4aea70c2012-07-26 14:23:24 -0700855 def __init__(self, project):
856 super(Change, self).__init__(project)
857 self.branch = None
858 self.number = None
859 self.url = None
860 self.patchset = None
861 self.refspec = None
862
James E. Blair70c71582013-03-06 08:50:50 -0800863 self.files = []
James E. Blair6965a4b2014-12-16 17:19:04 -0800864 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -0700865 self.needed_by_changes = []
866 self.is_current_patchset = True
867 self.can_merge = False
868 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -0700869 self.failed_to_merge = False
James E. Blairc053d022014-01-22 14:57:33 -0800870 self.approvals = []
James E. Blair11041d22014-05-02 14:49:53 -0700871 self.open = None
872 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -0500873 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700874
875 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -0700876 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -0700877
878 def __repr__(self):
879 return '<Change 0x%x %s>' % (id(self), self._id())
880
881 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +0800882 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -0700883 return True
884 return False
885
James E. Blair2fa50962013-01-30 21:50:41 -0800886 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -0800887 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -0700888 (hasattr(other, 'patchset') and
889 self.patchset is not None and
890 other.patchset is not None and
891 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -0800892 return True
893 return False
894
James E. Blairfee8d652013-06-07 08:57:52 -0700895 def getRelatedChanges(self):
896 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -0800897 for c in self.needs_changes:
898 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -0700899 for c in self.needed_by_changes:
900 related.add(c)
901 related.update(c.getRelatedChanges())
902 return related
James E. Blair4aea70c2012-07-26 14:23:24 -0700903
904
905class Ref(Changeish):
James E. Blair4aea70c2012-07-26 14:23:24 -0700906 def __init__(self, project):
James E. Blairbe765db2012-08-07 08:36:20 -0700907 super(Ref, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700908 self.ref = None
909 self.oldrev = None
910 self.newrev = None
911
James E. Blairbe765db2012-08-07 08:36:20 -0700912 def _id(self):
913 return self.newrev
914
Antoine Musso68bdcd72013-01-17 12:31:28 +0100915 def __repr__(self):
916 rep = None
917 if self.newrev == '0000000000000000000000000000000000000000':
918 rep = '<Ref 0x%x deletes %s from %s' % (
919 id(self), self.ref, self.oldrev)
920 elif self.oldrev == '0000000000000000000000000000000000000000':
921 rep = '<Ref 0x%x creates %s on %s>' % (
922 id(self), self.ref, self.newrev)
923 else:
924 # Catch all
925 rep = '<Ref 0x%x %s updated %s..%s>' % (
926 id(self), self.ref, self.oldrev, self.newrev)
927
928 return rep
929
James E. Blair4aea70c2012-07-26 14:23:24 -0700930 def equals(self, other):
James E. Blair9358c612012-09-28 08:29:39 -0700931 if (self.project == other.project
932 and self.ref == other.ref
933 and self.newrev == other.newrev):
James E. Blair4aea70c2012-07-26 14:23:24 -0700934 return True
935 return False
936
James E. Blair2fa50962013-01-30 21:50:41 -0800937 def isUpdateOf(self, other):
938 return False
939
James E. Blair4aea70c2012-07-26 14:23:24 -0700940
James E. Blair63bb0ef2013-07-29 17:14:51 -0700941class NullChange(Changeish):
James E. Blaire5910202013-12-27 09:50:31 -0800942 def __repr__(self):
943 return '<NullChange for %s>' % (self.project)
James E. Blair63bb0ef2013-07-29 17:14:51 -0700944
James E. Blair63bb0ef2013-07-29 17:14:51 -0700945 def _id(self):
Alex Gaynorddb9ef32013-09-16 21:04:58 -0700946 return None
James E. Blair63bb0ef2013-07-29 17:14:51 -0700947
948 def equals(self, other):
James E. Blair4f6033c2014-03-27 15:49:09 -0700949 if (self.project == other.project):
950 return True
James E. Blair63bb0ef2013-07-29 17:14:51 -0700951 return False
952
953 def isUpdateOf(self, other):
954 return False
955
956
James E. Blairee743612012-05-29 14:49:32 -0700957class TriggerEvent(object):
958 def __init__(self):
959 self.data = None
James E. Blair32663402012-06-01 10:04:18 -0700960 # common
James E. Blairee743612012-05-29 14:49:32 -0700961 self.type = None
962 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -0700963 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +0100964 # Representation of the user account that performed the event.
965 self.account = None
James E. Blair32663402012-06-01 10:04:18 -0700966 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -0700967 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -0700968 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -0700969 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -0700970 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -0700971 self.approvals = []
972 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -0700973 self.comment = None
James E. Blair32663402012-06-01 10:04:18 -0700974 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -0700975 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -0700976 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -0700977 self.newrev = None
James E. Blair63bb0ef2013-07-29 17:14:51 -0700978 # timer
979 self.timespec = None
James E. Blairc494d542014-08-06 09:23:52 -0700980 # zuultrigger
981 self.pipeline_name = None
James E. Blairad28e912013-11-27 10:43:22 -0800982 # For events that arrive with a destination pipeline (eg, from
983 # an admin command, etc):
984 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -0700985
James E. Blair9f9667e2012-06-12 17:51:08 -0700986 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -0700987 ret = '<TriggerEvent %s %s' % (self.type, self.project_name)
James E. Blair1e8dd892012-05-30 09:15:05 -0700988
James E. Blairee743612012-05-29 14:49:32 -0700989 if self.branch:
990 ret += " %s" % self.branch
991 if self.change_number:
992 ret += " %s,%s" % (self.change_number, self.patch_number)
993 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -0700994 ret += ' ' + ', '.join(
995 ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
James E. Blairee743612012-05-29 14:49:32 -0700996 ret += '>'
997
998 return ret
999
James E. Blair1e8dd892012-05-30 09:15:05 -07001000
James E. Blair9c17dbf2014-06-23 14:21:58 -07001001class BaseFilter(object):
1002 def __init__(self, required_approvals=[]):
James E. Blair1b265312014-06-24 09:35:21 -07001003 self._required_approvals = copy.deepcopy(required_approvals)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001004 self.required_approvals = required_approvals
1005
1006 for a in self.required_approvals:
1007 for k, v in a.items():
1008 if k == 'username':
1009 pass
James E. Blair1fbfceb2014-06-23 14:42:53 -07001010 elif k in ['email', 'email-filter']:
1011 a['email'] = re.compile(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001012 elif k == 'newer-than':
1013 a[k] = time_to_seconds(v)
1014 elif k == 'older-than':
1015 a[k] = time_to_seconds(v)
1016 else:
1017 if not isinstance(v, list):
1018 a[k] = [v]
James E. Blair1fbfceb2014-06-23 14:42:53 -07001019 if 'email-filter' in a:
1020 del a['email-filter']
James E. Blair9c17dbf2014-06-23 14:21:58 -07001021
1022 def matchesRequiredApprovals(self, change):
1023 now = time.time()
1024 for rapproval in self.required_approvals:
1025 matches_approval = False
1026 for approval in change.approvals:
1027 if 'description' not in approval:
1028 continue
1029 found_approval = True
1030 by = approval.get('by', {})
1031 for k, v in rapproval.items():
1032 if k == 'username':
1033 if (by.get('username', '') != v):
1034 found_approval = False
James E. Blair1fbfceb2014-06-23 14:42:53 -07001035 elif k == 'email':
James E. Blair9c17dbf2014-06-23 14:21:58 -07001036 if (not v.search(by.get('email', ''))):
1037 found_approval = False
1038 elif k == 'newer-than':
1039 t = now - v
1040 if (approval['grantedOn'] < t):
1041 found_approval = False
1042 elif k == 'older-than':
1043 t = now - v
1044 if (approval['grantedOn'] >= t):
1045 found_approval = False
1046 else:
1047 if (normalizeCategory(approval['description']) != k or
1048 int(approval['value']) not in v):
1049 found_approval = False
1050 if found_approval:
1051 matches_approval = True
1052 break
1053 if not matches_approval:
1054 return False
1055 return True
1056
1057
1058class EventFilter(BaseFilter):
James E. Blairc0dedf82014-08-06 09:37:52 -07001059 def __init__(self, trigger, types=[], branches=[], refs=[],
1060 event_approvals={}, comments=[], emails=[], usernames=[],
James E. Blairc494d542014-08-06 09:23:52 -07001061 timespecs=[], required_approvals=[], pipelines=[]):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001062 super(EventFilter, self).__init__(
1063 required_approvals=required_approvals)
James E. Blairc0dedf82014-08-06 09:37:52 -07001064 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07001065 self._types = types
1066 self._branches = branches
1067 self._refs = refs
James E. Blair1fbfceb2014-06-23 14:42:53 -07001068 self._comments = comments
1069 self._emails = emails
1070 self._usernames = usernames
James E. Blairc494d542014-08-06 09:23:52 -07001071 self._pipelines = pipelines
James E. Blairee743612012-05-29 14:49:32 -07001072 self.types = [re.compile(x) for x in types]
1073 self.branches = [re.compile(x) for x in branches]
1074 self.refs = [re.compile(x) for x in refs]
James E. Blair1fbfceb2014-06-23 14:42:53 -07001075 self.comments = [re.compile(x) for x in comments]
1076 self.emails = [re.compile(x) for x in emails]
1077 self.usernames = [re.compile(x) for x in usernames]
James E. Blairc494d542014-08-06 09:23:52 -07001078 self.pipelines = [re.compile(x) for x in pipelines]
James E. Blairc053d022014-01-22 14:57:33 -08001079 self.event_approvals = event_approvals
James E. Blair63bb0ef2013-07-29 17:14:51 -07001080 self.timespecs = timespecs
James E. Blairee743612012-05-29 14:49:32 -07001081
James E. Blair9f9667e2012-06-12 17:51:08 -07001082 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001083 ret = '<EventFilter'
James E. Blair1e8dd892012-05-30 09:15:05 -07001084
James E. Blairee743612012-05-29 14:49:32 -07001085 if self._types:
1086 ret += ' types: %s' % ', '.join(self._types)
James E. Blairc494d542014-08-06 09:23:52 -07001087 if self._pipelines:
1088 ret += ' pipelines: %s' % ', '.join(self._pipelines)
James E. Blairee743612012-05-29 14:49:32 -07001089 if self._branches:
1090 ret += ' branches: %s' % ', '.join(self._branches)
1091 if self._refs:
1092 ret += ' refs: %s' % ', '.join(self._refs)
James E. Blairc053d022014-01-22 14:57:33 -08001093 if self.event_approvals:
1094 ret += ' event_approvals: %s' % ', '.join(
1095 ['%s:%s' % a for a in self.event_approvals.items()])
James E. Blair9c17dbf2014-06-23 14:21:58 -07001096 if self.required_approvals:
1097 ret += ' required_approvals: %s' % ', '.join(
James E. Blair1b265312014-06-24 09:35:21 -07001098 ['%s' % a for a in self._required_approvals])
James E. Blair1fbfceb2014-06-23 14:42:53 -07001099 if self._comments:
1100 ret += ' comments: %s' % ', '.join(self._comments)
1101 if self._emails:
1102 ret += ' emails: %s' % ', '.join(self._emails)
1103 if self._usernames:
1104 ret += ' username_filters: %s' % ', '.join(self._usernames)
James E. Blair63bb0ef2013-07-29 17:14:51 -07001105 if self.timespecs:
1106 ret += ' timespecs: %s' % ', '.join(self.timespecs)
James E. Blairee743612012-05-29 14:49:32 -07001107 ret += '>'
1108
1109 return ret
1110
James E. Blairc053d022014-01-22 14:57:33 -08001111 def matches(self, event, change):
James E. Blairee743612012-05-29 14:49:32 -07001112 # event types are ORed
1113 matches_type = False
1114 for etype in self.types:
1115 if etype.match(event.type):
1116 matches_type = True
1117 if self.types and not matches_type:
1118 return False
1119
James E. Blairc494d542014-08-06 09:23:52 -07001120 # pipelines are ORed
1121 matches_pipeline = False
1122 for epipe in self.pipelines:
1123 if epipe.match(event.pipeline_name):
1124 matches_pipeline = True
1125 if self.pipelines and not matches_pipeline:
1126 return False
1127
James E. Blairee743612012-05-29 14:49:32 -07001128 # branches are ORed
1129 matches_branch = False
1130 for branch in self.branches:
1131 if branch.match(event.branch):
1132 matches_branch = True
1133 if self.branches and not matches_branch:
1134 return False
1135
1136 # refs are ORed
1137 matches_ref = False
Yolanda Robla16698872014-08-25 11:59:27 +02001138 if event.ref is not None:
1139 for ref in self.refs:
1140 if ref.match(event.ref):
1141 matches_ref = True
James E. Blairee743612012-05-29 14:49:32 -07001142 if self.refs and not matches_ref:
1143 return False
1144
James E. Blair1fbfceb2014-06-23 14:42:53 -07001145 # comments are ORed
1146 matches_comment_re = False
1147 for comment_re in self.comments:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001148 if (event.comment is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001149 comment_re.search(event.comment)):
1150 matches_comment_re = True
1151 if self.comments and not matches_comment_re:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001152 return False
1153
Antoine Mussob4e809e2012-12-06 16:58:06 +01001154 # We better have an account provided by Gerrit to do
1155 # email filtering.
1156 if event.account is not None:
James E. Blaircf429f32012-12-20 14:28:24 -08001157 account_email = event.account.get('email')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001158 # emails are ORed
1159 matches_email_re = False
1160 for email_re in self.emails:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001161 if (account_email is not None and
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001162 email_re.search(account_email)):
James E. Blair1fbfceb2014-06-23 14:42:53 -07001163 matches_email_re = True
1164 if self.emails and not matches_email_re:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001165 return False
1166
James E. Blair1fbfceb2014-06-23 14:42:53 -07001167 # usernames are ORed
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001168 account_username = event.account.get('username')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001169 matches_username_re = False
1170 for username_re in self.usernames:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001171 if (account_username is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001172 username_re.search(account_username)):
1173 matches_username_re = True
1174 if self.usernames and not matches_username_re:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001175 return False
1176
James E. Blairee743612012-05-29 14:49:32 -07001177 # approvals are ANDed
James E. Blairc053d022014-01-22 14:57:33 -08001178 for category, value in self.event_approvals.items():
James E. Blairee743612012-05-29 14:49:32 -07001179 matches_approval = False
1180 for eapproval in event.approvals:
1181 if (normalizeCategory(eapproval['description']) == category and
1182 int(eapproval['value']) == int(value)):
1183 matches_approval = True
James E. Blair1e8dd892012-05-30 09:15:05 -07001184 if not matches_approval:
1185 return False
James E. Blair63bb0ef2013-07-29 17:14:51 -07001186
James E. Blair9c17dbf2014-06-23 14:21:58 -07001187 if self.required_approvals and not change.approvals:
James E. Blairc053d022014-01-22 14:57:33 -08001188 # A change with no approvals can not match
1189 return False
1190
James E. Blair9c17dbf2014-06-23 14:21:58 -07001191 # required approvals are ANDed
1192 if not self.matchesRequiredApprovals(change):
1193 return False
James E. Blairc053d022014-01-22 14:57:33 -08001194
James E. Blair63bb0ef2013-07-29 17:14:51 -07001195 # timespecs are ORed
1196 matches_timespec = False
1197 for timespec in self.timespecs:
1198 if (event.timespec == timespec):
1199 matches_timespec = True
1200 if self.timespecs and not matches_timespec:
1201 return False
1202
James E. Blairee743612012-05-29 14:49:32 -07001203 return True
James E. Blaireff88162013-07-01 12:44:14 -04001204
1205
James E. Blair9c17dbf2014-06-23 14:21:58 -07001206class ChangeishFilter(BaseFilter):
Clark Boylana9702ad2014-05-08 17:17:24 -07001207 def __init__(self, open=None, current_patchset=None,
James E. Blair9c17dbf2014-06-23 14:21:58 -07001208 statuses=[], required_approvals=[]):
1209 super(ChangeishFilter, self).__init__(
1210 required_approvals=required_approvals)
James E. Blair11041d22014-05-02 14:49:53 -07001211 self.open = open
Clark Boylana9702ad2014-05-08 17:17:24 -07001212 self.current_patchset = current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001213 self.statuses = statuses
James E. Blair11041d22014-05-02 14:49:53 -07001214
1215 def __repr__(self):
1216 ret = '<ChangeishFilter'
1217
1218 if self.open is not None:
1219 ret += ' open: %s' % self.open
Clark Boylana9702ad2014-05-08 17:17:24 -07001220 if self.current_patchset is not None:
1221 ret += ' current-patchset: %s' % self.current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001222 if self.statuses:
1223 ret += ' statuses: %s' % ', '.join(self.statuses)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001224 if self.required_approvals:
1225 ret += ' required_approvals: %s' % str(self.required_approvals)
James E. Blair11041d22014-05-02 14:49:53 -07001226 ret += '>'
1227
1228 return ret
1229
1230 def matches(self, change):
1231 if self.open is not None:
1232 if self.open != change.open:
1233 return False
1234
Clark Boylana9702ad2014-05-08 17:17:24 -07001235 if self.current_patchset is not None:
1236 if self.current_patchset != change.is_current_patchset:
1237 return False
1238
James E. Blair11041d22014-05-02 14:49:53 -07001239 if self.statuses:
1240 if change.status not in self.statuses:
1241 return False
1242
James E. Blair9c17dbf2014-06-23 14:21:58 -07001243 if self.required_approvals and not change.approvals:
James E. Blair11041d22014-05-02 14:49:53 -07001244 # A change with no approvals can not match
1245 return False
1246
James E. Blair9c17dbf2014-06-23 14:21:58 -07001247 # required approvals are ANDed
1248 if not self.matchesRequiredApprovals(change):
1249 return False
James E. Blair11041d22014-05-02 14:49:53 -07001250
1251 return True
1252
1253
James E. Blaireff88162013-07-01 12:44:14 -04001254class Layout(object):
1255 def __init__(self):
1256 self.projects = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07001257 self.pipelines = OrderedDict()
James E. Blaireff88162013-07-01 12:44:14 -04001258 self.jobs = {}
James E. Blairc28d1b02013-07-19 11:37:06 -07001259 self.metajobs = []
James E. Blaireff88162013-07-01 12:44:14 -04001260
1261 def getJob(self, name):
1262 if name in self.jobs:
1263 return self.jobs[name]
1264 job = Job(name)
1265 if name.startswith('^'):
1266 # This is a meta-job
1267 regex = re.compile(name)
James E. Blairc28d1b02013-07-19 11:37:06 -07001268 self.metajobs.append((regex, job))
James E. Blaireff88162013-07-01 12:44:14 -04001269 else:
1270 # Apply attributes from matching meta-jobs
James E. Blairc28d1b02013-07-19 11:37:06 -07001271 for regex, metajob in self.metajobs:
James E. Blaireff88162013-07-01 12:44:14 -04001272 if regex.match(name):
1273 job.copy(metajob)
1274 self.jobs[name] = job
1275 return job