blob: 8dc28dfbeb8e24de6d7490548205b40945defa42 [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. Blair17dd6772015-02-09 14:45:18 -080076 self.ignore_dependencies = False
James E. Blair4aea70c2012-07-26 14:23:24 -070077 self.job_trees = {} # project -> JobTree
78 self.manager = None
James E. Blaire0487072012-08-29 17:38:31 -070079 self.queues = []
James E. Blair64ed6f22013-07-10 14:07:23 -070080 self.precedence = PRECEDENCE_NORMAL
James E. Blairc0dedf82014-08-06 09:37:52 -070081 self.source = None
Joshua Hesketh1879cf72013-08-19 14:13:15 +100082 self.start_actions = None
83 self.success_actions = None
84 self.failure_actions = None
Clark Boylan7603a372014-01-21 11:43:20 -080085 self.window = None
86 self.window_floor = None
87 self.window_increase_type = None
88 self.window_increase_factor = None
89 self.window_decrease_type = None
90 self.window_decrease_factor = None
James E. Blair4aea70c2012-07-26 14:23:24 -070091
James E. Blaird09c17a2012-08-07 09:23:14 -070092 def __repr__(self):
93 return '<Pipeline %s>' % self.name
94
James E. Blair4aea70c2012-07-26 14:23:24 -070095 def setManager(self, manager):
96 self.manager = manager
97
98 def addProject(self, project):
99 job_tree = JobTree(None) # Null job == job tree root
100 self.job_trees[project] = job_tree
101 return job_tree
102
103 def getProjects(self):
James E. Blairc3d428e2013-12-03 15:06:48 -0800104 return sorted(self.job_trees.keys(), lambda a, b: cmp(a.name, b.name))
James E. Blair4aea70c2012-07-26 14:23:24 -0700105
James E. Blaire0487072012-08-29 17:38:31 -0700106 def addQueue(self, queue):
107 self.queues.append(queue)
108
109 def getQueue(self, project):
110 for queue in self.queues:
111 if project in queue.projects:
112 return queue
113 return None
114
James E. Blairbfb8e042014-12-30 17:01:44 -0800115 def removeQueue(self, queue):
116 self.queues.remove(queue)
117
James E. Blair4aea70c2012-07-26 14:23:24 -0700118 def getJobTree(self, project):
119 tree = self.job_trees.get(project)
120 return tree
121
James E. Blair107c3852015-02-07 08:23:10 -0800122 def getJobs(self, item):
123 if not item.live:
124 return []
125 tree = self.getJobTree(item.change.project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700126 if not tree:
127 return []
James E. Blair107c3852015-02-07 08:23:10 -0800128 return item.change.filterJobs(tree.getJobs())
James E. Blair4aea70c2012-07-26 14:23:24 -0700129
James E. Blairfee8d652013-06-07 08:57:52 -0700130 def _findJobsToRun(self, job_trees, item):
James E. Blair4aea70c2012-07-26 14:23:24 -0700131 torun = []
James E. Blairfee8d652013-06-07 08:57:52 -0700132 if item.item_ahead:
James E. Blair4aea70c2012-07-26 14:23:24 -0700133 # Only run jobs if any 'hold' jobs on the change ahead
134 # have completed successfully.
James E. Blairfee8d652013-06-07 08:57:52 -0700135 if self.isHoldingFollowingChanges(item.item_ahead):
James E. Blair4aea70c2012-07-26 14:23:24 -0700136 return []
137 for tree in job_trees:
138 job = tree.job
139 result = None
140 if job:
James E. Blairfee8d652013-06-07 08:57:52 -0700141 if not job.changeMatches(item.change):
James E. Blair4aea70c2012-07-26 14:23:24 -0700142 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700143 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700144 if build:
145 result = build.result
146 else:
147 # There is no build for the root of this job tree,
148 # so we should run it.
149 torun.append(job)
150 # If there is no job, this is a null job tree, and we should
151 # run all of its jobs.
152 if result == 'SUCCESS' or not job:
James E. Blairfee8d652013-06-07 08:57:52 -0700153 torun.extend(self._findJobsToRun(tree.job_trees, item))
James E. Blair4aea70c2012-07-26 14:23:24 -0700154 return torun
155
James E. Blairfee8d652013-06-07 08:57:52 -0700156 def findJobsToRun(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800157 if not item.live:
158 return []
James E. Blairfee8d652013-06-07 08:57:52 -0700159 tree = self.getJobTree(item.change.project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700160 if not tree:
161 return []
James E. Blairfee8d652013-06-07 08:57:52 -0700162 return self._findJobsToRun(tree.job_trees, item)
James E. Blair4aea70c2012-07-26 14:23:24 -0700163
James E. Blairbea9ef12013-07-15 11:52:23 -0700164 def haveAllJobsStarted(self, item):
James E. Blair107c3852015-02-07 08:23:10 -0800165 for job in self.getJobs(item):
James E. Blairbea9ef12013-07-15 11:52:23 -0700166 build = item.current_build_set.getBuild(job.name)
167 if not build or not build.start_time:
168 return False
169 return True
170
James E. Blairfee8d652013-06-07 08:57:52 -0700171 def areAllJobsComplete(self, item):
James E. Blair107c3852015-02-07 08:23:10 -0800172 for job in self.getJobs(item):
James E. Blairfee8d652013-06-07 08:57:52 -0700173 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700174 if not build or not build.result:
175 return False
176 return True
177
James E. Blairfee8d652013-06-07 08:57:52 -0700178 def didAllJobsSucceed(self, item):
James E. Blair107c3852015-02-07 08:23:10 -0800179 for job in self.getJobs(item):
James E. Blair4ec821f2012-08-23 15:28:28 -0700180 if not job.voting:
181 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700182 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700183 if not build:
184 return False
185 if build.result != 'SUCCESS':
186 return False
187 return True
188
Joshua Heskethb7179772014-01-30 23:30:46 +1100189 def didMergerSucceed(self, item):
190 if item.current_build_set.unable_to_merge:
191 return False
192 return True
193
James E. Blairfee8d652013-06-07 08:57:52 -0700194 def didAnyJobFail(self, item):
James E. Blair107c3852015-02-07 08:23:10 -0800195 for job in self.getJobs(item):
James E. Blair4ec821f2012-08-23 15:28:28 -0700196 if not job.voting:
197 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700198 build = item.current_build_set.getBuild(job.name)
James E. Blair0018a6c2013-02-27 14:11:45 -0800199 if build and build.result and (build.result != 'SUCCESS'):
James E. Blair4aea70c2012-07-26 14:23:24 -0700200 return True
201 return False
202
James E. Blairfee8d652013-06-07 08:57:52 -0700203 def isHoldingFollowingChanges(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800204 if not item.live:
205 return False
James E. Blair107c3852015-02-07 08:23:10 -0800206 for job in self.getJobs(item):
James E. Blair4aea70c2012-07-26 14:23:24 -0700207 if not job.hold_following_changes:
208 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700209 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700210 if not build:
211 return True
212 if build.result != 'SUCCESS':
213 return True
James E. Blair972e3c72013-08-29 12:04:55 -0700214
James E. Blairfee8d652013-06-07 08:57:52 -0700215 if not item.item_ahead:
James E. Blair4aea70c2012-07-26 14:23:24 -0700216 return False
James E. Blairfee8d652013-06-07 08:57:52 -0700217 return self.isHoldingFollowingChanges(item.item_ahead)
James E. Blair4aea70c2012-07-26 14:23:24 -0700218
James E. Blairfee8d652013-06-07 08:57:52 -0700219 def setResult(self, item, build):
James E. Blair4a28a882013-08-23 15:17:33 -0700220 if build.retry:
221 item.removeBuild(build)
222 elif build.result != 'SUCCESS':
James E. Blair4aea70c2012-07-26 14:23:24 -0700223 # Get a JobTree from a Job so we can find only its dependent jobs
James E. Blairfee8d652013-06-07 08:57:52 -0700224 root = self.getJobTree(item.change.project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700225 tree = root.getJobTreeForJob(build.job)
226 for job in tree.getJobs():
227 fakebuild = Build(job, None)
228 fakebuild.result = 'SKIPPED'
James E. Blairfee8d652013-06-07 08:57:52 -0700229 item.addBuild(fakebuild)
James E. Blair4aea70c2012-07-26 14:23:24 -0700230
Joshua Heskethb7179772014-01-30 23:30:46 +1100231 def setUnableToMerge(self, item):
James E. Blairfee8d652013-06-07 08:57:52 -0700232 item.current_build_set.unable_to_merge = True
233 root = self.getJobTree(item.change.project)
James E. Blair973721f2012-08-15 10:19:43 -0700234 for job in root.getJobs():
235 fakebuild = Build(job, None)
236 fakebuild.result = 'SKIPPED'
James E. Blairfee8d652013-06-07 08:57:52 -0700237 item.addBuild(fakebuild)
James E. Blair973721f2012-08-15 10:19:43 -0700238
James E. Blairfee8d652013-06-07 08:57:52 -0700239 def setDequeuedNeedingChange(self, item):
240 item.dequeued_needing_change = True
241 root = self.getJobTree(item.change.project)
James E. Blaircaec0c52012-08-22 14:52:22 -0700242 for job in root.getJobs():
243 fakebuild = Build(job, None)
244 fakebuild.result = 'SKIPPED'
James E. Blairfee8d652013-06-07 08:57:52 -0700245 item.addBuild(fakebuild)
James E. Blaircaec0c52012-08-22 14:52:22 -0700246
James E. Blaire0487072012-08-29 17:38:31 -0700247 def getChangesInQueue(self):
248 changes = []
249 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700250 changes.extend([x.change for x in shared_queue.queue])
James E. Blaire0487072012-08-29 17:38:31 -0700251 return changes
252
James E. Blairfee8d652013-06-07 08:57:52 -0700253 def getAllItems(self):
254 items = []
James E. Blaire0487072012-08-29 17:38:31 -0700255 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700256 items.extend(shared_queue.queue)
James E. Blairfee8d652013-06-07 08:57:52 -0700257 return items
James E. Blaire0487072012-08-29 17:38:31 -0700258
James E. Blair8dbd56a2012-12-22 10:55:10 -0800259 def formatStatusJSON(self):
260 j_pipeline = dict(name=self.name,
261 description=self.description)
262 j_queues = []
263 j_pipeline['change_queues'] = j_queues
264 for queue in self.queues:
265 j_queue = dict(name=queue.name)
266 j_queues.append(j_queue)
267 j_queue['heads'] = []
Clark Boylanaf2476f2014-01-23 14:47:36 -0800268 j_queue['window'] = queue.window
James E. Blair972e3c72013-08-29 12:04:55 -0700269
270 j_changes = []
271 for e in queue.queue:
272 if not e.item_ahead:
273 if j_changes:
274 j_queue['heads'].append(j_changes)
275 j_changes = []
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800276 j_changes.append(e.formatJSON())
James E. Blair972e3c72013-08-29 12:04:55 -0700277 if (len(j_changes) > 1 and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000278 (j_changes[-2]['remaining_time'] is not None) and
279 (j_changes[-1]['remaining_time'] is not None)):
James E. Blair972e3c72013-08-29 12:04:55 -0700280 j_changes[-1]['remaining_time'] = max(
281 j_changes[-2]['remaining_time'],
282 j_changes[-1]['remaining_time'])
283 if j_changes:
James E. Blair8dbd56a2012-12-22 10:55:10 -0800284 j_queue['heads'].append(j_changes)
285 return j_pipeline
286
James E. Blair4aea70c2012-07-26 14:23:24 -0700287
Joshua Hesketh1879cf72013-08-19 14:13:15 +1000288class ActionReporter(object):
Alex Gaynor813d39b2014-05-17 16:17:16 -0700289 """An ActionReporter has a reporter and its configured parameters"""
Joshua Hesketh1879cf72013-08-19 14:13:15 +1000290
291 def __repr__(self):
292 return '<ActionReporter %s, %s>' % (self.reporter, self.params)
293
294 def __init__(self, reporter, params):
295 self.reporter = reporter
296 self.params = params
297
298 def report(self, change, message):
299 """Sends the built message off to the configured reporter.
300 Takes the change and message and adds the configured parameters.
301 """
302 return self.reporter.report(change, message, self.params)
303
304 def getSubmitAllowNeeds(self):
305 """Gets the submit allow needs from the reporter based off the
306 parameters."""
307 return self.reporter.getSubmitAllowNeeds(self.params)
308
309
James E. Blairee743612012-05-29 14:49:32 -0700310class ChangeQueue(object):
James E. Blair4aea70c2012-07-26 14:23:24 -0700311 """DependentPipelines have multiple parallel queues shared by
312 different projects; this is one of them. For instance, there may
313 a queue shared by interrelated projects foo and bar, and a second
314 queue for independent project baz. Pipelines have one or more
James E. Blairbfb8e042014-12-30 17:01:44 -0800315 ChangeQueues."""
316 def __init__(self, pipeline, window=0, window_floor=1,
Clark Boylan7603a372014-01-21 11:43:20 -0800317 window_increase_type='linear', window_increase_factor=1,
318 window_decrease_type='exponential', window_decrease_factor=2):
James E. Blair4aea70c2012-07-26 14:23:24 -0700319 self.pipeline = pipeline
James E. Blairee743612012-05-29 14:49:32 -0700320 self.name = ''
James E. Blairc8a1e052014-02-25 09:29:26 -0800321 self.assigned_name = None
322 self.generated_name = None
James E. Blairee743612012-05-29 14:49:32 -0700323 self.projects = []
324 self._jobs = set()
325 self.queue = []
Clark Boylan7603a372014-01-21 11:43:20 -0800326 self.window = window
327 self.window_floor = window_floor
328 self.window_increase_type = window_increase_type
329 self.window_increase_factor = window_increase_factor
330 self.window_decrease_type = window_decrease_type
331 self.window_decrease_factor = window_decrease_factor
James E. Blairee743612012-05-29 14:49:32 -0700332
James E. Blair9f9667e2012-06-12 17:51:08 -0700333 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700334 return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
James E. Blairee743612012-05-29 14:49:32 -0700335
336 def getJobs(self):
337 return self._jobs
338
339 def addProject(self, project):
340 if project not in self.projects:
341 self.projects.append(project)
James E. Blairc8a1e052014-02-25 09:29:26 -0800342 self._jobs |= set(self.pipeline.getJobTree(project).getJobs())
343
James E. Blairee743612012-05-29 14:49:32 -0700344 names = [x.name for x in self.projects]
345 names.sort()
James E. Blairc8a1e052014-02-25 09:29:26 -0800346 self.generated_name = ', '.join(names)
347
348 for job in self._jobs:
349 if job.queue_name:
350 if (self.assigned_name and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000351 job.queue_name != self.assigned_name):
James E. Blairc8a1e052014-02-25 09:29:26 -0800352 raise Exception("More than one name assigned to "
353 "change queue: %s != %s" %
354 (self.assigned_name, job.queue_name))
355 self.assigned_name = job.queue_name
356 self.name = self.assigned_name or self.generated_name
James E. Blairee743612012-05-29 14:49:32 -0700357
358 def enqueueChange(self, change):
James E. Blairbfb8e042014-12-30 17:01:44 -0800359 item = QueueItem(self, change)
James E. Blaircdccd972013-07-01 12:10:22 -0700360 self.enqueueItem(item)
361 item.enqueue_time = time.time()
362 return item
363
364 def enqueueItem(self, item):
James E. Blair4a035d92014-01-23 13:10:48 -0800365 item.pipeline = self.pipeline
James E. Blairbfb8e042014-12-30 17:01:44 -0800366 item.queue = self
367 if self.queue:
James E. Blairfee8d652013-06-07 08:57:52 -0700368 item.item_ahead = self.queue[-1]
James E. Blair972e3c72013-08-29 12:04:55 -0700369 item.item_ahead.items_behind.append(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700370 self.queue.append(item)
James E. Blairee743612012-05-29 14:49:32 -0700371
James E. Blairfee8d652013-06-07 08:57:52 -0700372 def dequeueItem(self, item):
373 if item in self.queue:
374 self.queue.remove(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700375 if item.item_ahead:
James E. Blair972e3c72013-08-29 12:04:55 -0700376 item.item_ahead.items_behind.remove(item)
377 for item_behind in item.items_behind:
378 if item.item_ahead:
379 item.item_ahead.items_behind.append(item_behind)
380 item_behind.item_ahead = item.item_ahead
James E. Blairfee8d652013-06-07 08:57:52 -0700381 item.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700382 item.items_behind = []
James E. Blairfee8d652013-06-07 08:57:52 -0700383 item.dequeue_time = time.time()
James E. Blaire0487072012-08-29 17:38:31 -0700384
James E. Blair972e3c72013-08-29 12:04:55 -0700385 def moveItem(self, item, item_ahead):
James E. Blair972e3c72013-08-29 12:04:55 -0700386 if item.item_ahead == item_ahead:
387 return False
388 # Remove from current location
389 if item.item_ahead:
390 item.item_ahead.items_behind.remove(item)
391 for item_behind in item.items_behind:
392 if item.item_ahead:
393 item.item_ahead.items_behind.append(item_behind)
394 item_behind.item_ahead = item.item_ahead
395 # Add to new location
396 item.item_ahead = item_ahead
James E. Blair00451262013-09-20 11:40:17 -0700397 item.items_behind = []
James E. Blair972e3c72013-08-29 12:04:55 -0700398 if item.item_ahead:
399 item.item_ahead.items_behind.append(item)
400 return True
James E. Blairee743612012-05-29 14:49:32 -0700401
402 def mergeChangeQueue(self, other):
403 for project in other.projects:
404 self.addProject(project)
Clark Boylan7603a372014-01-21 11:43:20 -0800405 self.window = min(self.window, other.window)
406 # TODO merge semantics
407
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800408 def isActionable(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800409 if self.window:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800410 return item in self.queue[:self.window]
Clark Boylan7603a372014-01-21 11:43:20 -0800411 else:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800412 return True
Clark Boylan7603a372014-01-21 11:43:20 -0800413
414 def increaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800415 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800416 if self.window_increase_type == 'linear':
417 self.window += self.window_increase_factor
418 elif self.window_increase_type == 'exponential':
419 self.window *= self.window_increase_factor
420
421 def decreaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800422 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800423 if self.window_decrease_type == 'linear':
424 self.window = max(
425 self.window_floor,
426 self.window - self.window_decrease_factor)
427 elif self.window_decrease_type == 'exponential':
428 self.window = max(
429 self.window_floor,
430 self.window / self.window_decrease_factor)
James E. Blairee743612012-05-29 14:49:32 -0700431
James E. Blair1e8dd892012-05-30 09:15:05 -0700432
James E. Blair4aea70c2012-07-26 14:23:24 -0700433class Project(object):
434 def __init__(self, name):
435 self.name = name
James E. Blair19deff22013-08-25 13:17:35 -0700436 self.merge_mode = MERGER_MERGE_RESOLVE
James E. Blair4aea70c2012-07-26 14:23:24 -0700437
438 def __str__(self):
439 return self.name
440
441 def __repr__(self):
442 return '<Project %s>' % (self.name)
443
444
James E. Blairee743612012-05-29 14:49:32 -0700445class Job(object):
446 def __init__(self, name):
James E. Blair222d4982012-07-16 09:31:19 -0700447 # If you add attributes here, be sure to add them to the copy method.
James E. Blairee743612012-05-29 14:49:32 -0700448 self.name = name
James E. Blairc8a1e052014-02-25 09:29:26 -0800449 self.queue_name = None
James E. Blairee743612012-05-29 14:49:32 -0700450 self.failure_message = None
451 self.success_message = None
James E. Blair6aea36d2012-12-17 13:03:24 -0800452 self.failure_pattern = None
453 self.success_pattern = None
James E. Blaire5a847f2012-07-10 15:29:14 -0700454 self.parameter_function = None
Maru Newby79427a42015-02-17 17:54:45 +0000455 # A metajob should only supply values for attributes that have
456 # been explicitly provided, so avoid setting boolean defaults.
457 if self.is_metajob:
458 self.hold_following_changes = None
459 self.voting = None
460 else:
461 self.hold_following_changes = False
462 self.voting = True
James E. Blaire421a232012-07-25 16:59:21 -0700463 self.branches = []
464 self._branches = []
James E. Blair70c71582013-03-06 08:50:50 -0800465 self.files = []
466 self._files = []
Maru Newby3fe5f852015-01-13 04:22:14 +0000467 self.skip_if_matcher = None
Joshua Hesketh36c3fa52014-01-22 11:40:52 +1100468 self.swift = {}
James E. Blairee743612012-05-29 14:49:32 -0700469
470 def __str__(self):
471 return self.name
472
473 def __repr__(self):
474 return '<Job %s>' % (self.name)
475
Maru Newby79427a42015-02-17 17:54:45 +0000476 @property
477 def is_metajob(self):
478 return self.name.startswith('^')
479
James E. Blairb0954652012-06-01 11:32:01 -0700480 def copy(self, other):
James E. Blairc28d1b02013-07-19 11:37:06 -0700481 if other.failure_message:
482 self.failure_message = other.failure_message
483 if other.success_message:
484 self.success_message = other.success_message
485 if other.failure_pattern:
486 self.failure_pattern = other.failure_pattern
487 if other.success_pattern:
488 self.success_pattern = other.success_pattern
489 if other.parameter_function:
490 self.parameter_function = other.parameter_function
491 if other.branches:
492 self.branches = other.branches[:]
493 self._branches = other._branches[:]
494 if other.files:
495 self.files = other.files[:]
496 self._files = other._files[:]
Maru Newby3fe5f852015-01-13 04:22:14 +0000497 if other.skip_if_matcher:
498 self.skip_if_matcher = other.skip_if_matcher.copy()
Joshua Hesketh36c3fa52014-01-22 11:40:52 +1100499 if other.swift:
500 self.swift.update(other.swift)
Maru Newby79427a42015-02-17 17:54:45 +0000501 # Only non-None values should be copied for boolean attributes.
502 if other.hold_following_changes is not None:
503 self.hold_following_changes = other.hold_following_changes
504 if other.voting is not None:
505 self.voting = other.voting
James E. Blairb0954652012-06-01 11:32:01 -0700506
James E. Blaire421a232012-07-25 16:59:21 -0700507 def changeMatches(self, change):
James E. Blair70c71582013-03-06 08:50:50 -0800508 matches_branch = False
James E. Blaire421a232012-07-25 16:59:21 -0700509 for branch in self.branches:
James E. Blair45865f32012-10-05 09:39:46 -0700510 if hasattr(change, 'branch') and branch.match(change.branch):
James E. Blair70c71582013-03-06 08:50:50 -0800511 matches_branch = True
James E. Blair45865f32012-10-05 09:39:46 -0700512 if hasattr(change, 'ref') and branch.match(change.ref):
James E. Blair70c71582013-03-06 08:50:50 -0800513 matches_branch = True
514 if self.branches and not matches_branch:
515 return False
516
517 matches_file = False
518 for f in self.files:
519 if hasattr(change, 'files'):
520 for cf in change.files:
521 if f.match(cf):
522 matches_file = True
523 if self.files and not matches_file:
524 return False
525
Maru Newby3fe5f852015-01-13 04:22:14 +0000526 if self.skip_if_matcher and self.skip_if_matcher.matches(change):
527 return False
528
James E. Blair70c71582013-03-06 08:50:50 -0800529 return True
James E. Blaire5a847f2012-07-10 15:29:14 -0700530
James E. Blair1e8dd892012-05-30 09:15:05 -0700531
James E. Blairee743612012-05-29 14:49:32 -0700532class JobTree(object):
533 """ A JobTree represents an instance of one Job, and holds JobTrees
534 whose jobs should be run if that Job succeeds. A root node of a
535 JobTree will have no associated Job. """
536
537 def __init__(self, job):
538 self.job = job
539 self.job_trees = []
540
541 def addJob(self, job):
James E. Blair12a92b12014-03-26 11:54:53 -0700542 if job not in [x.job for x in self.job_trees]:
543 t = JobTree(job)
544 self.job_trees.append(t)
545 return t
James E. Blairee743612012-05-29 14:49:32 -0700546
547 def getJobs(self):
548 jobs = []
549 for x in self.job_trees:
550 jobs.append(x.job)
551 jobs.extend(x.getJobs())
552 return jobs
553
554 def getJobTreeForJob(self, job):
555 if self.job == job:
556 return self
557 for tree in self.job_trees:
558 ret = tree.getJobTreeForJob(job)
559 if ret:
560 return ret
561 return None
562
James E. Blair1e8dd892012-05-30 09:15:05 -0700563
James E. Blair4aea70c2012-07-26 14:23:24 -0700564class Build(object):
565 def __init__(self, job, uuid):
566 self.job = job
567 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -0700568 self.url = None
569 self.number = None
570 self.result = None
571 self.build_set = None
572 self.launch_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -0800573 self.start_time = None
574 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -0700575 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -0700576 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -0700577 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -0700578 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -0700579 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +0800580 self.worker = Worker()
James E. Blairee743612012-05-29 14:49:32 -0700581
582 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +0800583 return ('<Build %s of %s on %s>' %
584 (self.uuid, self.job.name, self.worker))
585
586
587class Worker(object):
588 """A model of the worker running a job"""
589 def __init__(self):
590 self.name = "Unknown"
591 self.hostname = None
592 self.ips = []
593 self.fqdn = None
594 self.program = None
595 self.version = None
596 self.extra = {}
597
598 def updateFromData(self, data):
599 """Update worker information if contained in the WORK_DATA response."""
600 self.name = data.get('worker_name', self.name)
601 self.hostname = data.get('worker_hostname', self.hostname)
602 self.ips = data.get('worker_ips', self.ips)
603 self.fqdn = data.get('worker_fqdn', self.fqdn)
604 self.program = data.get('worker_program', self.program)
605 self.version = data.get('worker_version', self.version)
606 self.extra = data.get('worker_extra', self.extra)
607
608 def __repr__(self):
609 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -0700610
James E. Blair1e8dd892012-05-30 09:15:05 -0700611
James E. Blair7e530ad2012-07-03 16:12:28 -0700612class BuildSet(object):
James E. Blair4076e2b2014-01-28 12:42:20 -0800613 # Merge states:
614 NEW = 1
615 PENDING = 2
616 COMPLETE = 3
617
Antoine Musso9b229282014-08-18 23:45:43 +0200618 states_map = {
619 1: 'NEW',
620 2: 'PENDING',
621 3: 'COMPLETE',
622 }
623
James E. Blairfee8d652013-06-07 08:57:52 -0700624 def __init__(self, item):
625 self.item = item
James E. Blair11700c32012-07-05 17:50:05 -0700626 self.other_changes = []
James E. Blair7e530ad2012-07-03 16:12:28 -0700627 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -0700628 self.result = None
629 self.next_build_set = None
630 self.previous_build_set = None
James E. Blair4886cc12012-07-18 15:39:41 -0700631 self.ref = None
James E. Blair81515ad2012-10-01 18:29:08 -0700632 self.commit = None
James E. Blair4076e2b2014-01-28 12:42:20 -0800633 self.zuul_url = None
James E. Blair973721f2012-08-15 10:19:43 -0700634 self.unable_to_merge = False
James E. Blair972e3c72013-08-29 12:04:55 -0700635 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -0800636 self.merge_state = self.NEW
James E. Blair7e530ad2012-07-03 16:12:28 -0700637
Antoine Musso9b229282014-08-18 23:45:43 +0200638 def __repr__(self):
639 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
640 self.item,
641 len(self.builds),
642 self.getStateName(self.merge_state))
643
James E. Blair4886cc12012-07-18 15:39:41 -0700644 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -0700645 # The change isn't enqueued until after it's created
646 # so we don't know what the other changes ahead will be
647 # until jobs start.
648 if not self.other_changes:
James E. Blairfee8d652013-06-07 08:57:52 -0700649 next_item = self.item.item_ahead
650 while next_item:
651 self.other_changes.append(next_item.change)
652 next_item = next_item.item_ahead
James E. Blair4886cc12012-07-18 15:39:41 -0700653 if not self.ref:
654 self.ref = 'Z' + uuid4().hex
655
Antoine Musso9b229282014-08-18 23:45:43 +0200656 def getStateName(self, state_num):
657 return self.states_map.get(
658 state_num, 'UNKNOWN (%s)' % state_num)
659
James E. Blair4886cc12012-07-18 15:39:41 -0700660 def addBuild(self, build):
661 self.builds[build.job.name] = build
662 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -0700663
James E. Blair4a28a882013-08-23 15:17:33 -0700664 def removeBuild(self, build):
665 del self.builds[build.job.name]
666
James E. Blair7e530ad2012-07-03 16:12:28 -0700667 def getBuild(self, job_name):
668 return self.builds.get(job_name)
669
James E. Blair11700c32012-07-05 17:50:05 -0700670 def getBuilds(self):
671 keys = self.builds.keys()
672 keys.sort()
673 return [self.builds.get(x) for x in keys]
674
James E. Blair7e530ad2012-07-03 16:12:28 -0700675
James E. Blairfee8d652013-06-07 08:57:52 -0700676class QueueItem(object):
677 """A changish inside of a Pipeline queue"""
James E. Blair32663402012-06-01 10:04:18 -0700678
James E. Blairbfb8e042014-12-30 17:01:44 -0800679 def __init__(self, queue, change):
680 self.pipeline = queue.pipeline
681 self.queue = queue
James E. Blairfee8d652013-06-07 08:57:52 -0700682 self.change = change # a changeish
James E. Blair7e530ad2012-07-03 16:12:28 -0700683 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -0700684 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -0700685 self.current_build_set = BuildSet(self)
686 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -0700687 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700688 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -0800689 self.enqueue_time = None
690 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -0700691 self.reported = False
James E. Blairbfb8e042014-12-30 17:01:44 -0800692 self.active = False # Whether an item is within an active window
693 self.live = True # Whether an item is intended to be processed at all
James E. Blaire5a847f2012-07-10 15:29:14 -0700694
James E. Blair972e3c72013-08-29 12:04:55 -0700695 def __repr__(self):
696 if self.pipeline:
697 pipeline = self.pipeline.name
698 else:
699 pipeline = None
700 return '<QueueItem 0x%x for %s in %s>' % (
701 id(self), self.change, pipeline)
702
James E. Blairee743612012-05-29 14:49:32 -0700703 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -0700704 old = self.current_build_set
705 self.current_build_set.result = 'CANCELED'
706 self.current_build_set = BuildSet(self)
707 old.next_build_set = self.current_build_set
708 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -0700709 self.build_sets.append(self.current_build_set)
James E. Blairee743612012-05-29 14:49:32 -0700710
711 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -0700712 self.current_build_set.addBuild(build)
James E. Blair66eeebf2013-07-27 17:44:32 -0700713 build.pipeline = self.pipeline
James E. Blairee743612012-05-29 14:49:32 -0700714
James E. Blair4a28a882013-08-23 15:17:33 -0700715 def removeBuild(self, build):
716 self.current_build_set.removeBuild(build)
717
James E. Blairfee8d652013-06-07 08:57:52 -0700718 def setReportedResult(self, result):
719 self.current_build_set.result = result
720
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800721 def formatJSON(self):
722 changeish = self.change
723 ret = {}
724 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -0800725 ret['live'] = self.live
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800726 if hasattr(changeish, 'url') and changeish.url is not None:
727 ret['url'] = changeish.url
728 else:
729 ret['url'] = None
730 ret['id'] = changeish._id()
731 if self.item_ahead:
732 ret['item_ahead'] = self.item_ahead.change._id()
733 else:
734 ret['item_ahead'] = None
735 ret['items_behind'] = [i.change._id() for i in self.items_behind]
736 ret['failing_reasons'] = self.current_build_set.failing_reasons
737 ret['zuul_ref'] = self.current_build_set.ref
738 ret['project'] = changeish.project.name
739 ret['enqueue_time'] = int(self.enqueue_time * 1000)
740 ret['jobs'] = []
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -0500741 if hasattr(changeish, 'owner'):
742 ret['owner'] = changeish.owner
743 else:
744 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800745 max_remaining = 0
James E. Blair107c3852015-02-07 08:23:10 -0800746 for job in self.pipeline.getJobs(self):
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800747 now = time.time()
748 build = self.current_build_set.getBuild(job.name)
749 elapsed = None
750 remaining = None
751 result = None
752 url = None
753 worker = None
754 if build:
755 result = build.result
756 url = build.url
757 if build.start_time:
758 if build.end_time:
759 elapsed = int((build.end_time -
760 build.start_time) * 1000)
761 remaining = 0
762 else:
763 elapsed = int((now - build.start_time) * 1000)
764 if build.estimated_time:
765 remaining = max(
766 int(build.estimated_time * 1000) - elapsed,
767 0)
768 worker = {
769 'name': build.worker.name,
770 'hostname': build.worker.hostname,
771 'ips': build.worker.ips,
772 'fqdn': build.worker.fqdn,
773 'program': build.worker.program,
774 'version': build.worker.version,
775 'extra': build.worker.extra
776 }
777 if remaining and remaining > max_remaining:
778 max_remaining = remaining
779
780 ret['jobs'].append({
781 'name': job.name,
782 'elapsed_time': elapsed,
783 'remaining_time': remaining,
784 'url': url,
785 'result': result,
786 'voting': job.voting,
787 'uuid': build.uuid if build else None,
788 'launch_time': build.launch_time if build else None,
789 'start_time': build.start_time if build else None,
790 'end_time': build.end_time if build else None,
791 'estimated_time': build.estimated_time if build else None,
792 'pipeline': build.pipeline.name if build else None,
793 'canceled': build.canceled if build else None,
794 'retry': build.retry if build else None,
795 'number': build.number if build else None,
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800796 'worker': worker
797 })
798
799 if self.pipeline.haveAllJobsStarted(self):
800 ret['remaining_time'] = max_remaining
801 else:
802 ret['remaining_time'] = None
803 return ret
804
805 def formatStatus(self, indent=0, html=False):
806 changeish = self.change
807 indent_str = ' ' * indent
808 ret = ''
809 if html and hasattr(changeish, 'url') and changeish.url is not None:
810 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
811 indent_str,
812 changeish.project.name,
813 changeish.url,
814 changeish._id())
815 else:
816 ret += '%sProject %s change %s based on %s\n' % (
817 indent_str,
818 changeish.project.name,
819 changeish._id(),
820 self.item_ahead)
James E. Blair107c3852015-02-07 08:23:10 -0800821 for job in self.pipeline.getJobs(self):
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800822 build = self.current_build_set.getBuild(job.name)
823 if build:
824 result = build.result
825 else:
826 result = None
827 job_name = job.name
828 if not job.voting:
829 voting = ' (non-voting)'
830 else:
831 voting = ''
832 if html:
833 if build:
834 url = build.url
835 else:
836 url = None
837 if url is not None:
838 job_name = '<a href="%s">%s</a>' % (url, job_name)
839 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
840 ret += '\n'
841 return ret
842
James E. Blairfee8d652013-06-07 08:57:52 -0700843
844class Changeish(object):
845 """Something like a change; either a change or a ref"""
James E. Blairfee8d652013-06-07 08:57:52 -0700846
847 def __init__(self, project):
848 self.project = project
849
Joshua Hesketh36c3fa52014-01-22 11:40:52 +1100850 def getBasePath(self):
851 base_path = ''
852 if hasattr(self, 'refspec'):
853 base_path = "%s/%s/%s" % (
854 self.number[-2:], self.number, self.patchset)
855 elif hasattr(self, 'ref'):
856 base_path = "%s/%s" % (self.newrev[:2], self.newrev)
857
858 return base_path
859
James E. Blairfee8d652013-06-07 08:57:52 -0700860 def equals(self, other):
861 raise NotImplementedError()
862
863 def isUpdateOf(self, other):
864 raise NotImplementedError()
865
866 def filterJobs(self, jobs):
867 return filter(lambda job: job.changeMatches(self), jobs)
868
869 def getRelatedChanges(self):
870 return set()
871
James E. Blair1e8dd892012-05-30 09:15:05 -0700872
James E. Blair4aea70c2012-07-26 14:23:24 -0700873class Change(Changeish):
James E. Blair4aea70c2012-07-26 14:23:24 -0700874 def __init__(self, project):
875 super(Change, self).__init__(project)
876 self.branch = None
877 self.number = None
878 self.url = None
879 self.patchset = None
880 self.refspec = None
881
James E. Blair70c71582013-03-06 08:50:50 -0800882 self.files = []
James E. Blair6965a4b2014-12-16 17:19:04 -0800883 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -0700884 self.needed_by_changes = []
885 self.is_current_patchset = True
886 self.can_merge = False
887 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -0700888 self.failed_to_merge = False
James E. Blairc053d022014-01-22 14:57:33 -0800889 self.approvals = []
James E. Blair11041d22014-05-02 14:49:53 -0700890 self.open = None
891 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -0500892 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700893
894 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -0700895 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -0700896
897 def __repr__(self):
898 return '<Change 0x%x %s>' % (id(self), self._id())
899
900 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +0800901 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -0700902 return True
903 return False
904
James E. Blair2fa50962013-01-30 21:50:41 -0800905 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -0800906 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -0700907 (hasattr(other, 'patchset') and
908 self.patchset is not None and
909 other.patchset is not None and
910 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -0800911 return True
912 return False
913
James E. Blairfee8d652013-06-07 08:57:52 -0700914 def getRelatedChanges(self):
915 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -0800916 for c in self.needs_changes:
917 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -0700918 for c in self.needed_by_changes:
919 related.add(c)
920 related.update(c.getRelatedChanges())
921 return related
James E. Blair4aea70c2012-07-26 14:23:24 -0700922
923
924class Ref(Changeish):
James E. Blair4aea70c2012-07-26 14:23:24 -0700925 def __init__(self, project):
James E. Blairbe765db2012-08-07 08:36:20 -0700926 super(Ref, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700927 self.ref = None
928 self.oldrev = None
929 self.newrev = None
930
James E. Blairbe765db2012-08-07 08:36:20 -0700931 def _id(self):
932 return self.newrev
933
Antoine Musso68bdcd72013-01-17 12:31:28 +0100934 def __repr__(self):
935 rep = None
936 if self.newrev == '0000000000000000000000000000000000000000':
937 rep = '<Ref 0x%x deletes %s from %s' % (
938 id(self), self.ref, self.oldrev)
939 elif self.oldrev == '0000000000000000000000000000000000000000':
940 rep = '<Ref 0x%x creates %s on %s>' % (
941 id(self), self.ref, self.newrev)
942 else:
943 # Catch all
944 rep = '<Ref 0x%x %s updated %s..%s>' % (
945 id(self), self.ref, self.oldrev, self.newrev)
946
947 return rep
948
James E. Blair4aea70c2012-07-26 14:23:24 -0700949 def equals(self, other):
James E. Blair9358c612012-09-28 08:29:39 -0700950 if (self.project == other.project
951 and self.ref == other.ref
952 and self.newrev == other.newrev):
James E. Blair4aea70c2012-07-26 14:23:24 -0700953 return True
954 return False
955
James E. Blair2fa50962013-01-30 21:50:41 -0800956 def isUpdateOf(self, other):
957 return False
958
James E. Blair4aea70c2012-07-26 14:23:24 -0700959
James E. Blair63bb0ef2013-07-29 17:14:51 -0700960class NullChange(Changeish):
James E. Blaire5910202013-12-27 09:50:31 -0800961 def __repr__(self):
962 return '<NullChange for %s>' % (self.project)
James E. Blair63bb0ef2013-07-29 17:14:51 -0700963
James E. Blair63bb0ef2013-07-29 17:14:51 -0700964 def _id(self):
Alex Gaynorddb9ef32013-09-16 21:04:58 -0700965 return None
James E. Blair63bb0ef2013-07-29 17:14:51 -0700966
967 def equals(self, other):
James E. Blair4f6033c2014-03-27 15:49:09 -0700968 if (self.project == other.project):
969 return True
James E. Blair63bb0ef2013-07-29 17:14:51 -0700970 return False
971
972 def isUpdateOf(self, other):
973 return False
974
975
James E. Blairee743612012-05-29 14:49:32 -0700976class TriggerEvent(object):
977 def __init__(self):
978 self.data = None
James E. Blair32663402012-06-01 10:04:18 -0700979 # common
James E. Blairee743612012-05-29 14:49:32 -0700980 self.type = None
981 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -0700982 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +0100983 # Representation of the user account that performed the event.
984 self.account = None
James E. Blair32663402012-06-01 10:04:18 -0700985 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -0700986 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -0700987 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -0700988 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -0700989 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -0700990 self.approvals = []
991 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -0700992 self.comment = None
James E. Blair32663402012-06-01 10:04:18 -0700993 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -0700994 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -0700995 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -0700996 self.newrev = None
James E. Blair63bb0ef2013-07-29 17:14:51 -0700997 # timer
998 self.timespec = None
James E. Blairc494d542014-08-06 09:23:52 -0700999 # zuultrigger
1000 self.pipeline_name = None
James E. Blairad28e912013-11-27 10:43:22 -08001001 # For events that arrive with a destination pipeline (eg, from
1002 # an admin command, etc):
1003 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07001004
James E. Blair9f9667e2012-06-12 17:51:08 -07001005 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001006 ret = '<TriggerEvent %s %s' % (self.type, self.project_name)
James E. Blair1e8dd892012-05-30 09:15:05 -07001007
James E. Blairee743612012-05-29 14:49:32 -07001008 if self.branch:
1009 ret += " %s" % self.branch
1010 if self.change_number:
1011 ret += " %s,%s" % (self.change_number, self.patch_number)
1012 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -07001013 ret += ' ' + ', '.join(
1014 ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
James E. Blairee743612012-05-29 14:49:32 -07001015 ret += '>'
1016
1017 return ret
1018
James E. Blair1e8dd892012-05-30 09:15:05 -07001019
James E. Blair9c17dbf2014-06-23 14:21:58 -07001020class BaseFilter(object):
1021 def __init__(self, required_approvals=[]):
James E. Blair1b265312014-06-24 09:35:21 -07001022 self._required_approvals = copy.deepcopy(required_approvals)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001023 self.required_approvals = required_approvals
1024
1025 for a in self.required_approvals:
1026 for k, v in a.items():
1027 if k == 'username':
1028 pass
James E. Blair1fbfceb2014-06-23 14:42:53 -07001029 elif k in ['email', 'email-filter']:
1030 a['email'] = re.compile(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001031 elif k == 'newer-than':
1032 a[k] = time_to_seconds(v)
1033 elif k == 'older-than':
1034 a[k] = time_to_seconds(v)
1035 else:
1036 if not isinstance(v, list):
1037 a[k] = [v]
James E. Blair1fbfceb2014-06-23 14:42:53 -07001038 if 'email-filter' in a:
1039 del a['email-filter']
James E. Blair9c17dbf2014-06-23 14:21:58 -07001040
1041 def matchesRequiredApprovals(self, change):
1042 now = time.time()
1043 for rapproval in self.required_approvals:
1044 matches_approval = False
1045 for approval in change.approvals:
1046 if 'description' not in approval:
1047 continue
1048 found_approval = True
1049 by = approval.get('by', {})
1050 for k, v in rapproval.items():
1051 if k == 'username':
1052 if (by.get('username', '') != v):
1053 found_approval = False
James E. Blair1fbfceb2014-06-23 14:42:53 -07001054 elif k == 'email':
James E. Blair9c17dbf2014-06-23 14:21:58 -07001055 if (not v.search(by.get('email', ''))):
1056 found_approval = False
1057 elif k == 'newer-than':
1058 t = now - v
1059 if (approval['grantedOn'] < t):
1060 found_approval = False
1061 elif k == 'older-than':
1062 t = now - v
1063 if (approval['grantedOn'] >= t):
1064 found_approval = False
1065 else:
1066 if (normalizeCategory(approval['description']) != k or
1067 int(approval['value']) not in v):
1068 found_approval = False
1069 if found_approval:
1070 matches_approval = True
1071 break
1072 if not matches_approval:
1073 return False
1074 return True
1075
1076
1077class EventFilter(BaseFilter):
James E. Blairc0dedf82014-08-06 09:37:52 -07001078 def __init__(self, trigger, types=[], branches=[], refs=[],
1079 event_approvals={}, comments=[], emails=[], usernames=[],
James E. Blairc494d542014-08-06 09:23:52 -07001080 timespecs=[], required_approvals=[], pipelines=[]):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001081 super(EventFilter, self).__init__(
1082 required_approvals=required_approvals)
James E. Blairc0dedf82014-08-06 09:37:52 -07001083 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07001084 self._types = types
1085 self._branches = branches
1086 self._refs = refs
James E. Blair1fbfceb2014-06-23 14:42:53 -07001087 self._comments = comments
1088 self._emails = emails
1089 self._usernames = usernames
James E. Blairc494d542014-08-06 09:23:52 -07001090 self._pipelines = pipelines
James E. Blairee743612012-05-29 14:49:32 -07001091 self.types = [re.compile(x) for x in types]
1092 self.branches = [re.compile(x) for x in branches]
1093 self.refs = [re.compile(x) for x in refs]
James E. Blair1fbfceb2014-06-23 14:42:53 -07001094 self.comments = [re.compile(x) for x in comments]
1095 self.emails = [re.compile(x) for x in emails]
1096 self.usernames = [re.compile(x) for x in usernames]
James E. Blairc494d542014-08-06 09:23:52 -07001097 self.pipelines = [re.compile(x) for x in pipelines]
James E. Blairc053d022014-01-22 14:57:33 -08001098 self.event_approvals = event_approvals
James E. Blair63bb0ef2013-07-29 17:14:51 -07001099 self.timespecs = timespecs
James E. Blairee743612012-05-29 14:49:32 -07001100
James E. Blair9f9667e2012-06-12 17:51:08 -07001101 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001102 ret = '<EventFilter'
James E. Blair1e8dd892012-05-30 09:15:05 -07001103
James E. Blairee743612012-05-29 14:49:32 -07001104 if self._types:
1105 ret += ' types: %s' % ', '.join(self._types)
James E. Blairc494d542014-08-06 09:23:52 -07001106 if self._pipelines:
1107 ret += ' pipelines: %s' % ', '.join(self._pipelines)
James E. Blairee743612012-05-29 14:49:32 -07001108 if self._branches:
1109 ret += ' branches: %s' % ', '.join(self._branches)
1110 if self._refs:
1111 ret += ' refs: %s' % ', '.join(self._refs)
James E. Blairc053d022014-01-22 14:57:33 -08001112 if self.event_approvals:
1113 ret += ' event_approvals: %s' % ', '.join(
1114 ['%s:%s' % a for a in self.event_approvals.items()])
James E. Blair9c17dbf2014-06-23 14:21:58 -07001115 if self.required_approvals:
1116 ret += ' required_approvals: %s' % ', '.join(
James E. Blair1b265312014-06-24 09:35:21 -07001117 ['%s' % a for a in self._required_approvals])
James E. Blair1fbfceb2014-06-23 14:42:53 -07001118 if self._comments:
1119 ret += ' comments: %s' % ', '.join(self._comments)
1120 if self._emails:
1121 ret += ' emails: %s' % ', '.join(self._emails)
1122 if self._usernames:
1123 ret += ' username_filters: %s' % ', '.join(self._usernames)
James E. Blair63bb0ef2013-07-29 17:14:51 -07001124 if self.timespecs:
1125 ret += ' timespecs: %s' % ', '.join(self.timespecs)
James E. Blairee743612012-05-29 14:49:32 -07001126 ret += '>'
1127
1128 return ret
1129
James E. Blairc053d022014-01-22 14:57:33 -08001130 def matches(self, event, change):
James E. Blairee743612012-05-29 14:49:32 -07001131 # event types are ORed
1132 matches_type = False
1133 for etype in self.types:
1134 if etype.match(event.type):
1135 matches_type = True
1136 if self.types and not matches_type:
1137 return False
1138
James E. Blairc494d542014-08-06 09:23:52 -07001139 # pipelines are ORed
1140 matches_pipeline = False
1141 for epipe in self.pipelines:
1142 if epipe.match(event.pipeline_name):
1143 matches_pipeline = True
1144 if self.pipelines and not matches_pipeline:
1145 return False
1146
James E. Blairee743612012-05-29 14:49:32 -07001147 # branches are ORed
1148 matches_branch = False
1149 for branch in self.branches:
1150 if branch.match(event.branch):
1151 matches_branch = True
1152 if self.branches and not matches_branch:
1153 return False
1154
1155 # refs are ORed
1156 matches_ref = False
Yolanda Robla16698872014-08-25 11:59:27 +02001157 if event.ref is not None:
1158 for ref in self.refs:
1159 if ref.match(event.ref):
1160 matches_ref = True
James E. Blairee743612012-05-29 14:49:32 -07001161 if self.refs and not matches_ref:
1162 return False
1163
James E. Blair1fbfceb2014-06-23 14:42:53 -07001164 # comments are ORed
1165 matches_comment_re = False
1166 for comment_re in self.comments:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001167 if (event.comment is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001168 comment_re.search(event.comment)):
1169 matches_comment_re = True
1170 if self.comments and not matches_comment_re:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001171 return False
1172
Antoine Mussob4e809e2012-12-06 16:58:06 +01001173 # We better have an account provided by Gerrit to do
1174 # email filtering.
1175 if event.account is not None:
James E. Blaircf429f32012-12-20 14:28:24 -08001176 account_email = event.account.get('email')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001177 # emails are ORed
1178 matches_email_re = False
1179 for email_re in self.emails:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001180 if (account_email is not None and
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001181 email_re.search(account_email)):
James E. Blair1fbfceb2014-06-23 14:42:53 -07001182 matches_email_re = True
1183 if self.emails and not matches_email_re:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001184 return False
1185
James E. Blair1fbfceb2014-06-23 14:42:53 -07001186 # usernames are ORed
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001187 account_username = event.account.get('username')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001188 matches_username_re = False
1189 for username_re in self.usernames:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001190 if (account_username is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001191 username_re.search(account_username)):
1192 matches_username_re = True
1193 if self.usernames and not matches_username_re:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001194 return False
1195
James E. Blairee743612012-05-29 14:49:32 -07001196 # approvals are ANDed
James E. Blairc053d022014-01-22 14:57:33 -08001197 for category, value in self.event_approvals.items():
James E. Blairee743612012-05-29 14:49:32 -07001198 matches_approval = False
1199 for eapproval in event.approvals:
1200 if (normalizeCategory(eapproval['description']) == category and
1201 int(eapproval['value']) == int(value)):
1202 matches_approval = True
James E. Blair1e8dd892012-05-30 09:15:05 -07001203 if not matches_approval:
1204 return False
James E. Blair63bb0ef2013-07-29 17:14:51 -07001205
James E. Blair9c17dbf2014-06-23 14:21:58 -07001206 if self.required_approvals and not change.approvals:
James E. Blairc053d022014-01-22 14:57:33 -08001207 # A change with no approvals can not match
1208 return False
1209
James E. Blair9c17dbf2014-06-23 14:21:58 -07001210 # required approvals are ANDed
1211 if not self.matchesRequiredApprovals(change):
1212 return False
James E. Blairc053d022014-01-22 14:57:33 -08001213
James E. Blair63bb0ef2013-07-29 17:14:51 -07001214 # timespecs are ORed
1215 matches_timespec = False
1216 for timespec in self.timespecs:
1217 if (event.timespec == timespec):
1218 matches_timespec = True
1219 if self.timespecs and not matches_timespec:
1220 return False
1221
James E. Blairee743612012-05-29 14:49:32 -07001222 return True
James E. Blaireff88162013-07-01 12:44:14 -04001223
1224
James E. Blair9c17dbf2014-06-23 14:21:58 -07001225class ChangeishFilter(BaseFilter):
Clark Boylana9702ad2014-05-08 17:17:24 -07001226 def __init__(self, open=None, current_patchset=None,
James E. Blair9c17dbf2014-06-23 14:21:58 -07001227 statuses=[], required_approvals=[]):
1228 super(ChangeishFilter, self).__init__(
1229 required_approvals=required_approvals)
James E. Blair11041d22014-05-02 14:49:53 -07001230 self.open = open
Clark Boylana9702ad2014-05-08 17:17:24 -07001231 self.current_patchset = current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001232 self.statuses = statuses
James E. Blair11041d22014-05-02 14:49:53 -07001233
1234 def __repr__(self):
1235 ret = '<ChangeishFilter'
1236
1237 if self.open is not None:
1238 ret += ' open: %s' % self.open
Clark Boylana9702ad2014-05-08 17:17:24 -07001239 if self.current_patchset is not None:
1240 ret += ' current-patchset: %s' % self.current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001241 if self.statuses:
1242 ret += ' statuses: %s' % ', '.join(self.statuses)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001243 if self.required_approvals:
1244 ret += ' required_approvals: %s' % str(self.required_approvals)
James E. Blair11041d22014-05-02 14:49:53 -07001245 ret += '>'
1246
1247 return ret
1248
1249 def matches(self, change):
1250 if self.open is not None:
1251 if self.open != change.open:
1252 return False
1253
Clark Boylana9702ad2014-05-08 17:17:24 -07001254 if self.current_patchset is not None:
1255 if self.current_patchset != change.is_current_patchset:
1256 return False
1257
James E. Blair11041d22014-05-02 14:49:53 -07001258 if self.statuses:
1259 if change.status not in self.statuses:
1260 return False
1261
James E. Blair9c17dbf2014-06-23 14:21:58 -07001262 if self.required_approvals and not change.approvals:
James E. Blair11041d22014-05-02 14:49:53 -07001263 # A change with no approvals can not match
1264 return False
1265
James E. Blair9c17dbf2014-06-23 14:21:58 -07001266 # required approvals are ANDed
1267 if not self.matchesRequiredApprovals(change):
1268 return False
James E. Blair11041d22014-05-02 14:49:53 -07001269
1270 return True
1271
1272
James E. Blaireff88162013-07-01 12:44:14 -04001273class Layout(object):
1274 def __init__(self):
1275 self.projects = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07001276 self.pipelines = OrderedDict()
James E. Blaireff88162013-07-01 12:44:14 -04001277 self.jobs = {}
James E. Blairc28d1b02013-07-19 11:37:06 -07001278 self.metajobs = []
James E. Blaireff88162013-07-01 12:44:14 -04001279
1280 def getJob(self, name):
1281 if name in self.jobs:
1282 return self.jobs[name]
1283 job = Job(name)
Maru Newby79427a42015-02-17 17:54:45 +00001284 if job.is_metajob:
James E. Blaireff88162013-07-01 12:44:14 -04001285 regex = re.compile(name)
James E. Blairc28d1b02013-07-19 11:37:06 -07001286 self.metajobs.append((regex, job))
James E. Blaireff88162013-07-01 12:44:14 -04001287 else:
1288 # Apply attributes from matching meta-jobs
James E. Blairc28d1b02013-07-19 11:37:06 -07001289 for regex, metajob in self.metajobs:
James E. Blaireff88162013-07-01 12:44:14 -04001290 if regex.match(name):
1291 job.copy(metajob)
1292 self.jobs[name] = job
1293 return job