blob: ae8ec176ed7a3bc05c0be9ec0811bd5128e78591 [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. Blair803e94f2017-01-06 09:18:59 -080050# Request states
51STATE_REQUESTED = 'requested'
52STATE_PENDING = 'pending'
53STATE_FULFILLED = 'fulfilled'
54STATE_FAILED = 'failed'
55REQUEST_STATES = set([STATE_REQUESTED,
56 STATE_PENDING,
57 STATE_FULFILLED,
58 STATE_FAILED])
59
60# Node states
61STATE_BUILDING = 'building'
62STATE_TESTING = 'testing'
63STATE_READY = 'ready'
64STATE_IN_USE = 'in-use'
65STATE_USED = 'used'
66STATE_HOLD = 'hold'
67STATE_DELETING = 'deleting'
68NODE_STATES = set([STATE_BUILDING,
69 STATE_TESTING,
70 STATE_READY,
71 STATE_IN_USE,
72 STATE_USED,
73 STATE_HOLD,
74 STATE_DELETING])
75
James E. Blair1e8dd892012-05-30 09:15:05 -070076
James E. Blairc053d022014-01-22 14:57:33 -080077def time_to_seconds(s):
78 if s.endswith('s'):
79 return int(s[:-1])
80 if s.endswith('m'):
81 return int(s[:-1]) * 60
82 if s.endswith('h'):
83 return int(s[:-1]) * 60 * 60
84 if s.endswith('d'):
85 return int(s[:-1]) * 24 * 60 * 60
86 if s.endswith('w'):
87 return int(s[:-1]) * 7 * 24 * 60 * 60
88 raise Exception("Unable to parse time value: %s" % s)
89
90
James E. Blair11041d22014-05-02 14:49:53 -070091def normalizeCategory(name):
92 name = name.lower()
93 return re.sub(' ', '-', name)
94
95
James E. Blair4aea70c2012-07-26 14:23:24 -070096class Pipeline(object):
Monty Taylora42a55b2016-07-29 07:53:33 -070097 """A configuration that ties triggers, reporters, managers and sources.
98
Monty Taylor82dfd412016-07-29 12:01:28 -070099 Source
100 Where changes should come from. It is a named connection to
Monty Taylora42a55b2016-07-29 07:53:33 -0700101 an external service defined in zuul.conf
Monty Taylor82dfd412016-07-29 12:01:28 -0700102
103 Trigger
104 A description of which events should be processed
105
106 Manager
107 Responsible for enqueing and dequeing Changes
108
109 Reporter
110 Communicates success and failure results somewhere
Monty Taylora42a55b2016-07-29 07:53:33 -0700111 """
James E. Blair83005782015-12-11 14:46:03 -0800112 def __init__(self, name, layout):
James E. Blair4aea70c2012-07-26 14:23:24 -0700113 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800114 self.layout = layout
James E. Blair8dbd56a2012-12-22 10:55:10 -0800115 self.description = None
James E. Blair56370192013-01-14 15:47:28 -0800116 self.failure_message = None
Joshua Heskethb7179772014-01-30 23:30:46 +1100117 self.merge_failure_message = None
James E. Blair56370192013-01-14 15:47:28 -0800118 self.success_message = None
Joshua Hesketh3979e3e2014-03-04 11:21:10 +1100119 self.footer_message = None
James E. Blair60af7f42016-03-11 16:11:06 -0800120 self.start_message = None
James E. Blair2fa50962013-01-30 21:50:41 -0800121 self.dequeue_on_new_patchset = True
James E. Blair17dd6772015-02-09 14:45:18 -0800122 self.ignore_dependencies = False
James E. Blair4aea70c2012-07-26 14:23:24 -0700123 self.job_trees = {} # project -> JobTree
124 self.manager = None
James E. Blaire0487072012-08-29 17:38:31 -0700125 self.queues = []
James E. Blair64ed6f22013-07-10 14:07:23 -0700126 self.precedence = PRECEDENCE_NORMAL
James E. Blairc0dedf82014-08-06 09:37:52 -0700127 self.source = None
James E. Blair83005782015-12-11 14:46:03 -0800128 self.triggers = []
Joshua Hesketh352264b2015-08-11 23:42:08 +1000129 self.start_actions = []
130 self.success_actions = []
131 self.failure_actions = []
132 self.merge_failure_actions = []
133 self.disabled_actions = []
Joshua Hesketh89e829d2015-02-10 16:29:45 +1100134 self.disable_at = None
135 self._consecutive_failures = 0
136 self._disabled = False
Clark Boylan7603a372014-01-21 11:43:20 -0800137 self.window = None
138 self.window_floor = None
139 self.window_increase_type = None
140 self.window_increase_factor = None
141 self.window_decrease_type = None
142 self.window_decrease_factor = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700143
James E. Blair83005782015-12-11 14:46:03 -0800144 @property
145 def actions(self):
146 return (
147 self.start_actions +
148 self.success_actions +
149 self.failure_actions +
150 self.merge_failure_actions +
151 self.disabled_actions
152 )
153
James E. Blaird09c17a2012-08-07 09:23:14 -0700154 def __repr__(self):
155 return '<Pipeline %s>' % self.name
156
James E. Blair4aea70c2012-07-26 14:23:24 -0700157 def setManager(self, manager):
158 self.manager = manager
159
James E. Blair4aea70c2012-07-26 14:23:24 -0700160 def getProjects(self):
Monty Taylor74fa3862016-06-02 07:39:49 +0300161 # cmp is not in python3, applied idiom from
162 # http://python-future.org/compatible_idioms.html#cmp
163 return sorted(
164 self.job_trees.keys(),
165 key=lambda p: p.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700166
James E. Blaire0487072012-08-29 17:38:31 -0700167 def addQueue(self, queue):
168 self.queues.append(queue)
169
170 def getQueue(self, project):
171 for queue in self.queues:
172 if project in queue.projects:
173 return queue
174 return None
175
James E. Blairbfb8e042014-12-30 17:01:44 -0800176 def removeQueue(self, queue):
177 self.queues.remove(queue)
178
James E. Blair4aea70c2012-07-26 14:23:24 -0700179 def getJobTree(self, project):
180 tree = self.job_trees.get(project)
181 return tree
182
James E. Blaire0487072012-08-29 17:38:31 -0700183 def getChangesInQueue(self):
184 changes = []
185 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700186 changes.extend([x.change for x in shared_queue.queue])
James E. Blaire0487072012-08-29 17:38:31 -0700187 return changes
188
James E. Blairfee8d652013-06-07 08:57:52 -0700189 def getAllItems(self):
190 items = []
James E. Blaire0487072012-08-29 17:38:31 -0700191 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700192 items.extend(shared_queue.queue)
James E. Blairfee8d652013-06-07 08:57:52 -0700193 return items
James E. Blaire0487072012-08-29 17:38:31 -0700194
James E. Blairb7273ef2016-04-19 08:58:51 -0700195 def formatStatusJSON(self, url_pattern=None):
James E. Blair8dbd56a2012-12-22 10:55:10 -0800196 j_pipeline = dict(name=self.name,
197 description=self.description)
198 j_queues = []
199 j_pipeline['change_queues'] = j_queues
200 for queue in self.queues:
201 j_queue = dict(name=queue.name)
202 j_queues.append(j_queue)
203 j_queue['heads'] = []
Clark Boylanaf2476f2014-01-23 14:47:36 -0800204 j_queue['window'] = queue.window
James E. Blair972e3c72013-08-29 12:04:55 -0700205
206 j_changes = []
207 for e in queue.queue:
208 if not e.item_ahead:
209 if j_changes:
210 j_queue['heads'].append(j_changes)
211 j_changes = []
James E. Blairb7273ef2016-04-19 08:58:51 -0700212 j_changes.append(e.formatJSON(url_pattern))
James E. Blair972e3c72013-08-29 12:04:55 -0700213 if (len(j_changes) > 1 and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000214 (j_changes[-2]['remaining_time'] is not None) and
215 (j_changes[-1]['remaining_time'] is not None)):
James E. Blair972e3c72013-08-29 12:04:55 -0700216 j_changes[-1]['remaining_time'] = max(
217 j_changes[-2]['remaining_time'],
218 j_changes[-1]['remaining_time'])
219 if j_changes:
James E. Blair8dbd56a2012-12-22 10:55:10 -0800220 j_queue['heads'].append(j_changes)
221 return j_pipeline
222
James E. Blair4aea70c2012-07-26 14:23:24 -0700223
James E. Blairee743612012-05-29 14:49:32 -0700224class ChangeQueue(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700225 """A ChangeQueue contains Changes to be processed related projects.
226
Monty Taylor82dfd412016-07-29 12:01:28 -0700227 A Pipeline with a DependentPipelineManager has multiple parallel
228 ChangeQueues shared by different projects. For instance, there may a
229 ChangeQueue shared by interrelated projects foo and bar, and a second queue
230 for independent project baz.
Monty Taylora42a55b2016-07-29 07:53:33 -0700231
Monty Taylor82dfd412016-07-29 12:01:28 -0700232 A Pipeline with an IndependentPipelineManager puts every Change into its
233 own ChangeQueue
Monty Taylora42a55b2016-07-29 07:53:33 -0700234
235 The ChangeQueue Window is inspired by TCP windows and controlls how many
236 Changes in a given ChangeQueue will be considered active and ready to
237 be processed. If a Change succeeds, the Window is increased by
238 `window_increase_factor`. If a Change fails, the Window is decreased by
239 `window_decrease_factor`.
240 """
James E. Blairbfb8e042014-12-30 17:01:44 -0800241 def __init__(self, pipeline, window=0, window_floor=1,
Clark Boylan7603a372014-01-21 11:43:20 -0800242 window_increase_type='linear', window_increase_factor=1,
James E. Blair0dcef7a2016-08-19 09:35:17 -0700243 window_decrease_type='exponential', window_decrease_factor=2,
244 name=None):
James E. Blair4aea70c2012-07-26 14:23:24 -0700245 self.pipeline = pipeline
James E. Blair0dcef7a2016-08-19 09:35:17 -0700246 if name:
247 self.name = name
248 else:
249 self.name = ''
James E. Blairee743612012-05-29 14:49:32 -0700250 self.projects = []
251 self._jobs = set()
252 self.queue = []
Clark Boylan7603a372014-01-21 11:43:20 -0800253 self.window = window
254 self.window_floor = window_floor
255 self.window_increase_type = window_increase_type
256 self.window_increase_factor = window_increase_factor
257 self.window_decrease_type = window_decrease_type
258 self.window_decrease_factor = window_decrease_factor
James E. Blairee743612012-05-29 14:49:32 -0700259
James E. Blair9f9667e2012-06-12 17:51:08 -0700260 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700261 return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
James E. Blairee743612012-05-29 14:49:32 -0700262
263 def getJobs(self):
264 return self._jobs
265
266 def addProject(self, project):
267 if project not in self.projects:
268 self.projects.append(project)
James E. Blairc8a1e052014-02-25 09:29:26 -0800269
James E. Blair0dcef7a2016-08-19 09:35:17 -0700270 if not self.name:
271 self.name = project.name
James E. Blairee743612012-05-29 14:49:32 -0700272
273 def enqueueChange(self, change):
James E. Blairbfb8e042014-12-30 17:01:44 -0800274 item = QueueItem(self, change)
James E. Blaircdccd972013-07-01 12:10:22 -0700275 self.enqueueItem(item)
276 item.enqueue_time = time.time()
277 return item
278
279 def enqueueItem(self, item):
James E. Blair4a035d92014-01-23 13:10:48 -0800280 item.pipeline = self.pipeline
James E. Blairbfb8e042014-12-30 17:01:44 -0800281 item.queue = self
282 if self.queue:
James E. Blairfee8d652013-06-07 08:57:52 -0700283 item.item_ahead = self.queue[-1]
James E. Blair972e3c72013-08-29 12:04:55 -0700284 item.item_ahead.items_behind.append(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700285 self.queue.append(item)
James E. Blairee743612012-05-29 14:49:32 -0700286
James E. Blairfee8d652013-06-07 08:57:52 -0700287 def dequeueItem(self, item):
288 if item in self.queue:
289 self.queue.remove(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700290 if item.item_ahead:
James E. Blair972e3c72013-08-29 12:04:55 -0700291 item.item_ahead.items_behind.remove(item)
292 for item_behind in item.items_behind:
293 if item.item_ahead:
294 item.item_ahead.items_behind.append(item_behind)
295 item_behind.item_ahead = item.item_ahead
James E. Blairfee8d652013-06-07 08:57:52 -0700296 item.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700297 item.items_behind = []
James E. Blairfee8d652013-06-07 08:57:52 -0700298 item.dequeue_time = time.time()
James E. Blaire0487072012-08-29 17:38:31 -0700299
James E. Blair972e3c72013-08-29 12:04:55 -0700300 def moveItem(self, item, item_ahead):
James E. Blair972e3c72013-08-29 12:04:55 -0700301 if item.item_ahead == item_ahead:
302 return False
303 # Remove from current location
304 if item.item_ahead:
305 item.item_ahead.items_behind.remove(item)
306 for item_behind in item.items_behind:
307 if item.item_ahead:
308 item.item_ahead.items_behind.append(item_behind)
309 item_behind.item_ahead = item.item_ahead
310 # Add to new location
311 item.item_ahead = item_ahead
James E. Blair00451262013-09-20 11:40:17 -0700312 item.items_behind = []
James E. Blair972e3c72013-08-29 12:04:55 -0700313 if item.item_ahead:
314 item.item_ahead.items_behind.append(item)
315 return True
James E. Blairee743612012-05-29 14:49:32 -0700316
317 def mergeChangeQueue(self, other):
318 for project in other.projects:
319 self.addProject(project)
Clark Boylan7603a372014-01-21 11:43:20 -0800320 self.window = min(self.window, other.window)
321 # TODO merge semantics
322
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800323 def isActionable(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800324 if self.window:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800325 return item in self.queue[:self.window]
Clark Boylan7603a372014-01-21 11:43:20 -0800326 else:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800327 return True
Clark Boylan7603a372014-01-21 11:43:20 -0800328
329 def increaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800330 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800331 if self.window_increase_type == 'linear':
332 self.window += self.window_increase_factor
333 elif self.window_increase_type == 'exponential':
334 self.window *= self.window_increase_factor
335
336 def decreaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800337 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800338 if self.window_decrease_type == 'linear':
339 self.window = max(
340 self.window_floor,
341 self.window - self.window_decrease_factor)
342 elif self.window_decrease_type == 'exponential':
343 self.window = max(
344 self.window_floor,
Morgan Fainbergfdae2252016-06-07 20:13:20 -0700345 int(self.window / self.window_decrease_factor))
James E. Blairee743612012-05-29 14:49:32 -0700346
James E. Blair1e8dd892012-05-30 09:15:05 -0700347
James E. Blair4aea70c2012-07-26 14:23:24 -0700348class Project(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700349 """A Project represents a git repository such as openstack/nova."""
350
James E. Blaircf440a22016-07-15 09:11:58 -0700351 # NOTE: Projects should only be instantiated via a Source object
352 # so that they are associated with and cached by their Connection.
353 # This makes a Project instance a unique identifier for a given
354 # project from a given source.
355
James E. Blairc73c73a2017-01-20 15:15:15 -0800356 def __init__(self, name, connection_name, foreign=False):
James E. Blair4aea70c2012-07-26 14:23:24 -0700357 self.name = name
James E. Blairc73c73a2017-01-20 15:15:15 -0800358 self.connection_name = connection_name
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000359 # foreign projects are those referenced in dependencies
360 # of layout projects, this should matter
361 # when deciding whether to enqueue their changes
James E. Blaircf440a22016-07-15 09:11:58 -0700362 # TODOv3 (jeblair): re-add support for foreign projects if needed
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000363 self.foreign = foreign
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700364 self.unparsed_config = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700365
366 def __str__(self):
367 return self.name
368
369 def __repr__(self):
370 return '<Project %s>' % (self.name)
371
372
James E. Blair34776ee2016-08-25 13:53:54 -0700373class Node(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700374 """A single node for use by a job.
375
376 This may represent a request for a node, or an actual node
377 provided by Nodepool.
378 """
379
James E. Blair34776ee2016-08-25 13:53:54 -0700380 def __init__(self, name, image):
381 self.name = name
382 self.image = image
James E. Blaircbf43672017-01-04 14:33:41 -0800383 self.id = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800384 self.lock = None
385 # Attributes from Nodepool
386 self._state = 'unknown'
387 self.state_time = time.time()
388 self.public_ipv4 = None
389 self.private_ipv4 = None
390 self.public_ipv6 = None
James E. Blaircacdf2b2017-01-04 13:14:37 -0800391 self._keys = []
James E. Blaira38c28e2017-01-04 10:33:20 -0800392
393 @property
394 def state(self):
395 return self._state
396
397 @state.setter
398 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800399 if value not in NODE_STATES:
400 raise TypeError("'%s' is not a valid state" % value)
James E. Blaira38c28e2017-01-04 10:33:20 -0800401 self._state = value
402 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700403
404 def __repr__(self):
James E. Blaircbf43672017-01-04 14:33:41 -0800405 return '<Node %s %s:%s>' % (self.id, self.name, self.image)
James E. Blair34776ee2016-08-25 13:53:54 -0700406
James E. Blair0d952152017-02-07 17:14:44 -0800407 def __ne__(self, other):
408 return not self.__eq__(other)
409
410 def __eq__(self, other):
411 if not isinstance(other, Node):
412 return False
413 return (self.name == other.name and
414 self.image == other.image and
415 self.id == other.id)
416
James E. Blaircacdf2b2017-01-04 13:14:37 -0800417 def toDict(self):
418 d = {}
419 d['state'] = self.state
420 for k in self._keys:
421 d[k] = getattr(self, k)
422 return d
423
James E. Blaira38c28e2017-01-04 10:33:20 -0800424 def updateFromDict(self, data):
425 self._state = data['state']
James E. Blaircacdf2b2017-01-04 13:14:37 -0800426 keys = []
427 for k, v in data.items():
428 if k == 'state':
429 continue
430 keys.append(k)
431 setattr(self, k, v)
432 self._keys = keys
James E. Blaira38c28e2017-01-04 10:33:20 -0800433
James E. Blair34776ee2016-08-25 13:53:54 -0700434
James E. Blaira98340f2016-09-02 11:33:49 -0700435class NodeSet(object):
436 """A set of nodes.
437
438 In configuration, NodeSets are attributes of Jobs indicating that
439 a Job requires nodes matching this description.
440
441 They may appear as top-level configuration objects and be named,
442 or they may appears anonymously in in-line job definitions.
443 """
444
445 def __init__(self, name=None):
446 self.name = name or ''
447 self.nodes = OrderedDict()
448
James E. Blair1774dd52017-02-03 10:52:32 -0800449 def __ne__(self, other):
450 return not self.__eq__(other)
451
452 def __eq__(self, other):
453 if not isinstance(other, NodeSet):
454 return False
455 return (self.name == other.name and
456 self.nodes == other.nodes)
457
James E. Blaircbf43672017-01-04 14:33:41 -0800458 def copy(self):
459 n = NodeSet(self.name)
460 for name, node in self.nodes.items():
461 n.addNode(Node(node.name, node.image))
462 return n
463
James E. Blaira98340f2016-09-02 11:33:49 -0700464 def addNode(self, node):
465 if node.name in self.nodes:
466 raise Exception("Duplicate node in %s" % (self,))
467 self.nodes[node.name] = node
468
James E. Blair0eaad552016-09-02 12:09:54 -0700469 def getNodes(self):
470 return self.nodes.values()
471
James E. Blaira98340f2016-09-02 11:33:49 -0700472 def __repr__(self):
473 if self.name:
474 name = self.name + ' '
475 else:
476 name = ''
477 return '<NodeSet %s%s>' % (name, self.nodes)
478
479
James E. Blair34776ee2016-08-25 13:53:54 -0700480class NodeRequest(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700481 """A request for a set of nodes."""
482
James E. Blair0eaad552016-09-02 12:09:54 -0700483 def __init__(self, build_set, job, nodeset):
James E. Blair34776ee2016-08-25 13:53:54 -0700484 self.build_set = build_set
485 self.job = job
James E. Blair0eaad552016-09-02 12:09:54 -0700486 self.nodeset = nodeset
James E. Blair803e94f2017-01-06 09:18:59 -0800487 self._state = STATE_REQUESTED
James E. Blairdce6cea2016-12-20 16:45:32 -0800488 self.state_time = time.time()
489 self.stat = None
490 self.uid = uuid4().hex
James E. Blaircbf43672017-01-04 14:33:41 -0800491 self.id = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800492 # Zuul internal failure flag (not stored in ZK so it's not
493 # overwritten).
494 self.failed = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800495
496 @property
James E. Blair6ab79e02017-01-06 10:10:17 -0800497 def fulfilled(self):
498 return (self._state == STATE_FULFILLED) and not self.failed
499
500 @property
James E. Blairdce6cea2016-12-20 16:45:32 -0800501 def state(self):
502 return self._state
503
504 @state.setter
505 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800506 if value not in REQUEST_STATES:
507 raise TypeError("'%s' is not a valid state" % value)
James E. Blairdce6cea2016-12-20 16:45:32 -0800508 self._state = value
509 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700510
511 def __repr__(self):
James E. Blaircbf43672017-01-04 14:33:41 -0800512 return '<NodeRequest %s %s>' % (self.id, self.nodeset)
James E. Blair34776ee2016-08-25 13:53:54 -0700513
James E. Blairdce6cea2016-12-20 16:45:32 -0800514 def toDict(self):
515 d = {}
516 nodes = [n.image for n in self.nodeset.getNodes()]
517 d['node_types'] = nodes
518 d['requestor'] = 'zuul' # TODOv3(jeblair): better descriptor
519 d['state'] = self.state
520 d['state_time'] = self.state_time
521 return d
522
523 def updateFromDict(self, data):
524 self._state = data['state']
525 self.state_time = data['state_time']
526
James E. Blair34776ee2016-08-25 13:53:54 -0700527
James E. Blaircdab2032017-02-01 09:09:29 -0800528class SourceContext(object):
529 """A reference to the branch of a project in configuration.
530
531 Jobs and playbooks reference this to keep track of where they
532 originate."""
533
534 def __init__(self, project, branch, secure):
535 self.project = project
536 self.branch = branch
537 self.secure = secure
538
539 def __repr__(self):
540 return '<SourceContext %s:%s secure:%s>' % (self.project,
541 self.branch,
542 self.secure)
543
544 def __ne__(self, other):
545 return not self.__eq__(other)
546
547 def __eq__(self, other):
548 if not isinstance(other, SourceContext):
549 return False
550 return (self.project == other.project and
551 self.branch == other.branch and
552 self.secure == other.secure)
553
554
James E. Blair66b274e2017-01-31 14:47:52 -0800555class PlaybookContext(object):
James E. Blaircdab2032017-02-01 09:09:29 -0800556
James E. Blair66b274e2017-01-31 14:47:52 -0800557 """A reference to a playbook in the context of a project.
558
559 Jobs refer to objects of this class for their main, pre, and post
560 playbooks so that we can keep track of which repos and security
561 contexts are needed in order to run them."""
562
James E. Blaircdab2032017-02-01 09:09:29 -0800563 def __init__(self, source_context, path):
564 self.source_context = source_context
James E. Blair66b274e2017-01-31 14:47:52 -0800565 self.path = path
James E. Blair66b274e2017-01-31 14:47:52 -0800566
567 def __repr__(self):
James E. Blaircdab2032017-02-01 09:09:29 -0800568 return '<PlaybookContext %s %s>' % (self.source_context,
569 self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800570
571 def __ne__(self, other):
572 return not self.__eq__(other)
573
574 def __eq__(self, other):
575 if not isinstance(other, PlaybookContext):
576 return False
James E. Blaircdab2032017-02-01 09:09:29 -0800577 return (self.source_context == other.source_context and
578 self.path == other.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800579
580 def toDict(self):
581 # Render to a dict to use in passing json to the launcher
582 return dict(
James E. Blaircdab2032017-02-01 09:09:29 -0800583 connection=self.source_context.project.connection_name,
584 project=self.source_context.project.name,
585 branch=self.source_context.branch,
586 secure=self.source_context.secure,
587 path=self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800588
589
James E. Blairee743612012-05-29 14:49:32 -0700590class Job(object):
James E. Blair66b274e2017-01-31 14:47:52 -0800591
Monty Taylora42a55b2016-07-29 07:53:33 -0700592 """A Job represents the defintion of actions to perform."""
593
James E. Blairee743612012-05-29 14:49:32 -0700594 def __init__(self, name):
James E. Blair1774dd52017-02-03 10:52:32 -0800595 self.attributes = dict(
596 timeout=None,
597 # variables={},
598 nodeset=NodeSet(),
599 auth={},
600 workspace=None,
601 pre_run=[],
602 post_run=[],
James E. Blair66b274e2017-01-31 14:47:52 -0800603 run=None,
James E. Blair1774dd52017-02-03 10:52:32 -0800604 voting=None,
605 hold_following_changes=None,
606 failure_message=None,
607 success_message=None,
608 failure_url=None,
609 success_url=None,
610 # Matchers. These are separate so they can be individually
611 # overidden.
612 branch_matcher=None,
613 file_matcher=None,
614 irrelevant_file_matcher=None, # skip-if
615 tags=set(),
616 mutex=None,
617 attempts=3,
James E. Blaircdab2032017-02-01 09:09:29 -0800618 source_context=None,
James E. Blair72ae9da2017-02-03 14:21:30 -0800619 inheritance_path=[],
James E. Blair1774dd52017-02-03 10:52:32 -0800620 )
621
James E. Blairee743612012-05-29 14:49:32 -0700622 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800623 for k, v in self.attributes.items():
624 setattr(self, k, v)
625
James E. Blair66b274e2017-01-31 14:47:52 -0800626 def __ne__(self, other):
627 return not self.__eq__(other)
628
Paul Belangere22baea2016-11-03 16:59:27 -0400629 def __eq__(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800630 # Compare the name and all inheritable attributes to determine
631 # whether two jobs with the same name are identically
632 # configured. Useful upon reconfiguration.
633 if not isinstance(other, Job):
634 return False
635 if self.name != other.name:
636 return False
637 for k, v in self.attributes.items():
638 if getattr(self, k) != getattr(other, k):
639 return False
640 return True
James E. Blairee743612012-05-29 14:49:32 -0700641
642 def __str__(self):
643 return self.name
644
645 def __repr__(self):
James E. Blair72ae9da2017-02-03 14:21:30 -0800646 return '<Job %s branches: %s source: %s>' % (self.name,
647 self.branch_matcher,
648 self.source_context)
James E. Blair83005782015-12-11 14:46:03 -0800649
James E. Blair72ae9da2017-02-03 14:21:30 -0800650 def inheritFrom(self, other, comment='unknown'):
James E. Blair83005782015-12-11 14:46:03 -0800651 """Copy the inheritable attributes which have been set on the other
652 job to this job."""
653
654 if not isinstance(other, Job):
655 raise Exception("Job unable to inherit from %s" % (other,))
James E. Blair72ae9da2017-02-03 14:21:30 -0800656 self.inheritance_path.extend(other.inheritance_path)
657 self.inheritance_path.append('%s %s' % (repr(other), comment))
James E. Blair83005782015-12-11 14:46:03 -0800658 for k, v in self.attributes.items():
James E. Blair66b274e2017-01-31 14:47:52 -0800659 if (getattr(other, k) != v and k not in
James E. Blair72ae9da2017-02-03 14:21:30 -0800660 set(['auth', 'pre_run', 'post_run', 'inheritance_path'])):
James E. Blair83005782015-12-11 14:46:03 -0800661 setattr(self, k, getattr(other, k))
Ricardo Carrillo Cruz4e94f612016-07-25 16:11:56 +0000662 # Inherit auth only if explicitly allowed
663 if other.auth and 'inherit' in other.auth and other.auth['inherit']:
664 setattr(self, 'auth', getattr(other, 'auth'))
James E. Blair66b274e2017-01-31 14:47:52 -0800665 # Pre and post run are lists; make a copy
666 self.pre_run = other.pre_run + self.pre_run
667 self.post_run = self.post_run + other.post_run
James E. Blairee743612012-05-29 14:49:32 -0700668
James E. Blaire421a232012-07-25 16:59:21 -0700669 def changeMatches(self, change):
James E. Blair83005782015-12-11 14:46:03 -0800670 if self.branch_matcher and not self.branch_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800671 return False
672
James E. Blair83005782015-12-11 14:46:03 -0800673 if self.file_matcher and not self.file_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800674 return False
675
James E. Blair83005782015-12-11 14:46:03 -0800676 # NB: This is a negative match.
677 if (self.irrelevant_file_matcher and
678 self.irrelevant_file_matcher.matches(change)):
Maru Newby3fe5f852015-01-13 04:22:14 +0000679 return False
680
James E. Blair70c71582013-03-06 08:50:50 -0800681 return True
James E. Blaire5a847f2012-07-10 15:29:14 -0700682
James E. Blair1e8dd892012-05-30 09:15:05 -0700683
James E. Blairee743612012-05-29 14:49:32 -0700684class JobTree(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700685 """A JobTree holds one or more Jobs to represent Job dependencies.
686
687 If Job foo should only execute if Job bar succeeds, then there will
688 be a JobTree for foo, which will contain a JobTree for bar. A JobTree
689 can hold more than one dependent JobTrees, such that jobs bar and bang
690 both depend on job foo being successful.
691
692 A root node of a JobTree will have no associated Job."""
James E. Blairee743612012-05-29 14:49:32 -0700693
694 def __init__(self, job):
695 self.job = job
696 self.job_trees = []
697
James E. Blair12748b52017-02-07 17:17:53 -0800698 def __repr__(self):
699 return '<JobTree %s %s>' % (self.job, self.job_trees)
700
James E. Blairee743612012-05-29 14:49:32 -0700701 def addJob(self, job):
James E. Blair12a92b12014-03-26 11:54:53 -0700702 if job not in [x.job for x in self.job_trees]:
703 t = JobTree(job)
704 self.job_trees.append(t)
705 return t
James E. Blaire4ad55a2015-06-11 08:22:43 -0700706 for tree in self.job_trees:
707 if tree.job == job:
708 return tree
James E. Blairee743612012-05-29 14:49:32 -0700709
710 def getJobs(self):
711 jobs = []
712 for x in self.job_trees:
713 jobs.append(x.job)
714 jobs.extend(x.getJobs())
715 return jobs
716
717 def getJobTreeForJob(self, job):
718 if self.job == job:
719 return self
720 for tree in self.job_trees:
721 ret = tree.getJobTreeForJob(job)
722 if ret:
723 return ret
724 return None
725
James E. Blair72ae9da2017-02-03 14:21:30 -0800726 def inheritFrom(self, other, comment='unknown'):
James E. Blairb97ed802015-12-21 15:55:35 -0800727 if other.job:
728 self.job = Job(other.job.name)
James E. Blair72ae9da2017-02-03 14:21:30 -0800729 self.job.inheritFrom(other.job, comment)
James E. Blairb97ed802015-12-21 15:55:35 -0800730 for other_tree in other.job_trees:
731 this_tree = self.getJobTreeForJob(other_tree.job)
732 if not this_tree:
733 this_tree = JobTree(None)
734 self.job_trees.append(this_tree)
James E. Blair72ae9da2017-02-03 14:21:30 -0800735 this_tree.inheritFrom(other_tree, comment)
James E. Blairb97ed802015-12-21 15:55:35 -0800736
James E. Blair1e8dd892012-05-30 09:15:05 -0700737
James E. Blair4aea70c2012-07-26 14:23:24 -0700738class Build(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700739 """A Build is an instance of a single running Job."""
740
James E. Blair4aea70c2012-07-26 14:23:24 -0700741 def __init__(self, job, uuid):
742 self.job = job
743 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -0700744 self.url = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700745 self.result = None
746 self.build_set = None
747 self.launch_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -0800748 self.start_time = None
749 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -0700750 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -0700751 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -0700752 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -0700753 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -0700754 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +0800755 self.worker = Worker()
Timothy Chavezb2332082015-08-07 20:08:04 -0500756 self.node_labels = []
757 self.node_name = None
James E. Blairee743612012-05-29 14:49:32 -0700758
759 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +0800760 return ('<Build %s of %s on %s>' %
761 (self.uuid, self.job.name, self.worker))
762
763
764class Worker(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700765 """Information about the specific worker executing a Build."""
Joshua Heskethba8776a2014-01-12 14:35:40 +0800766 def __init__(self):
767 self.name = "Unknown"
768 self.hostname = None
769 self.ips = []
770 self.fqdn = None
771 self.program = None
772 self.version = None
773 self.extra = {}
774
775 def updateFromData(self, data):
776 """Update worker information if contained in the WORK_DATA response."""
777 self.name = data.get('worker_name', self.name)
778 self.hostname = data.get('worker_hostname', self.hostname)
779 self.ips = data.get('worker_ips', self.ips)
780 self.fqdn = data.get('worker_fqdn', self.fqdn)
781 self.program = data.get('worker_program', self.program)
782 self.version = data.get('worker_version', self.version)
783 self.extra = data.get('worker_extra', self.extra)
784
785 def __repr__(self):
786 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -0700787
James E. Blair1e8dd892012-05-30 09:15:05 -0700788
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700789class RepoFiles(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700790 """RepoFiles holds config-file content for per-project job config."""
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700791 # When we ask a merger to prepare a future multiple-repo state and
792 # collect files so that we can dynamically load our configuration,
793 # this class provides easy access to that data.
794 def __init__(self):
795 self.projects = {}
796
797 def __repr__(self):
798 return '<RepoFiles %s>' % self.projects
799
800 def setFiles(self, items):
801 self.projects = {}
802 for item in items:
803 project = self.projects.setdefault(item['project'], {})
804 branch = project.setdefault(item['branch'], {})
805 branch.update(item['files'])
806
807 def getFile(self, project, branch, fn):
808 return self.projects.get(project, {}).get(branch, {}).get(fn)
809
810
James E. Blair7e530ad2012-07-03 16:12:28 -0700811class BuildSet(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700812 """Contains the Builds for a Change representing potential future state.
813
814 A BuildSet also holds the UUID used to produce the Zuul Ref that builders
815 check out.
816 """
James E. Blair4076e2b2014-01-28 12:42:20 -0800817 # Merge states:
818 NEW = 1
819 PENDING = 2
820 COMPLETE = 3
821
Antoine Musso9b229282014-08-18 23:45:43 +0200822 states_map = {
823 1: 'NEW',
824 2: 'PENDING',
825 3: 'COMPLETE',
826 }
827
James E. Blairfee8d652013-06-07 08:57:52 -0700828 def __init__(self, item):
829 self.item = item
James E. Blair11700c32012-07-05 17:50:05 -0700830 self.other_changes = []
James E. Blair7e530ad2012-07-03 16:12:28 -0700831 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -0700832 self.result = None
833 self.next_build_set = None
834 self.previous_build_set = None
James E. Blair4886cc12012-07-18 15:39:41 -0700835 self.ref = None
James E. Blair81515ad2012-10-01 18:29:08 -0700836 self.commit = None
James E. Blair4076e2b2014-01-28 12:42:20 -0800837 self.zuul_url = None
James E. Blair973721f2012-08-15 10:19:43 -0700838 self.unable_to_merge = False
James E. Blair972e3c72013-08-29 12:04:55 -0700839 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -0800840 self.merge_state = self.NEW
James E. Blair0eaad552016-09-02 12:09:54 -0700841 self.nodesets = {} # job -> nodeset
James E. Blair8d692392016-04-08 17:47:58 -0700842 self.node_requests = {} # job -> reqs
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700843 self.files = RepoFiles()
844 self.layout = None
Paul Belanger71d98172016-11-08 10:56:31 -0500845 self.tries = {}
James E. Blair7e530ad2012-07-03 16:12:28 -0700846
Antoine Musso9b229282014-08-18 23:45:43 +0200847 def __repr__(self):
848 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
849 self.item,
850 len(self.builds),
851 self.getStateName(self.merge_state))
852
James E. Blair4886cc12012-07-18 15:39:41 -0700853 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -0700854 # The change isn't enqueued until after it's created
855 # so we don't know what the other changes ahead will be
856 # until jobs start.
857 if not self.other_changes:
James E. Blairfee8d652013-06-07 08:57:52 -0700858 next_item = self.item.item_ahead
859 while next_item:
860 self.other_changes.append(next_item.change)
861 next_item = next_item.item_ahead
James E. Blair4886cc12012-07-18 15:39:41 -0700862 if not self.ref:
863 self.ref = 'Z' + uuid4().hex
864
Antoine Musso9b229282014-08-18 23:45:43 +0200865 def getStateName(self, state_num):
866 return self.states_map.get(
867 state_num, 'UNKNOWN (%s)' % state_num)
868
James E. Blair4886cc12012-07-18 15:39:41 -0700869 def addBuild(self, build):
870 self.builds[build.job.name] = build
Paul Belanger71d98172016-11-08 10:56:31 -0500871 if build.job.name not in self.tries:
James E. Blair5aed1112016-12-15 09:18:05 -0800872 self.tries[build.job.name] = 1
James E. Blair4886cc12012-07-18 15:39:41 -0700873 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -0700874
James E. Blair4a28a882013-08-23 15:17:33 -0700875 def removeBuild(self, build):
Paul Belanger71d98172016-11-08 10:56:31 -0500876 self.tries[build.job.name] += 1
James E. Blair4a28a882013-08-23 15:17:33 -0700877 del self.builds[build.job.name]
878
James E. Blair7e530ad2012-07-03 16:12:28 -0700879 def getBuild(self, job_name):
880 return self.builds.get(job_name)
881
James E. Blair11700c32012-07-05 17:50:05 -0700882 def getBuilds(self):
883 keys = self.builds.keys()
884 keys.sort()
885 return [self.builds.get(x) for x in keys]
886
James E. Blair0eaad552016-09-02 12:09:54 -0700887 def getJobNodeSet(self, job_name):
888 # Return None if not provisioned; empty NodeSet if no nodes
889 # required
890 return self.nodesets.get(job_name)
James E. Blair8d692392016-04-08 17:47:58 -0700891
James E. Blaire18d4602017-01-05 11:17:28 -0800892 def removeJobNodeSet(self, job_name):
893 if job_name not in self.nodesets:
894 raise Exception("No job set for %s" % (job_name))
895 del self.nodesets[job_name]
896
James E. Blair8d692392016-04-08 17:47:58 -0700897 def setJobNodeRequest(self, job_name, req):
898 if job_name in self.node_requests:
899 raise Exception("Prior node request for %s" % (job_name))
900 self.node_requests[job_name] = req
901
902 def getJobNodeRequest(self, job_name):
903 return self.node_requests.get(job_name)
904
James E. Blair0eaad552016-09-02 12:09:54 -0700905 def jobNodeRequestComplete(self, job_name, req, nodeset):
906 if job_name in self.nodesets:
James E. Blair8d692392016-04-08 17:47:58 -0700907 raise Exception("Prior node request for %s" % (job_name))
James E. Blair0eaad552016-09-02 12:09:54 -0700908 self.nodesets[job_name] = nodeset
James E. Blair8d692392016-04-08 17:47:58 -0700909 del self.node_requests[job_name]
910
Paul Belanger71d98172016-11-08 10:56:31 -0500911 def getTries(self, job_name):
912 return self.tries.get(job_name)
913
Adam Gandelman8bd57102016-12-02 12:58:42 -0800914 def getMergeMode(self, job_name):
915 if not self.layout or job_name not in self.layout.project_configs:
916 return MERGER_MERGE_RESOLVE
917 return self.layout.project_configs[job_name].merge_mode
918
James E. Blair7e530ad2012-07-03 16:12:28 -0700919
James E. Blairfee8d652013-06-07 08:57:52 -0700920class QueueItem(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700921 """Represents the position of a Change in a ChangeQueue.
922
923 All Changes are enqueued into ChangeQueue in a QueueItem. The QueueItem
924 holds the current `BuildSet` as well as all previous `BuildSets` that were
925 produced for this `QueueItem`.
926 """
James E. Blair32663402012-06-01 10:04:18 -0700927
James E. Blairbfb8e042014-12-30 17:01:44 -0800928 def __init__(self, queue, change):
929 self.pipeline = queue.pipeline
930 self.queue = queue
James E. Blairfee8d652013-06-07 08:57:52 -0700931 self.change = change # a changeish
James E. Blair7e530ad2012-07-03 16:12:28 -0700932 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -0700933 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -0700934 self.current_build_set = BuildSet(self)
935 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -0700936 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700937 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -0800938 self.enqueue_time = None
939 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -0700940 self.reported = False
James E. Blairbfb8e042014-12-30 17:01:44 -0800941 self.active = False # Whether an item is within an active window
942 self.live = True # Whether an item is intended to be processed at all
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700943 self.layout = None # This item's shadow layout
James E. Blair83005782015-12-11 14:46:03 -0800944 self.job_tree = None
James E. Blaire5a847f2012-07-10 15:29:14 -0700945
James E. Blair972e3c72013-08-29 12:04:55 -0700946 def __repr__(self):
947 if self.pipeline:
948 pipeline = self.pipeline.name
949 else:
950 pipeline = None
951 return '<QueueItem 0x%x for %s in %s>' % (
952 id(self), self.change, pipeline)
953
James E. Blairee743612012-05-29 14:49:32 -0700954 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -0700955 old = self.current_build_set
956 self.current_build_set.result = 'CANCELED'
957 self.current_build_set = BuildSet(self)
958 old.next_build_set = self.current_build_set
959 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -0700960 self.build_sets.append(self.current_build_set)
James E. Blairee743612012-05-29 14:49:32 -0700961
962 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -0700963 self.current_build_set.addBuild(build)
James E. Blair66eeebf2013-07-27 17:44:32 -0700964 build.pipeline = self.pipeline
James E. Blairee743612012-05-29 14:49:32 -0700965
James E. Blair4a28a882013-08-23 15:17:33 -0700966 def removeBuild(self, build):
967 self.current_build_set.removeBuild(build)
968
James E. Blairfee8d652013-06-07 08:57:52 -0700969 def setReportedResult(self, result):
970 self.current_build_set.result = result
971
James E. Blair83005782015-12-11 14:46:03 -0800972 def freezeJobTree(self):
973 """Find or create actual matching jobs for this item's change and
974 store the resulting job tree."""
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700975 layout = self.current_build_set.layout
976 self.job_tree = layout.createJobTree(self)
977
978 def hasJobTree(self):
979 """Returns True if the item has a job tree."""
980 return self.job_tree is not None
James E. Blair83005782015-12-11 14:46:03 -0800981
982 def getJobs(self):
983 if not self.live or not self.job_tree:
984 return []
985 return self.job_tree.getJobs()
986
James E. Blairdbfd3282016-07-21 10:46:19 -0700987 def haveAllJobsStarted(self):
988 if not self.hasJobTree():
989 return False
990 for job in self.getJobs():
991 build = self.current_build_set.getBuild(job.name)
992 if not build or not build.start_time:
993 return False
994 return True
995
996 def areAllJobsComplete(self):
997 if not self.hasJobTree():
998 return False
999 for job in self.getJobs():
1000 build = self.current_build_set.getBuild(job.name)
1001 if not build or not build.result:
1002 return False
1003 return True
1004
1005 def didAllJobsSucceed(self):
1006 if not self.hasJobTree():
1007 return False
1008 for job in self.getJobs():
1009 if not job.voting:
1010 continue
1011 build = self.current_build_set.getBuild(job.name)
1012 if not build:
1013 return False
1014 if build.result != 'SUCCESS':
1015 return False
1016 return True
1017
1018 def didAnyJobFail(self):
1019 if not self.hasJobTree():
1020 return False
1021 for job in self.getJobs():
1022 if not job.voting:
1023 continue
1024 build = self.current_build_set.getBuild(job.name)
1025 if build and build.result and (build.result != 'SUCCESS'):
1026 return True
1027 return False
1028
1029 def didMergerFail(self):
1030 if self.current_build_set.unable_to_merge:
1031 return True
1032 return False
1033
James E. Blairdbfd3282016-07-21 10:46:19 -07001034 def isHoldingFollowingChanges(self):
1035 if not self.live:
1036 return False
1037 if not self.hasJobTree():
1038 return False
1039 for job in self.getJobs():
1040 if not job.hold_following_changes:
1041 continue
1042 build = self.current_build_set.getBuild(job.name)
1043 if not build:
1044 return True
1045 if build.result != 'SUCCESS':
1046 return True
1047
1048 if not self.item_ahead:
1049 return False
1050 return self.item_ahead.isHoldingFollowingChanges()
1051
1052 def _findJobsToRun(self, job_trees, mutex):
1053 torun = []
James E. Blair791b5392016-08-03 11:25:56 -07001054 if self.item_ahead:
1055 # Only run jobs if any 'hold' jobs on the change ahead
1056 # have completed successfully.
1057 if self.item_ahead.isHoldingFollowingChanges():
1058 return []
James E. Blairdbfd3282016-07-21 10:46:19 -07001059 for tree in job_trees:
1060 job = tree.job
1061 result = None
1062 if job:
1063 if not job.changeMatches(self.change):
1064 continue
1065 build = self.current_build_set.getBuild(job.name)
1066 if build:
1067 result = build.result
1068 else:
1069 # There is no build for the root of this job tree,
James E. Blair34776ee2016-08-25 13:53:54 -07001070 # so it has not run yet.
James E. Blair0eaad552016-09-02 12:09:54 -07001071 nodeset = self.current_build_set.getJobNodeSet(job.name)
1072 if nodeset is None:
James E. Blair34776ee2016-08-25 13:53:54 -07001073 # The nodes for this job are not ready, skip
1074 # it for now.
1075 continue
James E. Blairdbfd3282016-07-21 10:46:19 -07001076 if mutex.acquire(self, job):
1077 # If this job needs a mutex, either acquire it or make
1078 # sure that we have it before running the job.
1079 torun.append(job)
1080 # If there is no job, this is a null job tree, and we should
1081 # run all of its jobs.
1082 if result == 'SUCCESS' or not job:
1083 torun.extend(self._findJobsToRun(tree.job_trees, mutex))
1084 return torun
1085
1086 def findJobsToRun(self, mutex):
1087 if not self.live:
1088 return []
1089 tree = self.job_tree
1090 if not tree:
1091 return []
1092 return self._findJobsToRun(tree.job_trees, mutex)
1093
1094 def _findJobsToRequest(self, job_trees):
James E. Blair6ab79e02017-01-06 10:10:17 -08001095 build_set = self.current_build_set
James E. Blairdbfd3282016-07-21 10:46:19 -07001096 toreq = []
James E. Blair6ab79e02017-01-06 10:10:17 -08001097 if self.item_ahead:
1098 if self.item_ahead.isHoldingFollowingChanges():
1099 return []
James E. Blairdbfd3282016-07-21 10:46:19 -07001100 for tree in job_trees:
1101 job = tree.job
James E. Blair6ab79e02017-01-06 10:10:17 -08001102 result = None
James E. Blairdbfd3282016-07-21 10:46:19 -07001103 if job:
1104 if not job.changeMatches(self.change):
1105 continue
James E. Blair6ab79e02017-01-06 10:10:17 -08001106 build = build_set.getBuild(job.name)
1107 if build:
1108 result = build.result
1109 else:
1110 nodeset = build_set.getJobNodeSet(job.name)
1111 if nodeset is None:
1112 req = build_set.getJobNodeRequest(job.name)
1113 if req is None:
1114 toreq.append(job)
1115 if result == 'SUCCESS' or not job:
1116 toreq.extend(self._findJobsToRequest(tree.job_trees))
James E. Blairdbfd3282016-07-21 10:46:19 -07001117 return toreq
1118
1119 def findJobsToRequest(self):
1120 if not self.live:
1121 return []
1122 tree = self.job_tree
1123 if not tree:
1124 return []
1125 return self._findJobsToRequest(tree.job_trees)
1126
1127 def setResult(self, build):
1128 if build.retry:
1129 self.removeBuild(build)
1130 elif build.result != 'SUCCESS':
1131 # Get a JobTree from a Job so we can find only its dependent jobs
1132 tree = self.job_tree.getJobTreeForJob(build.job)
1133 for job in tree.getJobs():
1134 fakebuild = Build(job, None)
1135 fakebuild.result = 'SKIPPED'
1136 self.addBuild(fakebuild)
1137
James E. Blair6ab79e02017-01-06 10:10:17 -08001138 def setNodeRequestFailure(self, job):
1139 fakebuild = Build(job, None)
1140 self.addBuild(fakebuild)
1141 fakebuild.result = 'NODE_FAILURE'
1142 self.setResult(fakebuild)
1143
James E. Blairdbfd3282016-07-21 10:46:19 -07001144 def setDequeuedNeedingChange(self):
1145 self.dequeued_needing_change = True
1146 self._setAllJobsSkipped()
1147
1148 def setUnableToMerge(self):
1149 self.current_build_set.unable_to_merge = True
1150 self._setAllJobsSkipped()
1151
1152 def _setAllJobsSkipped(self):
1153 for job in self.getJobs():
1154 fakebuild = Build(job, None)
1155 fakebuild.result = 'SKIPPED'
1156 self.addBuild(fakebuild)
1157
James E. Blairb7273ef2016-04-19 08:58:51 -07001158 def formatJobResult(self, job, url_pattern=None):
1159 build = self.current_build_set.getBuild(job.name)
1160 result = build.result
1161 pattern = url_pattern
1162 if result == 'SUCCESS':
1163 if job.success_message:
1164 result = job.success_message
James E. Blaira5dba232016-08-08 15:53:24 -07001165 if job.success_url:
1166 pattern = job.success_url
James E. Blairb7273ef2016-04-19 08:58:51 -07001167 elif result == 'FAILURE':
1168 if job.failure_message:
1169 result = job.failure_message
James E. Blaira5dba232016-08-08 15:53:24 -07001170 if job.failure_url:
1171 pattern = job.failure_url
James E. Blairb7273ef2016-04-19 08:58:51 -07001172 url = None
1173 if pattern:
1174 try:
1175 url = pattern.format(change=self.change,
1176 pipeline=self.pipeline,
1177 job=job,
1178 build=build)
1179 except Exception:
1180 pass # FIXME: log this or something?
1181 if not url:
1182 url = build.url or job.name
1183 return (result, url)
1184
1185 def formatJSON(self, url_pattern=None):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001186 changeish = self.change
1187 ret = {}
1188 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -08001189 ret['live'] = self.live
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001190 if hasattr(changeish, 'url') and changeish.url is not None:
1191 ret['url'] = changeish.url
1192 else:
1193 ret['url'] = None
1194 ret['id'] = changeish._id()
1195 if self.item_ahead:
1196 ret['item_ahead'] = self.item_ahead.change._id()
1197 else:
1198 ret['item_ahead'] = None
1199 ret['items_behind'] = [i.change._id() for i in self.items_behind]
1200 ret['failing_reasons'] = self.current_build_set.failing_reasons
1201 ret['zuul_ref'] = self.current_build_set.ref
Ramy Asselin07cc33c2015-06-12 14:06:34 -07001202 if changeish.project:
1203 ret['project'] = changeish.project.name
1204 else:
1205 # For cross-project dependencies with the depends-on
1206 # project not known to zuul, the project is None
1207 # Set it to a static value
1208 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001209 ret['enqueue_time'] = int(self.enqueue_time * 1000)
1210 ret['jobs'] = []
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001211 if hasattr(changeish, 'owner'):
1212 ret['owner'] = changeish.owner
1213 else:
1214 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001215 max_remaining = 0
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001216 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001217 now = time.time()
1218 build = self.current_build_set.getBuild(job.name)
1219 elapsed = None
1220 remaining = None
1221 result = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001222 build_url = None
1223 report_url = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001224 worker = None
1225 if build:
1226 result = build.result
James E. Blairb7273ef2016-04-19 08:58:51 -07001227 build_url = build.url
1228 (unused, report_url) = self.formatJobResult(job, url_pattern)
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001229 if build.start_time:
1230 if build.end_time:
1231 elapsed = int((build.end_time -
1232 build.start_time) * 1000)
1233 remaining = 0
1234 else:
1235 elapsed = int((now - build.start_time) * 1000)
1236 if build.estimated_time:
1237 remaining = max(
1238 int(build.estimated_time * 1000) - elapsed,
1239 0)
1240 worker = {
1241 'name': build.worker.name,
1242 'hostname': build.worker.hostname,
1243 'ips': build.worker.ips,
1244 'fqdn': build.worker.fqdn,
1245 'program': build.worker.program,
1246 'version': build.worker.version,
1247 'extra': build.worker.extra
1248 }
1249 if remaining and remaining > max_remaining:
1250 max_remaining = remaining
1251
1252 ret['jobs'].append({
1253 'name': job.name,
1254 'elapsed_time': elapsed,
1255 'remaining_time': remaining,
James E. Blairb7273ef2016-04-19 08:58:51 -07001256 'url': build_url,
1257 'report_url': report_url,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001258 'result': result,
1259 'voting': job.voting,
1260 'uuid': build.uuid if build else None,
1261 'launch_time': build.launch_time if build else None,
1262 'start_time': build.start_time if build else None,
1263 'end_time': build.end_time if build else None,
1264 'estimated_time': build.estimated_time if build else None,
1265 'pipeline': build.pipeline.name if build else None,
1266 'canceled': build.canceled if build else None,
1267 'retry': build.retry if build else None,
Timothy Chavezb2332082015-08-07 20:08:04 -05001268 'node_labels': build.node_labels if build else [],
1269 'node_name': build.node_name if build else None,
1270 'worker': worker,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001271 })
1272
James E. Blairdbfd3282016-07-21 10:46:19 -07001273 if self.haveAllJobsStarted():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001274 ret['remaining_time'] = max_remaining
1275 else:
1276 ret['remaining_time'] = None
1277 return ret
1278
1279 def formatStatus(self, indent=0, html=False):
1280 changeish = self.change
1281 indent_str = ' ' * indent
1282 ret = ''
1283 if html and hasattr(changeish, 'url') and changeish.url is not None:
1284 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
1285 indent_str,
1286 changeish.project.name,
1287 changeish.url,
1288 changeish._id())
1289 else:
1290 ret += '%sProject %s change %s based on %s\n' % (
1291 indent_str,
1292 changeish.project.name,
1293 changeish._id(),
1294 self.item_ahead)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001295 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001296 build = self.current_build_set.getBuild(job.name)
1297 if build:
1298 result = build.result
1299 else:
1300 result = None
1301 job_name = job.name
1302 if not job.voting:
1303 voting = ' (non-voting)'
1304 else:
1305 voting = ''
1306 if html:
1307 if build:
1308 url = build.url
1309 else:
1310 url = None
1311 if url is not None:
1312 job_name = '<a href="%s">%s</a>' % (url, job_name)
1313 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
1314 ret += '\n'
1315 return ret
1316
James E. Blairfee8d652013-06-07 08:57:52 -07001317
1318class Changeish(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001319 """Base class for Change and Ref."""
James E. Blairfee8d652013-06-07 08:57:52 -07001320
1321 def __init__(self, project):
1322 self.project = project
1323
Joshua Hesketh36c3fa52014-01-22 11:40:52 +11001324 def getBasePath(self):
1325 base_path = ''
1326 if hasattr(self, 'refspec'):
1327 base_path = "%s/%s/%s" % (
1328 self.number[-2:], self.number, self.patchset)
1329 elif hasattr(self, 'ref'):
1330 base_path = "%s/%s" % (self.newrev[:2], self.newrev)
1331
1332 return base_path
1333
James E. Blairfee8d652013-06-07 08:57:52 -07001334 def equals(self, other):
1335 raise NotImplementedError()
1336
1337 def isUpdateOf(self, other):
1338 raise NotImplementedError()
1339
1340 def filterJobs(self, jobs):
1341 return filter(lambda job: job.changeMatches(self), jobs)
1342
1343 def getRelatedChanges(self):
1344 return set()
1345
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001346 def updatesConfig(self):
1347 return False
1348
James E. Blair1e8dd892012-05-30 09:15:05 -07001349
James E. Blair4aea70c2012-07-26 14:23:24 -07001350class Change(Changeish):
Monty Taylora42a55b2016-07-29 07:53:33 -07001351 """A proposed new state for a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001352 def __init__(self, project):
1353 super(Change, self).__init__(project)
1354 self.branch = None
1355 self.number = None
1356 self.url = None
1357 self.patchset = None
1358 self.refspec = None
1359
James E. Blair70c71582013-03-06 08:50:50 -08001360 self.files = []
James E. Blair6965a4b2014-12-16 17:19:04 -08001361 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -07001362 self.needed_by_changes = []
1363 self.is_current_patchset = True
1364 self.can_merge = False
1365 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -07001366 self.failed_to_merge = False
James E. Blairc053d022014-01-22 14:57:33 -08001367 self.approvals = []
James E. Blair11041d22014-05-02 14:49:53 -07001368 self.open = None
1369 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001370 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001371
1372 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -07001373 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -07001374
1375 def __repr__(self):
1376 return '<Change 0x%x %s>' % (id(self), self._id())
1377
1378 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +08001379 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -07001380 return True
1381 return False
1382
James E. Blair2fa50962013-01-30 21:50:41 -08001383 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -08001384 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -07001385 (hasattr(other, 'patchset') and
1386 self.patchset is not None and
1387 other.patchset is not None and
1388 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -08001389 return True
1390 return False
1391
James E. Blairfee8d652013-06-07 08:57:52 -07001392 def getRelatedChanges(self):
1393 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -08001394 for c in self.needs_changes:
1395 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -07001396 for c in self.needed_by_changes:
1397 related.add(c)
1398 related.update(c.getRelatedChanges())
1399 return related
James E. Blair4aea70c2012-07-26 14:23:24 -07001400
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001401 def updatesConfig(self):
1402 if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files:
1403 return True
1404 return False
1405
James E. Blair4aea70c2012-07-26 14:23:24 -07001406
1407class Ref(Changeish):
Monty Taylora42a55b2016-07-29 07:53:33 -07001408 """An existing state of a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001409 def __init__(self, project):
James E. Blairbe765db2012-08-07 08:36:20 -07001410 super(Ref, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -07001411 self.ref = None
1412 self.oldrev = None
1413 self.newrev = None
1414
James E. Blairbe765db2012-08-07 08:36:20 -07001415 def _id(self):
1416 return self.newrev
1417
Antoine Musso68bdcd72013-01-17 12:31:28 +01001418 def __repr__(self):
1419 rep = None
1420 if self.newrev == '0000000000000000000000000000000000000000':
1421 rep = '<Ref 0x%x deletes %s from %s' % (
1422 id(self), self.ref, self.oldrev)
1423 elif self.oldrev == '0000000000000000000000000000000000000000':
1424 rep = '<Ref 0x%x creates %s on %s>' % (
1425 id(self), self.ref, self.newrev)
1426 else:
1427 # Catch all
1428 rep = '<Ref 0x%x %s updated %s..%s>' % (
1429 id(self), self.ref, self.oldrev, self.newrev)
1430
1431 return rep
1432
James E. Blair4aea70c2012-07-26 14:23:24 -07001433 def equals(self, other):
James E. Blair9358c612012-09-28 08:29:39 -07001434 if (self.project == other.project
1435 and self.ref == other.ref
1436 and self.newrev == other.newrev):
James E. Blair4aea70c2012-07-26 14:23:24 -07001437 return True
1438 return False
1439
James E. Blair2fa50962013-01-30 21:50:41 -08001440 def isUpdateOf(self, other):
1441 return False
1442
James E. Blair4aea70c2012-07-26 14:23:24 -07001443
James E. Blair63bb0ef2013-07-29 17:14:51 -07001444class NullChange(Changeish):
James E. Blair23161912016-07-28 15:42:14 -07001445 # TODOv3(jeblair): remove this in favor of enqueueing Refs (eg
1446 # current master) instead.
James E. Blaire5910202013-12-27 09:50:31 -08001447 def __repr__(self):
1448 return '<NullChange for %s>' % (self.project)
James E. Blair63bb0ef2013-07-29 17:14:51 -07001449
James E. Blair63bb0ef2013-07-29 17:14:51 -07001450 def _id(self):
Alex Gaynorddb9ef32013-09-16 21:04:58 -07001451 return None
James E. Blair63bb0ef2013-07-29 17:14:51 -07001452
1453 def equals(self, other):
Steve Varnau7b78b312015-04-03 14:49:46 -07001454 if (self.project == other.project
1455 and other._id() is None):
James E. Blair4f6033c2014-03-27 15:49:09 -07001456 return True
James E. Blair63bb0ef2013-07-29 17:14:51 -07001457 return False
1458
1459 def isUpdateOf(self, other):
1460 return False
1461
1462
James E. Blairee743612012-05-29 14:49:32 -07001463class TriggerEvent(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001464 """Incoming event from an external system."""
James E. Blairee743612012-05-29 14:49:32 -07001465 def __init__(self):
1466 self.data = None
James E. Blair32663402012-06-01 10:04:18 -07001467 # common
James E. Blairee743612012-05-29 14:49:32 -07001468 self.type = None
Paul Belangerbaca3132016-11-04 12:49:54 -04001469 # For management events (eg: enqueue / promote)
1470 self.tenant_name = None
James E. Blairee743612012-05-29 14:49:32 -07001471 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -07001472 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +01001473 # Representation of the user account that performed the event.
1474 self.account = None
James E. Blair32663402012-06-01 10:04:18 -07001475 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -07001476 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -07001477 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -07001478 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -07001479 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -07001480 self.approvals = []
1481 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07001482 self.comment = None
James E. Blair32663402012-06-01 10:04:18 -07001483 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -07001484 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -07001485 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07001486 self.newrev = None
James E. Blair63bb0ef2013-07-29 17:14:51 -07001487 # timer
1488 self.timespec = None
James E. Blairc494d542014-08-06 09:23:52 -07001489 # zuultrigger
1490 self.pipeline_name = None
James E. Blairad28e912013-11-27 10:43:22 -08001491 # For events that arrive with a destination pipeline (eg, from
1492 # an admin command, etc):
1493 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07001494
James E. Blair9f9667e2012-06-12 17:51:08 -07001495 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001496 ret = '<TriggerEvent %s %s' % (self.type, self.project_name)
James E. Blair1e8dd892012-05-30 09:15:05 -07001497
James E. Blairee743612012-05-29 14:49:32 -07001498 if self.branch:
1499 ret += " %s" % self.branch
1500 if self.change_number:
1501 ret += " %s,%s" % (self.change_number, self.patch_number)
1502 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -07001503 ret += ' ' + ', '.join(
1504 ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
James E. Blairee743612012-05-29 14:49:32 -07001505 ret += '>'
1506
1507 return ret
1508
James E. Blair1e8dd892012-05-30 09:15:05 -07001509
James E. Blair9c17dbf2014-06-23 14:21:58 -07001510class BaseFilter(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001511 """Base Class for filtering which Changes and Events to process."""
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001512 def __init__(self, required_approvals=[], reject_approvals=[]):
James E. Blair5bf78a32015-07-30 18:08:24 +00001513 self._required_approvals = copy.deepcopy(required_approvals)
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001514 self.required_approvals = self._tidy_approvals(required_approvals)
1515 self._reject_approvals = copy.deepcopy(reject_approvals)
1516 self.reject_approvals = self._tidy_approvals(reject_approvals)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001517
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001518 def _tidy_approvals(self, approvals):
1519 for a in approvals:
James E. Blair9c17dbf2014-06-23 14:21:58 -07001520 for k, v in a.items():
1521 if k == 'username':
James E. Blairb01ec542016-06-16 09:46:49 -07001522 a['username'] = re.compile(v)
James E. Blair1fbfceb2014-06-23 14:42:53 -07001523 elif k in ['email', 'email-filter']:
James E. Blair5bf78a32015-07-30 18:08:24 +00001524 a['email'] = re.compile(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001525 elif k == 'newer-than':
James E. Blair5bf78a32015-07-30 18:08:24 +00001526 a[k] = time_to_seconds(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001527 elif k == 'older-than':
James E. Blair5bf78a32015-07-30 18:08:24 +00001528 a[k] = time_to_seconds(v)
James E. Blair1fbfceb2014-06-23 14:42:53 -07001529 if 'email-filter' in a:
1530 del a['email-filter']
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001531 return approvals
1532
1533 def _match_approval_required_approval(self, rapproval, approval):
1534 # Check if the required approval and approval match
1535 if 'description' not in approval:
1536 return False
1537 now = time.time()
1538 by = approval.get('by', {})
1539 for k, v in rapproval.items():
1540 if k == 'username':
James E. Blairb01ec542016-06-16 09:46:49 -07001541 if (not v.search(by.get('username', ''))):
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001542 return False
1543 elif k == 'email':
1544 if (not v.search(by.get('email', ''))):
1545 return False
1546 elif k == 'newer-than':
1547 t = now - v
1548 if (approval['grantedOn'] < t):
1549 return False
1550 elif k == 'older-than':
1551 t = now - v
1552 if (approval['grantedOn'] >= t):
1553 return False
1554 else:
1555 if not isinstance(v, list):
1556 v = [v]
1557 if (normalizeCategory(approval['description']) != k or
1558 int(approval['value']) not in v):
1559 return False
1560 return True
1561
1562 def matchesApprovals(self, change):
1563 if (self.required_approvals and not change.approvals
1564 or self.reject_approvals and not change.approvals):
1565 # A change with no approvals can not match
1566 return False
1567
1568 # TODO(jhesketh): If we wanted to optimise this slightly we could
1569 # analyse both the REQUIRE and REJECT filters by looping over the
1570 # approvals on the change and keeping track of what we have checked
1571 # rather than needing to loop on the change approvals twice
1572 return (self.matchesRequiredApprovals(change) and
1573 self.matchesNoRejectApprovals(change))
James E. Blair9c17dbf2014-06-23 14:21:58 -07001574
1575 def matchesRequiredApprovals(self, change):
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001576 # Check if any approvals match the requirements
James E. Blair5bf78a32015-07-30 18:08:24 +00001577 for rapproval in self.required_approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001578 matches_rapproval = False
James E. Blair9c17dbf2014-06-23 14:21:58 -07001579 for approval in change.approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001580 if self._match_approval_required_approval(rapproval, approval):
1581 # We have a matching approval so this requirement is
1582 # fulfilled
1583 matches_rapproval = True
James E. Blair5bf78a32015-07-30 18:08:24 +00001584 break
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001585 if not matches_rapproval:
James E. Blair5bf78a32015-07-30 18:08:24 +00001586 return False
James E. Blair9c17dbf2014-06-23 14:21:58 -07001587 return True
1588
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001589 def matchesNoRejectApprovals(self, change):
1590 # Check to make sure no approvals match a reject criteria
1591 for rapproval in self.reject_approvals:
1592 for approval in change.approvals:
1593 if self._match_approval_required_approval(rapproval, approval):
1594 # A reject approval has been matched, so we reject
1595 # immediately
1596 return False
1597 # To get here no rejects can have been matched so we should be good to
1598 # queue
1599 return True
1600
James E. Blair9c17dbf2014-06-23 14:21:58 -07001601
1602class EventFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07001603 """Allows a Pipeline to only respond to certain events."""
James E. Blairc0dedf82014-08-06 09:37:52 -07001604 def __init__(self, trigger, types=[], branches=[], refs=[],
1605 event_approvals={}, comments=[], emails=[], usernames=[],
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001606 timespecs=[], required_approvals=[], reject_approvals=[],
1607 pipelines=[], ignore_deletes=True):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001608 super(EventFilter, self).__init__(
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001609 required_approvals=required_approvals,
1610 reject_approvals=reject_approvals)
James E. Blairc0dedf82014-08-06 09:37:52 -07001611 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07001612 self._types = types
1613 self._branches = branches
1614 self._refs = refs
James E. Blair1fbfceb2014-06-23 14:42:53 -07001615 self._comments = comments
1616 self._emails = emails
1617 self._usernames = usernames
James E. Blairc494d542014-08-06 09:23:52 -07001618 self._pipelines = pipelines
James E. Blairee743612012-05-29 14:49:32 -07001619 self.types = [re.compile(x) for x in types]
1620 self.branches = [re.compile(x) for x in branches]
1621 self.refs = [re.compile(x) for x in refs]
James E. Blair1fbfceb2014-06-23 14:42:53 -07001622 self.comments = [re.compile(x) for x in comments]
1623 self.emails = [re.compile(x) for x in emails]
1624 self.usernames = [re.compile(x) for x in usernames]
James E. Blairc494d542014-08-06 09:23:52 -07001625 self.pipelines = [re.compile(x) for x in pipelines]
James E. Blairc053d022014-01-22 14:57:33 -08001626 self.event_approvals = event_approvals
James E. Blair63bb0ef2013-07-29 17:14:51 -07001627 self.timespecs = timespecs
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001628 self.ignore_deletes = ignore_deletes
James E. Blairee743612012-05-29 14:49:32 -07001629
James E. Blair9f9667e2012-06-12 17:51:08 -07001630 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001631 ret = '<EventFilter'
James E. Blair1e8dd892012-05-30 09:15:05 -07001632
James E. Blairee743612012-05-29 14:49:32 -07001633 if self._types:
1634 ret += ' types: %s' % ', '.join(self._types)
James E. Blairc494d542014-08-06 09:23:52 -07001635 if self._pipelines:
1636 ret += ' pipelines: %s' % ', '.join(self._pipelines)
James E. Blairee743612012-05-29 14:49:32 -07001637 if self._branches:
1638 ret += ' branches: %s' % ', '.join(self._branches)
1639 if self._refs:
1640 ret += ' refs: %s' % ', '.join(self._refs)
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001641 if self.ignore_deletes:
1642 ret += ' ignore_deletes: %s' % self.ignore_deletes
James E. Blairc053d022014-01-22 14:57:33 -08001643 if self.event_approvals:
1644 ret += ' event_approvals: %s' % ', '.join(
1645 ['%s:%s' % a for a in self.event_approvals.items()])
James E. Blair5bf78a32015-07-30 18:08:24 +00001646 if self.required_approvals:
1647 ret += ' required_approvals: %s' % ', '.join(
1648 ['%s' % a for a in self._required_approvals])
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001649 if self.reject_approvals:
1650 ret += ' reject_approvals: %s' % ', '.join(
1651 ['%s' % a for a in self._reject_approvals])
James E. Blair1fbfceb2014-06-23 14:42:53 -07001652 if self._comments:
1653 ret += ' comments: %s' % ', '.join(self._comments)
1654 if self._emails:
1655 ret += ' emails: %s' % ', '.join(self._emails)
1656 if self._usernames:
1657 ret += ' username_filters: %s' % ', '.join(self._usernames)
James E. Blair63bb0ef2013-07-29 17:14:51 -07001658 if self.timespecs:
1659 ret += ' timespecs: %s' % ', '.join(self.timespecs)
James E. Blairee743612012-05-29 14:49:32 -07001660 ret += '>'
1661
1662 return ret
1663
James E. Blairc053d022014-01-22 14:57:33 -08001664 def matches(self, event, change):
James E. Blairee743612012-05-29 14:49:32 -07001665 # event types are ORed
1666 matches_type = False
1667 for etype in self.types:
1668 if etype.match(event.type):
1669 matches_type = True
1670 if self.types and not matches_type:
1671 return False
1672
James E. Blairc494d542014-08-06 09:23:52 -07001673 # pipelines are ORed
1674 matches_pipeline = False
1675 for epipe in self.pipelines:
1676 if epipe.match(event.pipeline_name):
1677 matches_pipeline = True
1678 if self.pipelines and not matches_pipeline:
1679 return False
1680
James E. Blairee743612012-05-29 14:49:32 -07001681 # branches are ORed
1682 matches_branch = False
1683 for branch in self.branches:
1684 if branch.match(event.branch):
1685 matches_branch = True
1686 if self.branches and not matches_branch:
1687 return False
1688
1689 # refs are ORed
1690 matches_ref = False
Yolanda Robla16698872014-08-25 11:59:27 +02001691 if event.ref is not None:
1692 for ref in self.refs:
1693 if ref.match(event.ref):
1694 matches_ref = True
James E. Blairee743612012-05-29 14:49:32 -07001695 if self.refs and not matches_ref:
1696 return False
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001697 if self.ignore_deletes and event.newrev == EMPTY_GIT_REF:
1698 # If the updated ref has an empty git sha (all 0s),
1699 # then the ref is being deleted
1700 return False
James E. Blairee743612012-05-29 14:49:32 -07001701
James E. Blair1fbfceb2014-06-23 14:42:53 -07001702 # comments are ORed
1703 matches_comment_re = False
1704 for comment_re in self.comments:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001705 if (event.comment is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001706 comment_re.search(event.comment)):
1707 matches_comment_re = True
1708 if self.comments and not matches_comment_re:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001709 return False
1710
Antoine Mussob4e809e2012-12-06 16:58:06 +01001711 # We better have an account provided by Gerrit to do
1712 # email filtering.
1713 if event.account is not None:
James E. Blaircf429f32012-12-20 14:28:24 -08001714 account_email = event.account.get('email')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001715 # emails are ORed
1716 matches_email_re = False
1717 for email_re in self.emails:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001718 if (account_email is not None and
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001719 email_re.search(account_email)):
James E. Blair1fbfceb2014-06-23 14:42:53 -07001720 matches_email_re = True
1721 if self.emails and not matches_email_re:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001722 return False
1723
James E. Blair1fbfceb2014-06-23 14:42:53 -07001724 # usernames are ORed
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001725 account_username = event.account.get('username')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001726 matches_username_re = False
1727 for username_re in self.usernames:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001728 if (account_username is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001729 username_re.search(account_username)):
1730 matches_username_re = True
1731 if self.usernames and not matches_username_re:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001732 return False
1733
James E. Blairee743612012-05-29 14:49:32 -07001734 # approvals are ANDed
James E. Blairc053d022014-01-22 14:57:33 -08001735 for category, value in self.event_approvals.items():
James E. Blairee743612012-05-29 14:49:32 -07001736 matches_approval = False
1737 for eapproval in event.approvals:
1738 if (normalizeCategory(eapproval['description']) == category and
1739 int(eapproval['value']) == int(value)):
1740 matches_approval = True
James E. Blair1e8dd892012-05-30 09:15:05 -07001741 if not matches_approval:
1742 return False
James E. Blair63bb0ef2013-07-29 17:14:51 -07001743
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001744 # required approvals are ANDed (reject approvals are ORed)
1745 if not self.matchesApprovals(change):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001746 return False
James E. Blairc053d022014-01-22 14:57:33 -08001747
James E. Blair63bb0ef2013-07-29 17:14:51 -07001748 # timespecs are ORed
1749 matches_timespec = False
1750 for timespec in self.timespecs:
1751 if (event.timespec == timespec):
1752 matches_timespec = True
1753 if self.timespecs and not matches_timespec:
1754 return False
1755
James E. Blairee743612012-05-29 14:49:32 -07001756 return True
James E. Blaireff88162013-07-01 12:44:14 -04001757
1758
James E. Blair9c17dbf2014-06-23 14:21:58 -07001759class ChangeishFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07001760 """Allows a Manager to only enqueue Changes that meet certain criteria."""
Clark Boylana9702ad2014-05-08 17:17:24 -07001761 def __init__(self, open=None, current_patchset=None,
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001762 statuses=[], required_approvals=[],
1763 reject_approvals=[]):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001764 super(ChangeishFilter, self).__init__(
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001765 required_approvals=required_approvals,
1766 reject_approvals=reject_approvals)
James E. Blair11041d22014-05-02 14:49:53 -07001767 self.open = open
Clark Boylana9702ad2014-05-08 17:17:24 -07001768 self.current_patchset = current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001769 self.statuses = statuses
James E. Blair11041d22014-05-02 14:49:53 -07001770
1771 def __repr__(self):
1772 ret = '<ChangeishFilter'
1773
1774 if self.open is not None:
1775 ret += ' open: %s' % self.open
Clark Boylana9702ad2014-05-08 17:17:24 -07001776 if self.current_patchset is not None:
1777 ret += ' current-patchset: %s' % self.current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001778 if self.statuses:
1779 ret += ' statuses: %s' % ', '.join(self.statuses)
James E. Blair5bf78a32015-07-30 18:08:24 +00001780 if self.required_approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001781 ret += (' required_approvals: %s' %
1782 str(self.required_approvals))
1783 if self.reject_approvals:
1784 ret += (' reject_approvals: %s' %
1785 str(self.reject_approvals))
James E. Blair11041d22014-05-02 14:49:53 -07001786 ret += '>'
1787
1788 return ret
1789
1790 def matches(self, change):
1791 if self.open is not None:
1792 if self.open != change.open:
1793 return False
1794
Clark Boylana9702ad2014-05-08 17:17:24 -07001795 if self.current_patchset is not None:
1796 if self.current_patchset != change.is_current_patchset:
1797 return False
1798
James E. Blair11041d22014-05-02 14:49:53 -07001799 if self.statuses:
1800 if change.status not in self.statuses:
1801 return False
1802
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001803 # required approvals are ANDed (reject approvals are ORed)
1804 if not self.matchesApprovals(change):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001805 return False
James E. Blair11041d22014-05-02 14:49:53 -07001806
1807 return True
1808
1809
James E. Blairb97ed802015-12-21 15:55:35 -08001810class ProjectPipelineConfig(object):
1811 # Represents a project cofiguration in the context of a pipeline
1812 def __init__(self):
1813 self.job_tree = None
1814 self.queue_name = None
Adam Gandelman8bd57102016-12-02 12:58:42 -08001815 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08001816
1817
1818class ProjectConfig(object):
1819 # Represents a project cofiguration
1820 def __init__(self, name):
1821 self.name = name
Adam Gandelman8bd57102016-12-02 12:58:42 -08001822 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08001823 self.pipelines = {}
1824
1825
James E. Blaird8e778f2015-12-22 14:09:20 -08001826class UnparsedAbideConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001827 """A collection of yaml lists that has not yet been parsed into objects.
1828
1829 An Abide is a collection of tenants.
1830 """
1831
James E. Blaird8e778f2015-12-22 14:09:20 -08001832 def __init__(self):
1833 self.tenants = []
1834
1835 def extend(self, conf):
1836 if isinstance(conf, UnparsedAbideConfig):
1837 self.tenants.extend(conf.tenants)
1838 return
1839
1840 if not isinstance(conf, list):
1841 raise Exception("Configuration items must be in the form of "
1842 "a list of dictionaries (when parsing %s)" %
1843 (conf,))
1844 for item in conf:
1845 if not isinstance(item, dict):
1846 raise Exception("Configuration items must be in the form of "
1847 "a list of dictionaries (when parsing %s)" %
1848 (conf,))
1849 if len(item.keys()) > 1:
1850 raise Exception("Configuration item dictionaries must have "
1851 "a single key (when parsing %s)" %
1852 (conf,))
1853 key, value = item.items()[0]
1854 if key == 'tenant':
1855 self.tenants.append(value)
1856 else:
1857 raise Exception("Configuration item not recognized "
1858 "(when parsing %s)" %
1859 (conf,))
1860
1861
1862class UnparsedTenantConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001863 """A collection of yaml lists that has not yet been parsed into objects."""
1864
James E. Blaird8e778f2015-12-22 14:09:20 -08001865 def __init__(self):
1866 self.pipelines = []
1867 self.jobs = []
1868 self.project_templates = []
1869 self.projects = []
James E. Blaira98340f2016-09-02 11:33:49 -07001870 self.nodesets = []
James E. Blaird8e778f2015-12-22 14:09:20 -08001871
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001872 def copy(self):
1873 r = UnparsedTenantConfig()
1874 r.pipelines = copy.deepcopy(self.pipelines)
1875 r.jobs = copy.deepcopy(self.jobs)
1876 r.project_templates = copy.deepcopy(self.project_templates)
1877 r.projects = copy.deepcopy(self.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07001878 r.nodesets = copy.deepcopy(self.nodesets)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001879 return r
1880
James E. Blaircdab2032017-02-01 09:09:29 -08001881 def extend(self, conf, source_context=None):
James E. Blaird8e778f2015-12-22 14:09:20 -08001882 if isinstance(conf, UnparsedTenantConfig):
1883 self.pipelines.extend(conf.pipelines)
1884 self.jobs.extend(conf.jobs)
1885 self.project_templates.extend(conf.project_templates)
1886 self.projects.extend(conf.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07001887 self.nodesets.extend(conf.nodesets)
James E. Blaird8e778f2015-12-22 14:09:20 -08001888 return
1889
1890 if not isinstance(conf, list):
1891 raise Exception("Configuration items must be in the form of "
1892 "a list of dictionaries (when parsing %s)" %
1893 (conf,))
James E. Blaircdab2032017-02-01 09:09:29 -08001894
1895 if source_context is None:
1896 raise Exception("A source context must be provided "
1897 "(when parsing %s)" % (conf,))
1898
James E. Blaird8e778f2015-12-22 14:09:20 -08001899 for item in conf:
1900 if not isinstance(item, dict):
1901 raise Exception("Configuration items must be in the form of "
1902 "a list of dictionaries (when parsing %s)" %
1903 (conf,))
1904 if len(item.keys()) > 1:
1905 raise Exception("Configuration item dictionaries must have "
1906 "a single key (when parsing %s)" %
1907 (conf,))
1908 key, value = item.items()[0]
James E. Blair66b274e2017-01-31 14:47:52 -08001909 if key in ['project', 'project-template', 'job']:
James E. Blaircdab2032017-02-01 09:09:29 -08001910 value['_source_context'] = source_context
James E. Blair66b274e2017-01-31 14:47:52 -08001911 if key == 'project':
1912 self.projects.append(value)
1913 elif key == 'job':
James E. Blaird8e778f2015-12-22 14:09:20 -08001914 self.jobs.append(value)
1915 elif key == 'project-template':
1916 self.project_templates.append(value)
1917 elif key == 'pipeline':
1918 self.pipelines.append(value)
James E. Blaira98340f2016-09-02 11:33:49 -07001919 elif key == 'nodeset':
1920 self.nodesets.append(value)
James E. Blaird8e778f2015-12-22 14:09:20 -08001921 else:
Morgan Fainberg4245a422016-08-05 16:20:12 -07001922 raise Exception("Configuration item `%s` not recognized "
James E. Blaird8e778f2015-12-22 14:09:20 -08001923 "(when parsing %s)" %
Morgan Fainberg4245a422016-08-05 16:20:12 -07001924 (item, conf,))
James E. Blaird8e778f2015-12-22 14:09:20 -08001925
1926
James E. Blaireff88162013-07-01 12:44:14 -04001927class Layout(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001928 """Holds all of the Pipelines."""
1929
James E. Blaireff88162013-07-01 12:44:14 -04001930 def __init__(self):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001931 self.tenant = None
James E. Blairb97ed802015-12-21 15:55:35 -08001932 self.project_configs = {}
1933 self.project_templates = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07001934 self.pipelines = OrderedDict()
James E. Blair83005782015-12-11 14:46:03 -08001935 # This is a dictionary of name -> [jobs]. The first element
1936 # of the list is the first job added with that name. It is
1937 # the reference definition for a given job. Subsequent
1938 # elements are aspects of that job with different matchers
1939 # that override some attribute of the job. These aspects all
1940 # inherit from the reference definition.
James E. Blairfef88ec2016-11-30 08:38:27 -08001941 self.jobs = {'noop': [Job('noop')]}
James E. Blaira98340f2016-09-02 11:33:49 -07001942 self.nodesets = {}
James E. Blaireff88162013-07-01 12:44:14 -04001943
1944 def getJob(self, name):
1945 if name in self.jobs:
James E. Blair83005782015-12-11 14:46:03 -08001946 return self.jobs[name][0]
James E. Blairb97ed802015-12-21 15:55:35 -08001947 raise Exception("Job %s not defined" % (name,))
James E. Blair83005782015-12-11 14:46:03 -08001948
1949 def getJobs(self, name):
1950 return self.jobs.get(name, [])
1951
1952 def addJob(self, job):
James E. Blair4317e9f2016-07-15 10:05:47 -07001953 # We can have multiple variants of a job all with the same
1954 # name, but these variants must all be defined in the same repo.
James E. Blaircdab2032017-02-01 09:09:29 -08001955 prior_jobs = [j for j in self.getJobs(job.name) if
1956 j.source_context.project !=
1957 job.source_context.project]
James E. Blair4317e9f2016-07-15 10:05:47 -07001958 if prior_jobs:
1959 raise Exception("Job %s in %s is not permitted to shadow "
James E. Blaircdab2032017-02-01 09:09:29 -08001960 "job %s in %s" % (
1961 job,
1962 job.source_context.project,
1963 prior_jobs[0],
1964 prior_jobs[0].source_context.project))
James E. Blair4317e9f2016-07-15 10:05:47 -07001965
James E. Blair83005782015-12-11 14:46:03 -08001966 if job.name in self.jobs:
1967 self.jobs[job.name].append(job)
1968 else:
1969 self.jobs[job.name] = [job]
1970
James E. Blaira98340f2016-09-02 11:33:49 -07001971 def addNodeSet(self, nodeset):
1972 if nodeset.name in self.nodesets:
1973 raise Exception("NodeSet %s already defined" % (nodeset.name,))
1974 self.nodesets[nodeset.name] = nodeset
1975
James E. Blair83005782015-12-11 14:46:03 -08001976 def addPipeline(self, pipeline):
1977 self.pipelines[pipeline.name] = pipeline
James E. Blair59fdbac2015-12-07 17:08:06 -08001978
James E. Blairb97ed802015-12-21 15:55:35 -08001979 def addProjectTemplate(self, project_template):
1980 self.project_templates[project_template.name] = project_template
1981
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001982 def addProjectConfig(self, project_config, update_pipeline=True):
James E. Blairb97ed802015-12-21 15:55:35 -08001983 self.project_configs[project_config.name] = project_config
1984 # TODOv3(jeblair): tidy up the relationship between pipelines
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001985 # and projects and projectconfigs. Specifically, move
1986 # job_trees out of the pipeline since they are more dynamic
1987 # than pipelines. Remove the update_pipeline argument
1988 if not update_pipeline:
1989 return
James E. Blairb97ed802015-12-21 15:55:35 -08001990 for pipeline_name, pipeline_config in project_config.pipelines.items():
1991 pipeline = self.pipelines[pipeline_name]
1992 project = pipeline.source.getProject(project_config.name)
1993 pipeline.job_trees[project] = pipeline_config.job_tree
1994
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001995 def _createJobTree(self, change, job_trees, parent):
1996 for tree in job_trees:
1997 job = tree.job
1998 if not job.changeMatches(change):
1999 continue
2000 frozen_job = Job(job.name)
2001 frozen_tree = JobTree(frozen_job)
2002 inherited = set()
2003 for variant in self.getJobs(job.name):
2004 if variant.changeMatches(change):
2005 if variant not in inherited:
James E. Blair72ae9da2017-02-03 14:21:30 -08002006 frozen_job.inheritFrom(variant,
2007 'variant while freezing')
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002008 inherited.add(variant)
James E. Blair6e85c2b2016-11-21 16:47:01 -08002009 if not inherited:
2010 # A change must match at least one defined job variant
2011 # (that is to say that it must match more than just
2012 # the job that is defined in the tree).
2013 continue
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002014 if job not in inherited:
2015 # Only update from the job in the tree if it is
2016 # unique, otherwise we might unset an attribute we
2017 # have overloaded.
James E. Blair72ae9da2017-02-03 14:21:30 -08002018 frozen_job.inheritFrom(job, 'tree job while freezing')
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002019 parent.job_trees.append(frozen_tree)
2020 self._createJobTree(change, tree.job_trees, frozen_tree)
2021
2022 def createJobTree(self, item):
Paul Belanger160cb8e2016-11-11 19:04:24 -05002023 project_config = self.project_configs.get(
2024 item.change.project.name, None)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002025 ret = JobTree(None)
Paul Belanger15e3e202016-10-14 16:27:34 -04002026 # NOTE(pabelanger): It is possible for a foreign project not to have a
2027 # configured pipeline, if so return an empty JobTree.
Paul Belanger160cb8e2016-11-11 19:04:24 -05002028 if project_config and item.pipeline.name in project_config.pipelines:
Paul Belanger15e3e202016-10-14 16:27:34 -04002029 project_tree = \
2030 project_config.pipelines[item.pipeline.name].job_tree
2031 self._createJobTree(item.change, project_tree.job_trees, ret)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002032 return ret
2033
James E. Blair59fdbac2015-12-07 17:08:06 -08002034
2035class Tenant(object):
2036 def __init__(self, name):
2037 self.name = name
2038 self.layout = None
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002039 # The list of repos from which we will read main
2040 # configuration. (source, project)
2041 self.config_repos = []
2042 # The unparsed config from those repos.
2043 self.config_repos_config = None
2044 # The list of projects from which we will read in-repo
2045 # configuration. (source, project)
2046 self.project_repos = []
2047 # The unparsed config from those repos.
2048 self.project_repos_config = None
James E. Blair59fdbac2015-12-07 17:08:06 -08002049
2050
2051class Abide(object):
2052 def __init__(self):
2053 self.tenants = OrderedDict()
James E. Blairce8a2132016-05-19 15:21:52 -07002054
2055
2056class JobTimeData(object):
2057 format = 'B10H10H10B'
2058 version = 0
2059
2060 def __init__(self, path):
2061 self.path = path
2062 self.success_times = [0 for x in range(10)]
2063 self.failure_times = [0 for x in range(10)]
2064 self.results = [0 for x in range(10)]
2065
2066 def load(self):
2067 if not os.path.exists(self.path):
2068 return
2069 with open(self.path) as f:
2070 data = struct.unpack(self.format, f.read())
2071 version = data[0]
2072 if version != self.version:
2073 raise Exception("Unkown data version")
2074 self.success_times = list(data[1:11])
2075 self.failure_times = list(data[11:21])
2076 self.results = list(data[21:32])
2077
2078 def save(self):
2079 tmpfile = self.path + '.tmp'
2080 data = [self.version]
2081 data.extend(self.success_times)
2082 data.extend(self.failure_times)
2083 data.extend(self.results)
2084 data = struct.pack(self.format, *data)
2085 with open(tmpfile, 'w') as f:
2086 f.write(data)
2087 os.rename(tmpfile, self.path)
2088
2089 def add(self, elapsed, result):
2090 elapsed = int(elapsed)
2091 if result == 'SUCCESS':
2092 self.success_times.append(elapsed)
2093 self.success_times.pop(0)
2094 result = 0
2095 else:
2096 self.failure_times.append(elapsed)
2097 self.failure_times.pop(0)
2098 result = 1
2099 self.results.append(result)
2100 self.results.pop(0)
2101
2102 def getEstimatedTime(self):
2103 times = [x for x in self.success_times if x]
2104 if times:
2105 return float(sum(times)) / len(times)
2106 return 0.0
2107
2108
2109class TimeDataBase(object):
2110 def __init__(self, root):
2111 self.root = root
2112 self.jobs = {}
2113
2114 def _getTD(self, name):
2115 td = self.jobs.get(name)
2116 if not td:
2117 td = JobTimeData(os.path.join(self.root, name))
2118 self.jobs[name] = td
2119 td.load()
2120 return td
2121
2122 def getEstimatedTime(self, name):
2123 return self._getTD(name).getEstimatedTime()
2124
2125 def update(self, name, elapsed, result):
2126 td = self._getTD(name)
2127 td.add(elapsed, result)
2128 td.save()