blob: ac3a28613b8eefb53186337533f775c5e5fd5dfd [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. Blair5ac93842017-01-20 06:47:34 -080015import abc
James E. Blair1b265312014-06-24 09:35:21 -070016import copy
James E. Blairce8a2132016-05-19 15:21:52 -070017import os
James E. Blairee743612012-05-29 14:49:32 -070018import re
James E. Blairce8a2132016-05-19 15:21:52 -070019import struct
James E. Blairff986a12012-05-30 14:56:51 -070020import time
James E. Blair4886cc12012-07-18 15:39:41 -070021from uuid import uuid4
James E. Blair5a9918a2013-08-27 10:06:27 -070022import extras
23
James E. Blair5ac93842017-01-20 06:47:34 -080024import six
25
James E. Blair5a9918a2013-08-27 10:06:27 -070026OrderedDict = extras.try_imports(['collections.OrderedDict',
27 'ordereddict.OrderedDict'])
James E. Blair4886cc12012-07-18 15:39:41 -070028
29
K Jonathan Harkerf95e7232015-04-29 13:33:16 -070030EMPTY_GIT_REF = '0' * 40 # git sha of all zeros, used during creates/deletes
31
James E. Blair19deff22013-08-25 13:17:35 -070032MERGER_MERGE = 1 # "git merge"
33MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
34MERGER_CHERRY_PICK = 3 # "git cherry-pick"
35
36MERGER_MAP = {
37 'merge': MERGER_MERGE,
38 'merge-resolve': MERGER_MERGE_RESOLVE,
39 'cherry-pick': MERGER_CHERRY_PICK,
40}
James E. Blairee743612012-05-29 14:49:32 -070041
James E. Blair64ed6f22013-07-10 14:07:23 -070042PRECEDENCE_NORMAL = 0
43PRECEDENCE_LOW = 1
44PRECEDENCE_HIGH = 2
45
46PRECEDENCE_MAP = {
47 None: PRECEDENCE_NORMAL,
48 'low': PRECEDENCE_LOW,
49 'normal': PRECEDENCE_NORMAL,
50 'high': PRECEDENCE_HIGH,
51}
52
James E. Blair803e94f2017-01-06 09:18:59 -080053# Request states
54STATE_REQUESTED = 'requested'
55STATE_PENDING = 'pending'
56STATE_FULFILLED = 'fulfilled'
57STATE_FAILED = 'failed'
58REQUEST_STATES = set([STATE_REQUESTED,
59 STATE_PENDING,
60 STATE_FULFILLED,
61 STATE_FAILED])
62
63# Node states
64STATE_BUILDING = 'building'
65STATE_TESTING = 'testing'
66STATE_READY = 'ready'
67STATE_IN_USE = 'in-use'
68STATE_USED = 'used'
69STATE_HOLD = 'hold'
70STATE_DELETING = 'deleting'
71NODE_STATES = set([STATE_BUILDING,
72 STATE_TESTING,
73 STATE_READY,
74 STATE_IN_USE,
75 STATE_USED,
76 STATE_HOLD,
77 STATE_DELETING])
78
James E. Blair1e8dd892012-05-30 09:15:05 -070079
James E. Blairc053d022014-01-22 14:57:33 -080080def time_to_seconds(s):
81 if s.endswith('s'):
82 return int(s[:-1])
83 if s.endswith('m'):
84 return int(s[:-1]) * 60
85 if s.endswith('h'):
86 return int(s[:-1]) * 60 * 60
87 if s.endswith('d'):
88 return int(s[:-1]) * 24 * 60 * 60
89 if s.endswith('w'):
90 return int(s[:-1]) * 7 * 24 * 60 * 60
91 raise Exception("Unable to parse time value: %s" % s)
92
93
James E. Blair11041d22014-05-02 14:49:53 -070094def normalizeCategory(name):
95 name = name.lower()
96 return re.sub(' ', '-', name)
97
98
James E. Blair4aea70c2012-07-26 14:23:24 -070099class Pipeline(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700100 """A configuration that ties triggers, reporters, managers and sources.
101
Monty Taylor82dfd412016-07-29 12:01:28 -0700102 Source
103 Where changes should come from. It is a named connection to
Monty Taylora42a55b2016-07-29 07:53:33 -0700104 an external service defined in zuul.conf
Monty Taylor82dfd412016-07-29 12:01:28 -0700105
106 Trigger
107 A description of which events should be processed
108
109 Manager
110 Responsible for enqueing and dequeing Changes
111
112 Reporter
113 Communicates success and failure results somewhere
Monty Taylora42a55b2016-07-29 07:53:33 -0700114 """
James E. Blair83005782015-12-11 14:46:03 -0800115 def __init__(self, name, layout):
James E. Blair4aea70c2012-07-26 14:23:24 -0700116 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800117 self.layout = layout
James E. Blair8dbd56a2012-12-22 10:55:10 -0800118 self.description = None
James E. Blair56370192013-01-14 15:47:28 -0800119 self.failure_message = None
Joshua Heskethb7179772014-01-30 23:30:46 +1100120 self.merge_failure_message = None
James E. Blair56370192013-01-14 15:47:28 -0800121 self.success_message = None
Joshua Hesketh3979e3e2014-03-04 11:21:10 +1100122 self.footer_message = None
James E. Blair60af7f42016-03-11 16:11:06 -0800123 self.start_message = None
James E. Blair2fa50962013-01-30 21:50:41 -0800124 self.dequeue_on_new_patchset = True
James E. Blair17dd6772015-02-09 14:45:18 -0800125 self.ignore_dependencies = False
James E. Blair4aea70c2012-07-26 14:23:24 -0700126 self.job_trees = {} # project -> JobTree
127 self.manager = None
James E. Blaire0487072012-08-29 17:38:31 -0700128 self.queues = []
James E. Blair64ed6f22013-07-10 14:07:23 -0700129 self.precedence = PRECEDENCE_NORMAL
James E. Blairc0dedf82014-08-06 09:37:52 -0700130 self.source = None
James E. Blair83005782015-12-11 14:46:03 -0800131 self.triggers = []
Joshua Hesketh352264b2015-08-11 23:42:08 +1000132 self.start_actions = []
133 self.success_actions = []
134 self.failure_actions = []
135 self.merge_failure_actions = []
136 self.disabled_actions = []
Joshua Hesketh89e829d2015-02-10 16:29:45 +1100137 self.disable_at = None
138 self._consecutive_failures = 0
139 self._disabled = False
Clark Boylan7603a372014-01-21 11:43:20 -0800140 self.window = None
141 self.window_floor = None
142 self.window_increase_type = None
143 self.window_increase_factor = None
144 self.window_decrease_type = None
145 self.window_decrease_factor = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700146
James E. Blair83005782015-12-11 14:46:03 -0800147 @property
148 def actions(self):
149 return (
150 self.start_actions +
151 self.success_actions +
152 self.failure_actions +
153 self.merge_failure_actions +
154 self.disabled_actions
155 )
156
James E. Blaird09c17a2012-08-07 09:23:14 -0700157 def __repr__(self):
158 return '<Pipeline %s>' % self.name
159
James E. Blair4aea70c2012-07-26 14:23:24 -0700160 def setManager(self, manager):
161 self.manager = manager
162
James E. Blair4aea70c2012-07-26 14:23:24 -0700163 def getProjects(self):
Monty Taylor74fa3862016-06-02 07:39:49 +0300164 # cmp is not in python3, applied idiom from
165 # http://python-future.org/compatible_idioms.html#cmp
166 return sorted(
167 self.job_trees.keys(),
168 key=lambda p: p.name)
James E. Blair4aea70c2012-07-26 14:23:24 -0700169
James E. Blaire0487072012-08-29 17:38:31 -0700170 def addQueue(self, queue):
171 self.queues.append(queue)
172
173 def getQueue(self, project):
174 for queue in self.queues:
175 if project in queue.projects:
176 return queue
177 return None
178
James E. Blairbfb8e042014-12-30 17:01:44 -0800179 def removeQueue(self, queue):
180 self.queues.remove(queue)
181
James E. Blair4aea70c2012-07-26 14:23:24 -0700182 def getJobTree(self, project):
183 tree = self.job_trees.get(project)
184 return tree
185
James E. Blaire0487072012-08-29 17:38:31 -0700186 def getChangesInQueue(self):
187 changes = []
188 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700189 changes.extend([x.change for x in shared_queue.queue])
James E. Blaire0487072012-08-29 17:38:31 -0700190 return changes
191
James E. Blairfee8d652013-06-07 08:57:52 -0700192 def getAllItems(self):
193 items = []
James E. Blaire0487072012-08-29 17:38:31 -0700194 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700195 items.extend(shared_queue.queue)
James E. Blairfee8d652013-06-07 08:57:52 -0700196 return items
James E. Blaire0487072012-08-29 17:38:31 -0700197
James E. Blairb7273ef2016-04-19 08:58:51 -0700198 def formatStatusJSON(self, url_pattern=None):
James E. Blair8dbd56a2012-12-22 10:55:10 -0800199 j_pipeline = dict(name=self.name,
200 description=self.description)
201 j_queues = []
202 j_pipeline['change_queues'] = j_queues
203 for queue in self.queues:
204 j_queue = dict(name=queue.name)
205 j_queues.append(j_queue)
206 j_queue['heads'] = []
Clark Boylanaf2476f2014-01-23 14:47:36 -0800207 j_queue['window'] = queue.window
James E. Blair972e3c72013-08-29 12:04:55 -0700208
209 j_changes = []
210 for e in queue.queue:
211 if not e.item_ahead:
212 if j_changes:
213 j_queue['heads'].append(j_changes)
214 j_changes = []
James E. Blairb7273ef2016-04-19 08:58:51 -0700215 j_changes.append(e.formatJSON(url_pattern))
James E. Blair972e3c72013-08-29 12:04:55 -0700216 if (len(j_changes) > 1 and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000217 (j_changes[-2]['remaining_time'] is not None) and
218 (j_changes[-1]['remaining_time'] is not None)):
James E. Blair972e3c72013-08-29 12:04:55 -0700219 j_changes[-1]['remaining_time'] = max(
220 j_changes[-2]['remaining_time'],
221 j_changes[-1]['remaining_time'])
222 if j_changes:
James E. Blair8dbd56a2012-12-22 10:55:10 -0800223 j_queue['heads'].append(j_changes)
224 return j_pipeline
225
James E. Blair4aea70c2012-07-26 14:23:24 -0700226
James E. Blairee743612012-05-29 14:49:32 -0700227class ChangeQueue(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700228 """A ChangeQueue contains Changes to be processed related projects.
229
Monty Taylor82dfd412016-07-29 12:01:28 -0700230 A Pipeline with a DependentPipelineManager has multiple parallel
231 ChangeQueues shared by different projects. For instance, there may a
232 ChangeQueue shared by interrelated projects foo and bar, and a second queue
233 for independent project baz.
Monty Taylora42a55b2016-07-29 07:53:33 -0700234
Monty Taylor82dfd412016-07-29 12:01:28 -0700235 A Pipeline with an IndependentPipelineManager puts every Change into its
236 own ChangeQueue
Monty Taylora42a55b2016-07-29 07:53:33 -0700237
238 The ChangeQueue Window is inspired by TCP windows and controlls how many
239 Changes in a given ChangeQueue will be considered active and ready to
240 be processed. If a Change succeeds, the Window is increased by
241 `window_increase_factor`. If a Change fails, the Window is decreased by
242 `window_decrease_factor`.
243 """
James E. Blairbfb8e042014-12-30 17:01:44 -0800244 def __init__(self, pipeline, window=0, window_floor=1,
Clark Boylan7603a372014-01-21 11:43:20 -0800245 window_increase_type='linear', window_increase_factor=1,
James E. Blair0dcef7a2016-08-19 09:35:17 -0700246 window_decrease_type='exponential', window_decrease_factor=2,
247 name=None):
James E. Blair4aea70c2012-07-26 14:23:24 -0700248 self.pipeline = pipeline
James E. Blair0dcef7a2016-08-19 09:35:17 -0700249 if name:
250 self.name = name
251 else:
252 self.name = ''
James E. Blairee743612012-05-29 14:49:32 -0700253 self.projects = []
254 self._jobs = set()
255 self.queue = []
Clark Boylan7603a372014-01-21 11:43:20 -0800256 self.window = window
257 self.window_floor = window_floor
258 self.window_increase_type = window_increase_type
259 self.window_increase_factor = window_increase_factor
260 self.window_decrease_type = window_decrease_type
261 self.window_decrease_factor = window_decrease_factor
James E. Blairee743612012-05-29 14:49:32 -0700262
James E. Blair9f9667e2012-06-12 17:51:08 -0700263 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700264 return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
James E. Blairee743612012-05-29 14:49:32 -0700265
266 def getJobs(self):
267 return self._jobs
268
269 def addProject(self, project):
270 if project not in self.projects:
271 self.projects.append(project)
James E. Blairc8a1e052014-02-25 09:29:26 -0800272
James E. Blair0dcef7a2016-08-19 09:35:17 -0700273 if not self.name:
274 self.name = project.name
James E. Blairee743612012-05-29 14:49:32 -0700275
276 def enqueueChange(self, change):
James E. Blairbfb8e042014-12-30 17:01:44 -0800277 item = QueueItem(self, change)
James E. Blaircdccd972013-07-01 12:10:22 -0700278 self.enqueueItem(item)
279 item.enqueue_time = time.time()
280 return item
281
282 def enqueueItem(self, item):
James E. Blair4a035d92014-01-23 13:10:48 -0800283 item.pipeline = self.pipeline
James E. Blairbfb8e042014-12-30 17:01:44 -0800284 item.queue = self
285 if self.queue:
James E. Blairfee8d652013-06-07 08:57:52 -0700286 item.item_ahead = self.queue[-1]
James E. Blair972e3c72013-08-29 12:04:55 -0700287 item.item_ahead.items_behind.append(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700288 self.queue.append(item)
James E. Blairee743612012-05-29 14:49:32 -0700289
James E. Blairfee8d652013-06-07 08:57:52 -0700290 def dequeueItem(self, item):
291 if item in self.queue:
292 self.queue.remove(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700293 if item.item_ahead:
James E. Blair972e3c72013-08-29 12:04:55 -0700294 item.item_ahead.items_behind.remove(item)
295 for item_behind in item.items_behind:
296 if item.item_ahead:
297 item.item_ahead.items_behind.append(item_behind)
298 item_behind.item_ahead = item.item_ahead
James E. Blairfee8d652013-06-07 08:57:52 -0700299 item.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700300 item.items_behind = []
James E. Blairfee8d652013-06-07 08:57:52 -0700301 item.dequeue_time = time.time()
James E. Blaire0487072012-08-29 17:38:31 -0700302
James E. Blair972e3c72013-08-29 12:04:55 -0700303 def moveItem(self, item, item_ahead):
James E. Blair972e3c72013-08-29 12:04:55 -0700304 if item.item_ahead == item_ahead:
305 return False
306 # Remove from current location
307 if item.item_ahead:
308 item.item_ahead.items_behind.remove(item)
309 for item_behind in item.items_behind:
310 if item.item_ahead:
311 item.item_ahead.items_behind.append(item_behind)
312 item_behind.item_ahead = item.item_ahead
313 # Add to new location
314 item.item_ahead = item_ahead
James E. Blair00451262013-09-20 11:40:17 -0700315 item.items_behind = []
James E. Blair972e3c72013-08-29 12:04:55 -0700316 if item.item_ahead:
317 item.item_ahead.items_behind.append(item)
318 return True
James E. Blairee743612012-05-29 14:49:32 -0700319
320 def mergeChangeQueue(self, other):
321 for project in other.projects:
322 self.addProject(project)
Clark Boylan7603a372014-01-21 11:43:20 -0800323 self.window = min(self.window, other.window)
324 # TODO merge semantics
325
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800326 def isActionable(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800327 if self.window:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800328 return item in self.queue[:self.window]
Clark Boylan7603a372014-01-21 11:43:20 -0800329 else:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800330 return True
Clark Boylan7603a372014-01-21 11:43:20 -0800331
332 def increaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800333 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800334 if self.window_increase_type == 'linear':
335 self.window += self.window_increase_factor
336 elif self.window_increase_type == 'exponential':
337 self.window *= self.window_increase_factor
338
339 def decreaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800340 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800341 if self.window_decrease_type == 'linear':
342 self.window = max(
343 self.window_floor,
344 self.window - self.window_decrease_factor)
345 elif self.window_decrease_type == 'exponential':
346 self.window = max(
347 self.window_floor,
Morgan Fainbergfdae2252016-06-07 20:13:20 -0700348 int(self.window / self.window_decrease_factor))
James E. Blairee743612012-05-29 14:49:32 -0700349
James E. Blair1e8dd892012-05-30 09:15:05 -0700350
James E. Blair4aea70c2012-07-26 14:23:24 -0700351class Project(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700352 """A Project represents a git repository such as openstack/nova."""
353
James E. Blaircf440a22016-07-15 09:11:58 -0700354 # NOTE: Projects should only be instantiated via a Source object
355 # so that they are associated with and cached by their Connection.
356 # This makes a Project instance a unique identifier for a given
357 # project from a given source.
358
James E. Blairc73c73a2017-01-20 15:15:15 -0800359 def __init__(self, name, connection_name, foreign=False):
James E. Blair4aea70c2012-07-26 14:23:24 -0700360 self.name = name
James E. Blairc73c73a2017-01-20 15:15:15 -0800361 self.connection_name = connection_name
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000362 # foreign projects are those referenced in dependencies
363 # of layout projects, this should matter
364 # when deciding whether to enqueue their changes
James E. Blaircf440a22016-07-15 09:11:58 -0700365 # TODOv3 (jeblair): re-add support for foreign projects if needed
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000366 self.foreign = foreign
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700367 self.unparsed_config = None
James E. Blaire3162022017-02-20 16:47:27 -0500368 self.unparsed_branch_config = {} # branch -> UnparsedTenantConfig
James E. Blair4aea70c2012-07-26 14:23:24 -0700369
370 def __str__(self):
371 return self.name
372
373 def __repr__(self):
374 return '<Project %s>' % (self.name)
375
376
James E. Blair34776ee2016-08-25 13:53:54 -0700377class Node(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700378 """A single node for use by a job.
379
380 This may represent a request for a node, or an actual node
381 provided by Nodepool.
382 """
383
James E. Blair34776ee2016-08-25 13:53:54 -0700384 def __init__(self, name, image):
385 self.name = name
386 self.image = image
James E. Blaircbf43672017-01-04 14:33:41 -0800387 self.id = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800388 self.lock = None
389 # Attributes from Nodepool
390 self._state = 'unknown'
391 self.state_time = time.time()
392 self.public_ipv4 = None
393 self.private_ipv4 = None
394 self.public_ipv6 = None
James E. Blaircacdf2b2017-01-04 13:14:37 -0800395 self._keys = []
James E. Blaira38c28e2017-01-04 10:33:20 -0800396
397 @property
398 def state(self):
399 return self._state
400
401 @state.setter
402 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800403 if value not in NODE_STATES:
404 raise TypeError("'%s' is not a valid state" % value)
James E. Blaira38c28e2017-01-04 10:33:20 -0800405 self._state = value
406 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700407
408 def __repr__(self):
James E. Blaircbf43672017-01-04 14:33:41 -0800409 return '<Node %s %s:%s>' % (self.id, self.name, self.image)
James E. Blair34776ee2016-08-25 13:53:54 -0700410
James E. Blair0d952152017-02-07 17:14:44 -0800411 def __ne__(self, other):
412 return not self.__eq__(other)
413
414 def __eq__(self, other):
415 if not isinstance(other, Node):
416 return False
417 return (self.name == other.name and
418 self.image == other.image and
419 self.id == other.id)
420
James E. Blaircacdf2b2017-01-04 13:14:37 -0800421 def toDict(self):
422 d = {}
423 d['state'] = self.state
424 for k in self._keys:
425 d[k] = getattr(self, k)
426 return d
427
James E. Blaira38c28e2017-01-04 10:33:20 -0800428 def updateFromDict(self, data):
429 self._state = data['state']
James E. Blaircacdf2b2017-01-04 13:14:37 -0800430 keys = []
431 for k, v in data.items():
432 if k == 'state':
433 continue
434 keys.append(k)
435 setattr(self, k, v)
436 self._keys = keys
James E. Blaira38c28e2017-01-04 10:33:20 -0800437
James E. Blair34776ee2016-08-25 13:53:54 -0700438
James E. Blaira98340f2016-09-02 11:33:49 -0700439class NodeSet(object):
440 """A set of nodes.
441
442 In configuration, NodeSets are attributes of Jobs indicating that
443 a Job requires nodes matching this description.
444
445 They may appear as top-level configuration objects and be named,
446 or they may appears anonymously in in-line job definitions.
447 """
448
449 def __init__(self, name=None):
450 self.name = name or ''
451 self.nodes = OrderedDict()
452
James E. Blair1774dd52017-02-03 10:52:32 -0800453 def __ne__(self, other):
454 return not self.__eq__(other)
455
456 def __eq__(self, other):
457 if not isinstance(other, NodeSet):
458 return False
459 return (self.name == other.name and
460 self.nodes == other.nodes)
461
James E. Blaircbf43672017-01-04 14:33:41 -0800462 def copy(self):
463 n = NodeSet(self.name)
464 for name, node in self.nodes.items():
465 n.addNode(Node(node.name, node.image))
466 return n
467
James E. Blaira98340f2016-09-02 11:33:49 -0700468 def addNode(self, node):
469 if node.name in self.nodes:
470 raise Exception("Duplicate node in %s" % (self,))
471 self.nodes[node.name] = node
472
James E. Blair0eaad552016-09-02 12:09:54 -0700473 def getNodes(self):
474 return self.nodes.values()
475
James E. Blaira98340f2016-09-02 11:33:49 -0700476 def __repr__(self):
477 if self.name:
478 name = self.name + ' '
479 else:
480 name = ''
481 return '<NodeSet %s%s>' % (name, self.nodes)
482
483
James E. Blair34776ee2016-08-25 13:53:54 -0700484class NodeRequest(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700485 """A request for a set of nodes."""
486
James E. Blair0eaad552016-09-02 12:09:54 -0700487 def __init__(self, build_set, job, nodeset):
James E. Blair34776ee2016-08-25 13:53:54 -0700488 self.build_set = build_set
489 self.job = job
James E. Blair0eaad552016-09-02 12:09:54 -0700490 self.nodeset = nodeset
James E. Blair803e94f2017-01-06 09:18:59 -0800491 self._state = STATE_REQUESTED
James E. Blairdce6cea2016-12-20 16:45:32 -0800492 self.state_time = time.time()
493 self.stat = None
494 self.uid = uuid4().hex
James E. Blaircbf43672017-01-04 14:33:41 -0800495 self.id = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800496 # Zuul internal failure flag (not stored in ZK so it's not
497 # overwritten).
498 self.failed = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800499
500 @property
James E. Blair6ab79e02017-01-06 10:10:17 -0800501 def fulfilled(self):
502 return (self._state == STATE_FULFILLED) and not self.failed
503
504 @property
James E. Blairdce6cea2016-12-20 16:45:32 -0800505 def state(self):
506 return self._state
507
508 @state.setter
509 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800510 if value not in REQUEST_STATES:
511 raise TypeError("'%s' is not a valid state" % value)
James E. Blairdce6cea2016-12-20 16:45:32 -0800512 self._state = value
513 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700514
515 def __repr__(self):
James E. Blaircbf43672017-01-04 14:33:41 -0800516 return '<NodeRequest %s %s>' % (self.id, self.nodeset)
James E. Blair34776ee2016-08-25 13:53:54 -0700517
James E. Blairdce6cea2016-12-20 16:45:32 -0800518 def toDict(self):
519 d = {}
520 nodes = [n.image for n in self.nodeset.getNodes()]
521 d['node_types'] = nodes
522 d['requestor'] = 'zuul' # TODOv3(jeblair): better descriptor
523 d['state'] = self.state
524 d['state_time'] = self.state_time
525 return d
526
527 def updateFromDict(self, data):
528 self._state = data['state']
529 self.state_time = data['state_time']
530
James E. Blair34776ee2016-08-25 13:53:54 -0700531
James E. Blaircdab2032017-02-01 09:09:29 -0800532class SourceContext(object):
533 """A reference to the branch of a project in configuration.
534
535 Jobs and playbooks reference this to keep track of where they
536 originate."""
537
Monty Taylore6562aa2017-02-20 07:37:39 -0500538 def __init__(self, project, branch, trusted):
James E. Blaircdab2032017-02-01 09:09:29 -0800539 self.project = project
540 self.branch = branch
Monty Taylore6562aa2017-02-20 07:37:39 -0500541 self.trusted = trusted
James E. Blaircdab2032017-02-01 09:09:29 -0800542
543 def __repr__(self):
Monty Taylore6562aa2017-02-20 07:37:39 -0500544 return '<SourceContext %s:%s trusted:%s>' % (self.project,
545 self.branch,
546 self.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800547
James E. Blaira7f51ca2017-02-07 16:01:26 -0800548 def __deepcopy__(self, memo):
549 return self.copy()
550
551 def copy(self):
Monty Taylore6562aa2017-02-20 07:37:39 -0500552 return self.__class__(self.project, self.branch, self.trusted)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800553
James E. Blaircdab2032017-02-01 09:09:29 -0800554 def __ne__(self, other):
555 return not self.__eq__(other)
556
557 def __eq__(self, other):
558 if not isinstance(other, SourceContext):
559 return False
560 return (self.project == other.project and
561 self.branch == other.branch and
Monty Taylore6562aa2017-02-20 07:37:39 -0500562 self.trusted == other.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800563
564
James E. Blair66b274e2017-01-31 14:47:52 -0800565class PlaybookContext(object):
James E. Blaircdab2032017-02-01 09:09:29 -0800566
James E. Blair66b274e2017-01-31 14:47:52 -0800567 """A reference to a playbook in the context of a project.
568
569 Jobs refer to objects of this class for their main, pre, and post
570 playbooks so that we can keep track of which repos and security
571 contexts are needed in order to run them."""
572
James E. Blaircdab2032017-02-01 09:09:29 -0800573 def __init__(self, source_context, path):
574 self.source_context = source_context
James E. Blair66b274e2017-01-31 14:47:52 -0800575 self.path = path
James E. Blair66b274e2017-01-31 14:47:52 -0800576
577 def __repr__(self):
James E. Blaircdab2032017-02-01 09:09:29 -0800578 return '<PlaybookContext %s %s>' % (self.source_context,
579 self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800580
581 def __ne__(self, other):
582 return not self.__eq__(other)
583
584 def __eq__(self, other):
585 if not isinstance(other, PlaybookContext):
586 return False
James E. Blaircdab2032017-02-01 09:09:29 -0800587 return (self.source_context == other.source_context and
588 self.path == other.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800589
590 def toDict(self):
591 # Render to a dict to use in passing json to the launcher
592 return dict(
James E. Blaircdab2032017-02-01 09:09:29 -0800593 connection=self.source_context.project.connection_name,
594 project=self.source_context.project.name,
595 branch=self.source_context.branch,
Monty Taylore6562aa2017-02-20 07:37:39 -0500596 trusted=self.source_context.trusted,
James E. Blaircdab2032017-02-01 09:09:29 -0800597 path=self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800598
599
James E. Blair5ac93842017-01-20 06:47:34 -0800600@six.add_metaclass(abc.ABCMeta)
601class Role(object):
602 """A reference to an ansible role."""
603
604 def __init__(self, target_name):
605 self.target_name = target_name
606
607 @abc.abstractmethod
608 def __repr__(self):
609 pass
610
611 def __ne__(self, other):
612 return not self.__eq__(other)
613
614 @abc.abstractmethod
615 def __eq__(self, other):
616 if not isinstance(other, Role):
617 return False
618 return (self.target_name == other.target_name)
619
620 @abc.abstractmethod
621 def toDict(self):
622 # Render to a dict to use in passing json to the launcher
623 return dict(target_name=self.target_name)
624
625
626class ZuulRole(Role):
627 """A reference to an ansible role in a Zuul project."""
628
Monty Taylore6562aa2017-02-20 07:37:39 -0500629 def __init__(self, target_name, connection_name, project_name, trusted):
James E. Blair5ac93842017-01-20 06:47:34 -0800630 super(ZuulRole, self).__init__(target_name)
631 self.connection_name = connection_name
632 self.project_name = project_name
Monty Taylore6562aa2017-02-20 07:37:39 -0500633 self.trusted = trusted
James E. Blair5ac93842017-01-20 06:47:34 -0800634
635 def __repr__(self):
636 return '<ZuulRole %s %s>' % (self.project_name, self.target_name)
637
638 def __eq__(self, other):
639 if not isinstance(other, ZuulRole):
640 return False
641 return (super(ZuulRole, self).__eq__(other) and
642 self.connection_name == other.connection_name,
643 self.project_name == other.project_name,
Monty Taylore6562aa2017-02-20 07:37:39 -0500644 self.trusted == other.trusted)
James E. Blair5ac93842017-01-20 06:47:34 -0800645
646 def toDict(self):
647 # Render to a dict to use in passing json to the launcher
648 d = super(ZuulRole, self).toDict()
649 d['type'] = 'zuul'
650 d['connection'] = self.connection_name
651 d['project'] = self.project_name
Monty Taylore6562aa2017-02-20 07:37:39 -0500652 d['trusted'] = self.trusted
James E. Blair5ac93842017-01-20 06:47:34 -0800653 return d
654
655
James E. Blairee743612012-05-29 14:49:32 -0700656class Job(object):
James E. Blair66b274e2017-01-31 14:47:52 -0800657
James E. Blaira7f51ca2017-02-07 16:01:26 -0800658 """A Job represents the defintion of actions to perform.
659
660 NB: Do not modify attributes of this class, set them directly
661 (e.g., "job.run = ..." rather than "job.run.append(...)").
662 """
Monty Taylora42a55b2016-07-29 07:53:33 -0700663
James E. Blairee743612012-05-29 14:49:32 -0700664 def __init__(self, name):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800665 # These attributes may override even the final form of a job
666 # in the context of a project-pipeline. They can not affect
667 # the execution of the job, but only whether the job is run
668 # and how it is reported.
669 self.context_attributes = dict(
670 voting=True,
671 hold_following_changes=False,
James E. Blair1774dd52017-02-03 10:52:32 -0800672 failure_message=None,
673 success_message=None,
674 failure_url=None,
675 success_url=None,
676 # Matchers. These are separate so they can be individually
677 # overidden.
678 branch_matcher=None,
679 file_matcher=None,
680 irrelevant_file_matcher=None, # skip-if
James E. Blaira7f51ca2017-02-07 16:01:26 -0800681 tags=frozenset(),
James E. Blair1774dd52017-02-03 10:52:32 -0800682 )
683
James E. Blaira7f51ca2017-02-07 16:01:26 -0800684 # These attributes affect how the job is actually run and more
685 # care must be taken when overriding them. If a job is
686 # declared "final", these may not be overriden in a
687 # project-pipeline.
688 self.execution_attributes = dict(
689 timeout=None,
690 # variables={},
691 nodeset=NodeSet(),
692 auth={},
693 workspace=None,
694 pre_run=(),
695 post_run=(),
696 run=(),
697 implied_run=(),
698 mutex=None,
699 attempts=3,
700 final=False,
James E. Blair5ac93842017-01-20 06:47:34 -0800701 roles=frozenset(),
K Jonathan Harker2c1a6232017-02-21 14:34:08 -0800702 repos=frozenset(),
James E. Blaira7f51ca2017-02-07 16:01:26 -0800703 )
704
705 # These are generally internal attributes which are not
706 # accessible via configuration.
707 self.other_attributes = dict(
708 name=None,
709 source_context=None,
710 inheritance_path=(),
711 )
712
713 self.inheritable_attributes = {}
714 self.inheritable_attributes.update(self.context_attributes)
715 self.inheritable_attributes.update(self.execution_attributes)
716 self.attributes = {}
717 self.attributes.update(self.inheritable_attributes)
718 self.attributes.update(self.other_attributes)
719
James E. Blairee743612012-05-29 14:49:32 -0700720 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800721
James E. Blair66b274e2017-01-31 14:47:52 -0800722 def __ne__(self, other):
723 return not self.__eq__(other)
724
Paul Belangere22baea2016-11-03 16:59:27 -0400725 def __eq__(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800726 # Compare the name and all inheritable attributes to determine
727 # whether two jobs with the same name are identically
728 # configured. Useful upon reconfiguration.
729 if not isinstance(other, Job):
730 return False
731 if self.name != other.name:
732 return False
733 for k, v in self.attributes.items():
734 if getattr(self, k) != getattr(other, k):
735 return False
736 return True
James E. Blairee743612012-05-29 14:49:32 -0700737
738 def __str__(self):
739 return self.name
740
741 def __repr__(self):
James E. Blair72ae9da2017-02-03 14:21:30 -0800742 return '<Job %s branches: %s source: %s>' % (self.name,
743 self.branch_matcher,
744 self.source_context)
James E. Blair83005782015-12-11 14:46:03 -0800745
James E. Blaira7f51ca2017-02-07 16:01:26 -0800746 def __getattr__(self, name):
747 v = self.__dict__.get(name)
748 if v is None:
749 return copy.deepcopy(self.attributes[name])
750 return v
751
752 def _get(self, name):
753 return self.__dict__.get(name)
754
755 def setRun(self):
756 if not self.run:
757 self.run = self.implied_run
758
759 def inheritFrom(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800760 """Copy the inheritable attributes which have been set on the other
761 job to this job."""
James E. Blaira7f51ca2017-02-07 16:01:26 -0800762 if not isinstance(other, Job):
763 raise Exception("Job unable to inherit from %s" % (other,))
764
765 do_not_inherit = set()
766 if other.auth and not other.auth.get('inherit'):
767 do_not_inherit.add('auth')
768
769 # copy all attributes
770 for k in self.inheritable_attributes:
771 if (other._get(k) is not None and k not in do_not_inherit):
772 setattr(self, k, copy.deepcopy(getattr(other, k)))
773
774 msg = 'inherit from %s' % (repr(other),)
775 self.inheritance_path = other.inheritance_path + (msg,)
776
777 def copy(self):
778 job = Job(self.name)
779 for k in self.attributes:
780 if self._get(k) is not None:
781 setattr(job, k, copy.deepcopy(self._get(k)))
782 return job
783
784 def applyVariant(self, other):
785 """Copy the attributes which have been set on the other job to this
786 job."""
James E. Blair83005782015-12-11 14:46:03 -0800787
788 if not isinstance(other, Job):
789 raise Exception("Job unable to inherit from %s" % (other,))
James E. Blaira7f51ca2017-02-07 16:01:26 -0800790
791 for k in self.execution_attributes:
792 if (other._get(k) is not None and
793 k not in set(['final'])):
794 if self.final:
795 raise Exception("Unable to modify final job %s attribute "
796 "%s=%s with variant %s" % (
797 repr(self), k, other._get(k),
798 repr(other)))
James E. Blair5ac93842017-01-20 06:47:34 -0800799 if k not in set(['pre_run', 'post_run', 'roles']):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800800 setattr(self, k, copy.deepcopy(other._get(k)))
801
802 # Don't set final above so that we don't trip an error halfway
803 # through assignment.
804 if other.final != self.attributes['final']:
805 self.final = other.final
806
807 if other._get('pre_run') is not None:
808 self.pre_run = self.pre_run + other.pre_run
809 if other._get('post_run') is not None:
810 self.post_run = other.post_run + self.post_run
James E. Blair5ac93842017-01-20 06:47:34 -0800811 if other._get('roles') is not None:
812 self.roles = self.roles.union(other.roles)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800813
814 for k in self.context_attributes:
815 if (other._get(k) is not None and
816 k not in set(['tags'])):
817 setattr(self, k, copy.deepcopy(other._get(k)))
818
819 if other._get('tags') is not None:
820 self.tags = self.tags.union(other.tags)
821
822 msg = 'apply variant %s' % (repr(other),)
823 self.inheritance_path = self.inheritance_path + (msg,)
James E. Blairee743612012-05-29 14:49:32 -0700824
James E. Blaire421a232012-07-25 16:59:21 -0700825 def changeMatches(self, change):
James E. Blair83005782015-12-11 14:46:03 -0800826 if self.branch_matcher and not self.branch_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800827 return False
828
James E. Blair83005782015-12-11 14:46:03 -0800829 if self.file_matcher and not self.file_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800830 return False
831
James E. Blair83005782015-12-11 14:46:03 -0800832 # NB: This is a negative match.
833 if (self.irrelevant_file_matcher and
834 self.irrelevant_file_matcher.matches(change)):
Maru Newby3fe5f852015-01-13 04:22:14 +0000835 return False
836
James E. Blair70c71582013-03-06 08:50:50 -0800837 return True
James E. Blaire5a847f2012-07-10 15:29:14 -0700838
James E. Blair1e8dd892012-05-30 09:15:05 -0700839
James E. Blairee743612012-05-29 14:49:32 -0700840class JobTree(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700841 """A JobTree holds one or more Jobs to represent Job dependencies.
842
843 If Job foo should only execute if Job bar succeeds, then there will
844 be a JobTree for foo, which will contain a JobTree for bar. A JobTree
845 can hold more than one dependent JobTrees, such that jobs bar and bang
846 both depend on job foo being successful.
847
848 A root node of a JobTree will have no associated Job."""
James E. Blairee743612012-05-29 14:49:32 -0700849
850 def __init__(self, job):
851 self.job = job
852 self.job_trees = []
853
James E. Blair12748b52017-02-07 17:17:53 -0800854 def __repr__(self):
855 return '<JobTree %s %s>' % (self.job, self.job_trees)
856
James E. Blairee743612012-05-29 14:49:32 -0700857 def addJob(self, job):
James E. Blair12a92b12014-03-26 11:54:53 -0700858 if job not in [x.job for x in self.job_trees]:
859 t = JobTree(job)
860 self.job_trees.append(t)
861 return t
James E. Blaire4ad55a2015-06-11 08:22:43 -0700862 for tree in self.job_trees:
863 if tree.job == job:
864 return tree
James E. Blairee743612012-05-29 14:49:32 -0700865
866 def getJobs(self):
867 jobs = []
868 for x in self.job_trees:
869 jobs.append(x.job)
870 jobs.extend(x.getJobs())
871 return jobs
872
873 def getJobTreeForJob(self, job):
874 if self.job == job:
875 return self
876 for tree in self.job_trees:
877 ret = tree.getJobTreeForJob(job)
878 if ret:
879 return ret
880 return None
881
James E. Blaira7f51ca2017-02-07 16:01:26 -0800882 def inheritFrom(self, other):
James E. Blairb97ed802015-12-21 15:55:35 -0800883 if other.job:
James E. Blaira7f51ca2017-02-07 16:01:26 -0800884 if not self.job:
885 self.job = other.job.copy()
886 else:
887 self.job.applyVariant(other.job)
James E. Blairb97ed802015-12-21 15:55:35 -0800888 for other_tree in other.job_trees:
889 this_tree = self.getJobTreeForJob(other_tree.job)
890 if not this_tree:
891 this_tree = JobTree(None)
892 self.job_trees.append(this_tree)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800893 this_tree.inheritFrom(other_tree)
James E. Blairb97ed802015-12-21 15:55:35 -0800894
James E. Blair1e8dd892012-05-30 09:15:05 -0700895
James E. Blair4aea70c2012-07-26 14:23:24 -0700896class Build(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700897 """A Build is an instance of a single running Job."""
898
James E. Blair4aea70c2012-07-26 14:23:24 -0700899 def __init__(self, job, uuid):
900 self.job = job
901 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -0700902 self.url = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700903 self.result = None
904 self.build_set = None
905 self.launch_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -0800906 self.start_time = None
907 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -0700908 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -0700909 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -0700910 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -0700911 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -0700912 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +0800913 self.worker = Worker()
Timothy Chavezb2332082015-08-07 20:08:04 -0500914 self.node_labels = []
915 self.node_name = None
James E. Blairee743612012-05-29 14:49:32 -0700916
917 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +0800918 return ('<Build %s of %s on %s>' %
919 (self.uuid, self.job.name, self.worker))
920
921
922class Worker(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700923 """Information about the specific worker executing a Build."""
Joshua Heskethba8776a2014-01-12 14:35:40 +0800924 def __init__(self):
925 self.name = "Unknown"
926 self.hostname = None
927 self.ips = []
928 self.fqdn = None
929 self.program = None
930 self.version = None
931 self.extra = {}
932
933 def updateFromData(self, data):
934 """Update worker information if contained in the WORK_DATA response."""
935 self.name = data.get('worker_name', self.name)
936 self.hostname = data.get('worker_hostname', self.hostname)
937 self.ips = data.get('worker_ips', self.ips)
938 self.fqdn = data.get('worker_fqdn', self.fqdn)
939 self.program = data.get('worker_program', self.program)
940 self.version = data.get('worker_version', self.version)
941 self.extra = data.get('worker_extra', self.extra)
942
943 def __repr__(self):
944 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -0700945
James E. Blair1e8dd892012-05-30 09:15:05 -0700946
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700947class RepoFiles(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700948 """RepoFiles holds config-file content for per-project job config."""
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700949 # When we ask a merger to prepare a future multiple-repo state and
950 # collect files so that we can dynamically load our configuration,
951 # this class provides easy access to that data.
952 def __init__(self):
953 self.projects = {}
954
955 def __repr__(self):
956 return '<RepoFiles %s>' % self.projects
957
958 def setFiles(self, items):
959 self.projects = {}
960 for item in items:
961 project = self.projects.setdefault(item['project'], {})
962 branch = project.setdefault(item['branch'], {})
963 branch.update(item['files'])
964
965 def getFile(self, project, branch, fn):
966 return self.projects.get(project, {}).get(branch, {}).get(fn)
967
968
James E. Blair7e530ad2012-07-03 16:12:28 -0700969class BuildSet(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700970 """Contains the Builds for a Change representing potential future state.
971
972 A BuildSet also holds the UUID used to produce the Zuul Ref that builders
973 check out.
974 """
James E. Blair4076e2b2014-01-28 12:42:20 -0800975 # Merge states:
976 NEW = 1
977 PENDING = 2
978 COMPLETE = 3
979
Antoine Musso9b229282014-08-18 23:45:43 +0200980 states_map = {
981 1: 'NEW',
982 2: 'PENDING',
983 3: 'COMPLETE',
984 }
985
James E. Blairfee8d652013-06-07 08:57:52 -0700986 def __init__(self, item):
987 self.item = item
James E. Blair11700c32012-07-05 17:50:05 -0700988 self.other_changes = []
James E. Blair7e530ad2012-07-03 16:12:28 -0700989 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -0700990 self.result = None
991 self.next_build_set = None
992 self.previous_build_set = None
James E. Blair4886cc12012-07-18 15:39:41 -0700993 self.ref = None
James E. Blair81515ad2012-10-01 18:29:08 -0700994 self.commit = None
James E. Blair4076e2b2014-01-28 12:42:20 -0800995 self.zuul_url = None
James E. Blair973721f2012-08-15 10:19:43 -0700996 self.unable_to_merge = False
James E. Blair972e3c72013-08-29 12:04:55 -0700997 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -0800998 self.merge_state = self.NEW
James E. Blair0eaad552016-09-02 12:09:54 -0700999 self.nodesets = {} # job -> nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001000 self.node_requests = {} # job -> reqs
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001001 self.files = RepoFiles()
1002 self.layout = None
Paul Belanger71d98172016-11-08 10:56:31 -05001003 self.tries = {}
James E. Blair7e530ad2012-07-03 16:12:28 -07001004
Antoine Musso9b229282014-08-18 23:45:43 +02001005 def __repr__(self):
1006 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
1007 self.item,
1008 len(self.builds),
1009 self.getStateName(self.merge_state))
1010
James E. Blair4886cc12012-07-18 15:39:41 -07001011 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -07001012 # The change isn't enqueued until after it's created
1013 # so we don't know what the other changes ahead will be
1014 # until jobs start.
1015 if not self.other_changes:
James E. Blairfee8d652013-06-07 08:57:52 -07001016 next_item = self.item.item_ahead
1017 while next_item:
1018 self.other_changes.append(next_item.change)
1019 next_item = next_item.item_ahead
James E. Blair4886cc12012-07-18 15:39:41 -07001020 if not self.ref:
1021 self.ref = 'Z' + uuid4().hex
1022
Antoine Musso9b229282014-08-18 23:45:43 +02001023 def getStateName(self, state_num):
1024 return self.states_map.get(
1025 state_num, 'UNKNOWN (%s)' % state_num)
1026
James E. Blair4886cc12012-07-18 15:39:41 -07001027 def addBuild(self, build):
1028 self.builds[build.job.name] = build
Paul Belanger71d98172016-11-08 10:56:31 -05001029 if build.job.name not in self.tries:
James E. Blair5aed1112016-12-15 09:18:05 -08001030 self.tries[build.job.name] = 1
James E. Blair4886cc12012-07-18 15:39:41 -07001031 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -07001032
James E. Blair4a28a882013-08-23 15:17:33 -07001033 def removeBuild(self, build):
Paul Belanger71d98172016-11-08 10:56:31 -05001034 self.tries[build.job.name] += 1
James E. Blair4a28a882013-08-23 15:17:33 -07001035 del self.builds[build.job.name]
1036
James E. Blair7e530ad2012-07-03 16:12:28 -07001037 def getBuild(self, job_name):
1038 return self.builds.get(job_name)
1039
James E. Blair11700c32012-07-05 17:50:05 -07001040 def getBuilds(self):
1041 keys = self.builds.keys()
1042 keys.sort()
1043 return [self.builds.get(x) for x in keys]
1044
James E. Blair0eaad552016-09-02 12:09:54 -07001045 def getJobNodeSet(self, job_name):
1046 # Return None if not provisioned; empty NodeSet if no nodes
1047 # required
1048 return self.nodesets.get(job_name)
James E. Blair8d692392016-04-08 17:47:58 -07001049
James E. Blaire18d4602017-01-05 11:17:28 -08001050 def removeJobNodeSet(self, job_name):
1051 if job_name not in self.nodesets:
1052 raise Exception("No job set for %s" % (job_name))
1053 del self.nodesets[job_name]
1054
James E. Blair8d692392016-04-08 17:47:58 -07001055 def setJobNodeRequest(self, job_name, req):
1056 if job_name in self.node_requests:
1057 raise Exception("Prior node request for %s" % (job_name))
1058 self.node_requests[job_name] = req
1059
1060 def getJobNodeRequest(self, job_name):
1061 return self.node_requests.get(job_name)
1062
James E. Blair0eaad552016-09-02 12:09:54 -07001063 def jobNodeRequestComplete(self, job_name, req, nodeset):
1064 if job_name in self.nodesets:
James E. Blair8d692392016-04-08 17:47:58 -07001065 raise Exception("Prior node request for %s" % (job_name))
James E. Blair0eaad552016-09-02 12:09:54 -07001066 self.nodesets[job_name] = nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001067 del self.node_requests[job_name]
1068
Paul Belanger71d98172016-11-08 10:56:31 -05001069 def getTries(self, job_name):
1070 return self.tries.get(job_name)
1071
Adam Gandelman8bd57102016-12-02 12:58:42 -08001072 def getMergeMode(self, job_name):
1073 if not self.layout or job_name not in self.layout.project_configs:
1074 return MERGER_MERGE_RESOLVE
1075 return self.layout.project_configs[job_name].merge_mode
1076
James E. Blair7e530ad2012-07-03 16:12:28 -07001077
James E. Blairfee8d652013-06-07 08:57:52 -07001078class QueueItem(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001079 """Represents the position of a Change in a ChangeQueue.
1080
1081 All Changes are enqueued into ChangeQueue in a QueueItem. The QueueItem
1082 holds the current `BuildSet` as well as all previous `BuildSets` that were
1083 produced for this `QueueItem`.
1084 """
James E. Blair32663402012-06-01 10:04:18 -07001085
James E. Blairbfb8e042014-12-30 17:01:44 -08001086 def __init__(self, queue, change):
1087 self.pipeline = queue.pipeline
1088 self.queue = queue
James E. Blairfee8d652013-06-07 08:57:52 -07001089 self.change = change # a changeish
James E. Blair7e530ad2012-07-03 16:12:28 -07001090 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -07001091 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -07001092 self.current_build_set = BuildSet(self)
1093 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -07001094 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -07001095 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -08001096 self.enqueue_time = None
1097 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -07001098 self.reported = False
James E. Blairbfb8e042014-12-30 17:01:44 -08001099 self.active = False # Whether an item is within an active window
1100 self.live = True # Whether an item is intended to be processed at all
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001101 self.layout = None # This item's shadow layout
James E. Blair83005782015-12-11 14:46:03 -08001102 self.job_tree = None
James E. Blaire5a847f2012-07-10 15:29:14 -07001103
James E. Blair972e3c72013-08-29 12:04:55 -07001104 def __repr__(self):
1105 if self.pipeline:
1106 pipeline = self.pipeline.name
1107 else:
1108 pipeline = None
1109 return '<QueueItem 0x%x for %s in %s>' % (
1110 id(self), self.change, pipeline)
1111
James E. Blairee743612012-05-29 14:49:32 -07001112 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -07001113 old = self.current_build_set
1114 self.current_build_set.result = 'CANCELED'
1115 self.current_build_set = BuildSet(self)
1116 old.next_build_set = self.current_build_set
1117 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -07001118 self.build_sets.append(self.current_build_set)
James E. Blairee743612012-05-29 14:49:32 -07001119
1120 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -07001121 self.current_build_set.addBuild(build)
James E. Blair66eeebf2013-07-27 17:44:32 -07001122 build.pipeline = self.pipeline
James E. Blairee743612012-05-29 14:49:32 -07001123
James E. Blair4a28a882013-08-23 15:17:33 -07001124 def removeBuild(self, build):
1125 self.current_build_set.removeBuild(build)
1126
James E. Blairfee8d652013-06-07 08:57:52 -07001127 def setReportedResult(self, result):
1128 self.current_build_set.result = result
1129
James E. Blair83005782015-12-11 14:46:03 -08001130 def freezeJobTree(self):
1131 """Find or create actual matching jobs for this item's change and
1132 store the resulting job tree."""
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001133 layout = self.current_build_set.layout
1134 self.job_tree = layout.createJobTree(self)
1135
1136 def hasJobTree(self):
1137 """Returns True if the item has a job tree."""
1138 return self.job_tree is not None
James E. Blair83005782015-12-11 14:46:03 -08001139
1140 def getJobs(self):
1141 if not self.live or not self.job_tree:
1142 return []
1143 return self.job_tree.getJobs()
1144
James E. Blairdbfd3282016-07-21 10:46:19 -07001145 def haveAllJobsStarted(self):
1146 if not self.hasJobTree():
1147 return False
1148 for job in self.getJobs():
1149 build = self.current_build_set.getBuild(job.name)
1150 if not build or not build.start_time:
1151 return False
1152 return True
1153
1154 def areAllJobsComplete(self):
1155 if not self.hasJobTree():
1156 return False
1157 for job in self.getJobs():
1158 build = self.current_build_set.getBuild(job.name)
1159 if not build or not build.result:
1160 return False
1161 return True
1162
1163 def didAllJobsSucceed(self):
1164 if not self.hasJobTree():
1165 return False
1166 for job in self.getJobs():
1167 if not job.voting:
1168 continue
1169 build = self.current_build_set.getBuild(job.name)
1170 if not build:
1171 return False
1172 if build.result != 'SUCCESS':
1173 return False
1174 return True
1175
1176 def didAnyJobFail(self):
1177 if not self.hasJobTree():
1178 return False
1179 for job in self.getJobs():
1180 if not job.voting:
1181 continue
1182 build = self.current_build_set.getBuild(job.name)
1183 if build and build.result and (build.result != 'SUCCESS'):
1184 return True
1185 return False
1186
1187 def didMergerFail(self):
1188 if self.current_build_set.unable_to_merge:
1189 return True
1190 return False
1191
James E. Blairdbfd3282016-07-21 10:46:19 -07001192 def isHoldingFollowingChanges(self):
1193 if not self.live:
1194 return False
1195 if not self.hasJobTree():
1196 return False
1197 for job in self.getJobs():
1198 if not job.hold_following_changes:
1199 continue
1200 build = self.current_build_set.getBuild(job.name)
1201 if not build:
1202 return True
1203 if build.result != 'SUCCESS':
1204 return True
1205
1206 if not self.item_ahead:
1207 return False
1208 return self.item_ahead.isHoldingFollowingChanges()
1209
1210 def _findJobsToRun(self, job_trees, mutex):
1211 torun = []
James E. Blair791b5392016-08-03 11:25:56 -07001212 if self.item_ahead:
1213 # Only run jobs if any 'hold' jobs on the change ahead
1214 # have completed successfully.
1215 if self.item_ahead.isHoldingFollowingChanges():
1216 return []
James E. Blairdbfd3282016-07-21 10:46:19 -07001217 for tree in job_trees:
1218 job = tree.job
1219 result = None
1220 if job:
1221 if not job.changeMatches(self.change):
1222 continue
1223 build = self.current_build_set.getBuild(job.name)
1224 if build:
1225 result = build.result
1226 else:
1227 # There is no build for the root of this job tree,
James E. Blair34776ee2016-08-25 13:53:54 -07001228 # so it has not run yet.
James E. Blair0eaad552016-09-02 12:09:54 -07001229 nodeset = self.current_build_set.getJobNodeSet(job.name)
1230 if nodeset is None:
James E. Blair34776ee2016-08-25 13:53:54 -07001231 # The nodes for this job are not ready, skip
1232 # it for now.
1233 continue
James E. Blairdbfd3282016-07-21 10:46:19 -07001234 if mutex.acquire(self, job):
1235 # If this job needs a mutex, either acquire it or make
1236 # sure that we have it before running the job.
1237 torun.append(job)
1238 # If there is no job, this is a null job tree, and we should
1239 # run all of its jobs.
1240 if result == 'SUCCESS' or not job:
1241 torun.extend(self._findJobsToRun(tree.job_trees, mutex))
1242 return torun
1243
1244 def findJobsToRun(self, mutex):
1245 if not self.live:
1246 return []
1247 tree = self.job_tree
1248 if not tree:
1249 return []
1250 return self._findJobsToRun(tree.job_trees, mutex)
1251
1252 def _findJobsToRequest(self, job_trees):
James E. Blair6ab79e02017-01-06 10:10:17 -08001253 build_set = self.current_build_set
James E. Blairdbfd3282016-07-21 10:46:19 -07001254 toreq = []
James E. Blair6ab79e02017-01-06 10:10:17 -08001255 if self.item_ahead:
1256 if self.item_ahead.isHoldingFollowingChanges():
1257 return []
James E. Blairdbfd3282016-07-21 10:46:19 -07001258 for tree in job_trees:
1259 job = tree.job
James E. Blair6ab79e02017-01-06 10:10:17 -08001260 result = None
James E. Blairdbfd3282016-07-21 10:46:19 -07001261 if job:
1262 if not job.changeMatches(self.change):
1263 continue
James E. Blair6ab79e02017-01-06 10:10:17 -08001264 build = build_set.getBuild(job.name)
1265 if build:
1266 result = build.result
1267 else:
1268 nodeset = build_set.getJobNodeSet(job.name)
1269 if nodeset is None:
1270 req = build_set.getJobNodeRequest(job.name)
1271 if req is None:
1272 toreq.append(job)
1273 if result == 'SUCCESS' or not job:
1274 toreq.extend(self._findJobsToRequest(tree.job_trees))
James E. Blairdbfd3282016-07-21 10:46:19 -07001275 return toreq
1276
1277 def findJobsToRequest(self):
1278 if not self.live:
1279 return []
1280 tree = self.job_tree
1281 if not tree:
1282 return []
1283 return self._findJobsToRequest(tree.job_trees)
1284
1285 def setResult(self, build):
1286 if build.retry:
1287 self.removeBuild(build)
1288 elif build.result != 'SUCCESS':
1289 # Get a JobTree from a Job so we can find only its dependent jobs
1290 tree = self.job_tree.getJobTreeForJob(build.job)
1291 for job in tree.getJobs():
1292 fakebuild = Build(job, None)
1293 fakebuild.result = 'SKIPPED'
1294 self.addBuild(fakebuild)
1295
James E. Blair6ab79e02017-01-06 10:10:17 -08001296 def setNodeRequestFailure(self, job):
1297 fakebuild = Build(job, None)
1298 self.addBuild(fakebuild)
1299 fakebuild.result = 'NODE_FAILURE'
1300 self.setResult(fakebuild)
1301
James E. Blairdbfd3282016-07-21 10:46:19 -07001302 def setDequeuedNeedingChange(self):
1303 self.dequeued_needing_change = True
1304 self._setAllJobsSkipped()
1305
1306 def setUnableToMerge(self):
1307 self.current_build_set.unable_to_merge = True
1308 self._setAllJobsSkipped()
1309
1310 def _setAllJobsSkipped(self):
1311 for job in self.getJobs():
1312 fakebuild = Build(job, None)
1313 fakebuild.result = 'SKIPPED'
1314 self.addBuild(fakebuild)
1315
James E. Blairb7273ef2016-04-19 08:58:51 -07001316 def formatJobResult(self, job, url_pattern=None):
1317 build = self.current_build_set.getBuild(job.name)
1318 result = build.result
1319 pattern = url_pattern
1320 if result == 'SUCCESS':
1321 if job.success_message:
1322 result = job.success_message
James E. Blaira5dba232016-08-08 15:53:24 -07001323 if job.success_url:
1324 pattern = job.success_url
James E. Blairb7273ef2016-04-19 08:58:51 -07001325 elif result == 'FAILURE':
1326 if job.failure_message:
1327 result = job.failure_message
James E. Blaira5dba232016-08-08 15:53:24 -07001328 if job.failure_url:
1329 pattern = job.failure_url
James E. Blairb7273ef2016-04-19 08:58:51 -07001330 url = None
1331 if pattern:
1332 try:
1333 url = pattern.format(change=self.change,
1334 pipeline=self.pipeline,
1335 job=job,
1336 build=build)
1337 except Exception:
1338 pass # FIXME: log this or something?
1339 if not url:
1340 url = build.url or job.name
1341 return (result, url)
1342
1343 def formatJSON(self, url_pattern=None):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001344 changeish = self.change
1345 ret = {}
1346 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -08001347 ret['live'] = self.live
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001348 if hasattr(changeish, 'url') and changeish.url is not None:
1349 ret['url'] = changeish.url
1350 else:
1351 ret['url'] = None
1352 ret['id'] = changeish._id()
1353 if self.item_ahead:
1354 ret['item_ahead'] = self.item_ahead.change._id()
1355 else:
1356 ret['item_ahead'] = None
1357 ret['items_behind'] = [i.change._id() for i in self.items_behind]
1358 ret['failing_reasons'] = self.current_build_set.failing_reasons
1359 ret['zuul_ref'] = self.current_build_set.ref
Ramy Asselin07cc33c2015-06-12 14:06:34 -07001360 if changeish.project:
1361 ret['project'] = changeish.project.name
1362 else:
1363 # For cross-project dependencies with the depends-on
1364 # project not known to zuul, the project is None
1365 # Set it to a static value
1366 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001367 ret['enqueue_time'] = int(self.enqueue_time * 1000)
1368 ret['jobs'] = []
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001369 if hasattr(changeish, 'owner'):
1370 ret['owner'] = changeish.owner
1371 else:
1372 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001373 max_remaining = 0
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001374 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001375 now = time.time()
1376 build = self.current_build_set.getBuild(job.name)
1377 elapsed = None
1378 remaining = None
1379 result = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001380 build_url = None
1381 report_url = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001382 worker = None
1383 if build:
1384 result = build.result
James E. Blairb7273ef2016-04-19 08:58:51 -07001385 build_url = build.url
1386 (unused, report_url) = self.formatJobResult(job, url_pattern)
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001387 if build.start_time:
1388 if build.end_time:
1389 elapsed = int((build.end_time -
1390 build.start_time) * 1000)
1391 remaining = 0
1392 else:
1393 elapsed = int((now - build.start_time) * 1000)
1394 if build.estimated_time:
1395 remaining = max(
1396 int(build.estimated_time * 1000) - elapsed,
1397 0)
1398 worker = {
1399 'name': build.worker.name,
1400 'hostname': build.worker.hostname,
1401 'ips': build.worker.ips,
1402 'fqdn': build.worker.fqdn,
1403 'program': build.worker.program,
1404 'version': build.worker.version,
1405 'extra': build.worker.extra
1406 }
1407 if remaining and remaining > max_remaining:
1408 max_remaining = remaining
1409
1410 ret['jobs'].append({
1411 'name': job.name,
1412 'elapsed_time': elapsed,
1413 'remaining_time': remaining,
James E. Blairb7273ef2016-04-19 08:58:51 -07001414 'url': build_url,
1415 'report_url': report_url,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001416 'result': result,
1417 'voting': job.voting,
1418 'uuid': build.uuid if build else None,
1419 'launch_time': build.launch_time if build else None,
1420 'start_time': build.start_time if build else None,
1421 'end_time': build.end_time if build else None,
1422 'estimated_time': build.estimated_time if build else None,
1423 'pipeline': build.pipeline.name if build else None,
1424 'canceled': build.canceled if build else None,
1425 'retry': build.retry if build else None,
Timothy Chavezb2332082015-08-07 20:08:04 -05001426 'node_labels': build.node_labels if build else [],
1427 'node_name': build.node_name if build else None,
1428 'worker': worker,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001429 })
1430
James E. Blairdbfd3282016-07-21 10:46:19 -07001431 if self.haveAllJobsStarted():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001432 ret['remaining_time'] = max_remaining
1433 else:
1434 ret['remaining_time'] = None
1435 return ret
1436
1437 def formatStatus(self, indent=0, html=False):
1438 changeish = self.change
1439 indent_str = ' ' * indent
1440 ret = ''
1441 if html and hasattr(changeish, 'url') and changeish.url is not None:
1442 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
1443 indent_str,
1444 changeish.project.name,
1445 changeish.url,
1446 changeish._id())
1447 else:
1448 ret += '%sProject %s change %s based on %s\n' % (
1449 indent_str,
1450 changeish.project.name,
1451 changeish._id(),
1452 self.item_ahead)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001453 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001454 build = self.current_build_set.getBuild(job.name)
1455 if build:
1456 result = build.result
1457 else:
1458 result = None
1459 job_name = job.name
1460 if not job.voting:
1461 voting = ' (non-voting)'
1462 else:
1463 voting = ''
1464 if html:
1465 if build:
1466 url = build.url
1467 else:
1468 url = None
1469 if url is not None:
1470 job_name = '<a href="%s">%s</a>' % (url, job_name)
1471 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
1472 ret += '\n'
1473 return ret
1474
James E. Blairfee8d652013-06-07 08:57:52 -07001475
1476class Changeish(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001477 """Base class for Change and Ref."""
James E. Blairfee8d652013-06-07 08:57:52 -07001478
1479 def __init__(self, project):
1480 self.project = project
1481
Joshua Hesketh36c3fa52014-01-22 11:40:52 +11001482 def getBasePath(self):
1483 base_path = ''
1484 if hasattr(self, 'refspec'):
1485 base_path = "%s/%s/%s" % (
1486 self.number[-2:], self.number, self.patchset)
1487 elif hasattr(self, 'ref'):
1488 base_path = "%s/%s" % (self.newrev[:2], self.newrev)
1489
1490 return base_path
1491
James E. Blairfee8d652013-06-07 08:57:52 -07001492 def equals(self, other):
1493 raise NotImplementedError()
1494
1495 def isUpdateOf(self, other):
1496 raise NotImplementedError()
1497
1498 def filterJobs(self, jobs):
1499 return filter(lambda job: job.changeMatches(self), jobs)
1500
1501 def getRelatedChanges(self):
1502 return set()
1503
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001504 def updatesConfig(self):
1505 return False
1506
James E. Blair1e8dd892012-05-30 09:15:05 -07001507
James E. Blair4aea70c2012-07-26 14:23:24 -07001508class Change(Changeish):
Monty Taylora42a55b2016-07-29 07:53:33 -07001509 """A proposed new state for a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001510 def __init__(self, project):
1511 super(Change, self).__init__(project)
1512 self.branch = None
1513 self.number = None
1514 self.url = None
1515 self.patchset = None
1516 self.refspec = None
1517
James E. Blair70c71582013-03-06 08:50:50 -08001518 self.files = []
James E. Blair6965a4b2014-12-16 17:19:04 -08001519 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -07001520 self.needed_by_changes = []
1521 self.is_current_patchset = True
1522 self.can_merge = False
1523 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -07001524 self.failed_to_merge = False
James E. Blairc053d022014-01-22 14:57:33 -08001525 self.approvals = []
James E. Blair11041d22014-05-02 14:49:53 -07001526 self.open = None
1527 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001528 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001529
1530 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -07001531 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -07001532
1533 def __repr__(self):
1534 return '<Change 0x%x %s>' % (id(self), self._id())
1535
1536 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +08001537 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -07001538 return True
1539 return False
1540
James E. Blair2fa50962013-01-30 21:50:41 -08001541 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -08001542 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -07001543 (hasattr(other, 'patchset') and
1544 self.patchset is not None and
1545 other.patchset is not None and
1546 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -08001547 return True
1548 return False
1549
James E. Blairfee8d652013-06-07 08:57:52 -07001550 def getRelatedChanges(self):
1551 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -08001552 for c in self.needs_changes:
1553 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -07001554 for c in self.needed_by_changes:
1555 related.add(c)
1556 related.update(c.getRelatedChanges())
1557 return related
James E. Blair4aea70c2012-07-26 14:23:24 -07001558
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001559 def updatesConfig(self):
1560 if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files:
1561 return True
1562 return False
1563
James E. Blair4aea70c2012-07-26 14:23:24 -07001564
1565class Ref(Changeish):
Monty Taylora42a55b2016-07-29 07:53:33 -07001566 """An existing state of a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001567 def __init__(self, project):
James E. Blairbe765db2012-08-07 08:36:20 -07001568 super(Ref, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -07001569 self.ref = None
1570 self.oldrev = None
1571 self.newrev = None
1572
James E. Blairbe765db2012-08-07 08:36:20 -07001573 def _id(self):
1574 return self.newrev
1575
Antoine Musso68bdcd72013-01-17 12:31:28 +01001576 def __repr__(self):
1577 rep = None
1578 if self.newrev == '0000000000000000000000000000000000000000':
1579 rep = '<Ref 0x%x deletes %s from %s' % (
1580 id(self), self.ref, self.oldrev)
1581 elif self.oldrev == '0000000000000000000000000000000000000000':
1582 rep = '<Ref 0x%x creates %s on %s>' % (
1583 id(self), self.ref, self.newrev)
1584 else:
1585 # Catch all
1586 rep = '<Ref 0x%x %s updated %s..%s>' % (
1587 id(self), self.ref, self.oldrev, self.newrev)
1588
1589 return rep
1590
James E. Blair4aea70c2012-07-26 14:23:24 -07001591 def equals(self, other):
James E. Blair9358c612012-09-28 08:29:39 -07001592 if (self.project == other.project
1593 and self.ref == other.ref
1594 and self.newrev == other.newrev):
James E. Blair4aea70c2012-07-26 14:23:24 -07001595 return True
1596 return False
1597
James E. Blair2fa50962013-01-30 21:50:41 -08001598 def isUpdateOf(self, other):
1599 return False
1600
James E. Blair4aea70c2012-07-26 14:23:24 -07001601
James E. Blair63bb0ef2013-07-29 17:14:51 -07001602class NullChange(Changeish):
James E. Blair23161912016-07-28 15:42:14 -07001603 # TODOv3(jeblair): remove this in favor of enqueueing Refs (eg
1604 # current master) instead.
James E. Blaire5910202013-12-27 09:50:31 -08001605 def __repr__(self):
1606 return '<NullChange for %s>' % (self.project)
James E. Blair63bb0ef2013-07-29 17:14:51 -07001607
James E. Blair63bb0ef2013-07-29 17:14:51 -07001608 def _id(self):
Alex Gaynorddb9ef32013-09-16 21:04:58 -07001609 return None
James E. Blair63bb0ef2013-07-29 17:14:51 -07001610
1611 def equals(self, other):
Steve Varnau7b78b312015-04-03 14:49:46 -07001612 if (self.project == other.project
1613 and other._id() is None):
James E. Blair4f6033c2014-03-27 15:49:09 -07001614 return True
James E. Blair63bb0ef2013-07-29 17:14:51 -07001615 return False
1616
1617 def isUpdateOf(self, other):
1618 return False
1619
1620
James E. Blairee743612012-05-29 14:49:32 -07001621class TriggerEvent(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001622 """Incoming event from an external system."""
James E. Blairee743612012-05-29 14:49:32 -07001623 def __init__(self):
1624 self.data = None
James E. Blair32663402012-06-01 10:04:18 -07001625 # common
James E. Blairee743612012-05-29 14:49:32 -07001626 self.type = None
Paul Belangerbaca3132016-11-04 12:49:54 -04001627 # For management events (eg: enqueue / promote)
1628 self.tenant_name = None
James E. Blairee743612012-05-29 14:49:32 -07001629 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -07001630 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +01001631 # Representation of the user account that performed the event.
1632 self.account = None
James E. Blair32663402012-06-01 10:04:18 -07001633 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -07001634 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -07001635 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -07001636 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -07001637 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -07001638 self.approvals = []
1639 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07001640 self.comment = None
James E. Blair32663402012-06-01 10:04:18 -07001641 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -07001642 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -07001643 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07001644 self.newrev = None
James E. Blair63bb0ef2013-07-29 17:14:51 -07001645 # timer
1646 self.timespec = None
James E. Blairc494d542014-08-06 09:23:52 -07001647 # zuultrigger
1648 self.pipeline_name = None
James E. Blairad28e912013-11-27 10:43:22 -08001649 # For events that arrive with a destination pipeline (eg, from
1650 # an admin command, etc):
1651 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07001652
James E. Blair9f9667e2012-06-12 17:51:08 -07001653 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001654 ret = '<TriggerEvent %s %s' % (self.type, self.project_name)
James E. Blair1e8dd892012-05-30 09:15:05 -07001655
James E. Blairee743612012-05-29 14:49:32 -07001656 if self.branch:
1657 ret += " %s" % self.branch
1658 if self.change_number:
1659 ret += " %s,%s" % (self.change_number, self.patch_number)
1660 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -07001661 ret += ' ' + ', '.join(
1662 ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
James E. Blairee743612012-05-29 14:49:32 -07001663 ret += '>'
1664
1665 return ret
1666
James E. Blair1e8dd892012-05-30 09:15:05 -07001667
James E. Blair9c17dbf2014-06-23 14:21:58 -07001668class BaseFilter(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001669 """Base Class for filtering which Changes and Events to process."""
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001670 def __init__(self, required_approvals=[], reject_approvals=[]):
James E. Blair5bf78a32015-07-30 18:08:24 +00001671 self._required_approvals = copy.deepcopy(required_approvals)
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001672 self.required_approvals = self._tidy_approvals(required_approvals)
1673 self._reject_approvals = copy.deepcopy(reject_approvals)
1674 self.reject_approvals = self._tidy_approvals(reject_approvals)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001675
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001676 def _tidy_approvals(self, approvals):
1677 for a in approvals:
James E. Blair9c17dbf2014-06-23 14:21:58 -07001678 for k, v in a.items():
1679 if k == 'username':
James E. Blairb01ec542016-06-16 09:46:49 -07001680 a['username'] = re.compile(v)
James E. Blair1fbfceb2014-06-23 14:42:53 -07001681 elif k in ['email', 'email-filter']:
James E. Blair5bf78a32015-07-30 18:08:24 +00001682 a['email'] = re.compile(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001683 elif k == 'newer-than':
James E. Blair5bf78a32015-07-30 18:08:24 +00001684 a[k] = time_to_seconds(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001685 elif k == 'older-than':
James E. Blair5bf78a32015-07-30 18:08:24 +00001686 a[k] = time_to_seconds(v)
James E. Blair1fbfceb2014-06-23 14:42:53 -07001687 if 'email-filter' in a:
1688 del a['email-filter']
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001689 return approvals
1690
1691 def _match_approval_required_approval(self, rapproval, approval):
1692 # Check if the required approval and approval match
1693 if 'description' not in approval:
1694 return False
1695 now = time.time()
1696 by = approval.get('by', {})
1697 for k, v in rapproval.items():
1698 if k == 'username':
James E. Blairb01ec542016-06-16 09:46:49 -07001699 if (not v.search(by.get('username', ''))):
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001700 return False
1701 elif k == 'email':
1702 if (not v.search(by.get('email', ''))):
1703 return False
1704 elif k == 'newer-than':
1705 t = now - v
1706 if (approval['grantedOn'] < t):
1707 return False
1708 elif k == 'older-than':
1709 t = now - v
1710 if (approval['grantedOn'] >= t):
1711 return False
1712 else:
1713 if not isinstance(v, list):
1714 v = [v]
1715 if (normalizeCategory(approval['description']) != k or
1716 int(approval['value']) not in v):
1717 return False
1718 return True
1719
1720 def matchesApprovals(self, change):
1721 if (self.required_approvals and not change.approvals
1722 or self.reject_approvals and not change.approvals):
1723 # A change with no approvals can not match
1724 return False
1725
1726 # TODO(jhesketh): If we wanted to optimise this slightly we could
1727 # analyse both the REQUIRE and REJECT filters by looping over the
1728 # approvals on the change and keeping track of what we have checked
1729 # rather than needing to loop on the change approvals twice
1730 return (self.matchesRequiredApprovals(change) and
1731 self.matchesNoRejectApprovals(change))
James E. Blair9c17dbf2014-06-23 14:21:58 -07001732
1733 def matchesRequiredApprovals(self, change):
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001734 # Check if any approvals match the requirements
James E. Blair5bf78a32015-07-30 18:08:24 +00001735 for rapproval in self.required_approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001736 matches_rapproval = False
James E. Blair9c17dbf2014-06-23 14:21:58 -07001737 for approval in change.approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001738 if self._match_approval_required_approval(rapproval, approval):
1739 # We have a matching approval so this requirement is
1740 # fulfilled
1741 matches_rapproval = True
James E. Blair5bf78a32015-07-30 18:08:24 +00001742 break
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001743 if not matches_rapproval:
James E. Blair5bf78a32015-07-30 18:08:24 +00001744 return False
James E. Blair9c17dbf2014-06-23 14:21:58 -07001745 return True
1746
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001747 def matchesNoRejectApprovals(self, change):
1748 # Check to make sure no approvals match a reject criteria
1749 for rapproval in self.reject_approvals:
1750 for approval in change.approvals:
1751 if self._match_approval_required_approval(rapproval, approval):
1752 # A reject approval has been matched, so we reject
1753 # immediately
1754 return False
1755 # To get here no rejects can have been matched so we should be good to
1756 # queue
1757 return True
1758
James E. Blair9c17dbf2014-06-23 14:21:58 -07001759
1760class EventFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07001761 """Allows a Pipeline to only respond to certain events."""
James E. Blairc0dedf82014-08-06 09:37:52 -07001762 def __init__(self, trigger, types=[], branches=[], refs=[],
1763 event_approvals={}, comments=[], emails=[], usernames=[],
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001764 timespecs=[], required_approvals=[], reject_approvals=[],
1765 pipelines=[], ignore_deletes=True):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001766 super(EventFilter, self).__init__(
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001767 required_approvals=required_approvals,
1768 reject_approvals=reject_approvals)
James E. Blairc0dedf82014-08-06 09:37:52 -07001769 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07001770 self._types = types
1771 self._branches = branches
1772 self._refs = refs
James E. Blair1fbfceb2014-06-23 14:42:53 -07001773 self._comments = comments
1774 self._emails = emails
1775 self._usernames = usernames
James E. Blairc494d542014-08-06 09:23:52 -07001776 self._pipelines = pipelines
James E. Blairee743612012-05-29 14:49:32 -07001777 self.types = [re.compile(x) for x in types]
1778 self.branches = [re.compile(x) for x in branches]
1779 self.refs = [re.compile(x) for x in refs]
James E. Blair1fbfceb2014-06-23 14:42:53 -07001780 self.comments = [re.compile(x) for x in comments]
1781 self.emails = [re.compile(x) for x in emails]
1782 self.usernames = [re.compile(x) for x in usernames]
James E. Blairc494d542014-08-06 09:23:52 -07001783 self.pipelines = [re.compile(x) for x in pipelines]
James E. Blairc053d022014-01-22 14:57:33 -08001784 self.event_approvals = event_approvals
James E. Blair63bb0ef2013-07-29 17:14:51 -07001785 self.timespecs = timespecs
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001786 self.ignore_deletes = ignore_deletes
James E. Blairee743612012-05-29 14:49:32 -07001787
James E. Blair9f9667e2012-06-12 17:51:08 -07001788 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001789 ret = '<EventFilter'
James E. Blair1e8dd892012-05-30 09:15:05 -07001790
James E. Blairee743612012-05-29 14:49:32 -07001791 if self._types:
1792 ret += ' types: %s' % ', '.join(self._types)
James E. Blairc494d542014-08-06 09:23:52 -07001793 if self._pipelines:
1794 ret += ' pipelines: %s' % ', '.join(self._pipelines)
James E. Blairee743612012-05-29 14:49:32 -07001795 if self._branches:
1796 ret += ' branches: %s' % ', '.join(self._branches)
1797 if self._refs:
1798 ret += ' refs: %s' % ', '.join(self._refs)
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001799 if self.ignore_deletes:
1800 ret += ' ignore_deletes: %s' % self.ignore_deletes
James E. Blairc053d022014-01-22 14:57:33 -08001801 if self.event_approvals:
1802 ret += ' event_approvals: %s' % ', '.join(
1803 ['%s:%s' % a for a in self.event_approvals.items()])
James E. Blair5bf78a32015-07-30 18:08:24 +00001804 if self.required_approvals:
1805 ret += ' required_approvals: %s' % ', '.join(
1806 ['%s' % a for a in self._required_approvals])
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001807 if self.reject_approvals:
1808 ret += ' reject_approvals: %s' % ', '.join(
1809 ['%s' % a for a in self._reject_approvals])
James E. Blair1fbfceb2014-06-23 14:42:53 -07001810 if self._comments:
1811 ret += ' comments: %s' % ', '.join(self._comments)
1812 if self._emails:
1813 ret += ' emails: %s' % ', '.join(self._emails)
1814 if self._usernames:
1815 ret += ' username_filters: %s' % ', '.join(self._usernames)
James E. Blair63bb0ef2013-07-29 17:14:51 -07001816 if self.timespecs:
1817 ret += ' timespecs: %s' % ', '.join(self.timespecs)
James E. Blairee743612012-05-29 14:49:32 -07001818 ret += '>'
1819
1820 return ret
1821
James E. Blairc053d022014-01-22 14:57:33 -08001822 def matches(self, event, change):
James E. Blairee743612012-05-29 14:49:32 -07001823 # event types are ORed
1824 matches_type = False
1825 for etype in self.types:
1826 if etype.match(event.type):
1827 matches_type = True
1828 if self.types and not matches_type:
1829 return False
1830
James E. Blairc494d542014-08-06 09:23:52 -07001831 # pipelines are ORed
1832 matches_pipeline = False
1833 for epipe in self.pipelines:
1834 if epipe.match(event.pipeline_name):
1835 matches_pipeline = True
1836 if self.pipelines and not matches_pipeline:
1837 return False
1838
James E. Blairee743612012-05-29 14:49:32 -07001839 # branches are ORed
1840 matches_branch = False
1841 for branch in self.branches:
1842 if branch.match(event.branch):
1843 matches_branch = True
1844 if self.branches and not matches_branch:
1845 return False
1846
1847 # refs are ORed
1848 matches_ref = False
Yolanda Robla16698872014-08-25 11:59:27 +02001849 if event.ref is not None:
1850 for ref in self.refs:
1851 if ref.match(event.ref):
1852 matches_ref = True
James E. Blairee743612012-05-29 14:49:32 -07001853 if self.refs and not matches_ref:
1854 return False
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001855 if self.ignore_deletes and event.newrev == EMPTY_GIT_REF:
1856 # If the updated ref has an empty git sha (all 0s),
1857 # then the ref is being deleted
1858 return False
James E. Blairee743612012-05-29 14:49:32 -07001859
James E. Blair1fbfceb2014-06-23 14:42:53 -07001860 # comments are ORed
1861 matches_comment_re = False
1862 for comment_re in self.comments:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001863 if (event.comment is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001864 comment_re.search(event.comment)):
1865 matches_comment_re = True
1866 if self.comments and not matches_comment_re:
Clark Boylanb9bcb402012-06-29 17:44:05 -07001867 return False
1868
Antoine Mussob4e809e2012-12-06 16:58:06 +01001869 # We better have an account provided by Gerrit to do
1870 # email filtering.
1871 if event.account is not None:
James E. Blaircf429f32012-12-20 14:28:24 -08001872 account_email = event.account.get('email')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001873 # emails are ORed
1874 matches_email_re = False
1875 for email_re in self.emails:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001876 if (account_email is not None and
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001877 email_re.search(account_email)):
James E. Blair1fbfceb2014-06-23 14:42:53 -07001878 matches_email_re = True
1879 if self.emails and not matches_email_re:
Antoine Mussob4e809e2012-12-06 16:58:06 +01001880 return False
1881
James E. Blair1fbfceb2014-06-23 14:42:53 -07001882 # usernames are ORed
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001883 account_username = event.account.get('username')
James E. Blair1fbfceb2014-06-23 14:42:53 -07001884 matches_username_re = False
1885 for username_re in self.usernames:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001886 if (account_username is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07001887 username_re.search(account_username)):
1888 matches_username_re = True
1889 if self.usernames and not matches_username_re:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11001890 return False
1891
James E. Blairee743612012-05-29 14:49:32 -07001892 # approvals are ANDed
James E. Blairc053d022014-01-22 14:57:33 -08001893 for category, value in self.event_approvals.items():
James E. Blairee743612012-05-29 14:49:32 -07001894 matches_approval = False
1895 for eapproval in event.approvals:
1896 if (normalizeCategory(eapproval['description']) == category and
1897 int(eapproval['value']) == int(value)):
1898 matches_approval = True
James E. Blair1e8dd892012-05-30 09:15:05 -07001899 if not matches_approval:
1900 return False
James E. Blair63bb0ef2013-07-29 17:14:51 -07001901
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001902 # required approvals are ANDed (reject approvals are ORed)
1903 if not self.matchesApprovals(change):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001904 return False
James E. Blairc053d022014-01-22 14:57:33 -08001905
James E. Blair63bb0ef2013-07-29 17:14:51 -07001906 # timespecs are ORed
1907 matches_timespec = False
1908 for timespec in self.timespecs:
1909 if (event.timespec == timespec):
1910 matches_timespec = True
1911 if self.timespecs and not matches_timespec:
1912 return False
1913
James E. Blairee743612012-05-29 14:49:32 -07001914 return True
James E. Blaireff88162013-07-01 12:44:14 -04001915
1916
James E. Blair9c17dbf2014-06-23 14:21:58 -07001917class ChangeishFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07001918 """Allows a Manager to only enqueue Changes that meet certain criteria."""
Clark Boylana9702ad2014-05-08 17:17:24 -07001919 def __init__(self, open=None, current_patchset=None,
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001920 statuses=[], required_approvals=[],
1921 reject_approvals=[]):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001922 super(ChangeishFilter, self).__init__(
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001923 required_approvals=required_approvals,
1924 reject_approvals=reject_approvals)
James E. Blair11041d22014-05-02 14:49:53 -07001925 self.open = open
Clark Boylana9702ad2014-05-08 17:17:24 -07001926 self.current_patchset = current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001927 self.statuses = statuses
James E. Blair11041d22014-05-02 14:49:53 -07001928
1929 def __repr__(self):
1930 ret = '<ChangeishFilter'
1931
1932 if self.open is not None:
1933 ret += ' open: %s' % self.open
Clark Boylana9702ad2014-05-08 17:17:24 -07001934 if self.current_patchset is not None:
1935 ret += ' current-patchset: %s' % self.current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07001936 if self.statuses:
1937 ret += ' statuses: %s' % ', '.join(self.statuses)
James E. Blair5bf78a32015-07-30 18:08:24 +00001938 if self.required_approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001939 ret += (' required_approvals: %s' %
1940 str(self.required_approvals))
1941 if self.reject_approvals:
1942 ret += (' reject_approvals: %s' %
1943 str(self.reject_approvals))
James E. Blair11041d22014-05-02 14:49:53 -07001944 ret += '>'
1945
1946 return ret
1947
1948 def matches(self, change):
1949 if self.open is not None:
1950 if self.open != change.open:
1951 return False
1952
Clark Boylana9702ad2014-05-08 17:17:24 -07001953 if self.current_patchset is not None:
1954 if self.current_patchset != change.is_current_patchset:
1955 return False
1956
James E. Blair11041d22014-05-02 14:49:53 -07001957 if self.statuses:
1958 if change.status not in self.statuses:
1959 return False
1960
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001961 # required approvals are ANDed (reject approvals are ORed)
1962 if not self.matchesApprovals(change):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001963 return False
James E. Blair11041d22014-05-02 14:49:53 -07001964
1965 return True
1966
1967
James E. Blairb97ed802015-12-21 15:55:35 -08001968class ProjectPipelineConfig(object):
1969 # Represents a project cofiguration in the context of a pipeline
1970 def __init__(self):
1971 self.job_tree = None
1972 self.queue_name = None
Adam Gandelman8bd57102016-12-02 12:58:42 -08001973 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08001974
1975
1976class ProjectConfig(object):
1977 # Represents a project cofiguration
1978 def __init__(self, name):
1979 self.name = name
Adam Gandelman8bd57102016-12-02 12:58:42 -08001980 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08001981 self.pipelines = {}
1982
1983
James E. Blaird8e778f2015-12-22 14:09:20 -08001984class UnparsedAbideConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001985 """A collection of yaml lists that has not yet been parsed into objects.
1986
1987 An Abide is a collection of tenants.
1988 """
1989
James E. Blaird8e778f2015-12-22 14:09:20 -08001990 def __init__(self):
1991 self.tenants = []
1992
1993 def extend(self, conf):
1994 if isinstance(conf, UnparsedAbideConfig):
1995 self.tenants.extend(conf.tenants)
1996 return
1997
1998 if not isinstance(conf, list):
1999 raise Exception("Configuration items must be in the form of "
2000 "a list of dictionaries (when parsing %s)" %
2001 (conf,))
2002 for item in conf:
2003 if not isinstance(item, dict):
2004 raise Exception("Configuration items must be in the form of "
2005 "a list of dictionaries (when parsing %s)" %
2006 (conf,))
2007 if len(item.keys()) > 1:
2008 raise Exception("Configuration item dictionaries must have "
2009 "a single key (when parsing %s)" %
2010 (conf,))
2011 key, value = item.items()[0]
2012 if key == 'tenant':
2013 self.tenants.append(value)
2014 else:
2015 raise Exception("Configuration item not recognized "
2016 "(when parsing %s)" %
2017 (conf,))
2018
2019
2020class UnparsedTenantConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002021 """A collection of yaml lists that has not yet been parsed into objects."""
2022
James E. Blaird8e778f2015-12-22 14:09:20 -08002023 def __init__(self):
2024 self.pipelines = []
2025 self.jobs = []
2026 self.project_templates = []
James E. Blairff555742017-02-19 11:34:27 -08002027 self.projects = {}
James E. Blaira98340f2016-09-02 11:33:49 -07002028 self.nodesets = []
James E. Blaird8e778f2015-12-22 14:09:20 -08002029
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002030 def copy(self):
2031 r = UnparsedTenantConfig()
2032 r.pipelines = copy.deepcopy(self.pipelines)
2033 r.jobs = copy.deepcopy(self.jobs)
2034 r.project_templates = copy.deepcopy(self.project_templates)
2035 r.projects = copy.deepcopy(self.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07002036 r.nodesets = copy.deepcopy(self.nodesets)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002037 return r
2038
James E. Blaircdab2032017-02-01 09:09:29 -08002039 def extend(self, conf, source_context=None):
James E. Blaird8e778f2015-12-22 14:09:20 -08002040 if isinstance(conf, UnparsedTenantConfig):
2041 self.pipelines.extend(conf.pipelines)
2042 self.jobs.extend(conf.jobs)
2043 self.project_templates.extend(conf.project_templates)
James E. Blairff555742017-02-19 11:34:27 -08002044 for k, v in conf.projects.items():
2045 self.projects.setdefault(k, []).extend(v)
James E. Blaira98340f2016-09-02 11:33:49 -07002046 self.nodesets.extend(conf.nodesets)
James E. Blaird8e778f2015-12-22 14:09:20 -08002047 return
2048
2049 if not isinstance(conf, list):
2050 raise Exception("Configuration items must be in the form of "
2051 "a list of dictionaries (when parsing %s)" %
2052 (conf,))
James E. Blaircdab2032017-02-01 09:09:29 -08002053
2054 if source_context is None:
2055 raise Exception("A source context must be provided "
2056 "(when parsing %s)" % (conf,))
2057
James E. Blaird8e778f2015-12-22 14:09:20 -08002058 for item in conf:
2059 if not isinstance(item, dict):
2060 raise Exception("Configuration items must be in the form of "
2061 "a list of dictionaries (when parsing %s)" %
2062 (conf,))
2063 if len(item.keys()) > 1:
2064 raise Exception("Configuration item dictionaries must have "
2065 "a single key (when parsing %s)" %
2066 (conf,))
2067 key, value = item.items()[0]
James E. Blair66b274e2017-01-31 14:47:52 -08002068 if key in ['project', 'project-template', 'job']:
James E. Blaircdab2032017-02-01 09:09:29 -08002069 value['_source_context'] = source_context
James E. Blair66b274e2017-01-31 14:47:52 -08002070 if key == 'project':
James E. Blairff555742017-02-19 11:34:27 -08002071 name = value['name']
2072 self.projects.setdefault(name, []).append(value)
James E. Blair66b274e2017-01-31 14:47:52 -08002073 elif key == 'job':
James E. Blaird8e778f2015-12-22 14:09:20 -08002074 self.jobs.append(value)
2075 elif key == 'project-template':
2076 self.project_templates.append(value)
2077 elif key == 'pipeline':
2078 self.pipelines.append(value)
James E. Blaira98340f2016-09-02 11:33:49 -07002079 elif key == 'nodeset':
2080 self.nodesets.append(value)
James E. Blaird8e778f2015-12-22 14:09:20 -08002081 else:
Morgan Fainberg4245a422016-08-05 16:20:12 -07002082 raise Exception("Configuration item `%s` not recognized "
James E. Blaird8e778f2015-12-22 14:09:20 -08002083 "(when parsing %s)" %
Morgan Fainberg4245a422016-08-05 16:20:12 -07002084 (item, conf,))
James E. Blaird8e778f2015-12-22 14:09:20 -08002085
2086
James E. Blaireff88162013-07-01 12:44:14 -04002087class Layout(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002088 """Holds all of the Pipelines."""
2089
James E. Blaireff88162013-07-01 12:44:14 -04002090 def __init__(self):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002091 self.tenant = None
James E. Blairb97ed802015-12-21 15:55:35 -08002092 self.project_configs = {}
2093 self.project_templates = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07002094 self.pipelines = OrderedDict()
James E. Blair83005782015-12-11 14:46:03 -08002095 # This is a dictionary of name -> [jobs]. The first element
2096 # of the list is the first job added with that name. It is
2097 # the reference definition for a given job. Subsequent
2098 # elements are aspects of that job with different matchers
2099 # that override some attribute of the job. These aspects all
2100 # inherit from the reference definition.
James E. Blairfef88ec2016-11-30 08:38:27 -08002101 self.jobs = {'noop': [Job('noop')]}
James E. Blaira98340f2016-09-02 11:33:49 -07002102 self.nodesets = {}
James E. Blaireff88162013-07-01 12:44:14 -04002103
2104 def getJob(self, name):
2105 if name in self.jobs:
James E. Blair83005782015-12-11 14:46:03 -08002106 return self.jobs[name][0]
James E. Blairb97ed802015-12-21 15:55:35 -08002107 raise Exception("Job %s not defined" % (name,))
James E. Blair83005782015-12-11 14:46:03 -08002108
2109 def getJobs(self, name):
2110 return self.jobs.get(name, [])
2111
2112 def addJob(self, job):
James E. Blair4317e9f2016-07-15 10:05:47 -07002113 # We can have multiple variants of a job all with the same
2114 # name, but these variants must all be defined in the same repo.
James E. Blaircdab2032017-02-01 09:09:29 -08002115 prior_jobs = [j for j in self.getJobs(job.name) if
2116 j.source_context.project !=
2117 job.source_context.project]
James E. Blair4317e9f2016-07-15 10:05:47 -07002118 if prior_jobs:
2119 raise Exception("Job %s in %s is not permitted to shadow "
James E. Blaircdab2032017-02-01 09:09:29 -08002120 "job %s in %s" % (
2121 job,
2122 job.source_context.project,
2123 prior_jobs[0],
2124 prior_jobs[0].source_context.project))
James E. Blair4317e9f2016-07-15 10:05:47 -07002125
James E. Blair83005782015-12-11 14:46:03 -08002126 if job.name in self.jobs:
2127 self.jobs[job.name].append(job)
2128 else:
2129 self.jobs[job.name] = [job]
2130
James E. Blaira98340f2016-09-02 11:33:49 -07002131 def addNodeSet(self, nodeset):
2132 if nodeset.name in self.nodesets:
2133 raise Exception("NodeSet %s already defined" % (nodeset.name,))
2134 self.nodesets[nodeset.name] = nodeset
2135
James E. Blair83005782015-12-11 14:46:03 -08002136 def addPipeline(self, pipeline):
2137 self.pipelines[pipeline.name] = pipeline
James E. Blair59fdbac2015-12-07 17:08:06 -08002138
James E. Blairb97ed802015-12-21 15:55:35 -08002139 def addProjectTemplate(self, project_template):
2140 self.project_templates[project_template.name] = project_template
2141
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002142 def addProjectConfig(self, project_config, update_pipeline=True):
James E. Blairb97ed802015-12-21 15:55:35 -08002143 self.project_configs[project_config.name] = project_config
2144 # TODOv3(jeblair): tidy up the relationship between pipelines
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002145 # and projects and projectconfigs. Specifically, move
2146 # job_trees out of the pipeline since they are more dynamic
2147 # than pipelines. Remove the update_pipeline argument
2148 if not update_pipeline:
2149 return
James E. Blairb97ed802015-12-21 15:55:35 -08002150 for pipeline_name, pipeline_config in project_config.pipelines.items():
2151 pipeline = self.pipelines[pipeline_name]
2152 project = pipeline.source.getProject(project_config.name)
2153 pipeline.job_trees[project] = pipeline_config.job_tree
2154
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002155 def _createJobTree(self, change, job_trees, parent):
2156 for tree in job_trees:
2157 job = tree.job
2158 if not job.changeMatches(change):
2159 continue
James E. Blaira7f51ca2017-02-07 16:01:26 -08002160 frozen_job = None
2161 matched = False
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002162 for variant in self.getJobs(job.name):
2163 if variant.changeMatches(change):
James E. Blaira7f51ca2017-02-07 16:01:26 -08002164 if frozen_job is None:
2165 frozen_job = variant.copy()
2166 frozen_job.setRun()
2167 else:
2168 frozen_job.applyVariant(variant)
2169 matched = True
2170 if not matched:
James E. Blair6e85c2b2016-11-21 16:47:01 -08002171 # A change must match at least one defined job variant
2172 # (that is to say that it must match more than just
2173 # the job that is defined in the tree).
2174 continue
James E. Blaira7f51ca2017-02-07 16:01:26 -08002175 # If the job does not allow auth inheritance, do not allow
2176 # the project-pipeline variant to update its execution
2177 # attributes.
2178 if frozen_job.auth and not frozen_job.auth.get('inherit'):
2179 frozen_job.final = True
2180 frozen_job.applyVariant(job)
2181 frozen_tree = JobTree(frozen_job)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002182 parent.job_trees.append(frozen_tree)
2183 self._createJobTree(change, tree.job_trees, frozen_tree)
2184
2185 def createJobTree(self, item):
Paul Belanger160cb8e2016-11-11 19:04:24 -05002186 project_config = self.project_configs.get(
2187 item.change.project.name, None)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002188 ret = JobTree(None)
Paul Belanger15e3e202016-10-14 16:27:34 -04002189 # NOTE(pabelanger): It is possible for a foreign project not to have a
2190 # configured pipeline, if so return an empty JobTree.
Paul Belanger160cb8e2016-11-11 19:04:24 -05002191 if project_config and item.pipeline.name in project_config.pipelines:
Paul Belanger15e3e202016-10-14 16:27:34 -04002192 project_tree = \
2193 project_config.pipelines[item.pipeline.name].job_tree
2194 self._createJobTree(item.change, project_tree.job_trees, ret)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002195 return ret
2196
James E. Blair59fdbac2015-12-07 17:08:06 -08002197
2198class Tenant(object):
2199 def __init__(self, name):
2200 self.name = name
2201 self.layout = None
James E. Blair646322f2017-01-27 15:50:34 -08002202 # The unparsed configuration from the main zuul config for
2203 # this tenant.
2204 self.unparsed_config = None
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002205 # The list of repos from which we will read main
2206 # configuration. (source, project)
2207 self.config_repos = []
2208 # The unparsed config from those repos.
2209 self.config_repos_config = None
2210 # The list of projects from which we will read in-repo
2211 # configuration. (source, project)
2212 self.project_repos = []
2213 # The unparsed config from those repos.
2214 self.project_repos_config = None
James E. Blair5ac93842017-01-20 06:47:34 -08002215 # A mapping of source -> {config_repos: {}, project_repos: {}}
2216 self.sources = {}
2217
2218 def addConfigRepo(self, source, project):
2219 sd = self.sources.setdefault(source.name,
2220 {'config_repos': {},
2221 'project_repos': {}})
2222 sd['config_repos'][project.name] = project
2223
2224 def addProjectRepo(self, source, project):
2225 sd = self.sources.setdefault(source.name,
2226 {'config_repos': {},
2227 'project_repos': {}})
2228 sd['project_repos'][project.name] = project
2229
2230 def getRepo(self, source, project_name):
2231 """Get a project given a source and project name
2232
Monty Taylore6562aa2017-02-20 07:37:39 -05002233 Returns a tuple (trusted, project) or (None, None) if the
James E. Blair5ac93842017-01-20 06:47:34 -08002234 project is not found.
2235
Monty Taylore6562aa2017-02-20 07:37:39 -05002236 Trusted indicates the project is a config repo.
James E. Blair5ac93842017-01-20 06:47:34 -08002237
2238 """
2239
2240 sd = self.sources.get(source)
2241 if not sd:
2242 return (None, None)
2243 if project_name in sd['config_repos']:
2244 return (True, sd['config_repos'][project_name])
2245 if project_name in sd['project_repos']:
2246 return (False, sd['project_repos'][project_name])
2247 return (None, None)
James E. Blair59fdbac2015-12-07 17:08:06 -08002248
2249
2250class Abide(object):
2251 def __init__(self):
2252 self.tenants = OrderedDict()
James E. Blairce8a2132016-05-19 15:21:52 -07002253
2254
2255class JobTimeData(object):
2256 format = 'B10H10H10B'
2257 version = 0
2258
2259 def __init__(self, path):
2260 self.path = path
2261 self.success_times = [0 for x in range(10)]
2262 self.failure_times = [0 for x in range(10)]
2263 self.results = [0 for x in range(10)]
2264
2265 def load(self):
2266 if not os.path.exists(self.path):
2267 return
2268 with open(self.path) as f:
2269 data = struct.unpack(self.format, f.read())
2270 version = data[0]
2271 if version != self.version:
2272 raise Exception("Unkown data version")
2273 self.success_times = list(data[1:11])
2274 self.failure_times = list(data[11:21])
2275 self.results = list(data[21:32])
2276
2277 def save(self):
2278 tmpfile = self.path + '.tmp'
2279 data = [self.version]
2280 data.extend(self.success_times)
2281 data.extend(self.failure_times)
2282 data.extend(self.results)
2283 data = struct.pack(self.format, *data)
2284 with open(tmpfile, 'w') as f:
2285 f.write(data)
2286 os.rename(tmpfile, self.path)
2287
2288 def add(self, elapsed, result):
2289 elapsed = int(elapsed)
2290 if result == 'SUCCESS':
2291 self.success_times.append(elapsed)
2292 self.success_times.pop(0)
2293 result = 0
2294 else:
2295 self.failure_times.append(elapsed)
2296 self.failure_times.pop(0)
2297 result = 1
2298 self.results.append(result)
2299 self.results.pop(0)
2300
2301 def getEstimatedTime(self):
2302 times = [x for x in self.success_times if x]
2303 if times:
2304 return float(sum(times)) / len(times)
2305 return 0.0
2306
2307
2308class TimeDataBase(object):
2309 def __init__(self, root):
2310 self.root = root
2311 self.jobs = {}
2312
2313 def _getTD(self, name):
2314 td = self.jobs.get(name)
2315 if not td:
2316 td = JobTimeData(os.path.join(self.root, name))
2317 self.jobs[name] = td
2318 td.load()
2319 return td
2320
2321 def getEstimatedTime(self, name):
2322 return self._getTD(name).getEstimatedTime()
2323
2324 def update(self, name, elapsed, result):
2325 td = self._getTD(name)
2326 td.add(elapsed, result)
2327 td.save()