blob: 66487744358037c49c81942bc5af546cf619da5a [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
Joshua Heskethb2068e82014-06-26 15:30:08 +100015import ast
James E. Blair1b265312014-06-24 09:35:21 -070016import copy
James E. Blairee743612012-05-29 14:49:32 -070017import re
Joshua Heskethb2068e82014-06-26 15:30:08 +100018import six
James E. Blairff986a12012-05-30 14:56:51 -070019import time
James E. Blair4886cc12012-07-18 15:39:41 -070020from uuid import uuid4
James E. Blair5a9918a2013-08-27 10:06:27 -070021import extras
22
23OrderedDict = extras.try_imports(['collections.OrderedDict',
24 'ordereddict.OrderedDict'])
James E. Blair4886cc12012-07-18 15:39:41 -070025
26
James E. Blair19deff22013-08-25 13:17:35 -070027MERGER_MERGE = 1 # "git merge"
28MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
29MERGER_CHERRY_PICK = 3 # "git cherry-pick"
30
31MERGER_MAP = {
32 'merge': MERGER_MERGE,
33 'merge-resolve': MERGER_MERGE_RESOLVE,
34 'cherry-pick': MERGER_CHERRY_PICK,
35}
James E. Blairee743612012-05-29 14:49:32 -070036
James E. Blair64ed6f22013-07-10 14:07:23 -070037PRECEDENCE_NORMAL = 0
38PRECEDENCE_LOW = 1
39PRECEDENCE_HIGH = 2
40
41PRECEDENCE_MAP = {
42 None: PRECEDENCE_NORMAL,
43 'low': PRECEDENCE_LOW,
44 'normal': PRECEDENCE_NORMAL,
45 'high': PRECEDENCE_HIGH,
46}
47
James E. Blair1e8dd892012-05-30 09:15:05 -070048
James E. Blairc053d022014-01-22 14:57:33 -080049def time_to_seconds(s):
50 if s.endswith('s'):
51 return int(s[:-1])
52 if s.endswith('m'):
53 return int(s[:-1]) * 60
54 if s.endswith('h'):
55 return int(s[:-1]) * 60 * 60
56 if s.endswith('d'):
57 return int(s[:-1]) * 24 * 60 * 60
58 if s.endswith('w'):
59 return int(s[:-1]) * 7 * 24 * 60 * 60
60 raise Exception("Unable to parse time value: %s" % s)
61
62
James E. Blair11041d22014-05-02 14:49:53 -070063def normalizeCategory(name):
64 name = name.lower()
65 return re.sub(' ', '-', name)
66
67
James E. Blair4aea70c2012-07-26 14:23:24 -070068class Pipeline(object):
69 """A top-level pipeline such as check, gate, post, etc."""
70 def __init__(self, name):
71 self.name = name
James E. Blair8dbd56a2012-12-22 10:55:10 -080072 self.description = None
James E. Blair56370192013-01-14 15:47:28 -080073 self.failure_message = None
Joshua Heskethb7179772014-01-30 23:30:46 +110074 self.merge_failure_message = None
James E. Blair56370192013-01-14 15:47:28 -080075 self.success_message = None
Joshua Hesketh3979e3e2014-03-04 11:21:10 +110076 self.footer_message = None
James E. Blair2fa50962013-01-30 21:50:41 -080077 self.dequeue_on_new_patchset = True
James E. Blair17dd6772015-02-09 14:45:18 -080078 self.ignore_dependencies = False
James E. Blair4aea70c2012-07-26 14:23:24 -070079 self.job_trees = {} # project -> JobTree
80 self.manager = None
James E. Blaire0487072012-08-29 17:38:31 -070081 self.queues = []
James E. Blair64ed6f22013-07-10 14:07:23 -070082 self.precedence = PRECEDENCE_NORMAL
James E. Blairc0dedf82014-08-06 09:37:52 -070083 self.source = None
Joshua Hesketh1879cf72013-08-19 14:13:15 +100084 self.start_actions = None
85 self.success_actions = None
86 self.failure_actions = None
Clark Boylan7603a372014-01-21 11:43:20 -080087 self.window = None
88 self.window_floor = None
89 self.window_increase_type = None
90 self.window_increase_factor = None
91 self.window_decrease_type = None
92 self.window_decrease_factor = None
James E. Blair4aea70c2012-07-26 14:23:24 -070093
James E. Blaird09c17a2012-08-07 09:23:14 -070094 def __repr__(self):
95 return '<Pipeline %s>' % self.name
96
James E. Blair4aea70c2012-07-26 14:23:24 -070097 def setManager(self, manager):
98 self.manager = manager
99
100 def addProject(self, project):
101 job_tree = JobTree(None) # Null job == job tree root
102 self.job_trees[project] = job_tree
103 return job_tree
104
105 def getProjects(self):
James E. Blairc3d428e2013-12-03 15:06:48 -0800106 return sorted(self.job_trees.keys(), lambda a, b: cmp(a.name, b.name))
James E. Blair4aea70c2012-07-26 14:23:24 -0700107
James E. Blaire0487072012-08-29 17:38:31 -0700108 def addQueue(self, queue):
109 self.queues.append(queue)
110
111 def getQueue(self, project):
112 for queue in self.queues:
113 if project in queue.projects:
114 return queue
115 return None
116
James E. Blairbfb8e042014-12-30 17:01:44 -0800117 def removeQueue(self, queue):
118 self.queues.remove(queue)
119
James E. Blair4aea70c2012-07-26 14:23:24 -0700120 def getJobTree(self, project):
121 tree = self.job_trees.get(project)
122 return tree
123
James E. Blair107c3852015-02-07 08:23:10 -0800124 def getJobs(self, item):
125 if not item.live:
126 return []
127 tree = self.getJobTree(item.change.project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700128 if not tree:
129 return []
James E. Blair107c3852015-02-07 08:23:10 -0800130 return item.change.filterJobs(tree.getJobs())
James E. Blair4aea70c2012-07-26 14:23:24 -0700131
James E. Blairfee8d652013-06-07 08:57:52 -0700132 def _findJobsToRun(self, job_trees, item):
James E. Blair4aea70c2012-07-26 14:23:24 -0700133 torun = []
James E. Blairfee8d652013-06-07 08:57:52 -0700134 if item.item_ahead:
James E. Blair4aea70c2012-07-26 14:23:24 -0700135 # Only run jobs if any 'hold' jobs on the change ahead
136 # have completed successfully.
James E. Blairfee8d652013-06-07 08:57:52 -0700137 if self.isHoldingFollowingChanges(item.item_ahead):
James E. Blair4aea70c2012-07-26 14:23:24 -0700138 return []
139 for tree in job_trees:
140 job = tree.job
141 result = None
142 if job:
James E. Blairfee8d652013-06-07 08:57:52 -0700143 if not job.changeMatches(item.change):
James E. Blair4aea70c2012-07-26 14:23:24 -0700144 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700145 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700146 if build:
147 result = build.result
148 else:
149 # There is no build for the root of this job tree,
150 # so we should run it.
151 torun.append(job)
152 # If there is no job, this is a null job tree, and we should
153 # run all of its jobs.
154 if result == 'SUCCESS' or not job:
James E. Blairfee8d652013-06-07 08:57:52 -0700155 torun.extend(self._findJobsToRun(tree.job_trees, item))
James E. Blair4aea70c2012-07-26 14:23:24 -0700156 return torun
157
James E. Blairfee8d652013-06-07 08:57:52 -0700158 def findJobsToRun(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800159 if not item.live:
160 return []
James E. Blairfee8d652013-06-07 08:57:52 -0700161 tree = self.getJobTree(item.change.project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700162 if not tree:
163 return []
James E. Blairfee8d652013-06-07 08:57:52 -0700164 return self._findJobsToRun(tree.job_trees, item)
James E. Blair4aea70c2012-07-26 14:23:24 -0700165
James E. Blairbea9ef12013-07-15 11:52:23 -0700166 def haveAllJobsStarted(self, item):
James E. Blair107c3852015-02-07 08:23:10 -0800167 for job in self.getJobs(item):
James E. Blairbea9ef12013-07-15 11:52:23 -0700168 build = item.current_build_set.getBuild(job.name)
169 if not build or not build.start_time:
170 return False
171 return True
172
James E. Blairfee8d652013-06-07 08:57:52 -0700173 def areAllJobsComplete(self, item):
James E. Blair107c3852015-02-07 08:23:10 -0800174 for job in self.getJobs(item):
James E. Blairfee8d652013-06-07 08:57:52 -0700175 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700176 if not build or not build.result:
177 return False
178 return True
179
James E. Blairfee8d652013-06-07 08:57:52 -0700180 def didAllJobsSucceed(self, item):
James E. Blair107c3852015-02-07 08:23:10 -0800181 for job in self.getJobs(item):
James E. Blair4ec821f2012-08-23 15:28:28 -0700182 if not job.voting:
183 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700184 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700185 if not build:
186 return False
187 if build.result != 'SUCCESS':
188 return False
189 return True
190
Joshua Heskethb7179772014-01-30 23:30:46 +1100191 def didMergerSucceed(self, item):
192 if item.current_build_set.unable_to_merge:
193 return False
194 return True
195
James E. Blairfee8d652013-06-07 08:57:52 -0700196 def didAnyJobFail(self, item):
James E. Blair107c3852015-02-07 08:23:10 -0800197 for job in self.getJobs(item):
James E. Blair4ec821f2012-08-23 15:28:28 -0700198 if not job.voting:
199 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700200 build = item.current_build_set.getBuild(job.name)
James E. Blair0018a6c2013-02-27 14:11:45 -0800201 if build and build.result and (build.result != 'SUCCESS'):
James E. Blair4aea70c2012-07-26 14:23:24 -0700202 return True
203 return False
204
James E. Blairfee8d652013-06-07 08:57:52 -0700205 def isHoldingFollowingChanges(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800206 if not item.live:
207 return False
James E. Blair107c3852015-02-07 08:23:10 -0800208 for job in self.getJobs(item):
James E. Blair4aea70c2012-07-26 14:23:24 -0700209 if not job.hold_following_changes:
210 continue
James E. Blairfee8d652013-06-07 08:57:52 -0700211 build = item.current_build_set.getBuild(job.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700212 if not build:
213 return True
214 if build.result != 'SUCCESS':
215 return True
James E. Blair972e3c72013-08-29 12:04:55 -0700216
James E. Blairfee8d652013-06-07 08:57:52 -0700217 if not item.item_ahead:
James E. Blair4aea70c2012-07-26 14:23:24 -0700218 return False
James E. Blairfee8d652013-06-07 08:57:52 -0700219 return self.isHoldingFollowingChanges(item.item_ahead)
James E. Blair4aea70c2012-07-26 14:23:24 -0700220
James E. Blairfee8d652013-06-07 08:57:52 -0700221 def setResult(self, item, build):
James E. Blair4a28a882013-08-23 15:17:33 -0700222 if build.retry:
223 item.removeBuild(build)
224 elif build.result != 'SUCCESS':
James E. Blair4aea70c2012-07-26 14:23:24 -0700225 # Get a JobTree from a Job so we can find only its dependent jobs
James E. Blairfee8d652013-06-07 08:57:52 -0700226 root = self.getJobTree(item.change.project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700227 tree = root.getJobTreeForJob(build.job)
228 for job in tree.getJobs():
229 fakebuild = Build(job, None)
230 fakebuild.result = 'SKIPPED'
James E. Blairfee8d652013-06-07 08:57:52 -0700231 item.addBuild(fakebuild)
James E. Blair4aea70c2012-07-26 14:23:24 -0700232
Joshua Heskethb7179772014-01-30 23:30:46 +1100233 def setUnableToMerge(self, item):
James E. Blairfee8d652013-06-07 08:57:52 -0700234 item.current_build_set.unable_to_merge = True
235 root = self.getJobTree(item.change.project)
James E. Blair973721f2012-08-15 10:19:43 -0700236 for job in root.getJobs():
237 fakebuild = Build(job, None)
238 fakebuild.result = 'SKIPPED'
James E. Blairfee8d652013-06-07 08:57:52 -0700239 item.addBuild(fakebuild)
James E. Blair973721f2012-08-15 10:19:43 -0700240
James E. Blairfee8d652013-06-07 08:57:52 -0700241 def setDequeuedNeedingChange(self, item):
242 item.dequeued_needing_change = True
243 root = self.getJobTree(item.change.project)
James E. Blaircaec0c52012-08-22 14:52:22 -0700244 for job in root.getJobs():
245 fakebuild = Build(job, None)
246 fakebuild.result = 'SKIPPED'
James E. Blairfee8d652013-06-07 08:57:52 -0700247 item.addBuild(fakebuild)
James E. Blaircaec0c52012-08-22 14:52:22 -0700248
James E. Blaire0487072012-08-29 17:38:31 -0700249 def getChangesInQueue(self):
250 changes = []
251 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700252 changes.extend([x.change for x in shared_queue.queue])
James E. Blaire0487072012-08-29 17:38:31 -0700253 return changes
254
James E. Blairfee8d652013-06-07 08:57:52 -0700255 def getAllItems(self):
256 items = []
James E. Blaire0487072012-08-29 17:38:31 -0700257 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700258 items.extend(shared_queue.queue)
James E. Blairfee8d652013-06-07 08:57:52 -0700259 return items
James E. Blaire0487072012-08-29 17:38:31 -0700260
James E. Blair8dbd56a2012-12-22 10:55:10 -0800261 def formatStatusJSON(self):
262 j_pipeline = dict(name=self.name,
263 description=self.description)
264 j_queues = []
265 j_pipeline['change_queues'] = j_queues
266 for queue in self.queues:
267 j_queue = dict(name=queue.name)
268 j_queues.append(j_queue)
269 j_queue['heads'] = []
Clark Boylanaf2476f2014-01-23 14:47:36 -0800270 j_queue['window'] = queue.window
James E. Blair972e3c72013-08-29 12:04:55 -0700271
272 j_changes = []
273 for e in queue.queue:
274 if not e.item_ahead:
275 if j_changes:
276 j_queue['heads'].append(j_changes)
277 j_changes = []
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800278 j_changes.append(e.formatJSON())
James E. Blair972e3c72013-08-29 12:04:55 -0700279 if (len(j_changes) > 1 and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000280 (j_changes[-2]['remaining_time'] is not None) and
281 (j_changes[-1]['remaining_time'] is not None)):
James E. Blair972e3c72013-08-29 12:04:55 -0700282 j_changes[-1]['remaining_time'] = max(
283 j_changes[-2]['remaining_time'],
284 j_changes[-1]['remaining_time'])
285 if j_changes:
James E. Blair8dbd56a2012-12-22 10:55:10 -0800286 j_queue['heads'].append(j_changes)
287 return j_pipeline
288
James E. Blair4aea70c2012-07-26 14:23:24 -0700289
Joshua Hesketh1879cf72013-08-19 14:13:15 +1000290class ActionReporter(object):
Alex Gaynor813d39b2014-05-17 16:17:16 -0700291 """An ActionReporter has a reporter and its configured parameters"""
Joshua Hesketh1879cf72013-08-19 14:13:15 +1000292
293 def __repr__(self):
294 return '<ActionReporter %s, %s>' % (self.reporter, self.params)
295
296 def __init__(self, reporter, params):
297 self.reporter = reporter
298 self.params = params
299
300 def report(self, change, message):
301 """Sends the built message off to the configured reporter.
302 Takes the change and message and adds the configured parameters.
303 """
304 return self.reporter.report(change, message, self.params)
305
306 def getSubmitAllowNeeds(self):
307 """Gets the submit allow needs from the reporter based off the
308 parameters."""
309 return self.reporter.getSubmitAllowNeeds(self.params)
310
311
James E. Blairee743612012-05-29 14:49:32 -0700312class ChangeQueue(object):
James E. Blair4aea70c2012-07-26 14:23:24 -0700313 """DependentPipelines have multiple parallel queues shared by
314 different projects; this is one of them. For instance, there may
315 a queue shared by interrelated projects foo and bar, and a second
316 queue for independent project baz. Pipelines have one or more
James E. Blairbfb8e042014-12-30 17:01:44 -0800317 ChangeQueues."""
318 def __init__(self, pipeline, window=0, window_floor=1,
Clark Boylan7603a372014-01-21 11:43:20 -0800319 window_increase_type='linear', window_increase_factor=1,
320 window_decrease_type='exponential', window_decrease_factor=2):
James E. Blair4aea70c2012-07-26 14:23:24 -0700321 self.pipeline = pipeline
James E. Blairee743612012-05-29 14:49:32 -0700322 self.name = ''
James E. Blairc8a1e052014-02-25 09:29:26 -0800323 self.assigned_name = None
324 self.generated_name = None
James E. Blairee743612012-05-29 14:49:32 -0700325 self.projects = []
326 self._jobs = set()
327 self.queue = []
Clark Boylan7603a372014-01-21 11:43:20 -0800328 self.window = window
329 self.window_floor = window_floor
330 self.window_increase_type = window_increase_type
331 self.window_increase_factor = window_increase_factor
332 self.window_decrease_type = window_decrease_type
333 self.window_decrease_factor = window_decrease_factor
James E. Blairee743612012-05-29 14:49:32 -0700334
James E. Blair9f9667e2012-06-12 17:51:08 -0700335 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700336 return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
James E. Blairee743612012-05-29 14:49:32 -0700337
338 def getJobs(self):
339 return self._jobs
340
341 def addProject(self, project):
342 if project not in self.projects:
343 self.projects.append(project)
James E. Blairc8a1e052014-02-25 09:29:26 -0800344 self._jobs |= set(self.pipeline.getJobTree(project).getJobs())
345
James E. Blairee743612012-05-29 14:49:32 -0700346 names = [x.name for x in self.projects]
347 names.sort()
James E. Blairc8a1e052014-02-25 09:29:26 -0800348 self.generated_name = ', '.join(names)
349
350 for job in self._jobs:
351 if job.queue_name:
352 if (self.assigned_name and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000353 job.queue_name != self.assigned_name):
James E. Blairc8a1e052014-02-25 09:29:26 -0800354 raise Exception("More than one name assigned to "
355 "change queue: %s != %s" %
356 (self.assigned_name, job.queue_name))
357 self.assigned_name = job.queue_name
358 self.name = self.assigned_name or self.generated_name
James E. Blairee743612012-05-29 14:49:32 -0700359
360 def enqueueChange(self, change):
James E. Blairbfb8e042014-12-30 17:01:44 -0800361 item = QueueItem(self, change)
James E. Blaircdccd972013-07-01 12:10:22 -0700362 self.enqueueItem(item)
363 item.enqueue_time = time.time()
364 return item
365
366 def enqueueItem(self, item):
James E. Blair4a035d92014-01-23 13:10:48 -0800367 item.pipeline = self.pipeline
James E. Blairbfb8e042014-12-30 17:01:44 -0800368 item.queue = self
369 if self.queue:
James E. Blairfee8d652013-06-07 08:57:52 -0700370 item.item_ahead = self.queue[-1]
James E. Blair972e3c72013-08-29 12:04:55 -0700371 item.item_ahead.items_behind.append(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700372 self.queue.append(item)
James E. Blairee743612012-05-29 14:49:32 -0700373
James E. Blairfee8d652013-06-07 08:57:52 -0700374 def dequeueItem(self, item):
375 if item in self.queue:
376 self.queue.remove(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700377 if item.item_ahead:
James E. Blair972e3c72013-08-29 12:04:55 -0700378 item.item_ahead.items_behind.remove(item)
379 for item_behind in item.items_behind:
380 if item.item_ahead:
381 item.item_ahead.items_behind.append(item_behind)
382 item_behind.item_ahead = item.item_ahead
James E. Blairfee8d652013-06-07 08:57:52 -0700383 item.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700384 item.items_behind = []
James E. Blairfee8d652013-06-07 08:57:52 -0700385 item.dequeue_time = time.time()
James E. Blaire0487072012-08-29 17:38:31 -0700386
James E. Blair972e3c72013-08-29 12:04:55 -0700387 def moveItem(self, item, item_ahead):
James E. Blair972e3c72013-08-29 12:04:55 -0700388 if item.item_ahead == item_ahead:
389 return False
390 # Remove from current location
391 if item.item_ahead:
392 item.item_ahead.items_behind.remove(item)
393 for item_behind in item.items_behind:
394 if item.item_ahead:
395 item.item_ahead.items_behind.append(item_behind)
396 item_behind.item_ahead = item.item_ahead
397 # Add to new location
398 item.item_ahead = item_ahead
James E. Blair00451262013-09-20 11:40:17 -0700399 item.items_behind = []
James E. Blair972e3c72013-08-29 12:04:55 -0700400 if item.item_ahead:
401 item.item_ahead.items_behind.append(item)
402 return True
James E. Blairee743612012-05-29 14:49:32 -0700403
404 def mergeChangeQueue(self, other):
405 for project in other.projects:
406 self.addProject(project)
Clark Boylan7603a372014-01-21 11:43:20 -0800407 self.window = min(self.window, other.window)
408 # TODO merge semantics
409
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800410 def isActionable(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800411 if self.window:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800412 return item in self.queue[:self.window]
Clark Boylan7603a372014-01-21 11:43:20 -0800413 else:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800414 return True
Clark Boylan7603a372014-01-21 11:43:20 -0800415
416 def increaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800417 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800418 if self.window_increase_type == 'linear':
419 self.window += self.window_increase_factor
420 elif self.window_increase_type == 'exponential':
421 self.window *= self.window_increase_factor
422
423 def decreaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800424 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800425 if self.window_decrease_type == 'linear':
426 self.window = max(
427 self.window_floor,
428 self.window - self.window_decrease_factor)
429 elif self.window_decrease_type == 'exponential':
430 self.window = max(
431 self.window_floor,
432 self.window / self.window_decrease_factor)
James E. Blairee743612012-05-29 14:49:32 -0700433
James E. Blair1e8dd892012-05-30 09:15:05 -0700434
James E. Blair4aea70c2012-07-26 14:23:24 -0700435class Project(object):
436 def __init__(self, name):
437 self.name = name
James E. Blair19deff22013-08-25 13:17:35 -0700438 self.merge_mode = MERGER_MERGE_RESOLVE
James E. Blair4aea70c2012-07-26 14:23:24 -0700439
440 def __str__(self):
441 return self.name
442
443 def __repr__(self):
444 return '<Project %s>' % (self.name)
445
446
James E. Blairee743612012-05-29 14:49:32 -0700447class Job(object):
448 def __init__(self, name):
James E. Blair222d4982012-07-16 09:31:19 -0700449 # If you add attributes here, be sure to add them to the copy method.
James E. Blairee743612012-05-29 14:49:32 -0700450 self.name = name
James E. Blairc8a1e052014-02-25 09:29:26 -0800451 self.queue_name = None
James E. Blairee743612012-05-29 14:49:32 -0700452 self.failure_message = None
453 self.success_message = None
James E. Blair6aea36d2012-12-17 13:03:24 -0800454 self.failure_pattern = None
455 self.success_pattern = None
James E. Blaire5a847f2012-07-10 15:29:14 -0700456 self.parameter_function = None
Maru Newby79427a42015-02-17 17:54:45 +0000457 # A metajob should only supply values for attributes that have
458 # been explicitly provided, so avoid setting boolean defaults.
459 if self.is_metajob:
460 self.hold_following_changes = None
461 self.voting = None
462 else:
463 self.hold_following_changes = False
464 self.voting = True
James E. Blaire421a232012-07-25 16:59:21 -0700465 self.branches = []
466 self._branches = []
James E. Blair70c71582013-03-06 08:50:50 -0800467 self.files = []
468 self._files = []
Maru Newby3fe5f852015-01-13 04:22:14 +0000469 self.skip_if_matcher = None
Joshua Hesketh36c3fa52014-01-22 11:40:52 +1100470 self.swift = {}
James E. Blairee743612012-05-29 14:49:32 -0700471
472 def __str__(self):
473 return self.name
474
475 def __repr__(self):
476 return '<Job %s>' % (self.name)
477
Maru Newby79427a42015-02-17 17:54:45 +0000478 @property
479 def is_metajob(self):
480 return self.name.startswith('^')
481
James E. Blairb0954652012-06-01 11:32:01 -0700482 def copy(self, other):
James E. Blairc28d1b02013-07-19 11:37:06 -0700483 if other.failure_message:
484 self.failure_message = other.failure_message
485 if other.success_message:
486 self.success_message = other.success_message
487 if other.failure_pattern:
488 self.failure_pattern = other.failure_pattern
489 if other.success_pattern:
490 self.success_pattern = other.success_pattern
491 if other.parameter_function:
492 self.parameter_function = other.parameter_function
493 if other.branches:
494 self.branches = other.branches[:]
495 self._branches = other._branches[:]
496 if other.files:
497 self.files = other.files[:]
498 self._files = other._files[:]
Maru Newby3fe5f852015-01-13 04:22:14 +0000499 if other.skip_if_matcher:
500 self.skip_if_matcher = other.skip_if_matcher.copy()
Joshua Hesketh36c3fa52014-01-22 11:40:52 +1100501 if other.swift:
502 self.swift.update(other.swift)
Maru Newby79427a42015-02-17 17:54:45 +0000503 # Only non-None values should be copied for boolean attributes.
504 if other.hold_following_changes is not None:
505 self.hold_following_changes = other.hold_following_changes
506 if other.voting is not None:
507 self.voting = other.voting
James E. Blairb0954652012-06-01 11:32:01 -0700508
James E. Blaire421a232012-07-25 16:59:21 -0700509 def changeMatches(self, change):
James E. Blair70c71582013-03-06 08:50:50 -0800510 matches_branch = False
James E. Blaire421a232012-07-25 16:59:21 -0700511 for branch in self.branches:
James E. Blair45865f32012-10-05 09:39:46 -0700512 if hasattr(change, 'branch') and branch.match(change.branch):
James E. Blair70c71582013-03-06 08:50:50 -0800513 matches_branch = True
James E. Blair45865f32012-10-05 09:39:46 -0700514 if hasattr(change, 'ref') and branch.match(change.ref):
James E. Blair70c71582013-03-06 08:50:50 -0800515 matches_branch = True
516 if self.branches and not matches_branch:
517 return False
518
519 matches_file = False
520 for f in self.files:
521 if hasattr(change, 'files'):
522 for cf in change.files:
523 if f.match(cf):
524 matches_file = True
525 if self.files and not matches_file:
526 return False
527
Maru Newby3fe5f852015-01-13 04:22:14 +0000528 if self.skip_if_matcher and self.skip_if_matcher.matches(change):
529 return False
530
James E. Blair70c71582013-03-06 08:50:50 -0800531 return True
James E. Blaire5a847f2012-07-10 15:29:14 -0700532
James E. Blair1e8dd892012-05-30 09:15:05 -0700533
James E. Blairee743612012-05-29 14:49:32 -0700534class JobTree(object):
535 """ A JobTree represents an instance of one Job, and holds JobTrees
536 whose jobs should be run if that Job succeeds. A root node of a
537 JobTree will have no associated Job. """
538
539 def __init__(self, job):
540 self.job = job
541 self.job_trees = []
542
543 def addJob(self, job):
James E. Blair12a92b12014-03-26 11:54:53 -0700544 if job not in [x.job for x in self.job_trees]:
545 t = JobTree(job)
546 self.job_trees.append(t)
547 return t
James E. Blairee743612012-05-29 14:49:32 -0700548
549 def getJobs(self):
550 jobs = []
551 for x in self.job_trees:
552 jobs.append(x.job)
553 jobs.extend(x.getJobs())
554 return jobs
555
556 def getJobTreeForJob(self, job):
557 if self.job == job:
558 return self
559 for tree in self.job_trees:
560 ret = tree.getJobTreeForJob(job)
561 if ret:
562 return ret
563 return None
564
James E. Blair1e8dd892012-05-30 09:15:05 -0700565
James E. Blair4aea70c2012-07-26 14:23:24 -0700566class Build(object):
567 def __init__(self, job, uuid):
568 self.job = job
569 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -0700570 self.url = None
571 self.number = None
572 self.result = None
573 self.build_set = None
574 self.launch_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -0800575 self.start_time = None
576 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -0700577 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -0700578 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -0700579 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -0700580 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -0700581 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +0800582 self.worker = Worker()
James E. Blairee743612012-05-29 14:49:32 -0700583
584 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +0800585 return ('<Build %s of %s on %s>' %
586 (self.uuid, self.job.name, self.worker))
587
588
589class Worker(object):
590 """A model of the worker running a job"""
591 def __init__(self):
592 self.name = "Unknown"
593 self.hostname = None
594 self.ips = []
595 self.fqdn = None
596 self.program = None
597 self.version = None
598 self.extra = {}
599
600 def updateFromData(self, data):
601 """Update worker information if contained in the WORK_DATA response."""
602 self.name = data.get('worker_name', self.name)
603 self.hostname = data.get('worker_hostname', self.hostname)
604 self.ips = data.get('worker_ips', self.ips)
605 self.fqdn = data.get('worker_fqdn', self.fqdn)
606 self.program = data.get('worker_program', self.program)
607 self.version = data.get('worker_version', self.version)
608 self.extra = data.get('worker_extra', self.extra)
609
610 def __repr__(self):
611 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -0700612
James E. Blair1e8dd892012-05-30 09:15:05 -0700613
James E. Blair7e530ad2012-07-03 16:12:28 -0700614class BuildSet(object):
James E. Blair4076e2b2014-01-28 12:42:20 -0800615 # Merge states:
616 NEW = 1
617 PENDING = 2
618 COMPLETE = 3
619
Antoine Musso9b229282014-08-18 23:45:43 +0200620 states_map = {
621 1: 'NEW',
622 2: 'PENDING',
623 3: 'COMPLETE',
624 }
625
James E. Blairfee8d652013-06-07 08:57:52 -0700626 def __init__(self, item):
627 self.item = item
James E. Blair11700c32012-07-05 17:50:05 -0700628 self.other_changes = []
James E. Blair7e530ad2012-07-03 16:12:28 -0700629 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -0700630 self.result = None
631 self.next_build_set = None
632 self.previous_build_set = None
James E. Blair4886cc12012-07-18 15:39:41 -0700633 self.ref = None
James E. Blair81515ad2012-10-01 18:29:08 -0700634 self.commit = None
James E. Blair4076e2b2014-01-28 12:42:20 -0800635 self.zuul_url = None
James E. Blair973721f2012-08-15 10:19:43 -0700636 self.unable_to_merge = False
James E. Blair972e3c72013-08-29 12:04:55 -0700637 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -0800638 self.merge_state = self.NEW
James E. Blair7e530ad2012-07-03 16:12:28 -0700639
Antoine Musso9b229282014-08-18 23:45:43 +0200640 def __repr__(self):
641 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
642 self.item,
643 len(self.builds),
644 self.getStateName(self.merge_state))
645
James E. Blair4886cc12012-07-18 15:39:41 -0700646 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -0700647 # The change isn't enqueued until after it's created
648 # so we don't know what the other changes ahead will be
649 # until jobs start.
650 if not self.other_changes:
James E. Blairfee8d652013-06-07 08:57:52 -0700651 next_item = self.item.item_ahead
652 while next_item:
653 self.other_changes.append(next_item.change)
654 next_item = next_item.item_ahead
James E. Blair4886cc12012-07-18 15:39:41 -0700655 if not self.ref:
656 self.ref = 'Z' + uuid4().hex
657
Antoine Musso9b229282014-08-18 23:45:43 +0200658 def getStateName(self, state_num):
659 return self.states_map.get(
660 state_num, 'UNKNOWN (%s)' % state_num)
661
James E. Blair4886cc12012-07-18 15:39:41 -0700662 def addBuild(self, build):
663 self.builds[build.job.name] = build
664 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -0700665
James E. Blair4a28a882013-08-23 15:17:33 -0700666 def removeBuild(self, build):
667 del self.builds[build.job.name]
668
James E. Blair7e530ad2012-07-03 16:12:28 -0700669 def getBuild(self, job_name):
670 return self.builds.get(job_name)
671
James E. Blair11700c32012-07-05 17:50:05 -0700672 def getBuilds(self):
673 keys = self.builds.keys()
674 keys.sort()
675 return [self.builds.get(x) for x in keys]
676
James E. Blair7e530ad2012-07-03 16:12:28 -0700677
James E. Blairfee8d652013-06-07 08:57:52 -0700678class QueueItem(object):
679 """A changish inside of a Pipeline queue"""
James E. Blair32663402012-06-01 10:04:18 -0700680
James E. Blairbfb8e042014-12-30 17:01:44 -0800681 def __init__(self, queue, change):
682 self.pipeline = queue.pipeline
683 self.queue = queue
James E. Blairfee8d652013-06-07 08:57:52 -0700684 self.change = change # a changeish
James E. Blair7e530ad2012-07-03 16:12:28 -0700685 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -0700686 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -0700687 self.current_build_set = BuildSet(self)
688 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -0700689 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700690 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -0800691 self.enqueue_time = None
692 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -0700693 self.reported = False
James E. Blairbfb8e042014-12-30 17:01:44 -0800694 self.active = False # Whether an item is within an active window
695 self.live = True # Whether an item is intended to be processed at all
James E. Blaire5a847f2012-07-10 15:29:14 -0700696
James E. Blair972e3c72013-08-29 12:04:55 -0700697 def __repr__(self):
698 if self.pipeline:
699 pipeline = self.pipeline.name
700 else:
701 pipeline = None
702 return '<QueueItem 0x%x for %s in %s>' % (
703 id(self), self.change, pipeline)
704
James E. Blairee743612012-05-29 14:49:32 -0700705 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -0700706 old = self.current_build_set
707 self.current_build_set.result = 'CANCELED'
708 self.current_build_set = BuildSet(self)
709 old.next_build_set = self.current_build_set
710 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -0700711 self.build_sets.append(self.current_build_set)
James E. Blairee743612012-05-29 14:49:32 -0700712
713 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -0700714 self.current_build_set.addBuild(build)
James E. Blair66eeebf2013-07-27 17:44:32 -0700715 build.pipeline = self.pipeline
James E. Blairee743612012-05-29 14:49:32 -0700716
James E. Blair4a28a882013-08-23 15:17:33 -0700717 def removeBuild(self, build):
718 self.current_build_set.removeBuild(build)
719
James E. Blairfee8d652013-06-07 08:57:52 -0700720 def setReportedResult(self, result):
721 self.current_build_set.result = result
722
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800723 def formatJSON(self):
724 changeish = self.change
725 ret = {}
726 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -0800727 ret['live'] = self.live
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800728 if hasattr(changeish, 'url') and changeish.url is not None:
729 ret['url'] = changeish.url
730 else:
731 ret['url'] = None
732 ret['id'] = changeish._id()
733 if self.item_ahead:
734 ret['item_ahead'] = self.item_ahead.change._id()
735 else:
736 ret['item_ahead'] = None
737 ret['items_behind'] = [i.change._id() for i in self.items_behind]
738 ret['failing_reasons'] = self.current_build_set.failing_reasons
739 ret['zuul_ref'] = self.current_build_set.ref
Ramy Asselin07cc33c2015-06-12 14:06:34 -0700740 if changeish.project:
741 ret['project'] = changeish.project.name
742 else:
743 # For cross-project dependencies with the depends-on
744 # project not known to zuul, the project is None
745 # Set it to a static value
746 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800747 ret['enqueue_time'] = int(self.enqueue_time * 1000)
748 ret['jobs'] = []
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -0500749 if hasattr(changeish, 'owner'):
750 ret['owner'] = changeish.owner
751 else:
752 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800753 max_remaining = 0
James E. Blair107c3852015-02-07 08:23:10 -0800754 for job in self.pipeline.getJobs(self):
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800755 now = time.time()
756 build = self.current_build_set.getBuild(job.name)
757 elapsed = None
758 remaining = None
759 result = None
760 url = None
761 worker = None
762 if build:
763 result = build.result
764 url = build.url
765 if build.start_time:
766 if build.end_time:
767 elapsed = int((build.end_time -
768 build.start_time) * 1000)
769 remaining = 0
770 else:
771 elapsed = int((now - build.start_time) * 1000)
772 if build.estimated_time:
773 remaining = max(
774 int(build.estimated_time * 1000) - elapsed,
775 0)
776 worker = {
777 'name': build.worker.name,
778 'hostname': build.worker.hostname,
779 'ips': build.worker.ips,
780 'fqdn': build.worker.fqdn,
781 'program': build.worker.program,
782 'version': build.worker.version,
783 'extra': build.worker.extra
784 }
785 if remaining and remaining > max_remaining:
786 max_remaining = remaining
787
788 ret['jobs'].append({
789 'name': job.name,
790 'elapsed_time': elapsed,
791 'remaining_time': remaining,
792 'url': url,
793 'result': result,
794 'voting': job.voting,
795 'uuid': build.uuid if build else None,
796 'launch_time': build.launch_time if build else None,
797 'start_time': build.start_time if build else None,
798 'end_time': build.end_time if build else None,
799 'estimated_time': build.estimated_time if build else None,
800 'pipeline': build.pipeline.name if build else None,
801 'canceled': build.canceled if build else None,
802 'retry': build.retry if build else None,
803 'number': build.number if build else None,
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800804 'worker': worker
805 })
806
807 if self.pipeline.haveAllJobsStarted(self):
808 ret['remaining_time'] = max_remaining
809 else:
810 ret['remaining_time'] = None
811 return ret
812
813 def formatStatus(self, indent=0, html=False):
814 changeish = self.change
815 indent_str = ' ' * indent
816 ret = ''
817 if html and hasattr(changeish, 'url') and changeish.url is not None:
818 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
819 indent_str,
820 changeish.project.name,
821 changeish.url,
822 changeish._id())
823 else:
824 ret += '%sProject %s change %s based on %s\n' % (
825 indent_str,
826 changeish.project.name,
827 changeish._id(),
828 self.item_ahead)
James E. Blair107c3852015-02-07 08:23:10 -0800829 for job in self.pipeline.getJobs(self):
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800830 build = self.current_build_set.getBuild(job.name)
831 if build:
832 result = build.result
833 else:
834 result = None
835 job_name = job.name
836 if not job.voting:
837 voting = ' (non-voting)'
838 else:
839 voting = ''
840 if html:
841 if build:
842 url = build.url
843 else:
844 url = None
845 if url is not None:
846 job_name = '<a href="%s">%s</a>' % (url, job_name)
847 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
848 ret += '\n'
849 return ret
850
James E. Blairfee8d652013-06-07 08:57:52 -0700851
852class Changeish(object):
853 """Something like a change; either a change or a ref"""
James E. Blairfee8d652013-06-07 08:57:52 -0700854
855 def __init__(self, project):
856 self.project = project
857
Joshua Hesketh36c3fa52014-01-22 11:40:52 +1100858 def getBasePath(self):
859 base_path = ''
860 if hasattr(self, 'refspec'):
861 base_path = "%s/%s/%s" % (
862 self.number[-2:], self.number, self.patchset)
863 elif hasattr(self, 'ref'):
864 base_path = "%s/%s" % (self.newrev[:2], self.newrev)
865
866 return base_path
867
James E. Blairfee8d652013-06-07 08:57:52 -0700868 def equals(self, other):
869 raise NotImplementedError()
870
871 def isUpdateOf(self, other):
872 raise NotImplementedError()
873
874 def filterJobs(self, jobs):
875 return filter(lambda job: job.changeMatches(self), jobs)
876
877 def getRelatedChanges(self):
878 return set()
879
James E. Blair1e8dd892012-05-30 09:15:05 -0700880
James E. Blair4aea70c2012-07-26 14:23:24 -0700881class Change(Changeish):
James E. Blair4aea70c2012-07-26 14:23:24 -0700882 def __init__(self, project):
883 super(Change, self).__init__(project)
884 self.branch = None
885 self.number = None
886 self.url = None
887 self.patchset = None
888 self.refspec = None
889
James E. Blair70c71582013-03-06 08:50:50 -0800890 self.files = []
James E. Blair6965a4b2014-12-16 17:19:04 -0800891 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -0700892 self.needed_by_changes = []
893 self.is_current_patchset = True
894 self.can_merge = False
895 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -0700896 self.failed_to_merge = False
James E. Blairc053d022014-01-22 14:57:33 -0800897 self.approvals = []
James E. Blair11041d22014-05-02 14:49:53 -0700898 self.open = None
899 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -0500900 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700901
902 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -0700903 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -0700904
905 def __repr__(self):
906 return '<Change 0x%x %s>' % (id(self), self._id())
907
908 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +0800909 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -0700910 return True
911 return False
912
James E. Blair2fa50962013-01-30 21:50:41 -0800913 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -0800914 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -0700915 (hasattr(other, 'patchset') and
916 self.patchset is not None and
917 other.patchset is not None and
918 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -0800919 return True
920 return False
921
James E. Blairfee8d652013-06-07 08:57:52 -0700922 def getRelatedChanges(self):
923 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -0800924 for c in self.needs_changes:
925 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -0700926 for c in self.needed_by_changes:
927 related.add(c)
928 related.update(c.getRelatedChanges())
929 return related
James E. Blair4aea70c2012-07-26 14:23:24 -0700930
931
932class Ref(Changeish):
James E. Blair4aea70c2012-07-26 14:23:24 -0700933 def __init__(self, project):
James E. Blairbe765db2012-08-07 08:36:20 -0700934 super(Ref, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700935 self.ref = None
936 self.oldrev = None
937 self.newrev = None
938
James E. Blairbe765db2012-08-07 08:36:20 -0700939 def _id(self):
940 return self.newrev
941
Antoine Musso68bdcd72013-01-17 12:31:28 +0100942 def __repr__(self):
943 rep = None
944 if self.newrev == '0000000000000000000000000000000000000000':
945 rep = '<Ref 0x%x deletes %s from %s' % (
946 id(self), self.ref, self.oldrev)
947 elif self.oldrev == '0000000000000000000000000000000000000000':
948 rep = '<Ref 0x%x creates %s on %s>' % (
949 id(self), self.ref, self.newrev)
950 else:
951 # Catch all
952 rep = '<Ref 0x%x %s updated %s..%s>' % (
953 id(self), self.ref, self.oldrev, self.newrev)
954
955 return rep
956
James E. Blair4aea70c2012-07-26 14:23:24 -0700957 def equals(self, other):
James E. Blair9358c612012-09-28 08:29:39 -0700958 if (self.project == other.project
959 and self.ref == other.ref
960 and self.newrev == other.newrev):
James E. Blair4aea70c2012-07-26 14:23:24 -0700961 return True
962 return False
963
James E. Blair2fa50962013-01-30 21:50:41 -0800964 def isUpdateOf(self, other):
965 return False
966
James E. Blair4aea70c2012-07-26 14:23:24 -0700967
James E. Blair63bb0ef2013-07-29 17:14:51 -0700968class NullChange(Changeish):
James E. Blaire5910202013-12-27 09:50:31 -0800969 def __repr__(self):
970 return '<NullChange for %s>' % (self.project)
James E. Blair63bb0ef2013-07-29 17:14:51 -0700971
James E. Blair63bb0ef2013-07-29 17:14:51 -0700972 def _id(self):
Alex Gaynorddb9ef32013-09-16 21:04:58 -0700973 return None
James E. Blair63bb0ef2013-07-29 17:14:51 -0700974
975 def equals(self, other):
Steve Varnau7b78b312015-04-03 14:49:46 -0700976 if (self.project == other.project
977 and other._id() is None):
James E. Blair4f6033c2014-03-27 15:49:09 -0700978 return True
James E. Blair63bb0ef2013-07-29 17:14:51 -0700979 return False
980
981 def isUpdateOf(self, other):
982 return False
983
984
James E. Blairee743612012-05-29 14:49:32 -0700985class TriggerEvent(object):
986 def __init__(self):
987 self.data = None
James E. Blair32663402012-06-01 10:04:18 -0700988 # common
James E. Blairee743612012-05-29 14:49:32 -0700989 self.type = None
990 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -0700991 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +0100992 # Representation of the user account that performed the event.
993 self.account = None
James E. Blair32663402012-06-01 10:04:18 -0700994 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -0700995 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -0700996 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -0700997 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -0700998 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -0700999 self.approvals = []
1000 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07001001 self.comment = None
James E. Blair32663402012-06-01 10:04:18 -07001002 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -07001003 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -07001004 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07001005 self.newrev = None
James E. Blair63bb0ef2013-07-29 17:14:51 -07001006 # timer
1007 self.timespec = None
James E. Blairc494d542014-08-06 09:23:52 -07001008 # zuultrigger
1009 self.pipeline_name = None
James E. Blairad28e912013-11-27 10:43:22 -08001010 # For events that arrive with a destination pipeline (eg, from
1011 # an admin command, etc):
1012 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07001013
James E. Blair9f9667e2012-06-12 17:51:08 -07001014 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001015 ret = '<TriggerEvent %s %s' % (self.type, self.project_name)
James E. Blair1e8dd892012-05-30 09:15:05 -07001016
James E. Blairee743612012-05-29 14:49:32 -07001017 if self.branch:
1018 ret += " %s" % self.branch
1019 if self.change_number:
1020 ret += " %s,%s" % (self.change_number, self.patch_number)
1021 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -07001022 ret += ' ' + ', '.join(
1023 ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
James E. Blairee743612012-05-29 14:49:32 -07001024 ret += '>'
1025
1026 return ret
1027
James E. Blair1e8dd892012-05-30 09:15:05 -07001028
James E. Blair9c17dbf2014-06-23 14:21:58 -07001029class BaseFilter(object):
Joshua Heskethb2068e82014-06-26 15:30:08 +10001030 def __init__(self, required_any_approval=[], required_all_approvals=[]):
1031 self._required_any_approval = copy.deepcopy(required_any_approval)
1032 self.required_any_approval = self._tidy_approvals(
1033 required_any_approval)
1034 self._required_all_approvals = copy.deepcopy(required_all_approvals)
1035 self.required_all_approvals = self._tidy_approvals(
1036 required_all_approvals)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001037
Joshua Heskethb2068e82014-06-26 15:30:08 +10001038 def _tidy_approvals(self, approvals):
1039 for a in approvals:
James E. Blair9c17dbf2014-06-23 14:21:58 -07001040 for k, v in a.items():
1041 if k == 'username':
1042 pass
James E. Blair1fbfceb2014-06-23 14:42:53 -07001043 elif k in ['email', 'email-filter']:
Joshua Heskethb2068e82014-06-26 15:30:08 +10001044 a['email'] = v
James E. Blair9c17dbf2014-06-23 14:21:58 -07001045 elif k == 'newer-than':
Joshua Heskethb2068e82014-06-26 15:30:08 +10001046 a[k] = v
James E. Blair9c17dbf2014-06-23 14:21:58 -07001047 elif k == 'older-than':
Joshua Heskethb2068e82014-06-26 15:30:08 +10001048 a[k] = v
James E. Blair1fbfceb2014-06-23 14:42:53 -07001049 if 'email-filter' in a:
1050 del a['email-filter']
Joshua Heskethb2068e82014-06-26 15:30:08 +10001051 return approvals
1052
1053 def _match_approval_required_approval(self, rapproval, approval):
1054 # Check if the required approval and approval match
1055 if 'description' not in approval:
1056 return False
1057 now = time.time()
1058 found_approval = True
1059 by = approval.get('by', {})
1060 for k, v in rapproval.items():
1061 negative_match = False
1062 item_match = True
1063 if isinstance(v, six.string_types) and v[0] == '!':
1064 v = v[1:].strip()
1065 item_match = False
1066 negative_match = True
1067
1068 if k == 'username':
1069 if (by.get('username', '') != v):
1070 item_match = negative_match
1071 elif k == 'email':
1072 v = re.compile(v)
1073 if (not v.search(by.get('email', ''))):
1074 item_match = negative_match
1075 elif k == 'newer-than':
1076 t = now - time_to_seconds(v)
1077 if (approval['grantedOn'] < t):
1078 item_match = negative_match
1079 elif k == 'older-than':
1080 t = now - time_to_seconds(v)
1081 if (approval['grantedOn'] >= t):
1082 item_match = negative_match
1083 else:
1084 if isinstance(v, six.string_types):
1085 v = ast.literal_eval(v)
1086 if not isinstance(v, list):
1087 v = [v]
1088 if (normalizeCategory(approval['description']) != k or
1089 int(approval['value']) not in v):
1090 item_match = negative_match
1091 if not item_match:
1092 found_approval = False
1093 return found_approval
James E. Blair9c17dbf2014-06-23 14:21:58 -07001094
1095 def matchesRequiredApprovals(self, change):
Joshua Heskethb2068e82014-06-26 15:30:08 +10001096 if (self.required_any_approval and not change.approvals
1097 or self.required_all_approvals and not change.approvals):
1098 # A change with no approvals can not match
1099 return False
1100
1101 # TODO(jhesketh): If we wanted to optimise this slightly we could
1102 # analyse both the ANY and ALL filters by looping over the approvals
1103 # on the change and keeping track of what we have checked rather than
1104 # needing to loop on the change approvals twice
1105 return (self.matchesRequiredAnyApproval(change) and
1106 self.matchesRequiredAllApprovals(change))
1107
1108 def matchesRequiredAnyApproval(self, change):
1109 # Check if any approvals match the any requirements
1110 if not self.required_any_approval:
1111 # No approval required, so we must match
1112 return True
1113
1114 for rapproval in self.required_any_approval:
James E. Blair9c17dbf2014-06-23 14:21:58 -07001115 matches_approval = False
1116 for approval in change.approvals:
Joshua Heskethb2068e82014-06-26 15:30:08 +10001117 matches_approval = self._match_approval_required_approval(
1118 rapproval, approval)
1119 if matches_approval:
1120 # We have a matching approval so this requirement is
1121 # fulfilled
1122 return True
1123 return False
1124
1125 def matchesRequiredAllApprovals(self, change):
1126 # Check that /all/ of the approvals match the requirements
1127 if not self.required_all_approvals:
1128 # No approvals required, so we must match
1129 return True
1130
1131 for rapproval in self.required_all_approvals:
1132 for approval in change.approvals:
1133 matches_approval = self._match_approval_required_approval(
1134 rapproval, approval)
1135 if not matches_approval:
1136 # We have an approval that doesn't match so this
1137 # requirement can't be fulfilled
1138 return False
1139 # We must have matched everything
James E. Blair9c17dbf2014-06-23 14:21:58 -07001140 return True
1141
1142
1143class EventFilter(BaseFilter):
James E. Blairc0dedf82014-08-06 09:37:52 -07001144 def __init__(self, trigger, types=[], branches=[], refs=[],
1145 event_approvals={}, comments=[], emails=[], usernames=[],
Joshua Heskethb2068e82014-06-26 15:30:08 +10001146 timespecs=[], required_any_approval=[],
1147 required_all_approvals=[], pipelines=[]):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001148 super(EventFilter, self).__init__(
Joshua Heskethb2068e82014-06-26 15:30:08 +10001149 required_any_approval=required_any_approval,
1150 required_all_approvals=required_all_approvals)
James E. Blairc0dedf82014-08-06 09:37:52 -07001151 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07001152 self._types = types
1153 self._branches = branches
1154 self._refs = refs
James E. Blair1fbfceb2014-06-23 14:42:53 -07001155 self._comments = comments
1156 self._emails = emails
1157 self._usernames = usernames
James E. Blairc494d542014-08-06 09:23:52 -07001158 self._pipelines = pipelines
James E. Blairee743612012-05-29 14:49:32 -07001159 self.types = [re.compile(x) for x in types]
1160 self.branches = [re.compile(x) for x in branches]
1161 self.refs = [re.compile(x) for x in refs]
James E. Blair1fbfceb2014-06-23 14:42:53 -07001162 self.comments = [re.compile(x) for x in comments]
1163 self.emails = [re.compile(x) for x in emails]
1164 self.usernames = [re.compile(x) for x in usernames]
James E. Blairc494d542014-08-06 09:23:52 -07001165 self.pipelines = [re.compile(x) for x in pipelines]
James E. Blairc053d022014-01-22 14:57:33 -08001166 self.event_approvals = event_approvals
James E. Blair63bb0ef2013-07-29 17:14:51 -07001167 self.timespecs = timespecs
James E. Blairee743612012-05-29 14:49:32 -07001168
James E. Blair9f9667e2012-06-12 17:51:08 -07001169 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001170 ret = '<EventFilter'
James E. Blair1e8dd892012-05-30 09:15:05 -07001171
James E. Blairee743612012-05-29 14:49:32 -07001172 if self._types:
1173 ret += ' types: %s' % ', '.join(self._types)
James E. Blairc494d542014-08-06 09:23:52 -07001174 if self._pipelines:
1175 ret += ' pipelines: %s' % ', '.join(self._pipelines)
James E. Blairee743612012-05-29 14:49:32 -07001176 if self._branches:
1177 ret += ' branches: %s' % ', '.join(self._branches)
1178 if self._refs:
1179 ret += ' refs: %s' % ', '.join(self._refs)
James E. Blairc053d022014-01-22 14:57:33 -08001180 if self.event_approvals:
1181 ret += ' event_approvals: %s' % ', '.join(
1182 ['%s:%s' % a for a in self.event_approvals.items()])
Joshua Heskethb2068e82014-06-26 15:30:08 +10001183 if self.required_any_approval:
1184 ret += ' required_any_approval: %s' % ', '.join(
1185 ['%s' % a for a in self._required_any_approval])
1186 if self.required_all_approvals:
1187 ret += ' required_all_approvals: %s' % ', '.join(
1188 ['%s' % a for a in self._required_all_approvals])
James E. Blair1fbfceb2014-06-23 14:42:53 -07001189 if self._comments:
1190 ret += ' comments: %s' % ', '.join(self._comments)
1191 if self._emails:
1192 ret += ' emails: %s' % ', '.join(self._emails)
1193 if self._usernames:
1194 ret += ' username_filters: %s' % ', '.join(self._usernames)
James E. Blair63bb0ef2013-07-29 17:14:51 -07001195 if self.timespecs:
1196 ret += ' timespecs: %s' % ', '.join(self.timespecs)
James E. Blairee743612012-05-29 14:49:32 -07001197 ret += '>'
1198
1199 return ret
1200
James E. Blairc053d022014-01-22 14:57:33 -08001201 def matches(self, event, change):
James E. Blairee743612012-05-29 14:49:32 -07001202 # event types are ORed
1203 matches_type = False
1204 for etype in self.types:
1205 if etype.match(event.type):
1206 matches_type = True
1207 if self.types and not matches_type:
1208 return False
1209
James E. Blairc494d542014-08-06 09:23:52 -07001210 # pipelines are ORed
1211 matches_pipeline = False
1212 for epipe in self.pipelines:
1213 if epipe.match(event.pipeline_name):
1214 matches_pipeline = True
1215 if self.pipelines and not matches_pipeline:
1216 return False
1217
James E. Blairee743612012-05-29 14:49:32 -07001218 # branches are ORed
1219 matches_branch = False
1220 for branch in self.branches:
1221 if branch.match(event.branch):
1222 matches_branch = True
1223 if self.branches and not matches_branch:
1224 return False
1225
1226 # refs are ORed
1227 matches_ref = False
Yolanda Robla16698872014-08-25 11:59:27 +02001228 if event.ref is not None:
1229 for ref in self.refs:
1230 if ref.match(event.ref):
1231 matches_ref = True
James E. Blairee743612012-05-29 14:49:32 -07001232 if self.refs and not matches_ref:
1233 return False
1234
James E. Blair1fbfceb2014-06-23 14:42:53 -07001235 # comments are ORed
1236 matches_comment_re = False
1237 for comment_re in self.comments:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001238 if (event.comment is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001239 comment_re.search(event.comment)):
1240 matches_comment_re = True
1241 if self.comments and not matches_comment_re:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001242 return False
1243
Antoine Mussob4e809e2012-12-06 16:58:06 +01001244 # We better have an account provided by Gerrit to do
1245 # email filtering.
1246 if event.account is not None:
James E. Blaircf429f32012-12-20 14:28:24 -08001247 account_email = event.account.get('email')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001248 # emails are ORed
1249 matches_email_re = False
1250 for email_re in self.emails:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001251 if (account_email is not None and
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001252 email_re.search(account_email)):
James E. Blair1fbfceb2014-06-23 14:42:53 -07001253 matches_email_re = True
1254 if self.emails and not matches_email_re:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001255 return False
1256
James E. Blair1fbfceb2014-06-23 14:42:53 -07001257 # usernames are ORed
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001258 account_username = event.account.get('username')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001259 matches_username_re = False
1260 for username_re in self.usernames:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001261 if (account_username is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001262 username_re.search(account_username)):
1263 matches_username_re = True
1264 if self.usernames and not matches_username_re:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001265 return False
1266
James E. Blairee743612012-05-29 14:49:32 -07001267 # approvals are ANDed
James E. Blairc053d022014-01-22 14:57:33 -08001268 for category, value in self.event_approvals.items():
James E. Blairee743612012-05-29 14:49:32 -07001269 matches_approval = False
1270 for eapproval in event.approvals:
1271 if (normalizeCategory(eapproval['description']) == category and
1272 int(eapproval['value']) == int(value)):
1273 matches_approval = True
James E. Blair1e8dd892012-05-30 09:15:05 -07001274 if not matches_approval:
1275 return False
James E. Blair63bb0ef2013-07-29 17:14:51 -07001276
James E. Blair9c17dbf2014-06-23 14:21:58 -07001277 # required approvals are ANDed
1278 if not self.matchesRequiredApprovals(change):
1279 return False
James E. Blairc053d022014-01-22 14:57:33 -08001280
James E. Blair63bb0ef2013-07-29 17:14:51 -07001281 # timespecs are ORed
1282 matches_timespec = False
1283 for timespec in self.timespecs:
1284 if (event.timespec == timespec):
1285 matches_timespec = True
1286 if self.timespecs and not matches_timespec:
1287 return False
1288
James E. Blairee743612012-05-29 14:49:32 -07001289 return True
James E. Blaireff88162013-07-01 12:44:14 -04001290
1291
James E. Blair9c17dbf2014-06-23 14:21:58 -07001292class ChangeishFilter(BaseFilter):
Clark Boylana9702ad2014-05-08 17:17:24 -07001293 def __init__(self, open=None, current_patchset=None,
Joshua Heskethb2068e82014-06-26 15:30:08 +10001294 statuses=[], required_any_approval=[],
1295 required_all_approvals=[]):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001296 super(ChangeishFilter, self).__init__(
Joshua Heskethb2068e82014-06-26 15:30:08 +10001297 required_any_approval=required_any_approval,
1298 required_all_approvals=required_all_approvals)
James E. Blair11041d22014-05-02 14:49:53 -07001299 self.open = open
Clark Boylana9702ad2014-05-08 17:17:24 -07001300 self.current_patchset = current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001301 self.statuses = statuses
James E. Blair11041d22014-05-02 14:49:53 -07001302
1303 def __repr__(self):
1304 ret = '<ChangeishFilter'
1305
1306 if self.open is not None:
1307 ret += ' open: %s' % self.open
Clark Boylana9702ad2014-05-08 17:17:24 -07001308 if self.current_patchset is not None:
1309 ret += ' current-patchset: %s' % self.current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001310 if self.statuses:
1311 ret += ' statuses: %s' % ', '.join(self.statuses)
Joshua Heskethb2068e82014-06-26 15:30:08 +10001312 if self.required_any_approval:
1313 ret += (' required_any_approval: %s' %
1314 str(self.required_any_approval))
1315 if self.required_all_approvals:
1316 ret += (' required_all_approvals: %s' %
1317 str(self.required_all_approvals))
James E. Blair11041d22014-05-02 14:49:53 -07001318 ret += '>'
1319
1320 return ret
1321
1322 def matches(self, change):
1323 if self.open is not None:
1324 if self.open != change.open:
1325 return False
1326
Clark Boylana9702ad2014-05-08 17:17:24 -07001327 if self.current_patchset is not None:
1328 if self.current_patchset != change.is_current_patchset:
1329 return False
1330
James E. Blair11041d22014-05-02 14:49:53 -07001331 if self.statuses:
1332 if change.status not in self.statuses:
1333 return False
1334
James E. Blair9c17dbf2014-06-23 14:21:58 -07001335 # required approvals are ANDed
1336 if not self.matchesRequiredApprovals(change):
1337 return False
James E. Blair11041d22014-05-02 14:49:53 -07001338
1339 return True
1340
1341
James E. Blaireff88162013-07-01 12:44:14 -04001342class Layout(object):
1343 def __init__(self):
1344 self.projects = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07001345 self.pipelines = OrderedDict()
James E. Blaireff88162013-07-01 12:44:14 -04001346 self.jobs = {}
James E. Blairc28d1b02013-07-19 11:37:06 -07001347 self.metajobs = []
James E. Blaireff88162013-07-01 12:44:14 -04001348
1349 def getJob(self, name):
1350 if name in self.jobs:
1351 return self.jobs[name]
1352 job = Job(name)
Maru Newby79427a42015-02-17 17:54:45 +00001353 if job.is_metajob:
James E. Blaireff88162013-07-01 12:44:14 -04001354 regex = re.compile(name)
James E. Blairc28d1b02013-07-19 11:37:06 -07001355 self.metajobs.append((regex, job))
James E. Blaireff88162013-07-01 12:44:14 -04001356 else:
1357 # Apply attributes from matching meta-jobs
James E. Blairc28d1b02013-07-19 11:37:06 -07001358 for regex, metajob in self.metajobs:
James E. Blaireff88162013-07-01 12:44:14 -04001359 if regex.match(name):
1360 job.copy(metajob)
1361 self.jobs[name] = job
1362 return job