blob: a0424b6a14eea88cb77c018265f0b97a75f297cb [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
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000332 # foreign projects are those referenced in dependencies
333 # of layout projects, this should matter
334 # when deciding whether to enqueue their changes
James E. Blaircf440a22016-07-15 09:11:58 -0700335 # TODOv3 (jeblair): re-add support for foreign projects if needed
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000336 self.foreign = foreign
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700337 self.unparsed_config = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700338
339 def __str__(self):
340 return self.name
341
342 def __repr__(self):
343 return '<Project %s>' % (self.name)
344
345
James E. Blair34776ee2016-08-25 13:53:54 -0700346class Node(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700347 """A single node for use by a job.
348
349 This may represent a request for a node, or an actual node
350 provided by Nodepool.
351 """
352
James E. Blair34776ee2016-08-25 13:53:54 -0700353 def __init__(self, name, image):
354 self.name = name
355 self.image = image
356
357 def __repr__(self):
358 return '<Node %s:%s>' % (self.name, self.image)
359
360
James E. Blaira98340f2016-09-02 11:33:49 -0700361class NodeSet(object):
362 """A set of nodes.
363
364 In configuration, NodeSets are attributes of Jobs indicating that
365 a Job requires nodes matching this description.
366
367 They may appear as top-level configuration objects and be named,
368 or they may appears anonymously in in-line job definitions.
369 """
370
371 def __init__(self, name=None):
372 self.name = name or ''
373 self.nodes = OrderedDict()
374
375 def addNode(self, node):
376 if node.name in self.nodes:
377 raise Exception("Duplicate node in %s" % (self,))
378 self.nodes[node.name] = node
379
James E. Blair0eaad552016-09-02 12:09:54 -0700380 def getNodes(self):
381 return self.nodes.values()
382
James E. Blaira98340f2016-09-02 11:33:49 -0700383 def __repr__(self):
384 if self.name:
385 name = self.name + ' '
386 else:
387 name = ''
388 return '<NodeSet %s%s>' % (name, self.nodes)
389
390
James E. Blair34776ee2016-08-25 13:53:54 -0700391class NodeRequest(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700392 """A request for a set of nodes."""
393
James E. Blair0eaad552016-09-02 12:09:54 -0700394 def __init__(self, build_set, job, nodeset):
James E. Blair34776ee2016-08-25 13:53:54 -0700395 self.build_set = build_set
396 self.job = job
James E. Blair0eaad552016-09-02 12:09:54 -0700397 self.nodeset = nodeset
James E. Blairdce6cea2016-12-20 16:45:32 -0800398 self._state = 'requested'
399 self.state_time = time.time()
400 self.stat = None
401 self.uid = uuid4().hex
402
403 @property
404 def state(self):
405 return self._state
406
407 @state.setter
408 def state(self, value):
409 # TODOv3(jeblair): reinstate
410 # if value not in STATES:
411 # raise TypeError("'%s' is not a valid state" % value)
412 self._state = value
413 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700414
415 def __repr__(self):
James E. Blair0eaad552016-09-02 12:09:54 -0700416 return '<NodeRequest %s>' % (self.nodeset,)
James E. Blair34776ee2016-08-25 13:53:54 -0700417
James E. Blairdce6cea2016-12-20 16:45:32 -0800418 def toDict(self):
419 d = {}
420 nodes = [n.image for n in self.nodeset.getNodes()]
421 d['node_types'] = nodes
422 d['requestor'] = 'zuul' # TODOv3(jeblair): better descriptor
423 d['state'] = self.state
424 d['state_time'] = self.state_time
425 return d
426
427 def updateFromDict(self, data):
428 self._state = data['state']
429 self.state_time = data['state_time']
430
James E. Blair34776ee2016-08-25 13:53:54 -0700431
James E. Blairee743612012-05-29 14:49:32 -0700432class Job(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700433 """A Job represents the defintion of actions to perform."""
434
James E. Blair83005782015-12-11 14:46:03 -0800435 attributes = dict(
436 timeout=None,
437 # variables={},
James E. Blair0eaad552016-09-02 12:09:54 -0700438 nodeset=NodeSet(),
Ricardo Carrillo Cruz4e94f612016-07-25 16:11:56 +0000439 auth={},
James E. Blair83005782015-12-11 14:46:03 -0800440 workspace=None,
441 pre_run=None,
442 post_run=None,
443 voting=None,
James E. Blair791b5392016-08-03 11:25:56 -0700444 hold_following_changes=None,
James E. Blair83005782015-12-11 14:46:03 -0800445 failure_message=None,
446 success_message=None,
447 failure_url=None,
448 success_url=None,
449 # Matchers. These are separate so they can be individually
450 # overidden.
451 branch_matcher=None,
452 file_matcher=None,
453 irrelevant_file_matcher=None, # skip-if
Joshua Heskethdc7820c2016-03-11 13:14:28 +1100454 tags=set(),
Joshua Hesketh89b67f62016-02-11 21:22:14 +1100455 mutex=None,
Joshua Hesketh3f7def32016-11-21 17:36:44 +1100456 attempts=3,
James E. Blair83005782015-12-11 14:46:03 -0800457 )
458
James E. Blairee743612012-05-29 14:49:32 -0700459 def __init__(self, name):
460 self.name = name
James E. Blair4317e9f2016-07-15 10:05:47 -0700461 self.project_source = None
James E. Blair83005782015-12-11 14:46:03 -0800462 for k, v in self.attributes.items():
463 setattr(self, k, v)
464
Paul Belangere22baea2016-11-03 16:59:27 -0400465 def __eq__(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800466 # Compare the name and all inheritable attributes to determine
467 # whether two jobs with the same name are identically
468 # configured. Useful upon reconfiguration.
469 if not isinstance(other, Job):
470 return False
471 if self.name != other.name:
472 return False
473 for k, v in self.attributes.items():
474 if getattr(self, k) != getattr(other, k):
475 return False
476 return True
James E. Blairee743612012-05-29 14:49:32 -0700477
478 def __str__(self):
479 return self.name
480
481 def __repr__(self):
James E. Blair34776ee2016-08-25 13:53:54 -0700482 return '<Job %s branches: %s>' % (self.name, self.branch_matcher)
James E. Blair83005782015-12-11 14:46:03 -0800483
484 def inheritFrom(self, other):
485 """Copy the inheritable attributes which have been set on the other
486 job to this job."""
487
488 if not isinstance(other, Job):
489 raise Exception("Job unable to inherit from %s" % (other,))
490 for k, v in self.attributes.items():
Ricardo Carrillo Cruz4e94f612016-07-25 16:11:56 +0000491 if getattr(other, k) != v and k != 'auth':
James E. Blair83005782015-12-11 14:46:03 -0800492 setattr(self, k, getattr(other, k))
Ricardo Carrillo Cruz4e94f612016-07-25 16:11:56 +0000493 # Inherit auth only if explicitly allowed
494 if other.auth and 'inherit' in other.auth and other.auth['inherit']:
495 setattr(self, 'auth', getattr(other, 'auth'))
James E. Blairee743612012-05-29 14:49:32 -0700496
James E. Blaire421a232012-07-25 16:59:21 -0700497 def changeMatches(self, change):
James E. Blair83005782015-12-11 14:46:03 -0800498 if self.branch_matcher and not self.branch_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800499 return False
500
James E. Blair83005782015-12-11 14:46:03 -0800501 if self.file_matcher and not self.file_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800502 return False
503
James E. Blair83005782015-12-11 14:46:03 -0800504 # NB: This is a negative match.
505 if (self.irrelevant_file_matcher and
506 self.irrelevant_file_matcher.matches(change)):
Maru Newby3fe5f852015-01-13 04:22:14 +0000507 return False
508
James E. Blair70c71582013-03-06 08:50:50 -0800509 return True
James E. Blaire5a847f2012-07-10 15:29:14 -0700510
James E. Blair1e8dd892012-05-30 09:15:05 -0700511
James E. Blairee743612012-05-29 14:49:32 -0700512class JobTree(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700513 """A JobTree holds one or more Jobs to represent Job dependencies.
514
515 If Job foo should only execute if Job bar succeeds, then there will
516 be a JobTree for foo, which will contain a JobTree for bar. A JobTree
517 can hold more than one dependent JobTrees, such that jobs bar and bang
518 both depend on job foo being successful.
519
520 A root node of a JobTree will have no associated Job."""
James E. Blairee743612012-05-29 14:49:32 -0700521
522 def __init__(self, job):
523 self.job = job
524 self.job_trees = []
525
526 def addJob(self, job):
James E. Blair12a92b12014-03-26 11:54:53 -0700527 if job not in [x.job for x in self.job_trees]:
528 t = JobTree(job)
529 self.job_trees.append(t)
530 return t
James E. Blaire4ad55a2015-06-11 08:22:43 -0700531 for tree in self.job_trees:
532 if tree.job == job:
533 return tree
James E. Blairee743612012-05-29 14:49:32 -0700534
535 def getJobs(self):
536 jobs = []
537 for x in self.job_trees:
538 jobs.append(x.job)
539 jobs.extend(x.getJobs())
540 return jobs
541
542 def getJobTreeForJob(self, job):
543 if self.job == job:
544 return self
545 for tree in self.job_trees:
546 ret = tree.getJobTreeForJob(job)
547 if ret:
548 return ret
549 return None
550
James E. Blairb97ed802015-12-21 15:55:35 -0800551 def inheritFrom(self, other):
552 if other.job:
553 self.job = Job(other.job.name)
554 self.job.inheritFrom(other.job)
555 for other_tree in other.job_trees:
556 this_tree = self.getJobTreeForJob(other_tree.job)
557 if not this_tree:
558 this_tree = JobTree(None)
559 self.job_trees.append(this_tree)
560 this_tree.inheritFrom(other_tree)
561
James E. Blair1e8dd892012-05-30 09:15:05 -0700562
James E. Blair4aea70c2012-07-26 14:23:24 -0700563class Build(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700564 """A Build is an instance of a single running Job."""
565
James E. Blair4aea70c2012-07-26 14:23:24 -0700566 def __init__(self, job, uuid):
567 self.job = job
568 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -0700569 self.url = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700570 self.result = None
571 self.build_set = None
572 self.launch_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -0800573 self.start_time = None
574 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -0700575 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -0700576 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -0700577 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -0700578 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -0700579 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +0800580 self.worker = Worker()
Timothy Chavezb2332082015-08-07 20:08:04 -0500581 self.node_labels = []
582 self.node_name = None
James E. Blairee743612012-05-29 14:49:32 -0700583
584 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +0800585 return ('<Build %s of %s on %s>' %
586 (self.uuid, self.job.name, self.worker))
587
588
589class Worker(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700590 """Information about the specific worker executing a Build."""
Joshua Heskethba8776a2014-01-12 14:35:40 +0800591 def __init__(self):
592 self.name = "Unknown"
593 self.hostname = None
594 self.ips = []
595 self.fqdn = None
596 self.program = None
597 self.version = None
598 self.extra = {}
599
600 def updateFromData(self, data):
601 """Update worker information if contained in the WORK_DATA response."""
602 self.name = data.get('worker_name', self.name)
603 self.hostname = data.get('worker_hostname', self.hostname)
604 self.ips = data.get('worker_ips', self.ips)
605 self.fqdn = data.get('worker_fqdn', self.fqdn)
606 self.program = data.get('worker_program', self.program)
607 self.version = data.get('worker_version', self.version)
608 self.extra = data.get('worker_extra', self.extra)
609
610 def __repr__(self):
611 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -0700612
James E. Blair1e8dd892012-05-30 09:15:05 -0700613
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700614class RepoFiles(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700615 """RepoFiles holds config-file content for per-project job config."""
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700616 # When we ask a merger to prepare a future multiple-repo state and
617 # collect files so that we can dynamically load our configuration,
618 # this class provides easy access to that data.
619 def __init__(self):
620 self.projects = {}
621
622 def __repr__(self):
623 return '<RepoFiles %s>' % self.projects
624
625 def setFiles(self, items):
626 self.projects = {}
627 for item in items:
628 project = self.projects.setdefault(item['project'], {})
629 branch = project.setdefault(item['branch'], {})
630 branch.update(item['files'])
631
632 def getFile(self, project, branch, fn):
633 return self.projects.get(project, {}).get(branch, {}).get(fn)
634
635
James E. Blair7e530ad2012-07-03 16:12:28 -0700636class BuildSet(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700637 """Contains the Builds for a Change representing potential future state.
638
639 A BuildSet also holds the UUID used to produce the Zuul Ref that builders
640 check out.
641 """
James E. Blair4076e2b2014-01-28 12:42:20 -0800642 # Merge states:
643 NEW = 1
644 PENDING = 2
645 COMPLETE = 3
646
Antoine Musso9b229282014-08-18 23:45:43 +0200647 states_map = {
648 1: 'NEW',
649 2: 'PENDING',
650 3: 'COMPLETE',
651 }
652
James E. Blairfee8d652013-06-07 08:57:52 -0700653 def __init__(self, item):
654 self.item = item
James E. Blair11700c32012-07-05 17:50:05 -0700655 self.other_changes = []
James E. Blair7e530ad2012-07-03 16:12:28 -0700656 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -0700657 self.result = None
658 self.next_build_set = None
659 self.previous_build_set = None
James E. Blair4886cc12012-07-18 15:39:41 -0700660 self.ref = None
James E. Blair81515ad2012-10-01 18:29:08 -0700661 self.commit = None
James E. Blair4076e2b2014-01-28 12:42:20 -0800662 self.zuul_url = None
James E. Blair973721f2012-08-15 10:19:43 -0700663 self.unable_to_merge = False
James E. Blair972e3c72013-08-29 12:04:55 -0700664 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -0800665 self.merge_state = self.NEW
James E. Blair0eaad552016-09-02 12:09:54 -0700666 self.nodesets = {} # job -> nodeset
James E. Blair8d692392016-04-08 17:47:58 -0700667 self.node_requests = {} # job -> reqs
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700668 self.files = RepoFiles()
669 self.layout = None
Paul Belanger71d98172016-11-08 10:56:31 -0500670 self.tries = {}
James E. Blair7e530ad2012-07-03 16:12:28 -0700671
Antoine Musso9b229282014-08-18 23:45:43 +0200672 def __repr__(self):
673 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
674 self.item,
675 len(self.builds),
676 self.getStateName(self.merge_state))
677
James E. Blair4886cc12012-07-18 15:39:41 -0700678 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -0700679 # The change isn't enqueued until after it's created
680 # so we don't know what the other changes ahead will be
681 # until jobs start.
682 if not self.other_changes:
James E. Blairfee8d652013-06-07 08:57:52 -0700683 next_item = self.item.item_ahead
684 while next_item:
685 self.other_changes.append(next_item.change)
686 next_item = next_item.item_ahead
James E. Blair4886cc12012-07-18 15:39:41 -0700687 if not self.ref:
688 self.ref = 'Z' + uuid4().hex
689
Antoine Musso9b229282014-08-18 23:45:43 +0200690 def getStateName(self, state_num):
691 return self.states_map.get(
692 state_num, 'UNKNOWN (%s)' % state_num)
693
James E. Blair4886cc12012-07-18 15:39:41 -0700694 def addBuild(self, build):
695 self.builds[build.job.name] = build
Paul Belanger71d98172016-11-08 10:56:31 -0500696 if build.job.name not in self.tries:
Clint Byrum5bb5feb2016-12-08 12:39:05 -0800697 self.tries[build.job.name] = 0
James E. Blair4886cc12012-07-18 15:39:41 -0700698 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -0700699
James E. Blair4a28a882013-08-23 15:17:33 -0700700 def removeBuild(self, build):
Paul Belanger71d98172016-11-08 10:56:31 -0500701 self.tries[build.job.name] += 1
James E. Blair4a28a882013-08-23 15:17:33 -0700702 del self.builds[build.job.name]
703
James E. Blair7e530ad2012-07-03 16:12:28 -0700704 def getBuild(self, job_name):
705 return self.builds.get(job_name)
706
James E. Blair11700c32012-07-05 17:50:05 -0700707 def getBuilds(self):
708 keys = self.builds.keys()
709 keys.sort()
710 return [self.builds.get(x) for x in keys]
711
James E. Blair0eaad552016-09-02 12:09:54 -0700712 def getJobNodeSet(self, job_name):
713 # Return None if not provisioned; empty NodeSet if no nodes
714 # required
715 return self.nodesets.get(job_name)
James E. Blair8d692392016-04-08 17:47:58 -0700716
717 def setJobNodeRequest(self, job_name, req):
718 if job_name in self.node_requests:
719 raise Exception("Prior node request for %s" % (job_name))
720 self.node_requests[job_name] = req
721
722 def getJobNodeRequest(self, job_name):
723 return self.node_requests.get(job_name)
724
James E. Blair0eaad552016-09-02 12:09:54 -0700725 def jobNodeRequestComplete(self, job_name, req, nodeset):
726 if job_name in self.nodesets:
James E. Blair8d692392016-04-08 17:47:58 -0700727 raise Exception("Prior node request for %s" % (job_name))
James E. Blair0eaad552016-09-02 12:09:54 -0700728 self.nodesets[job_name] = nodeset
James E. Blair8d692392016-04-08 17:47:58 -0700729 del self.node_requests[job_name]
730
Paul Belanger71d98172016-11-08 10:56:31 -0500731 def getTries(self, job_name):
732 return self.tries.get(job_name)
733
Adam Gandelman8bd57102016-12-02 12:58:42 -0800734 def getMergeMode(self, job_name):
735 if not self.layout or job_name not in self.layout.project_configs:
736 return MERGER_MERGE_RESOLVE
737 return self.layout.project_configs[job_name].merge_mode
738
James E. Blair7e530ad2012-07-03 16:12:28 -0700739
James E. Blairfee8d652013-06-07 08:57:52 -0700740class QueueItem(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700741 """Represents the position of a Change in a ChangeQueue.
742
743 All Changes are enqueued into ChangeQueue in a QueueItem. The QueueItem
744 holds the current `BuildSet` as well as all previous `BuildSets` that were
745 produced for this `QueueItem`.
746 """
James E. Blair32663402012-06-01 10:04:18 -0700747
James E. Blairbfb8e042014-12-30 17:01:44 -0800748 def __init__(self, queue, change):
749 self.pipeline = queue.pipeline
750 self.queue = queue
James E. Blairfee8d652013-06-07 08:57:52 -0700751 self.change = change # a changeish
James E. Blair7e530ad2012-07-03 16:12:28 -0700752 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -0700753 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -0700754 self.current_build_set = BuildSet(self)
755 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -0700756 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700757 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -0800758 self.enqueue_time = None
759 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -0700760 self.reported = False
James E. Blairbfb8e042014-12-30 17:01:44 -0800761 self.active = False # Whether an item is within an active window
762 self.live = True # Whether an item is intended to be processed at all
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700763 self.layout = None # This item's shadow layout
James E. Blair83005782015-12-11 14:46:03 -0800764 self.job_tree = None
James E. Blaire5a847f2012-07-10 15:29:14 -0700765
James E. Blair972e3c72013-08-29 12:04:55 -0700766 def __repr__(self):
767 if self.pipeline:
768 pipeline = self.pipeline.name
769 else:
770 pipeline = None
771 return '<QueueItem 0x%x for %s in %s>' % (
772 id(self), self.change, pipeline)
773
James E. Blairee743612012-05-29 14:49:32 -0700774 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -0700775 old = self.current_build_set
776 self.current_build_set.result = 'CANCELED'
777 self.current_build_set = BuildSet(self)
778 old.next_build_set = self.current_build_set
779 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -0700780 self.build_sets.append(self.current_build_set)
James E. Blairee743612012-05-29 14:49:32 -0700781
782 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -0700783 self.current_build_set.addBuild(build)
James E. Blair66eeebf2013-07-27 17:44:32 -0700784 build.pipeline = self.pipeline
James E. Blairee743612012-05-29 14:49:32 -0700785
James E. Blair4a28a882013-08-23 15:17:33 -0700786 def removeBuild(self, build):
787 self.current_build_set.removeBuild(build)
788
James E. Blairfee8d652013-06-07 08:57:52 -0700789 def setReportedResult(self, result):
790 self.current_build_set.result = result
791
James E. Blair83005782015-12-11 14:46:03 -0800792 def freezeJobTree(self):
793 """Find or create actual matching jobs for this item's change and
794 store the resulting job tree."""
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700795 layout = self.current_build_set.layout
796 self.job_tree = layout.createJobTree(self)
797
798 def hasJobTree(self):
799 """Returns True if the item has a job tree."""
800 return self.job_tree is not None
James E. Blair83005782015-12-11 14:46:03 -0800801
802 def getJobs(self):
803 if not self.live or not self.job_tree:
804 return []
805 return self.job_tree.getJobs()
806
James E. Blairdbfd3282016-07-21 10:46:19 -0700807 def haveAllJobsStarted(self):
808 if not self.hasJobTree():
809 return False
810 for job in self.getJobs():
811 build = self.current_build_set.getBuild(job.name)
812 if not build or not build.start_time:
813 return False
814 return True
815
816 def areAllJobsComplete(self):
817 if not self.hasJobTree():
818 return False
819 for job in self.getJobs():
820 build = self.current_build_set.getBuild(job.name)
821 if not build or not build.result:
822 return False
823 return True
824
825 def didAllJobsSucceed(self):
826 if not self.hasJobTree():
827 return False
828 for job in self.getJobs():
829 if not job.voting:
830 continue
831 build = self.current_build_set.getBuild(job.name)
832 if not build:
833 return False
834 if build.result != 'SUCCESS':
835 return False
836 return True
837
838 def didAnyJobFail(self):
839 if not self.hasJobTree():
840 return False
841 for job in self.getJobs():
842 if not job.voting:
843 continue
844 build = self.current_build_set.getBuild(job.name)
845 if build and build.result and (build.result != 'SUCCESS'):
846 return True
847 return False
848
849 def didMergerFail(self):
850 if self.current_build_set.unable_to_merge:
851 return True
852 return False
853
James E. Blairdbfd3282016-07-21 10:46:19 -0700854 def isHoldingFollowingChanges(self):
855 if not self.live:
856 return False
857 if not self.hasJobTree():
858 return False
859 for job in self.getJobs():
860 if not job.hold_following_changes:
861 continue
862 build = self.current_build_set.getBuild(job.name)
863 if not build:
864 return True
865 if build.result != 'SUCCESS':
866 return True
867
868 if not self.item_ahead:
869 return False
870 return self.item_ahead.isHoldingFollowingChanges()
871
872 def _findJobsToRun(self, job_trees, mutex):
873 torun = []
James E. Blair791b5392016-08-03 11:25:56 -0700874 if self.item_ahead:
875 # Only run jobs if any 'hold' jobs on the change ahead
876 # have completed successfully.
877 if self.item_ahead.isHoldingFollowingChanges():
878 return []
James E. Blairdbfd3282016-07-21 10:46:19 -0700879 for tree in job_trees:
880 job = tree.job
881 result = None
882 if job:
883 if not job.changeMatches(self.change):
884 continue
885 build = self.current_build_set.getBuild(job.name)
886 if build:
887 result = build.result
888 else:
889 # There is no build for the root of this job tree,
James E. Blair34776ee2016-08-25 13:53:54 -0700890 # so it has not run yet.
James E. Blair0eaad552016-09-02 12:09:54 -0700891 nodeset = self.current_build_set.getJobNodeSet(job.name)
892 if nodeset is None:
James E. Blair34776ee2016-08-25 13:53:54 -0700893 # The nodes for this job are not ready, skip
894 # it for now.
895 continue
James E. Blairdbfd3282016-07-21 10:46:19 -0700896 if mutex.acquire(self, job):
897 # If this job needs a mutex, either acquire it or make
898 # sure that we have it before running the job.
899 torun.append(job)
900 # If there is no job, this is a null job tree, and we should
901 # run all of its jobs.
902 if result == 'SUCCESS' or not job:
903 torun.extend(self._findJobsToRun(tree.job_trees, mutex))
904 return torun
905
906 def findJobsToRun(self, mutex):
907 if not self.live:
908 return []
909 tree = self.job_tree
910 if not tree:
911 return []
912 return self._findJobsToRun(tree.job_trees, mutex)
913
914 def _findJobsToRequest(self, job_trees):
915 toreq = []
916 for tree in job_trees:
917 job = tree.job
918 if job:
919 if not job.changeMatches(self.change):
920 continue
James E. Blair0eaad552016-09-02 12:09:54 -0700921 nodeset = self.current_build_set.getJobNodeSet(job.name)
922 if nodeset is None:
James E. Blairdbfd3282016-07-21 10:46:19 -0700923 req = self.current_build_set.getJobNodeRequest(job.name)
924 if req is None:
925 toreq.append(job)
James E. Blair34776ee2016-08-25 13:53:54 -0700926 toreq.extend(self._findJobsToRequest(tree.job_trees))
James E. Blairdbfd3282016-07-21 10:46:19 -0700927 return toreq
928
929 def findJobsToRequest(self):
930 if not self.live:
931 return []
932 tree = self.job_tree
933 if not tree:
934 return []
935 return self._findJobsToRequest(tree.job_trees)
936
937 def setResult(self, build):
938 if build.retry:
939 self.removeBuild(build)
940 elif build.result != 'SUCCESS':
941 # Get a JobTree from a Job so we can find only its dependent jobs
942 tree = self.job_tree.getJobTreeForJob(build.job)
943 for job in tree.getJobs():
944 fakebuild = Build(job, None)
945 fakebuild.result = 'SKIPPED'
946 self.addBuild(fakebuild)
947
948 def setDequeuedNeedingChange(self):
949 self.dequeued_needing_change = True
950 self._setAllJobsSkipped()
951
952 def setUnableToMerge(self):
953 self.current_build_set.unable_to_merge = True
954 self._setAllJobsSkipped()
955
956 def _setAllJobsSkipped(self):
957 for job in self.getJobs():
958 fakebuild = Build(job, None)
959 fakebuild.result = 'SKIPPED'
960 self.addBuild(fakebuild)
961
James E. Blairb7273ef2016-04-19 08:58:51 -0700962 def formatJobResult(self, job, url_pattern=None):
963 build = self.current_build_set.getBuild(job.name)
964 result = build.result
965 pattern = url_pattern
966 if result == 'SUCCESS':
967 if job.success_message:
968 result = job.success_message
James E. Blaira5dba232016-08-08 15:53:24 -0700969 if job.success_url:
970 pattern = job.success_url
James E. Blairb7273ef2016-04-19 08:58:51 -0700971 elif result == 'FAILURE':
972 if job.failure_message:
973 result = job.failure_message
James E. Blaira5dba232016-08-08 15:53:24 -0700974 if job.failure_url:
975 pattern = job.failure_url
James E. Blairb7273ef2016-04-19 08:58:51 -0700976 url = None
977 if pattern:
978 try:
979 url = pattern.format(change=self.change,
980 pipeline=self.pipeline,
981 job=job,
982 build=build)
983 except Exception:
984 pass # FIXME: log this or something?
985 if not url:
986 url = build.url or job.name
987 return (result, url)
988
989 def formatJSON(self, url_pattern=None):
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800990 changeish = self.change
991 ret = {}
992 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -0800993 ret['live'] = self.live
Joshua Hesketh85af4e92014-02-21 08:28:58 -0800994 if hasattr(changeish, 'url') and changeish.url is not None:
995 ret['url'] = changeish.url
996 else:
997 ret['url'] = None
998 ret['id'] = changeish._id()
999 if self.item_ahead:
1000 ret['item_ahead'] = self.item_ahead.change._id()
1001 else:
1002 ret['item_ahead'] = None
1003 ret['items_behind'] = [i.change._id() for i in self.items_behind]
1004 ret['failing_reasons'] = self.current_build_set.failing_reasons
1005 ret['zuul_ref'] = self.current_build_set.ref
Ramy Asselin07cc33c2015-06-12 14:06:34 -07001006 if changeish.project:
1007 ret['project'] = changeish.project.name
1008 else:
1009 # For cross-project dependencies with the depends-on
1010 # project not known to zuul, the project is None
1011 # Set it to a static value
1012 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001013 ret['enqueue_time'] = int(self.enqueue_time * 1000)
1014 ret['jobs'] = []
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001015 if hasattr(changeish, 'owner'):
1016 ret['owner'] = changeish.owner
1017 else:
1018 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001019 max_remaining = 0
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001020 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001021 now = time.time()
1022 build = self.current_build_set.getBuild(job.name)
1023 elapsed = None
1024 remaining = None
1025 result = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001026 build_url = None
1027 report_url = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001028 worker = None
1029 if build:
1030 result = build.result
James E. Blairb7273ef2016-04-19 08:58:51 -07001031 build_url = build.url
1032 (unused, report_url) = self.formatJobResult(job, url_pattern)
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001033 if build.start_time:
1034 if build.end_time:
1035 elapsed = int((build.end_time -
1036 build.start_time) * 1000)
1037 remaining = 0
1038 else:
1039 elapsed = int((now - build.start_time) * 1000)
1040 if build.estimated_time:
1041 remaining = max(
1042 int(build.estimated_time * 1000) - elapsed,
1043 0)
1044 worker = {
1045 'name': build.worker.name,
1046 'hostname': build.worker.hostname,
1047 'ips': build.worker.ips,
1048 'fqdn': build.worker.fqdn,
1049 'program': build.worker.program,
1050 'version': build.worker.version,
1051 'extra': build.worker.extra
1052 }
1053 if remaining and remaining > max_remaining:
1054 max_remaining = remaining
1055
1056 ret['jobs'].append({
1057 'name': job.name,
1058 'elapsed_time': elapsed,
1059 'remaining_time': remaining,
James E. Blairb7273ef2016-04-19 08:58:51 -07001060 'url': build_url,
1061 'report_url': report_url,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001062 'result': result,
1063 'voting': job.voting,
1064 'uuid': build.uuid if build else None,
1065 'launch_time': build.launch_time if build else None,
1066 'start_time': build.start_time if build else None,
1067 'end_time': build.end_time if build else None,
1068 'estimated_time': build.estimated_time if build else None,
1069 'pipeline': build.pipeline.name if build else None,
1070 'canceled': build.canceled if build else None,
1071 'retry': build.retry if build else None,
Timothy Chavezb2332082015-08-07 20:08:04 -05001072 'node_labels': build.node_labels if build else [],
1073 'node_name': build.node_name if build else None,
1074 'worker': worker,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001075 })
1076
James E. Blairdbfd3282016-07-21 10:46:19 -07001077 if self.haveAllJobsStarted():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001078 ret['remaining_time'] = max_remaining
1079 else:
1080 ret['remaining_time'] = None
1081 return ret
1082
1083 def formatStatus(self, indent=0, html=False):
1084 changeish = self.change
1085 indent_str = ' ' * indent
1086 ret = ''
1087 if html and hasattr(changeish, 'url') and changeish.url is not None:
1088 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
1089 indent_str,
1090 changeish.project.name,
1091 changeish.url,
1092 changeish._id())
1093 else:
1094 ret += '%sProject %s change %s based on %s\n' % (
1095 indent_str,
1096 changeish.project.name,
1097 changeish._id(),
1098 self.item_ahead)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001099 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001100 build = self.current_build_set.getBuild(job.name)
1101 if build:
1102 result = build.result
1103 else:
1104 result = None
1105 job_name = job.name
1106 if not job.voting:
1107 voting = ' (non-voting)'
1108 else:
1109 voting = ''
1110 if html:
1111 if build:
1112 url = build.url
1113 else:
1114 url = None
1115 if url is not None:
1116 job_name = '<a href="%s">%s</a>' % (url, job_name)
1117 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
1118 ret += '\n'
1119 return ret
1120
James E. Blairfee8d652013-06-07 08:57:52 -07001121
1122class Changeish(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001123 """Base class for Change and Ref."""
James E. Blairfee8d652013-06-07 08:57:52 -07001124
1125 def __init__(self, project):
1126 self.project = project
1127
Joshua Hesketh36c3fa52014-01-22 11:40:52 +11001128 def getBasePath(self):
1129 base_path = ''
1130 if hasattr(self, 'refspec'):
1131 base_path = "%s/%s/%s" % (
1132 self.number[-2:], self.number, self.patchset)
1133 elif hasattr(self, 'ref'):
1134 base_path = "%s/%s" % (self.newrev[:2], self.newrev)
1135
1136 return base_path
1137
James E. Blairfee8d652013-06-07 08:57:52 -07001138 def equals(self, other):
1139 raise NotImplementedError()
1140
1141 def isUpdateOf(self, other):
1142 raise NotImplementedError()
1143
1144 def filterJobs(self, jobs):
1145 return filter(lambda job: job.changeMatches(self), jobs)
1146
1147 def getRelatedChanges(self):
1148 return set()
1149
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001150 def updatesConfig(self):
1151 return False
1152
James E. Blair1e8dd892012-05-30 09:15:05 -07001153
James E. Blair4aea70c2012-07-26 14:23:24 -07001154class Change(Changeish):
Monty Taylora42a55b2016-07-29 07:53:33 -07001155 """A proposed new state for a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001156 def __init__(self, project):
1157 super(Change, self).__init__(project)
1158 self.branch = None
1159 self.number = None
1160 self.url = None
1161 self.patchset = None
1162 self.refspec = None
1163
James E. Blair70c71582013-03-06 08:50:50 -08001164 self.files = []
James E. Blair6965a4b2014-12-16 17:19:04 -08001165 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -07001166 self.needed_by_changes = []
1167 self.is_current_patchset = True
1168 self.can_merge = False
1169 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -07001170 self.failed_to_merge = False
James E. Blairc053d022014-01-22 14:57:33 -08001171 self.approvals = []
James E. Blair11041d22014-05-02 14:49:53 -07001172 self.open = None
1173 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001174 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001175
1176 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -07001177 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -07001178
1179 def __repr__(self):
1180 return '<Change 0x%x %s>' % (id(self), self._id())
1181
1182 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +08001183 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -07001184 return True
1185 return False
1186
James E. Blair2fa50962013-01-30 21:50:41 -08001187 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -08001188 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -07001189 (hasattr(other, 'patchset') and
1190 self.patchset is not None and
1191 other.patchset is not None and
1192 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -08001193 return True
1194 return False
1195
James E. Blairfee8d652013-06-07 08:57:52 -07001196 def getRelatedChanges(self):
1197 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -08001198 for c in self.needs_changes:
1199 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -07001200 for c in self.needed_by_changes:
1201 related.add(c)
1202 related.update(c.getRelatedChanges())
1203 return related
James E. Blair4aea70c2012-07-26 14:23:24 -07001204
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001205 def updatesConfig(self):
1206 if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files:
1207 return True
1208 return False
1209
James E. Blair4aea70c2012-07-26 14:23:24 -07001210
1211class Ref(Changeish):
Monty Taylora42a55b2016-07-29 07:53:33 -07001212 """An existing state of a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001213 def __init__(self, project):
James E. Blairbe765db2012-08-07 08:36:20 -07001214 super(Ref, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -07001215 self.ref = None
1216 self.oldrev = None
1217 self.newrev = None
1218
James E. Blairbe765db2012-08-07 08:36:20 -07001219 def _id(self):
1220 return self.newrev
1221
Antoine Musso68bdcd72013-01-17 12:31:28 +01001222 def __repr__(self):
1223 rep = None
1224 if self.newrev == '0000000000000000000000000000000000000000':
1225 rep = '<Ref 0x%x deletes %s from %s' % (
1226 id(self), self.ref, self.oldrev)
1227 elif self.oldrev == '0000000000000000000000000000000000000000':
1228 rep = '<Ref 0x%x creates %s on %s>' % (
1229 id(self), self.ref, self.newrev)
1230 else:
1231 # Catch all
1232 rep = '<Ref 0x%x %s updated %s..%s>' % (
1233 id(self), self.ref, self.oldrev, self.newrev)
1234
1235 return rep
1236
James E. Blair4aea70c2012-07-26 14:23:24 -07001237 def equals(self, other):
James E. Blair9358c612012-09-28 08:29:39 -07001238 if (self.project == other.project
1239 and self.ref == other.ref
1240 and self.newrev == other.newrev):
James E. Blair4aea70c2012-07-26 14:23:24 -07001241 return True
1242 return False
1243
James E. Blair2fa50962013-01-30 21:50:41 -08001244 def isUpdateOf(self, other):
1245 return False
1246
James E. Blair4aea70c2012-07-26 14:23:24 -07001247
James E. Blair63bb0ef2013-07-29 17:14:51 -07001248class NullChange(Changeish):
James E. Blair23161912016-07-28 15:42:14 -07001249 # TODOv3(jeblair): remove this in favor of enqueueing Refs (eg
1250 # current master) instead.
James E. Blaire5910202013-12-27 09:50:31 -08001251 def __repr__(self):
1252 return '<NullChange for %s>' % (self.project)
James E. Blair63bb0ef2013-07-29 17:14:51 -07001253
James E. Blair63bb0ef2013-07-29 17:14:51 -07001254 def _id(self):
Alex Gaynorddb9ef32013-09-16 21:04:58 -07001255 return None
James E. Blair63bb0ef2013-07-29 17:14:51 -07001256
1257 def equals(self, other):
Steve Varnau7b78b312015-04-03 14:49:46 -07001258 if (self.project == other.project
1259 and other._id() is None):
James E. Blair4f6033c2014-03-27 15:49:09 -07001260 return True
James E. Blair63bb0ef2013-07-29 17:14:51 -07001261 return False
1262
1263 def isUpdateOf(self, other):
1264 return False
1265
1266
James E. Blairee743612012-05-29 14:49:32 -07001267class TriggerEvent(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001268 """Incoming event from an external system."""
James E. Blairee743612012-05-29 14:49:32 -07001269 def __init__(self):
1270 self.data = None
James E. Blair32663402012-06-01 10:04:18 -07001271 # common
James E. Blairee743612012-05-29 14:49:32 -07001272 self.type = None
Paul Belangerbaca3132016-11-04 12:49:54 -04001273 # For management events (eg: enqueue / promote)
1274 self.tenant_name = None
James E. Blairee743612012-05-29 14:49:32 -07001275 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -07001276 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +01001277 # Representation of the user account that performed the event.
1278 self.account = None
James E. Blair32663402012-06-01 10:04:18 -07001279 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -07001280 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -07001281 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -07001282 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -07001283 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -07001284 self.approvals = []
1285 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07001286 self.comment = None
James E. Blair32663402012-06-01 10:04:18 -07001287 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -07001288 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -07001289 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07001290 self.newrev = None
James E. Blair63bb0ef2013-07-29 17:14:51 -07001291 # timer
1292 self.timespec = None
James E. Blairc494d542014-08-06 09:23:52 -07001293 # zuultrigger
1294 self.pipeline_name = None
James E. Blairad28e912013-11-27 10:43:22 -08001295 # For events that arrive with a destination pipeline (eg, from
1296 # an admin command, etc):
1297 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07001298
James E. Blair9f9667e2012-06-12 17:51:08 -07001299 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001300 ret = '<TriggerEvent %s %s' % (self.type, self.project_name)
James E. Blair1e8dd892012-05-30 09:15:05 -07001301
James E. Blairee743612012-05-29 14:49:32 -07001302 if self.branch:
1303 ret += " %s" % self.branch
1304 if self.change_number:
1305 ret += " %s,%s" % (self.change_number, self.patch_number)
1306 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -07001307 ret += ' ' + ', '.join(
1308 ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
James E. Blairee743612012-05-29 14:49:32 -07001309 ret += '>'
1310
1311 return ret
1312
James E. Blair1e8dd892012-05-30 09:15:05 -07001313
James E. Blair9c17dbf2014-06-23 14:21:58 -07001314class BaseFilter(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001315 """Base Class for filtering which Changes and Events to process."""
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001316 def __init__(self, required_approvals=[], reject_approvals=[]):
James E. Blair5bf78a32015-07-30 18:08:24 +00001317 self._required_approvals = copy.deepcopy(required_approvals)
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001318 self.required_approvals = self._tidy_approvals(required_approvals)
1319 self._reject_approvals = copy.deepcopy(reject_approvals)
1320 self.reject_approvals = self._tidy_approvals(reject_approvals)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001321
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001322 def _tidy_approvals(self, approvals):
1323 for a in approvals:
James E. Blair9c17dbf2014-06-23 14:21:58 -07001324 for k, v in a.items():
1325 if k == 'username':
James E. Blairb01ec542016-06-16 09:46:49 -07001326 a['username'] = re.compile(v)
James E. Blair1fbfceb2014-06-23 14:42:53 -07001327 elif k in ['email', 'email-filter']:
James E. Blair5bf78a32015-07-30 18:08:24 +00001328 a['email'] = re.compile(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001329 elif k == 'newer-than':
James E. Blair5bf78a32015-07-30 18:08:24 +00001330 a[k] = time_to_seconds(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001331 elif k == 'older-than':
James E. Blair5bf78a32015-07-30 18:08:24 +00001332 a[k] = time_to_seconds(v)
James E. Blair1fbfceb2014-06-23 14:42:53 -07001333 if 'email-filter' in a:
1334 del a['email-filter']
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001335 return approvals
1336
1337 def _match_approval_required_approval(self, rapproval, approval):
1338 # Check if the required approval and approval match
1339 if 'description' not in approval:
1340 return False
1341 now = time.time()
1342 by = approval.get('by', {})
1343 for k, v in rapproval.items():
1344 if k == 'username':
James E. Blairb01ec542016-06-16 09:46:49 -07001345 if (not v.search(by.get('username', ''))):
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001346 return False
1347 elif k == 'email':
1348 if (not v.search(by.get('email', ''))):
1349 return False
1350 elif k == 'newer-than':
1351 t = now - v
1352 if (approval['grantedOn'] < t):
1353 return False
1354 elif k == 'older-than':
1355 t = now - v
1356 if (approval['grantedOn'] >= t):
1357 return False
1358 else:
1359 if not isinstance(v, list):
1360 v = [v]
1361 if (normalizeCategory(approval['description']) != k or
1362 int(approval['value']) not in v):
1363 return False
1364 return True
1365
1366 def matchesApprovals(self, change):
1367 if (self.required_approvals and not change.approvals
1368 or self.reject_approvals and not change.approvals):
1369 # A change with no approvals can not match
1370 return False
1371
1372 # TODO(jhesketh): If we wanted to optimise this slightly we could
1373 # analyse both the REQUIRE and REJECT filters by looping over the
1374 # approvals on the change and keeping track of what we have checked
1375 # rather than needing to loop on the change approvals twice
1376 return (self.matchesRequiredApprovals(change) and
1377 self.matchesNoRejectApprovals(change))
James E. Blair9c17dbf2014-06-23 14:21:58 -07001378
1379 def matchesRequiredApprovals(self, change):
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001380 # Check if any approvals match the requirements
James E. Blair5bf78a32015-07-30 18:08:24 +00001381 for rapproval in self.required_approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001382 matches_rapproval = False
James E. Blair9c17dbf2014-06-23 14:21:58 -07001383 for approval in change.approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001384 if self._match_approval_required_approval(rapproval, approval):
1385 # We have a matching approval so this requirement is
1386 # fulfilled
1387 matches_rapproval = True
James E. Blair5bf78a32015-07-30 18:08:24 +00001388 break
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001389 if not matches_rapproval:
James E. Blair5bf78a32015-07-30 18:08:24 +00001390 return False
James E. Blair9c17dbf2014-06-23 14:21:58 -07001391 return True
1392
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001393 def matchesNoRejectApprovals(self, change):
1394 # Check to make sure no approvals match a reject criteria
1395 for rapproval in self.reject_approvals:
1396 for approval in change.approvals:
1397 if self._match_approval_required_approval(rapproval, approval):
1398 # A reject approval has been matched, so we reject
1399 # immediately
1400 return False
1401 # To get here no rejects can have been matched so we should be good to
1402 # queue
1403 return True
1404
James E. Blair9c17dbf2014-06-23 14:21:58 -07001405
1406class EventFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07001407 """Allows a Pipeline to only respond to certain events."""
James E. Blairc0dedf82014-08-06 09:37:52 -07001408 def __init__(self, trigger, types=[], branches=[], refs=[],
1409 event_approvals={}, comments=[], emails=[], usernames=[],
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001410 timespecs=[], required_approvals=[], reject_approvals=[],
1411 pipelines=[], ignore_deletes=True):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001412 super(EventFilter, self).__init__(
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001413 required_approvals=required_approvals,
1414 reject_approvals=reject_approvals)
James E. Blairc0dedf82014-08-06 09:37:52 -07001415 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07001416 self._types = types
1417 self._branches = branches
1418 self._refs = refs
James E. Blair1fbfceb2014-06-23 14:42:53 -07001419 self._comments = comments
1420 self._emails = emails
1421 self._usernames = usernames
James E. Blairc494d542014-08-06 09:23:52 -07001422 self._pipelines = pipelines
James E. Blairee743612012-05-29 14:49:32 -07001423 self.types = [re.compile(x) for x in types]
1424 self.branches = [re.compile(x) for x in branches]
1425 self.refs = [re.compile(x) for x in refs]
James E. Blair1fbfceb2014-06-23 14:42:53 -07001426 self.comments = [re.compile(x) for x in comments]
1427 self.emails = [re.compile(x) for x in emails]
1428 self.usernames = [re.compile(x) for x in usernames]
James E. Blairc494d542014-08-06 09:23:52 -07001429 self.pipelines = [re.compile(x) for x in pipelines]
James E. Blairc053d022014-01-22 14:57:33 -08001430 self.event_approvals = event_approvals
James E. Blair63bb0ef2013-07-29 17:14:51 -07001431 self.timespecs = timespecs
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001432 self.ignore_deletes = ignore_deletes
James E. Blairee743612012-05-29 14:49:32 -07001433
James E. Blair9f9667e2012-06-12 17:51:08 -07001434 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001435 ret = '<EventFilter'
James E. Blair1e8dd892012-05-30 09:15:05 -07001436
James E. Blairee743612012-05-29 14:49:32 -07001437 if self._types:
1438 ret += ' types: %s' % ', '.join(self._types)
James E. Blairc494d542014-08-06 09:23:52 -07001439 if self._pipelines:
1440 ret += ' pipelines: %s' % ', '.join(self._pipelines)
James E. Blairee743612012-05-29 14:49:32 -07001441 if self._branches:
1442 ret += ' branches: %s' % ', '.join(self._branches)
1443 if self._refs:
1444 ret += ' refs: %s' % ', '.join(self._refs)
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001445 if self.ignore_deletes:
1446 ret += ' ignore_deletes: %s' % self.ignore_deletes
James E. Blairc053d022014-01-22 14:57:33 -08001447 if self.event_approvals:
1448 ret += ' event_approvals: %s' % ', '.join(
1449 ['%s:%s' % a for a in self.event_approvals.items()])
James E. Blair5bf78a32015-07-30 18:08:24 +00001450 if self.required_approvals:
1451 ret += ' required_approvals: %s' % ', '.join(
1452 ['%s' % a for a in self._required_approvals])
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001453 if self.reject_approvals:
1454 ret += ' reject_approvals: %s' % ', '.join(
1455 ['%s' % a for a in self._reject_approvals])
James E. Blair1fbfceb2014-06-23 14:42:53 -07001456 if self._comments:
1457 ret += ' comments: %s' % ', '.join(self._comments)
1458 if self._emails:
1459 ret += ' emails: %s' % ', '.join(self._emails)
1460 if self._usernames:
1461 ret += ' username_filters: %s' % ', '.join(self._usernames)
James E. Blair63bb0ef2013-07-29 17:14:51 -07001462 if self.timespecs:
1463 ret += ' timespecs: %s' % ', '.join(self.timespecs)
James E. Blairee743612012-05-29 14:49:32 -07001464 ret += '>'
1465
1466 return ret
1467
James E. Blairc053d022014-01-22 14:57:33 -08001468 def matches(self, event, change):
James E. Blairee743612012-05-29 14:49:32 -07001469 # event types are ORed
1470 matches_type = False
1471 for etype in self.types:
1472 if etype.match(event.type):
1473 matches_type = True
1474 if self.types and not matches_type:
1475 return False
1476
James E. Blairc494d542014-08-06 09:23:52 -07001477 # pipelines are ORed
1478 matches_pipeline = False
1479 for epipe in self.pipelines:
1480 if epipe.match(event.pipeline_name):
1481 matches_pipeline = True
1482 if self.pipelines and not matches_pipeline:
1483 return False
1484
James E. Blairee743612012-05-29 14:49:32 -07001485 # branches are ORed
1486 matches_branch = False
1487 for branch in self.branches:
1488 if branch.match(event.branch):
1489 matches_branch = True
1490 if self.branches and not matches_branch:
1491 return False
1492
1493 # refs are ORed
1494 matches_ref = False
Yolanda Robla16698872014-08-25 11:59:27 +02001495 if event.ref is not None:
1496 for ref in self.refs:
1497 if ref.match(event.ref):
1498 matches_ref = True
James E. Blairee743612012-05-29 14:49:32 -07001499 if self.refs and not matches_ref:
1500 return False
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001501 if self.ignore_deletes and event.newrev == EMPTY_GIT_REF:
1502 # If the updated ref has an empty git sha (all 0s),
1503 # then the ref is being deleted
1504 return False
James E. Blairee743612012-05-29 14:49:32 -07001505
James E. Blair1fbfceb2014-06-23 14:42:53 -07001506 # comments are ORed
1507 matches_comment_re = False
1508 for comment_re in self.comments:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001509 if (event.comment is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001510 comment_re.search(event.comment)):
1511 matches_comment_re = True
1512 if self.comments and not matches_comment_re:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001513 return False
1514
Antoine Mussob4e809e2012-12-06 16:58:06 +01001515 # We better have an account provided by Gerrit to do
1516 # email filtering.
1517 if event.account is not None:
James E. Blaircf429f32012-12-20 14:28:24 -08001518 account_email = event.account.get('email')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001519 # emails are ORed
1520 matches_email_re = False
1521 for email_re in self.emails:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001522 if (account_email is not None and
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001523 email_re.search(account_email)):
James E. Blair1fbfceb2014-06-23 14:42:53 -07001524 matches_email_re = True
1525 if self.emails and not matches_email_re:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001526 return False
1527
James E. Blair1fbfceb2014-06-23 14:42:53 -07001528 # usernames are ORed
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001529 account_username = event.account.get('username')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001530 matches_username_re = False
1531 for username_re in self.usernames:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001532 if (account_username is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001533 username_re.search(account_username)):
1534 matches_username_re = True
1535 if self.usernames and not matches_username_re:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001536 return False
1537
James E. Blairee743612012-05-29 14:49:32 -07001538 # approvals are ANDed
James E. Blairc053d022014-01-22 14:57:33 -08001539 for category, value in self.event_approvals.items():
James E. Blairee743612012-05-29 14:49:32 -07001540 matches_approval = False
1541 for eapproval in event.approvals:
1542 if (normalizeCategory(eapproval['description']) == category and
1543 int(eapproval['value']) == int(value)):
1544 matches_approval = True
James E. Blair1e8dd892012-05-30 09:15:05 -07001545 if not matches_approval:
1546 return False
James E. Blair63bb0ef2013-07-29 17:14:51 -07001547
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001548 # required approvals are ANDed (reject approvals are ORed)
1549 if not self.matchesApprovals(change):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001550 return False
James E. Blairc053d022014-01-22 14:57:33 -08001551
James E. Blair63bb0ef2013-07-29 17:14:51 -07001552 # timespecs are ORed
1553 matches_timespec = False
1554 for timespec in self.timespecs:
1555 if (event.timespec == timespec):
1556 matches_timespec = True
1557 if self.timespecs and not matches_timespec:
1558 return False
1559
James E. Blairee743612012-05-29 14:49:32 -07001560 return True
James E. Blaireff88162013-07-01 12:44:14 -04001561
1562
James E. Blair9c17dbf2014-06-23 14:21:58 -07001563class ChangeishFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07001564 """Allows a Manager to only enqueue Changes that meet certain criteria."""
Clark Boylana9702ad2014-05-08 17:17:24 -07001565 def __init__(self, open=None, current_patchset=None,
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001566 statuses=[], required_approvals=[],
1567 reject_approvals=[]):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001568 super(ChangeishFilter, self).__init__(
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001569 required_approvals=required_approvals,
1570 reject_approvals=reject_approvals)
James E. Blair11041d22014-05-02 14:49:53 -07001571 self.open = open
Clark Boylana9702ad2014-05-08 17:17:24 -07001572 self.current_patchset = current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001573 self.statuses = statuses
James E. Blair11041d22014-05-02 14:49:53 -07001574
1575 def __repr__(self):
1576 ret = '<ChangeishFilter'
1577
1578 if self.open is not None:
1579 ret += ' open: %s' % self.open
Clark Boylana9702ad2014-05-08 17:17:24 -07001580 if self.current_patchset is not None:
1581 ret += ' current-patchset: %s' % self.current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001582 if self.statuses:
1583 ret += ' statuses: %s' % ', '.join(self.statuses)
James E. Blair5bf78a32015-07-30 18:08:24 +00001584 if self.required_approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001585 ret += (' required_approvals: %s' %
1586 str(self.required_approvals))
1587 if self.reject_approvals:
1588 ret += (' reject_approvals: %s' %
1589 str(self.reject_approvals))
James E. Blair11041d22014-05-02 14:49:53 -07001590 ret += '>'
1591
1592 return ret
1593
1594 def matches(self, change):
1595 if self.open is not None:
1596 if self.open != change.open:
1597 return False
1598
Clark Boylana9702ad2014-05-08 17:17:24 -07001599 if self.current_patchset is not None:
1600 if self.current_patchset != change.is_current_patchset:
1601 return False
1602
James E. Blair11041d22014-05-02 14:49:53 -07001603 if self.statuses:
1604 if change.status not in self.statuses:
1605 return False
1606
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001607 # required approvals are ANDed (reject approvals are ORed)
1608 if not self.matchesApprovals(change):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001609 return False
James E. Blair11041d22014-05-02 14:49:53 -07001610
1611 return True
1612
1613
James E. Blairb97ed802015-12-21 15:55:35 -08001614class ProjectPipelineConfig(object):
1615 # Represents a project cofiguration in the context of a pipeline
1616 def __init__(self):
1617 self.job_tree = None
1618 self.queue_name = None
Adam Gandelman8bd57102016-12-02 12:58:42 -08001619 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08001620
1621
1622class ProjectConfig(object):
1623 # Represents a project cofiguration
1624 def __init__(self, name):
1625 self.name = name
Adam Gandelman8bd57102016-12-02 12:58:42 -08001626 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08001627 self.pipelines = {}
1628
1629
James E. Blaird8e778f2015-12-22 14:09:20 -08001630class UnparsedAbideConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001631 """A collection of yaml lists that has not yet been parsed into objects.
1632
1633 An Abide is a collection of tenants.
1634 """
1635
James E. Blaird8e778f2015-12-22 14:09:20 -08001636 def __init__(self):
1637 self.tenants = []
1638
1639 def extend(self, conf):
1640 if isinstance(conf, UnparsedAbideConfig):
1641 self.tenants.extend(conf.tenants)
1642 return
1643
1644 if not isinstance(conf, list):
1645 raise Exception("Configuration items must be in the form of "
1646 "a list of dictionaries (when parsing %s)" %
1647 (conf,))
1648 for item in conf:
1649 if not isinstance(item, dict):
1650 raise Exception("Configuration items must be in the form of "
1651 "a list of dictionaries (when parsing %s)" %
1652 (conf,))
1653 if len(item.keys()) > 1:
1654 raise Exception("Configuration item dictionaries must have "
1655 "a single key (when parsing %s)" %
1656 (conf,))
1657 key, value = item.items()[0]
1658 if key == 'tenant':
1659 self.tenants.append(value)
1660 else:
1661 raise Exception("Configuration item not recognized "
1662 "(when parsing %s)" %
1663 (conf,))
1664
1665
1666class UnparsedTenantConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001667 """A collection of yaml lists that has not yet been parsed into objects."""
1668
James E. Blaird8e778f2015-12-22 14:09:20 -08001669 def __init__(self):
1670 self.pipelines = []
1671 self.jobs = []
1672 self.project_templates = []
1673 self.projects = []
James E. Blaira98340f2016-09-02 11:33:49 -07001674 self.nodesets = []
James E. Blaird8e778f2015-12-22 14:09:20 -08001675
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001676 def copy(self):
1677 r = UnparsedTenantConfig()
1678 r.pipelines = copy.deepcopy(self.pipelines)
1679 r.jobs = copy.deepcopy(self.jobs)
1680 r.project_templates = copy.deepcopy(self.project_templates)
1681 r.projects = copy.deepcopy(self.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07001682 r.nodesets = copy.deepcopy(self.nodesets)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001683 return r
1684
James E. Blaire208c482016-10-04 14:35:30 -07001685 def extend(self, conf, source_project=None, source_branch=None):
James E. Blaird8e778f2015-12-22 14:09:20 -08001686 if isinstance(conf, UnparsedTenantConfig):
1687 self.pipelines.extend(conf.pipelines)
1688 self.jobs.extend(conf.jobs)
1689 self.project_templates.extend(conf.project_templates)
1690 self.projects.extend(conf.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07001691 self.nodesets.extend(conf.nodesets)
James E. Blaird8e778f2015-12-22 14:09:20 -08001692 return
1693
1694 if not isinstance(conf, list):
1695 raise Exception("Configuration items must be in the form of "
1696 "a list of dictionaries (when parsing %s)" %
1697 (conf,))
1698 for item in conf:
1699 if not isinstance(item, dict):
1700 raise Exception("Configuration items must be in the form of "
1701 "a list of dictionaries (when parsing %s)" %
1702 (conf,))
1703 if len(item.keys()) > 1:
1704 raise Exception("Configuration item dictionaries must have "
1705 "a single key (when parsing %s)" %
1706 (conf,))
1707 key, value = item.items()[0]
1708 if key == 'project':
1709 self.projects.append(value)
1710 elif key == 'job':
James E. Blair4317e9f2016-07-15 10:05:47 -07001711 if source_project is not None:
1712 value['_source_project'] = source_project
James E. Blaire208c482016-10-04 14:35:30 -07001713 if source_branch is not None:
1714 value['_source_branch'] = source_branch
James E. Blaird8e778f2015-12-22 14:09:20 -08001715 self.jobs.append(value)
1716 elif key == 'project-template':
1717 self.project_templates.append(value)
1718 elif key == 'pipeline':
1719 self.pipelines.append(value)
James E. Blaira98340f2016-09-02 11:33:49 -07001720 elif key == 'nodeset':
1721 self.nodesets.append(value)
James E. Blaird8e778f2015-12-22 14:09:20 -08001722 else:
Morgan Fainberg4245a422016-08-05 16:20:12 -07001723 raise Exception("Configuration item `%s` not recognized "
James E. Blaird8e778f2015-12-22 14:09:20 -08001724 "(when parsing %s)" %
Morgan Fainberg4245a422016-08-05 16:20:12 -07001725 (item, conf,))
James E. Blaird8e778f2015-12-22 14:09:20 -08001726
1727
James E. Blaireff88162013-07-01 12:44:14 -04001728class Layout(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001729 """Holds all of the Pipelines."""
1730
James E. Blaireff88162013-07-01 12:44:14 -04001731 def __init__(self):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001732 self.tenant = None
James E. Blaireff88162013-07-01 12:44:14 -04001733 self.projects = {}
James E. Blairb97ed802015-12-21 15:55:35 -08001734 self.project_configs = {}
1735 self.project_templates = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07001736 self.pipelines = OrderedDict()
James E. Blair83005782015-12-11 14:46:03 -08001737 # This is a dictionary of name -> [jobs]. The first element
1738 # of the list is the first job added with that name. It is
1739 # the reference definition for a given job. Subsequent
1740 # elements are aspects of that job with different matchers
1741 # that override some attribute of the job. These aspects all
1742 # inherit from the reference definition.
James E. Blairfef88ec2016-11-30 08:38:27 -08001743 self.jobs = {'noop': [Job('noop')]}
James E. Blaira98340f2016-09-02 11:33:49 -07001744 self.nodesets = {}
James E. Blaireff88162013-07-01 12:44:14 -04001745
1746 def getJob(self, name):
1747 if name in self.jobs:
James E. Blair83005782015-12-11 14:46:03 -08001748 return self.jobs[name][0]
James E. Blairb97ed802015-12-21 15:55:35 -08001749 raise Exception("Job %s not defined" % (name,))
James E. Blair83005782015-12-11 14:46:03 -08001750
1751 def getJobs(self, name):
1752 return self.jobs.get(name, [])
1753
1754 def addJob(self, job):
James E. Blair4317e9f2016-07-15 10:05:47 -07001755 # We can have multiple variants of a job all with the same
1756 # name, but these variants must all be defined in the same repo.
1757 prior_jobs = [j for j in self.getJobs(job.name)
1758 if j.source_project != job.source_project]
1759 if prior_jobs:
1760 raise Exception("Job %s in %s is not permitted to shadow "
1761 "job %s in %s" % (job, job.source_project,
1762 prior_jobs[0],
1763 prior_jobs[0].source_project))
1764
James E. Blair83005782015-12-11 14:46:03 -08001765 if job.name in self.jobs:
1766 self.jobs[job.name].append(job)
1767 else:
1768 self.jobs[job.name] = [job]
1769
James E. Blaira98340f2016-09-02 11:33:49 -07001770 def addNodeSet(self, nodeset):
1771 if nodeset.name in self.nodesets:
1772 raise Exception("NodeSet %s already defined" % (nodeset.name,))
1773 self.nodesets[nodeset.name] = nodeset
1774
James E. Blair83005782015-12-11 14:46:03 -08001775 def addPipeline(self, pipeline):
1776 self.pipelines[pipeline.name] = pipeline
James E. Blair59fdbac2015-12-07 17:08:06 -08001777
James E. Blairb97ed802015-12-21 15:55:35 -08001778 def addProjectTemplate(self, project_template):
1779 self.project_templates[project_template.name] = project_template
1780
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001781 def addProjectConfig(self, project_config, update_pipeline=True):
James E. Blairb97ed802015-12-21 15:55:35 -08001782 self.project_configs[project_config.name] = project_config
1783 # TODOv3(jeblair): tidy up the relationship between pipelines
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001784 # and projects and projectconfigs. Specifically, move
1785 # job_trees out of the pipeline since they are more dynamic
1786 # than pipelines. Remove the update_pipeline argument
1787 if not update_pipeline:
1788 return
James E. Blairb97ed802015-12-21 15:55:35 -08001789 for pipeline_name, pipeline_config in project_config.pipelines.items():
1790 pipeline = self.pipelines[pipeline_name]
1791 project = pipeline.source.getProject(project_config.name)
1792 pipeline.job_trees[project] = pipeline_config.job_tree
1793
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001794 def _createJobTree(self, change, job_trees, parent):
1795 for tree in job_trees:
1796 job = tree.job
1797 if not job.changeMatches(change):
1798 continue
1799 frozen_job = Job(job.name)
1800 frozen_tree = JobTree(frozen_job)
1801 inherited = set()
1802 for variant in self.getJobs(job.name):
1803 if variant.changeMatches(change):
1804 if variant not in inherited:
1805 frozen_job.inheritFrom(variant)
1806 inherited.add(variant)
James E. Blair6e85c2b2016-11-21 16:47:01 -08001807 if not inherited:
1808 # A change must match at least one defined job variant
1809 # (that is to say that it must match more than just
1810 # the job that is defined in the tree).
1811 continue
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001812 if job not in inherited:
1813 # Only update from the job in the tree if it is
1814 # unique, otherwise we might unset an attribute we
1815 # have overloaded.
1816 frozen_job.inheritFrom(job)
1817 parent.job_trees.append(frozen_tree)
1818 self._createJobTree(change, tree.job_trees, frozen_tree)
1819
1820 def createJobTree(self, item):
Paul Belanger160cb8e2016-11-11 19:04:24 -05001821 project_config = self.project_configs.get(
1822 item.change.project.name, None)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001823 ret = JobTree(None)
Paul Belanger15e3e202016-10-14 16:27:34 -04001824 # NOTE(pabelanger): It is possible for a foreign project not to have a
1825 # configured pipeline, if so return an empty JobTree.
Paul Belanger160cb8e2016-11-11 19:04:24 -05001826 if project_config and item.pipeline.name in project_config.pipelines:
Paul Belanger15e3e202016-10-14 16:27:34 -04001827 project_tree = \
1828 project_config.pipelines[item.pipeline.name].job_tree
1829 self._createJobTree(item.change, project_tree.job_trees, ret)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001830 return ret
1831
James E. Blair59fdbac2015-12-07 17:08:06 -08001832
1833class Tenant(object):
1834 def __init__(self, name):
1835 self.name = name
1836 self.layout = None
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001837 # The list of repos from which we will read main
1838 # configuration. (source, project)
1839 self.config_repos = []
1840 # The unparsed config from those repos.
1841 self.config_repos_config = None
1842 # The list of projects from which we will read in-repo
1843 # configuration. (source, project)
1844 self.project_repos = []
1845 # The unparsed config from those repos.
1846 self.project_repos_config = None
James E. Blair59fdbac2015-12-07 17:08:06 -08001847
1848
1849class Abide(object):
1850 def __init__(self):
1851 self.tenants = OrderedDict()
James E. Blairce8a2132016-05-19 15:21:52 -07001852
1853
1854class JobTimeData(object):
1855 format = 'B10H10H10B'
1856 version = 0
1857
1858 def __init__(self, path):
1859 self.path = path
1860 self.success_times = [0 for x in range(10)]
1861 self.failure_times = [0 for x in range(10)]
1862 self.results = [0 for x in range(10)]
1863
1864 def load(self):
1865 if not os.path.exists(self.path):
1866 return
1867 with open(self.path) as f:
1868 data = struct.unpack(self.format, f.read())
1869 version = data[0]
1870 if version != self.version:
1871 raise Exception("Unkown data version")
1872 self.success_times = list(data[1:11])
1873 self.failure_times = list(data[11:21])
1874 self.results = list(data[21:32])
1875
1876 def save(self):
1877 tmpfile = self.path + '.tmp'
1878 data = [self.version]
1879 data.extend(self.success_times)
1880 data.extend(self.failure_times)
1881 data.extend(self.results)
1882 data = struct.pack(self.format, *data)
1883 with open(tmpfile, 'w') as f:
1884 f.write(data)
1885 os.rename(tmpfile, self.path)
1886
1887 def add(self, elapsed, result):
1888 elapsed = int(elapsed)
1889 if result == 'SUCCESS':
1890 self.success_times.append(elapsed)
1891 self.success_times.pop(0)
1892 result = 0
1893 else:
1894 self.failure_times.append(elapsed)
1895 self.failure_times.pop(0)
1896 result = 1
1897 self.results.append(result)
1898 self.results.pop(0)
1899
1900 def getEstimatedTime(self):
1901 times = [x for x in self.success_times if x]
1902 if times:
1903 return float(sum(times)) / len(times)
1904 return 0.0
1905
1906
1907class TimeDataBase(object):
1908 def __init__(self, root):
1909 self.root = root
1910 self.jobs = {}
1911
1912 def _getTD(self, name):
1913 td = self.jobs.get(name)
1914 if not td:
1915 td = JobTimeData(os.path.join(self.root, name))
1916 self.jobs[name] = td
1917 td.load()
1918 return td
1919
1920 def getEstimatedTime(self, name):
1921 return self._getTD(name).getEstimatedTime()
1922
1923 def update(self, name, elapsed, result):
1924 td = self._getTD(name)
1925 td.add(elapsed, result)
1926 td.save()