blob: 6528658d89b5ce0cb174ca1d29125aa8a63b54bc [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. Blair34776ee2016-08-25 13:53:54 -0700347class Node(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700348 """A single node for use by a job.
349
350 This may represent a request for a node, or an actual node
351 provided by Nodepool.
352 """
353
James E. Blair34776ee2016-08-25 13:53:54 -0700354 def __init__(self, name, image):
355 self.name = name
356 self.image = image
357
358 def __repr__(self):
359 return '<Node %s:%s>' % (self.name, self.image)
360
361
James E. Blaira98340f2016-09-02 11:33:49 -0700362class NodeSet(object):
363 """A set of nodes.
364
365 In configuration, NodeSets are attributes of Jobs indicating that
366 a Job requires nodes matching this description.
367
368 They may appear as top-level configuration objects and be named,
369 or they may appears anonymously in in-line job definitions.
370 """
371
372 def __init__(self, name=None):
373 self.name = name or ''
374 self.nodes = OrderedDict()
375
376 def addNode(self, node):
377 if node.name in self.nodes:
378 raise Exception("Duplicate node in %s" % (self,))
379 self.nodes[node.name] = node
380
James E. Blair0eaad552016-09-02 12:09:54 -0700381 def getNodes(self):
382 return self.nodes.values()
383
James E. Blaira98340f2016-09-02 11:33:49 -0700384 def __repr__(self):
385 if self.name:
386 name = self.name + ' '
387 else:
388 name = ''
389 return '<NodeSet %s%s>' % (name, self.nodes)
390
391
James E. Blair34776ee2016-08-25 13:53:54 -0700392class NodeRequest(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700393 """A request for a set of nodes."""
394
James E. Blair0eaad552016-09-02 12:09:54 -0700395 def __init__(self, build_set, job, nodeset):
James E. Blair34776ee2016-08-25 13:53:54 -0700396 self.build_set = build_set
397 self.job = job
James E. Blair0eaad552016-09-02 12:09:54 -0700398 self.nodeset = nodeset
James E. Blair34776ee2016-08-25 13:53:54 -0700399 self.id = uuid4().hex
400
401 def __repr__(self):
James E. Blair0eaad552016-09-02 12:09:54 -0700402 return '<NodeRequest %s>' % (self.nodeset,)
James E. Blair34776ee2016-08-25 13:53:54 -0700403
404
James E. Blairee743612012-05-29 14:49:32 -0700405class Job(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700406 """A Job represents the defintion of actions to perform."""
407
James E. Blair83005782015-12-11 14:46:03 -0800408 attributes = dict(
409 timeout=None,
410 # variables={},
James E. Blair0eaad552016-09-02 12:09:54 -0700411 nodeset=NodeSet(),
Ricardo Carrillo Cruz4e94f612016-07-25 16:11:56 +0000412 auth={},
James E. Blair83005782015-12-11 14:46:03 -0800413 workspace=None,
414 pre_run=None,
415 post_run=None,
416 voting=None,
James E. Blair791b5392016-08-03 11:25:56 -0700417 hold_following_changes=None,
James E. Blair83005782015-12-11 14:46:03 -0800418 failure_message=None,
419 success_message=None,
420 failure_url=None,
421 success_url=None,
422 # Matchers. These are separate so they can be individually
423 # overidden.
424 branch_matcher=None,
425 file_matcher=None,
426 irrelevant_file_matcher=None, # skip-if
James E. Blair83005782015-12-11 14:46:03 -0800427 parameter_function=None, # TODOv3(jeblair): remove
Joshua Heskethdc7820c2016-03-11 13:14:28 +1100428 tags=set(),
Joshua Hesketh89b67f62016-02-11 21:22:14 +1100429 mutex=None,
James E. Blair83005782015-12-11 14:46:03 -0800430 )
431
James E. Blairee743612012-05-29 14:49:32 -0700432 def __init__(self, name):
433 self.name = name
James E. Blair4317e9f2016-07-15 10:05:47 -0700434 self.project_source = None
James E. Blair83005782015-12-11 14:46:03 -0800435 for k, v in self.attributes.items():
436 setattr(self, k, v)
437
Paul Belangere22baea2016-11-03 16:59:27 -0400438 def __eq__(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800439 # Compare the name and all inheritable attributes to determine
440 # whether two jobs with the same name are identically
441 # configured. Useful upon reconfiguration.
442 if not isinstance(other, Job):
443 return False
444 if self.name != other.name:
445 return False
446 for k, v in self.attributes.items():
447 if getattr(self, k) != getattr(other, k):
448 return False
449 return True
James E. Blairee743612012-05-29 14:49:32 -0700450
451 def __str__(self):
452 return self.name
453
454 def __repr__(self):
James E. Blair34776ee2016-08-25 13:53:54 -0700455 return '<Job %s branches: %s>' % (self.name, self.branch_matcher)
James E. Blair83005782015-12-11 14:46:03 -0800456
457 def inheritFrom(self, other):
458 """Copy the inheritable attributes which have been set on the other
459 job to this job."""
460
461 if not isinstance(other, Job):
462 raise Exception("Job unable to inherit from %s" % (other,))
463 for k, v in self.attributes.items():
Ricardo Carrillo Cruz4e94f612016-07-25 16:11:56 +0000464 if getattr(other, k) != v and k != 'auth':
James E. Blair83005782015-12-11 14:46:03 -0800465 setattr(self, k, getattr(other, k))
Ricardo Carrillo Cruz4e94f612016-07-25 16:11:56 +0000466 # Inherit auth only if explicitly allowed
467 if other.auth and 'inherit' in other.auth and other.auth['inherit']:
468 setattr(self, 'auth', getattr(other, 'auth'))
James E. Blairee743612012-05-29 14:49:32 -0700469
James E. Blaire421a232012-07-25 16:59:21 -0700470 def changeMatches(self, change):
James E. Blair83005782015-12-11 14:46:03 -0800471 if self.branch_matcher and not self.branch_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800472 return False
473
James E. Blair83005782015-12-11 14:46:03 -0800474 if self.file_matcher and not self.file_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800475 return False
476
James E. Blair83005782015-12-11 14:46:03 -0800477 # NB: This is a negative match.
478 if (self.irrelevant_file_matcher and
479 self.irrelevant_file_matcher.matches(change)):
Maru Newby3fe5f852015-01-13 04:22:14 +0000480 return False
481
James E. Blair70c71582013-03-06 08:50:50 -0800482 return True
James E. Blaire5a847f2012-07-10 15:29:14 -0700483
James E. Blair1e8dd892012-05-30 09:15:05 -0700484
James E. Blairee743612012-05-29 14:49:32 -0700485class JobTree(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700486 """A JobTree holds one or more Jobs to represent Job dependencies.
487
488 If Job foo should only execute if Job bar succeeds, then there will
489 be a JobTree for foo, which will contain a JobTree for bar. A JobTree
490 can hold more than one dependent JobTrees, such that jobs bar and bang
491 both depend on job foo being successful.
492
493 A root node of a JobTree will have no associated Job."""
James E. Blairee743612012-05-29 14:49:32 -0700494
495 def __init__(self, job):
496 self.job = job
497 self.job_trees = []
498
499 def addJob(self, job):
James E. Blair12a92b12014-03-26 11:54:53 -0700500 if job not in [x.job for x in self.job_trees]:
501 t = JobTree(job)
502 self.job_trees.append(t)
503 return t
James E. Blaire4ad55a2015-06-11 08:22:43 -0700504 for tree in self.job_trees:
505 if tree.job == job:
506 return tree
James E. Blairee743612012-05-29 14:49:32 -0700507
508 def getJobs(self):
509 jobs = []
510 for x in self.job_trees:
511 jobs.append(x.job)
512 jobs.extend(x.getJobs())
513 return jobs
514
515 def getJobTreeForJob(self, job):
516 if self.job == job:
517 return self
518 for tree in self.job_trees:
519 ret = tree.getJobTreeForJob(job)
520 if ret:
521 return ret
522 return None
523
James E. Blairb97ed802015-12-21 15:55:35 -0800524 def inheritFrom(self, other):
525 if other.job:
526 self.job = Job(other.job.name)
527 self.job.inheritFrom(other.job)
528 for other_tree in other.job_trees:
529 this_tree = self.getJobTreeForJob(other_tree.job)
530 if not this_tree:
531 this_tree = JobTree(None)
532 self.job_trees.append(this_tree)
533 this_tree.inheritFrom(other_tree)
534
James E. Blair1e8dd892012-05-30 09:15:05 -0700535
James E. Blair4aea70c2012-07-26 14:23:24 -0700536class Build(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700537 """A Build is an instance of a single running Job."""
538
James E. Blair4aea70c2012-07-26 14:23:24 -0700539 def __init__(self, job, uuid):
540 self.job = job
541 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -0700542 self.url = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700543 self.result = None
544 self.build_set = None
545 self.launch_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -0800546 self.start_time = None
547 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -0700548 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -0700549 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -0700550 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -0700551 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -0700552 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +0800553 self.worker = Worker()
Timothy Chavezb2332082015-08-07 20:08:04 -0500554 self.node_labels = []
555 self.node_name = None
James E. Blairee743612012-05-29 14:49:32 -0700556
557 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +0800558 return ('<Build %s of %s on %s>' %
559 (self.uuid, self.job.name, self.worker))
560
561
562class Worker(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700563 """Information about the specific worker executing a Build."""
Joshua Heskethba8776a2014-01-12 14:35:40 +0800564 def __init__(self):
565 self.name = "Unknown"
566 self.hostname = None
567 self.ips = []
568 self.fqdn = None
569 self.program = None
570 self.version = None
571 self.extra = {}
572
573 def updateFromData(self, data):
574 """Update worker information if contained in the WORK_DATA response."""
575 self.name = data.get('worker_name', self.name)
576 self.hostname = data.get('worker_hostname', self.hostname)
577 self.ips = data.get('worker_ips', self.ips)
578 self.fqdn = data.get('worker_fqdn', self.fqdn)
579 self.program = data.get('worker_program', self.program)
580 self.version = data.get('worker_version', self.version)
581 self.extra = data.get('worker_extra', self.extra)
582
583 def __repr__(self):
584 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -0700585
James E. Blair1e8dd892012-05-30 09:15:05 -0700586
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700587class RepoFiles(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700588 """RepoFiles holds config-file content for per-project job config."""
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700589 # When we ask a merger to prepare a future multiple-repo state and
590 # collect files so that we can dynamically load our configuration,
591 # this class provides easy access to that data.
592 def __init__(self):
593 self.projects = {}
594
595 def __repr__(self):
596 return '<RepoFiles %s>' % self.projects
597
598 def setFiles(self, items):
599 self.projects = {}
600 for item in items:
601 project = self.projects.setdefault(item['project'], {})
602 branch = project.setdefault(item['branch'], {})
603 branch.update(item['files'])
604
605 def getFile(self, project, branch, fn):
606 return self.projects.get(project, {}).get(branch, {}).get(fn)
607
608
James E. Blair7e530ad2012-07-03 16:12:28 -0700609class BuildSet(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700610 """Contains the Builds for a Change representing potential future state.
611
612 A BuildSet also holds the UUID used to produce the Zuul Ref that builders
613 check out.
614 """
James E. Blair4076e2b2014-01-28 12:42:20 -0800615 # Merge states:
616 NEW = 1
617 PENDING = 2
618 COMPLETE = 3
619
Antoine Musso9b229282014-08-18 23:45:43 +0200620 states_map = {
621 1: 'NEW',
622 2: 'PENDING',
623 3: 'COMPLETE',
624 }
625
James E. Blairfee8d652013-06-07 08:57:52 -0700626 def __init__(self, item):
627 self.item = item
James E. Blair11700c32012-07-05 17:50:05 -0700628 self.other_changes = []
James E. Blair7e530ad2012-07-03 16:12:28 -0700629 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -0700630 self.result = None
631 self.next_build_set = None
632 self.previous_build_set = None
James E. Blair4886cc12012-07-18 15:39:41 -0700633 self.ref = None
James E. Blair81515ad2012-10-01 18:29:08 -0700634 self.commit = None
James E. Blair4076e2b2014-01-28 12:42:20 -0800635 self.zuul_url = None
James E. Blair973721f2012-08-15 10:19:43 -0700636 self.unable_to_merge = False
James E. Blair972e3c72013-08-29 12:04:55 -0700637 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -0800638 self.merge_state = self.NEW
James E. Blair0eaad552016-09-02 12:09:54 -0700639 self.nodesets = {} # job -> nodeset
James E. Blair8d692392016-04-08 17:47:58 -0700640 self.node_requests = {} # job -> reqs
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700641 self.files = RepoFiles()
642 self.layout = None
James E. Blair7e530ad2012-07-03 16:12:28 -0700643
Antoine Musso9b229282014-08-18 23:45:43 +0200644 def __repr__(self):
645 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
646 self.item,
647 len(self.builds),
648 self.getStateName(self.merge_state))
649
James E. Blair4886cc12012-07-18 15:39:41 -0700650 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -0700651 # The change isn't enqueued until after it's created
652 # so we don't know what the other changes ahead will be
653 # until jobs start.
654 if not self.other_changes:
James E. Blairfee8d652013-06-07 08:57:52 -0700655 next_item = self.item.item_ahead
656 while next_item:
657 self.other_changes.append(next_item.change)
658 next_item = next_item.item_ahead
James E. Blair4886cc12012-07-18 15:39:41 -0700659 if not self.ref:
660 self.ref = 'Z' + uuid4().hex
661
Antoine Musso9b229282014-08-18 23:45:43 +0200662 def getStateName(self, state_num):
663 return self.states_map.get(
664 state_num, 'UNKNOWN (%s)' % state_num)
665
James E. Blair4886cc12012-07-18 15:39:41 -0700666 def addBuild(self, build):
667 self.builds[build.job.name] = build
668 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -0700669
James E. Blair4a28a882013-08-23 15:17:33 -0700670 def removeBuild(self, build):
671 del self.builds[build.job.name]
672
James E. Blair7e530ad2012-07-03 16:12:28 -0700673 def getBuild(self, job_name):
674 return self.builds.get(job_name)
675
James E. Blair11700c32012-07-05 17:50:05 -0700676 def getBuilds(self):
677 keys = self.builds.keys()
678 keys.sort()
679 return [self.builds.get(x) for x in keys]
680
James E. Blair0eaad552016-09-02 12:09:54 -0700681 def getJobNodeSet(self, job_name):
682 # Return None if not provisioned; empty NodeSet if no nodes
683 # required
684 return self.nodesets.get(job_name)
James E. Blair8d692392016-04-08 17:47:58 -0700685
686 def setJobNodeRequest(self, job_name, req):
687 if job_name in self.node_requests:
688 raise Exception("Prior node request for %s" % (job_name))
689 self.node_requests[job_name] = req
690
691 def getJobNodeRequest(self, job_name):
692 return self.node_requests.get(job_name)
693
James E. Blair0eaad552016-09-02 12:09:54 -0700694 def jobNodeRequestComplete(self, job_name, req, nodeset):
695 if job_name in self.nodesets:
James E. Blair8d692392016-04-08 17:47:58 -0700696 raise Exception("Prior node request for %s" % (job_name))
James E. Blair0eaad552016-09-02 12:09:54 -0700697 self.nodesets[job_name] = nodeset
James E. Blair8d692392016-04-08 17:47:58 -0700698 del self.node_requests[job_name]
699
James E. Blair7e530ad2012-07-03 16:12:28 -0700700
James E. Blairfee8d652013-06-07 08:57:52 -0700701class QueueItem(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700702 """Represents the position of a Change in a ChangeQueue.
703
704 All Changes are enqueued into ChangeQueue in a QueueItem. The QueueItem
705 holds the current `BuildSet` as well as all previous `BuildSets` that were
706 produced for this `QueueItem`.
707 """
James E. Blair32663402012-06-01 10:04:18 -0700708
James E. Blairbfb8e042014-12-30 17:01:44 -0800709 def __init__(self, queue, change):
710 self.pipeline = queue.pipeline
711 self.queue = queue
James E. Blairfee8d652013-06-07 08:57:52 -0700712 self.change = change # a changeish
James E. Blair7e530ad2012-07-03 16:12:28 -0700713 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -0700714 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -0700715 self.current_build_set = BuildSet(self)
716 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -0700717 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700718 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -0800719 self.enqueue_time = None
720 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -0700721 self.reported = False
James E. Blairbfb8e042014-12-30 17:01:44 -0800722 self.active = False # Whether an item is within an active window
723 self.live = True # Whether an item is intended to be processed at all
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700724 self.layout = None # This item's shadow layout
James E. Blair83005782015-12-11 14:46:03 -0800725 self.job_tree = None
James E. Blaire5a847f2012-07-10 15:29:14 -0700726
James E. Blair972e3c72013-08-29 12:04:55 -0700727 def __repr__(self):
728 if self.pipeline:
729 pipeline = self.pipeline.name
730 else:
731 pipeline = None
732 return '<QueueItem 0x%x for %s in %s>' % (
733 id(self), self.change, pipeline)
734
James E. Blairee743612012-05-29 14:49:32 -0700735 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -0700736 old = self.current_build_set
737 self.current_build_set.result = 'CANCELED'
738 self.current_build_set = BuildSet(self)
739 old.next_build_set = self.current_build_set
740 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -0700741 self.build_sets.append(self.current_build_set)
James E. Blairee743612012-05-29 14:49:32 -0700742
743 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -0700744 self.current_build_set.addBuild(build)
James E. Blair66eeebf2013-07-27 17:44:32 -0700745 build.pipeline = self.pipeline
James E. Blairee743612012-05-29 14:49:32 -0700746
James E. Blair4a28a882013-08-23 15:17:33 -0700747 def removeBuild(self, build):
748 self.current_build_set.removeBuild(build)
749
James E. Blairfee8d652013-06-07 08:57:52 -0700750 def setReportedResult(self, result):
751 self.current_build_set.result = result
752
James E. Blair83005782015-12-11 14:46:03 -0800753 def freezeJobTree(self):
754 """Find or create actual matching jobs for this item's change and
755 store the resulting job tree."""
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700756 layout = self.current_build_set.layout
757 self.job_tree = layout.createJobTree(self)
758
759 def hasJobTree(self):
760 """Returns True if the item has a job tree."""
761 return self.job_tree is not None
James E. Blair83005782015-12-11 14:46:03 -0800762
763 def getJobs(self):
764 if not self.live or not self.job_tree:
765 return []
766 return self.job_tree.getJobs()
767
James E. Blairdbfd3282016-07-21 10:46:19 -0700768 def haveAllJobsStarted(self):
769 if not self.hasJobTree():
770 return False
771 for job in self.getJobs():
772 build = self.current_build_set.getBuild(job.name)
773 if not build or not build.start_time:
774 return False
775 return True
776
777 def areAllJobsComplete(self):
778 if not self.hasJobTree():
779 return False
780 for job in self.getJobs():
781 build = self.current_build_set.getBuild(job.name)
782 if not build or not build.result:
783 return False
784 return True
785
786 def didAllJobsSucceed(self):
787 if not self.hasJobTree():
788 return False
789 for job in self.getJobs():
790 if not job.voting:
791 continue
792 build = self.current_build_set.getBuild(job.name)
793 if not build:
794 return False
795 if build.result != 'SUCCESS':
796 return False
797 return True
798
799 def didAnyJobFail(self):
800 if not self.hasJobTree():
801 return False
802 for job in self.getJobs():
803 if not job.voting:
804 continue
805 build = self.current_build_set.getBuild(job.name)
806 if build and build.result and (build.result != 'SUCCESS'):
807 return True
808 return False
809
810 def didMergerFail(self):
811 if self.current_build_set.unable_to_merge:
812 return True
813 return False
814
James E. Blairdbfd3282016-07-21 10:46:19 -0700815 def isHoldingFollowingChanges(self):
816 if not self.live:
817 return False
818 if not self.hasJobTree():
819 return False
820 for job in self.getJobs():
821 if not job.hold_following_changes:
822 continue
823 build = self.current_build_set.getBuild(job.name)
824 if not build:
825 return True
826 if build.result != 'SUCCESS':
827 return True
828
829 if not self.item_ahead:
830 return False
831 return self.item_ahead.isHoldingFollowingChanges()
832
833 def _findJobsToRun(self, job_trees, mutex):
834 torun = []
James E. Blair791b5392016-08-03 11:25:56 -0700835 if self.item_ahead:
836 # Only run jobs if any 'hold' jobs on the change ahead
837 # have completed successfully.
838 if self.item_ahead.isHoldingFollowingChanges():
839 return []
James E. Blairdbfd3282016-07-21 10:46:19 -0700840 for tree in job_trees:
841 job = tree.job
842 result = None
843 if job:
844 if not job.changeMatches(self.change):
845 continue
846 build = self.current_build_set.getBuild(job.name)
847 if build:
848 result = build.result
849 else:
850 # There is no build for the root of this job tree,
James E. Blair34776ee2016-08-25 13:53:54 -0700851 # so it has not run yet.
James E. Blair0eaad552016-09-02 12:09:54 -0700852 nodeset = self.current_build_set.getJobNodeSet(job.name)
853 if nodeset is None:
James E. Blair34776ee2016-08-25 13:53:54 -0700854 # The nodes for this job are not ready, skip
855 # it for now.
856 continue
James E. Blairdbfd3282016-07-21 10:46:19 -0700857 if mutex.acquire(self, job):
858 # If this job needs a mutex, either acquire it or make
859 # sure that we have it before running the job.
860 torun.append(job)
861 # If there is no job, this is a null job tree, and we should
862 # run all of its jobs.
863 if result == 'SUCCESS' or not job:
864 torun.extend(self._findJobsToRun(tree.job_trees, mutex))
865 return torun
866
867 def findJobsToRun(self, mutex):
868 if not self.live:
869 return []
870 tree = self.job_tree
871 if not tree:
872 return []
873 return self._findJobsToRun(tree.job_trees, mutex)
874
875 def _findJobsToRequest(self, job_trees):
876 toreq = []
877 for tree in job_trees:
878 job = tree.job
879 if job:
880 if not job.changeMatches(self.change):
881 continue
James E. Blair0eaad552016-09-02 12:09:54 -0700882 nodeset = self.current_build_set.getJobNodeSet(job.name)
883 if nodeset is None:
James E. Blairdbfd3282016-07-21 10:46:19 -0700884 req = self.current_build_set.getJobNodeRequest(job.name)
885 if req is None:
886 toreq.append(job)
James E. Blair34776ee2016-08-25 13:53:54 -0700887 toreq.extend(self._findJobsToRequest(tree.job_trees))
James E. Blairdbfd3282016-07-21 10:46:19 -0700888 return toreq
889
890 def findJobsToRequest(self):
891 if not self.live:
892 return []
893 tree = self.job_tree
894 if not tree:
895 return []
896 return self._findJobsToRequest(tree.job_trees)
897
898 def setResult(self, build):
899 if build.retry:
900 self.removeBuild(build)
901 elif build.result != 'SUCCESS':
902 # Get a JobTree from a Job so we can find only its dependent jobs
903 tree = self.job_tree.getJobTreeForJob(build.job)
904 for job in tree.getJobs():
905 fakebuild = Build(job, None)
906 fakebuild.result = 'SKIPPED'
907 self.addBuild(fakebuild)
908
909 def setDequeuedNeedingChange(self):
910 self.dequeued_needing_change = True
911 self._setAllJobsSkipped()
912
913 def setUnableToMerge(self):
914 self.current_build_set.unable_to_merge = True
915 self._setAllJobsSkipped()
916
917 def _setAllJobsSkipped(self):
918 for job in self.getJobs():
919 fakebuild = Build(job, None)
920 fakebuild.result = 'SKIPPED'
921 self.addBuild(fakebuild)
922
James E. Blairb7273ef2016-04-19 08:58:51 -0700923 def formatJobResult(self, job, url_pattern=None):
924 build = self.current_build_set.getBuild(job.name)
925 result = build.result
926 pattern = url_pattern
927 if result == 'SUCCESS':
928 if job.success_message:
929 result = job.success_message
James E. Blaira5dba232016-08-08 15:53:24 -0700930 if job.success_url:
931 pattern = job.success_url
James E. Blairb7273ef2016-04-19 08:58:51 -0700932 elif result == 'FAILURE':
933 if job.failure_message:
934 result = job.failure_message
James E. Blaira5dba232016-08-08 15:53:24 -0700935 if job.failure_url:
936 pattern = job.failure_url
James E. Blairb7273ef2016-04-19 08:58:51 -0700937 url = None
938 if pattern:
939 try:
940 url = pattern.format(change=self.change,
941 pipeline=self.pipeline,
942 job=job,
943 build=build)
944 except Exception:
945 pass # FIXME: log this or something?
946 if not url:
947 url = build.url or job.name
948 return (result, url)
949
950 def formatJSON(self, url_pattern=None):
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800951 changeish = self.change
952 ret = {}
953 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -0800954 ret['live'] = self.live
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800955 if hasattr(changeish, 'url') and changeish.url is not None:
956 ret['url'] = changeish.url
957 else:
958 ret['url'] = None
959 ret['id'] = changeish._id()
960 if self.item_ahead:
961 ret['item_ahead'] = self.item_ahead.change._id()
962 else:
963 ret['item_ahead'] = None
964 ret['items_behind'] = [i.change._id() for i in self.items_behind]
965 ret['failing_reasons'] = self.current_build_set.failing_reasons
966 ret['zuul_ref'] = self.current_build_set.ref
Ramy Asselin07cc33c2015-06-12 14:06:34 -0700967 if changeish.project:
968 ret['project'] = changeish.project.name
969 else:
970 # For cross-project dependencies with the depends-on
971 # project not known to zuul, the project is None
972 # Set it to a static value
973 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800974 ret['enqueue_time'] = int(self.enqueue_time * 1000)
975 ret['jobs'] = []
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -0500976 if hasattr(changeish, 'owner'):
977 ret['owner'] = changeish.owner
978 else:
979 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800980 max_remaining = 0
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700981 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800982 now = time.time()
983 build = self.current_build_set.getBuild(job.name)
984 elapsed = None
985 remaining = None
986 result = None
James E. Blairb7273ef2016-04-19 08:58:51 -0700987 build_url = None
988 report_url = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800989 worker = None
990 if build:
991 result = build.result
James E. Blairb7273ef2016-04-19 08:58:51 -0700992 build_url = build.url
993 (unused, report_url) = self.formatJobResult(job, url_pattern)
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800994 if build.start_time:
995 if build.end_time:
996 elapsed = int((build.end_time -
997 build.start_time) * 1000)
998 remaining = 0
999 else:
1000 elapsed = int((now - build.start_time) * 1000)
1001 if build.estimated_time:
1002 remaining = max(
1003 int(build.estimated_time * 1000) - elapsed,
1004 0)
1005 worker = {
1006 'name': build.worker.name,
1007 'hostname': build.worker.hostname,
1008 'ips': build.worker.ips,
1009 'fqdn': build.worker.fqdn,
1010 'program': build.worker.program,
1011 'version': build.worker.version,
1012 'extra': build.worker.extra
1013 }
1014 if remaining and remaining > max_remaining:
1015 max_remaining = remaining
1016
1017 ret['jobs'].append({
1018 'name': job.name,
1019 'elapsed_time': elapsed,
1020 'remaining_time': remaining,
James E. Blairb7273ef2016-04-19 08:58:51 -07001021 'url': build_url,
1022 'report_url': report_url,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001023 'result': result,
1024 'voting': job.voting,
1025 'uuid': build.uuid if build else None,
1026 'launch_time': build.launch_time if build else None,
1027 'start_time': build.start_time if build else None,
1028 'end_time': build.end_time if build else None,
1029 'estimated_time': build.estimated_time if build else None,
1030 'pipeline': build.pipeline.name if build else None,
1031 'canceled': build.canceled if build else None,
1032 'retry': build.retry if build else None,
Timothy Chavezb2332082015-08-07 20:08:04 -05001033 'node_labels': build.node_labels if build else [],
1034 'node_name': build.node_name if build else None,
1035 'worker': worker,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001036 })
1037
James E. Blairdbfd3282016-07-21 10:46:19 -07001038 if self.haveAllJobsStarted():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001039 ret['remaining_time'] = max_remaining
1040 else:
1041 ret['remaining_time'] = None
1042 return ret
1043
1044 def formatStatus(self, indent=0, html=False):
1045 changeish = self.change
1046 indent_str = ' ' * indent
1047 ret = ''
1048 if html and hasattr(changeish, 'url') and changeish.url is not None:
1049 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
1050 indent_str,
1051 changeish.project.name,
1052 changeish.url,
1053 changeish._id())
1054 else:
1055 ret += '%sProject %s change %s based on %s\n' % (
1056 indent_str,
1057 changeish.project.name,
1058 changeish._id(),
1059 self.item_ahead)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001060 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001061 build = self.current_build_set.getBuild(job.name)
1062 if build:
1063 result = build.result
1064 else:
1065 result = None
1066 job_name = job.name
1067 if not job.voting:
1068 voting = ' (non-voting)'
1069 else:
1070 voting = ''
1071 if html:
1072 if build:
1073 url = build.url
1074 else:
1075 url = None
1076 if url is not None:
1077 job_name = '<a href="%s">%s</a>' % (url, job_name)
1078 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
1079 ret += '\n'
1080 return ret
1081
James E. Blairfee8d652013-06-07 08:57:52 -07001082
1083class Changeish(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001084 """Base class for Change and Ref."""
James E. Blairfee8d652013-06-07 08:57:52 -07001085
1086 def __init__(self, project):
1087 self.project = project
1088
Joshua Hesketh36c3fa52014-01-22 11:40:52 +11001089 def getBasePath(self):
1090 base_path = ''
1091 if hasattr(self, 'refspec'):
1092 base_path = "%s/%s/%s" % (
1093 self.number[-2:], self.number, self.patchset)
1094 elif hasattr(self, 'ref'):
1095 base_path = "%s/%s" % (self.newrev[:2], self.newrev)
1096
1097 return base_path
1098
James E. Blairfee8d652013-06-07 08:57:52 -07001099 def equals(self, other):
1100 raise NotImplementedError()
1101
1102 def isUpdateOf(self, other):
1103 raise NotImplementedError()
1104
1105 def filterJobs(self, jobs):
1106 return filter(lambda job: job.changeMatches(self), jobs)
1107
1108 def getRelatedChanges(self):
1109 return set()
1110
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001111 def updatesConfig(self):
1112 return False
1113
James E. Blair1e8dd892012-05-30 09:15:05 -07001114
James E. Blair4aea70c2012-07-26 14:23:24 -07001115class Change(Changeish):
Monty Taylora42a55b2016-07-29 07:53:33 -07001116 """A proposed new state for a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001117 def __init__(self, project):
1118 super(Change, self).__init__(project)
1119 self.branch = None
1120 self.number = None
1121 self.url = None
1122 self.patchset = None
1123 self.refspec = None
1124
James E. Blair70c71582013-03-06 08:50:50 -08001125 self.files = []
James E. Blair6965a4b2014-12-16 17:19:04 -08001126 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -07001127 self.needed_by_changes = []
1128 self.is_current_patchset = True
1129 self.can_merge = False
1130 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -07001131 self.failed_to_merge = False
James E. Blairc053d022014-01-22 14:57:33 -08001132 self.approvals = []
James E. Blair11041d22014-05-02 14:49:53 -07001133 self.open = None
1134 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001135 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001136
1137 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -07001138 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -07001139
1140 def __repr__(self):
1141 return '<Change 0x%x %s>' % (id(self), self._id())
1142
1143 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +08001144 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -07001145 return True
1146 return False
1147
James E. Blair2fa50962013-01-30 21:50:41 -08001148 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -08001149 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -07001150 (hasattr(other, 'patchset') and
1151 self.patchset is not None and
1152 other.patchset is not None and
1153 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -08001154 return True
1155 return False
1156
James E. Blairfee8d652013-06-07 08:57:52 -07001157 def getRelatedChanges(self):
1158 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -08001159 for c in self.needs_changes:
1160 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -07001161 for c in self.needed_by_changes:
1162 related.add(c)
1163 related.update(c.getRelatedChanges())
1164 return related
James E. Blair4aea70c2012-07-26 14:23:24 -07001165
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001166 def updatesConfig(self):
1167 if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files:
1168 return True
1169 return False
1170
James E. Blair4aea70c2012-07-26 14:23:24 -07001171
1172class Ref(Changeish):
Monty Taylora42a55b2016-07-29 07:53:33 -07001173 """An existing state of a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001174 def __init__(self, project):
James E. Blairbe765db2012-08-07 08:36:20 -07001175 super(Ref, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -07001176 self.ref = None
1177 self.oldrev = None
1178 self.newrev = None
1179
James E. Blairbe765db2012-08-07 08:36:20 -07001180 def _id(self):
1181 return self.newrev
1182
Antoine Musso68bdcd72013-01-17 12:31:28 +01001183 def __repr__(self):
1184 rep = None
1185 if self.newrev == '0000000000000000000000000000000000000000':
1186 rep = '<Ref 0x%x deletes %s from %s' % (
1187 id(self), self.ref, self.oldrev)
1188 elif self.oldrev == '0000000000000000000000000000000000000000':
1189 rep = '<Ref 0x%x creates %s on %s>' % (
1190 id(self), self.ref, self.newrev)
1191 else:
1192 # Catch all
1193 rep = '<Ref 0x%x %s updated %s..%s>' % (
1194 id(self), self.ref, self.oldrev, self.newrev)
1195
1196 return rep
1197
James E. Blair4aea70c2012-07-26 14:23:24 -07001198 def equals(self, other):
James E. Blair9358c612012-09-28 08:29:39 -07001199 if (self.project == other.project
1200 and self.ref == other.ref
1201 and self.newrev == other.newrev):
James E. Blair4aea70c2012-07-26 14:23:24 -07001202 return True
1203 return False
1204
James E. Blair2fa50962013-01-30 21:50:41 -08001205 def isUpdateOf(self, other):
1206 return False
1207
James E. Blair4aea70c2012-07-26 14:23:24 -07001208
James E. Blair63bb0ef2013-07-29 17:14:51 -07001209class NullChange(Changeish):
James E. Blair23161912016-07-28 15:42:14 -07001210 # TODOv3(jeblair): remove this in favor of enqueueing Refs (eg
1211 # current master) instead.
James E. Blaire5910202013-12-27 09:50:31 -08001212 def __repr__(self):
1213 return '<NullChange for %s>' % (self.project)
James E. Blair63bb0ef2013-07-29 17:14:51 -07001214
James E. Blair63bb0ef2013-07-29 17:14:51 -07001215 def _id(self):
Alex Gaynorddb9ef32013-09-16 21:04:58 -07001216 return None
James E. Blair63bb0ef2013-07-29 17:14:51 -07001217
1218 def equals(self, other):
Steve Varnau7b78b312015-04-03 14:49:46 -07001219 if (self.project == other.project
1220 and other._id() is None):
James E. Blair4f6033c2014-03-27 15:49:09 -07001221 return True
James E. Blair63bb0ef2013-07-29 17:14:51 -07001222 return False
1223
1224 def isUpdateOf(self, other):
1225 return False
1226
1227
James E. Blairee743612012-05-29 14:49:32 -07001228class TriggerEvent(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001229 """Incoming event from an external system."""
James E. Blairee743612012-05-29 14:49:32 -07001230 def __init__(self):
1231 self.data = None
James E. Blair32663402012-06-01 10:04:18 -07001232 # common
James E. Blairee743612012-05-29 14:49:32 -07001233 self.type = None
1234 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -07001235 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +01001236 # Representation of the user account that performed the event.
1237 self.account = None
James E. Blair32663402012-06-01 10:04:18 -07001238 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -07001239 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -07001240 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -07001241 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -07001242 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -07001243 self.approvals = []
1244 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07001245 self.comment = None
James E. Blair32663402012-06-01 10:04:18 -07001246 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -07001247 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -07001248 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07001249 self.newrev = None
James E. Blair63bb0ef2013-07-29 17:14:51 -07001250 # timer
1251 self.timespec = None
James E. Blairc494d542014-08-06 09:23:52 -07001252 # zuultrigger
1253 self.pipeline_name = None
James E. Blairad28e912013-11-27 10:43:22 -08001254 # For events that arrive with a destination pipeline (eg, from
1255 # an admin command, etc):
1256 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07001257
James E. Blair9f9667e2012-06-12 17:51:08 -07001258 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001259 ret = '<TriggerEvent %s %s' % (self.type, self.project_name)
James E. Blair1e8dd892012-05-30 09:15:05 -07001260
James E. Blairee743612012-05-29 14:49:32 -07001261 if self.branch:
1262 ret += " %s" % self.branch
1263 if self.change_number:
1264 ret += " %s,%s" % (self.change_number, self.patch_number)
1265 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -07001266 ret += ' ' + ', '.join(
1267 ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
James E. Blairee743612012-05-29 14:49:32 -07001268 ret += '>'
1269
1270 return ret
1271
James E. Blair1e8dd892012-05-30 09:15:05 -07001272
James E. Blair9c17dbf2014-06-23 14:21:58 -07001273class BaseFilter(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001274 """Base Class for filtering which Changes and Events to process."""
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001275 def __init__(self, required_approvals=[], reject_approvals=[]):
James E. Blair5bf78a32015-07-30 18:08:24 +00001276 self._required_approvals = copy.deepcopy(required_approvals)
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001277 self.required_approvals = self._tidy_approvals(required_approvals)
1278 self._reject_approvals = copy.deepcopy(reject_approvals)
1279 self.reject_approvals = self._tidy_approvals(reject_approvals)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001280
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001281 def _tidy_approvals(self, approvals):
1282 for a in approvals:
James E. Blair9c17dbf2014-06-23 14:21:58 -07001283 for k, v in a.items():
1284 if k == 'username':
James E. Blairb01ec542016-06-16 09:46:49 -07001285 a['username'] = re.compile(v)
James E. Blair1fbfceb2014-06-23 14:42:53 -07001286 elif k in ['email', 'email-filter']:
James E. Blair5bf78a32015-07-30 18:08:24 +00001287 a['email'] = re.compile(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001288 elif k == 'newer-than':
James E. Blair5bf78a32015-07-30 18:08:24 +00001289 a[k] = time_to_seconds(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001290 elif k == 'older-than':
James E. Blair5bf78a32015-07-30 18:08:24 +00001291 a[k] = time_to_seconds(v)
James E. Blair1fbfceb2014-06-23 14:42:53 -07001292 if 'email-filter' in a:
1293 del a['email-filter']
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001294 return approvals
1295
1296 def _match_approval_required_approval(self, rapproval, approval):
1297 # Check if the required approval and approval match
1298 if 'description' not in approval:
1299 return False
1300 now = time.time()
1301 by = approval.get('by', {})
1302 for k, v in rapproval.items():
1303 if k == 'username':
James E. Blairb01ec542016-06-16 09:46:49 -07001304 if (not v.search(by.get('username', ''))):
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001305 return False
1306 elif k == 'email':
1307 if (not v.search(by.get('email', ''))):
1308 return False
1309 elif k == 'newer-than':
1310 t = now - v
1311 if (approval['grantedOn'] < t):
1312 return False
1313 elif k == 'older-than':
1314 t = now - v
1315 if (approval['grantedOn'] >= t):
1316 return False
1317 else:
1318 if not isinstance(v, list):
1319 v = [v]
1320 if (normalizeCategory(approval['description']) != k or
1321 int(approval['value']) not in v):
1322 return False
1323 return True
1324
1325 def matchesApprovals(self, change):
1326 if (self.required_approvals and not change.approvals
1327 or self.reject_approvals and not change.approvals):
1328 # A change with no approvals can not match
1329 return False
1330
1331 # TODO(jhesketh): If we wanted to optimise this slightly we could
1332 # analyse both the REQUIRE and REJECT filters by looping over the
1333 # approvals on the change and keeping track of what we have checked
1334 # rather than needing to loop on the change approvals twice
1335 return (self.matchesRequiredApprovals(change) and
1336 self.matchesNoRejectApprovals(change))
James E. Blair9c17dbf2014-06-23 14:21:58 -07001337
1338 def matchesRequiredApprovals(self, change):
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001339 # Check if any approvals match the requirements
James E. Blair5bf78a32015-07-30 18:08:24 +00001340 for rapproval in self.required_approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001341 matches_rapproval = False
James E. Blair9c17dbf2014-06-23 14:21:58 -07001342 for approval in change.approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001343 if self._match_approval_required_approval(rapproval, approval):
1344 # We have a matching approval so this requirement is
1345 # fulfilled
1346 matches_rapproval = True
James E. Blair5bf78a32015-07-30 18:08:24 +00001347 break
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001348 if not matches_rapproval:
James E. Blair5bf78a32015-07-30 18:08:24 +00001349 return False
James E. Blair9c17dbf2014-06-23 14:21:58 -07001350 return True
1351
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001352 def matchesNoRejectApprovals(self, change):
1353 # Check to make sure no approvals match a reject criteria
1354 for rapproval in self.reject_approvals:
1355 for approval in change.approvals:
1356 if self._match_approval_required_approval(rapproval, approval):
1357 # A reject approval has been matched, so we reject
1358 # immediately
1359 return False
1360 # To get here no rejects can have been matched so we should be good to
1361 # queue
1362 return True
1363
James E. Blair9c17dbf2014-06-23 14:21:58 -07001364
1365class EventFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07001366 """Allows a Pipeline to only respond to certain events."""
James E. Blairc0dedf82014-08-06 09:37:52 -07001367 def __init__(self, trigger, types=[], branches=[], refs=[],
1368 event_approvals={}, comments=[], emails=[], usernames=[],
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001369 timespecs=[], required_approvals=[], reject_approvals=[],
1370 pipelines=[], ignore_deletes=True):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001371 super(EventFilter, self).__init__(
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001372 required_approvals=required_approvals,
1373 reject_approvals=reject_approvals)
James E. Blairc0dedf82014-08-06 09:37:52 -07001374 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07001375 self._types = types
1376 self._branches = branches
1377 self._refs = refs
James E. Blair1fbfceb2014-06-23 14:42:53 -07001378 self._comments = comments
1379 self._emails = emails
1380 self._usernames = usernames
James E. Blairc494d542014-08-06 09:23:52 -07001381 self._pipelines = pipelines
James E. Blairee743612012-05-29 14:49:32 -07001382 self.types = [re.compile(x) for x in types]
1383 self.branches = [re.compile(x) for x in branches]
1384 self.refs = [re.compile(x) for x in refs]
James E. Blair1fbfceb2014-06-23 14:42:53 -07001385 self.comments = [re.compile(x) for x in comments]
1386 self.emails = [re.compile(x) for x in emails]
1387 self.usernames = [re.compile(x) for x in usernames]
James E. Blairc494d542014-08-06 09:23:52 -07001388 self.pipelines = [re.compile(x) for x in pipelines]
James E. Blairc053d022014-01-22 14:57:33 -08001389 self.event_approvals = event_approvals
James E. Blair63bb0ef2013-07-29 17:14:51 -07001390 self.timespecs = timespecs
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001391 self.ignore_deletes = ignore_deletes
James E. Blairee743612012-05-29 14:49:32 -07001392
James E. Blair9f9667e2012-06-12 17:51:08 -07001393 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001394 ret = '<EventFilter'
James E. Blair1e8dd892012-05-30 09:15:05 -07001395
James E. Blairee743612012-05-29 14:49:32 -07001396 if self._types:
1397 ret += ' types: %s' % ', '.join(self._types)
James E. Blairc494d542014-08-06 09:23:52 -07001398 if self._pipelines:
1399 ret += ' pipelines: %s' % ', '.join(self._pipelines)
James E. Blairee743612012-05-29 14:49:32 -07001400 if self._branches:
1401 ret += ' branches: %s' % ', '.join(self._branches)
1402 if self._refs:
1403 ret += ' refs: %s' % ', '.join(self._refs)
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001404 if self.ignore_deletes:
1405 ret += ' ignore_deletes: %s' % self.ignore_deletes
James E. Blairc053d022014-01-22 14:57:33 -08001406 if self.event_approvals:
1407 ret += ' event_approvals: %s' % ', '.join(
1408 ['%s:%s' % a for a in self.event_approvals.items()])
James E. Blair5bf78a32015-07-30 18:08:24 +00001409 if self.required_approvals:
1410 ret += ' required_approvals: %s' % ', '.join(
1411 ['%s' % a for a in self._required_approvals])
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001412 if self.reject_approvals:
1413 ret += ' reject_approvals: %s' % ', '.join(
1414 ['%s' % a for a in self._reject_approvals])
James E. Blair1fbfceb2014-06-23 14:42:53 -07001415 if self._comments:
1416 ret += ' comments: %s' % ', '.join(self._comments)
1417 if self._emails:
1418 ret += ' emails: %s' % ', '.join(self._emails)
1419 if self._usernames:
1420 ret += ' username_filters: %s' % ', '.join(self._usernames)
James E. Blair63bb0ef2013-07-29 17:14:51 -07001421 if self.timespecs:
1422 ret += ' timespecs: %s' % ', '.join(self.timespecs)
James E. Blairee743612012-05-29 14:49:32 -07001423 ret += '>'
1424
1425 return ret
1426
James E. Blairc053d022014-01-22 14:57:33 -08001427 def matches(self, event, change):
James E. Blairee743612012-05-29 14:49:32 -07001428 # event types are ORed
1429 matches_type = False
1430 for etype in self.types:
1431 if etype.match(event.type):
1432 matches_type = True
1433 if self.types and not matches_type:
1434 return False
1435
James E. Blairc494d542014-08-06 09:23:52 -07001436 # pipelines are ORed
1437 matches_pipeline = False
1438 for epipe in self.pipelines:
1439 if epipe.match(event.pipeline_name):
1440 matches_pipeline = True
1441 if self.pipelines and not matches_pipeline:
1442 return False
1443
James E. Blairee743612012-05-29 14:49:32 -07001444 # branches are ORed
1445 matches_branch = False
1446 for branch in self.branches:
1447 if branch.match(event.branch):
1448 matches_branch = True
1449 if self.branches and not matches_branch:
1450 return False
1451
1452 # refs are ORed
1453 matches_ref = False
Yolanda Robla16698872014-08-25 11:59:27 +02001454 if event.ref is not None:
1455 for ref in self.refs:
1456 if ref.match(event.ref):
1457 matches_ref = True
James E. Blairee743612012-05-29 14:49:32 -07001458 if self.refs and not matches_ref:
1459 return False
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001460 if self.ignore_deletes and event.newrev == EMPTY_GIT_REF:
1461 # If the updated ref has an empty git sha (all 0s),
1462 # then the ref is being deleted
1463 return False
James E. Blairee743612012-05-29 14:49:32 -07001464
James E. Blair1fbfceb2014-06-23 14:42:53 -07001465 # comments are ORed
1466 matches_comment_re = False
1467 for comment_re in self.comments:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001468 if (event.comment is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001469 comment_re.search(event.comment)):
1470 matches_comment_re = True
1471 if self.comments and not matches_comment_re:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001472 return False
1473
Antoine Mussob4e809e2012-12-06 16:58:06 +01001474 # We better have an account provided by Gerrit to do
1475 # email filtering.
1476 if event.account is not None:
James E. Blaircf429f32012-12-20 14:28:24 -08001477 account_email = event.account.get('email')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001478 # emails are ORed
1479 matches_email_re = False
1480 for email_re in self.emails:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001481 if (account_email is not None and
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001482 email_re.search(account_email)):
James E. Blair1fbfceb2014-06-23 14:42:53 -07001483 matches_email_re = True
1484 if self.emails and not matches_email_re:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001485 return False
1486
James E. Blair1fbfceb2014-06-23 14:42:53 -07001487 # usernames are ORed
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001488 account_username = event.account.get('username')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001489 matches_username_re = False
1490 for username_re in self.usernames:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001491 if (account_username is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001492 username_re.search(account_username)):
1493 matches_username_re = True
1494 if self.usernames and not matches_username_re:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001495 return False
1496
James E. Blairee743612012-05-29 14:49:32 -07001497 # approvals are ANDed
James E. Blairc053d022014-01-22 14:57:33 -08001498 for category, value in self.event_approvals.items():
James E. Blairee743612012-05-29 14:49:32 -07001499 matches_approval = False
1500 for eapproval in event.approvals:
1501 if (normalizeCategory(eapproval['description']) == category and
1502 int(eapproval['value']) == int(value)):
1503 matches_approval = True
James E. Blair1e8dd892012-05-30 09:15:05 -07001504 if not matches_approval:
1505 return False
James E. Blair63bb0ef2013-07-29 17:14:51 -07001506
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001507 # required approvals are ANDed (reject approvals are ORed)
1508 if not self.matchesApprovals(change):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001509 return False
James E. Blairc053d022014-01-22 14:57:33 -08001510
James E. Blair63bb0ef2013-07-29 17:14:51 -07001511 # timespecs are ORed
1512 matches_timespec = False
1513 for timespec in self.timespecs:
1514 if (event.timespec == timespec):
1515 matches_timespec = True
1516 if self.timespecs and not matches_timespec:
1517 return False
1518
James E. Blairee743612012-05-29 14:49:32 -07001519 return True
James E. Blaireff88162013-07-01 12:44:14 -04001520
1521
James E. Blair9c17dbf2014-06-23 14:21:58 -07001522class ChangeishFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07001523 """Allows a Manager to only enqueue Changes that meet certain criteria."""
Clark Boylana9702ad2014-05-08 17:17:24 -07001524 def __init__(self, open=None, current_patchset=None,
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001525 statuses=[], required_approvals=[],
1526 reject_approvals=[]):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001527 super(ChangeishFilter, self).__init__(
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001528 required_approvals=required_approvals,
1529 reject_approvals=reject_approvals)
James E. Blair11041d22014-05-02 14:49:53 -07001530 self.open = open
Clark Boylana9702ad2014-05-08 17:17:24 -07001531 self.current_patchset = current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001532 self.statuses = statuses
James E. Blair11041d22014-05-02 14:49:53 -07001533
1534 def __repr__(self):
1535 ret = '<ChangeishFilter'
1536
1537 if self.open is not None:
1538 ret += ' open: %s' % self.open
Clark Boylana9702ad2014-05-08 17:17:24 -07001539 if self.current_patchset is not None:
1540 ret += ' current-patchset: %s' % self.current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001541 if self.statuses:
1542 ret += ' statuses: %s' % ', '.join(self.statuses)
James E. Blair5bf78a32015-07-30 18:08:24 +00001543 if self.required_approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001544 ret += (' required_approvals: %s' %
1545 str(self.required_approvals))
1546 if self.reject_approvals:
1547 ret += (' reject_approvals: %s' %
1548 str(self.reject_approvals))
James E. Blair11041d22014-05-02 14:49:53 -07001549 ret += '>'
1550
1551 return ret
1552
1553 def matches(self, change):
1554 if self.open is not None:
1555 if self.open != change.open:
1556 return False
1557
Clark Boylana9702ad2014-05-08 17:17:24 -07001558 if self.current_patchset is not None:
1559 if self.current_patchset != change.is_current_patchset:
1560 return False
1561
James E. Blair11041d22014-05-02 14:49:53 -07001562 if self.statuses:
1563 if change.status not in self.statuses:
1564 return False
1565
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001566 # required approvals are ANDed (reject approvals are ORed)
1567 if not self.matchesApprovals(change):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001568 return False
James E. Blair11041d22014-05-02 14:49:53 -07001569
1570 return True
1571
1572
James E. Blairb97ed802015-12-21 15:55:35 -08001573class ProjectPipelineConfig(object):
1574 # Represents a project cofiguration in the context of a pipeline
1575 def __init__(self):
1576 self.job_tree = None
1577 self.queue_name = None
1578 # TODOv3(jeblair): add merge mode
1579
1580
1581class ProjectConfig(object):
1582 # Represents a project cofiguration
1583 def __init__(self, name):
1584 self.name = name
1585 self.pipelines = {}
1586
1587
James E. Blaird8e778f2015-12-22 14:09:20 -08001588class UnparsedAbideConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001589 """A collection of yaml lists that has not yet been parsed into objects.
1590
1591 An Abide is a collection of tenants.
1592 """
1593
James E. Blaird8e778f2015-12-22 14:09:20 -08001594 def __init__(self):
1595 self.tenants = []
1596
1597 def extend(self, conf):
1598 if isinstance(conf, UnparsedAbideConfig):
1599 self.tenants.extend(conf.tenants)
1600 return
1601
1602 if not isinstance(conf, list):
1603 raise Exception("Configuration items must be in the form of "
1604 "a list of dictionaries (when parsing %s)" %
1605 (conf,))
1606 for item in conf:
1607 if not isinstance(item, dict):
1608 raise Exception("Configuration items must be in the form of "
1609 "a list of dictionaries (when parsing %s)" %
1610 (conf,))
1611 if len(item.keys()) > 1:
1612 raise Exception("Configuration item dictionaries must have "
1613 "a single key (when parsing %s)" %
1614 (conf,))
1615 key, value = item.items()[0]
1616 if key == 'tenant':
1617 self.tenants.append(value)
1618 else:
1619 raise Exception("Configuration item not recognized "
1620 "(when parsing %s)" %
1621 (conf,))
1622
1623
1624class UnparsedTenantConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001625 """A collection of yaml lists that has not yet been parsed into objects."""
1626
James E. Blaird8e778f2015-12-22 14:09:20 -08001627 def __init__(self):
1628 self.pipelines = []
1629 self.jobs = []
1630 self.project_templates = []
1631 self.projects = []
James E. Blaira98340f2016-09-02 11:33:49 -07001632 self.nodesets = []
James E. Blaird8e778f2015-12-22 14:09:20 -08001633
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001634 def copy(self):
1635 r = UnparsedTenantConfig()
1636 r.pipelines = copy.deepcopy(self.pipelines)
1637 r.jobs = copy.deepcopy(self.jobs)
1638 r.project_templates = copy.deepcopy(self.project_templates)
1639 r.projects = copy.deepcopy(self.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07001640 r.nodesets = copy.deepcopy(self.nodesets)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001641 return r
1642
James E. Blaire208c482016-10-04 14:35:30 -07001643 def extend(self, conf, source_project=None, source_branch=None):
James E. Blaird8e778f2015-12-22 14:09:20 -08001644 if isinstance(conf, UnparsedTenantConfig):
1645 self.pipelines.extend(conf.pipelines)
1646 self.jobs.extend(conf.jobs)
1647 self.project_templates.extend(conf.project_templates)
1648 self.projects.extend(conf.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07001649 self.nodesets.extend(conf.nodesets)
James E. Blaird8e778f2015-12-22 14:09:20 -08001650 return
1651
1652 if not isinstance(conf, list):
1653 raise Exception("Configuration items must be in the form of "
1654 "a list of dictionaries (when parsing %s)" %
1655 (conf,))
1656 for item in conf:
1657 if not isinstance(item, dict):
1658 raise Exception("Configuration items must be in the form of "
1659 "a list of dictionaries (when parsing %s)" %
1660 (conf,))
1661 if len(item.keys()) > 1:
1662 raise Exception("Configuration item dictionaries must have "
1663 "a single key (when parsing %s)" %
1664 (conf,))
1665 key, value = item.items()[0]
1666 if key == 'project':
1667 self.projects.append(value)
1668 elif key == 'job':
James E. Blair4317e9f2016-07-15 10:05:47 -07001669 if source_project is not None:
1670 value['_source_project'] = source_project
James E. Blaire208c482016-10-04 14:35:30 -07001671 if source_branch is not None:
1672 value['_source_branch'] = source_branch
James E. Blaird8e778f2015-12-22 14:09:20 -08001673 self.jobs.append(value)
1674 elif key == 'project-template':
1675 self.project_templates.append(value)
1676 elif key == 'pipeline':
1677 self.pipelines.append(value)
James E. Blaira98340f2016-09-02 11:33:49 -07001678 elif key == 'nodeset':
1679 self.nodesets.append(value)
James E. Blaird8e778f2015-12-22 14:09:20 -08001680 else:
Morgan Fainberg4245a422016-08-05 16:20:12 -07001681 raise Exception("Configuration item `%s` not recognized "
James E. Blaird8e778f2015-12-22 14:09:20 -08001682 "(when parsing %s)" %
Morgan Fainberg4245a422016-08-05 16:20:12 -07001683 (item, conf,))
James E. Blaird8e778f2015-12-22 14:09:20 -08001684
1685
James E. Blaireff88162013-07-01 12:44:14 -04001686class Layout(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001687 """Holds all of the Pipelines."""
1688
James E. Blaireff88162013-07-01 12:44:14 -04001689 def __init__(self):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001690 self.tenant = None
James E. Blaireff88162013-07-01 12:44:14 -04001691 self.projects = {}
James E. Blairb97ed802015-12-21 15:55:35 -08001692 self.project_configs = {}
1693 self.project_templates = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07001694 self.pipelines = OrderedDict()
James E. Blair83005782015-12-11 14:46:03 -08001695 # This is a dictionary of name -> [jobs]. The first element
1696 # of the list is the first job added with that name. It is
1697 # the reference definition for a given job. Subsequent
1698 # elements are aspects of that job with different matchers
1699 # that override some attribute of the job. These aspects all
1700 # inherit from the reference definition.
James E. Blairfef88ec2016-11-30 08:38:27 -08001701 self.jobs = {'noop': [Job('noop')]}
James E. Blaira98340f2016-09-02 11:33:49 -07001702 self.nodesets = {}
James E. Blaireff88162013-07-01 12:44:14 -04001703
1704 def getJob(self, name):
1705 if name in self.jobs:
James E. Blair83005782015-12-11 14:46:03 -08001706 return self.jobs[name][0]
James E. Blairb97ed802015-12-21 15:55:35 -08001707 raise Exception("Job %s not defined" % (name,))
James E. Blair83005782015-12-11 14:46:03 -08001708
1709 def getJobs(self, name):
1710 return self.jobs.get(name, [])
1711
1712 def addJob(self, job):
James E. Blair4317e9f2016-07-15 10:05:47 -07001713 # We can have multiple variants of a job all with the same
1714 # name, but these variants must all be defined in the same repo.
1715 prior_jobs = [j for j in self.getJobs(job.name)
1716 if j.source_project != job.source_project]
1717 if prior_jobs:
1718 raise Exception("Job %s in %s is not permitted to shadow "
1719 "job %s in %s" % (job, job.source_project,
1720 prior_jobs[0],
1721 prior_jobs[0].source_project))
1722
James E. Blair83005782015-12-11 14:46:03 -08001723 if job.name in self.jobs:
1724 self.jobs[job.name].append(job)
1725 else:
1726 self.jobs[job.name] = [job]
1727
James E. Blaira98340f2016-09-02 11:33:49 -07001728 def addNodeSet(self, nodeset):
1729 if nodeset.name in self.nodesets:
1730 raise Exception("NodeSet %s already defined" % (nodeset.name,))
1731 self.nodesets[nodeset.name] = nodeset
1732
James E. Blair83005782015-12-11 14:46:03 -08001733 def addPipeline(self, pipeline):
1734 self.pipelines[pipeline.name] = pipeline
James E. Blair59fdbac2015-12-07 17:08:06 -08001735
James E. Blairb97ed802015-12-21 15:55:35 -08001736 def addProjectTemplate(self, project_template):
1737 self.project_templates[project_template.name] = project_template
1738
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001739 def addProjectConfig(self, project_config, update_pipeline=True):
James E. Blairb97ed802015-12-21 15:55:35 -08001740 self.project_configs[project_config.name] = project_config
1741 # TODOv3(jeblair): tidy up the relationship between pipelines
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001742 # and projects and projectconfigs. Specifically, move
1743 # job_trees out of the pipeline since they are more dynamic
1744 # than pipelines. Remove the update_pipeline argument
1745 if not update_pipeline:
1746 return
James E. Blairb97ed802015-12-21 15:55:35 -08001747 for pipeline_name, pipeline_config in project_config.pipelines.items():
1748 pipeline = self.pipelines[pipeline_name]
1749 project = pipeline.source.getProject(project_config.name)
1750 pipeline.job_trees[project] = pipeline_config.job_tree
1751
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001752 def _createJobTree(self, change, job_trees, parent):
1753 for tree in job_trees:
1754 job = tree.job
1755 if not job.changeMatches(change):
1756 continue
1757 frozen_job = Job(job.name)
1758 frozen_tree = JobTree(frozen_job)
1759 inherited = set()
1760 for variant in self.getJobs(job.name):
1761 if variant.changeMatches(change):
1762 if variant not in inherited:
1763 frozen_job.inheritFrom(variant)
1764 inherited.add(variant)
1765 if job not in inherited:
1766 # Only update from the job in the tree if it is
1767 # unique, otherwise we might unset an attribute we
1768 # have overloaded.
1769 frozen_job.inheritFrom(job)
1770 parent.job_trees.append(frozen_tree)
1771 self._createJobTree(change, tree.job_trees, frozen_tree)
1772
1773 def createJobTree(self, item):
Paul Belanger160cb8e2016-11-11 19:04:24 -05001774 project_config = self.project_configs.get(
1775 item.change.project.name, None)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001776 ret = JobTree(None)
Paul Belanger15e3e202016-10-14 16:27:34 -04001777 # NOTE(pabelanger): It is possible for a foreign project not to have a
1778 # configured pipeline, if so return an empty JobTree.
Paul Belanger160cb8e2016-11-11 19:04:24 -05001779 if project_config and item.pipeline.name in project_config.pipelines:
Paul Belanger15e3e202016-10-14 16:27:34 -04001780 project_tree = \
1781 project_config.pipelines[item.pipeline.name].job_tree
1782 self._createJobTree(item.change, project_tree.job_trees, ret)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001783 return ret
1784
James E. Blair59fdbac2015-12-07 17:08:06 -08001785
1786class Tenant(object):
1787 def __init__(self, name):
1788 self.name = name
1789 self.layout = None
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001790 # The list of repos from which we will read main
1791 # configuration. (source, project)
1792 self.config_repos = []
1793 # The unparsed config from those repos.
1794 self.config_repos_config = None
1795 # The list of projects from which we will read in-repo
1796 # configuration. (source, project)
1797 self.project_repos = []
1798 # The unparsed config from those repos.
1799 self.project_repos_config = None
James E. Blair59fdbac2015-12-07 17:08:06 -08001800
1801
1802class Abide(object):
1803 def __init__(self):
1804 self.tenants = OrderedDict()
James E. Blairce8a2132016-05-19 15:21:52 -07001805
1806
1807class JobTimeData(object):
1808 format = 'B10H10H10B'
1809 version = 0
1810
1811 def __init__(self, path):
1812 self.path = path
1813 self.success_times = [0 for x in range(10)]
1814 self.failure_times = [0 for x in range(10)]
1815 self.results = [0 for x in range(10)]
1816
1817 def load(self):
1818 if not os.path.exists(self.path):
1819 return
1820 with open(self.path) as f:
1821 data = struct.unpack(self.format, f.read())
1822 version = data[0]
1823 if version != self.version:
1824 raise Exception("Unkown data version")
1825 self.success_times = list(data[1:11])
1826 self.failure_times = list(data[11:21])
1827 self.results = list(data[21:32])
1828
1829 def save(self):
1830 tmpfile = self.path + '.tmp'
1831 data = [self.version]
1832 data.extend(self.success_times)
1833 data.extend(self.failure_times)
1834 data.extend(self.results)
1835 data = struct.pack(self.format, *data)
1836 with open(tmpfile, 'w') as f:
1837 f.write(data)
1838 os.rename(tmpfile, self.path)
1839
1840 def add(self, elapsed, result):
1841 elapsed = int(elapsed)
1842 if result == 'SUCCESS':
1843 self.success_times.append(elapsed)
1844 self.success_times.pop(0)
1845 result = 0
1846 else:
1847 self.failure_times.append(elapsed)
1848 self.failure_times.pop(0)
1849 result = 1
1850 self.results.append(result)
1851 self.results.pop(0)
1852
1853 def getEstimatedTime(self):
1854 times = [x for x in self.success_times if x]
1855 if times:
1856 return float(sum(times)) / len(times)
1857 return 0.0
1858
1859
1860class TimeDataBase(object):
1861 def __init__(self, root):
1862 self.root = root
1863 self.jobs = {}
1864
1865 def _getTD(self, name):
1866 td = self.jobs.get(name)
1867 if not td:
1868 td = JobTimeData(os.path.join(self.root, name))
1869 self.jobs[name] = td
1870 td.load()
1871 return td
1872
1873 def getEstimatedTime(self, name):
1874 return self._getTD(name).getEstimatedTime()
1875
1876 def update(self, name, elapsed, result):
1877 td = self._getTD(name)
1878 td.add(elapsed, result)
1879 td.save()