blob: 4b907c3e4524257eb928867a2bc40533e248ea8f [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
Ramy Asselin07cc33c2015-06-12 14:06:34 -0700738 if changeish.project:
739 ret['project'] = changeish.project.name
740 else:
741 # For cross-project dependencies with the depends-on
742 # project not known to zuul, the project is None
743 # Set it to a static value
744 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800745 ret['enqueue_time'] = int(self.enqueue_time * 1000)
746 ret['jobs'] = []
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -0500747 if hasattr(changeish, 'owner'):
748 ret['owner'] = changeish.owner
749 else:
750 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800751 max_remaining = 0
James E. Blair107c3852015-02-07 08:23:10 -0800752 for job in self.pipeline.getJobs(self):
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800753 now = time.time()
754 build = self.current_build_set.getBuild(job.name)
755 elapsed = None
756 remaining = None
757 result = None
758 url = None
759 worker = None
760 if build:
761 result = build.result
762 url = build.url
763 if build.start_time:
764 if build.end_time:
765 elapsed = int((build.end_time -
766 build.start_time) * 1000)
767 remaining = 0
768 else:
769 elapsed = int((now - build.start_time) * 1000)
770 if build.estimated_time:
771 remaining = max(
772 int(build.estimated_time * 1000) - elapsed,
773 0)
774 worker = {
775 'name': build.worker.name,
776 'hostname': build.worker.hostname,
777 'ips': build.worker.ips,
778 'fqdn': build.worker.fqdn,
779 'program': build.worker.program,
780 'version': build.worker.version,
781 'extra': build.worker.extra
782 }
783 if remaining and remaining > max_remaining:
784 max_remaining = remaining
785
786 ret['jobs'].append({
787 'name': job.name,
788 'elapsed_time': elapsed,
789 'remaining_time': remaining,
790 'url': url,
791 'result': result,
792 'voting': job.voting,
793 'uuid': build.uuid if build else None,
794 'launch_time': build.launch_time if build else None,
795 'start_time': build.start_time if build else None,
796 'end_time': build.end_time if build else None,
797 'estimated_time': build.estimated_time if build else None,
798 'pipeline': build.pipeline.name if build else None,
799 'canceled': build.canceled if build else None,
800 'retry': build.retry if build else None,
801 'number': build.number if build else None,
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800802 'worker': worker
803 })
804
805 if self.pipeline.haveAllJobsStarted(self):
806 ret['remaining_time'] = max_remaining
807 else:
808 ret['remaining_time'] = None
809 return ret
810
811 def formatStatus(self, indent=0, html=False):
812 changeish = self.change
813 indent_str = ' ' * indent
814 ret = ''
815 if html and hasattr(changeish, 'url') and changeish.url is not None:
816 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
817 indent_str,
818 changeish.project.name,
819 changeish.url,
820 changeish._id())
821 else:
822 ret += '%sProject %s change %s based on %s\n' % (
823 indent_str,
824 changeish.project.name,
825 changeish._id(),
826 self.item_ahead)
James E. Blair107c3852015-02-07 08:23:10 -0800827 for job in self.pipeline.getJobs(self):
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800828 build = self.current_build_set.getBuild(job.name)
829 if build:
830 result = build.result
831 else:
832 result = None
833 job_name = job.name
834 if not job.voting:
835 voting = ' (non-voting)'
836 else:
837 voting = ''
838 if html:
839 if build:
840 url = build.url
841 else:
842 url = None
843 if url is not None:
844 job_name = '<a href="%s">%s</a>' % (url, job_name)
845 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
846 ret += '\n'
847 return ret
848
James E. Blairfee8d652013-06-07 08:57:52 -0700849
850class Changeish(object):
851 """Something like a change; either a change or a ref"""
James E. Blairfee8d652013-06-07 08:57:52 -0700852
853 def __init__(self, project):
854 self.project = project
855
Joshua Hesketh36c3fa52014-01-22 11:40:52 +1100856 def getBasePath(self):
857 base_path = ''
858 if hasattr(self, 'refspec'):
859 base_path = "%s/%s/%s" % (
860 self.number[-2:], self.number, self.patchset)
861 elif hasattr(self, 'ref'):
862 base_path = "%s/%s" % (self.newrev[:2], self.newrev)
863
864 return base_path
865
James E. Blairfee8d652013-06-07 08:57:52 -0700866 def equals(self, other):
867 raise NotImplementedError()
868
869 def isUpdateOf(self, other):
870 raise NotImplementedError()
871
872 def filterJobs(self, jobs):
873 return filter(lambda job: job.changeMatches(self), jobs)
874
875 def getRelatedChanges(self):
876 return set()
877
James E. Blair1e8dd892012-05-30 09:15:05 -0700878
James E. Blair4aea70c2012-07-26 14:23:24 -0700879class Change(Changeish):
James E. Blair4aea70c2012-07-26 14:23:24 -0700880 def __init__(self, project):
881 super(Change, self).__init__(project)
882 self.branch = None
883 self.number = None
884 self.url = None
885 self.patchset = None
886 self.refspec = None
887
James E. Blair70c71582013-03-06 08:50:50 -0800888 self.files = []
James E. Blair6965a4b2014-12-16 17:19:04 -0800889 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -0700890 self.needed_by_changes = []
891 self.is_current_patchset = True
892 self.can_merge = False
893 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -0700894 self.failed_to_merge = False
James E. Blairc053d022014-01-22 14:57:33 -0800895 self.approvals = []
James E. Blair11041d22014-05-02 14:49:53 -0700896 self.open = None
897 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -0500898 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700899
900 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -0700901 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -0700902
903 def __repr__(self):
904 return '<Change 0x%x %s>' % (id(self), self._id())
905
906 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +0800907 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -0700908 return True
909 return False
910
James E. Blair2fa50962013-01-30 21:50:41 -0800911 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -0800912 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -0700913 (hasattr(other, 'patchset') and
914 self.patchset is not None and
915 other.patchset is not None and
916 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -0800917 return True
918 return False
919
James E. Blairfee8d652013-06-07 08:57:52 -0700920 def getRelatedChanges(self):
921 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -0800922 for c in self.needs_changes:
923 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -0700924 for c in self.needed_by_changes:
925 related.add(c)
926 related.update(c.getRelatedChanges())
927 return related
James E. Blair4aea70c2012-07-26 14:23:24 -0700928
929
930class Ref(Changeish):
James E. Blair4aea70c2012-07-26 14:23:24 -0700931 def __init__(self, project):
James E. Blairbe765db2012-08-07 08:36:20 -0700932 super(Ref, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -0700933 self.ref = None
934 self.oldrev = None
935 self.newrev = None
936
James E. Blairbe765db2012-08-07 08:36:20 -0700937 def _id(self):
938 return self.newrev
939
Antoine Musso68bdcd72013-01-17 12:31:28 +0100940 def __repr__(self):
941 rep = None
942 if self.newrev == '0000000000000000000000000000000000000000':
943 rep = '<Ref 0x%x deletes %s from %s' % (
944 id(self), self.ref, self.oldrev)
945 elif self.oldrev == '0000000000000000000000000000000000000000':
946 rep = '<Ref 0x%x creates %s on %s>' % (
947 id(self), self.ref, self.newrev)
948 else:
949 # Catch all
950 rep = '<Ref 0x%x %s updated %s..%s>' % (
951 id(self), self.ref, self.oldrev, self.newrev)
952
953 return rep
954
James E. Blair4aea70c2012-07-26 14:23:24 -0700955 def equals(self, other):
James E. Blair9358c612012-09-28 08:29:39 -0700956 if (self.project == other.project
957 and self.ref == other.ref
958 and self.newrev == other.newrev):
James E. Blair4aea70c2012-07-26 14:23:24 -0700959 return True
960 return False
961
James E. Blair2fa50962013-01-30 21:50:41 -0800962 def isUpdateOf(self, other):
963 return False
964
James E. Blair4aea70c2012-07-26 14:23:24 -0700965
James E. Blair63bb0ef2013-07-29 17:14:51 -0700966class NullChange(Changeish):
James E. Blaire5910202013-12-27 09:50:31 -0800967 def __repr__(self):
968 return '<NullChange for %s>' % (self.project)
James E. Blair63bb0ef2013-07-29 17:14:51 -0700969
James E. Blair63bb0ef2013-07-29 17:14:51 -0700970 def _id(self):
Alex Gaynorddb9ef32013-09-16 21:04:58 -0700971 return None
James E. Blair63bb0ef2013-07-29 17:14:51 -0700972
973 def equals(self, other):
Steve Varnau7b78b312015-04-03 14:49:46 -0700974 if (self.project == other.project
975 and other._id() is None):
James E. Blair4f6033c2014-03-27 15:49:09 -0700976 return True
James E. Blair63bb0ef2013-07-29 17:14:51 -0700977 return False
978
979 def isUpdateOf(self, other):
980 return False
981
982
James E. Blairee743612012-05-29 14:49:32 -0700983class TriggerEvent(object):
984 def __init__(self):
985 self.data = None
James E. Blair32663402012-06-01 10:04:18 -0700986 # common
James E. Blairee743612012-05-29 14:49:32 -0700987 self.type = None
988 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -0700989 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +0100990 # Representation of the user account that performed the event.
991 self.account = None
James E. Blair32663402012-06-01 10:04:18 -0700992 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -0700993 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -0700994 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -0700995 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -0700996 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -0700997 self.approvals = []
998 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -0700999 self.comment = None
James E. Blair32663402012-06-01 10:04:18 -07001000 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -07001001 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -07001002 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07001003 self.newrev = None
James E. Blair63bb0ef2013-07-29 17:14:51 -07001004 # timer
1005 self.timespec = None
James E. Blairc494d542014-08-06 09:23:52 -07001006 # zuultrigger
1007 self.pipeline_name = None
James E. Blairad28e912013-11-27 10:43:22 -08001008 # For events that arrive with a destination pipeline (eg, from
1009 # an admin command, etc):
1010 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07001011
James E. Blair9f9667e2012-06-12 17:51:08 -07001012 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001013 ret = '<TriggerEvent %s %s' % (self.type, self.project_name)
James E. Blair1e8dd892012-05-30 09:15:05 -07001014
James E. Blairee743612012-05-29 14:49:32 -07001015 if self.branch:
1016 ret += " %s" % self.branch
1017 if self.change_number:
1018 ret += " %s,%s" % (self.change_number, self.patch_number)
1019 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -07001020 ret += ' ' + ', '.join(
1021 ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
James E. Blairee743612012-05-29 14:49:32 -07001022 ret += '>'
1023
1024 return ret
1025
James E. Blair1e8dd892012-05-30 09:15:05 -07001026
James E. Blair9c17dbf2014-06-23 14:21:58 -07001027class BaseFilter(object):
James E. Blair5bf78a32015-07-30 18:08:24 +00001028 def __init__(self, required_approvals=[]):
1029 self._required_approvals = copy.deepcopy(required_approvals)
1030 self.required_approvals = required_approvals
James E. Blair9c17dbf2014-06-23 14:21:58 -07001031
James E. Blair5bf78a32015-07-30 18:08:24 +00001032 for a in self.required_approvals:
James E. Blair9c17dbf2014-06-23 14:21:58 -07001033 for k, v in a.items():
1034 if k == 'username':
1035 pass
James E. Blair1fbfceb2014-06-23 14:42:53 -07001036 elif k in ['email', 'email-filter']:
James E. Blair5bf78a32015-07-30 18:08:24 +00001037 a['email'] = re.compile(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001038 elif k == 'newer-than':
James E. Blair5bf78a32015-07-30 18:08:24 +00001039 a[k] = time_to_seconds(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001040 elif k == 'older-than':
James E. Blair5bf78a32015-07-30 18:08:24 +00001041 a[k] = time_to_seconds(v)
1042 else:
1043 if not isinstance(v, list):
1044 a[k] = [v]
James E. Blair1fbfceb2014-06-23 14:42:53 -07001045 if 'email-filter' in a:
1046 del a['email-filter']
James E. Blair9c17dbf2014-06-23 14:21:58 -07001047
1048 def matchesRequiredApprovals(self, change):
James E. Blair5bf78a32015-07-30 18:08:24 +00001049 now = time.time()
1050 for rapproval in self.required_approvals:
James E. Blair9c17dbf2014-06-23 14:21:58 -07001051 matches_approval = False
1052 for approval in change.approvals:
James E. Blair5bf78a32015-07-30 18:08:24 +00001053 if 'description' not in approval:
1054 continue
1055 found_approval = True
1056 by = approval.get('by', {})
1057 for k, v in rapproval.items():
1058 if k == 'username':
1059 if (by.get('username', '') != v):
1060 found_approval = False
1061 elif k == 'email':
1062 if (not v.search(by.get('email', ''))):
1063 found_approval = False
1064 elif k == 'newer-than':
1065 t = now - v
1066 if (approval['grantedOn'] < t):
1067 found_approval = False
1068 elif k == 'older-than':
1069 t = now - v
1070 if (approval['grantedOn'] >= t):
1071 found_approval = False
1072 else:
1073 if (normalizeCategory(approval['description']) != k or
1074 int(approval['value']) not in v):
1075 found_approval = False
1076 if found_approval:
1077 matches_approval = True
1078 break
1079 if not matches_approval:
1080 return False
James E. Blair9c17dbf2014-06-23 14:21:58 -07001081 return True
1082
1083
1084class EventFilter(BaseFilter):
James E. Blairc0dedf82014-08-06 09:37:52 -07001085 def __init__(self, trigger, types=[], branches=[], refs=[],
1086 event_approvals={}, comments=[], emails=[], usernames=[],
James E. Blair5bf78a32015-07-30 18:08:24 +00001087 timespecs=[], required_approvals=[], pipelines=[]):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001088 super(EventFilter, self).__init__(
James E. Blair5bf78a32015-07-30 18:08:24 +00001089 required_approvals=required_approvals)
James E. Blairc0dedf82014-08-06 09:37:52 -07001090 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07001091 self._types = types
1092 self._branches = branches
1093 self._refs = refs
James E. Blair1fbfceb2014-06-23 14:42:53 -07001094 self._comments = comments
1095 self._emails = emails
1096 self._usernames = usernames
James E. Blairc494d542014-08-06 09:23:52 -07001097 self._pipelines = pipelines
James E. Blairee743612012-05-29 14:49:32 -07001098 self.types = [re.compile(x) for x in types]
1099 self.branches = [re.compile(x) for x in branches]
1100 self.refs = [re.compile(x) for x in refs]
James E. Blair1fbfceb2014-06-23 14:42:53 -07001101 self.comments = [re.compile(x) for x in comments]
1102 self.emails = [re.compile(x) for x in emails]
1103 self.usernames = [re.compile(x) for x in usernames]
James E. Blairc494d542014-08-06 09:23:52 -07001104 self.pipelines = [re.compile(x) for x in pipelines]
James E. Blairc053d022014-01-22 14:57:33 -08001105 self.event_approvals = event_approvals
James E. Blair63bb0ef2013-07-29 17:14:51 -07001106 self.timespecs = timespecs
James E. Blairee743612012-05-29 14:49:32 -07001107
James E. Blair9f9667e2012-06-12 17:51:08 -07001108 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001109 ret = '<EventFilter'
James E. Blair1e8dd892012-05-30 09:15:05 -07001110
James E. Blairee743612012-05-29 14:49:32 -07001111 if self._types:
1112 ret += ' types: %s' % ', '.join(self._types)
James E. Blairc494d542014-08-06 09:23:52 -07001113 if self._pipelines:
1114 ret += ' pipelines: %s' % ', '.join(self._pipelines)
James E. Blairee743612012-05-29 14:49:32 -07001115 if self._branches:
1116 ret += ' branches: %s' % ', '.join(self._branches)
1117 if self._refs:
1118 ret += ' refs: %s' % ', '.join(self._refs)
James E. Blairc053d022014-01-22 14:57:33 -08001119 if self.event_approvals:
1120 ret += ' event_approvals: %s' % ', '.join(
1121 ['%s:%s' % a for a in self.event_approvals.items()])
James E. Blair5bf78a32015-07-30 18:08:24 +00001122 if self.required_approvals:
1123 ret += ' required_approvals: %s' % ', '.join(
1124 ['%s' % a for a in self._required_approvals])
James E. Blair1fbfceb2014-06-23 14:42:53 -07001125 if self._comments:
1126 ret += ' comments: %s' % ', '.join(self._comments)
1127 if self._emails:
1128 ret += ' emails: %s' % ', '.join(self._emails)
1129 if self._usernames:
1130 ret += ' username_filters: %s' % ', '.join(self._usernames)
James E. Blair63bb0ef2013-07-29 17:14:51 -07001131 if self.timespecs:
1132 ret += ' timespecs: %s' % ', '.join(self.timespecs)
James E. Blairee743612012-05-29 14:49:32 -07001133 ret += '>'
1134
1135 return ret
1136
James E. Blairc053d022014-01-22 14:57:33 -08001137 def matches(self, event, change):
James E. Blairee743612012-05-29 14:49:32 -07001138 # event types are ORed
1139 matches_type = False
1140 for etype in self.types:
1141 if etype.match(event.type):
1142 matches_type = True
1143 if self.types and not matches_type:
1144 return False
1145
James E. Blairc494d542014-08-06 09:23:52 -07001146 # pipelines are ORed
1147 matches_pipeline = False
1148 for epipe in self.pipelines:
1149 if epipe.match(event.pipeline_name):
1150 matches_pipeline = True
1151 if self.pipelines and not matches_pipeline:
1152 return False
1153
James E. Blairee743612012-05-29 14:49:32 -07001154 # branches are ORed
1155 matches_branch = False
1156 for branch in self.branches:
1157 if branch.match(event.branch):
1158 matches_branch = True
1159 if self.branches and not matches_branch:
1160 return False
1161
1162 # refs are ORed
1163 matches_ref = False
Yolanda Robla16698872014-08-25 11:59:27 +02001164 if event.ref is not None:
1165 for ref in self.refs:
1166 if ref.match(event.ref):
1167 matches_ref = True
James E. Blairee743612012-05-29 14:49:32 -07001168 if self.refs and not matches_ref:
1169 return False
1170
James E. Blair1fbfceb2014-06-23 14:42:53 -07001171 # comments are ORed
1172 matches_comment_re = False
1173 for comment_re in self.comments:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001174 if (event.comment is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001175 comment_re.search(event.comment)):
1176 matches_comment_re = True
1177 if self.comments and not matches_comment_re:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001178 return False
1179
Antoine Mussob4e809e2012-12-06 16:58:06 +01001180 # We better have an account provided by Gerrit to do
1181 # email filtering.
1182 if event.account is not None:
James E. Blaircf429f32012-12-20 14:28:24 -08001183 account_email = event.account.get('email')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001184 # emails are ORed
1185 matches_email_re = False
1186 for email_re in self.emails:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001187 if (account_email is not None and
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001188 email_re.search(account_email)):
James E. Blair1fbfceb2014-06-23 14:42:53 -07001189 matches_email_re = True
1190 if self.emails and not matches_email_re:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001191 return False
1192
James E. Blair1fbfceb2014-06-23 14:42:53 -07001193 # usernames are ORed
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001194 account_username = event.account.get('username')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001195 matches_username_re = False
1196 for username_re in self.usernames:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001197 if (account_username is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001198 username_re.search(account_username)):
1199 matches_username_re = True
1200 if self.usernames and not matches_username_re:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001201 return False
1202
James E. Blairee743612012-05-29 14:49:32 -07001203 # approvals are ANDed
James E. Blairc053d022014-01-22 14:57:33 -08001204 for category, value in self.event_approvals.items():
James E. Blairee743612012-05-29 14:49:32 -07001205 matches_approval = False
1206 for eapproval in event.approvals:
1207 if (normalizeCategory(eapproval['description']) == category and
1208 int(eapproval['value']) == int(value)):
1209 matches_approval = True
James E. Blair1e8dd892012-05-30 09:15:05 -07001210 if not matches_approval:
1211 return False
James E. Blair63bb0ef2013-07-29 17:14:51 -07001212
James E. Blair5bf78a32015-07-30 18:08:24 +00001213 if self.required_approvals and not change.approvals:
1214 # A change with no approvals can not match
1215 return False
1216
James E. Blair9c17dbf2014-06-23 14:21:58 -07001217 # required approvals are ANDed
1218 if not self.matchesRequiredApprovals(change):
1219 return False
James E. Blairc053d022014-01-22 14:57:33 -08001220
James E. Blair63bb0ef2013-07-29 17:14:51 -07001221 # timespecs are ORed
1222 matches_timespec = False
1223 for timespec in self.timespecs:
1224 if (event.timespec == timespec):
1225 matches_timespec = True
1226 if self.timespecs and not matches_timespec:
1227 return False
1228
James E. Blairee743612012-05-29 14:49:32 -07001229 return True
James E. Blaireff88162013-07-01 12:44:14 -04001230
1231
James E. Blair9c17dbf2014-06-23 14:21:58 -07001232class ChangeishFilter(BaseFilter):
Clark Boylana9702ad2014-05-08 17:17:24 -07001233 def __init__(self, open=None, current_patchset=None,
James E. Blair5bf78a32015-07-30 18:08:24 +00001234 statuses=[], required_approvals=[]):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001235 super(ChangeishFilter, self).__init__(
James E. Blair5bf78a32015-07-30 18:08:24 +00001236 required_approvals=required_approvals)
James E. Blair11041d22014-05-02 14:49:53 -07001237 self.open = open
Clark Boylana9702ad2014-05-08 17:17:24 -07001238 self.current_patchset = current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001239 self.statuses = statuses
James E. Blair11041d22014-05-02 14:49:53 -07001240
1241 def __repr__(self):
1242 ret = '<ChangeishFilter'
1243
1244 if self.open is not None:
1245 ret += ' open: %s' % self.open
Clark Boylana9702ad2014-05-08 17:17:24 -07001246 if self.current_patchset is not None:
1247 ret += ' current-patchset: %s' % self.current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001248 if self.statuses:
1249 ret += ' statuses: %s' % ', '.join(self.statuses)
James E. Blair5bf78a32015-07-30 18:08:24 +00001250 if self.required_approvals:
1251 ret += ' required_approvals: %s' % str(self.required_approvals)
James E. Blair11041d22014-05-02 14:49:53 -07001252 ret += '>'
1253
1254 return ret
1255
1256 def matches(self, change):
1257 if self.open is not None:
1258 if self.open != change.open:
1259 return False
1260
Clark Boylana9702ad2014-05-08 17:17:24 -07001261 if self.current_patchset is not None:
1262 if self.current_patchset != change.is_current_patchset:
1263 return False
1264
James E. Blair11041d22014-05-02 14:49:53 -07001265 if self.statuses:
1266 if change.status not in self.statuses:
1267 return False
1268
James E. Blair5bf78a32015-07-30 18:08:24 +00001269 if self.required_approvals and not change.approvals:
1270 # A change with no approvals can not match
1271 return False
1272
James E. Blair9c17dbf2014-06-23 14:21:58 -07001273 # required approvals are ANDed
1274 if not self.matchesRequiredApprovals(change):
1275 return False
James E. Blair11041d22014-05-02 14:49:53 -07001276
1277 return True
1278
1279
James E. Blaireff88162013-07-01 12:44:14 -04001280class Layout(object):
1281 def __init__(self):
1282 self.projects = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07001283 self.pipelines = OrderedDict()
James E. Blaireff88162013-07-01 12:44:14 -04001284 self.jobs = {}
James E. Blairc28d1b02013-07-19 11:37:06 -07001285 self.metajobs = []
James E. Blaireff88162013-07-01 12:44:14 -04001286
1287 def getJob(self, name):
1288 if name in self.jobs:
1289 return self.jobs[name]
1290 job = Job(name)
Maru Newby79427a42015-02-17 17:54:45 +00001291 if job.is_metajob:
James E. Blaireff88162013-07-01 12:44:14 -04001292 regex = re.compile(name)
James E. Blairc28d1b02013-07-19 11:37:06 -07001293 self.metajobs.append((regex, job))
James E. Blaireff88162013-07-01 12:44:14 -04001294 else:
1295 # Apply attributes from matching meta-jobs
James E. Blairc28d1b02013-07-19 11:37:06 -07001296 for regex, metajob in self.metajobs:
James E. Blaireff88162013-07-01 12:44:14 -04001297 if regex.match(name):
1298 job.copy(metajob)
1299 self.jobs[name] = job
1300 return job