blob: 77c5990def86ccb6e7ec4c35945273d6f7250336 [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
Tobias Henkel9a0e1942017-03-20 16:16:02 +010017
18import logging
James E. Blairce8a2132016-05-19 15:21:52 -070019import os
James E. Blairee743612012-05-29 14:49:32 -070020import re
James E. Blairce8a2132016-05-19 15:21:52 -070021import struct
James E. Blairff986a12012-05-30 14:56:51 -070022import time
James E. Blair4886cc12012-07-18 15:39:41 -070023from uuid import uuid4
James E. Blair5a9918a2013-08-27 10:06:27 -070024import extras
25
James E. Blair5ac93842017-01-20 06:47:34 -080026import six
27
James E. Blair5a9918a2013-08-27 10:06:27 -070028OrderedDict = extras.try_imports(['collections.OrderedDict',
29 'ordereddict.OrderedDict'])
James E. Blair4886cc12012-07-18 15:39:41 -070030
31
K Jonathan Harkerf95e7232015-04-29 13:33:16 -070032EMPTY_GIT_REF = '0' * 40 # git sha of all zeros, used during creates/deletes
33
James E. Blair19deff22013-08-25 13:17:35 -070034MERGER_MERGE = 1 # "git merge"
35MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
36MERGER_CHERRY_PICK = 3 # "git cherry-pick"
37
38MERGER_MAP = {
39 'merge': MERGER_MERGE,
40 'merge-resolve': MERGER_MERGE_RESOLVE,
41 'cherry-pick': MERGER_CHERRY_PICK,
42}
James E. Blairee743612012-05-29 14:49:32 -070043
James E. Blair64ed6f22013-07-10 14:07:23 -070044PRECEDENCE_NORMAL = 0
45PRECEDENCE_LOW = 1
46PRECEDENCE_HIGH = 2
47
48PRECEDENCE_MAP = {
49 None: PRECEDENCE_NORMAL,
50 'low': PRECEDENCE_LOW,
51 'normal': PRECEDENCE_NORMAL,
52 'high': PRECEDENCE_HIGH,
53}
54
James E. Blair803e94f2017-01-06 09:18:59 -080055# Request states
56STATE_REQUESTED = 'requested'
57STATE_PENDING = 'pending'
58STATE_FULFILLED = 'fulfilled'
59STATE_FAILED = 'failed'
60REQUEST_STATES = set([STATE_REQUESTED,
61 STATE_PENDING,
62 STATE_FULFILLED,
63 STATE_FAILED])
64
65# Node states
66STATE_BUILDING = 'building'
67STATE_TESTING = 'testing'
68STATE_READY = 'ready'
69STATE_IN_USE = 'in-use'
70STATE_USED = 'used'
71STATE_HOLD = 'hold'
72STATE_DELETING = 'deleting'
73NODE_STATES = set([STATE_BUILDING,
74 STATE_TESTING,
75 STATE_READY,
76 STATE_IN_USE,
77 STATE_USED,
78 STATE_HOLD,
79 STATE_DELETING])
80
James E. Blair1e8dd892012-05-30 09:15:05 -070081
James E. Blairc053d022014-01-22 14:57:33 -080082def time_to_seconds(s):
83 if s.endswith('s'):
84 return int(s[:-1])
85 if s.endswith('m'):
86 return int(s[:-1]) * 60
87 if s.endswith('h'):
88 return int(s[:-1]) * 60 * 60
89 if s.endswith('d'):
90 return int(s[:-1]) * 24 * 60 * 60
91 if s.endswith('w'):
92 return int(s[:-1]) * 7 * 24 * 60 * 60
93 raise Exception("Unable to parse time value: %s" % s)
94
95
James E. Blair11041d22014-05-02 14:49:53 -070096def normalizeCategory(name):
97 name = name.lower()
98 return re.sub(' ', '-', name)
99
100
Joshua Hesketh58419cb2017-02-24 13:09:22 -0500101class Attributes(object):
102 """A class to hold attributes for string formatting."""
103
104 def __init__(self, **kw):
105 setattr(self, '__dict__', kw)
106
107
James E. Blair4aea70c2012-07-26 14:23:24 -0700108class Pipeline(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700109 """A configuration that ties triggers, reporters, managers and sources.
110
Monty Taylor82dfd412016-07-29 12:01:28 -0700111 Source
112 Where changes should come from. It is a named connection to
Monty Taylora42a55b2016-07-29 07:53:33 -0700113 an external service defined in zuul.conf
Monty Taylor82dfd412016-07-29 12:01:28 -0700114
115 Trigger
116 A description of which events should be processed
117
118 Manager
119 Responsible for enqueing and dequeing Changes
120
121 Reporter
122 Communicates success and failure results somewhere
Monty Taylora42a55b2016-07-29 07:53:33 -0700123 """
James E. Blair83005782015-12-11 14:46:03 -0800124 def __init__(self, name, layout):
James E. Blair4aea70c2012-07-26 14:23:24 -0700125 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800126 self.layout = layout
James E. Blair8dbd56a2012-12-22 10:55:10 -0800127 self.description = None
James E. Blair56370192013-01-14 15:47:28 -0800128 self.failure_message = None
Joshua Heskethb7179772014-01-30 23:30:46 +1100129 self.merge_failure_message = None
James E. Blair56370192013-01-14 15:47:28 -0800130 self.success_message = None
Joshua Hesketh3979e3e2014-03-04 11:21:10 +1100131 self.footer_message = None
James E. Blair60af7f42016-03-11 16:11:06 -0800132 self.start_message = None
James E. Blaird2348362017-03-17 13:59:35 -0700133 self.allow_secrets = False
James E. Blair2fa50962013-01-30 21:50:41 -0800134 self.dequeue_on_new_patchset = True
James E. Blair17dd6772015-02-09 14:45:18 -0800135 self.ignore_dependencies = False
James E. Blair4aea70c2012-07-26 14:23:24 -0700136 self.manager = None
James E. Blaire0487072012-08-29 17:38:31 -0700137 self.queues = []
James E. Blair64ed6f22013-07-10 14:07:23 -0700138 self.precedence = PRECEDENCE_NORMAL
James E. Blairc0dedf82014-08-06 09:37:52 -0700139 self.source = None
James E. Blair83005782015-12-11 14:46:03 -0800140 self.triggers = []
Joshua Hesketh352264b2015-08-11 23:42:08 +1000141 self.start_actions = []
142 self.success_actions = []
143 self.failure_actions = []
144 self.merge_failure_actions = []
145 self.disabled_actions = []
Joshua Hesketh89e829d2015-02-10 16:29:45 +1100146 self.disable_at = None
147 self._consecutive_failures = 0
148 self._disabled = False
Clark Boylan7603a372014-01-21 11:43:20 -0800149 self.window = None
150 self.window_floor = None
151 self.window_increase_type = None
152 self.window_increase_factor = None
153 self.window_decrease_type = None
154 self.window_decrease_factor = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700155
James E. Blair83005782015-12-11 14:46:03 -0800156 @property
157 def actions(self):
158 return (
159 self.start_actions +
160 self.success_actions +
161 self.failure_actions +
162 self.merge_failure_actions +
163 self.disabled_actions
164 )
165
James E. Blaird09c17a2012-08-07 09:23:14 -0700166 def __repr__(self):
167 return '<Pipeline %s>' % self.name
168
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100169 def getSafeAttributes(self):
170 return Attributes(name=self.name)
171
James E. Blair4aea70c2012-07-26 14:23:24 -0700172 def setManager(self, manager):
173 self.manager = manager
174
James E. Blaire0487072012-08-29 17:38:31 -0700175 def addQueue(self, queue):
176 self.queues.append(queue)
177
178 def getQueue(self, project):
179 for queue in self.queues:
180 if project in queue.projects:
181 return queue
182 return None
183
James E. Blairbfb8e042014-12-30 17:01:44 -0800184 def removeQueue(self, queue):
185 self.queues.remove(queue)
186
James E. Blaire0487072012-08-29 17:38:31 -0700187 def getChangesInQueue(self):
188 changes = []
189 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700190 changes.extend([x.change for x in shared_queue.queue])
James E. Blaire0487072012-08-29 17:38:31 -0700191 return changes
192
James E. Blairfee8d652013-06-07 08:57:52 -0700193 def getAllItems(self):
194 items = []
James E. Blaire0487072012-08-29 17:38:31 -0700195 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700196 items.extend(shared_queue.queue)
James E. Blairfee8d652013-06-07 08:57:52 -0700197 return items
James E. Blaire0487072012-08-29 17:38:31 -0700198
James E. Blair800e7ff2017-03-17 16:06:52 -0700199 def formatStatusJSON(self):
James E. Blair8dbd56a2012-12-22 10:55:10 -0800200 j_pipeline = dict(name=self.name,
201 description=self.description)
202 j_queues = []
203 j_pipeline['change_queues'] = j_queues
204 for queue in self.queues:
205 j_queue = dict(name=queue.name)
206 j_queues.append(j_queue)
207 j_queue['heads'] = []
Clark Boylanaf2476f2014-01-23 14:47:36 -0800208 j_queue['window'] = queue.window
James E. Blair972e3c72013-08-29 12:04:55 -0700209
210 j_changes = []
211 for e in queue.queue:
212 if not e.item_ahead:
213 if j_changes:
214 j_queue['heads'].append(j_changes)
215 j_changes = []
James E. Blair800e7ff2017-03-17 16:06:52 -0700216 j_changes.append(e.formatJSON())
James E. Blair972e3c72013-08-29 12:04:55 -0700217 if (len(j_changes) > 1 and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000218 (j_changes[-2]['remaining_time'] is not None) and
219 (j_changes[-1]['remaining_time'] is not None)):
James E. Blair972e3c72013-08-29 12:04:55 -0700220 j_changes[-1]['remaining_time'] = max(
221 j_changes[-2]['remaining_time'],
222 j_changes[-1]['remaining_time'])
223 if j_changes:
James E. Blair8dbd56a2012-12-22 10:55:10 -0800224 j_queue['heads'].append(j_changes)
225 return j_pipeline
226
James E. Blair4aea70c2012-07-26 14:23:24 -0700227
James E. Blairee743612012-05-29 14:49:32 -0700228class ChangeQueue(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700229 """A ChangeQueue contains Changes to be processed related projects.
230
Monty Taylor82dfd412016-07-29 12:01:28 -0700231 A Pipeline with a DependentPipelineManager has multiple parallel
232 ChangeQueues shared by different projects. For instance, there may a
233 ChangeQueue shared by interrelated projects foo and bar, and a second queue
234 for independent project baz.
Monty Taylora42a55b2016-07-29 07:53:33 -0700235
Monty Taylor82dfd412016-07-29 12:01:28 -0700236 A Pipeline with an IndependentPipelineManager puts every Change into its
237 own ChangeQueue
Monty Taylora42a55b2016-07-29 07:53:33 -0700238
239 The ChangeQueue Window is inspired by TCP windows and controlls how many
240 Changes in a given ChangeQueue will be considered active and ready to
241 be processed. If a Change succeeds, the Window is increased by
242 `window_increase_factor`. If a Change fails, the Window is decreased by
243 `window_decrease_factor`.
244 """
James E. Blairbfb8e042014-12-30 17:01:44 -0800245 def __init__(self, pipeline, window=0, window_floor=1,
Clark Boylan7603a372014-01-21 11:43:20 -0800246 window_increase_type='linear', window_increase_factor=1,
James E. Blair0dcef7a2016-08-19 09:35:17 -0700247 window_decrease_type='exponential', window_decrease_factor=2,
248 name=None):
James E. Blair4aea70c2012-07-26 14:23:24 -0700249 self.pipeline = pipeline
James E. Blair0dcef7a2016-08-19 09:35:17 -0700250 if name:
251 self.name = name
252 else:
253 self.name = ''
James E. Blairee743612012-05-29 14:49:32 -0700254 self.projects = []
255 self._jobs = set()
256 self.queue = []
Clark Boylan7603a372014-01-21 11:43:20 -0800257 self.window = window
258 self.window_floor = window_floor
259 self.window_increase_type = window_increase_type
260 self.window_increase_factor = window_increase_factor
261 self.window_decrease_type = window_decrease_type
262 self.window_decrease_factor = window_decrease_factor
James E. Blairee743612012-05-29 14:49:32 -0700263
James E. Blair9f9667e2012-06-12 17:51:08 -0700264 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700265 return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
James E. Blairee743612012-05-29 14:49:32 -0700266
267 def getJobs(self):
268 return self._jobs
269
270 def addProject(self, project):
271 if project not in self.projects:
272 self.projects.append(project)
James E. Blairc8a1e052014-02-25 09:29:26 -0800273
James E. Blair0dcef7a2016-08-19 09:35:17 -0700274 if not self.name:
275 self.name = project.name
James E. Blairee743612012-05-29 14:49:32 -0700276
277 def enqueueChange(self, change):
James E. Blairbfb8e042014-12-30 17:01:44 -0800278 item = QueueItem(self, change)
James E. Blaircdccd972013-07-01 12:10:22 -0700279 self.enqueueItem(item)
280 item.enqueue_time = time.time()
281 return item
282
283 def enqueueItem(self, item):
James E. Blair4a035d92014-01-23 13:10:48 -0800284 item.pipeline = self.pipeline
James E. Blairbfb8e042014-12-30 17:01:44 -0800285 item.queue = self
286 if self.queue:
James E. Blairfee8d652013-06-07 08:57:52 -0700287 item.item_ahead = self.queue[-1]
James E. Blair972e3c72013-08-29 12:04:55 -0700288 item.item_ahead.items_behind.append(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700289 self.queue.append(item)
James E. Blairee743612012-05-29 14:49:32 -0700290
James E. Blairfee8d652013-06-07 08:57:52 -0700291 def dequeueItem(self, item):
292 if item in self.queue:
293 self.queue.remove(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700294 if item.item_ahead:
James E. Blair972e3c72013-08-29 12:04:55 -0700295 item.item_ahead.items_behind.remove(item)
296 for item_behind in item.items_behind:
297 if item.item_ahead:
298 item.item_ahead.items_behind.append(item_behind)
299 item_behind.item_ahead = item.item_ahead
James E. Blairfee8d652013-06-07 08:57:52 -0700300 item.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700301 item.items_behind = []
James E. Blairfee8d652013-06-07 08:57:52 -0700302 item.dequeue_time = time.time()
James E. Blaire0487072012-08-29 17:38:31 -0700303
James E. Blair972e3c72013-08-29 12:04:55 -0700304 def moveItem(self, item, item_ahead):
James E. Blair972e3c72013-08-29 12:04:55 -0700305 if item.item_ahead == item_ahead:
306 return False
307 # Remove from current location
308 if item.item_ahead:
309 item.item_ahead.items_behind.remove(item)
310 for item_behind in item.items_behind:
311 if item.item_ahead:
312 item.item_ahead.items_behind.append(item_behind)
313 item_behind.item_ahead = item.item_ahead
314 # Add to new location
315 item.item_ahead = item_ahead
James E. Blair00451262013-09-20 11:40:17 -0700316 item.items_behind = []
James E. Blair972e3c72013-08-29 12:04:55 -0700317 if item.item_ahead:
318 item.item_ahead.items_behind.append(item)
319 return True
James E. Blairee743612012-05-29 14:49:32 -0700320
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800321 def isActionable(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800322 if self.window:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800323 return item in self.queue[:self.window]
Clark Boylan7603a372014-01-21 11:43:20 -0800324 else:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800325 return True
Clark Boylan7603a372014-01-21 11:43:20 -0800326
327 def increaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800328 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800329 if self.window_increase_type == 'linear':
330 self.window += self.window_increase_factor
331 elif self.window_increase_type == 'exponential':
332 self.window *= self.window_increase_factor
333
334 def decreaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800335 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800336 if self.window_decrease_type == 'linear':
337 self.window = max(
338 self.window_floor,
339 self.window - self.window_decrease_factor)
340 elif self.window_decrease_type == 'exponential':
341 self.window = max(
342 self.window_floor,
Morgan Fainbergfdae2252016-06-07 20:13:20 -0700343 int(self.window / self.window_decrease_factor))
James E. Blairee743612012-05-29 14:49:32 -0700344
James E. Blair1e8dd892012-05-30 09:15:05 -0700345
James E. Blair4aea70c2012-07-26 14:23:24 -0700346class Project(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700347 """A Project represents a git repository such as openstack/nova."""
348
James E. Blaircf440a22016-07-15 09:11:58 -0700349 # NOTE: Projects should only be instantiated via a Source object
350 # so that they are associated with and cached by their Connection.
351 # This makes a Project instance a unique identifier for a given
352 # project from a given source.
353
James E. Blair0a899752017-03-29 13:22:16 -0700354 def __init__(self, name, source, foreign=False):
James E. Blair4aea70c2012-07-26 14:23:24 -0700355 self.name = name
James E. Blair8a395f92017-03-30 11:15:33 -0700356 self.source = source
James E. Blair0a899752017-03-29 13:22:16 -0700357 self.connection_name = source.connection.connection_name
358 self.canonical_hostname = source.canonical_hostname
James E. Blairc2a54fd2017-03-29 15:19:26 -0700359 self.canonical_name = source.canonical_hostname + '/' + name
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000360 # foreign projects are those referenced in dependencies
361 # of layout projects, this should matter
362 # when deciding whether to enqueue their changes
James E. Blaircf440a22016-07-15 09:11:58 -0700363 # TODOv3 (jeblair): re-add support for foreign projects if needed
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000364 self.foreign = foreign
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700365 self.unparsed_config = None
James E. Blaire3162022017-02-20 16:47:27 -0500366 self.unparsed_branch_config = {} # branch -> UnparsedTenantConfig
James E. Blair4aea70c2012-07-26 14:23:24 -0700367
368 def __str__(self):
369 return self.name
370
371 def __repr__(self):
372 return '<Project %s>' % (self.name)
373
374
James E. Blair34776ee2016-08-25 13:53:54 -0700375class Node(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700376 """A single node for use by a job.
377
378 This may represent a request for a node, or an actual node
379 provided by Nodepool.
380 """
381
James E. Blair34776ee2016-08-25 13:53:54 -0700382 def __init__(self, name, image):
383 self.name = name
384 self.image = image
James E. Blaircbf43672017-01-04 14:33:41 -0800385 self.id = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800386 self.lock = None
387 # Attributes from Nodepool
388 self._state = 'unknown'
389 self.state_time = time.time()
Monty Taylor56f61332017-04-11 05:38:12 -0500390 self.interface_ip = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800391 self.public_ipv4 = None
392 self.private_ipv4 = None
393 self.public_ipv6 = None
James E. Blaircacdf2b2017-01-04 13:14:37 -0800394 self._keys = []
Paul Belanger30ba93a2017-03-16 16:28:10 -0400395 self.az = None
396 self.provider = None
397 self.region = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800398
399 @property
400 def state(self):
401 return self._state
402
403 @state.setter
404 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800405 if value not in NODE_STATES:
406 raise TypeError("'%s' is not a valid state" % value)
James E. Blaira38c28e2017-01-04 10:33:20 -0800407 self._state = value
408 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700409
410 def __repr__(self):
James E. Blaircbf43672017-01-04 14:33:41 -0800411 return '<Node %s %s:%s>' % (self.id, self.name, self.image)
James E. Blair34776ee2016-08-25 13:53:54 -0700412
James E. Blair0d952152017-02-07 17:14:44 -0800413 def __ne__(self, other):
414 return not self.__eq__(other)
415
416 def __eq__(self, other):
417 if not isinstance(other, Node):
418 return False
419 return (self.name == other.name and
420 self.image == other.image and
421 self.id == other.id)
422
James E. Blaircacdf2b2017-01-04 13:14:37 -0800423 def toDict(self):
424 d = {}
425 d['state'] = self.state
426 for k in self._keys:
427 d[k] = getattr(self, k)
428 return d
429
James E. Blaira38c28e2017-01-04 10:33:20 -0800430 def updateFromDict(self, data):
431 self._state = data['state']
James E. Blaircacdf2b2017-01-04 13:14:37 -0800432 keys = []
433 for k, v in data.items():
434 if k == 'state':
435 continue
436 keys.append(k)
437 setattr(self, k, v)
438 self._keys = keys
James E. Blaira38c28e2017-01-04 10:33:20 -0800439
James E. Blair34776ee2016-08-25 13:53:54 -0700440
James E. Blaira98340f2016-09-02 11:33:49 -0700441class NodeSet(object):
442 """A set of nodes.
443
444 In configuration, NodeSets are attributes of Jobs indicating that
445 a Job requires nodes matching this description.
446
447 They may appear as top-level configuration objects and be named,
448 or they may appears anonymously in in-line job definitions.
449 """
450
451 def __init__(self, name=None):
452 self.name = name or ''
453 self.nodes = OrderedDict()
454
James E. Blair1774dd52017-02-03 10:52:32 -0800455 def __ne__(self, other):
456 return not self.__eq__(other)
457
458 def __eq__(self, other):
459 if not isinstance(other, NodeSet):
460 return False
461 return (self.name == other.name and
462 self.nodes == other.nodes)
463
James E. Blaircbf43672017-01-04 14:33:41 -0800464 def copy(self):
465 n = NodeSet(self.name)
466 for name, node in self.nodes.items():
467 n.addNode(Node(node.name, node.image))
468 return n
469
James E. Blaira98340f2016-09-02 11:33:49 -0700470 def addNode(self, node):
471 if node.name in self.nodes:
472 raise Exception("Duplicate node in %s" % (self,))
473 self.nodes[node.name] = node
474
James E. Blair0eaad552016-09-02 12:09:54 -0700475 def getNodes(self):
476 return self.nodes.values()
477
James E. Blaira98340f2016-09-02 11:33:49 -0700478 def __repr__(self):
479 if self.name:
480 name = self.name + ' '
481 else:
482 name = ''
483 return '<NodeSet %s%s>' % (name, self.nodes)
484
485
James E. Blair34776ee2016-08-25 13:53:54 -0700486class NodeRequest(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700487 """A request for a set of nodes."""
488
James E. Blair8b2a1472017-02-19 15:33:55 -0800489 def __init__(self, requestor, build_set, job, nodeset):
490 self.requestor = requestor
James E. Blair34776ee2016-08-25 13:53:54 -0700491 self.build_set = build_set
492 self.job = job
James E. Blair0eaad552016-09-02 12:09:54 -0700493 self.nodeset = nodeset
James E. Blair803e94f2017-01-06 09:18:59 -0800494 self._state = STATE_REQUESTED
James E. Blairdce6cea2016-12-20 16:45:32 -0800495 self.state_time = time.time()
496 self.stat = None
497 self.uid = uuid4().hex
James E. Blaircbf43672017-01-04 14:33:41 -0800498 self.id = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800499 # Zuul internal failure flag (not stored in ZK so it's not
500 # overwritten).
501 self.failed = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800502
503 @property
James E. Blair6ab79e02017-01-06 10:10:17 -0800504 def fulfilled(self):
505 return (self._state == STATE_FULFILLED) and not self.failed
506
507 @property
James E. Blairdce6cea2016-12-20 16:45:32 -0800508 def state(self):
509 return self._state
510
511 @state.setter
512 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800513 if value not in REQUEST_STATES:
514 raise TypeError("'%s' is not a valid state" % value)
James E. Blairdce6cea2016-12-20 16:45:32 -0800515 self._state = value
516 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700517
518 def __repr__(self):
James E. Blaircbf43672017-01-04 14:33:41 -0800519 return '<NodeRequest %s %s>' % (self.id, self.nodeset)
James E. Blair34776ee2016-08-25 13:53:54 -0700520
James E. Blairdce6cea2016-12-20 16:45:32 -0800521 def toDict(self):
522 d = {}
523 nodes = [n.image for n in self.nodeset.getNodes()]
524 d['node_types'] = nodes
James E. Blair8b2a1472017-02-19 15:33:55 -0800525 d['requestor'] = self.requestor
James E. Blairdce6cea2016-12-20 16:45:32 -0800526 d['state'] = self.state
527 d['state_time'] = self.state_time
528 return d
529
530 def updateFromDict(self, data):
531 self._state = data['state']
532 self.state_time = data['state_time']
533
James E. Blair34776ee2016-08-25 13:53:54 -0700534
James E. Blair01f83b72017-03-15 13:03:40 -0700535class Secret(object):
536 """A collection of private data.
537
538 In configuration, Secrets are collections of private data in
539 key-value pair format. They are defined as top-level
540 configuration objects and then referenced by Jobs.
541
542 """
543
James E. Blair8525e2b2017-03-15 14:05:47 -0700544 def __init__(self, name, source_context):
James E. Blair01f83b72017-03-15 13:03:40 -0700545 self.name = name
James E. Blair8525e2b2017-03-15 14:05:47 -0700546 self.source_context = source_context
James E. Blair01f83b72017-03-15 13:03:40 -0700547 # The secret data may or may not be encrypted. This attribute
548 # is named 'secret_data' to make it easy to search for and
549 # spot where it is directly used.
550 self.secret_data = {}
551
552 def __ne__(self, other):
553 return not self.__eq__(other)
554
555 def __eq__(self, other):
556 if not isinstance(other, Secret):
557 return False
558 return (self.name == other.name and
James E. Blair8525e2b2017-03-15 14:05:47 -0700559 self.source_context == other.source_context and
James E. Blair01f83b72017-03-15 13:03:40 -0700560 self.secret_data == other.secret_data)
561
562 def __repr__(self):
563 return '<Secret %s>' % (self.name,)
564
James E. Blair18f86a32017-03-15 14:43:26 -0700565 def decrypt(self, private_key):
566 """Return a copy of this secret with any encrypted data decrypted.
567 Note that the original remains encrypted."""
568
569 r = copy.deepcopy(self)
570 decrypted_secret_data = {}
571 for k, v in r.secret_data.items():
572 if hasattr(v, 'decrypt'):
573 decrypted_secret_data[k] = v.decrypt(private_key)
574 else:
575 decrypted_secret_data[k] = v
576 r.secret_data = decrypted_secret_data
577 return r
578
James E. Blair01f83b72017-03-15 13:03:40 -0700579
James E. Blaircdab2032017-02-01 09:09:29 -0800580class SourceContext(object):
581 """A reference to the branch of a project in configuration.
582
583 Jobs and playbooks reference this to keep track of where they
584 originate."""
585
James E. Blair6f140c72017-03-03 10:32:07 -0800586 def __init__(self, project, branch, path, trusted):
James E. Blaircdab2032017-02-01 09:09:29 -0800587 self.project = project
588 self.branch = branch
James E. Blair6f140c72017-03-03 10:32:07 -0800589 self.path = path
Monty Taylore6562aa2017-02-20 07:37:39 -0500590 self.trusted = trusted
James E. Blaircdab2032017-02-01 09:09:29 -0800591
James E. Blair6f140c72017-03-03 10:32:07 -0800592 def __str__(self):
593 return '%s/%s@%s' % (self.project, self.path, self.branch)
594
James E. Blaircdab2032017-02-01 09:09:29 -0800595 def __repr__(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800596 return '<SourceContext %s trusted:%s>' % (str(self),
597 self.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800598
James E. Blaira7f51ca2017-02-07 16:01:26 -0800599 def __deepcopy__(self, memo):
600 return self.copy()
601
602 def copy(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800603 return self.__class__(self.project, self.branch, self.path,
604 self.trusted)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800605
James E. Blaircdab2032017-02-01 09:09:29 -0800606 def __ne__(self, other):
607 return not self.__eq__(other)
608
609 def __eq__(self, other):
610 if not isinstance(other, SourceContext):
611 return False
612 return (self.project == other.project and
613 self.branch == other.branch and
James E. Blair6f140c72017-03-03 10:32:07 -0800614 self.path == other.path and
Monty Taylore6562aa2017-02-20 07:37:39 -0500615 self.trusted == other.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800616
617
James E. Blair66b274e2017-01-31 14:47:52 -0800618class PlaybookContext(object):
James E. Blaircdab2032017-02-01 09:09:29 -0800619
James E. Blair66b274e2017-01-31 14:47:52 -0800620 """A reference to a playbook in the context of a project.
621
622 Jobs refer to objects of this class for their main, pre, and post
623 playbooks so that we can keep track of which repos and security
624 contexts are needed in order to run them."""
625
James E. Blaircdab2032017-02-01 09:09:29 -0800626 def __init__(self, source_context, path):
627 self.source_context = source_context
James E. Blair66b274e2017-01-31 14:47:52 -0800628 self.path = path
James E. Blair66b274e2017-01-31 14:47:52 -0800629
630 def __repr__(self):
James E. Blaircdab2032017-02-01 09:09:29 -0800631 return '<PlaybookContext %s %s>' % (self.source_context,
632 self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800633
634 def __ne__(self, other):
635 return not self.__eq__(other)
636
637 def __eq__(self, other):
638 if not isinstance(other, PlaybookContext):
639 return False
James E. Blaircdab2032017-02-01 09:09:29 -0800640 return (self.source_context == other.source_context and
641 self.path == other.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800642
643 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400644 # Render to a dict to use in passing json to the executor
James E. Blair66b274e2017-01-31 14:47:52 -0800645 return dict(
James E. Blaircdab2032017-02-01 09:09:29 -0800646 connection=self.source_context.project.connection_name,
647 project=self.source_context.project.name,
648 branch=self.source_context.branch,
Monty Taylore6562aa2017-02-20 07:37:39 -0500649 trusted=self.source_context.trusted,
James E. Blaircdab2032017-02-01 09:09:29 -0800650 path=self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800651
652
James E. Blair5ac93842017-01-20 06:47:34 -0800653@six.add_metaclass(abc.ABCMeta)
654class Role(object):
655 """A reference to an ansible role."""
656
657 def __init__(self, target_name):
658 self.target_name = target_name
659
660 @abc.abstractmethod
661 def __repr__(self):
662 pass
663
664 def __ne__(self, other):
665 return not self.__eq__(other)
666
667 @abc.abstractmethod
668 def __eq__(self, other):
669 if not isinstance(other, Role):
670 return False
671 return (self.target_name == other.target_name)
672
673 @abc.abstractmethod
674 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400675 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800676 return dict(target_name=self.target_name)
677
678
679class ZuulRole(Role):
680 """A reference to an ansible role in a Zuul project."""
681
Monty Taylore6562aa2017-02-20 07:37:39 -0500682 def __init__(self, target_name, connection_name, project_name, trusted):
James E. Blair5ac93842017-01-20 06:47:34 -0800683 super(ZuulRole, self).__init__(target_name)
684 self.connection_name = connection_name
685 self.project_name = project_name
Monty Taylore6562aa2017-02-20 07:37:39 -0500686 self.trusted = trusted
James E. Blair5ac93842017-01-20 06:47:34 -0800687
688 def __repr__(self):
689 return '<ZuulRole %s %s>' % (self.project_name, self.target_name)
690
691 def __eq__(self, other):
692 if not isinstance(other, ZuulRole):
693 return False
694 return (super(ZuulRole, self).__eq__(other) and
695 self.connection_name == other.connection_name,
696 self.project_name == other.project_name,
Monty Taylore6562aa2017-02-20 07:37:39 -0500697 self.trusted == other.trusted)
James E. Blair5ac93842017-01-20 06:47:34 -0800698
699 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400700 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800701 d = super(ZuulRole, self).toDict()
702 d['type'] = 'zuul'
703 d['connection'] = self.connection_name
704 d['project'] = self.project_name
Monty Taylore6562aa2017-02-20 07:37:39 -0500705 d['trusted'] = self.trusted
James E. Blair5ac93842017-01-20 06:47:34 -0800706 return d
707
708
James E. Blair8525e2b2017-03-15 14:05:47 -0700709class AuthContext(object):
710 """The authentication information for a job.
711
712 Authentication information (both the actual data and metadata such
713 as whether it should be inherited) for a job is grouped together
714 in this object.
715 """
716
717 def __init__(self, inherit=False):
718 self.inherit = inherit
719 self.secrets = []
720
721 def __ne__(self, other):
722 return not self.__eq__(other)
723
724 def __eq__(self, other):
725 if not isinstance(other, AuthContext):
726 return False
727 return (self.inherit == other.inherit and
728 self.secrets == other.secrets)
729
730
James E. Blairee743612012-05-29 14:49:32 -0700731class Job(object):
James E. Blair66b274e2017-01-31 14:47:52 -0800732
James E. Blaira7f51ca2017-02-07 16:01:26 -0800733 """A Job represents the defintion of actions to perform.
734
James E. Blaird4ade8c2017-02-19 15:25:46 -0800735 A Job is an abstract configuration concept. It describes what,
736 where, and under what circumstances something should be run
737 (contrast this with Build which is a concrete single execution of
738 a Job).
739
James E. Blaira7f51ca2017-02-07 16:01:26 -0800740 NB: Do not modify attributes of this class, set them directly
741 (e.g., "job.run = ..." rather than "job.run.append(...)").
742 """
Monty Taylora42a55b2016-07-29 07:53:33 -0700743
James E. Blairee743612012-05-29 14:49:32 -0700744 def __init__(self, name):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800745 # These attributes may override even the final form of a job
746 # in the context of a project-pipeline. They can not affect
747 # the execution of the job, but only whether the job is run
748 # and how it is reported.
749 self.context_attributes = dict(
750 voting=True,
751 hold_following_changes=False,
James E. Blair1774dd52017-02-03 10:52:32 -0800752 failure_message=None,
753 success_message=None,
754 failure_url=None,
755 success_url=None,
756 # Matchers. These are separate so they can be individually
757 # overidden.
758 branch_matcher=None,
759 file_matcher=None,
760 irrelevant_file_matcher=None, # skip-if
James E. Blaira7f51ca2017-02-07 16:01:26 -0800761 tags=frozenset(),
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200762 dependencies=frozenset(),
James E. Blair1774dd52017-02-03 10:52:32 -0800763 )
764
James E. Blaira7f51ca2017-02-07 16:01:26 -0800765 # These attributes affect how the job is actually run and more
766 # care must be taken when overriding them. If a job is
767 # declared "final", these may not be overriden in a
768 # project-pipeline.
769 self.execution_attributes = dict(
770 timeout=None,
James E. Blair490cf042017-02-24 23:07:21 -0500771 variables={},
James E. Blaira7f51ca2017-02-07 16:01:26 -0800772 nodeset=NodeSet(),
James E. Blair8525e2b2017-03-15 14:05:47 -0700773 auth=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800774 workspace=None,
775 pre_run=(),
776 post_run=(),
777 run=(),
778 implied_run=(),
Tobias Henkel9a0e1942017-03-20 16:16:02 +0100779 semaphore=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800780 attempts=3,
781 final=False,
James E. Blair5ac93842017-01-20 06:47:34 -0800782 roles=frozenset(),
K Jonathan Harker2c1a6232017-02-21 14:34:08 -0800783 repos=frozenset(),
James E. Blairb3f5db12017-03-17 12:57:39 -0700784 allowed_projects=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800785 )
786
787 # These are generally internal attributes which are not
788 # accessible via configuration.
789 self.other_attributes = dict(
790 name=None,
791 source_context=None,
792 inheritance_path=(),
793 )
794
795 self.inheritable_attributes = {}
796 self.inheritable_attributes.update(self.context_attributes)
797 self.inheritable_attributes.update(self.execution_attributes)
798 self.attributes = {}
799 self.attributes.update(self.inheritable_attributes)
800 self.attributes.update(self.other_attributes)
801
James E. Blairee743612012-05-29 14:49:32 -0700802 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800803
James E. Blair66b274e2017-01-31 14:47:52 -0800804 def __ne__(self, other):
805 return not self.__eq__(other)
806
Paul Belangere22baea2016-11-03 16:59:27 -0400807 def __eq__(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800808 # Compare the name and all inheritable attributes to determine
809 # whether two jobs with the same name are identically
810 # configured. Useful upon reconfiguration.
811 if not isinstance(other, Job):
812 return False
813 if self.name != other.name:
814 return False
815 for k, v in self.attributes.items():
816 if getattr(self, k) != getattr(other, k):
817 return False
818 return True
James E. Blairee743612012-05-29 14:49:32 -0700819
820 def __str__(self):
821 return self.name
822
823 def __repr__(self):
James E. Blair72ae9da2017-02-03 14:21:30 -0800824 return '<Job %s branches: %s source: %s>' % (self.name,
825 self.branch_matcher,
826 self.source_context)
James E. Blair83005782015-12-11 14:46:03 -0800827
James E. Blaira7f51ca2017-02-07 16:01:26 -0800828 def __getattr__(self, name):
829 v = self.__dict__.get(name)
830 if v is None:
831 return copy.deepcopy(self.attributes[name])
832 return v
833
834 def _get(self, name):
835 return self.__dict__.get(name)
836
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100837 def getSafeAttributes(self):
838 return Attributes(name=self.name)
839
James E. Blaira7f51ca2017-02-07 16:01:26 -0800840 def setRun(self):
841 if not self.run:
842 self.run = self.implied_run
843
James E. Blair490cf042017-02-24 23:07:21 -0500844 def updateVariables(self, other_vars):
845 v = self.variables
846 Job._deepUpdate(v, other_vars)
847 self.variables = v
848
849 @staticmethod
850 def _deepUpdate(a, b):
851 # Merge nested dictionaries if possible, otherwise, overwrite
852 # the value in 'a' with the value in 'b'.
853 for k, bv in b.items():
854 av = a.get(k)
855 if isinstance(av, dict) and isinstance(bv, dict):
856 Job._deepUpdate(av, bv)
857 else:
858 a[k] = bv
859
James E. Blaira7f51ca2017-02-07 16:01:26 -0800860 def inheritFrom(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800861 """Copy the inheritable attributes which have been set on the other
862 job to this job."""
James E. Blaira7f51ca2017-02-07 16:01:26 -0800863 if not isinstance(other, Job):
864 raise Exception("Job unable to inherit from %s" % (other,))
865
866 do_not_inherit = set()
James E. Blair8525e2b2017-03-15 14:05:47 -0700867 if other.auth and not other.auth.inherit:
James E. Blaira7f51ca2017-02-07 16:01:26 -0800868 do_not_inherit.add('auth')
869
870 # copy all attributes
871 for k in self.inheritable_attributes:
872 if (other._get(k) is not None and k not in do_not_inherit):
873 setattr(self, k, copy.deepcopy(getattr(other, k)))
874
875 msg = 'inherit from %s' % (repr(other),)
876 self.inheritance_path = other.inheritance_path + (msg,)
877
878 def copy(self):
879 job = Job(self.name)
880 for k in self.attributes:
881 if self._get(k) is not None:
882 setattr(job, k, copy.deepcopy(self._get(k)))
883 return job
884
885 def applyVariant(self, other):
886 """Copy the attributes which have been set on the other job to this
887 job."""
James E. Blair83005782015-12-11 14:46:03 -0800888
889 if not isinstance(other, Job):
890 raise Exception("Job unable to inherit from %s" % (other,))
James E. Blaira7f51ca2017-02-07 16:01:26 -0800891
892 for k in self.execution_attributes:
893 if (other._get(k) is not None and
894 k not in set(['final'])):
895 if self.final:
896 raise Exception("Unable to modify final job %s attribute "
897 "%s=%s with variant %s" % (
898 repr(self), k, other._get(k),
899 repr(other)))
James E. Blair490cf042017-02-24 23:07:21 -0500900 if k not in set(['pre_run', 'post_run', 'roles', 'variables']):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800901 setattr(self, k, copy.deepcopy(other._get(k)))
902
903 # Don't set final above so that we don't trip an error halfway
904 # through assignment.
905 if other.final != self.attributes['final']:
906 self.final = other.final
907
908 if other._get('pre_run') is not None:
909 self.pre_run = self.pre_run + other.pre_run
910 if other._get('post_run') is not None:
911 self.post_run = other.post_run + self.post_run
James E. Blair5ac93842017-01-20 06:47:34 -0800912 if other._get('roles') is not None:
913 self.roles = self.roles.union(other.roles)
James E. Blair490cf042017-02-24 23:07:21 -0500914 if other._get('variables') is not None:
915 self.updateVariables(other.variables)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800916
917 for k in self.context_attributes:
918 if (other._get(k) is not None and
919 k not in set(['tags'])):
920 setattr(self, k, copy.deepcopy(other._get(k)))
921
922 if other._get('tags') is not None:
923 self.tags = self.tags.union(other.tags)
924
925 msg = 'apply variant %s' % (repr(other),)
926 self.inheritance_path = self.inheritance_path + (msg,)
James E. Blairee743612012-05-29 14:49:32 -0700927
James E. Blaire421a232012-07-25 16:59:21 -0700928 def changeMatches(self, change):
James E. Blair83005782015-12-11 14:46:03 -0800929 if self.branch_matcher and not self.branch_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800930 return False
931
James E. Blair83005782015-12-11 14:46:03 -0800932 if self.file_matcher and not self.file_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800933 return False
934
James E. Blair83005782015-12-11 14:46:03 -0800935 # NB: This is a negative match.
936 if (self.irrelevant_file_matcher and
937 self.irrelevant_file_matcher.matches(change)):
Maru Newby3fe5f852015-01-13 04:22:14 +0000938 return False
939
James E. Blair70c71582013-03-06 08:50:50 -0800940 return True
James E. Blaire5a847f2012-07-10 15:29:14 -0700941
James E. Blair1e8dd892012-05-30 09:15:05 -0700942
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200943class JobList(object):
944 """ A list of jobs in a project's pipeline. """
Monty Taylora42a55b2016-07-29 07:53:33 -0700945
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200946 def __init__(self):
947 self.jobs = OrderedDict() # job.name -> [job, ...]
James E. Blair12748b52017-02-07 17:17:53 -0800948
James E. Blairee743612012-05-29 14:49:32 -0700949 def addJob(self, job):
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200950 if job.name in self.jobs:
951 self.jobs[job.name].append(job)
952 else:
953 self.jobs[job.name] = [job]
James E. Blairee743612012-05-29 14:49:32 -0700954
James E. Blaira7f51ca2017-02-07 16:01:26 -0800955 def inheritFrom(self, other):
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200956 for jobname, jobs in other.jobs.items():
957 if jobname in self.jobs:
958 self.jobs[jobname].append(jobs)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800959 else:
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200960 self.jobs[jobname] = jobs
961
962
963class JobGraph(object):
964 """ A JobGraph represents the dependency graph between Job."""
965
966 def __init__(self):
967 self.jobs = OrderedDict() # job_name -> Job
968 self._dependencies = {} # dependent_job_name -> set(parent_job_names)
969
970 def __repr__(self):
971 return '<JobGraph %s>' % (self.jobs)
972
973 def addJob(self, job):
974 # A graph must be created after the job list is frozen,
975 # therefore we should only get one job with the same name.
976 if job.name in self.jobs:
977 raise Exception("Job %s already added" % (job.name,))
978 self.jobs[job.name] = job
979 # Append the dependency information
980 self._dependencies.setdefault(job.name, set())
981 try:
982 for dependency in job.dependencies:
983 # Make sure a circular dependency is never created
984 ancestor_jobs = self._getParentJobNamesRecursively(
985 dependency, soft=True)
986 ancestor_jobs.add(dependency)
987 if any((job.name == anc_job) for anc_job in ancestor_jobs):
988 raise Exception("Dependency cycle detected in job %s" %
989 (job.name,))
990 self._dependencies[job.name].add(dependency)
991 except Exception:
992 del self.jobs[job.name]
993 del self._dependencies[job.name]
994 raise
995
996 def getJobs(self):
997 return self.jobs.values() # Report in the order of the layout config
998
999 def _getDirectDependentJobs(self, parent_job):
1000 ret = set()
1001 for dependent_name, parent_names in self._dependencies.items():
1002 if parent_job in parent_names:
1003 ret.add(dependent_name)
1004 return ret
1005
1006 def getDependentJobsRecursively(self, parent_job):
1007 all_dependent_jobs = set()
1008 jobs_to_iterate = set([parent_job])
1009 while len(jobs_to_iterate) > 0:
1010 current_job = jobs_to_iterate.pop()
1011 current_dependent_jobs = self._getDirectDependentJobs(current_job)
1012 new_dependent_jobs = current_dependent_jobs - all_dependent_jobs
1013 jobs_to_iterate |= new_dependent_jobs
1014 all_dependent_jobs |= new_dependent_jobs
1015 return [self.jobs[name] for name in all_dependent_jobs]
1016
1017 def getParentJobsRecursively(self, dependent_job):
1018 return [self.jobs[name] for name in
1019 self._getParentJobNamesRecursively(dependent_job)]
1020
1021 def _getParentJobNamesRecursively(self, dependent_job, soft=False):
1022 all_parent_jobs = set()
1023 jobs_to_iterate = set([dependent_job])
1024 while len(jobs_to_iterate) > 0:
1025 current_job = jobs_to_iterate.pop()
1026 current_parent_jobs = self._dependencies.get(current_job)
1027 if current_parent_jobs is None:
1028 if soft:
1029 current_parent_jobs = set()
1030 else:
1031 raise Exception("Dependent job %s not found: " %
1032 (dependent_job,))
1033 new_parent_jobs = current_parent_jobs - all_parent_jobs
1034 jobs_to_iterate |= new_parent_jobs
1035 all_parent_jobs |= new_parent_jobs
1036 return all_parent_jobs
James E. Blairb97ed802015-12-21 15:55:35 -08001037
James E. Blair1e8dd892012-05-30 09:15:05 -07001038
James E. Blair4aea70c2012-07-26 14:23:24 -07001039class Build(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001040 """A Build is an instance of a single execution of a Job.
1041
1042 While a Job describes what to run, a Build describes an actual
1043 execution of that Job. Each build is associated with exactly one
1044 Job (related builds are grouped together in a BuildSet).
1045 """
Monty Taylora42a55b2016-07-29 07:53:33 -07001046
James E. Blair4aea70c2012-07-26 14:23:24 -07001047 def __init__(self, job, uuid):
1048 self.job = job
1049 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -07001050 self.url = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001051 self.result = None
1052 self.build_set = None
Paul Belanger174a8272017-03-14 13:20:10 -04001053 self.execute_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -08001054 self.start_time = None
1055 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -07001056 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -07001057 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -07001058 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -07001059 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -07001060 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +08001061 self.worker = Worker()
Timothy Chavezb2332082015-08-07 20:08:04 -05001062 self.node_labels = []
1063 self.node_name = None
James E. Blairee743612012-05-29 14:49:32 -07001064
1065 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +08001066 return ('<Build %s of %s on %s>' %
1067 (self.uuid, self.job.name, self.worker))
1068
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001069 def getSafeAttributes(self):
1070 return Attributes(uuid=self.uuid)
1071
Joshua Heskethba8776a2014-01-12 14:35:40 +08001072
1073class Worker(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001074 """Information about the specific worker executing a Build."""
Joshua Heskethba8776a2014-01-12 14:35:40 +08001075 def __init__(self):
1076 self.name = "Unknown"
1077 self.hostname = None
1078 self.ips = []
1079 self.fqdn = None
1080 self.program = None
1081 self.version = None
1082 self.extra = {}
1083
1084 def updateFromData(self, data):
1085 """Update worker information if contained in the WORK_DATA response."""
1086 self.name = data.get('worker_name', self.name)
1087 self.hostname = data.get('worker_hostname', self.hostname)
1088 self.ips = data.get('worker_ips', self.ips)
1089 self.fqdn = data.get('worker_fqdn', self.fqdn)
1090 self.program = data.get('worker_program', self.program)
1091 self.version = data.get('worker_version', self.version)
1092 self.extra = data.get('worker_extra', self.extra)
1093
1094 def __repr__(self):
1095 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -07001096
James E. Blair1e8dd892012-05-30 09:15:05 -07001097
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001098class RepoFiles(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001099 """RepoFiles holds config-file content for per-project job config.
1100
1101 When Zuul asks a merger to prepare a future multiple-repo state
1102 and collect Zuul configuration files so that we can dynamically
1103 load our configuration, this class provides cached access to that
1104 data for use by the Change which updated the config files and any
1105 changes that follow it in a ChangeQueue.
1106
1107 It is attached to a BuildSet since the content of Zuul
1108 configuration files can change with each new BuildSet.
1109 """
1110
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001111 def __init__(self):
1112 self.projects = {}
1113
1114 def __repr__(self):
1115 return '<RepoFiles %s>' % self.projects
1116
1117 def setFiles(self, items):
1118 self.projects = {}
1119 for item in items:
1120 project = self.projects.setdefault(item['project'], {})
1121 branch = project.setdefault(item['branch'], {})
1122 branch.update(item['files'])
1123
1124 def getFile(self, project, branch, fn):
1125 return self.projects.get(project, {}).get(branch, {}).get(fn)
1126
1127
James E. Blair7e530ad2012-07-03 16:12:28 -07001128class BuildSet(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001129 """A collection of Builds for one specific potential future repository
1130 state.
Monty Taylora42a55b2016-07-29 07:53:33 -07001131
Paul Belanger174a8272017-03-14 13:20:10 -04001132 When Zuul executes Builds for a change, it creates a Build to
James E. Blaird4ade8c2017-02-19 15:25:46 -08001133 represent each execution of each job and a BuildSet to keep track
Paul Belanger174a8272017-03-14 13:20:10 -04001134 of all the Builds running for that Change. When Zuul re-executes
James E. Blaird4ade8c2017-02-19 15:25:46 -08001135 Builds for a Change with a different configuration, all of the
1136 running Builds in the BuildSet for that change are aborted, and a
1137 new BuildSet is created to hold the Builds for the Jobs being
1138 run with the new configuration.
1139
1140 A BuildSet also holds the UUID used to produce the Zuul Ref that
1141 builders check out.
1142
Monty Taylora42a55b2016-07-29 07:53:33 -07001143 """
James E. Blair4076e2b2014-01-28 12:42:20 -08001144 # Merge states:
1145 NEW = 1
1146 PENDING = 2
1147 COMPLETE = 3
1148
Antoine Musso9b229282014-08-18 23:45:43 +02001149 states_map = {
1150 1: 'NEW',
1151 2: 'PENDING',
1152 3: 'COMPLETE',
1153 }
1154
James E. Blairfee8d652013-06-07 08:57:52 -07001155 def __init__(self, item):
1156 self.item = item
James E. Blair11700c32012-07-05 17:50:05 -07001157 self.other_changes = []
James E. Blair7e530ad2012-07-03 16:12:28 -07001158 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -07001159 self.result = None
1160 self.next_build_set = None
1161 self.previous_build_set = None
James E. Blair4886cc12012-07-18 15:39:41 -07001162 self.ref = None
James E. Blair81515ad2012-10-01 18:29:08 -07001163 self.commit = None
James E. Blair4076e2b2014-01-28 12:42:20 -08001164 self.zuul_url = None
James E. Blair973721f2012-08-15 10:19:43 -07001165 self.unable_to_merge = False
James E. Blaire53250c2017-03-01 14:34:36 -08001166 self.config_error = None # None or an error message string.
James E. Blair972e3c72013-08-29 12:04:55 -07001167 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -08001168 self.merge_state = self.NEW
James E. Blair0eaad552016-09-02 12:09:54 -07001169 self.nodesets = {} # job -> nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001170 self.node_requests = {} # job -> reqs
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001171 self.files = RepoFiles()
1172 self.layout = None
Paul Belanger71d98172016-11-08 10:56:31 -05001173 self.tries = {}
James E. Blair7e530ad2012-07-03 16:12:28 -07001174
Antoine Musso9b229282014-08-18 23:45:43 +02001175 def __repr__(self):
1176 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
1177 self.item,
1178 len(self.builds),
1179 self.getStateName(self.merge_state))
1180
James E. Blair4886cc12012-07-18 15:39:41 -07001181 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -07001182 # The change isn't enqueued until after it's created
1183 # so we don't know what the other changes ahead will be
1184 # until jobs start.
1185 if not self.other_changes:
James E. Blairfee8d652013-06-07 08:57:52 -07001186 next_item = self.item.item_ahead
1187 while next_item:
1188 self.other_changes.append(next_item.change)
1189 next_item = next_item.item_ahead
James E. Blair4886cc12012-07-18 15:39:41 -07001190 if not self.ref:
1191 self.ref = 'Z' + uuid4().hex
1192
Antoine Musso9b229282014-08-18 23:45:43 +02001193 def getStateName(self, state_num):
1194 return self.states_map.get(
1195 state_num, 'UNKNOWN (%s)' % state_num)
1196
James E. Blair4886cc12012-07-18 15:39:41 -07001197 def addBuild(self, build):
1198 self.builds[build.job.name] = build
Paul Belanger71d98172016-11-08 10:56:31 -05001199 if build.job.name not in self.tries:
James E. Blair5aed1112016-12-15 09:18:05 -08001200 self.tries[build.job.name] = 1
James E. Blair4886cc12012-07-18 15:39:41 -07001201 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -07001202
James E. Blair4a28a882013-08-23 15:17:33 -07001203 def removeBuild(self, build):
Paul Belanger71d98172016-11-08 10:56:31 -05001204 self.tries[build.job.name] += 1
James E. Blair4a28a882013-08-23 15:17:33 -07001205 del self.builds[build.job.name]
1206
James E. Blair7e530ad2012-07-03 16:12:28 -07001207 def getBuild(self, job_name):
1208 return self.builds.get(job_name)
1209
James E. Blair11700c32012-07-05 17:50:05 -07001210 def getBuilds(self):
1211 keys = self.builds.keys()
1212 keys.sort()
1213 return [self.builds.get(x) for x in keys]
1214
James E. Blair0eaad552016-09-02 12:09:54 -07001215 def getJobNodeSet(self, job_name):
1216 # Return None if not provisioned; empty NodeSet if no nodes
1217 # required
1218 return self.nodesets.get(job_name)
James E. Blair8d692392016-04-08 17:47:58 -07001219
James E. Blaire18d4602017-01-05 11:17:28 -08001220 def removeJobNodeSet(self, job_name):
1221 if job_name not in self.nodesets:
1222 raise Exception("No job set for %s" % (job_name))
1223 del self.nodesets[job_name]
1224
James E. Blair8d692392016-04-08 17:47:58 -07001225 def setJobNodeRequest(self, job_name, req):
1226 if job_name in self.node_requests:
1227 raise Exception("Prior node request for %s" % (job_name))
1228 self.node_requests[job_name] = req
1229
1230 def getJobNodeRequest(self, job_name):
1231 return self.node_requests.get(job_name)
1232
James E. Blair0eaad552016-09-02 12:09:54 -07001233 def jobNodeRequestComplete(self, job_name, req, nodeset):
1234 if job_name in self.nodesets:
James E. Blair8d692392016-04-08 17:47:58 -07001235 raise Exception("Prior node request for %s" % (job_name))
James E. Blair0eaad552016-09-02 12:09:54 -07001236 self.nodesets[job_name] = nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001237 del self.node_requests[job_name]
1238
Paul Belanger71d98172016-11-08 10:56:31 -05001239 def getTries(self, job_name):
1240 return self.tries.get(job_name)
1241
Adam Gandelman8bd57102016-12-02 12:58:42 -08001242 def getMergeMode(self, job_name):
1243 if not self.layout or job_name not in self.layout.project_configs:
1244 return MERGER_MERGE_RESOLVE
1245 return self.layout.project_configs[job_name].merge_mode
1246
James E. Blair7e530ad2012-07-03 16:12:28 -07001247
James E. Blairfee8d652013-06-07 08:57:52 -07001248class QueueItem(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001249 """Represents the position of a Change in a ChangeQueue.
1250
1251 All Changes are enqueued into ChangeQueue in a QueueItem. The QueueItem
1252 holds the current `BuildSet` as well as all previous `BuildSets` that were
1253 produced for this `QueueItem`.
1254 """
James E. Blair32663402012-06-01 10:04:18 -07001255
James E. Blairbfb8e042014-12-30 17:01:44 -08001256 def __init__(self, queue, change):
1257 self.pipeline = queue.pipeline
1258 self.queue = queue
James E. Blairfee8d652013-06-07 08:57:52 -07001259 self.change = change # a changeish
James E. Blair7e530ad2012-07-03 16:12:28 -07001260 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -07001261 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -07001262 self.current_build_set = BuildSet(self)
1263 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -07001264 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -07001265 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -08001266 self.enqueue_time = None
1267 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -07001268 self.reported = False
James E. Blairbfb8e042014-12-30 17:01:44 -08001269 self.active = False # Whether an item is within an active window
1270 self.live = True # Whether an item is intended to be processed at all
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001271 self.layout = None # This item's shadow layout
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001272 self.job_graph = None
James E. Blaire5a847f2012-07-10 15:29:14 -07001273
James E. Blair972e3c72013-08-29 12:04:55 -07001274 def __repr__(self):
1275 if self.pipeline:
1276 pipeline = self.pipeline.name
1277 else:
1278 pipeline = None
1279 return '<QueueItem 0x%x for %s in %s>' % (
1280 id(self), self.change, pipeline)
1281
James E. Blairee743612012-05-29 14:49:32 -07001282 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -07001283 old = self.current_build_set
1284 self.current_build_set.result = 'CANCELED'
1285 self.current_build_set = BuildSet(self)
1286 old.next_build_set = self.current_build_set
1287 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -07001288 self.build_sets.append(self.current_build_set)
James E. Blairee743612012-05-29 14:49:32 -07001289
1290 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -07001291 self.current_build_set.addBuild(build)
James E. Blair66eeebf2013-07-27 17:44:32 -07001292 build.pipeline = self.pipeline
James E. Blairee743612012-05-29 14:49:32 -07001293
James E. Blair4a28a882013-08-23 15:17:33 -07001294 def removeBuild(self, build):
1295 self.current_build_set.removeBuild(build)
1296
James E. Blairfee8d652013-06-07 08:57:52 -07001297 def setReportedResult(self, result):
1298 self.current_build_set.result = result
1299
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001300 def freezeJobGraph(self):
James E. Blair83005782015-12-11 14:46:03 -08001301 """Find or create actual matching jobs for this item's change and
1302 store the resulting job tree."""
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001303 layout = self.current_build_set.layout
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001304 job_graph = layout.createJobGraph(self)
1305 for job in job_graph.getJobs():
1306 # Ensure that each jobs's dependencies are fully
1307 # accessible. This will raise an exception if not.
1308 job_graph.getParentJobsRecursively(job.name)
1309 self.job_graph = job_graph
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001310
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001311 def hasJobGraph(self):
1312 """Returns True if the item has a job graph."""
1313 return self.job_graph is not None
James E. Blair83005782015-12-11 14:46:03 -08001314
1315 def getJobs(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001316 if not self.live or not self.job_graph:
James E. Blair83005782015-12-11 14:46:03 -08001317 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001318 return self.job_graph.getJobs()
1319
1320 def getJob(self, name):
1321 if not self.job_graph:
1322 return None
1323 return self.job_graph.jobs.get(name)
James E. Blair83005782015-12-11 14:46:03 -08001324
James E. Blairdbfd3282016-07-21 10:46:19 -07001325 def haveAllJobsStarted(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001326 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001327 return False
1328 for job in self.getJobs():
1329 build = self.current_build_set.getBuild(job.name)
1330 if not build or not build.start_time:
1331 return False
1332 return True
1333
1334 def areAllJobsComplete(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001335 if (self.current_build_set.config_error or
1336 self.current_build_set.unable_to_merge):
1337 return True
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001338 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001339 return False
1340 for job in self.getJobs():
1341 build = self.current_build_set.getBuild(job.name)
1342 if not build or not build.result:
1343 return False
1344 return True
1345
1346 def didAllJobsSucceed(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001347 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001348 return False
1349 for job in self.getJobs():
1350 if not job.voting:
1351 continue
1352 build = self.current_build_set.getBuild(job.name)
1353 if not build:
1354 return False
1355 if build.result != 'SUCCESS':
1356 return False
1357 return True
1358
1359 def didAnyJobFail(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001360 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001361 return False
1362 for job in self.getJobs():
1363 if not job.voting:
1364 continue
1365 build = self.current_build_set.getBuild(job.name)
1366 if build and build.result and (build.result != 'SUCCESS'):
1367 return True
1368 return False
1369
1370 def didMergerFail(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001371 return self.current_build_set.unable_to_merge
1372
1373 def getConfigError(self):
1374 return self.current_build_set.config_error
James E. Blairdbfd3282016-07-21 10:46:19 -07001375
James E. Blairdbfd3282016-07-21 10:46:19 -07001376 def isHoldingFollowingChanges(self):
1377 if not self.live:
1378 return False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001379 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001380 return False
1381 for job in self.getJobs():
1382 if not job.hold_following_changes:
1383 continue
1384 build = self.current_build_set.getBuild(job.name)
1385 if not build:
1386 return True
1387 if build.result != 'SUCCESS':
1388 return True
1389
1390 if not self.item_ahead:
1391 return False
1392 return self.item_ahead.isHoldingFollowingChanges()
1393
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001394 def findJobsToRun(self, semaphore_handler):
James E. Blairdbfd3282016-07-21 10:46:19 -07001395 torun = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001396 if not self.live:
1397 return []
1398 if not self.job_graph:
1399 return []
James E. Blair791b5392016-08-03 11:25:56 -07001400 if self.item_ahead:
1401 # Only run jobs if any 'hold' jobs on the change ahead
1402 # have completed successfully.
1403 if self.item_ahead.isHoldingFollowingChanges():
1404 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001405
1406 successful_job_names = set()
1407 jobs_not_started = set()
1408 for job in self.job_graph.getJobs():
1409 build = self.current_build_set.getBuild(job.name)
1410 if build:
1411 if build.result == 'SUCCESS':
1412 successful_job_names.add(job.name)
1413 else:
1414 jobs_not_started.add(job)
1415
1416 # Attempt to request nodes for jobs in the order jobs appear
1417 # in configuration.
1418 for job in self.job_graph.getJobs():
1419 if job not in jobs_not_started:
1420 continue
1421 all_parent_jobs_successful = True
1422 for parent_job in self.job_graph.getParentJobsRecursively(
1423 job.name):
1424 if parent_job.name not in successful_job_names:
1425 all_parent_jobs_successful = False
1426 break
1427 if all_parent_jobs_successful:
1428 nodeset = self.current_build_set.getJobNodeSet(job.name)
1429 if nodeset is None:
1430 # The nodes for this job are not ready, skip
1431 # it for now.
James E. Blairdbfd3282016-07-21 10:46:19 -07001432 continue
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001433 if semaphore_handler.acquire(self, job):
1434 # If this job needs a semaphore, either acquire it or
1435 # make sure that we have it before running the job.
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001436 torun.append(job)
James E. Blairdbfd3282016-07-21 10:46:19 -07001437 return torun
1438
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001439 def findJobsToRequest(self):
James E. Blair6ab79e02017-01-06 10:10:17 -08001440 build_set = self.current_build_set
James E. Blairdbfd3282016-07-21 10:46:19 -07001441 toreq = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001442 if not self.live:
1443 return []
1444 if not self.job_graph:
1445 return []
James E. Blair6ab79e02017-01-06 10:10:17 -08001446 if self.item_ahead:
1447 if self.item_ahead.isHoldingFollowingChanges():
1448 return []
James E. Blairdbfd3282016-07-21 10:46:19 -07001449
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001450 successful_job_names = set()
1451 jobs_not_requested = set()
1452 for job in self.job_graph.getJobs():
1453 build = build_set.getBuild(job.name)
1454 if build and build.result == 'SUCCESS':
1455 successful_job_names.add(job.name)
1456 else:
1457 nodeset = build_set.getJobNodeSet(job.name)
1458 if nodeset is None:
1459 req = build_set.getJobNodeRequest(job.name)
1460 if req is None:
1461 jobs_not_requested.add(job)
1462
1463 # Attempt to request nodes for jobs in the order jobs appear
1464 # in configuration.
1465 for job in self.job_graph.getJobs():
1466 if job not in jobs_not_requested:
1467 continue
1468 all_parent_jobs_successful = True
1469 for parent_job in self.job_graph.getParentJobsRecursively(
1470 job.name):
1471 if parent_job.name not in successful_job_names:
1472 all_parent_jobs_successful = False
1473 break
1474 if all_parent_jobs_successful:
1475 toreq.append(job)
1476 return toreq
James E. Blairdbfd3282016-07-21 10:46:19 -07001477
1478 def setResult(self, build):
1479 if build.retry:
1480 self.removeBuild(build)
1481 elif build.result != 'SUCCESS':
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001482 for job in self.job_graph.getDependentJobsRecursively(
1483 build.job.name):
James E. Blairdbfd3282016-07-21 10:46:19 -07001484 fakebuild = Build(job, None)
1485 fakebuild.result = 'SKIPPED'
1486 self.addBuild(fakebuild)
1487
James E. Blair6ab79e02017-01-06 10:10:17 -08001488 def setNodeRequestFailure(self, job):
1489 fakebuild = Build(job, None)
1490 self.addBuild(fakebuild)
1491 fakebuild.result = 'NODE_FAILURE'
1492 self.setResult(fakebuild)
1493
James E. Blairdbfd3282016-07-21 10:46:19 -07001494 def setDequeuedNeedingChange(self):
1495 self.dequeued_needing_change = True
1496 self._setAllJobsSkipped()
1497
1498 def setUnableToMerge(self):
1499 self.current_build_set.unable_to_merge = True
1500 self._setAllJobsSkipped()
1501
James E. Blaire53250c2017-03-01 14:34:36 -08001502 def setConfigError(self, error):
1503 self.current_build_set.config_error = error
1504 self._setAllJobsSkipped()
1505
James E. Blairdbfd3282016-07-21 10:46:19 -07001506 def _setAllJobsSkipped(self):
1507 for job in self.getJobs():
1508 fakebuild = Build(job, None)
1509 fakebuild.result = 'SKIPPED'
1510 self.addBuild(fakebuild)
1511
James E. Blair800e7ff2017-03-17 16:06:52 -07001512 def formatJobResult(self, job):
James E. Blairb7273ef2016-04-19 08:58:51 -07001513 build = self.current_build_set.getBuild(job.name)
1514 result = build.result
James E. Blair800e7ff2017-03-17 16:06:52 -07001515 pattern = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001516 if result == 'SUCCESS':
1517 if job.success_message:
1518 result = job.success_message
James E. Blaira5dba232016-08-08 15:53:24 -07001519 if job.success_url:
1520 pattern = job.success_url
James E. Blairb7273ef2016-04-19 08:58:51 -07001521 elif result == 'FAILURE':
1522 if job.failure_message:
1523 result = job.failure_message
James E. Blaira5dba232016-08-08 15:53:24 -07001524 if job.failure_url:
1525 pattern = job.failure_url
James E. Blairb7273ef2016-04-19 08:58:51 -07001526 url = None
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001527 # Produce safe versions of objects which may be useful in
1528 # result formatting, but don't allow users to crawl through
1529 # the entire data structure where they might be able to access
1530 # secrets, etc.
1531 safe_change = self.change.getSafeAttributes()
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001532 safe_pipeline = self.pipeline.getSafeAttributes()
1533 safe_job = job.getSafeAttributes()
1534 safe_build = build.getSafeAttributes()
James E. Blairb7273ef2016-04-19 08:58:51 -07001535 if pattern:
1536 try:
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001537 url = pattern.format(change=safe_change,
1538 pipeline=safe_pipeline,
1539 job=safe_job,
1540 build=safe_build)
James E. Blairb7273ef2016-04-19 08:58:51 -07001541 except Exception:
1542 pass # FIXME: log this or something?
1543 if not url:
1544 url = build.url or job.name
1545 return (result, url)
1546
James E. Blair800e7ff2017-03-17 16:06:52 -07001547 def formatJSON(self):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001548 changeish = self.change
1549 ret = {}
1550 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -08001551 ret['live'] = self.live
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001552 if hasattr(changeish, 'url') and changeish.url is not None:
1553 ret['url'] = changeish.url
1554 else:
1555 ret['url'] = None
1556 ret['id'] = changeish._id()
1557 if self.item_ahead:
1558 ret['item_ahead'] = self.item_ahead.change._id()
1559 else:
1560 ret['item_ahead'] = None
1561 ret['items_behind'] = [i.change._id() for i in self.items_behind]
1562 ret['failing_reasons'] = self.current_build_set.failing_reasons
1563 ret['zuul_ref'] = self.current_build_set.ref
Ramy Asselin07cc33c2015-06-12 14:06:34 -07001564 if changeish.project:
1565 ret['project'] = changeish.project.name
1566 else:
1567 # For cross-project dependencies with the depends-on
1568 # project not known to zuul, the project is None
1569 # Set it to a static value
1570 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001571 ret['enqueue_time'] = int(self.enqueue_time * 1000)
1572 ret['jobs'] = []
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001573 if hasattr(changeish, 'owner'):
1574 ret['owner'] = changeish.owner
1575 else:
1576 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001577 max_remaining = 0
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001578 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001579 now = time.time()
1580 build = self.current_build_set.getBuild(job.name)
1581 elapsed = None
1582 remaining = None
1583 result = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001584 build_url = None
1585 report_url = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001586 worker = None
1587 if build:
1588 result = build.result
James E. Blairb7273ef2016-04-19 08:58:51 -07001589 build_url = build.url
James E. Blair800e7ff2017-03-17 16:06:52 -07001590 (unused, report_url) = self.formatJobResult(job)
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001591 if build.start_time:
1592 if build.end_time:
1593 elapsed = int((build.end_time -
1594 build.start_time) * 1000)
1595 remaining = 0
1596 else:
1597 elapsed = int((now - build.start_time) * 1000)
1598 if build.estimated_time:
1599 remaining = max(
1600 int(build.estimated_time * 1000) - elapsed,
1601 0)
1602 worker = {
1603 'name': build.worker.name,
1604 'hostname': build.worker.hostname,
1605 'ips': build.worker.ips,
1606 'fqdn': build.worker.fqdn,
1607 'program': build.worker.program,
1608 'version': build.worker.version,
1609 'extra': build.worker.extra
1610 }
1611 if remaining and remaining > max_remaining:
1612 max_remaining = remaining
1613
1614 ret['jobs'].append({
1615 'name': job.name,
1616 'elapsed_time': elapsed,
1617 'remaining_time': remaining,
James E. Blairb7273ef2016-04-19 08:58:51 -07001618 'url': build_url,
1619 'report_url': report_url,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001620 'result': result,
1621 'voting': job.voting,
1622 'uuid': build.uuid if build else None,
Paul Belanger174a8272017-03-14 13:20:10 -04001623 'execute_time': build.execute_time if build else None,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001624 'start_time': build.start_time if build else None,
1625 'end_time': build.end_time if build else None,
1626 'estimated_time': build.estimated_time if build else None,
1627 'pipeline': build.pipeline.name if build else None,
1628 'canceled': build.canceled if build else None,
1629 'retry': build.retry if build else None,
Timothy Chavezb2332082015-08-07 20:08:04 -05001630 'node_labels': build.node_labels if build else [],
1631 'node_name': build.node_name if build else None,
1632 'worker': worker,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001633 })
1634
James E. Blairdbfd3282016-07-21 10:46:19 -07001635 if self.haveAllJobsStarted():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001636 ret['remaining_time'] = max_remaining
1637 else:
1638 ret['remaining_time'] = None
1639 return ret
1640
1641 def formatStatus(self, indent=0, html=False):
1642 changeish = self.change
1643 indent_str = ' ' * indent
1644 ret = ''
1645 if html and hasattr(changeish, 'url') and changeish.url is not None:
1646 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
1647 indent_str,
1648 changeish.project.name,
1649 changeish.url,
1650 changeish._id())
1651 else:
1652 ret += '%sProject %s change %s based on %s\n' % (
1653 indent_str,
1654 changeish.project.name,
1655 changeish._id(),
1656 self.item_ahead)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001657 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001658 build = self.current_build_set.getBuild(job.name)
1659 if build:
1660 result = build.result
1661 else:
1662 result = None
1663 job_name = job.name
1664 if not job.voting:
1665 voting = ' (non-voting)'
1666 else:
1667 voting = ''
1668 if html:
1669 if build:
1670 url = build.url
1671 else:
1672 url = None
1673 if url is not None:
1674 job_name = '<a href="%s">%s</a>' % (url, job_name)
1675 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
1676 ret += '\n'
1677 return ret
1678
James E. Blairfee8d652013-06-07 08:57:52 -07001679
Clint Byrumf8cc9902017-03-22 22:38:25 -07001680class Ref(object):
1681 """An existing state of a Project."""
James E. Blairfee8d652013-06-07 08:57:52 -07001682
1683 def __init__(self, project):
1684 self.project = project
Clint Byrumf8cc9902017-03-22 22:38:25 -07001685 self.ref = None
1686 self.oldrev = None
1687 self.newrev = None
James E. Blairfee8d652013-06-07 08:57:52 -07001688
Joshua Hesketh36c3fa52014-01-22 11:40:52 +11001689 def getBasePath(self):
1690 base_path = ''
Clint Byrumf8cc9902017-03-22 22:38:25 -07001691 if hasattr(self, 'ref'):
Joshua Hesketh36c3fa52014-01-22 11:40:52 +11001692 base_path = "%s/%s" % (self.newrev[:2], self.newrev)
1693
1694 return base_path
1695
Clint Byrumf8cc9902017-03-22 22:38:25 -07001696 def _id(self):
1697 return self.newrev
1698
1699 def __repr__(self):
1700 rep = None
1701 if self.newrev == '0000000000000000000000000000000000000000':
1702 rep = '<Ref 0x%x deletes %s from %s' % (
1703 id(self), self.ref, self.oldrev)
1704 elif self.oldrev == '0000000000000000000000000000000000000000':
1705 rep = '<Ref 0x%x creates %s on %s>' % (
1706 id(self), self.ref, self.newrev)
1707 else:
1708 # Catch all
1709 rep = '<Ref 0x%x %s updated %s..%s>' % (
1710 id(self), self.ref, self.oldrev, self.newrev)
1711
1712 return rep
1713
James E. Blairfee8d652013-06-07 08:57:52 -07001714 def equals(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001715 if (self.project == other.project
1716 and self.ref == other.ref
1717 and self.newrev == other.newrev):
1718 return True
1719 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001720
1721 def isUpdateOf(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001722 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001723
1724 def filterJobs(self, jobs):
1725 return filter(lambda job: job.changeMatches(self), jobs)
1726
1727 def getRelatedChanges(self):
1728 return set()
1729
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001730 def updatesConfig(self):
1731 return False
1732
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001733 def getSafeAttributes(self):
1734 return Attributes(project=self.project,
1735 ref=self.ref,
1736 oldrev=self.oldrev,
1737 newrev=self.newrev)
1738
James E. Blair1e8dd892012-05-30 09:15:05 -07001739
Clint Byrumf8cc9902017-03-22 22:38:25 -07001740class Change(Ref):
Monty Taylora42a55b2016-07-29 07:53:33 -07001741 """A proposed new state for a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001742 def __init__(self, project):
1743 super(Change, self).__init__(project)
1744 self.branch = None
1745 self.number = None
1746 self.url = None
1747 self.patchset = None
1748 self.refspec = None
1749
James E. Blair70c71582013-03-06 08:50:50 -08001750 self.files = []
James E. Blair6965a4b2014-12-16 17:19:04 -08001751 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -07001752 self.needed_by_changes = []
1753 self.is_current_patchset = True
1754 self.can_merge = False
1755 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -07001756 self.failed_to_merge = False
James E. Blairc053d022014-01-22 14:57:33 -08001757 self.approvals = []
James E. Blair11041d22014-05-02 14:49:53 -07001758 self.open = None
1759 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001760 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001761
1762 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -07001763 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -07001764
1765 def __repr__(self):
1766 return '<Change 0x%x %s>' % (id(self), self._id())
1767
Clint Byrumf8cc9902017-03-22 22:38:25 -07001768 def getBasePath(self):
1769 if hasattr(self, 'refspec'):
1770 return "%s/%s/%s" % (
1771 self.number[-2:], self.number, self.patchset)
1772 return super(Change, self).getBasePath()
1773
James E. Blair4aea70c2012-07-26 14:23:24 -07001774 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +08001775 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -07001776 return True
1777 return False
1778
James E. Blair2fa50962013-01-30 21:50:41 -08001779 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -08001780 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -07001781 (hasattr(other, 'patchset') and
1782 self.patchset is not None and
1783 other.patchset is not None and
1784 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -08001785 return True
1786 return False
1787
James E. Blairfee8d652013-06-07 08:57:52 -07001788 def getRelatedChanges(self):
1789 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -08001790 for c in self.needs_changes:
1791 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -07001792 for c in self.needed_by_changes:
1793 related.add(c)
1794 related.update(c.getRelatedChanges())
1795 return related
James E. Blair4aea70c2012-07-26 14:23:24 -07001796
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001797 def updatesConfig(self):
1798 if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files:
1799 return True
1800 return False
1801
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001802 def getSafeAttributes(self):
1803 return Attributes(project=self.project,
1804 number=self.number,
1805 patchset=self.patchset)
1806
James E. Blair4aea70c2012-07-26 14:23:24 -07001807
James E. Blairee743612012-05-29 14:49:32 -07001808class TriggerEvent(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001809 """Incoming event from an external system."""
James E. Blairee743612012-05-29 14:49:32 -07001810 def __init__(self):
1811 self.data = None
James E. Blair32663402012-06-01 10:04:18 -07001812 # common
James E. Blairee743612012-05-29 14:49:32 -07001813 self.type = None
Paul Belangerbaca3132016-11-04 12:49:54 -04001814 # For management events (eg: enqueue / promote)
1815 self.tenant_name = None
James E. Blair6f284b42017-03-31 14:14:41 -07001816 self.project_hostname = None
James E. Blairee743612012-05-29 14:49:32 -07001817 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -07001818 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +01001819 # Representation of the user account that performed the event.
1820 self.account = None
James E. Blair32663402012-06-01 10:04:18 -07001821 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -07001822 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -07001823 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -07001824 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -07001825 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -07001826 self.approvals = []
1827 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07001828 self.comment = None
James E. Blair32663402012-06-01 10:04:18 -07001829 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -07001830 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -07001831 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07001832 self.newrev = None
James E. Blair63bb0ef2013-07-29 17:14:51 -07001833 # timer
1834 self.timespec = None
James E. Blairc494d542014-08-06 09:23:52 -07001835 # zuultrigger
1836 self.pipeline_name = None
James E. Blairad28e912013-11-27 10:43:22 -08001837 # For events that arrive with a destination pipeline (eg, from
1838 # an admin command, etc):
1839 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07001840
James E. Blair6f284b42017-03-31 14:14:41 -07001841 @property
1842 def canonical_project_name(self):
1843 return self.project_hostname + '/' + self.project_name
1844
James E. Blair9f9667e2012-06-12 17:51:08 -07001845 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001846 ret = '<TriggerEvent %s %s' % (self.type, self.project_name)
James E. Blair1e8dd892012-05-30 09:15:05 -07001847
James E. Blairee743612012-05-29 14:49:32 -07001848 if self.branch:
1849 ret += " %s" % self.branch
1850 if self.change_number:
1851 ret += " %s,%s" % (self.change_number, self.patch_number)
1852 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -07001853 ret += ' ' + ', '.join(
1854 ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
James E. Blairee743612012-05-29 14:49:32 -07001855 ret += '>'
1856
1857 return ret
1858
James E. Blair1e8dd892012-05-30 09:15:05 -07001859
James E. Blair9c17dbf2014-06-23 14:21:58 -07001860class BaseFilter(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001861 """Base Class for filtering which Changes and Events to process."""
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001862 def __init__(self, required_approvals=[], reject_approvals=[]):
James E. Blair5bf78a32015-07-30 18:08:24 +00001863 self._required_approvals = copy.deepcopy(required_approvals)
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001864 self.required_approvals = self._tidy_approvals(required_approvals)
1865 self._reject_approvals = copy.deepcopy(reject_approvals)
1866 self.reject_approvals = self._tidy_approvals(reject_approvals)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001867
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001868 def _tidy_approvals(self, approvals):
1869 for a in approvals:
James E. Blair9c17dbf2014-06-23 14:21:58 -07001870 for k, v in a.items():
1871 if k == 'username':
James E. Blairb01ec542016-06-16 09:46:49 -07001872 a['username'] = re.compile(v)
James E. Blair1fbfceb2014-06-23 14:42:53 -07001873 elif k in ['email', 'email-filter']:
James E. Blair5bf78a32015-07-30 18:08:24 +00001874 a['email'] = re.compile(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001875 elif k == 'newer-than':
James E. Blair5bf78a32015-07-30 18:08:24 +00001876 a[k] = time_to_seconds(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001877 elif k == 'older-than':
James E. Blair5bf78a32015-07-30 18:08:24 +00001878 a[k] = time_to_seconds(v)
James E. Blair1fbfceb2014-06-23 14:42:53 -07001879 if 'email-filter' in a:
1880 del a['email-filter']
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001881 return approvals
1882
1883 def _match_approval_required_approval(self, rapproval, approval):
1884 # Check if the required approval and approval match
1885 if 'description' not in approval:
1886 return False
1887 now = time.time()
1888 by = approval.get('by', {})
1889 for k, v in rapproval.items():
1890 if k == 'username':
James E. Blairb01ec542016-06-16 09:46:49 -07001891 if (not v.search(by.get('username', ''))):
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001892 return False
1893 elif k == 'email':
1894 if (not v.search(by.get('email', ''))):
1895 return False
1896 elif k == 'newer-than':
1897 t = now - v
1898 if (approval['grantedOn'] < t):
1899 return False
1900 elif k == 'older-than':
1901 t = now - v
1902 if (approval['grantedOn'] >= t):
1903 return False
1904 else:
1905 if not isinstance(v, list):
1906 v = [v]
1907 if (normalizeCategory(approval['description']) != k or
1908 int(approval['value']) not in v):
1909 return False
1910 return True
1911
1912 def matchesApprovals(self, change):
1913 if (self.required_approvals and not change.approvals
1914 or self.reject_approvals and not change.approvals):
1915 # A change with no approvals can not match
1916 return False
1917
1918 # TODO(jhesketh): If we wanted to optimise this slightly we could
1919 # analyse both the REQUIRE and REJECT filters by looping over the
1920 # approvals on the change and keeping track of what we have checked
1921 # rather than needing to loop on the change approvals twice
1922 return (self.matchesRequiredApprovals(change) and
1923 self.matchesNoRejectApprovals(change))
James E. Blair9c17dbf2014-06-23 14:21:58 -07001924
1925 def matchesRequiredApprovals(self, change):
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001926 # Check if any approvals match the requirements
James E. Blair5bf78a32015-07-30 18:08:24 +00001927 for rapproval in self.required_approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001928 matches_rapproval = False
James E. Blair9c17dbf2014-06-23 14:21:58 -07001929 for approval in change.approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001930 if self._match_approval_required_approval(rapproval, approval):
1931 # We have a matching approval so this requirement is
1932 # fulfilled
1933 matches_rapproval = True
James E. Blair5bf78a32015-07-30 18:08:24 +00001934 break
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001935 if not matches_rapproval:
James E. Blair5bf78a32015-07-30 18:08:24 +00001936 return False
James E. Blair9c17dbf2014-06-23 14:21:58 -07001937 return True
1938
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001939 def matchesNoRejectApprovals(self, change):
1940 # Check to make sure no approvals match a reject criteria
1941 for rapproval in self.reject_approvals:
1942 for approval in change.approvals:
1943 if self._match_approval_required_approval(rapproval, approval):
1944 # A reject approval has been matched, so we reject
1945 # immediately
1946 return False
1947 # To get here no rejects can have been matched so we should be good to
1948 # queue
1949 return True
1950
James E. Blair9c17dbf2014-06-23 14:21:58 -07001951
1952class EventFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07001953 """Allows a Pipeline to only respond to certain events."""
James E. Blairc0dedf82014-08-06 09:37:52 -07001954 def __init__(self, trigger, types=[], branches=[], refs=[],
1955 event_approvals={}, comments=[], emails=[], usernames=[],
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001956 timespecs=[], required_approvals=[], reject_approvals=[],
1957 pipelines=[], ignore_deletes=True):
James E. Blair9c17dbf2014-06-23 14:21:58 -07001958 super(EventFilter, self).__init__(
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001959 required_approvals=required_approvals,
1960 reject_approvals=reject_approvals)
James E. Blairc0dedf82014-08-06 09:37:52 -07001961 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07001962 self._types = types
1963 self._branches = branches
1964 self._refs = refs
James E. Blair1fbfceb2014-06-23 14:42:53 -07001965 self._comments = comments
1966 self._emails = emails
1967 self._usernames = usernames
James E. Blairc494d542014-08-06 09:23:52 -07001968 self._pipelines = pipelines
James E. Blairee743612012-05-29 14:49:32 -07001969 self.types = [re.compile(x) for x in types]
1970 self.branches = [re.compile(x) for x in branches]
1971 self.refs = [re.compile(x) for x in refs]
James E. Blair1fbfceb2014-06-23 14:42:53 -07001972 self.comments = [re.compile(x) for x in comments]
1973 self.emails = [re.compile(x) for x in emails]
1974 self.usernames = [re.compile(x) for x in usernames]
James E. Blairc494d542014-08-06 09:23:52 -07001975 self.pipelines = [re.compile(x) for x in pipelines]
James E. Blairc053d022014-01-22 14:57:33 -08001976 self.event_approvals = event_approvals
James E. Blair63bb0ef2013-07-29 17:14:51 -07001977 self.timespecs = timespecs
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001978 self.ignore_deletes = ignore_deletes
James E. Blairee743612012-05-29 14:49:32 -07001979
James E. Blair9f9667e2012-06-12 17:51:08 -07001980 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07001981 ret = '<EventFilter'
James E. Blair1e8dd892012-05-30 09:15:05 -07001982
James E. Blairee743612012-05-29 14:49:32 -07001983 if self._types:
1984 ret += ' types: %s' % ', '.join(self._types)
James E. Blairc494d542014-08-06 09:23:52 -07001985 if self._pipelines:
1986 ret += ' pipelines: %s' % ', '.join(self._pipelines)
James E. Blairee743612012-05-29 14:49:32 -07001987 if self._branches:
1988 ret += ' branches: %s' % ', '.join(self._branches)
1989 if self._refs:
1990 ret += ' refs: %s' % ', '.join(self._refs)
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07001991 if self.ignore_deletes:
1992 ret += ' ignore_deletes: %s' % self.ignore_deletes
James E. Blairc053d022014-01-22 14:57:33 -08001993 if self.event_approvals:
1994 ret += ' event_approvals: %s' % ', '.join(
1995 ['%s:%s' % a for a in self.event_approvals.items()])
James E. Blair5bf78a32015-07-30 18:08:24 +00001996 if self.required_approvals:
1997 ret += ' required_approvals: %s' % ', '.join(
1998 ['%s' % a for a in self._required_approvals])
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001999 if self.reject_approvals:
2000 ret += ' reject_approvals: %s' % ', '.join(
2001 ['%s' % a for a in self._reject_approvals])
James E. Blair1fbfceb2014-06-23 14:42:53 -07002002 if self._comments:
2003 ret += ' comments: %s' % ', '.join(self._comments)
2004 if self._emails:
2005 ret += ' emails: %s' % ', '.join(self._emails)
2006 if self._usernames:
2007 ret += ' username_filters: %s' % ', '.join(self._usernames)
James E. Blair63bb0ef2013-07-29 17:14:51 -07002008 if self.timespecs:
2009 ret += ' timespecs: %s' % ', '.join(self.timespecs)
James E. Blairee743612012-05-29 14:49:32 -07002010 ret += '>'
2011
2012 return ret
2013
James E. Blairc053d022014-01-22 14:57:33 -08002014 def matches(self, event, change):
James E. Blairee743612012-05-29 14:49:32 -07002015 # event types are ORed
2016 matches_type = False
2017 for etype in self.types:
2018 if etype.match(event.type):
2019 matches_type = True
2020 if self.types and not matches_type:
2021 return False
2022
James E. Blairc494d542014-08-06 09:23:52 -07002023 # pipelines are ORed
2024 matches_pipeline = False
2025 for epipe in self.pipelines:
2026 if epipe.match(event.pipeline_name):
2027 matches_pipeline = True
2028 if self.pipelines and not matches_pipeline:
2029 return False
2030
James E. Blairee743612012-05-29 14:49:32 -07002031 # branches are ORed
2032 matches_branch = False
2033 for branch in self.branches:
2034 if branch.match(event.branch):
2035 matches_branch = True
2036 if self.branches and not matches_branch:
2037 return False
2038
2039 # refs are ORed
2040 matches_ref = False
Yolanda Robla16698872014-08-25 11:59:27 +02002041 if event.ref is not None:
2042 for ref in self.refs:
2043 if ref.match(event.ref):
2044 matches_ref = True
James E. Blairee743612012-05-29 14:49:32 -07002045 if self.refs and not matches_ref:
2046 return False
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07002047 if self.ignore_deletes and event.newrev == EMPTY_GIT_REF:
2048 # If the updated ref has an empty git sha (all 0s),
2049 # then the ref is being deleted
2050 return False
James E. Blairee743612012-05-29 14:49:32 -07002051
James E. Blair1fbfceb2014-06-23 14:42:53 -07002052 # comments are ORed
2053 matches_comment_re = False
2054 for comment_re in self.comments:
Clark Boylanb9bcb402012-06-29 17:44:05 -07002055 if (event.comment is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07002056 comment_re.search(event.comment)):
2057 matches_comment_re = True
2058 if self.comments and not matches_comment_re:
Clark Boylanb9bcb402012-06-29 17:44:05 -07002059 return False
2060
Antoine Mussob4e809e2012-12-06 16:58:06 +01002061 # We better have an account provided by Gerrit to do
2062 # email filtering.
2063 if event.account is not None:
James E. Blaircf429f32012-12-20 14:28:24 -08002064 account_email = event.account.get('email')
James E. Blair1fbfceb2014-06-23 14:42:53 -07002065 # emails are ORed
2066 matches_email_re = False
2067 for email_re in self.emails:
Antoine Mussob4e809e2012-12-06 16:58:06 +01002068 if (account_email is not None and
Joshua Hesketh29d99b72014-08-19 16:27:42 +10002069 email_re.search(account_email)):
James E. Blair1fbfceb2014-06-23 14:42:53 -07002070 matches_email_re = True
2071 if self.emails and not matches_email_re:
Antoine Mussob4e809e2012-12-06 16:58:06 +01002072 return False
2073
James E. Blair1fbfceb2014-06-23 14:42:53 -07002074 # usernames are ORed
Joshua Heskethb8a817e2013-12-27 11:21:38 +11002075 account_username = event.account.get('username')
James E. Blair1fbfceb2014-06-23 14:42:53 -07002076 matches_username_re = False
2077 for username_re in self.usernames:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11002078 if (account_username is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07002079 username_re.search(account_username)):
2080 matches_username_re = True
2081 if self.usernames and not matches_username_re:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11002082 return False
2083
James E. Blairee743612012-05-29 14:49:32 -07002084 # approvals are ANDed
James E. Blairc053d022014-01-22 14:57:33 -08002085 for category, value in self.event_approvals.items():
James E. Blairee743612012-05-29 14:49:32 -07002086 matches_approval = False
2087 for eapproval in event.approvals:
2088 if (normalizeCategory(eapproval['description']) == category and
2089 int(eapproval['value']) == int(value)):
2090 matches_approval = True
James E. Blair1e8dd892012-05-30 09:15:05 -07002091 if not matches_approval:
2092 return False
James E. Blair63bb0ef2013-07-29 17:14:51 -07002093
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002094 # required approvals are ANDed (reject approvals are ORed)
2095 if not self.matchesApprovals(change):
James E. Blair9c17dbf2014-06-23 14:21:58 -07002096 return False
James E. Blairc053d022014-01-22 14:57:33 -08002097
James E. Blair63bb0ef2013-07-29 17:14:51 -07002098 # timespecs are ORed
2099 matches_timespec = False
2100 for timespec in self.timespecs:
2101 if (event.timespec == timespec):
2102 matches_timespec = True
2103 if self.timespecs and not matches_timespec:
2104 return False
2105
James E. Blairee743612012-05-29 14:49:32 -07002106 return True
James E. Blaireff88162013-07-01 12:44:14 -04002107
2108
James E. Blair9c17dbf2014-06-23 14:21:58 -07002109class ChangeishFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002110 """Allows a Manager to only enqueue Changes that meet certain criteria."""
Clark Boylana9702ad2014-05-08 17:17:24 -07002111 def __init__(self, open=None, current_patchset=None,
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002112 statuses=[], required_approvals=[],
2113 reject_approvals=[]):
James E. Blair9c17dbf2014-06-23 14:21:58 -07002114 super(ChangeishFilter, self).__init__(
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002115 required_approvals=required_approvals,
2116 reject_approvals=reject_approvals)
James E. Blair11041d22014-05-02 14:49:53 -07002117 self.open = open
Clark Boylana9702ad2014-05-08 17:17:24 -07002118 self.current_patchset = current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07002119 self.statuses = statuses
James E. Blair11041d22014-05-02 14:49:53 -07002120
2121 def __repr__(self):
2122 ret = '<ChangeishFilter'
2123
2124 if self.open is not None:
2125 ret += ' open: %s' % self.open
Clark Boylana9702ad2014-05-08 17:17:24 -07002126 if self.current_patchset is not None:
2127 ret += ' current-patchset: %s' % self.current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07002128 if self.statuses:
2129 ret += ' statuses: %s' % ', '.join(self.statuses)
James E. Blair5bf78a32015-07-30 18:08:24 +00002130 if self.required_approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002131 ret += (' required_approvals: %s' %
2132 str(self.required_approvals))
2133 if self.reject_approvals:
2134 ret += (' reject_approvals: %s' %
2135 str(self.reject_approvals))
James E. Blair11041d22014-05-02 14:49:53 -07002136 ret += '>'
2137
2138 return ret
2139
2140 def matches(self, change):
2141 if self.open is not None:
2142 if self.open != change.open:
2143 return False
2144
Clark Boylana9702ad2014-05-08 17:17:24 -07002145 if self.current_patchset is not None:
2146 if self.current_patchset != change.is_current_patchset:
2147 return False
2148
James E. Blair11041d22014-05-02 14:49:53 -07002149 if self.statuses:
2150 if change.status not in self.statuses:
2151 return False
2152
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002153 # required approvals are ANDed (reject approvals are ORed)
2154 if not self.matchesApprovals(change):
James E. Blair9c17dbf2014-06-23 14:21:58 -07002155 return False
James E. Blair11041d22014-05-02 14:49:53 -07002156
2157 return True
2158
2159
James E. Blairb97ed802015-12-21 15:55:35 -08002160class ProjectPipelineConfig(object):
2161 # Represents a project cofiguration in the context of a pipeline
2162 def __init__(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002163 self.job_list = JobList()
James E. Blairb97ed802015-12-21 15:55:35 -08002164 self.queue_name = None
Adam Gandelman8bd57102016-12-02 12:58:42 -08002165 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08002166
2167
2168class ProjectConfig(object):
2169 # Represents a project cofiguration
2170 def __init__(self, name):
2171 self.name = name
Adam Gandelman8bd57102016-12-02 12:58:42 -08002172 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08002173 self.pipelines = {}
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002174 self.private_key_file = None
James E. Blairb97ed802015-12-21 15:55:35 -08002175
2176
James E. Blaird8e778f2015-12-22 14:09:20 -08002177class UnparsedAbideConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002178 """A collection of yaml lists that has not yet been parsed into objects.
2179
2180 An Abide is a collection of tenants.
2181 """
2182
James E. Blaird8e778f2015-12-22 14:09:20 -08002183 def __init__(self):
2184 self.tenants = []
2185
2186 def extend(self, conf):
2187 if isinstance(conf, UnparsedAbideConfig):
2188 self.tenants.extend(conf.tenants)
2189 return
2190
2191 if not isinstance(conf, list):
2192 raise Exception("Configuration items must be in the form of "
2193 "a list of dictionaries (when parsing %s)" %
2194 (conf,))
2195 for item in conf:
2196 if not isinstance(item, dict):
2197 raise Exception("Configuration items must be in the form of "
2198 "a list of dictionaries (when parsing %s)" %
2199 (conf,))
2200 if len(item.keys()) > 1:
2201 raise Exception("Configuration item dictionaries must have "
2202 "a single key (when parsing %s)" %
2203 (conf,))
2204 key, value = item.items()[0]
2205 if key == 'tenant':
2206 self.tenants.append(value)
2207 else:
2208 raise Exception("Configuration item not recognized "
2209 "(when parsing %s)" %
2210 (conf,))
2211
2212
2213class UnparsedTenantConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002214 """A collection of yaml lists that has not yet been parsed into objects."""
2215
James E. Blaird8e778f2015-12-22 14:09:20 -08002216 def __init__(self):
2217 self.pipelines = []
2218 self.jobs = []
2219 self.project_templates = []
James E. Blairff555742017-02-19 11:34:27 -08002220 self.projects = {}
James E. Blaira98340f2016-09-02 11:33:49 -07002221 self.nodesets = []
James E. Blair01f83b72017-03-15 13:03:40 -07002222 self.secrets = []
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002223 self.semaphores = []
James E. Blaird8e778f2015-12-22 14:09:20 -08002224
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002225 def copy(self):
2226 r = UnparsedTenantConfig()
2227 r.pipelines = copy.deepcopy(self.pipelines)
2228 r.jobs = copy.deepcopy(self.jobs)
2229 r.project_templates = copy.deepcopy(self.project_templates)
2230 r.projects = copy.deepcopy(self.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07002231 r.nodesets = copy.deepcopy(self.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002232 r.secrets = copy.deepcopy(self.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002233 r.semaphores = copy.deepcopy(self.semaphores)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002234 return r
2235
James E. Blairec7ff302017-03-04 07:31:32 -08002236 def extend(self, conf):
James E. Blaird8e778f2015-12-22 14:09:20 -08002237 if isinstance(conf, UnparsedTenantConfig):
2238 self.pipelines.extend(conf.pipelines)
2239 self.jobs.extend(conf.jobs)
2240 self.project_templates.extend(conf.project_templates)
James E. Blairff555742017-02-19 11:34:27 -08002241 for k, v in conf.projects.items():
2242 self.projects.setdefault(k, []).extend(v)
James E. Blaira98340f2016-09-02 11:33:49 -07002243 self.nodesets.extend(conf.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002244 self.secrets.extend(conf.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002245 self.semaphores.extend(conf.semaphores)
James E. Blaird8e778f2015-12-22 14:09:20 -08002246 return
2247
2248 if not isinstance(conf, list):
2249 raise Exception("Configuration items must be in the form of "
2250 "a list of dictionaries (when parsing %s)" %
2251 (conf,))
James E. Blaircdab2032017-02-01 09:09:29 -08002252
James E. Blaird8e778f2015-12-22 14:09:20 -08002253 for item in conf:
2254 if not isinstance(item, dict):
2255 raise Exception("Configuration items must be in the form of "
2256 "a list of dictionaries (when parsing %s)" %
2257 (conf,))
2258 if len(item.keys()) > 1:
2259 raise Exception("Configuration item dictionaries must have "
2260 "a single key (when parsing %s)" %
2261 (conf,))
2262 key, value = item.items()[0]
James E. Blair66b274e2017-01-31 14:47:52 -08002263 if key == 'project':
James E. Blairff555742017-02-19 11:34:27 -08002264 name = value['name']
2265 self.projects.setdefault(name, []).append(value)
James E. Blair66b274e2017-01-31 14:47:52 -08002266 elif key == 'job':
James E. Blaird8e778f2015-12-22 14:09:20 -08002267 self.jobs.append(value)
2268 elif key == 'project-template':
2269 self.project_templates.append(value)
2270 elif key == 'pipeline':
2271 self.pipelines.append(value)
James E. Blaira98340f2016-09-02 11:33:49 -07002272 elif key == 'nodeset':
2273 self.nodesets.append(value)
James E. Blair01f83b72017-03-15 13:03:40 -07002274 elif key == 'secret':
2275 self.secrets.append(value)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002276 elif key == 'semaphore':
2277 self.semaphores.append(value)
James E. Blaird8e778f2015-12-22 14:09:20 -08002278 else:
Morgan Fainberg4245a422016-08-05 16:20:12 -07002279 raise Exception("Configuration item `%s` not recognized "
James E. Blaird8e778f2015-12-22 14:09:20 -08002280 "(when parsing %s)" %
Morgan Fainberg4245a422016-08-05 16:20:12 -07002281 (item, conf,))
James E. Blaird8e778f2015-12-22 14:09:20 -08002282
2283
James E. Blaireff88162013-07-01 12:44:14 -04002284class Layout(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002285 """Holds all of the Pipelines."""
2286
James E. Blaireff88162013-07-01 12:44:14 -04002287 def __init__(self):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002288 self.tenant = None
James E. Blairb97ed802015-12-21 15:55:35 -08002289 self.project_configs = {}
2290 self.project_templates = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07002291 self.pipelines = OrderedDict()
James E. Blair83005782015-12-11 14:46:03 -08002292 # This is a dictionary of name -> [jobs]. The first element
2293 # of the list is the first job added with that name. It is
2294 # the reference definition for a given job. Subsequent
2295 # elements are aspects of that job with different matchers
2296 # that override some attribute of the job. These aspects all
2297 # inherit from the reference definition.
James E. Blairfef88ec2016-11-30 08:38:27 -08002298 self.jobs = {'noop': [Job('noop')]}
James E. Blaira98340f2016-09-02 11:33:49 -07002299 self.nodesets = {}
James E. Blair01f83b72017-03-15 13:03:40 -07002300 self.secrets = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002301 self.semaphores = {}
James E. Blaireff88162013-07-01 12:44:14 -04002302
2303 def getJob(self, name):
2304 if name in self.jobs:
James E. Blair83005782015-12-11 14:46:03 -08002305 return self.jobs[name][0]
James E. Blairb97ed802015-12-21 15:55:35 -08002306 raise Exception("Job %s not defined" % (name,))
James E. Blair83005782015-12-11 14:46:03 -08002307
2308 def getJobs(self, name):
2309 return self.jobs.get(name, [])
2310
2311 def addJob(self, job):
James E. Blair4317e9f2016-07-15 10:05:47 -07002312 # We can have multiple variants of a job all with the same
2313 # name, but these variants must all be defined in the same repo.
James E. Blaircdab2032017-02-01 09:09:29 -08002314 prior_jobs = [j for j in self.getJobs(job.name) if
2315 j.source_context.project !=
2316 job.source_context.project]
James E. Blair4317e9f2016-07-15 10:05:47 -07002317 if prior_jobs:
2318 raise Exception("Job %s in %s is not permitted to shadow "
James E. Blaircdab2032017-02-01 09:09:29 -08002319 "job %s in %s" % (
2320 job,
2321 job.source_context.project,
2322 prior_jobs[0],
2323 prior_jobs[0].source_context.project))
James E. Blair4317e9f2016-07-15 10:05:47 -07002324
James E. Blair83005782015-12-11 14:46:03 -08002325 if job.name in self.jobs:
2326 self.jobs[job.name].append(job)
2327 else:
2328 self.jobs[job.name] = [job]
2329
James E. Blaira98340f2016-09-02 11:33:49 -07002330 def addNodeSet(self, nodeset):
2331 if nodeset.name in self.nodesets:
2332 raise Exception("NodeSet %s already defined" % (nodeset.name,))
2333 self.nodesets[nodeset.name] = nodeset
2334
James E. Blair01f83b72017-03-15 13:03:40 -07002335 def addSecret(self, secret):
2336 if secret.name in self.secrets:
2337 raise Exception("Secret %s already defined" % (secret.name,))
2338 self.secrets[secret.name] = secret
2339
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002340 def addSemaphore(self, semaphore):
2341 if semaphore.name in self.semaphores:
2342 raise Exception("Semaphore %s already defined" % (semaphore.name,))
2343 self.semaphores[semaphore.name] = semaphore
2344
James E. Blair83005782015-12-11 14:46:03 -08002345 def addPipeline(self, pipeline):
2346 self.pipelines[pipeline.name] = pipeline
James E. Blair59fdbac2015-12-07 17:08:06 -08002347
James E. Blairb97ed802015-12-21 15:55:35 -08002348 def addProjectTemplate(self, project_template):
2349 self.project_templates[project_template.name] = project_template
2350
James E. Blairf59f3cf2017-02-19 14:50:26 -08002351 def addProjectConfig(self, project_config):
James E. Blairb97ed802015-12-21 15:55:35 -08002352 self.project_configs[project_config.name] = project_config
James E. Blairb97ed802015-12-21 15:55:35 -08002353
James E. Blaird2348362017-03-17 13:59:35 -07002354 def _createJobGraph(self, item, job_list, job_graph):
2355 change = item.change
2356 pipeline = item.pipeline
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002357 for jobname in job_list.jobs:
2358 # This is the final job we are constructing
James E. Blaira7f51ca2017-02-07 16:01:26 -08002359 frozen_job = None
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002360 # Whether the change matches any globally defined variant
James E. Blaira7f51ca2017-02-07 16:01:26 -08002361 matched = False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002362 for variant in self.getJobs(jobname):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002363 if variant.changeMatches(change):
James E. Blaira7f51ca2017-02-07 16:01:26 -08002364 if frozen_job is None:
2365 frozen_job = variant.copy()
2366 frozen_job.setRun()
2367 else:
2368 frozen_job.applyVariant(variant)
2369 matched = True
2370 if not matched:
James E. Blair6e85c2b2016-11-21 16:47:01 -08002371 # A change must match at least one defined job variant
2372 # (that is to say that it must match more than just
2373 # the job that is defined in the tree).
2374 continue
James E. Blaira7f51ca2017-02-07 16:01:26 -08002375 # If the job does not allow auth inheritance, do not allow
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002376 # the project-pipeline variants to update its execution
James E. Blaira7f51ca2017-02-07 16:01:26 -08002377 # attributes.
James E. Blair8525e2b2017-03-15 14:05:47 -07002378 if frozen_job.auth and not frozen_job.auth.inherit:
James E. Blaira7f51ca2017-02-07 16:01:26 -08002379 frozen_job.final = True
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002380 # Whether the change matches any of the project pipeline
2381 # variants
2382 matched = False
2383 for variant in job_list.jobs[jobname]:
2384 if variant.changeMatches(change):
2385 frozen_job.applyVariant(variant)
2386 matched = True
2387 if not matched:
2388 # A change must match at least one project pipeline
2389 # job variant.
2390 continue
James E. Blairb3f5db12017-03-17 12:57:39 -07002391 if (frozen_job.allowed_projects and
2392 change.project.name not in frozen_job.allowed_projects):
2393 raise Exception("Project %s is not allowed to run job %s" %
2394 (change.project.name, frozen_job.name))
James E. Blaird2348362017-03-17 13:59:35 -07002395 if ((not pipeline.allow_secrets) and frozen_job.auth and
2396 frozen_job.auth.secrets):
2397 raise Exception("Pipeline %s does not allow jobs with "
2398 "secrets (job %s)" % (
2399 pipeline.name, frozen_job.name))
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002400 job_graph.addJob(frozen_job)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002401
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002402 def createJobGraph(self, item):
Paul Belanger160cb8e2016-11-11 19:04:24 -05002403 project_config = self.project_configs.get(
2404 item.change.project.name, None)
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002405 ret = JobGraph()
Paul Belanger15e3e202016-10-14 16:27:34 -04002406 # NOTE(pabelanger): It is possible for a foreign project not to have a
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002407 # configured pipeline, if so return an empty JobGraph.
Paul Belanger160cb8e2016-11-11 19:04:24 -05002408 if project_config and item.pipeline.name in project_config.pipelines:
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002409 project_job_list = \
2410 project_config.pipelines[item.pipeline.name].job_list
James E. Blaird2348362017-03-17 13:59:35 -07002411 self._createJobGraph(item, project_job_list, ret)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002412 return ret
2413
James E. Blair59fdbac2015-12-07 17:08:06 -08002414
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002415class Semaphore(object):
2416 def __init__(self, name, max=1):
2417 self.name = name
2418 self.max = int(max)
2419
2420
2421class SemaphoreHandler(object):
2422 log = logging.getLogger("zuul.SemaphoreHandler")
2423
2424 def __init__(self):
2425 self.semaphores = {}
2426
2427 def acquire(self, item, job):
2428 if not job.semaphore:
2429 return True
2430
2431 semaphore_key = job.semaphore
2432
2433 m = self.semaphores.get(semaphore_key)
2434 if not m:
2435 # The semaphore is not held, acquire it
2436 self._acquire(semaphore_key, item, job.name)
2437 return True
2438 if (item, job.name) in m:
2439 # This item already holds the semaphore
2440 return True
2441
2442 # semaphore is there, check max
2443 if len(m) < self._max_count(item, job.semaphore):
2444 self._acquire(semaphore_key, item, job.name)
2445 return True
2446
2447 return False
2448
2449 def release(self, item, job):
2450 if not job.semaphore:
2451 return
2452
2453 semaphore_key = job.semaphore
2454
2455 m = self.semaphores.get(semaphore_key)
2456 if not m:
2457 # The semaphore is not held, nothing to do
2458 self.log.error("Semaphore can not be released for %s "
2459 "because the semaphore is not held" %
2460 item)
2461 return
2462 if (item, job.name) in m:
2463 # This item is a holder of the semaphore
2464 self._release(semaphore_key, item, job.name)
2465 return
2466 self.log.error("Semaphore can not be released for %s "
2467 "which does not hold it" % item)
2468
2469 def _acquire(self, semaphore_key, item, job_name):
2470 self.log.debug("Semaphore acquire {semaphore}: job {job}, item {item}"
2471 .format(semaphore=semaphore_key,
2472 job=job_name,
2473 item=item))
2474 if semaphore_key not in self.semaphores:
2475 self.semaphores[semaphore_key] = []
2476 self.semaphores[semaphore_key].append((item, job_name))
2477
2478 def _release(self, semaphore_key, item, job_name):
2479 self.log.debug("Semaphore release {semaphore}: job {job}, item {item}"
2480 .format(semaphore=semaphore_key,
2481 job=job_name,
2482 item=item))
2483 sem_item = (item, job_name)
2484 if sem_item in self.semaphores[semaphore_key]:
2485 self.semaphores[semaphore_key].remove(sem_item)
2486
2487 # cleanup if there is no user of the semaphore anymore
2488 if len(self.semaphores[semaphore_key]) == 0:
2489 del self.semaphores[semaphore_key]
2490
2491 @staticmethod
2492 def _max_count(item, semaphore_name):
2493 if not item.current_build_set.layout:
2494 # This should not occur as the layout of the item must already be
2495 # built when acquiring or releasing a semaphore for a job.
2496 raise Exception("Item {} has no layout".format(item))
2497
2498 # find the right semaphore
2499 default_semaphore = Semaphore(semaphore_name, 1)
2500 semaphores = item.current_build_set.layout.semaphores
2501 return semaphores.get(semaphore_name, default_semaphore).max
2502
2503
James E. Blair59fdbac2015-12-07 17:08:06 -08002504class Tenant(object):
2505 def __init__(self, name):
2506 self.name = name
2507 self.layout = None
James E. Blair646322f2017-01-27 15:50:34 -08002508 # The unparsed configuration from the main zuul config for
2509 # this tenant.
2510 self.unparsed_config = None
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002511 # The list of repos from which we will read main
James E. Blair8a395f92017-03-30 11:15:33 -07002512 # configuration.
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002513 self.config_repos = []
2514 # The unparsed config from those repos.
2515 self.config_repos_config = None
2516 # The list of projects from which we will read in-repo
James E. Blair8a395f92017-03-30 11:15:33 -07002517 # configuration.
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002518 self.project_repos = []
2519 # The unparsed config from those repos.
2520 self.project_repos_config = None
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002521 self.semaphore_handler = SemaphoreHandler()
2522
James E. Blairc2a54fd2017-03-29 15:19:26 -07002523 # A mapping of project names to projects. project_name ->
2524 # VALUE where VALUE is a further dictionary of
2525 # canonical_hostname -> Project.
2526 self.projects = {}
2527 self.canonical_hostnames = set()
2528
2529 def _addProject(self, project):
2530 """Add a project to the project index
2531
2532 :arg Project project: The project to add.
2533 """
2534 self.canonical_hostnames.add(project.canonical_hostname)
2535 hostname_dict = self.projects.setdefault(project.name, {})
2536 if project.canonical_hostname in hostname_dict:
2537 raise Exception("Project %s is already in project index" %
2538 (project,))
2539 hostname_dict[project.canonical_hostname] = project
2540
2541 def getProject(self, name):
2542 """Return a project given its name.
2543
2544 :arg str name: The name of the project. It may be fully
2545 qualified (E.g., "git.example.com/subpath/project") or may
2546 contain only the project name name may be supplied (E.g.,
2547 "subpath/project").
2548
2549 :returns: A tuple (trusted, project) or (None, None) if the
2550 project is not found or ambiguous. The "trusted" boolean
2551 indicates whether or not the project is trusted by this
2552 tenant.
2553 :rtype: (bool, Project)
2554
2555 """
2556 path = name.split('/', 1)
2557 if path[0] in self.canonical_hostnames:
2558 hostname = path[0]
2559 project_name = path[1]
2560 else:
2561 hostname = None
2562 project_name = name
2563 hostname_dict = self.projects.get(project_name)
2564 project = None
2565 if hostname_dict:
2566 if hostname:
2567 project = hostname_dict.get(hostname)
2568 else:
2569 values = hostname_dict.values()
2570 if len(values) == 1:
2571 project = values[0]
2572 else:
2573 raise Exception("Project name '%s' is ambiguous, "
2574 "please fully qualify the project "
2575 "with a hostname" % (name,))
2576 if project is None:
2577 return (None, None)
James E. Blair8a395f92017-03-30 11:15:33 -07002578 if project in self.config_repos:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002579 return (True, project)
James E. Blair8a395f92017-03-30 11:15:33 -07002580 if project in self.project_repos:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002581 return (False, project)
2582 # This should never happen:
2583 raise Exception("Project %s is neither trusted nor untrusted" %
2584 (project,))
2585
James E. Blair8a395f92017-03-30 11:15:33 -07002586 def addConfigRepo(self, project):
2587 self.config_repos.append(project)
James E. Blairc2a54fd2017-03-29 15:19:26 -07002588 self._addProject(project)
James E. Blair5ac93842017-01-20 06:47:34 -08002589
James E. Blair8a395f92017-03-30 11:15:33 -07002590 def addProjectRepo(self, project):
2591 self.project_repos.append(project)
James E. Blairc2a54fd2017-03-29 15:19:26 -07002592 self._addProject(project)
James E. Blair5ac93842017-01-20 06:47:34 -08002593
James E. Blair59fdbac2015-12-07 17:08:06 -08002594
2595class Abide(object):
2596 def __init__(self):
2597 self.tenants = OrderedDict()
James E. Blairce8a2132016-05-19 15:21:52 -07002598
2599
2600class JobTimeData(object):
2601 format = 'B10H10H10B'
2602 version = 0
2603
2604 def __init__(self, path):
2605 self.path = path
2606 self.success_times = [0 for x in range(10)]
2607 self.failure_times = [0 for x in range(10)]
2608 self.results = [0 for x in range(10)]
2609
2610 def load(self):
2611 if not os.path.exists(self.path):
2612 return
2613 with open(self.path) as f:
2614 data = struct.unpack(self.format, f.read())
2615 version = data[0]
2616 if version != self.version:
2617 raise Exception("Unkown data version")
2618 self.success_times = list(data[1:11])
2619 self.failure_times = list(data[11:21])
2620 self.results = list(data[21:32])
2621
2622 def save(self):
2623 tmpfile = self.path + '.tmp'
2624 data = [self.version]
2625 data.extend(self.success_times)
2626 data.extend(self.failure_times)
2627 data.extend(self.results)
2628 data = struct.pack(self.format, *data)
2629 with open(tmpfile, 'w') as f:
2630 f.write(data)
2631 os.rename(tmpfile, self.path)
2632
2633 def add(self, elapsed, result):
2634 elapsed = int(elapsed)
2635 if result == 'SUCCESS':
2636 self.success_times.append(elapsed)
2637 self.success_times.pop(0)
2638 result = 0
2639 else:
2640 self.failure_times.append(elapsed)
2641 self.failure_times.pop(0)
2642 result = 1
2643 self.results.append(result)
2644 self.results.pop(0)
2645
2646 def getEstimatedTime(self):
2647 times = [x for x in self.success_times if x]
2648 if times:
2649 return float(sum(times)) / len(times)
2650 return 0.0
2651
2652
2653class TimeDataBase(object):
2654 def __init__(self, root):
2655 self.root = root
2656 self.jobs = {}
2657
2658 def _getTD(self, name):
2659 td = self.jobs.get(name)
2660 if not td:
2661 td = JobTimeData(os.path.join(self.root, name))
2662 self.jobs[name] = td
2663 td.load()
2664 return td
2665
2666 def getEstimatedTime(self, name):
2667 return self._getTD(name).getEstimatedTime()
2668
2669 def update(self, name, elapsed, result):
2670 td = self._getTD(name)
2671 td.add(elapsed, result)
2672 td.save()