blob: 512e9c927fe0d60cc91024031fb058a04855ee6e [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. Blairce8a2132016-05-19 15:21:52 -070016import os
James E. Blairee743612012-05-29 14:49:32 -070017import re
James E. Blairce8a2132016-05-19 15:21:52 -070018import struct
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
K Jonathan Harkerf95e7232015-04-29 13:33:16 -070027EMPTY_GIT_REF = '0' * 40 # git sha of all zeros, used during creates/deletes
28
James E. Blair19deff22013-08-25 13:17:35 -070029MERGER_MERGE = 1 # "git merge"
30MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
31MERGER_CHERRY_PICK = 3 # "git cherry-pick"
32
33MERGER_MAP = {
34 'merge': MERGER_MERGE,
35 'merge-resolve': MERGER_MERGE_RESOLVE,
36 'cherry-pick': MERGER_CHERRY_PICK,
37}
James E. Blairee743612012-05-29 14:49:32 -070038
James E. Blair64ed6f22013-07-10 14:07:23 -070039PRECEDENCE_NORMAL = 0
40PRECEDENCE_LOW = 1
41PRECEDENCE_HIGH = 2
42
43PRECEDENCE_MAP = {
44 None: PRECEDENCE_NORMAL,
45 'low': PRECEDENCE_LOW,
46 'normal': PRECEDENCE_NORMAL,
47 'high': PRECEDENCE_HIGH,
48}
49
James E. Blair1e8dd892012-05-30 09:15:05 -070050
James E. Blairc053d022014-01-22 14:57:33 -080051def time_to_seconds(s):
52 if s.endswith('s'):
53 return int(s[:-1])
54 if s.endswith('m'):
55 return int(s[:-1]) * 60
56 if s.endswith('h'):
57 return int(s[:-1]) * 60 * 60
58 if s.endswith('d'):
59 return int(s[:-1]) * 24 * 60 * 60
60 if s.endswith('w'):
61 return int(s[:-1]) * 7 * 24 * 60 * 60
62 raise Exception("Unable to parse time value: %s" % s)
63
64
James E. Blair11041d22014-05-02 14:49:53 -070065def normalizeCategory(name):
66 name = name.lower()
67 return re.sub(' ', '-', name)
68
69
James E. Blair4aea70c2012-07-26 14:23:24 -070070class Pipeline(object):
Monty Taylora42a55b2016-07-29 07:53:33 -070071 """A configuration that ties triggers, reporters, managers and sources.
72
Monty Taylor82dfd412016-07-29 12:01:28 -070073 Source
74 Where changes should come from. It is a named connection to
Monty Taylora42a55b2016-07-29 07:53:33 -070075 an external service defined in zuul.conf
Monty Taylor82dfd412016-07-29 12:01:28 -070076
77 Trigger
78 A description of which events should be processed
79
80 Manager
81 Responsible for enqueing and dequeing Changes
82
83 Reporter
84 Communicates success and failure results somewhere
Monty Taylora42a55b2016-07-29 07:53:33 -070085 """
James E. Blair83005782015-12-11 14:46:03 -080086 def __init__(self, name, layout):
James E. Blair4aea70c2012-07-26 14:23:24 -070087 self.name = name
James E. Blair83005782015-12-11 14:46:03 -080088 self.layout = layout
James E. Blair8dbd56a2012-12-22 10:55:10 -080089 self.description = None
James E. Blair56370192013-01-14 15:47:28 -080090 self.failure_message = None
Joshua Heskethb7179772014-01-30 23:30:46 +110091 self.merge_failure_message = None
James E. Blair56370192013-01-14 15:47:28 -080092 self.success_message = None
Joshua Hesketh3979e3e2014-03-04 11:21:10 +110093 self.footer_message = None
James E. Blair60af7f42016-03-11 16:11:06 -080094 self.start_message = None
James E. Blair2fa50962013-01-30 21:50:41 -080095 self.dequeue_on_new_patchset = True
James E. Blair17dd6772015-02-09 14:45:18 -080096 self.ignore_dependencies = False
James E. Blair4aea70c2012-07-26 14:23:24 -070097 self.job_trees = {} # project -> JobTree
98 self.manager = None
James E. Blaire0487072012-08-29 17:38:31 -070099 self.queues = []
James E. Blair64ed6f22013-07-10 14:07:23 -0700100 self.precedence = PRECEDENCE_NORMAL
James E. Blairc0dedf82014-08-06 09:37:52 -0700101 self.source = None
James E. Blair83005782015-12-11 14:46:03 -0800102 self.triggers = []
Joshua Hesketh352264b2015-08-11 23:42:08 +1000103 self.start_actions = []
104 self.success_actions = []
105 self.failure_actions = []
106 self.merge_failure_actions = []
107 self.disabled_actions = []
Joshua Hesketh89e829d2015-02-10 16:29:45 +1100108 self.disable_at = None
109 self._consecutive_failures = 0
110 self._disabled = False
Clark Boylan7603a372014-01-21 11:43:20 -0800111 self.window = None
112 self.window_floor = None
113 self.window_increase_type = None
114 self.window_increase_factor = None
115 self.window_decrease_type = None
116 self.window_decrease_factor = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700117
James E. Blair83005782015-12-11 14:46:03 -0800118 @property
119 def actions(self):
120 return (
121 self.start_actions +
122 self.success_actions +
123 self.failure_actions +
124 self.merge_failure_actions +
125 self.disabled_actions
126 )
127
James E. Blaird09c17a2012-08-07 09:23:14 -0700128 def __repr__(self):
129 return '<Pipeline %s>' % self.name
130
James E. Blair4aea70c2012-07-26 14:23:24 -0700131 def setManager(self, manager):
132 self.manager = manager
133
James E. Blair4aea70c2012-07-26 14:23:24 -0700134 def getProjects(self):
Monty Taylor74fa3862016-06-02 07:39:49 +0300135 # cmp is not in python3, applied idiom from
136 # http://python-future.org/compatible_idioms.html#cmp
137 return sorted(
138 self.job_trees.keys(),
139 key=lambda p: p.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700140
James E. Blaire0487072012-08-29 17:38:31 -0700141 def addQueue(self, queue):
142 self.queues.append(queue)
143
144 def getQueue(self, project):
145 for queue in self.queues:
146 if project in queue.projects:
147 return queue
148 return None
149
James E. Blairbfb8e042014-12-30 17:01:44 -0800150 def removeQueue(self, queue):
151 self.queues.remove(queue)
152
James E. Blair4aea70c2012-07-26 14:23:24 -0700153 def getJobTree(self, project):
154 tree = self.job_trees.get(project)
155 return tree
156
James E. Blaire0487072012-08-29 17:38:31 -0700157 def getChangesInQueue(self):
158 changes = []
159 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700160 changes.extend([x.change for x in shared_queue.queue])
James E. Blaire0487072012-08-29 17:38:31 -0700161 return changes
162
James E. Blairfee8d652013-06-07 08:57:52 -0700163 def getAllItems(self):
164 items = []
James E. Blaire0487072012-08-29 17:38:31 -0700165 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700166 items.extend(shared_queue.queue)
James E. Blairfee8d652013-06-07 08:57:52 -0700167 return items
James E. Blaire0487072012-08-29 17:38:31 -0700168
James E. Blairb7273ef2016-04-19 08:58:51 -0700169 def formatStatusJSON(self, url_pattern=None):
James E. Blair8dbd56a2012-12-22 10:55:10 -0800170 j_pipeline = dict(name=self.name,
171 description=self.description)
172 j_queues = []
173 j_pipeline['change_queues'] = j_queues
174 for queue in self.queues:
175 j_queue = dict(name=queue.name)
176 j_queues.append(j_queue)
177 j_queue['heads'] = []
Clark Boylanaf2476f2014-01-23 14:47:36 -0800178 j_queue['window'] = queue.window
James E. Blair972e3c72013-08-29 12:04:55 -0700179
180 j_changes = []
181 for e in queue.queue:
182 if not e.item_ahead:
183 if j_changes:
184 j_queue['heads'].append(j_changes)
185 j_changes = []
James E. Blairb7273ef2016-04-19 08:58:51 -0700186 j_changes.append(e.formatJSON(url_pattern))
James E. Blair972e3c72013-08-29 12:04:55 -0700187 if (len(j_changes) > 1 and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000188 (j_changes[-2]['remaining_time'] is not None) and
189 (j_changes[-1]['remaining_time'] is not None)):
James E. Blair972e3c72013-08-29 12:04:55 -0700190 j_changes[-1]['remaining_time'] = max(
191 j_changes[-2]['remaining_time'],
192 j_changes[-1]['remaining_time'])
193 if j_changes:
James E. Blair8dbd56a2012-12-22 10:55:10 -0800194 j_queue['heads'].append(j_changes)
195 return j_pipeline
196
James E. Blair4aea70c2012-07-26 14:23:24 -0700197
James E. Blairee743612012-05-29 14:49:32 -0700198class ChangeQueue(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700199 """A ChangeQueue contains Changes to be processed related projects.
200
Monty Taylor82dfd412016-07-29 12:01:28 -0700201 A Pipeline with a DependentPipelineManager has multiple parallel
202 ChangeQueues shared by different projects. For instance, there may a
203 ChangeQueue shared by interrelated projects foo and bar, and a second queue
204 for independent project baz.
Monty Taylora42a55b2016-07-29 07:53:33 -0700205
Monty Taylor82dfd412016-07-29 12:01:28 -0700206 A Pipeline with an IndependentPipelineManager puts every Change into its
207 own ChangeQueue
Monty Taylora42a55b2016-07-29 07:53:33 -0700208
209 The ChangeQueue Window is inspired by TCP windows and controlls how many
210 Changes in a given ChangeQueue will be considered active and ready to
211 be processed. If a Change succeeds, the Window is increased by
212 `window_increase_factor`. If a Change fails, the Window is decreased by
213 `window_decrease_factor`.
214 """
James E. Blairbfb8e042014-12-30 17:01:44 -0800215 def __init__(self, pipeline, window=0, window_floor=1,
Clark Boylan7603a372014-01-21 11:43:20 -0800216 window_increase_type='linear', window_increase_factor=1,
James E. Blair0dcef7a2016-08-19 09:35:17 -0700217 window_decrease_type='exponential', window_decrease_factor=2,
218 name=None):
James E. Blair4aea70c2012-07-26 14:23:24 -0700219 self.pipeline = pipeline
James E. Blair0dcef7a2016-08-19 09:35:17 -0700220 if name:
221 self.name = name
222 else:
223 self.name = ''
James E. Blairee743612012-05-29 14:49:32 -0700224 self.projects = []
225 self._jobs = set()
226 self.queue = []
Clark Boylan7603a372014-01-21 11:43:20 -0800227 self.window = window
228 self.window_floor = window_floor
229 self.window_increase_type = window_increase_type
230 self.window_increase_factor = window_increase_factor
231 self.window_decrease_type = window_decrease_type
232 self.window_decrease_factor = window_decrease_factor
James E. Blairee743612012-05-29 14:49:32 -0700233
James E. Blair9f9667e2012-06-12 17:51:08 -0700234 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700235 return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
James E. Blairee743612012-05-29 14:49:32 -0700236
237 def getJobs(self):
238 return self._jobs
239
240 def addProject(self, project):
241 if project not in self.projects:
242 self.projects.append(project)
James E. Blairc8a1e052014-02-25 09:29:26 -0800243
James E. Blair0dcef7a2016-08-19 09:35:17 -0700244 if not self.name:
245 self.name = project.name
James E. Blairee743612012-05-29 14:49:32 -0700246
247 def enqueueChange(self, change):
James E. Blairbfb8e042014-12-30 17:01:44 -0800248 item = QueueItem(self, change)
James E. Blaircdccd972013-07-01 12:10:22 -0700249 self.enqueueItem(item)
250 item.enqueue_time = time.time()
251 return item
252
253 def enqueueItem(self, item):
James E. Blair4a035d92014-01-23 13:10:48 -0800254 item.pipeline = self.pipeline
James E. Blairbfb8e042014-12-30 17:01:44 -0800255 item.queue = self
256 if self.queue:
James E. Blairfee8d652013-06-07 08:57:52 -0700257 item.item_ahead = self.queue[-1]
James E. Blair972e3c72013-08-29 12:04:55 -0700258 item.item_ahead.items_behind.append(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700259 self.queue.append(item)
James E. Blairee743612012-05-29 14:49:32 -0700260
James E. Blairfee8d652013-06-07 08:57:52 -0700261 def dequeueItem(self, item):
262 if item in self.queue:
263 self.queue.remove(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700264 if item.item_ahead:
James E. Blair972e3c72013-08-29 12:04:55 -0700265 item.item_ahead.items_behind.remove(item)
266 for item_behind in item.items_behind:
267 if item.item_ahead:
268 item.item_ahead.items_behind.append(item_behind)
269 item_behind.item_ahead = item.item_ahead
James E. Blairfee8d652013-06-07 08:57:52 -0700270 item.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700271 item.items_behind = []
James E. Blairfee8d652013-06-07 08:57:52 -0700272 item.dequeue_time = time.time()
James E. Blaire0487072012-08-29 17:38:31 -0700273
James E. Blair972e3c72013-08-29 12:04:55 -0700274 def moveItem(self, item, item_ahead):
James E. Blair972e3c72013-08-29 12:04:55 -0700275 if item.item_ahead == item_ahead:
276 return False
277 # Remove from current location
278 if item.item_ahead:
279 item.item_ahead.items_behind.remove(item)
280 for item_behind in item.items_behind:
281 if item.item_ahead:
282 item.item_ahead.items_behind.append(item_behind)
283 item_behind.item_ahead = item.item_ahead
284 # Add to new location
285 item.item_ahead = item_ahead
James E. Blair00451262013-09-20 11:40:17 -0700286 item.items_behind = []
James E. Blair972e3c72013-08-29 12:04:55 -0700287 if item.item_ahead:
288 item.item_ahead.items_behind.append(item)
289 return True
James E. Blairee743612012-05-29 14:49:32 -0700290
291 def mergeChangeQueue(self, other):
292 for project in other.projects:
293 self.addProject(project)
Clark Boylan7603a372014-01-21 11:43:20 -0800294 self.window = min(self.window, other.window)
295 # TODO merge semantics
296
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800297 def isActionable(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800298 if self.window:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800299 return item in self.queue[:self.window]
Clark Boylan7603a372014-01-21 11:43:20 -0800300 else:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800301 return True
Clark Boylan7603a372014-01-21 11:43:20 -0800302
303 def increaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800304 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800305 if self.window_increase_type == 'linear':
306 self.window += self.window_increase_factor
307 elif self.window_increase_type == 'exponential':
308 self.window *= self.window_increase_factor
309
310 def decreaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800311 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800312 if self.window_decrease_type == 'linear':
313 self.window = max(
314 self.window_floor,
315 self.window - self.window_decrease_factor)
316 elif self.window_decrease_type == 'exponential':
317 self.window = max(
318 self.window_floor,
Morgan Fainbergfdae2252016-06-07 20:13:20 -0700319 int(self.window / self.window_decrease_factor))
James E. Blairee743612012-05-29 14:49:32 -0700320
James E. Blair1e8dd892012-05-30 09:15:05 -0700321
James E. Blair4aea70c2012-07-26 14:23:24 -0700322class Project(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700323 """A Project represents a git repository such as openstack/nova."""
324
James E. Blaircf440a22016-07-15 09:11:58 -0700325 # NOTE: Projects should only be instantiated via a Source object
326 # so that they are associated with and cached by their Connection.
327 # This makes a Project instance a unique identifier for a given
328 # project from a given source.
329
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000330 def __init__(self, name, foreign=False):
James E. Blair4aea70c2012-07-26 14:23:24 -0700331 self.name = name
James E. Blair19deff22013-08-25 13:17:35 -0700332 self.merge_mode = MERGER_MERGE_RESOLVE
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000333 # foreign projects are those referenced in dependencies
334 # of layout projects, this should matter
335 # when deciding whether to enqueue their changes
James E. Blaircf440a22016-07-15 09:11:58 -0700336 # TODOv3 (jeblair): re-add support for foreign projects if needed
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000337 self.foreign = foreign
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700338 self.unparsed_config = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700339
340 def __str__(self):
341 return self.name
342
343 def __repr__(self):
344 return '<Project %s>' % (self.name)
345
346
James E. Blairee743612012-05-29 14:49:32 -0700347class Job(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700348 """A Job represents the defintion of actions to perform."""
349
James E. Blair83005782015-12-11 14:46:03 -0800350 attributes = dict(
351 timeout=None,
352 # variables={},
James E. Blair8d692392016-04-08 17:47:58 -0700353 nodes=[],
Ricardo Carrillo Cruz4e94f612016-07-25 16:11:56 +0000354 auth={},
James E. Blair83005782015-12-11 14:46:03 -0800355 workspace=None,
356 pre_run=None,
357 post_run=None,
358 voting=None,
James E. Blair791b5392016-08-03 11:25:56 -0700359 hold_following_changes=None,
James E. Blair83005782015-12-11 14:46:03 -0800360 failure_message=None,
361 success_message=None,
362 failure_url=None,
363 success_url=None,
364 # Matchers. These are separate so they can be individually
365 # overidden.
366 branch_matcher=None,
367 file_matcher=None,
368 irrelevant_file_matcher=None, # skip-if
James E. Blair83005782015-12-11 14:46:03 -0800369 parameter_function=None, # TODOv3(jeblair): remove
Joshua Heskethdc7820c2016-03-11 13:14:28 +1100370 tags=set(),
Joshua Hesketh89b67f62016-02-11 21:22:14 +1100371 mutex=None,
James E. Blair83005782015-12-11 14:46:03 -0800372 )
373
James E. Blairee743612012-05-29 14:49:32 -0700374 def __init__(self, name):
375 self.name = name
James E. Blair4317e9f2016-07-15 10:05:47 -0700376 self.project_source = None
James E. Blair83005782015-12-11 14:46:03 -0800377 for k, v in self.attributes.items():
378 setattr(self, k, v)
379
380 def __equals__(self, other):
381 # Compare the name and all inheritable attributes to determine
382 # whether two jobs with the same name are identically
383 # configured. Useful upon reconfiguration.
384 if not isinstance(other, Job):
385 return False
386 if self.name != other.name:
387 return False
388 for k, v in self.attributes.items():
389 if getattr(self, k) != getattr(other, k):
390 return False
391 return True
James E. Blairee743612012-05-29 14:49:32 -0700392
393 def __str__(self):
394 return self.name
395
396 def __repr__(self):
James E. Blair83005782015-12-11 14:46:03 -0800397 return '<Job %s>' % (self.name,)
398
399 def inheritFrom(self, other):
400 """Copy the inheritable attributes which have been set on the other
401 job to this job."""
402
403 if not isinstance(other, Job):
404 raise Exception("Job unable to inherit from %s" % (other,))
405 for k, v in self.attributes.items():
Ricardo Carrillo Cruz4e94f612016-07-25 16:11:56 +0000406 if getattr(other, k) != v and k != 'auth':
James E. Blair83005782015-12-11 14:46:03 -0800407 setattr(self, k, getattr(other, k))
Ricardo Carrillo Cruz4e94f612016-07-25 16:11:56 +0000408 # Inherit auth only if explicitly allowed
409 if other.auth and 'inherit' in other.auth and other.auth['inherit']:
410 setattr(self, 'auth', getattr(other, 'auth'))
James E. Blairee743612012-05-29 14:49:32 -0700411
James E. Blaire421a232012-07-25 16:59:21 -0700412 def changeMatches(self, change):
James E. Blair83005782015-12-11 14:46:03 -0800413 if self.branch_matcher and not self.branch_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800414 return False
415
James E. Blair83005782015-12-11 14:46:03 -0800416 if self.file_matcher and not self.file_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800417 return False
418
James E. Blair83005782015-12-11 14:46:03 -0800419 # NB: This is a negative match.
420 if (self.irrelevant_file_matcher and
421 self.irrelevant_file_matcher.matches(change)):
Maru Newby3fe5f852015-01-13 04:22:14 +0000422 return False
423
James E. Blair70c71582013-03-06 08:50:50 -0800424 return True
James E. Blaire5a847f2012-07-10 15:29:14 -0700425
James E. Blair1e8dd892012-05-30 09:15:05 -0700426
James E. Blairee743612012-05-29 14:49:32 -0700427class JobTree(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700428 """A JobTree holds one or more Jobs to represent Job dependencies.
429
430 If Job foo should only execute if Job bar succeeds, then there will
431 be a JobTree for foo, which will contain a JobTree for bar. A JobTree
432 can hold more than one dependent JobTrees, such that jobs bar and bang
433 both depend on job foo being successful.
434
435 A root node of a JobTree will have no associated Job."""
James E. Blairee743612012-05-29 14:49:32 -0700436
437 def __init__(self, job):
438 self.job = job
439 self.job_trees = []
440
441 def addJob(self, job):
James E. Blair12a92b12014-03-26 11:54:53 -0700442 if job not in [x.job for x in self.job_trees]:
443 t = JobTree(job)
444 self.job_trees.append(t)
445 return t
James E. Blaire4ad55a2015-06-11 08:22:43 -0700446 for tree in self.job_trees:
447 if tree.job == job:
448 return tree
James E. Blairee743612012-05-29 14:49:32 -0700449
450 def getJobs(self):
451 jobs = []
452 for x in self.job_trees:
453 jobs.append(x.job)
454 jobs.extend(x.getJobs())
455 return jobs
456
457 def getJobTreeForJob(self, job):
458 if self.job == job:
459 return self
460 for tree in self.job_trees:
461 ret = tree.getJobTreeForJob(job)
462 if ret:
463 return ret
464 return None
465
James E. Blairb97ed802015-12-21 15:55:35 -0800466 def inheritFrom(self, other):
467 if other.job:
468 self.job = Job(other.job.name)
469 self.job.inheritFrom(other.job)
470 for other_tree in other.job_trees:
471 this_tree = self.getJobTreeForJob(other_tree.job)
472 if not this_tree:
473 this_tree = JobTree(None)
474 self.job_trees.append(this_tree)
475 this_tree.inheritFrom(other_tree)
476
James E. Blair1e8dd892012-05-30 09:15:05 -0700477
James E. Blair4aea70c2012-07-26 14:23:24 -0700478class Build(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700479 """A Build is an instance of a single running Job."""
480
James E. Blair4aea70c2012-07-26 14:23:24 -0700481 def __init__(self, job, uuid):
482 self.job = job
483 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -0700484 self.url = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700485 self.result = None
486 self.build_set = None
487 self.launch_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -0800488 self.start_time = None
489 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -0700490 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -0700491 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -0700492 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -0700493 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -0700494 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +0800495 self.worker = Worker()
Timothy Chavezb2332082015-08-07 20:08:04 -0500496 self.node_labels = []
497 self.node_name = None
James E. Blairee743612012-05-29 14:49:32 -0700498
499 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +0800500 return ('<Build %s of %s on %s>' %
501 (self.uuid, self.job.name, self.worker))
502
503
504class Worker(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700505 """Information about the specific worker executing a Build."""
Joshua Heskethba8776a2014-01-12 14:35:40 +0800506 def __init__(self):
507 self.name = "Unknown"
508 self.hostname = None
509 self.ips = []
510 self.fqdn = None
511 self.program = None
512 self.version = None
513 self.extra = {}
514
515 def updateFromData(self, data):
516 """Update worker information if contained in the WORK_DATA response."""
517 self.name = data.get('worker_name', self.name)
518 self.hostname = data.get('worker_hostname', self.hostname)
519 self.ips = data.get('worker_ips', self.ips)
520 self.fqdn = data.get('worker_fqdn', self.fqdn)
521 self.program = data.get('worker_program', self.program)
522 self.version = data.get('worker_version', self.version)
523 self.extra = data.get('worker_extra', self.extra)
524
525 def __repr__(self):
526 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -0700527
James E. Blair1e8dd892012-05-30 09:15:05 -0700528
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700529class RepoFiles(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700530 """RepoFiles holds config-file content for per-project job config."""
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700531 # When we ask a merger to prepare a future multiple-repo state and
532 # collect files so that we can dynamically load our configuration,
533 # this class provides easy access to that data.
534 def __init__(self):
535 self.projects = {}
536
537 def __repr__(self):
538 return '<RepoFiles %s>' % self.projects
539
540 def setFiles(self, items):
541 self.projects = {}
542 for item in items:
543 project = self.projects.setdefault(item['project'], {})
544 branch = project.setdefault(item['branch'], {})
545 branch.update(item['files'])
546
547 def getFile(self, project, branch, fn):
548 return self.projects.get(project, {}).get(branch, {}).get(fn)
549
550
James E. Blair7e530ad2012-07-03 16:12:28 -0700551class BuildSet(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700552 """Contains the Builds for a Change representing potential future state.
553
554 A BuildSet also holds the UUID used to produce the Zuul Ref that builders
555 check out.
556 """
James E. Blair4076e2b2014-01-28 12:42:20 -0800557 # Merge states:
558 NEW = 1
559 PENDING = 2
560 COMPLETE = 3
561
Antoine Musso9b229282014-08-18 23:45:43 +0200562 states_map = {
563 1: 'NEW',
564 2: 'PENDING',
565 3: 'COMPLETE',
566 }
567
James E. Blairfee8d652013-06-07 08:57:52 -0700568 def __init__(self, item):
569 self.item = item
James E. Blair11700c32012-07-05 17:50:05 -0700570 self.other_changes = []
James E. Blair7e530ad2012-07-03 16:12:28 -0700571 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -0700572 self.result = None
573 self.next_build_set = None
574 self.previous_build_set = None
James E. Blair4886cc12012-07-18 15:39:41 -0700575 self.ref = None
James E. Blair81515ad2012-10-01 18:29:08 -0700576 self.commit = None
James E. Blair4076e2b2014-01-28 12:42:20 -0800577 self.zuul_url = None
James E. Blair973721f2012-08-15 10:19:43 -0700578 self.unable_to_merge = False
James E. Blair972e3c72013-08-29 12:04:55 -0700579 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -0800580 self.merge_state = self.NEW
James E. Blair8d692392016-04-08 17:47:58 -0700581 self.nodes = {} # job -> nodes
582 self.node_requests = {} # job -> reqs
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700583 self.files = RepoFiles()
584 self.layout = None
James E. Blair7e530ad2012-07-03 16:12:28 -0700585
Antoine Musso9b229282014-08-18 23:45:43 +0200586 def __repr__(self):
587 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
588 self.item,
589 len(self.builds),
590 self.getStateName(self.merge_state))
591
James E. Blair4886cc12012-07-18 15:39:41 -0700592 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -0700593 # The change isn't enqueued until after it's created
594 # so we don't know what the other changes ahead will be
595 # until jobs start.
596 if not self.other_changes:
James E. Blairfee8d652013-06-07 08:57:52 -0700597 next_item = self.item.item_ahead
598 while next_item:
599 self.other_changes.append(next_item.change)
600 next_item = next_item.item_ahead
James E. Blair4886cc12012-07-18 15:39:41 -0700601 if not self.ref:
602 self.ref = 'Z' + uuid4().hex
603
Antoine Musso9b229282014-08-18 23:45:43 +0200604 def getStateName(self, state_num):
605 return self.states_map.get(
606 state_num, 'UNKNOWN (%s)' % state_num)
607
James E. Blair4886cc12012-07-18 15:39:41 -0700608 def addBuild(self, build):
609 self.builds[build.job.name] = build
610 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -0700611
James E. Blair4a28a882013-08-23 15:17:33 -0700612 def removeBuild(self, build):
613 del self.builds[build.job.name]
614
James E. Blair7e530ad2012-07-03 16:12:28 -0700615 def getBuild(self, job_name):
616 return self.builds.get(job_name)
617
James E. Blair11700c32012-07-05 17:50:05 -0700618 def getBuilds(self):
619 keys = self.builds.keys()
620 keys.sort()
621 return [self.builds.get(x) for x in keys]
622
James E. Blair8d692392016-04-08 17:47:58 -0700623 def getJobNodes(self, job_name):
624 # Return None if not provisioned; [] if no nodes required
625 return self.nodes.get(job_name)
626
627 def setJobNodeRequest(self, job_name, req):
628 if job_name in self.node_requests:
629 raise Exception("Prior node request for %s" % (job_name))
630 self.node_requests[job_name] = req
631
632 def getJobNodeRequest(self, job_name):
633 return self.node_requests.get(job_name)
634
635 def jobNodeRequestComplete(self, job_name, req, nodes):
636 if job_name in self.nodes:
637 raise Exception("Prior node request for %s" % (job_name))
638 self.nodes[job_name] = nodes
639 del self.node_requests[job_name]
640
James E. Blair7e530ad2012-07-03 16:12:28 -0700641
James E. Blairfee8d652013-06-07 08:57:52 -0700642class QueueItem(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700643 """Represents the position of a Change in a ChangeQueue.
644
645 All Changes are enqueued into ChangeQueue in a QueueItem. The QueueItem
646 holds the current `BuildSet` as well as all previous `BuildSets` that were
647 produced for this `QueueItem`.
648 """
James E. Blair32663402012-06-01 10:04:18 -0700649
James E. Blairbfb8e042014-12-30 17:01:44 -0800650 def __init__(self, queue, change):
651 self.pipeline = queue.pipeline
652 self.queue = queue
James E. Blairfee8d652013-06-07 08:57:52 -0700653 self.change = change # a changeish
James E. Blair7e530ad2012-07-03 16:12:28 -0700654 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -0700655 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -0700656 self.current_build_set = BuildSet(self)
657 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -0700658 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700659 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -0800660 self.enqueue_time = None
661 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -0700662 self.reported = False
James E. Blairbfb8e042014-12-30 17:01:44 -0800663 self.active = False # Whether an item is within an active window
664 self.live = True # Whether an item is intended to be processed at all
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700665 self.layout = None # This item's shadow layout
James E. Blair83005782015-12-11 14:46:03 -0800666 self.job_tree = None
James E. Blaire5a847f2012-07-10 15:29:14 -0700667
James E. Blair972e3c72013-08-29 12:04:55 -0700668 def __repr__(self):
669 if self.pipeline:
670 pipeline = self.pipeline.name
671 else:
672 pipeline = None
673 return '<QueueItem 0x%x for %s in %s>' % (
674 id(self), self.change, pipeline)
675
James E. Blairee743612012-05-29 14:49:32 -0700676 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -0700677 old = self.current_build_set
678 self.current_build_set.result = 'CANCELED'
679 self.current_build_set = BuildSet(self)
680 old.next_build_set = self.current_build_set
681 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -0700682 self.build_sets.append(self.current_build_set)
James E. Blairee743612012-05-29 14:49:32 -0700683
684 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -0700685 self.current_build_set.addBuild(build)
James E. Blair66eeebf2013-07-27 17:44:32 -0700686 build.pipeline = self.pipeline
James E. Blairee743612012-05-29 14:49:32 -0700687
James E. Blair4a28a882013-08-23 15:17:33 -0700688 def removeBuild(self, build):
689 self.current_build_set.removeBuild(build)
690
James E. Blairfee8d652013-06-07 08:57:52 -0700691 def setReportedResult(self, result):
692 self.current_build_set.result = result
693
James E. Blair83005782015-12-11 14:46:03 -0800694 def freezeJobTree(self):
695 """Find or create actual matching jobs for this item's change and
696 store the resulting job tree."""
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700697 layout = self.current_build_set.layout
698 self.job_tree = layout.createJobTree(self)
699
700 def hasJobTree(self):
701 """Returns True if the item has a job tree."""
702 return self.job_tree is not None
James E. Blair83005782015-12-11 14:46:03 -0800703
704 def getJobs(self):
705 if not self.live or not self.job_tree:
706 return []
707 return self.job_tree.getJobs()
708
James E. Blairdbfd3282016-07-21 10:46:19 -0700709 def haveAllJobsStarted(self):
710 if not self.hasJobTree():
711 return False
712 for job in self.getJobs():
713 build = self.current_build_set.getBuild(job.name)
714 if not build or not build.start_time:
715 return False
716 return True
717
718 def areAllJobsComplete(self):
719 if not self.hasJobTree():
720 return False
721 for job in self.getJobs():
722 build = self.current_build_set.getBuild(job.name)
723 if not build or not build.result:
724 return False
725 return True
726
727 def didAllJobsSucceed(self):
728 if not self.hasJobTree():
729 return False
730 for job in self.getJobs():
731 if not job.voting:
732 continue
733 build = self.current_build_set.getBuild(job.name)
734 if not build:
735 return False
736 if build.result != 'SUCCESS':
737 return False
738 return True
739
740 def didAnyJobFail(self):
741 if not self.hasJobTree():
742 return False
743 for job in self.getJobs():
744 if not job.voting:
745 continue
746 build = self.current_build_set.getBuild(job.name)
747 if build and build.result and (build.result != 'SUCCESS'):
748 return True
749 return False
750
751 def didMergerFail(self):
752 if self.current_build_set.unable_to_merge:
753 return True
754 return False
755
James E. Blairdbfd3282016-07-21 10:46:19 -0700756 def isHoldingFollowingChanges(self):
757 if not self.live:
758 return False
759 if not self.hasJobTree():
760 return False
761 for job in self.getJobs():
762 if not job.hold_following_changes:
763 continue
764 build = self.current_build_set.getBuild(job.name)
765 if not build:
766 return True
767 if build.result != 'SUCCESS':
768 return True
769
770 if not self.item_ahead:
771 return False
772 return self.item_ahead.isHoldingFollowingChanges()
773
774 def _findJobsToRun(self, job_trees, mutex):
775 torun = []
James E. Blair791b5392016-08-03 11:25:56 -0700776 if self.item_ahead:
777 # Only run jobs if any 'hold' jobs on the change ahead
778 # have completed successfully.
779 if self.item_ahead.isHoldingFollowingChanges():
780 return []
James E. Blairdbfd3282016-07-21 10:46:19 -0700781 for tree in job_trees:
782 job = tree.job
783 result = None
784 if job:
785 if not job.changeMatches(self.change):
786 continue
787 build = self.current_build_set.getBuild(job.name)
788 if build:
789 result = build.result
790 else:
791 # There is no build for the root of this job tree,
792 # so we should run it.
793 if mutex.acquire(self, job):
794 # If this job needs a mutex, either acquire it or make
795 # sure that we have it before running the job.
796 torun.append(job)
797 # If there is no job, this is a null job tree, and we should
798 # run all of its jobs.
799 if result == 'SUCCESS' or not job:
800 torun.extend(self._findJobsToRun(tree.job_trees, mutex))
801 return torun
802
803 def findJobsToRun(self, mutex):
804 if not self.live:
805 return []
806 tree = self.job_tree
807 if not tree:
808 return []
809 return self._findJobsToRun(tree.job_trees, mutex)
810
811 def _findJobsToRequest(self, job_trees):
812 toreq = []
813 for tree in job_trees:
814 job = tree.job
815 if job:
816 if not job.changeMatches(self.change):
817 continue
818 nodes = self.current_build_set.getJobNodes(job.name)
819 if nodes is None:
820 req = self.current_build_set.getJobNodeRequest(job.name)
821 if req is None:
822 toreq.append(job)
823 # If there is no job, this is a null job tree, and we should
824 # run all of its jobs.
825 if not job:
826 toreq.extend(self._findJobsToRequest(tree.job_trees))
827 return toreq
828
829 def findJobsToRequest(self):
830 if not self.live:
831 return []
832 tree = self.job_tree
833 if not tree:
834 return []
835 return self._findJobsToRequest(tree.job_trees)
836
837 def setResult(self, build):
838 if build.retry:
839 self.removeBuild(build)
840 elif build.result != 'SUCCESS':
841 # Get a JobTree from a Job so we can find only its dependent jobs
842 tree = self.job_tree.getJobTreeForJob(build.job)
843 for job in tree.getJobs():
844 fakebuild = Build(job, None)
845 fakebuild.result = 'SKIPPED'
846 self.addBuild(fakebuild)
847
848 def setDequeuedNeedingChange(self):
849 self.dequeued_needing_change = True
850 self._setAllJobsSkipped()
851
852 def setUnableToMerge(self):
853 self.current_build_set.unable_to_merge = True
854 self._setAllJobsSkipped()
855
856 def _setAllJobsSkipped(self):
857 for job in self.getJobs():
858 fakebuild = Build(job, None)
859 fakebuild.result = 'SKIPPED'
860 self.addBuild(fakebuild)
861
James E. Blairb7273ef2016-04-19 08:58:51 -0700862 def formatJobResult(self, job, url_pattern=None):
863 build = self.current_build_set.getBuild(job.name)
864 result = build.result
865 pattern = url_pattern
866 if result == 'SUCCESS':
867 if job.success_message:
868 result = job.success_message
James E. Blaira5dba232016-08-08 15:53:24 -0700869 if job.success_url:
870 pattern = job.success_url
James E. Blairb7273ef2016-04-19 08:58:51 -0700871 elif result == 'FAILURE':
872 if job.failure_message:
873 result = job.failure_message
James E. Blaira5dba232016-08-08 15:53:24 -0700874 if job.failure_url:
875 pattern = job.failure_url
James E. Blairb7273ef2016-04-19 08:58:51 -0700876 url = None
877 if pattern:
878 try:
879 url = pattern.format(change=self.change,
880 pipeline=self.pipeline,
881 job=job,
882 build=build)
883 except Exception:
884 pass # FIXME: log this or something?
885 if not url:
886 url = build.url or job.name
887 return (result, url)
888
889 def formatJSON(self, url_pattern=None):
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800890 changeish = self.change
891 ret = {}
892 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -0800893 ret['live'] = self.live
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800894 if hasattr(changeish, 'url') and changeish.url is not None:
895 ret['url'] = changeish.url
896 else:
897 ret['url'] = None
898 ret['id'] = changeish._id()
899 if self.item_ahead:
900 ret['item_ahead'] = self.item_ahead.change._id()
901 else:
902 ret['item_ahead'] = None
903 ret['items_behind'] = [i.change._id() for i in self.items_behind]
904 ret['failing_reasons'] = self.current_build_set.failing_reasons
905 ret['zuul_ref'] = self.current_build_set.ref
Ramy Asselin07cc33c2015-06-12 14:06:34 -0700906 if changeish.project:
907 ret['project'] = changeish.project.name
908 else:
909 # For cross-project dependencies with the depends-on
910 # project not known to zuul, the project is None
911 # Set it to a static value
912 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800913 ret['enqueue_time'] = int(self.enqueue_time * 1000)
914 ret['jobs'] = []
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -0500915 if hasattr(changeish, 'owner'):
916 ret['owner'] = changeish.owner
917 else:
918 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800919 max_remaining = 0
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700920 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800921 now = time.time()
922 build = self.current_build_set.getBuild(job.name)
923 elapsed = None
924 remaining = None
925 result = None
James E. Blairb7273ef2016-04-19 08:58:51 -0700926 build_url = None
927 report_url = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800928 worker = None
929 if build:
930 result = build.result
James E. Blairb7273ef2016-04-19 08:58:51 -0700931 build_url = build.url
932 (unused, report_url) = self.formatJobResult(job, url_pattern)
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800933 if build.start_time:
934 if build.end_time:
935 elapsed = int((build.end_time -
936 build.start_time) * 1000)
937 remaining = 0
938 else:
939 elapsed = int((now - build.start_time) * 1000)
940 if build.estimated_time:
941 remaining = max(
942 int(build.estimated_time * 1000) - elapsed,
943 0)
944 worker = {
945 'name': build.worker.name,
946 'hostname': build.worker.hostname,
947 'ips': build.worker.ips,
948 'fqdn': build.worker.fqdn,
949 'program': build.worker.program,
950 'version': build.worker.version,
951 'extra': build.worker.extra
952 }
953 if remaining and remaining > max_remaining:
954 max_remaining = remaining
955
956 ret['jobs'].append({
957 'name': job.name,
958 'elapsed_time': elapsed,
959 'remaining_time': remaining,
James E. Blairb7273ef2016-04-19 08:58:51 -0700960 'url': build_url,
961 'report_url': report_url,
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800962 'result': result,
963 'voting': job.voting,
964 'uuid': build.uuid if build else None,
965 'launch_time': build.launch_time if build else None,
966 'start_time': build.start_time if build else None,
967 'end_time': build.end_time if build else None,
968 'estimated_time': build.estimated_time if build else None,
969 'pipeline': build.pipeline.name if build else None,
970 'canceled': build.canceled if build else None,
971 'retry': build.retry if build else None,
Timothy Chavezb2332082015-08-07 20:08:04 -0500972 'node_labels': build.node_labels if build else [],
973 'node_name': build.node_name if build else None,
974 'worker': worker,
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800975 })
976
James E. Blairdbfd3282016-07-21 10:46:19 -0700977 if self.haveAllJobsStarted():
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800978 ret['remaining_time'] = max_remaining
979 else:
980 ret['remaining_time'] = None
981 return ret
982
983 def formatStatus(self, indent=0, html=False):
984 changeish = self.change
985 indent_str = ' ' * indent
986 ret = ''
987 if html and hasattr(changeish, 'url') and changeish.url is not None:
988 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
989 indent_str,
990 changeish.project.name,
991 changeish.url,
992 changeish._id())
993 else:
994 ret += '%sProject %s change %s based on %s\n' % (
995 indent_str,
996 changeish.project.name,
997 changeish._id(),
998 self.item_ahead)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700999 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001000 build = self.current_build_set.getBuild(job.name)
1001 if build:
1002 result = build.result
1003 else:
1004 result = None
1005 job_name = job.name
1006 if not job.voting:
1007 voting = ' (non-voting)'
1008 else:
1009 voting = ''
1010 if html:
1011 if build:
1012 url = build.url
1013 else:
1014 url = None
1015 if url is not None:
1016 job_name = '<a href="%s">%s</a>' % (url, job_name)
1017 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
1018 ret += '\n'
1019 return ret
1020
James E. Blairfee8d652013-06-07 08:57:52 -07001021
1022class Changeish(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001023 """Base class for Change and Ref."""
James E. Blairfee8d652013-06-07 08:57:52 -07001024
1025 def __init__(self, project):
1026 self.project = project
1027
Joshua Hesketh36c3fa52014-01-22 11:40:52 +11001028 def getBasePath(self):
1029 base_path = ''
1030 if hasattr(self, 'refspec'):
1031 base_path = "%s/%s/%s" % (
1032 self.number[-2:], self.number, self.patchset)
1033 elif hasattr(self, 'ref'):
1034 base_path = "%s/%s" % (self.newrev[:2], self.newrev)
1035
1036 return base_path
1037
James E. Blairfee8d652013-06-07 08:57:52 -07001038 def equals(self, other):
1039 raise NotImplementedError()
1040
1041 def isUpdateOf(self, other):
1042 raise NotImplementedError()
1043
1044 def filterJobs(self, jobs):
1045 return filter(lambda job: job.changeMatches(self), jobs)
1046
1047 def getRelatedChanges(self):
1048 return set()
1049
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001050 def updatesConfig(self):
1051 return False
1052
James E. Blair1e8dd892012-05-30 09:15:05 -07001053
James E. Blair4aea70c2012-07-26 14:23:24 -07001054class Change(Changeish):
Monty Taylora42a55b2016-07-29 07:53:33 -07001055 """A proposed new state for a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001056 def __init__(self, project):
1057 super(Change, self).__init__(project)
1058 self.branch = None
1059 self.number = None
1060 self.url = None
1061 self.patchset = None
1062 self.refspec = None
1063
James E. Blair70c71582013-03-06 08:50:50 -08001064 self.files = []
James E. Blair6965a4b2014-12-16 17:19:04 -08001065 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -07001066 self.needed_by_changes = []
1067 self.is_current_patchset = True
1068 self.can_merge = False
1069 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -07001070 self.failed_to_merge = False
James E. Blairc053d022014-01-22 14:57:33 -08001071 self.approvals = []
James E. Blair11041d22014-05-02 14:49:53 -07001072 self.open = None
1073 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001074 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001075
1076 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -07001077 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -07001078
1079 def __repr__(self):
1080 return '<Change 0x%x %s>' % (id(self), self._id())
1081
1082 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +08001083 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -07001084 return True
1085 return False
1086
James E. Blair2fa50962013-01-30 21:50:41 -08001087 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -08001088 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -07001089 (hasattr(other, 'patchset') and
1090 self.patchset is not None and
1091 other.patchset is not None and
1092 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -08001093 return True
1094 return False
1095
James E. Blairfee8d652013-06-07 08:57:52 -07001096 def getRelatedChanges(self):
1097 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -08001098 for c in self.needs_changes:
1099 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -07001100 for c in self.needed_by_changes:
1101 related.add(c)
1102 related.update(c.getRelatedChanges())
1103 return related
James E. Blair4aea70c2012-07-26 14:23:24 -07001104
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001105 def updatesConfig(self):
1106 if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files:
1107 return True
1108 return False
1109
James E. Blair4aea70c2012-07-26 14:23:24 -07001110
1111class Ref(Changeish):
Monty Taylora42a55b2016-07-29 07:53:33 -07001112 """An existing state of a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001113 def __init__(self, project):
James E. Blairbe765db2012-08-07 08:36:20 -07001114 super(Ref, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -07001115 self.ref = None
1116 self.oldrev = None
1117 self.newrev = None
1118
James E. Blairbe765db2012-08-07 08:36:20 -07001119 def _id(self):
1120 return self.newrev
1121
Antoine Musso68bdcd72013-01-17 12:31:28 +01001122 def __repr__(self):
1123 rep = None
1124 if self.newrev == '0000000000000000000000000000000000000000':
1125 rep = '<Ref 0x%x deletes %s from %s' % (
1126 id(self), self.ref, self.oldrev)
1127 elif self.oldrev == '0000000000000000000000000000000000000000':
1128 rep = '<Ref 0x%x creates %s on %s>' % (
1129 id(self), self.ref, self.newrev)
1130 else:
1131 # Catch all
1132 rep = '<Ref 0x%x %s updated %s..%s>' % (
1133 id(self), self.ref, self.oldrev, self.newrev)
1134
1135 return rep
1136
James E. Blair4aea70c2012-07-26 14:23:24 -07001137 def equals(self, other):
James E. Blair9358c612012-09-28 08:29:39 -07001138 if (self.project == other.project
1139 and self.ref == other.ref
1140 and self.newrev == other.newrev):
James E. Blair4aea70c2012-07-26 14:23:24 -07001141 return True
1142 return False
1143
James E. Blair2fa50962013-01-30 21:50:41 -08001144 def isUpdateOf(self, other):
1145 return False
1146
James E. Blair4aea70c2012-07-26 14:23:24 -07001147
James E. Blair63bb0ef2013-07-29 17:14:51 -07001148class NullChange(Changeish):
James E. Blair23161912016-07-28 15:42:14 -07001149 # TODOv3(jeblair): remove this in favor of enqueueing Refs (eg
1150 # current master) instead.
James E. Blaire5910202013-12-27 09:50:31 -08001151 def __repr__(self):
1152 return '<NullChange for %s>' % (self.project)
James E. Blair63bb0ef2013-07-29 17:14:51 -07001153
James E. Blair63bb0ef2013-07-29 17:14:51 -07001154 def _id(self):
Alex Gaynorddb9ef32013-09-16 21:04:58 -07001155 return None
James E. Blair63bb0ef2013-07-29 17:14:51 -07001156
1157 def equals(self, other):
Steve Varnau7b78b312015-04-03 14:49:46 -07001158 if (self.project == other.project
1159 and other._id() is None):
James E. Blair4f6033c2014-03-27 15:49:09 -07001160 return True
James E. Blair63bb0ef2013-07-29 17:14:51 -07001161 return False
1162
1163 def isUpdateOf(self, other):
1164 return False
1165
1166
James E. Blairee743612012-05-29 14:49:32 -07001167class TriggerEvent(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001168 """Incoming event from an external system."""
James E. Blairee743612012-05-29 14:49:32 -07001169 def __init__(self):
1170 self.data = None
James E. Blair32663402012-06-01 10:04:18 -07001171 # common
James E. Blairee743612012-05-29 14:49:32 -07001172 self.type = None
1173 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -07001174 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +01001175 # Representation of the user account that performed the event.
1176 self.account = None
James E. Blair32663402012-06-01 10:04:18 -07001177 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -07001178 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -07001179 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -07001180 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -07001181 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -07001182 self.approvals = []
1183 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07001184 self.comment = None
James E. Blair32663402012-06-01 10:04:18 -07001185 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -07001186 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -07001187 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07001188 self.newrev = None
James E. Blair63bb0ef2013-07-29 17:14:51 -07001189 # timer
1190 self.timespec = None
James E. Blairc494d542014-08-06 09:23:52 -07001191 # zuultrigger
1192 self.pipeline_name = None
James E. Blairad28e912013-11-27 10:43:22 -08001193 # For events that arrive with a destination pipeline (eg, from
1194 # an admin command, etc):
1195 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07001196
James E. Blair9f9667e2012-06-12 17:51:08 -07001197 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001198 ret = '<TriggerEvent %s %s' % (self.type, self.project_name)
James E. Blair1e8dd892012-05-30 09:15:05 -07001199
James E. Blairee743612012-05-29 14:49:32 -07001200 if self.branch:
1201 ret += " %s" % self.branch
1202 if self.change_number:
1203 ret += " %s,%s" % (self.change_number, self.patch_number)
1204 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -07001205 ret += ' ' + ', '.join(
1206 ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
James E. Blairee743612012-05-29 14:49:32 -07001207 ret += '>'
1208
1209 return ret
1210
James E. Blair1e8dd892012-05-30 09:15:05 -07001211
James E. Blair9c17dbf2014-06-23 14:21:58 -07001212class BaseFilter(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001213 """Base Class for filtering which Changes and Events to process."""
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001214 def __init__(self, required_approvals=[], reject_approvals=[]):
James E. Blair5bf78a32015-07-30 18:08:24 +00001215 self._required_approvals = copy.deepcopy(required_approvals)
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001216 self.required_approvals = self._tidy_approvals(required_approvals)
1217 self._reject_approvals = copy.deepcopy(reject_approvals)
1218 self.reject_approvals = self._tidy_approvals(reject_approvals)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001219
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001220 def _tidy_approvals(self, approvals):
1221 for a in approvals:
James E. Blair9c17dbf2014-06-23 14:21:58 -07001222 for k, v in a.items():
1223 if k == 'username':
James E. Blairb01ec542016-06-16 09:46:49 -07001224 a['username'] = re.compile(v)
James E. Blair1fbfceb2014-06-23 14:42:53 -07001225 elif k in ['email', 'email-filter']:
James E. Blair5bf78a32015-07-30 18:08:24 +00001226 a['email'] = re.compile(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001227 elif k == 'newer-than':
James E. Blair5bf78a32015-07-30 18:08:24 +00001228 a[k] = time_to_seconds(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001229 elif k == 'older-than':
James E. Blair5bf78a32015-07-30 18:08:24 +00001230 a[k] = time_to_seconds(v)
James E. Blair1fbfceb2014-06-23 14:42:53 -07001231 if 'email-filter' in a:
1232 del a['email-filter']
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001233 return approvals
1234
1235 def _match_approval_required_approval(self, rapproval, approval):
1236 # Check if the required approval and approval match
1237 if 'description' not in approval:
1238 return False
1239 now = time.time()
1240 by = approval.get('by', {})
1241 for k, v in rapproval.items():
1242 if k == 'username':
James E. Blairb01ec542016-06-16 09:46:49 -07001243 if (not v.search(by.get('username', ''))):
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001244 return False
1245 elif k == 'email':
1246 if (not v.search(by.get('email', ''))):
1247 return False
1248 elif k == 'newer-than':
1249 t = now - v
1250 if (approval['grantedOn'] < t):
1251 return False
1252 elif k == 'older-than':
1253 t = now - v
1254 if (approval['grantedOn'] >= t):
1255 return False
1256 else:
1257 if not isinstance(v, list):
1258 v = [v]
1259 if (normalizeCategory(approval['description']) != k or
1260 int(approval['value']) not in v):
1261 return False
1262 return True
1263
1264 def matchesApprovals(self, change):
1265 if (self.required_approvals and not change.approvals
1266 or self.reject_approvals and not change.approvals):
1267 # A change with no approvals can not match
1268 return False
1269
1270 # TODO(jhesketh): If we wanted to optimise this slightly we could
1271 # analyse both the REQUIRE and REJECT filters by looping over the
1272 # approvals on the change and keeping track of what we have checked
1273 # rather than needing to loop on the change approvals twice
1274 return (self.matchesRequiredApprovals(change) and
1275 self.matchesNoRejectApprovals(change))
James E. Blair9c17dbf2014-06-23 14:21:58 -07001276
1277 def matchesRequiredApprovals(self, change):
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001278 # Check if any approvals match the requirements
James E. Blair5bf78a32015-07-30 18:08:24 +00001279 for rapproval in self.required_approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001280 matches_rapproval = False
James E. Blair9c17dbf2014-06-23 14:21:58 -07001281 for approval in change.approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001282 if self._match_approval_required_approval(rapproval, approval):
1283 # We have a matching approval so this requirement is
1284 # fulfilled
1285 matches_rapproval = True
James E. Blair5bf78a32015-07-30 18:08:24 +00001286 break
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001287 if not matches_rapproval:
James E. Blair5bf78a32015-07-30 18:08:24 +00001288 return False
James E. Blair9c17dbf2014-06-23 14:21:58 -07001289 return True
1290
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001291 def matchesNoRejectApprovals(self, change):
1292 # Check to make sure no approvals match a reject criteria
1293 for rapproval in self.reject_approvals:
1294 for approval in change.approvals:
1295 if self._match_approval_required_approval(rapproval, approval):
1296 # A reject approval has been matched, so we reject
1297 # immediately
1298 return False
1299 # To get here no rejects can have been matched so we should be good to
1300 # queue
1301 return True
1302
James E. Blair9c17dbf2014-06-23 14:21:58 -07001303
1304class EventFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07001305 """Allows a Pipeline to only respond to certain events."""
James E. Blairc0dedf82014-08-06 09:37:52 -07001306 def __init__(self, trigger, types=[], branches=[], refs=[],
1307 event_approvals={}, comments=[], emails=[], usernames=[],
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001308 timespecs=[], required_approvals=[], reject_approvals=[],
1309 pipelines=[], ignore_deletes=True):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001310 super(EventFilter, self).__init__(
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001311 required_approvals=required_approvals,
1312 reject_approvals=reject_approvals)
James E. Blairc0dedf82014-08-06 09:37:52 -07001313 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07001314 self._types = types
1315 self._branches = branches
1316 self._refs = refs
James E. Blair1fbfceb2014-06-23 14:42:53 -07001317 self._comments = comments
1318 self._emails = emails
1319 self._usernames = usernames
James E. Blairc494d542014-08-06 09:23:52 -07001320 self._pipelines = pipelines
James E. Blairee743612012-05-29 14:49:32 -07001321 self.types = [re.compile(x) for x in types]
1322 self.branches = [re.compile(x) for x in branches]
1323 self.refs = [re.compile(x) for x in refs]
James E. Blair1fbfceb2014-06-23 14:42:53 -07001324 self.comments = [re.compile(x) for x in comments]
1325 self.emails = [re.compile(x) for x in emails]
1326 self.usernames = [re.compile(x) for x in usernames]
James E. Blairc494d542014-08-06 09:23:52 -07001327 self.pipelines = [re.compile(x) for x in pipelines]
James E. Blairc053d022014-01-22 14:57:33 -08001328 self.event_approvals = event_approvals
James E. Blair63bb0ef2013-07-29 17:14:51 -07001329 self.timespecs = timespecs
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001330 self.ignore_deletes = ignore_deletes
James E. Blairee743612012-05-29 14:49:32 -07001331
James E. Blair9f9667e2012-06-12 17:51:08 -07001332 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001333 ret = '<EventFilter'
James E. Blair1e8dd892012-05-30 09:15:05 -07001334
James E. Blairee743612012-05-29 14:49:32 -07001335 if self._types:
1336 ret += ' types: %s' % ', '.join(self._types)
James E. Blairc494d542014-08-06 09:23:52 -07001337 if self._pipelines:
1338 ret += ' pipelines: %s' % ', '.join(self._pipelines)
James E. Blairee743612012-05-29 14:49:32 -07001339 if self._branches:
1340 ret += ' branches: %s' % ', '.join(self._branches)
1341 if self._refs:
1342 ret += ' refs: %s' % ', '.join(self._refs)
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001343 if self.ignore_deletes:
1344 ret += ' ignore_deletes: %s' % self.ignore_deletes
James E. Blairc053d022014-01-22 14:57:33 -08001345 if self.event_approvals:
1346 ret += ' event_approvals: %s' % ', '.join(
1347 ['%s:%s' % a for a in self.event_approvals.items()])
James E. Blair5bf78a32015-07-30 18:08:24 +00001348 if self.required_approvals:
1349 ret += ' required_approvals: %s' % ', '.join(
1350 ['%s' % a for a in self._required_approvals])
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001351 if self.reject_approvals:
1352 ret += ' reject_approvals: %s' % ', '.join(
1353 ['%s' % a for a in self._reject_approvals])
James E. Blair1fbfceb2014-06-23 14:42:53 -07001354 if self._comments:
1355 ret += ' comments: %s' % ', '.join(self._comments)
1356 if self._emails:
1357 ret += ' emails: %s' % ', '.join(self._emails)
1358 if self._usernames:
1359 ret += ' username_filters: %s' % ', '.join(self._usernames)
James E. Blair63bb0ef2013-07-29 17:14:51 -07001360 if self.timespecs:
1361 ret += ' timespecs: %s' % ', '.join(self.timespecs)
James E. Blairee743612012-05-29 14:49:32 -07001362 ret += '>'
1363
1364 return ret
1365
James E. Blairc053d022014-01-22 14:57:33 -08001366 def matches(self, event, change):
James E. Blairee743612012-05-29 14:49:32 -07001367 # event types are ORed
1368 matches_type = False
1369 for etype in self.types:
1370 if etype.match(event.type):
1371 matches_type = True
1372 if self.types and not matches_type:
1373 return False
1374
James E. Blairc494d542014-08-06 09:23:52 -07001375 # pipelines are ORed
1376 matches_pipeline = False
1377 for epipe in self.pipelines:
1378 if epipe.match(event.pipeline_name):
1379 matches_pipeline = True
1380 if self.pipelines and not matches_pipeline:
1381 return False
1382
James E. Blairee743612012-05-29 14:49:32 -07001383 # branches are ORed
1384 matches_branch = False
1385 for branch in self.branches:
1386 if branch.match(event.branch):
1387 matches_branch = True
1388 if self.branches and not matches_branch:
1389 return False
1390
1391 # refs are ORed
1392 matches_ref = False
Yolanda Robla16698872014-08-25 11:59:27 +02001393 if event.ref is not None:
1394 for ref in self.refs:
1395 if ref.match(event.ref):
1396 matches_ref = True
James E. Blairee743612012-05-29 14:49:32 -07001397 if self.refs and not matches_ref:
1398 return False
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001399 if self.ignore_deletes and event.newrev == EMPTY_GIT_REF:
1400 # If the updated ref has an empty git sha (all 0s),
1401 # then the ref is being deleted
1402 return False
James E. Blairee743612012-05-29 14:49:32 -07001403
James E. Blair1fbfceb2014-06-23 14:42:53 -07001404 # comments are ORed
1405 matches_comment_re = False
1406 for comment_re in self.comments:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001407 if (event.comment is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001408 comment_re.search(event.comment)):
1409 matches_comment_re = True
1410 if self.comments and not matches_comment_re:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001411 return False
1412
Antoine Mussob4e809e2012-12-06 16:58:06 +01001413 # We better have an account provided by Gerrit to do
1414 # email filtering.
1415 if event.account is not None:
James E. Blaircf429f32012-12-20 14:28:24 -08001416 account_email = event.account.get('email')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001417 # emails are ORed
1418 matches_email_re = False
1419 for email_re in self.emails:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001420 if (account_email is not None and
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001421 email_re.search(account_email)):
James E. Blair1fbfceb2014-06-23 14:42:53 -07001422 matches_email_re = True
1423 if self.emails and not matches_email_re:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001424 return False
1425
James E. Blair1fbfceb2014-06-23 14:42:53 -07001426 # usernames are ORed
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001427 account_username = event.account.get('username')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001428 matches_username_re = False
1429 for username_re in self.usernames:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001430 if (account_username is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001431 username_re.search(account_username)):
1432 matches_username_re = True
1433 if self.usernames and not matches_username_re:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001434 return False
1435
James E. Blairee743612012-05-29 14:49:32 -07001436 # approvals are ANDed
James E. Blairc053d022014-01-22 14:57:33 -08001437 for category, value in self.event_approvals.items():
James E. Blairee743612012-05-29 14:49:32 -07001438 matches_approval = False
1439 for eapproval in event.approvals:
1440 if (normalizeCategory(eapproval['description']) == category and
1441 int(eapproval['value']) == int(value)):
1442 matches_approval = True
James E. Blair1e8dd892012-05-30 09:15:05 -07001443 if not matches_approval:
1444 return False
James E. Blair63bb0ef2013-07-29 17:14:51 -07001445
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001446 # required approvals are ANDed (reject approvals are ORed)
1447 if not self.matchesApprovals(change):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001448 return False
James E. Blairc053d022014-01-22 14:57:33 -08001449
James E. Blair63bb0ef2013-07-29 17:14:51 -07001450 # timespecs are ORed
1451 matches_timespec = False
1452 for timespec in self.timespecs:
1453 if (event.timespec == timespec):
1454 matches_timespec = True
1455 if self.timespecs and not matches_timespec:
1456 return False
1457
James E. Blairee743612012-05-29 14:49:32 -07001458 return True
James E. Blaireff88162013-07-01 12:44:14 -04001459
1460
James E. Blair9c17dbf2014-06-23 14:21:58 -07001461class ChangeishFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07001462 """Allows a Manager to only enqueue Changes that meet certain criteria."""
Clark Boylana9702ad2014-05-08 17:17:24 -07001463 def __init__(self, open=None, current_patchset=None,
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001464 statuses=[], required_approvals=[],
1465 reject_approvals=[]):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001466 super(ChangeishFilter, self).__init__(
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001467 required_approvals=required_approvals,
1468 reject_approvals=reject_approvals)
James E. Blair11041d22014-05-02 14:49:53 -07001469 self.open = open
Clark Boylana9702ad2014-05-08 17:17:24 -07001470 self.current_patchset = current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001471 self.statuses = statuses
James E. Blair11041d22014-05-02 14:49:53 -07001472
1473 def __repr__(self):
1474 ret = '<ChangeishFilter'
1475
1476 if self.open is not None:
1477 ret += ' open: %s' % self.open
Clark Boylana9702ad2014-05-08 17:17:24 -07001478 if self.current_patchset is not None:
1479 ret += ' current-patchset: %s' % self.current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001480 if self.statuses:
1481 ret += ' statuses: %s' % ', '.join(self.statuses)
James E. Blair5bf78a32015-07-30 18:08:24 +00001482 if self.required_approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001483 ret += (' required_approvals: %s' %
1484 str(self.required_approvals))
1485 if self.reject_approvals:
1486 ret += (' reject_approvals: %s' %
1487 str(self.reject_approvals))
James E. Blair11041d22014-05-02 14:49:53 -07001488 ret += '>'
1489
1490 return ret
1491
1492 def matches(self, change):
1493 if self.open is not None:
1494 if self.open != change.open:
1495 return False
1496
Clark Boylana9702ad2014-05-08 17:17:24 -07001497 if self.current_patchset is not None:
1498 if self.current_patchset != change.is_current_patchset:
1499 return False
1500
James E. Blair11041d22014-05-02 14:49:53 -07001501 if self.statuses:
1502 if change.status not in self.statuses:
1503 return False
1504
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001505 # required approvals are ANDed (reject approvals are ORed)
1506 if not self.matchesApprovals(change):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001507 return False
James E. Blair11041d22014-05-02 14:49:53 -07001508
1509 return True
1510
1511
James E. Blairb97ed802015-12-21 15:55:35 -08001512class ProjectPipelineConfig(object):
1513 # Represents a project cofiguration in the context of a pipeline
1514 def __init__(self):
1515 self.job_tree = None
1516 self.queue_name = None
1517 # TODOv3(jeblair): add merge mode
1518
1519
1520class ProjectConfig(object):
1521 # Represents a project cofiguration
1522 def __init__(self, name):
1523 self.name = name
1524 self.pipelines = {}
1525
1526
James E. Blaird8e778f2015-12-22 14:09:20 -08001527class UnparsedAbideConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001528 """A collection of yaml lists that has not yet been parsed into objects.
1529
1530 An Abide is a collection of tenants.
1531 """
1532
James E. Blaird8e778f2015-12-22 14:09:20 -08001533 def __init__(self):
1534 self.tenants = []
1535
1536 def extend(self, conf):
1537 if isinstance(conf, UnparsedAbideConfig):
1538 self.tenants.extend(conf.tenants)
1539 return
1540
1541 if not isinstance(conf, list):
1542 raise Exception("Configuration items must be in the form of "
1543 "a list of dictionaries (when parsing %s)" %
1544 (conf,))
1545 for item in conf:
1546 if not isinstance(item, dict):
1547 raise Exception("Configuration items must be in the form of "
1548 "a list of dictionaries (when parsing %s)" %
1549 (conf,))
1550 if len(item.keys()) > 1:
1551 raise Exception("Configuration item dictionaries must have "
1552 "a single key (when parsing %s)" %
1553 (conf,))
1554 key, value = item.items()[0]
1555 if key == 'tenant':
1556 self.tenants.append(value)
1557 else:
1558 raise Exception("Configuration item not recognized "
1559 "(when parsing %s)" %
1560 (conf,))
1561
1562
1563class UnparsedTenantConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001564 """A collection of yaml lists that has not yet been parsed into objects."""
1565
James E. Blaird8e778f2015-12-22 14:09:20 -08001566 def __init__(self):
1567 self.pipelines = []
1568 self.jobs = []
1569 self.project_templates = []
1570 self.projects = []
1571
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001572 def copy(self):
1573 r = UnparsedTenantConfig()
1574 r.pipelines = copy.deepcopy(self.pipelines)
1575 r.jobs = copy.deepcopy(self.jobs)
1576 r.project_templates = copy.deepcopy(self.project_templates)
1577 r.projects = copy.deepcopy(self.projects)
1578 return r
1579
James E. Blair4317e9f2016-07-15 10:05:47 -07001580 def extend(self, conf, source_project=None):
James E. Blaird8e778f2015-12-22 14:09:20 -08001581 if isinstance(conf, UnparsedTenantConfig):
1582 self.pipelines.extend(conf.pipelines)
1583 self.jobs.extend(conf.jobs)
1584 self.project_templates.extend(conf.project_templates)
1585 self.projects.extend(conf.projects)
1586 return
1587
1588 if not isinstance(conf, list):
1589 raise Exception("Configuration items must be in the form of "
1590 "a list of dictionaries (when parsing %s)" %
1591 (conf,))
1592 for item in conf:
1593 if not isinstance(item, dict):
1594 raise Exception("Configuration items must be in the form of "
1595 "a list of dictionaries (when parsing %s)" %
1596 (conf,))
1597 if len(item.keys()) > 1:
1598 raise Exception("Configuration item dictionaries must have "
1599 "a single key (when parsing %s)" %
1600 (conf,))
1601 key, value = item.items()[0]
1602 if key == 'project':
1603 self.projects.append(value)
1604 elif key == 'job':
James E. Blair4317e9f2016-07-15 10:05:47 -07001605 if source_project is not None:
1606 value['_source_project'] = source_project
James E. Blaird8e778f2015-12-22 14:09:20 -08001607 self.jobs.append(value)
1608 elif key == 'project-template':
1609 self.project_templates.append(value)
1610 elif key == 'pipeline':
1611 self.pipelines.append(value)
1612 else:
Morgan Fainberg4245a422016-08-05 16:20:12 -07001613 raise Exception("Configuration item `%s` not recognized "
James E. Blaird8e778f2015-12-22 14:09:20 -08001614 "(when parsing %s)" %
Morgan Fainberg4245a422016-08-05 16:20:12 -07001615 (item, conf,))
James E. Blaird8e778f2015-12-22 14:09:20 -08001616
1617
James E. Blaireff88162013-07-01 12:44:14 -04001618class Layout(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001619 """Holds all of the Pipelines."""
1620
James E. Blaireff88162013-07-01 12:44:14 -04001621 def __init__(self):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001622 self.tenant = None
James E. Blaireff88162013-07-01 12:44:14 -04001623 self.projects = {}
James E. Blairb97ed802015-12-21 15:55:35 -08001624 self.project_configs = {}
1625 self.project_templates = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07001626 self.pipelines = OrderedDict()
James E. Blair83005782015-12-11 14:46:03 -08001627 # This is a dictionary of name -> [jobs]. The first element
1628 # of the list is the first job added with that name. It is
1629 # the reference definition for a given job. Subsequent
1630 # elements are aspects of that job with different matchers
1631 # that override some attribute of the job. These aspects all
1632 # inherit from the reference definition.
James E. Blaireff88162013-07-01 12:44:14 -04001633 self.jobs = {}
James E. Blaireff88162013-07-01 12:44:14 -04001634
1635 def getJob(self, name):
1636 if name in self.jobs:
James E. Blair83005782015-12-11 14:46:03 -08001637 return self.jobs[name][0]
James E. Blairb97ed802015-12-21 15:55:35 -08001638 raise Exception("Job %s not defined" % (name,))
James E. Blair83005782015-12-11 14:46:03 -08001639
1640 def getJobs(self, name):
1641 return self.jobs.get(name, [])
1642
1643 def addJob(self, job):
James E. Blair4317e9f2016-07-15 10:05:47 -07001644 # We can have multiple variants of a job all with the same
1645 # name, but these variants must all be defined in the same repo.
1646 prior_jobs = [j for j in self.getJobs(job.name)
1647 if j.source_project != job.source_project]
1648 if prior_jobs:
1649 raise Exception("Job %s in %s is not permitted to shadow "
1650 "job %s in %s" % (job, job.source_project,
1651 prior_jobs[0],
1652 prior_jobs[0].source_project))
1653
James E. Blair83005782015-12-11 14:46:03 -08001654 if job.name in self.jobs:
1655 self.jobs[job.name].append(job)
1656 else:
1657 self.jobs[job.name] = [job]
1658
1659 def addPipeline(self, pipeline):
1660 self.pipelines[pipeline.name] = pipeline
James E. Blair59fdbac2015-12-07 17:08:06 -08001661
James E. Blairb97ed802015-12-21 15:55:35 -08001662 def addProjectTemplate(self, project_template):
1663 self.project_templates[project_template.name] = project_template
1664
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001665 def addProjectConfig(self, project_config, update_pipeline=True):
James E. Blairb97ed802015-12-21 15:55:35 -08001666 self.project_configs[project_config.name] = project_config
1667 # TODOv3(jeblair): tidy up the relationship between pipelines
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001668 # and projects and projectconfigs. Specifically, move
1669 # job_trees out of the pipeline since they are more dynamic
1670 # than pipelines. Remove the update_pipeline argument
1671 if not update_pipeline:
1672 return
James E. Blairb97ed802015-12-21 15:55:35 -08001673 for pipeline_name, pipeline_config in project_config.pipelines.items():
1674 pipeline = self.pipelines[pipeline_name]
1675 project = pipeline.source.getProject(project_config.name)
1676 pipeline.job_trees[project] = pipeline_config.job_tree
1677
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001678 def _createJobTree(self, change, job_trees, parent):
1679 for tree in job_trees:
1680 job = tree.job
1681 if not job.changeMatches(change):
1682 continue
1683 frozen_job = Job(job.name)
1684 frozen_tree = JobTree(frozen_job)
1685 inherited = set()
1686 for variant in self.getJobs(job.name):
1687 if variant.changeMatches(change):
1688 if variant not in inherited:
1689 frozen_job.inheritFrom(variant)
1690 inherited.add(variant)
1691 if job not in inherited:
1692 # Only update from the job in the tree if it is
1693 # unique, otherwise we might unset an attribute we
1694 # have overloaded.
1695 frozen_job.inheritFrom(job)
1696 parent.job_trees.append(frozen_tree)
1697 self._createJobTree(change, tree.job_trees, frozen_tree)
1698
1699 def createJobTree(self, item):
1700 project_config = self.project_configs[item.change.project.name]
1701 project_tree = project_config.pipelines[item.pipeline.name].job_tree
1702 ret = JobTree(None)
1703 self._createJobTree(item.change, project_tree.job_trees, ret)
1704 return ret
1705
James E. Blair59fdbac2015-12-07 17:08:06 -08001706
1707class Tenant(object):
1708 def __init__(self, name):
1709 self.name = name
1710 self.layout = None
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001711 # The list of repos from which we will read main
1712 # configuration. (source, project)
1713 self.config_repos = []
1714 # The unparsed config from those repos.
1715 self.config_repos_config = None
1716 # The list of projects from which we will read in-repo
1717 # configuration. (source, project)
1718 self.project_repos = []
1719 # The unparsed config from those repos.
1720 self.project_repos_config = None
James E. Blair59fdbac2015-12-07 17:08:06 -08001721
1722
1723class Abide(object):
1724 def __init__(self):
1725 self.tenants = OrderedDict()
James E. Blairce8a2132016-05-19 15:21:52 -07001726
1727
1728class JobTimeData(object):
1729 format = 'B10H10H10B'
1730 version = 0
1731
1732 def __init__(self, path):
1733 self.path = path
1734 self.success_times = [0 for x in range(10)]
1735 self.failure_times = [0 for x in range(10)]
1736 self.results = [0 for x in range(10)]
1737
1738 def load(self):
1739 if not os.path.exists(self.path):
1740 return
1741 with open(self.path) as f:
1742 data = struct.unpack(self.format, f.read())
1743 version = data[0]
1744 if version != self.version:
1745 raise Exception("Unkown data version")
1746 self.success_times = list(data[1:11])
1747 self.failure_times = list(data[11:21])
1748 self.results = list(data[21:32])
1749
1750 def save(self):
1751 tmpfile = self.path + '.tmp'
1752 data = [self.version]
1753 data.extend(self.success_times)
1754 data.extend(self.failure_times)
1755 data.extend(self.results)
1756 data = struct.pack(self.format, *data)
1757 with open(tmpfile, 'w') as f:
1758 f.write(data)
1759 os.rename(tmpfile, self.path)
1760
1761 def add(self, elapsed, result):
1762 elapsed = int(elapsed)
1763 if result == 'SUCCESS':
1764 self.success_times.append(elapsed)
1765 self.success_times.pop(0)
1766 result = 0
1767 else:
1768 self.failure_times.append(elapsed)
1769 self.failure_times.pop(0)
1770 result = 1
1771 self.results.append(result)
1772 self.results.pop(0)
1773
1774 def getEstimatedTime(self):
1775 times = [x for x in self.success_times if x]
1776 if times:
1777 return float(sum(times)) / len(times)
1778 return 0.0
1779
1780
1781class TimeDataBase(object):
1782 def __init__(self, root):
1783 self.root = root
1784 self.jobs = {}
1785
1786 def _getTD(self, name):
1787 td = self.jobs.get(name)
1788 if not td:
1789 td = JobTimeData(os.path.join(self.root, name))
1790 self.jobs[name] = td
1791 td.load()
1792 return td
1793
1794 def getEstimatedTime(self, name):
1795 return self._getTD(name).getEstimatedTime()
1796
1797 def update(self, name, elapsed, result):
1798 td = self._getTD(name)
1799 td.add(elapsed, result)
1800 td.save()