blob: b14ce120ab19522c847282c4c9bb4195d018546d [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 +010017import logging
James E. Blairce8a2132016-05-19 15:21:52 -070018import os
James E. Blairee743612012-05-29 14:49:32 -070019import re
James E. Blairce8a2132016-05-19 15:21:52 -070020import struct
James E. Blairff986a12012-05-30 14:56:51 -070021import time
James E. Blair4886cc12012-07-18 15:39:41 -070022from uuid import uuid4
James E. Blair5a9918a2013-08-27 10:06:27 -070023import extras
24
James E. Blair5ac93842017-01-20 06:47:34 -080025import six
26
James E. Blair5a9918a2013-08-27 10:06:27 -070027OrderedDict = extras.try_imports(['collections.OrderedDict',
28 'ordereddict.OrderedDict'])
James E. Blair4886cc12012-07-18 15:39:41 -070029
30
K Jonathan Harkerf95e7232015-04-29 13:33:16 -070031EMPTY_GIT_REF = '0' * 40 # git sha of all zeros, used during creates/deletes
32
James E. Blair19deff22013-08-25 13:17:35 -070033MERGER_MERGE = 1 # "git merge"
34MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
35MERGER_CHERRY_PICK = 3 # "git cherry-pick"
36
37MERGER_MAP = {
38 'merge': MERGER_MERGE,
39 'merge-resolve': MERGER_MERGE_RESOLVE,
40 'cherry-pick': MERGER_CHERRY_PICK,
41}
James E. Blairee743612012-05-29 14:49:32 -070042
James E. Blair64ed6f22013-07-10 14:07:23 -070043PRECEDENCE_NORMAL = 0
44PRECEDENCE_LOW = 1
45PRECEDENCE_HIGH = 2
46
47PRECEDENCE_MAP = {
48 None: PRECEDENCE_NORMAL,
49 'low': PRECEDENCE_LOW,
50 'normal': PRECEDENCE_NORMAL,
51 'high': PRECEDENCE_HIGH,
52}
53
James E. Blair803e94f2017-01-06 09:18:59 -080054# Request states
55STATE_REQUESTED = 'requested'
56STATE_PENDING = 'pending'
57STATE_FULFILLED = 'fulfilled'
58STATE_FAILED = 'failed'
59REQUEST_STATES = set([STATE_REQUESTED,
60 STATE_PENDING,
61 STATE_FULFILLED,
62 STATE_FAILED])
63
64# Node states
65STATE_BUILDING = 'building'
66STATE_TESTING = 'testing'
67STATE_READY = 'ready'
68STATE_IN_USE = 'in-use'
69STATE_USED = 'used'
70STATE_HOLD = 'hold'
71STATE_DELETING = 'deleting'
72NODE_STATES = set([STATE_BUILDING,
73 STATE_TESTING,
74 STATE_READY,
75 STATE_IN_USE,
76 STATE_USED,
77 STATE_HOLD,
78 STATE_DELETING])
79
James E. Blair1e8dd892012-05-30 09:15:05 -070080
James E. Blairc053d022014-01-22 14:57:33 -080081def time_to_seconds(s):
82 if s.endswith('s'):
83 return int(s[:-1])
84 if s.endswith('m'):
85 return int(s[:-1]) * 60
86 if s.endswith('h'):
87 return int(s[:-1]) * 60 * 60
88 if s.endswith('d'):
89 return int(s[:-1]) * 24 * 60 * 60
90 if s.endswith('w'):
91 return int(s[:-1]) * 7 * 24 * 60 * 60
92 raise Exception("Unable to parse time value: %s" % s)
93
94
James E. Blair11041d22014-05-02 14:49:53 -070095def normalizeCategory(name):
96 name = name.lower()
97 return re.sub(' ', '-', name)
98
99
Joshua Hesketh58419cb2017-02-24 13:09:22 -0500100class Attributes(object):
101 """A class to hold attributes for string formatting."""
102
103 def __init__(self, **kw):
104 setattr(self, '__dict__', kw)
105
106
James E. Blair4aea70c2012-07-26 14:23:24 -0700107class Pipeline(object):
James E. Blair6053de42017-04-05 11:27:11 -0700108 """A configuration that ties together triggers, reporters and managers
Monty Taylor82dfd412016-07-29 12:01:28 -0700109
110 Trigger
111 A description of which events should be processed
112
113 Manager
114 Responsible for enqueing and dequeing Changes
115
116 Reporter
117 Communicates success and failure results somewhere
Monty Taylora42a55b2016-07-29 07:53:33 -0700118 """
James E. Blair83005782015-12-11 14:46:03 -0800119 def __init__(self, name, layout):
James E. Blair4aea70c2012-07-26 14:23:24 -0700120 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800121 self.layout = layout
James E. Blair8dbd56a2012-12-22 10:55:10 -0800122 self.description = None
James E. Blair56370192013-01-14 15:47:28 -0800123 self.failure_message = None
Joshua Heskethb7179772014-01-30 23:30:46 +1100124 self.merge_failure_message = None
James E. Blair56370192013-01-14 15:47:28 -0800125 self.success_message = None
Joshua Hesketh3979e3e2014-03-04 11:21:10 +1100126 self.footer_message = None
James E. Blair60af7f42016-03-11 16:11:06 -0800127 self.start_message = None
James E. Blaird2348362017-03-17 13:59:35 -0700128 self.allow_secrets = False
James E. Blair2fa50962013-01-30 21:50:41 -0800129 self.dequeue_on_new_patchset = True
James E. Blair17dd6772015-02-09 14:45:18 -0800130 self.ignore_dependencies = False
James E. Blair4aea70c2012-07-26 14:23:24 -0700131 self.manager = None
James E. Blaire0487072012-08-29 17:38:31 -0700132 self.queues = []
James E. Blair64ed6f22013-07-10 14:07:23 -0700133 self.precedence = PRECEDENCE_NORMAL
James E. Blair83005782015-12-11 14:46:03 -0800134 self.triggers = []
Joshua Hesketh352264b2015-08-11 23:42:08 +1000135 self.start_actions = []
136 self.success_actions = []
137 self.failure_actions = []
138 self.merge_failure_actions = []
139 self.disabled_actions = []
Joshua Hesketh89e829d2015-02-10 16:29:45 +1100140 self.disable_at = None
141 self._consecutive_failures = 0
142 self._disabled = False
Clark Boylan7603a372014-01-21 11:43:20 -0800143 self.window = None
144 self.window_floor = None
145 self.window_increase_type = None
146 self.window_increase_factor = None
147 self.window_decrease_type = None
148 self.window_decrease_factor = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700149
James E. Blair83005782015-12-11 14:46:03 -0800150 @property
151 def actions(self):
152 return (
153 self.start_actions +
154 self.success_actions +
155 self.failure_actions +
156 self.merge_failure_actions +
157 self.disabled_actions
158 )
159
James E. Blaird09c17a2012-08-07 09:23:14 -0700160 def __repr__(self):
161 return '<Pipeline %s>' % self.name
162
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100163 def getSafeAttributes(self):
164 return Attributes(name=self.name)
165
James E. Blair4aea70c2012-07-26 14:23:24 -0700166 def setManager(self, manager):
167 self.manager = manager
168
James E. Blaire0487072012-08-29 17:38:31 -0700169 def addQueue(self, queue):
170 self.queues.append(queue)
171
172 def getQueue(self, project):
173 for queue in self.queues:
174 if project in queue.projects:
175 return queue
176 return None
177
James E. Blairbfb8e042014-12-30 17:01:44 -0800178 def removeQueue(self, queue):
179 self.queues.remove(queue)
180
James E. Blaire0487072012-08-29 17:38:31 -0700181 def getChangesInQueue(self):
182 changes = []
183 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700184 changes.extend([x.change for x in shared_queue.queue])
James E. Blaire0487072012-08-29 17:38:31 -0700185 return changes
186
James E. Blairfee8d652013-06-07 08:57:52 -0700187 def getAllItems(self):
188 items = []
James E. Blaire0487072012-08-29 17:38:31 -0700189 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700190 items.extend(shared_queue.queue)
James E. Blairfee8d652013-06-07 08:57:52 -0700191 return items
James E. Blaire0487072012-08-29 17:38:31 -0700192
James E. Blair800e7ff2017-03-17 16:06:52 -0700193 def formatStatusJSON(self):
James E. Blair8dbd56a2012-12-22 10:55:10 -0800194 j_pipeline = dict(name=self.name,
195 description=self.description)
196 j_queues = []
197 j_pipeline['change_queues'] = j_queues
198 for queue in self.queues:
199 j_queue = dict(name=queue.name)
200 j_queues.append(j_queue)
201 j_queue['heads'] = []
Clark Boylanaf2476f2014-01-23 14:47:36 -0800202 j_queue['window'] = queue.window
James E. Blair972e3c72013-08-29 12:04:55 -0700203
204 j_changes = []
205 for e in queue.queue:
206 if not e.item_ahead:
207 if j_changes:
208 j_queue['heads'].append(j_changes)
209 j_changes = []
James E. Blair800e7ff2017-03-17 16:06:52 -0700210 j_changes.append(e.formatJSON())
James E. Blair972e3c72013-08-29 12:04:55 -0700211 if (len(j_changes) > 1 and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000212 (j_changes[-2]['remaining_time'] is not None) and
213 (j_changes[-1]['remaining_time'] is not None)):
James E. Blair972e3c72013-08-29 12:04:55 -0700214 j_changes[-1]['remaining_time'] = max(
215 j_changes[-2]['remaining_time'],
216 j_changes[-1]['remaining_time'])
217 if j_changes:
James E. Blair8dbd56a2012-12-22 10:55:10 -0800218 j_queue['heads'].append(j_changes)
219 return j_pipeline
220
James E. Blair4aea70c2012-07-26 14:23:24 -0700221
James E. Blairee743612012-05-29 14:49:32 -0700222class ChangeQueue(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700223 """A ChangeQueue contains Changes to be processed related projects.
224
Monty Taylor82dfd412016-07-29 12:01:28 -0700225 A Pipeline with a DependentPipelineManager has multiple parallel
226 ChangeQueues shared by different projects. For instance, there may a
227 ChangeQueue shared by interrelated projects foo and bar, and a second queue
228 for independent project baz.
Monty Taylora42a55b2016-07-29 07:53:33 -0700229
Monty Taylor82dfd412016-07-29 12:01:28 -0700230 A Pipeline with an IndependentPipelineManager puts every Change into its
231 own ChangeQueue
Monty Taylora42a55b2016-07-29 07:53:33 -0700232
233 The ChangeQueue Window is inspired by TCP windows and controlls how many
234 Changes in a given ChangeQueue will be considered active and ready to
235 be processed. If a Change succeeds, the Window is increased by
236 `window_increase_factor`. If a Change fails, the Window is decreased by
237 `window_decrease_factor`.
238 """
James E. Blairbfb8e042014-12-30 17:01:44 -0800239 def __init__(self, pipeline, window=0, window_floor=1,
Clark Boylan7603a372014-01-21 11:43:20 -0800240 window_increase_type='linear', window_increase_factor=1,
James E. Blair0dcef7a2016-08-19 09:35:17 -0700241 window_decrease_type='exponential', window_decrease_factor=2,
242 name=None):
James E. Blair4aea70c2012-07-26 14:23:24 -0700243 self.pipeline = pipeline
James E. Blair0dcef7a2016-08-19 09:35:17 -0700244 if name:
245 self.name = name
246 else:
247 self.name = ''
James E. Blairee743612012-05-29 14:49:32 -0700248 self.projects = []
249 self._jobs = set()
250 self.queue = []
Clark Boylan7603a372014-01-21 11:43:20 -0800251 self.window = window
252 self.window_floor = window_floor
253 self.window_increase_type = window_increase_type
254 self.window_increase_factor = window_increase_factor
255 self.window_decrease_type = window_decrease_type
256 self.window_decrease_factor = window_decrease_factor
James E. Blairee743612012-05-29 14:49:32 -0700257
James E. Blair9f9667e2012-06-12 17:51:08 -0700258 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700259 return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
James E. Blairee743612012-05-29 14:49:32 -0700260
261 def getJobs(self):
262 return self._jobs
263
264 def addProject(self, project):
265 if project not in self.projects:
266 self.projects.append(project)
James E. Blairc8a1e052014-02-25 09:29:26 -0800267
James E. Blair0dcef7a2016-08-19 09:35:17 -0700268 if not self.name:
269 self.name = project.name
James E. Blairee743612012-05-29 14:49:32 -0700270
271 def enqueueChange(self, change):
James E. Blairbfb8e042014-12-30 17:01:44 -0800272 item = QueueItem(self, change)
James E. Blaircdccd972013-07-01 12:10:22 -0700273 self.enqueueItem(item)
274 item.enqueue_time = time.time()
275 return item
276
277 def enqueueItem(self, item):
James E. Blair4a035d92014-01-23 13:10:48 -0800278 item.pipeline = self.pipeline
James E. Blairbfb8e042014-12-30 17:01:44 -0800279 item.queue = self
280 if self.queue:
James E. Blairfee8d652013-06-07 08:57:52 -0700281 item.item_ahead = self.queue[-1]
James E. Blair972e3c72013-08-29 12:04:55 -0700282 item.item_ahead.items_behind.append(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700283 self.queue.append(item)
James E. Blairee743612012-05-29 14:49:32 -0700284
James E. Blairfee8d652013-06-07 08:57:52 -0700285 def dequeueItem(self, item):
286 if item in self.queue:
287 self.queue.remove(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700288 if item.item_ahead:
James E. Blair972e3c72013-08-29 12:04:55 -0700289 item.item_ahead.items_behind.remove(item)
290 for item_behind in item.items_behind:
291 if item.item_ahead:
292 item.item_ahead.items_behind.append(item_behind)
293 item_behind.item_ahead = item.item_ahead
James E. Blairfee8d652013-06-07 08:57:52 -0700294 item.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700295 item.items_behind = []
James E. Blairfee8d652013-06-07 08:57:52 -0700296 item.dequeue_time = time.time()
James E. Blaire0487072012-08-29 17:38:31 -0700297
James E. Blair972e3c72013-08-29 12:04:55 -0700298 def moveItem(self, item, item_ahead):
James E. Blair972e3c72013-08-29 12:04:55 -0700299 if item.item_ahead == item_ahead:
300 return False
301 # Remove from current location
302 if item.item_ahead:
303 item.item_ahead.items_behind.remove(item)
304 for item_behind in item.items_behind:
305 if item.item_ahead:
306 item.item_ahead.items_behind.append(item_behind)
307 item_behind.item_ahead = item.item_ahead
308 # Add to new location
309 item.item_ahead = item_ahead
James E. Blair00451262013-09-20 11:40:17 -0700310 item.items_behind = []
James E. Blair972e3c72013-08-29 12:04:55 -0700311 if item.item_ahead:
312 item.item_ahead.items_behind.append(item)
313 return True
James E. Blairee743612012-05-29 14:49:32 -0700314
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800315 def isActionable(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800316 if self.window:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800317 return item in self.queue[:self.window]
Clark Boylan7603a372014-01-21 11:43:20 -0800318 else:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800319 return True
Clark Boylan7603a372014-01-21 11:43:20 -0800320
321 def increaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800322 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800323 if self.window_increase_type == 'linear':
324 self.window += self.window_increase_factor
325 elif self.window_increase_type == 'exponential':
326 self.window *= self.window_increase_factor
327
328 def decreaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800329 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800330 if self.window_decrease_type == 'linear':
331 self.window = max(
332 self.window_floor,
333 self.window - self.window_decrease_factor)
334 elif self.window_decrease_type == 'exponential':
335 self.window = max(
336 self.window_floor,
Morgan Fainbergfdae2252016-06-07 20:13:20 -0700337 int(self.window / self.window_decrease_factor))
James E. Blairee743612012-05-29 14:49:32 -0700338
James E. Blair1e8dd892012-05-30 09:15:05 -0700339
James E. Blair4aea70c2012-07-26 14:23:24 -0700340class Project(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700341 """A Project represents a git repository such as openstack/nova."""
342
James E. Blaircf440a22016-07-15 09:11:58 -0700343 # NOTE: Projects should only be instantiated via a Source object
344 # so that they are associated with and cached by their Connection.
345 # This makes a Project instance a unique identifier for a given
346 # project from a given source.
347
James E. Blair0a899752017-03-29 13:22:16 -0700348 def __init__(self, name, source, foreign=False):
James E. Blair4aea70c2012-07-26 14:23:24 -0700349 self.name = name
James E. Blair8a395f92017-03-30 11:15:33 -0700350 self.source = source
James E. Blair0a899752017-03-29 13:22:16 -0700351 self.connection_name = source.connection.connection_name
352 self.canonical_hostname = source.canonical_hostname
James E. Blairc2a54fd2017-03-29 15:19:26 -0700353 self.canonical_name = source.canonical_hostname + '/' + name
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000354 # foreign projects are those referenced in dependencies
355 # of layout projects, this should matter
356 # when deciding whether to enqueue their changes
James E. Blaircf440a22016-07-15 09:11:58 -0700357 # TODOv3 (jeblair): re-add support for foreign projects if needed
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000358 self.foreign = foreign
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700359 self.unparsed_config = None
James E. Blaire3162022017-02-20 16:47:27 -0500360 self.unparsed_branch_config = {} # branch -> UnparsedTenantConfig
James E. Blair4aea70c2012-07-26 14:23:24 -0700361
362 def __str__(self):
363 return self.name
364
365 def __repr__(self):
366 return '<Project %s>' % (self.name)
367
368
James E. Blair34776ee2016-08-25 13:53:54 -0700369class Node(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700370 """A single node for use by a job.
371
372 This may represent a request for a node, or an actual node
373 provided by Nodepool.
374 """
375
James E. Blair34776ee2016-08-25 13:53:54 -0700376 def __init__(self, name, image):
377 self.name = name
378 self.image = image
James E. Blaircbf43672017-01-04 14:33:41 -0800379 self.id = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800380 self.lock = None
381 # Attributes from Nodepool
382 self._state = 'unknown'
383 self.state_time = time.time()
Monty Taylor56f61332017-04-11 05:38:12 -0500384 self.interface_ip = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800385 self.public_ipv4 = None
386 self.private_ipv4 = None
387 self.public_ipv6 = None
James E. Blaircacdf2b2017-01-04 13:14:37 -0800388 self._keys = []
Paul Belanger30ba93a2017-03-16 16:28:10 -0400389 self.az = None
390 self.provider = None
391 self.region = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800392
393 @property
394 def state(self):
395 return self._state
396
397 @state.setter
398 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800399 if value not in NODE_STATES:
400 raise TypeError("'%s' is not a valid state" % value)
James E. Blaira38c28e2017-01-04 10:33:20 -0800401 self._state = value
402 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700403
404 def __repr__(self):
James E. Blaircbf43672017-01-04 14:33:41 -0800405 return '<Node %s %s:%s>' % (self.id, self.name, self.image)
James E. Blair34776ee2016-08-25 13:53:54 -0700406
James E. Blair0d952152017-02-07 17:14:44 -0800407 def __ne__(self, other):
408 return not self.__eq__(other)
409
410 def __eq__(self, other):
411 if not isinstance(other, Node):
412 return False
413 return (self.name == other.name and
414 self.image == other.image and
415 self.id == other.id)
416
James E. Blaircacdf2b2017-01-04 13:14:37 -0800417 def toDict(self):
418 d = {}
419 d['state'] = self.state
420 for k in self._keys:
421 d[k] = getattr(self, k)
422 return d
423
James E. Blaira38c28e2017-01-04 10:33:20 -0800424 def updateFromDict(self, data):
425 self._state = data['state']
James E. Blaircacdf2b2017-01-04 13:14:37 -0800426 keys = []
427 for k, v in data.items():
428 if k == 'state':
429 continue
430 keys.append(k)
431 setattr(self, k, v)
432 self._keys = keys
James E. Blaira38c28e2017-01-04 10:33:20 -0800433
James E. Blair34776ee2016-08-25 13:53:54 -0700434
James E. Blaira98340f2016-09-02 11:33:49 -0700435class NodeSet(object):
436 """A set of nodes.
437
438 In configuration, NodeSets are attributes of Jobs indicating that
439 a Job requires nodes matching this description.
440
441 They may appear as top-level configuration objects and be named,
442 or they may appears anonymously in in-line job definitions.
443 """
444
445 def __init__(self, name=None):
446 self.name = name or ''
447 self.nodes = OrderedDict()
448
James E. Blair1774dd52017-02-03 10:52:32 -0800449 def __ne__(self, other):
450 return not self.__eq__(other)
451
452 def __eq__(self, other):
453 if not isinstance(other, NodeSet):
454 return False
455 return (self.name == other.name and
456 self.nodes == other.nodes)
457
James E. Blaircbf43672017-01-04 14:33:41 -0800458 def copy(self):
459 n = NodeSet(self.name)
460 for name, node in self.nodes.items():
461 n.addNode(Node(node.name, node.image))
462 return n
463
James E. Blaira98340f2016-09-02 11:33:49 -0700464 def addNode(self, node):
465 if node.name in self.nodes:
466 raise Exception("Duplicate node in %s" % (self,))
467 self.nodes[node.name] = node
468
James E. Blair0eaad552016-09-02 12:09:54 -0700469 def getNodes(self):
470 return self.nodes.values()
471
James E. Blaira98340f2016-09-02 11:33:49 -0700472 def __repr__(self):
473 if self.name:
474 name = self.name + ' '
475 else:
476 name = ''
477 return '<NodeSet %s%s>' % (name, self.nodes)
478
479
James E. Blair34776ee2016-08-25 13:53:54 -0700480class NodeRequest(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700481 """A request for a set of nodes."""
482
James E. Blair8b2a1472017-02-19 15:33:55 -0800483 def __init__(self, requestor, build_set, job, nodeset):
484 self.requestor = requestor
James E. Blair34776ee2016-08-25 13:53:54 -0700485 self.build_set = build_set
486 self.job = job
James E. Blair0eaad552016-09-02 12:09:54 -0700487 self.nodeset = nodeset
James E. Blair803e94f2017-01-06 09:18:59 -0800488 self._state = STATE_REQUESTED
James E. Blairdce6cea2016-12-20 16:45:32 -0800489 self.state_time = time.time()
490 self.stat = None
491 self.uid = uuid4().hex
James E. Blaircbf43672017-01-04 14:33:41 -0800492 self.id = None
James E. Blaircbbce0d2017-05-19 07:28:29 -0700493 # Zuul internal flags (not stored in ZK so they are not
James E. Blaira38c28e2017-01-04 10:33:20 -0800494 # overwritten).
495 self.failed = False
James E. Blaircbbce0d2017-05-19 07:28:29 -0700496 self.canceled = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800497
498 @property
James E. Blair6ab79e02017-01-06 10:10:17 -0800499 def fulfilled(self):
500 return (self._state == STATE_FULFILLED) and not self.failed
501
502 @property
James E. Blairdce6cea2016-12-20 16:45:32 -0800503 def state(self):
504 return self._state
505
506 @state.setter
507 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800508 if value not in REQUEST_STATES:
509 raise TypeError("'%s' is not a valid state" % value)
James E. Blairdce6cea2016-12-20 16:45:32 -0800510 self._state = value
511 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700512
513 def __repr__(self):
James E. Blaircbf43672017-01-04 14:33:41 -0800514 return '<NodeRequest %s %s>' % (self.id, self.nodeset)
James E. Blair34776ee2016-08-25 13:53:54 -0700515
James E. Blairdce6cea2016-12-20 16:45:32 -0800516 def toDict(self):
517 d = {}
518 nodes = [n.image for n in self.nodeset.getNodes()]
519 d['node_types'] = nodes
James E. Blair8b2a1472017-02-19 15:33:55 -0800520 d['requestor'] = self.requestor
James E. Blairdce6cea2016-12-20 16:45:32 -0800521 d['state'] = self.state
522 d['state_time'] = self.state_time
523 return d
524
525 def updateFromDict(self, data):
526 self._state = data['state']
527 self.state_time = data['state_time']
528
James E. Blair34776ee2016-08-25 13:53:54 -0700529
James E. Blair01f83b72017-03-15 13:03:40 -0700530class Secret(object):
531 """A collection of private data.
532
533 In configuration, Secrets are collections of private data in
534 key-value pair format. They are defined as top-level
535 configuration objects and then referenced by Jobs.
536
537 """
538
James E. Blair8525e2b2017-03-15 14:05:47 -0700539 def __init__(self, name, source_context):
James E. Blair01f83b72017-03-15 13:03:40 -0700540 self.name = name
James E. Blair8525e2b2017-03-15 14:05:47 -0700541 self.source_context = source_context
James E. Blair01f83b72017-03-15 13:03:40 -0700542 # The secret data may or may not be encrypted. This attribute
543 # is named 'secret_data' to make it easy to search for and
544 # spot where it is directly used.
545 self.secret_data = {}
546
547 def __ne__(self, other):
548 return not self.__eq__(other)
549
550 def __eq__(self, other):
551 if not isinstance(other, Secret):
552 return False
553 return (self.name == other.name and
James E. Blair8525e2b2017-03-15 14:05:47 -0700554 self.source_context == other.source_context and
James E. Blair01f83b72017-03-15 13:03:40 -0700555 self.secret_data == other.secret_data)
556
557 def __repr__(self):
558 return '<Secret %s>' % (self.name,)
559
James E. Blair18f86a32017-03-15 14:43:26 -0700560 def decrypt(self, private_key):
561 """Return a copy of this secret with any encrypted data decrypted.
562 Note that the original remains encrypted."""
563
564 r = copy.deepcopy(self)
565 decrypted_secret_data = {}
566 for k, v in r.secret_data.items():
567 if hasattr(v, 'decrypt'):
568 decrypted_secret_data[k] = v.decrypt(private_key)
569 else:
570 decrypted_secret_data[k] = v
571 r.secret_data = decrypted_secret_data
572 return r
573
James E. Blair01f83b72017-03-15 13:03:40 -0700574
James E. Blaircdab2032017-02-01 09:09:29 -0800575class SourceContext(object):
576 """A reference to the branch of a project in configuration.
577
578 Jobs and playbooks reference this to keep track of where they
579 originate."""
580
James E. Blair6f140c72017-03-03 10:32:07 -0800581 def __init__(self, project, branch, path, trusted):
James E. Blaircdab2032017-02-01 09:09:29 -0800582 self.project = project
583 self.branch = branch
James E. Blair6f140c72017-03-03 10:32:07 -0800584 self.path = path
Monty Taylore6562aa2017-02-20 07:37:39 -0500585 self.trusted = trusted
James E. Blaircdab2032017-02-01 09:09:29 -0800586
James E. Blair6f140c72017-03-03 10:32:07 -0800587 def __str__(self):
588 return '%s/%s@%s' % (self.project, self.path, self.branch)
589
James E. Blaircdab2032017-02-01 09:09:29 -0800590 def __repr__(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800591 return '<SourceContext %s trusted:%s>' % (str(self),
592 self.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800593
James E. Blaira7f51ca2017-02-07 16:01:26 -0800594 def __deepcopy__(self, memo):
595 return self.copy()
596
597 def copy(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800598 return self.__class__(self.project, self.branch, self.path,
599 self.trusted)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800600
James E. Blaircdab2032017-02-01 09:09:29 -0800601 def __ne__(self, other):
602 return not self.__eq__(other)
603
604 def __eq__(self, other):
605 if not isinstance(other, SourceContext):
606 return False
607 return (self.project == other.project and
608 self.branch == other.branch and
James E. Blair6f140c72017-03-03 10:32:07 -0800609 self.path == other.path and
Monty Taylore6562aa2017-02-20 07:37:39 -0500610 self.trusted == other.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800611
612
James E. Blair66b274e2017-01-31 14:47:52 -0800613class PlaybookContext(object):
James E. Blaircdab2032017-02-01 09:09:29 -0800614
James E. Blair66b274e2017-01-31 14:47:52 -0800615 """A reference to a playbook in the context of a project.
616
617 Jobs refer to objects of this class for their main, pre, and post
618 playbooks so that we can keep track of which repos and security
619 contexts are needed in order to run them."""
620
James E. Blaircdab2032017-02-01 09:09:29 -0800621 def __init__(self, source_context, path):
622 self.source_context = source_context
James E. Blair66b274e2017-01-31 14:47:52 -0800623 self.path = path
James E. Blair66b274e2017-01-31 14:47:52 -0800624
625 def __repr__(self):
James E. Blaircdab2032017-02-01 09:09:29 -0800626 return '<PlaybookContext %s %s>' % (self.source_context,
627 self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800628
629 def __ne__(self, other):
630 return not self.__eq__(other)
631
632 def __eq__(self, other):
633 if not isinstance(other, PlaybookContext):
634 return False
James E. Blaircdab2032017-02-01 09:09:29 -0800635 return (self.source_context == other.source_context and
636 self.path == other.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800637
638 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400639 # Render to a dict to use in passing json to the executor
James E. Blair66b274e2017-01-31 14:47:52 -0800640 return dict(
James E. Blaircdab2032017-02-01 09:09:29 -0800641 connection=self.source_context.project.connection_name,
642 project=self.source_context.project.name,
643 branch=self.source_context.branch,
Monty Taylore6562aa2017-02-20 07:37:39 -0500644 trusted=self.source_context.trusted,
James E. Blaircdab2032017-02-01 09:09:29 -0800645 path=self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800646
647
James E. Blair5ac93842017-01-20 06:47:34 -0800648@six.add_metaclass(abc.ABCMeta)
649class Role(object):
650 """A reference to an ansible role."""
651
652 def __init__(self, target_name):
653 self.target_name = target_name
654
655 @abc.abstractmethod
656 def __repr__(self):
657 pass
658
659 def __ne__(self, other):
660 return not self.__eq__(other)
661
662 @abc.abstractmethod
663 def __eq__(self, other):
664 if not isinstance(other, Role):
665 return False
666 return (self.target_name == other.target_name)
667
668 @abc.abstractmethod
669 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400670 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800671 return dict(target_name=self.target_name)
672
673
674class ZuulRole(Role):
675 """A reference to an ansible role in a Zuul project."""
676
James E. Blair6563e4b2017-04-28 08:14:48 -0700677 def __init__(self, target_name, connection_name, project_name):
James E. Blair5ac93842017-01-20 06:47:34 -0800678 super(ZuulRole, self).__init__(target_name)
679 self.connection_name = connection_name
680 self.project_name = project_name
James E. Blair5ac93842017-01-20 06:47:34 -0800681
682 def __repr__(self):
683 return '<ZuulRole %s %s>' % (self.project_name, self.target_name)
684
685 def __eq__(self, other):
686 if not isinstance(other, ZuulRole):
687 return False
688 return (super(ZuulRole, self).__eq__(other) and
689 self.connection_name == other.connection_name,
James E. Blair6563e4b2017-04-28 08:14:48 -0700690 self.project_name == other.project_name)
James E. Blair5ac93842017-01-20 06:47:34 -0800691
692 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400693 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800694 d = super(ZuulRole, self).toDict()
695 d['type'] = 'zuul'
696 d['connection'] = self.connection_name
697 d['project'] = self.project_name
James E. Blair5ac93842017-01-20 06:47:34 -0800698 return d
699
700
James E. Blair8525e2b2017-03-15 14:05:47 -0700701class AuthContext(object):
702 """The authentication information for a job.
703
704 Authentication information (both the actual data and metadata such
705 as whether it should be inherited) for a job is grouped together
706 in this object.
707 """
708
709 def __init__(self, inherit=False):
710 self.inherit = inherit
711 self.secrets = []
712
713 def __ne__(self, other):
714 return not self.__eq__(other)
715
716 def __eq__(self, other):
717 if not isinstance(other, AuthContext):
718 return False
719 return (self.inherit == other.inherit and
720 self.secrets == other.secrets)
721
722
James E. Blairee743612012-05-29 14:49:32 -0700723class Job(object):
James E. Blair66b274e2017-01-31 14:47:52 -0800724
James E. Blaira7f51ca2017-02-07 16:01:26 -0800725 """A Job represents the defintion of actions to perform.
726
James E. Blaird4ade8c2017-02-19 15:25:46 -0800727 A Job is an abstract configuration concept. It describes what,
728 where, and under what circumstances something should be run
729 (contrast this with Build which is a concrete single execution of
730 a Job).
731
James E. Blaira7f51ca2017-02-07 16:01:26 -0800732 NB: Do not modify attributes of this class, set them directly
733 (e.g., "job.run = ..." rather than "job.run.append(...)").
734 """
Monty Taylora42a55b2016-07-29 07:53:33 -0700735
James E. Blairee743612012-05-29 14:49:32 -0700736 def __init__(self, name):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800737 # These attributes may override even the final form of a job
738 # in the context of a project-pipeline. They can not affect
739 # the execution of the job, but only whether the job is run
740 # and how it is reported.
741 self.context_attributes = dict(
742 voting=True,
743 hold_following_changes=False,
James E. Blair1774dd52017-02-03 10:52:32 -0800744 failure_message=None,
745 success_message=None,
746 failure_url=None,
747 success_url=None,
748 # Matchers. These are separate so they can be individually
749 # overidden.
750 branch_matcher=None,
751 file_matcher=None,
752 irrelevant_file_matcher=None, # skip-if
James E. Blaira7f51ca2017-02-07 16:01:26 -0800753 tags=frozenset(),
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200754 dependencies=frozenset(),
James E. Blair1774dd52017-02-03 10:52:32 -0800755 )
756
James E. Blaira7f51ca2017-02-07 16:01:26 -0800757 # These attributes affect how the job is actually run and more
758 # care must be taken when overriding them. If a job is
759 # declared "final", these may not be overriden in a
760 # project-pipeline.
761 self.execution_attributes = dict(
762 timeout=None,
James E. Blair490cf042017-02-24 23:07:21 -0500763 variables={},
James E. Blaira7f51ca2017-02-07 16:01:26 -0800764 nodeset=NodeSet(),
James E. Blair8525e2b2017-03-15 14:05:47 -0700765 auth=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800766 workspace=None,
767 pre_run=(),
768 post_run=(),
769 run=(),
770 implied_run=(),
Tobias Henkel9a0e1942017-03-20 16:16:02 +0100771 semaphore=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800772 attempts=3,
773 final=False,
James E. Blair5ac93842017-01-20 06:47:34 -0800774 roles=frozenset(),
K Jonathan Harker2c1a6232017-02-21 14:34:08 -0800775 repos=frozenset(),
James E. Blairb3f5db12017-03-17 12:57:39 -0700776 allowed_projects=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800777 )
778
779 # These are generally internal attributes which are not
780 # accessible via configuration.
781 self.other_attributes = dict(
782 name=None,
783 source_context=None,
784 inheritance_path=(),
785 )
786
787 self.inheritable_attributes = {}
788 self.inheritable_attributes.update(self.context_attributes)
789 self.inheritable_attributes.update(self.execution_attributes)
790 self.attributes = {}
791 self.attributes.update(self.inheritable_attributes)
792 self.attributes.update(self.other_attributes)
793
James E. Blairee743612012-05-29 14:49:32 -0700794 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800795
James E. Blair66b274e2017-01-31 14:47:52 -0800796 def __ne__(self, other):
797 return not self.__eq__(other)
798
Paul Belangere22baea2016-11-03 16:59:27 -0400799 def __eq__(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800800 # Compare the name and all inheritable attributes to determine
801 # whether two jobs with the same name are identically
802 # configured. Useful upon reconfiguration.
803 if not isinstance(other, Job):
804 return False
805 if self.name != other.name:
806 return False
807 for k, v in self.attributes.items():
808 if getattr(self, k) != getattr(other, k):
809 return False
810 return True
James E. Blairee743612012-05-29 14:49:32 -0700811
812 def __str__(self):
813 return self.name
814
815 def __repr__(self):
James E. Blair72ae9da2017-02-03 14:21:30 -0800816 return '<Job %s branches: %s source: %s>' % (self.name,
817 self.branch_matcher,
818 self.source_context)
James E. Blair83005782015-12-11 14:46:03 -0800819
James E. Blaira7f51ca2017-02-07 16:01:26 -0800820 def __getattr__(self, name):
821 v = self.__dict__.get(name)
822 if v is None:
823 return copy.deepcopy(self.attributes[name])
824 return v
825
826 def _get(self, name):
827 return self.__dict__.get(name)
828
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100829 def getSafeAttributes(self):
830 return Attributes(name=self.name)
831
James E. Blaira7f51ca2017-02-07 16:01:26 -0800832 def setRun(self):
833 if not self.run:
834 self.run = self.implied_run
835
James E. Blair490cf042017-02-24 23:07:21 -0500836 def updateVariables(self, other_vars):
837 v = self.variables
838 Job._deepUpdate(v, other_vars)
839 self.variables = v
840
841 @staticmethod
842 def _deepUpdate(a, b):
843 # Merge nested dictionaries if possible, otherwise, overwrite
844 # the value in 'a' with the value in 'b'.
845 for k, bv in b.items():
846 av = a.get(k)
847 if isinstance(av, dict) and isinstance(bv, dict):
848 Job._deepUpdate(av, bv)
849 else:
850 a[k] = bv
851
James E. Blaira7f51ca2017-02-07 16:01:26 -0800852 def inheritFrom(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800853 """Copy the inheritable attributes which have been set on the other
854 job to this job."""
James E. Blaira7f51ca2017-02-07 16:01:26 -0800855 if not isinstance(other, Job):
856 raise Exception("Job unable to inherit from %s" % (other,))
857
858 do_not_inherit = set()
James E. Blair8525e2b2017-03-15 14:05:47 -0700859 if other.auth and not other.auth.inherit:
James E. Blaira7f51ca2017-02-07 16:01:26 -0800860 do_not_inherit.add('auth')
861
862 # copy all attributes
863 for k in self.inheritable_attributes:
864 if (other._get(k) is not None and k not in do_not_inherit):
865 setattr(self, k, copy.deepcopy(getattr(other, k)))
866
867 msg = 'inherit from %s' % (repr(other),)
868 self.inheritance_path = other.inheritance_path + (msg,)
869
870 def copy(self):
871 job = Job(self.name)
872 for k in self.attributes:
873 if self._get(k) is not None:
874 setattr(job, k, copy.deepcopy(self._get(k)))
875 return job
876
877 def applyVariant(self, other):
878 """Copy the attributes which have been set on the other job to this
879 job."""
James E. Blair83005782015-12-11 14:46:03 -0800880
881 if not isinstance(other, Job):
882 raise Exception("Job unable to inherit from %s" % (other,))
James E. Blaira7f51ca2017-02-07 16:01:26 -0800883
884 for k in self.execution_attributes:
885 if (other._get(k) is not None and
886 k not in set(['final'])):
887 if self.final:
888 raise Exception("Unable to modify final job %s attribute "
889 "%s=%s with variant %s" % (
890 repr(self), k, other._get(k),
891 repr(other)))
James E. Blair490cf042017-02-24 23:07:21 -0500892 if k not in set(['pre_run', 'post_run', 'roles', 'variables']):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800893 setattr(self, k, copy.deepcopy(other._get(k)))
894
895 # Don't set final above so that we don't trip an error halfway
896 # through assignment.
897 if other.final != self.attributes['final']:
898 self.final = other.final
899
900 if other._get('pre_run') is not None:
901 self.pre_run = self.pre_run + other.pre_run
902 if other._get('post_run') is not None:
903 self.post_run = other.post_run + self.post_run
James E. Blair5ac93842017-01-20 06:47:34 -0800904 if other._get('roles') is not None:
905 self.roles = self.roles.union(other.roles)
James E. Blair490cf042017-02-24 23:07:21 -0500906 if other._get('variables') is not None:
907 self.updateVariables(other.variables)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800908
909 for k in self.context_attributes:
910 if (other._get(k) is not None and
911 k not in set(['tags'])):
912 setattr(self, k, copy.deepcopy(other._get(k)))
913
914 if other._get('tags') is not None:
915 self.tags = self.tags.union(other.tags)
916
917 msg = 'apply variant %s' % (repr(other),)
918 self.inheritance_path = self.inheritance_path + (msg,)
James E. Blairee743612012-05-29 14:49:32 -0700919
James E. Blaire421a232012-07-25 16:59:21 -0700920 def changeMatches(self, change):
James E. Blair83005782015-12-11 14:46:03 -0800921 if self.branch_matcher and not self.branch_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800922 return False
923
James E. Blair83005782015-12-11 14:46:03 -0800924 if self.file_matcher and not self.file_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800925 return False
926
James E. Blair83005782015-12-11 14:46:03 -0800927 # NB: This is a negative match.
928 if (self.irrelevant_file_matcher and
929 self.irrelevant_file_matcher.matches(change)):
Maru Newby3fe5f852015-01-13 04:22:14 +0000930 return False
931
James E. Blair70c71582013-03-06 08:50:50 -0800932 return True
James E. Blaire5a847f2012-07-10 15:29:14 -0700933
James E. Blair1e8dd892012-05-30 09:15:05 -0700934
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200935class JobList(object):
936 """ A list of jobs in a project's pipeline. """
Monty Taylora42a55b2016-07-29 07:53:33 -0700937
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200938 def __init__(self):
939 self.jobs = OrderedDict() # job.name -> [job, ...]
James E. Blair12748b52017-02-07 17:17:53 -0800940
James E. Blairee743612012-05-29 14:49:32 -0700941 def addJob(self, job):
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200942 if job.name in self.jobs:
943 self.jobs[job.name].append(job)
944 else:
945 self.jobs[job.name] = [job]
James E. Blairee743612012-05-29 14:49:32 -0700946
James E. Blaira7f51ca2017-02-07 16:01:26 -0800947 def inheritFrom(self, other):
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200948 for jobname, jobs in other.jobs.items():
949 if jobname in self.jobs:
950 self.jobs[jobname].append(jobs)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800951 else:
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200952 self.jobs[jobname] = jobs
953
954
955class JobGraph(object):
956 """ A JobGraph represents the dependency graph between Job."""
957
958 def __init__(self):
959 self.jobs = OrderedDict() # job_name -> Job
960 self._dependencies = {} # dependent_job_name -> set(parent_job_names)
961
962 def __repr__(self):
963 return '<JobGraph %s>' % (self.jobs)
964
965 def addJob(self, job):
966 # A graph must be created after the job list is frozen,
967 # therefore we should only get one job with the same name.
968 if job.name in self.jobs:
969 raise Exception("Job %s already added" % (job.name,))
970 self.jobs[job.name] = job
971 # Append the dependency information
972 self._dependencies.setdefault(job.name, set())
973 try:
974 for dependency in job.dependencies:
975 # Make sure a circular dependency is never created
976 ancestor_jobs = self._getParentJobNamesRecursively(
977 dependency, soft=True)
978 ancestor_jobs.add(dependency)
979 if any((job.name == anc_job) for anc_job in ancestor_jobs):
980 raise Exception("Dependency cycle detected in job %s" %
981 (job.name,))
982 self._dependencies[job.name].add(dependency)
983 except Exception:
984 del self.jobs[job.name]
985 del self._dependencies[job.name]
986 raise
987
988 def getJobs(self):
989 return self.jobs.values() # Report in the order of the layout config
990
991 def _getDirectDependentJobs(self, parent_job):
992 ret = set()
993 for dependent_name, parent_names in self._dependencies.items():
994 if parent_job in parent_names:
995 ret.add(dependent_name)
996 return ret
997
998 def getDependentJobsRecursively(self, parent_job):
999 all_dependent_jobs = set()
1000 jobs_to_iterate = set([parent_job])
1001 while len(jobs_to_iterate) > 0:
1002 current_job = jobs_to_iterate.pop()
1003 current_dependent_jobs = self._getDirectDependentJobs(current_job)
1004 new_dependent_jobs = current_dependent_jobs - all_dependent_jobs
1005 jobs_to_iterate |= new_dependent_jobs
1006 all_dependent_jobs |= new_dependent_jobs
1007 return [self.jobs[name] for name in all_dependent_jobs]
1008
1009 def getParentJobsRecursively(self, dependent_job):
1010 return [self.jobs[name] for name in
1011 self._getParentJobNamesRecursively(dependent_job)]
1012
1013 def _getParentJobNamesRecursively(self, dependent_job, soft=False):
1014 all_parent_jobs = set()
1015 jobs_to_iterate = set([dependent_job])
1016 while len(jobs_to_iterate) > 0:
1017 current_job = jobs_to_iterate.pop()
1018 current_parent_jobs = self._dependencies.get(current_job)
1019 if current_parent_jobs is None:
1020 if soft:
1021 current_parent_jobs = set()
1022 else:
1023 raise Exception("Dependent job %s not found: " %
1024 (dependent_job,))
1025 new_parent_jobs = current_parent_jobs - all_parent_jobs
1026 jobs_to_iterate |= new_parent_jobs
1027 all_parent_jobs |= new_parent_jobs
1028 return all_parent_jobs
James E. Blairb97ed802015-12-21 15:55:35 -08001029
James E. Blair1e8dd892012-05-30 09:15:05 -07001030
James E. Blair4aea70c2012-07-26 14:23:24 -07001031class Build(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001032 """A Build is an instance of a single execution of a Job.
1033
1034 While a Job describes what to run, a Build describes an actual
1035 execution of that Job. Each build is associated with exactly one
1036 Job (related builds are grouped together in a BuildSet).
1037 """
Monty Taylora42a55b2016-07-29 07:53:33 -07001038
James E. Blair4aea70c2012-07-26 14:23:24 -07001039 def __init__(self, job, uuid):
1040 self.job = job
1041 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -07001042 self.url = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001043 self.result = None
1044 self.build_set = None
Paul Belanger174a8272017-03-14 13:20:10 -04001045 self.execute_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -08001046 self.start_time = None
1047 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -07001048 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -07001049 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -07001050 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -07001051 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -07001052 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +08001053 self.worker = Worker()
Timothy Chavezb2332082015-08-07 20:08:04 -05001054 self.node_labels = []
1055 self.node_name = None
James E. Blairee743612012-05-29 14:49:32 -07001056
1057 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +08001058 return ('<Build %s of %s on %s>' %
1059 (self.uuid, self.job.name, self.worker))
1060
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001061 def getSafeAttributes(self):
1062 return Attributes(uuid=self.uuid)
1063
Joshua Heskethba8776a2014-01-12 14:35:40 +08001064
1065class Worker(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001066 """Information about the specific worker executing a Build."""
Joshua Heskethba8776a2014-01-12 14:35:40 +08001067 def __init__(self):
1068 self.name = "Unknown"
1069 self.hostname = None
1070 self.ips = []
1071 self.fqdn = None
1072 self.program = None
1073 self.version = None
1074 self.extra = {}
1075
1076 def updateFromData(self, data):
1077 """Update worker information if contained in the WORK_DATA response."""
1078 self.name = data.get('worker_name', self.name)
1079 self.hostname = data.get('worker_hostname', self.hostname)
1080 self.ips = data.get('worker_ips', self.ips)
1081 self.fqdn = data.get('worker_fqdn', self.fqdn)
1082 self.program = data.get('worker_program', self.program)
1083 self.version = data.get('worker_version', self.version)
1084 self.extra = data.get('worker_extra', self.extra)
1085
1086 def __repr__(self):
1087 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -07001088
James E. Blair1e8dd892012-05-30 09:15:05 -07001089
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001090class RepoFiles(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001091 """RepoFiles holds config-file content for per-project job config.
1092
1093 When Zuul asks a merger to prepare a future multiple-repo state
1094 and collect Zuul configuration files so that we can dynamically
1095 load our configuration, this class provides cached access to that
1096 data for use by the Change which updated the config files and any
1097 changes that follow it in a ChangeQueue.
1098
1099 It is attached to a BuildSet since the content of Zuul
1100 configuration files can change with each new BuildSet.
1101 """
1102
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001103 def __init__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001104 self.connections = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001105
1106 def __repr__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001107 return '<RepoFiles %s>' % self.connections
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001108
1109 def setFiles(self, items):
James E. Blair2a535672017-04-27 12:03:15 -07001110 self.hostnames = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001111 for item in items:
James E. Blair2a535672017-04-27 12:03:15 -07001112 connection = self.connections.setdefault(
1113 item['connection'], {})
1114 project = connection.setdefault(item['project'], {})
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001115 branch = project.setdefault(item['branch'], {})
1116 branch.update(item['files'])
1117
James E. Blair2a535672017-04-27 12:03:15 -07001118 def getFile(self, connection_name, project_name, branch, fn):
1119 host = self.connections.get(connection_name, {})
1120 return host.get(project_name, {}).get(branch, {}).get(fn)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001121
1122
James E. Blair7e530ad2012-07-03 16:12:28 -07001123class BuildSet(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001124 """A collection of Builds for one specific potential future repository
1125 state.
Monty Taylora42a55b2016-07-29 07:53:33 -07001126
Paul Belanger174a8272017-03-14 13:20:10 -04001127 When Zuul executes Builds for a change, it creates a Build to
James E. Blaird4ade8c2017-02-19 15:25:46 -08001128 represent each execution of each job and a BuildSet to keep track
Paul Belanger174a8272017-03-14 13:20:10 -04001129 of all the Builds running for that Change. When Zuul re-executes
James E. Blaird4ade8c2017-02-19 15:25:46 -08001130 Builds for a Change with a different configuration, all of the
1131 running Builds in the BuildSet for that change are aborted, and a
1132 new BuildSet is created to hold the Builds for the Jobs being
1133 run with the new configuration.
1134
1135 A BuildSet also holds the UUID used to produce the Zuul Ref that
1136 builders check out.
1137
Monty Taylora42a55b2016-07-29 07:53:33 -07001138 """
James E. Blair4076e2b2014-01-28 12:42:20 -08001139 # Merge states:
1140 NEW = 1
1141 PENDING = 2
1142 COMPLETE = 3
1143
Antoine Musso9b229282014-08-18 23:45:43 +02001144 states_map = {
1145 1: 'NEW',
1146 2: 'PENDING',
1147 3: 'COMPLETE',
1148 }
1149
James E. Blairfee8d652013-06-07 08:57:52 -07001150 def __init__(self, item):
1151 self.item = item
James E. Blair11700c32012-07-05 17:50:05 -07001152 self.other_changes = []
James E. Blair7e530ad2012-07-03 16:12:28 -07001153 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -07001154 self.result = None
1155 self.next_build_set = None
1156 self.previous_build_set = None
James E. Blair4886cc12012-07-18 15:39:41 -07001157 self.ref = None
James E. Blair81515ad2012-10-01 18:29:08 -07001158 self.commit = None
James E. Blair4076e2b2014-01-28 12:42:20 -08001159 self.zuul_url = None
James E. Blair973721f2012-08-15 10:19:43 -07001160 self.unable_to_merge = False
James E. Blaire53250c2017-03-01 14:34:36 -08001161 self.config_error = None # None or an error message string.
James E. Blair972e3c72013-08-29 12:04:55 -07001162 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -08001163 self.merge_state = self.NEW
James E. Blair0eaad552016-09-02 12:09:54 -07001164 self.nodesets = {} # job -> nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001165 self.node_requests = {} # job -> reqs
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001166 self.files = RepoFiles()
1167 self.layout = None
Paul Belanger71d98172016-11-08 10:56:31 -05001168 self.tries = {}
James E. Blair7e530ad2012-07-03 16:12:28 -07001169
Antoine Musso9b229282014-08-18 23:45:43 +02001170 def __repr__(self):
1171 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
1172 self.item,
1173 len(self.builds),
1174 self.getStateName(self.merge_state))
1175
James E. Blair4886cc12012-07-18 15:39:41 -07001176 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -07001177 # The change isn't enqueued until after it's created
1178 # so we don't know what the other changes ahead will be
1179 # until jobs start.
1180 if not self.other_changes:
James E. Blairfee8d652013-06-07 08:57:52 -07001181 next_item = self.item.item_ahead
1182 while next_item:
1183 self.other_changes.append(next_item.change)
1184 next_item = next_item.item_ahead
James E. Blair4886cc12012-07-18 15:39:41 -07001185 if not self.ref:
1186 self.ref = 'Z' + uuid4().hex
1187
Antoine Musso9b229282014-08-18 23:45:43 +02001188 def getStateName(self, state_num):
1189 return self.states_map.get(
1190 state_num, 'UNKNOWN (%s)' % state_num)
1191
James E. Blair4886cc12012-07-18 15:39:41 -07001192 def addBuild(self, build):
1193 self.builds[build.job.name] = build
Paul Belanger71d98172016-11-08 10:56:31 -05001194 if build.job.name not in self.tries:
James E. Blair5aed1112016-12-15 09:18:05 -08001195 self.tries[build.job.name] = 1
James E. Blair4886cc12012-07-18 15:39:41 -07001196 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -07001197
James E. Blair4a28a882013-08-23 15:17:33 -07001198 def removeBuild(self, build):
Paul Belanger71d98172016-11-08 10:56:31 -05001199 self.tries[build.job.name] += 1
James E. Blair4a28a882013-08-23 15:17:33 -07001200 del self.builds[build.job.name]
1201
James E. Blair7e530ad2012-07-03 16:12:28 -07001202 def getBuild(self, job_name):
1203 return self.builds.get(job_name)
1204
James E. Blair11700c32012-07-05 17:50:05 -07001205 def getBuilds(self):
1206 keys = self.builds.keys()
1207 keys.sort()
1208 return [self.builds.get(x) for x in keys]
1209
James E. Blair0eaad552016-09-02 12:09:54 -07001210 def getJobNodeSet(self, job_name):
1211 # Return None if not provisioned; empty NodeSet if no nodes
1212 # required
1213 return self.nodesets.get(job_name)
James E. Blair8d692392016-04-08 17:47:58 -07001214
James E. Blaire18d4602017-01-05 11:17:28 -08001215 def removeJobNodeSet(self, job_name):
1216 if job_name not in self.nodesets:
1217 raise Exception("No job set for %s" % (job_name))
1218 del self.nodesets[job_name]
1219
James E. Blair8d692392016-04-08 17:47:58 -07001220 def setJobNodeRequest(self, job_name, req):
1221 if job_name in self.node_requests:
1222 raise Exception("Prior node request for %s" % (job_name))
1223 self.node_requests[job_name] = req
1224
1225 def getJobNodeRequest(self, job_name):
1226 return self.node_requests.get(job_name)
1227
James E. Blair0eaad552016-09-02 12:09:54 -07001228 def jobNodeRequestComplete(self, job_name, req, nodeset):
1229 if job_name in self.nodesets:
James E. Blair8d692392016-04-08 17:47:58 -07001230 raise Exception("Prior node request for %s" % (job_name))
James E. Blair0eaad552016-09-02 12:09:54 -07001231 self.nodesets[job_name] = nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001232 del self.node_requests[job_name]
1233
Paul Belanger71d98172016-11-08 10:56:31 -05001234 def getTries(self, job_name):
1235 return self.tries.get(job_name)
1236
James E. Blair0ffa0102017-03-30 13:11:33 -07001237 def getMergeMode(self):
1238 if self.layout:
1239 project = self.item.change.project
1240 project_config = self.layout.project_configs.get(
1241 project.canonical_name)
1242 if project_config:
1243 return project_config.merge_mode
1244 return MERGER_MERGE_RESOLVE
Adam Gandelman8bd57102016-12-02 12:58:42 -08001245
James E. Blair7e530ad2012-07-03 16:12:28 -07001246
James E. Blairfee8d652013-06-07 08:57:52 -07001247class QueueItem(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001248 """Represents the position of a Change in a ChangeQueue.
1249
1250 All Changes are enqueued into ChangeQueue in a QueueItem. The QueueItem
1251 holds the current `BuildSet` as well as all previous `BuildSets` that were
1252 produced for this `QueueItem`.
1253 """
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001254 log = logging.getLogger("zuul.QueueItem")
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
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001512 def formatUrlPattern(self, url_pattern, job=None, build=None):
1513 url = None
1514 # Produce safe versions of objects which may be useful in
1515 # result formatting, but don't allow users to crawl through
1516 # the entire data structure where they might be able to access
1517 # secrets, etc.
1518 safe_change = self.change.getSafeAttributes()
1519 safe_pipeline = self.pipeline.getSafeAttributes()
1520 safe_job = job.getSafeAttributes()
1521 safe_build = build.getSafeAttributes()
1522 try:
1523 url = url_pattern.format(change=safe_change,
1524 pipeline=safe_pipeline,
1525 job=safe_job,
1526 build=safe_build)
1527 except KeyError as e:
1528 self.log.error("Error while formatting url for job %s: unknown "
1529 "key %s in pattern %s"
1530 % (job, e.message, url_pattern))
1531 except AttributeError as e:
1532 self.log.error("Error while formatting url for job %s: unknown "
1533 "attribute %s in pattern %s"
1534 % (job, e.message, url_pattern))
1535 except Exception:
1536 self.log.exception("Error while formatting url for job %s with "
1537 "pattern %s:" % (job, url_pattern))
1538
1539 return url
1540
James E. Blair800e7ff2017-03-17 16:06:52 -07001541 def formatJobResult(self, job):
James E. Blairb7273ef2016-04-19 08:58:51 -07001542 build = self.current_build_set.getBuild(job.name)
1543 result = build.result
James E. Blair800e7ff2017-03-17 16:06:52 -07001544 pattern = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001545 if result == 'SUCCESS':
1546 if job.success_message:
1547 result = job.success_message
James E. Blaira5dba232016-08-08 15:53:24 -07001548 if job.success_url:
1549 pattern = job.success_url
James E. Blairb7273ef2016-04-19 08:58:51 -07001550 elif result == 'FAILURE':
1551 if job.failure_message:
1552 result = job.failure_message
James E. Blaira5dba232016-08-08 15:53:24 -07001553 if job.failure_url:
1554 pattern = job.failure_url
James E. Blairb7273ef2016-04-19 08:58:51 -07001555 url = None
1556 if pattern:
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001557 url = self.formatUrlPattern(pattern, job, build)
James E. Blairb7273ef2016-04-19 08:58:51 -07001558 if not url:
1559 url = build.url or job.name
1560 return (result, url)
1561
James E. Blair800e7ff2017-03-17 16:06:52 -07001562 def formatJSON(self):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001563 changeish = self.change
1564 ret = {}
1565 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -08001566 ret['live'] = self.live
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001567 if hasattr(changeish, 'url') and changeish.url is not None:
1568 ret['url'] = changeish.url
1569 else:
1570 ret['url'] = None
1571 ret['id'] = changeish._id()
1572 if self.item_ahead:
1573 ret['item_ahead'] = self.item_ahead.change._id()
1574 else:
1575 ret['item_ahead'] = None
1576 ret['items_behind'] = [i.change._id() for i in self.items_behind]
1577 ret['failing_reasons'] = self.current_build_set.failing_reasons
1578 ret['zuul_ref'] = self.current_build_set.ref
Ramy Asselin07cc33c2015-06-12 14:06:34 -07001579 if changeish.project:
1580 ret['project'] = changeish.project.name
1581 else:
1582 # For cross-project dependencies with the depends-on
1583 # project not known to zuul, the project is None
1584 # Set it to a static value
1585 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001586 ret['enqueue_time'] = int(self.enqueue_time * 1000)
1587 ret['jobs'] = []
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001588 if hasattr(changeish, 'owner'):
1589 ret['owner'] = changeish.owner
1590 else:
1591 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001592 max_remaining = 0
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001593 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001594 now = time.time()
1595 build = self.current_build_set.getBuild(job.name)
1596 elapsed = None
1597 remaining = None
1598 result = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001599 build_url = None
1600 report_url = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001601 worker = None
1602 if build:
1603 result = build.result
James E. Blairb7273ef2016-04-19 08:58:51 -07001604 build_url = build.url
James E. Blair800e7ff2017-03-17 16:06:52 -07001605 (unused, report_url) = self.formatJobResult(job)
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001606 if build.start_time:
1607 if build.end_time:
1608 elapsed = int((build.end_time -
1609 build.start_time) * 1000)
1610 remaining = 0
1611 else:
1612 elapsed = int((now - build.start_time) * 1000)
1613 if build.estimated_time:
1614 remaining = max(
1615 int(build.estimated_time * 1000) - elapsed,
1616 0)
1617 worker = {
1618 'name': build.worker.name,
1619 'hostname': build.worker.hostname,
1620 'ips': build.worker.ips,
1621 'fqdn': build.worker.fqdn,
1622 'program': build.worker.program,
1623 'version': build.worker.version,
1624 'extra': build.worker.extra
1625 }
1626 if remaining and remaining > max_remaining:
1627 max_remaining = remaining
1628
1629 ret['jobs'].append({
1630 'name': job.name,
1631 'elapsed_time': elapsed,
1632 'remaining_time': remaining,
James E. Blairb7273ef2016-04-19 08:58:51 -07001633 'url': build_url,
1634 'report_url': report_url,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001635 'result': result,
1636 'voting': job.voting,
1637 'uuid': build.uuid if build else None,
Paul Belanger174a8272017-03-14 13:20:10 -04001638 'execute_time': build.execute_time if build else None,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001639 'start_time': build.start_time if build else None,
1640 'end_time': build.end_time if build else None,
1641 'estimated_time': build.estimated_time if build else None,
1642 'pipeline': build.pipeline.name if build else None,
1643 'canceled': build.canceled if build else None,
1644 'retry': build.retry if build else None,
Timothy Chavezb2332082015-08-07 20:08:04 -05001645 'node_labels': build.node_labels if build else [],
1646 'node_name': build.node_name if build else None,
1647 'worker': worker,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001648 })
1649
James E. Blairdbfd3282016-07-21 10:46:19 -07001650 if self.haveAllJobsStarted():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001651 ret['remaining_time'] = max_remaining
1652 else:
1653 ret['remaining_time'] = None
1654 return ret
1655
1656 def formatStatus(self, indent=0, html=False):
1657 changeish = self.change
1658 indent_str = ' ' * indent
1659 ret = ''
1660 if html and hasattr(changeish, 'url') and changeish.url is not None:
1661 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
1662 indent_str,
1663 changeish.project.name,
1664 changeish.url,
1665 changeish._id())
1666 else:
1667 ret += '%sProject %s change %s based on %s\n' % (
1668 indent_str,
1669 changeish.project.name,
1670 changeish._id(),
1671 self.item_ahead)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001672 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001673 build = self.current_build_set.getBuild(job.name)
1674 if build:
1675 result = build.result
1676 else:
1677 result = None
1678 job_name = job.name
1679 if not job.voting:
1680 voting = ' (non-voting)'
1681 else:
1682 voting = ''
1683 if html:
1684 if build:
1685 url = build.url
1686 else:
1687 url = None
1688 if url is not None:
1689 job_name = '<a href="%s">%s</a>' % (url, job_name)
1690 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
1691 ret += '\n'
1692 return ret
1693
James E. Blaira04b0792017-04-27 09:59:06 -07001694 def makeMergerItem(self):
1695 # Create a dictionary with all info about the item needed by
1696 # the merger.
1697 number = None
1698 patchset = None
1699 oldrev = None
1700 newrev = None
1701 refspec = None
1702 if hasattr(self.change, 'number'):
1703 number = self.change.number
1704 patchset = self.change.patchset
1705 refspec = self.change.refspec
1706 branch = self.change.branch
1707 elif hasattr(self.change, 'newrev'):
1708 oldrev = self.change.oldrev
1709 newrev = self.change.newrev
1710 branch = self.change.ref
1711 else:
1712 oldrev = None
1713 newrev = None
1714 branch = None
1715 source = self.change.project.source
1716 connection_name = source.connection.connection_name
James E. Blair2a535672017-04-27 12:03:15 -07001717 project = self.change.project
James E. Blaira04b0792017-04-27 09:59:06 -07001718
James E. Blair2a535672017-04-27 12:03:15 -07001719 return dict(project=project.name,
1720 connection=connection_name,
James E. Blaira04b0792017-04-27 09:59:06 -07001721 merge_mode=self.current_build_set.getMergeMode(),
1722 refspec=refspec,
1723 branch=branch,
1724 ref=self.current_build_set.ref,
1725 number=number,
1726 patchset=patchset,
1727 oldrev=oldrev,
1728 newrev=newrev,
1729 )
1730
James E. Blairfee8d652013-06-07 08:57:52 -07001731
Clint Byrumf8cc9902017-03-22 22:38:25 -07001732class Ref(object):
1733 """An existing state of a Project."""
James E. Blairfee8d652013-06-07 08:57:52 -07001734
1735 def __init__(self, project):
1736 self.project = project
Clint Byrumf8cc9902017-03-22 22:38:25 -07001737 self.ref = None
1738 self.oldrev = None
1739 self.newrev = None
James E. Blairfee8d652013-06-07 08:57:52 -07001740
Joshua Hesketh36c3fa52014-01-22 11:40:52 +11001741 def getBasePath(self):
1742 base_path = ''
Clint Byrumf8cc9902017-03-22 22:38:25 -07001743 if hasattr(self, 'ref'):
Joshua Hesketh36c3fa52014-01-22 11:40:52 +11001744 base_path = "%s/%s" % (self.newrev[:2], self.newrev)
1745
1746 return base_path
1747
Clint Byrumf8cc9902017-03-22 22:38:25 -07001748 def _id(self):
1749 return self.newrev
1750
1751 def __repr__(self):
1752 rep = None
1753 if self.newrev == '0000000000000000000000000000000000000000':
1754 rep = '<Ref 0x%x deletes %s from %s' % (
1755 id(self), self.ref, self.oldrev)
1756 elif self.oldrev == '0000000000000000000000000000000000000000':
1757 rep = '<Ref 0x%x creates %s on %s>' % (
1758 id(self), self.ref, self.newrev)
1759 else:
1760 # Catch all
1761 rep = '<Ref 0x%x %s updated %s..%s>' % (
1762 id(self), self.ref, self.oldrev, self.newrev)
1763
1764 return rep
1765
James E. Blairfee8d652013-06-07 08:57:52 -07001766 def equals(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001767 if (self.project == other.project
1768 and self.ref == other.ref
1769 and self.newrev == other.newrev):
1770 return True
1771 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001772
1773 def isUpdateOf(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001774 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001775
1776 def filterJobs(self, jobs):
1777 return filter(lambda job: job.changeMatches(self), jobs)
1778
1779 def getRelatedChanges(self):
1780 return set()
1781
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001782 def updatesConfig(self):
1783 return False
1784
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001785 def getSafeAttributes(self):
1786 return Attributes(project=self.project,
1787 ref=self.ref,
1788 oldrev=self.oldrev,
1789 newrev=self.newrev)
1790
James E. Blair1e8dd892012-05-30 09:15:05 -07001791
Clint Byrumf8cc9902017-03-22 22:38:25 -07001792class Change(Ref):
Monty Taylora42a55b2016-07-29 07:53:33 -07001793 """A proposed new state for a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001794 def __init__(self, project):
1795 super(Change, self).__init__(project)
1796 self.branch = None
1797 self.number = None
1798 self.url = None
1799 self.patchset = None
1800 self.refspec = None
1801
James E. Blair70c71582013-03-06 08:50:50 -08001802 self.files = []
James E. Blair6965a4b2014-12-16 17:19:04 -08001803 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -07001804 self.needed_by_changes = []
1805 self.is_current_patchset = True
1806 self.can_merge = False
1807 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -07001808 self.failed_to_merge = False
James E. Blairc053d022014-01-22 14:57:33 -08001809 self.approvals = []
James E. Blair11041d22014-05-02 14:49:53 -07001810 self.open = None
1811 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001812 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001813
Jan Hruban3b415922016-02-03 13:10:22 +01001814 self.source_event = None
1815
James E. Blair4aea70c2012-07-26 14:23:24 -07001816 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -07001817 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -07001818
1819 def __repr__(self):
1820 return '<Change 0x%x %s>' % (id(self), self._id())
1821
Clint Byrumf8cc9902017-03-22 22:38:25 -07001822 def getBasePath(self):
1823 if hasattr(self, 'refspec'):
1824 return "%s/%s/%s" % (
Gregory Haynes4fc12542015-04-22 20:38:06 -07001825 str(self.number)[-2:], self.number, self.patchset)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001826 return super(Change, self).getBasePath()
1827
James E. Blair4aea70c2012-07-26 14:23:24 -07001828 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +08001829 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -07001830 return True
1831 return False
1832
James E. Blair2fa50962013-01-30 21:50:41 -08001833 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -08001834 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -07001835 (hasattr(other, 'patchset') and
1836 self.patchset is not None and
1837 other.patchset is not None and
1838 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -08001839 return True
1840 return False
1841
James E. Blairfee8d652013-06-07 08:57:52 -07001842 def getRelatedChanges(self):
1843 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -08001844 for c in self.needs_changes:
1845 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -07001846 for c in self.needed_by_changes:
1847 related.add(c)
1848 related.update(c.getRelatedChanges())
1849 return related
James E. Blair4aea70c2012-07-26 14:23:24 -07001850
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001851 def updatesConfig(self):
1852 if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files:
1853 return True
1854 return False
1855
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001856 def getSafeAttributes(self):
1857 return Attributes(project=self.project,
1858 number=self.number,
1859 patchset=self.patchset)
1860
James E. Blair4aea70c2012-07-26 14:23:24 -07001861
Gregory Haynes4fc12542015-04-22 20:38:06 -07001862class PullRequest(Change):
1863 def __init__(self, project):
1864 super(PullRequest, self).__init__(project)
1865 self.updated_at = None
Jan Hruban3b415922016-02-03 13:10:22 +01001866 self.title = None
Gregory Haynes4fc12542015-04-22 20:38:06 -07001867
1868 def isUpdateOf(self, other):
1869 if (hasattr(other, 'number') and self.number == other.number and
1870 hasattr(other, 'patchset') and self.patchset != other.patchset and
1871 hasattr(other, 'updated_at') and
1872 self.updated_at > other.updated_at):
1873 return True
1874 return False
1875
1876
James E. Blairee743612012-05-29 14:49:32 -07001877class TriggerEvent(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001878 """Incoming event from an external system."""
James E. Blairee743612012-05-29 14:49:32 -07001879 def __init__(self):
1880 self.data = None
James E. Blair32663402012-06-01 10:04:18 -07001881 # common
James E. Blairee743612012-05-29 14:49:32 -07001882 self.type = None
Paul Belangerbaca3132016-11-04 12:49:54 -04001883 # For management events (eg: enqueue / promote)
1884 self.tenant_name = None
James E. Blair6f284b42017-03-31 14:14:41 -07001885 self.project_hostname = None
James E. Blairee743612012-05-29 14:49:32 -07001886 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -07001887 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +01001888 # Representation of the user account that performed the event.
1889 self.account = None
James E. Blair32663402012-06-01 10:04:18 -07001890 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -07001891 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -07001892 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -07001893 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -07001894 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -07001895 self.approvals = []
1896 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07001897 self.comment = None
Jan Hruban16ad31f2015-11-07 14:39:07 +01001898 self.label = None
1899 self.unlabel = None
Jesse Keating5c05a9f2017-01-12 14:44:58 -08001900 self.state = None
James E. Blair32663402012-06-01 10:04:18 -07001901 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -07001902 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -07001903 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07001904 self.newrev = None
James E. Blair63bb0ef2013-07-29 17:14:51 -07001905 # timer
1906 self.timespec = None
James E. Blairc494d542014-08-06 09:23:52 -07001907 # zuultrigger
1908 self.pipeline_name = None
James E. Blairad28e912013-11-27 10:43:22 -08001909 # For events that arrive with a destination pipeline (eg, from
1910 # an admin command, etc):
1911 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07001912
James E. Blair6f284b42017-03-31 14:14:41 -07001913 @property
1914 def canonical_project_name(self):
1915 return self.project_hostname + '/' + self.project_name
1916
James E. Blair9f9667e2012-06-12 17:51:08 -07001917 def __repr__(self):
James E. Blair0ffa0102017-03-30 13:11:33 -07001918 ret = '<TriggerEvent %s %s' % (self.type, self.canonical_project_name)
James E. Blair1e8dd892012-05-30 09:15:05 -07001919
James E. Blairee743612012-05-29 14:49:32 -07001920 if self.branch:
1921 ret += " %s" % self.branch
1922 if self.change_number:
1923 ret += " %s,%s" % (self.change_number, self.patch_number)
1924 if self.approvals:
James E. Blair1e8dd892012-05-30 09:15:05 -07001925 ret += ' ' + ', '.join(
1926 ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
James E. Blairee743612012-05-29 14:49:32 -07001927 ret += '>'
1928
1929 return ret
1930
Jan Hruban324ca5b2015-11-05 19:28:54 +01001931 def isPatchsetCreated(self):
1932 return 'patchset-created' == self.type
1933
1934 def isChangeAbandoned(self):
1935 return 'change-abandoned' == self.type
1936
1937
1938class GithubTriggerEvent(TriggerEvent):
1939
Jan Hruban3b415922016-02-03 13:10:22 +01001940 def __init__(self):
1941 super(GithubTriggerEvent, self).__init__()
1942 self.title = None
1943
Jan Hruban324ca5b2015-11-05 19:28:54 +01001944 def isPatchsetCreated(self):
1945 if self.type == 'pull_request':
1946 return self.action in ['opened', 'changed']
1947 return False
1948
1949 def isChangeAbandoned(self):
1950 if self.type == 'pull_request':
1951 return 'closed' == self.action
1952 return False
1953
James E. Blair1e8dd892012-05-30 09:15:05 -07001954
James E. Blair9c17dbf2014-06-23 14:21:58 -07001955class BaseFilter(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001956 """Base Class for filtering which Changes and Events to process."""
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001957 def __init__(self, required_approvals=[], reject_approvals=[]):
James E. Blair5bf78a32015-07-30 18:08:24 +00001958 self._required_approvals = copy.deepcopy(required_approvals)
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001959 self.required_approvals = self._tidy_approvals(required_approvals)
1960 self._reject_approvals = copy.deepcopy(reject_approvals)
1961 self.reject_approvals = self._tidy_approvals(reject_approvals)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001962
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001963 def _tidy_approvals(self, approvals):
1964 for a in approvals:
James E. Blair9c17dbf2014-06-23 14:21:58 -07001965 for k, v in a.items():
1966 if k == 'username':
James E. Blairb01ec542016-06-16 09:46:49 -07001967 a['username'] = re.compile(v)
James E. Blair1fbfceb2014-06-23 14:42:53 -07001968 elif k in ['email', 'email-filter']:
James E. Blair5bf78a32015-07-30 18:08:24 +00001969 a['email'] = re.compile(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001970 elif k == 'newer-than':
James E. Blair5bf78a32015-07-30 18:08:24 +00001971 a[k] = time_to_seconds(v)
James E. Blair9c17dbf2014-06-23 14:21:58 -07001972 elif k == 'older-than':
James E. Blair5bf78a32015-07-30 18:08:24 +00001973 a[k] = time_to_seconds(v)
James E. Blair1fbfceb2014-06-23 14:42:53 -07001974 if 'email-filter' in a:
1975 del a['email-filter']
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001976 return approvals
1977
1978 def _match_approval_required_approval(self, rapproval, approval):
1979 # Check if the required approval and approval match
1980 if 'description' not in approval:
1981 return False
1982 now = time.time()
1983 by = approval.get('by', {})
1984 for k, v in rapproval.items():
1985 if k == 'username':
James E. Blairb01ec542016-06-16 09:46:49 -07001986 if (not v.search(by.get('username', ''))):
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001987 return False
1988 elif k == 'email':
1989 if (not v.search(by.get('email', ''))):
1990 return False
1991 elif k == 'newer-than':
1992 t = now - v
1993 if (approval['grantedOn'] < t):
1994 return False
1995 elif k == 'older-than':
1996 t = now - v
1997 if (approval['grantedOn'] >= t):
1998 return False
1999 else:
2000 if not isinstance(v, list):
2001 v = [v]
2002 if (normalizeCategory(approval['description']) != k or
2003 int(approval['value']) not in v):
2004 return False
2005 return True
2006
2007 def matchesApprovals(self, change):
2008 if (self.required_approvals and not change.approvals
2009 or self.reject_approvals and not change.approvals):
2010 # A change with no approvals can not match
2011 return False
2012
2013 # TODO(jhesketh): If we wanted to optimise this slightly we could
2014 # analyse both the REQUIRE and REJECT filters by looping over the
2015 # approvals on the change and keeping track of what we have checked
2016 # rather than needing to loop on the change approvals twice
2017 return (self.matchesRequiredApprovals(change) and
2018 self.matchesNoRejectApprovals(change))
James E. Blair9c17dbf2014-06-23 14:21:58 -07002019
2020 def matchesRequiredApprovals(self, change):
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002021 # Check if any approvals match the requirements
James E. Blair5bf78a32015-07-30 18:08:24 +00002022 for rapproval in self.required_approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002023 matches_rapproval = False
James E. Blair9c17dbf2014-06-23 14:21:58 -07002024 for approval in change.approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002025 if self._match_approval_required_approval(rapproval, approval):
2026 # We have a matching approval so this requirement is
2027 # fulfilled
2028 matches_rapproval = True
James E. Blair5bf78a32015-07-30 18:08:24 +00002029 break
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002030 if not matches_rapproval:
James E. Blair5bf78a32015-07-30 18:08:24 +00002031 return False
James E. Blair9c17dbf2014-06-23 14:21:58 -07002032 return True
2033
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002034 def matchesNoRejectApprovals(self, change):
2035 # Check to make sure no approvals match a reject criteria
2036 for rapproval in self.reject_approvals:
2037 for approval in change.approvals:
2038 if self._match_approval_required_approval(rapproval, approval):
2039 # A reject approval has been matched, so we reject
2040 # immediately
2041 return False
2042 # To get here no rejects can have been matched so we should be good to
2043 # queue
2044 return True
2045
James E. Blair9c17dbf2014-06-23 14:21:58 -07002046
2047class EventFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002048 """Allows a Pipeline to only respond to certain events."""
James E. Blairc0dedf82014-08-06 09:37:52 -07002049 def __init__(self, trigger, types=[], branches=[], refs=[],
2050 event_approvals={}, comments=[], emails=[], usernames=[],
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002051 timespecs=[], required_approvals=[], reject_approvals=[],
Jesse Keating5c05a9f2017-01-12 14:44:58 -08002052 pipelines=[], actions=[], labels=[], unlabels=[], states=[],
Jan Hruban16ad31f2015-11-07 14:39:07 +01002053 ignore_deletes=True):
James E. Blair9c17dbf2014-06-23 14:21:58 -07002054 super(EventFilter, self).__init__(
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002055 required_approvals=required_approvals,
2056 reject_approvals=reject_approvals)
James E. Blairc0dedf82014-08-06 09:37:52 -07002057 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07002058 self._types = types
2059 self._branches = branches
2060 self._refs = refs
James E. Blair1fbfceb2014-06-23 14:42:53 -07002061 self._comments = comments
2062 self._emails = emails
2063 self._usernames = usernames
James E. Blairc494d542014-08-06 09:23:52 -07002064 self._pipelines = pipelines
James E. Blairee743612012-05-29 14:49:32 -07002065 self.types = [re.compile(x) for x in types]
2066 self.branches = [re.compile(x) for x in branches]
2067 self.refs = [re.compile(x) for x in refs]
James E. Blair1fbfceb2014-06-23 14:42:53 -07002068 self.comments = [re.compile(x) for x in comments]
2069 self.emails = [re.compile(x) for x in emails]
2070 self.usernames = [re.compile(x) for x in usernames]
James E. Blairc494d542014-08-06 09:23:52 -07002071 self.pipelines = [re.compile(x) for x in pipelines]
Gregory Haynes4fc12542015-04-22 20:38:06 -07002072 self.actions = actions
James E. Blairc053d022014-01-22 14:57:33 -08002073 self.event_approvals = event_approvals
James E. Blair63bb0ef2013-07-29 17:14:51 -07002074 self.timespecs = timespecs
Jan Hruban16ad31f2015-11-07 14:39:07 +01002075 self.labels = labels
2076 self.unlabels = unlabels
Jesse Keating5c05a9f2017-01-12 14:44:58 -08002077 self.states = states
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07002078 self.ignore_deletes = ignore_deletes
James E. Blairee743612012-05-29 14:49:32 -07002079
James E. Blair9f9667e2012-06-12 17:51:08 -07002080 def __repr__(self):
James E. Blairee743612012-05-29 14:49:32 -07002081 ret = '<EventFilter'
James E. Blair1e8dd892012-05-30 09:15:05 -07002082
James E. Blairee743612012-05-29 14:49:32 -07002083 if self._types:
2084 ret += ' types: %s' % ', '.join(self._types)
James E. Blairc494d542014-08-06 09:23:52 -07002085 if self._pipelines:
2086 ret += ' pipelines: %s' % ', '.join(self._pipelines)
James E. Blairee743612012-05-29 14:49:32 -07002087 if self._branches:
2088 ret += ' branches: %s' % ', '.join(self._branches)
2089 if self._refs:
2090 ret += ' refs: %s' % ', '.join(self._refs)
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07002091 if self.ignore_deletes:
2092 ret += ' ignore_deletes: %s' % self.ignore_deletes
James E. Blairc053d022014-01-22 14:57:33 -08002093 if self.event_approvals:
2094 ret += ' event_approvals: %s' % ', '.join(
2095 ['%s:%s' % a for a in self.event_approvals.items()])
James E. Blair5bf78a32015-07-30 18:08:24 +00002096 if self.required_approvals:
2097 ret += ' required_approvals: %s' % ', '.join(
2098 ['%s' % a for a in self._required_approvals])
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002099 if self.reject_approvals:
2100 ret += ' reject_approvals: %s' % ', '.join(
2101 ['%s' % a for a in self._reject_approvals])
James E. Blair1fbfceb2014-06-23 14:42:53 -07002102 if self._comments:
2103 ret += ' comments: %s' % ', '.join(self._comments)
2104 if self._emails:
2105 ret += ' emails: %s' % ', '.join(self._emails)
2106 if self._usernames:
2107 ret += ' username_filters: %s' % ', '.join(self._usernames)
James E. Blair63bb0ef2013-07-29 17:14:51 -07002108 if self.timespecs:
2109 ret += ' timespecs: %s' % ', '.join(self.timespecs)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002110 if self.actions:
2111 ret += ' actions: %s' % ', '.join(self.actions)
Jan Hruban16ad31f2015-11-07 14:39:07 +01002112 if self.labels:
2113 ret += ' labels: %s' % ', '.join(self.labels)
2114 if self.unlabels:
2115 ret += ' unlabels: %s' % ', '.join(self.unlabels)
Jesse Keating5c05a9f2017-01-12 14:44:58 -08002116 if self.states:
2117 ret += ' states: %s' % ', '.join(self.states)
James E. Blairee743612012-05-29 14:49:32 -07002118 ret += '>'
2119
2120 return ret
2121
James E. Blairc053d022014-01-22 14:57:33 -08002122 def matches(self, event, change):
James E. Blairee743612012-05-29 14:49:32 -07002123 # event types are ORed
2124 matches_type = False
2125 for etype in self.types:
2126 if etype.match(event.type):
2127 matches_type = True
2128 if self.types and not matches_type:
2129 return False
2130
James E. Blairc494d542014-08-06 09:23:52 -07002131 # pipelines are ORed
2132 matches_pipeline = False
2133 for epipe in self.pipelines:
2134 if epipe.match(event.pipeline_name):
2135 matches_pipeline = True
2136 if self.pipelines and not matches_pipeline:
2137 return False
2138
James E. Blairee743612012-05-29 14:49:32 -07002139 # branches are ORed
2140 matches_branch = False
2141 for branch in self.branches:
2142 if branch.match(event.branch):
2143 matches_branch = True
2144 if self.branches and not matches_branch:
2145 return False
2146
2147 # refs are ORed
2148 matches_ref = False
Yolanda Robla16698872014-08-25 11:59:27 +02002149 if event.ref is not None:
2150 for ref in self.refs:
2151 if ref.match(event.ref):
2152 matches_ref = True
James E. Blairee743612012-05-29 14:49:32 -07002153 if self.refs and not matches_ref:
2154 return False
K Jonathan Harkerf95e7232015-04-29 13:33:16 -07002155 if self.ignore_deletes and event.newrev == EMPTY_GIT_REF:
2156 # If the updated ref has an empty git sha (all 0s),
2157 # then the ref is being deleted
2158 return False
James E. Blairee743612012-05-29 14:49:32 -07002159
James E. Blair1fbfceb2014-06-23 14:42:53 -07002160 # comments are ORed
2161 matches_comment_re = False
2162 for comment_re in self.comments:
Clark Boylanb9bcb402012-06-29 17:44:05 -07002163 if (event.comment is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07002164 comment_re.search(event.comment)):
2165 matches_comment_re = True
2166 if self.comments and not matches_comment_re:
Clark Boylanb9bcb402012-06-29 17:44:05 -07002167 return False
2168
Antoine Mussob4e809e2012-12-06 16:58:06 +01002169 # We better have an account provided by Gerrit to do
2170 # email filtering.
2171 if event.account is not None:
James E. Blaircf429f32012-12-20 14:28:24 -08002172 account_email = event.account.get('email')
James E. Blair1fbfceb2014-06-23 14:42:53 -07002173 # emails are ORed
2174 matches_email_re = False
2175 for email_re in self.emails:
Antoine Mussob4e809e2012-12-06 16:58:06 +01002176 if (account_email is not None and
Joshua Hesketh29d99b72014-08-19 16:27:42 +10002177 email_re.search(account_email)):
James E. Blair1fbfceb2014-06-23 14:42:53 -07002178 matches_email_re = True
2179 if self.emails and not matches_email_re:
Antoine Mussob4e809e2012-12-06 16:58:06 +01002180 return False
2181
James E. Blair1fbfceb2014-06-23 14:42:53 -07002182 # usernames are ORed
Joshua Heskethb8a817e2013-12-27 11:21:38 +11002183 account_username = event.account.get('username')
James E. Blair1fbfceb2014-06-23 14:42:53 -07002184 matches_username_re = False
2185 for username_re in self.usernames:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11002186 if (account_username is not None and
James E. Blair1fbfceb2014-06-23 14:42:53 -07002187 username_re.search(account_username)):
2188 matches_username_re = True
2189 if self.usernames and not matches_username_re:
Joshua Heskethb8a817e2013-12-27 11:21:38 +11002190 return False
2191
James E. Blairee743612012-05-29 14:49:32 -07002192 # approvals are ANDed
James E. Blairc053d022014-01-22 14:57:33 -08002193 for category, value in self.event_approvals.items():
James E. Blairee743612012-05-29 14:49:32 -07002194 matches_approval = False
2195 for eapproval in event.approvals:
2196 if (normalizeCategory(eapproval['description']) == category and
2197 int(eapproval['value']) == int(value)):
2198 matches_approval = True
James E. Blair1e8dd892012-05-30 09:15:05 -07002199 if not matches_approval:
2200 return False
James E. Blair63bb0ef2013-07-29 17:14:51 -07002201
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002202 # required approvals are ANDed (reject approvals are ORed)
2203 if not self.matchesApprovals(change):
James E. Blair9c17dbf2014-06-23 14:21:58 -07002204 return False
James E. Blairc053d022014-01-22 14:57:33 -08002205
James E. Blair63bb0ef2013-07-29 17:14:51 -07002206 # timespecs are ORed
2207 matches_timespec = False
2208 for timespec in self.timespecs:
2209 if (event.timespec == timespec):
2210 matches_timespec = True
2211 if self.timespecs and not matches_timespec:
2212 return False
2213
Gregory Haynes4fc12542015-04-22 20:38:06 -07002214 # actions are ORed
2215 matches_action = False
2216 for action in self.actions:
2217 if (event.action == action):
2218 matches_action = True
2219 if self.actions and not matches_action:
2220 return False
2221
Jan Hruban16ad31f2015-11-07 14:39:07 +01002222 # labels are ORed
2223 if self.labels and event.label not in self.labels:
2224 return False
2225
2226 # unlabels are ORed
2227 if self.unlabels and event.unlabel not in self.unlabels:
2228 return False
2229
Jesse Keating5c05a9f2017-01-12 14:44:58 -08002230 # states are ORed
2231 if self.states and event.state not in self.states:
2232 return False
2233
James E. Blairee743612012-05-29 14:49:32 -07002234 return True
James E. Blaireff88162013-07-01 12:44:14 -04002235
2236
James E. Blair9c17dbf2014-06-23 14:21:58 -07002237class ChangeishFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002238 """Allows a Manager to only enqueue Changes that meet certain criteria."""
Clark Boylana9702ad2014-05-08 17:17:24 -07002239 def __init__(self, open=None, current_patchset=None,
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002240 statuses=[], required_approvals=[],
2241 reject_approvals=[]):
James E. Blair9c17dbf2014-06-23 14:21:58 -07002242 super(ChangeishFilter, self).__init__(
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002243 required_approvals=required_approvals,
2244 reject_approvals=reject_approvals)
James E. Blair11041d22014-05-02 14:49:53 -07002245 self.open = open
Clark Boylana9702ad2014-05-08 17:17:24 -07002246 self.current_patchset = current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07002247 self.statuses = statuses
James E. Blair11041d22014-05-02 14:49:53 -07002248
2249 def __repr__(self):
2250 ret = '<ChangeishFilter'
2251
2252 if self.open is not None:
2253 ret += ' open: %s' % self.open
Clark Boylana9702ad2014-05-08 17:17:24 -07002254 if self.current_patchset is not None:
2255 ret += ' current-patchset: %s' % self.current_patchset
James E. Blair11041d22014-05-02 14:49:53 -07002256 if self.statuses:
2257 ret += ' statuses: %s' % ', '.join(self.statuses)
James E. Blair5bf78a32015-07-30 18:08:24 +00002258 if self.required_approvals:
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002259 ret += (' required_approvals: %s' %
2260 str(self.required_approvals))
2261 if self.reject_approvals:
2262 ret += (' reject_approvals: %s' %
2263 str(self.reject_approvals))
James E. Blair11041d22014-05-02 14:49:53 -07002264 ret += '>'
2265
2266 return ret
2267
2268 def matches(self, change):
2269 if self.open is not None:
2270 if self.open != change.open:
2271 return False
2272
Clark Boylana9702ad2014-05-08 17:17:24 -07002273 if self.current_patchset is not None:
2274 if self.current_patchset != change.is_current_patchset:
2275 return False
2276
James E. Blair11041d22014-05-02 14:49:53 -07002277 if self.statuses:
2278 if change.status not in self.statuses:
2279 return False
2280
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002281 # required approvals are ANDed (reject approvals are ORed)
2282 if not self.matchesApprovals(change):
James E. Blair9c17dbf2014-06-23 14:21:58 -07002283 return False
James E. Blair11041d22014-05-02 14:49:53 -07002284
2285 return True
2286
2287
James E. Blairb97ed802015-12-21 15:55:35 -08002288class ProjectPipelineConfig(object):
2289 # Represents a project cofiguration in the context of a pipeline
2290 def __init__(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002291 self.job_list = JobList()
James E. Blairb97ed802015-12-21 15:55:35 -08002292 self.queue_name = None
Adam Gandelman8bd57102016-12-02 12:58:42 -08002293 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08002294
2295
2296class ProjectConfig(object):
2297 # Represents a project cofiguration
2298 def __init__(self, name):
2299 self.name = name
Adam Gandelman8bd57102016-12-02 12:58:42 -08002300 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08002301 self.pipelines = {}
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002302 self.private_key_file = None
James E. Blairb97ed802015-12-21 15:55:35 -08002303
2304
James E. Blaird8e778f2015-12-22 14:09:20 -08002305class UnparsedAbideConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002306 """A collection of yaml lists that has not yet been parsed into objects.
2307
2308 An Abide is a collection of tenants.
2309 """
2310
James E. Blaird8e778f2015-12-22 14:09:20 -08002311 def __init__(self):
2312 self.tenants = []
2313
2314 def extend(self, conf):
2315 if isinstance(conf, UnparsedAbideConfig):
2316 self.tenants.extend(conf.tenants)
2317 return
2318
2319 if not isinstance(conf, list):
2320 raise Exception("Configuration items must be in the form of "
2321 "a list of dictionaries (when parsing %s)" %
2322 (conf,))
2323 for item in conf:
2324 if not isinstance(item, dict):
2325 raise Exception("Configuration items must be in the form of "
2326 "a list of dictionaries (when parsing %s)" %
2327 (conf,))
2328 if len(item.keys()) > 1:
2329 raise Exception("Configuration item dictionaries must have "
2330 "a single key (when parsing %s)" %
2331 (conf,))
2332 key, value = item.items()[0]
2333 if key == 'tenant':
2334 self.tenants.append(value)
2335 else:
2336 raise Exception("Configuration item not recognized "
2337 "(when parsing %s)" %
2338 (conf,))
2339
2340
2341class UnparsedTenantConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002342 """A collection of yaml lists that has not yet been parsed into objects."""
2343
James E. Blaird8e778f2015-12-22 14:09:20 -08002344 def __init__(self):
2345 self.pipelines = []
2346 self.jobs = []
2347 self.project_templates = []
James E. Blairff555742017-02-19 11:34:27 -08002348 self.projects = {}
James E. Blaira98340f2016-09-02 11:33:49 -07002349 self.nodesets = []
James E. Blair01f83b72017-03-15 13:03:40 -07002350 self.secrets = []
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002351 self.semaphores = []
James E. Blaird8e778f2015-12-22 14:09:20 -08002352
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002353 def copy(self):
2354 r = UnparsedTenantConfig()
2355 r.pipelines = copy.deepcopy(self.pipelines)
2356 r.jobs = copy.deepcopy(self.jobs)
2357 r.project_templates = copy.deepcopy(self.project_templates)
2358 r.projects = copy.deepcopy(self.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07002359 r.nodesets = copy.deepcopy(self.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002360 r.secrets = copy.deepcopy(self.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002361 r.semaphores = copy.deepcopy(self.semaphores)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002362 return r
2363
James E. Blairec7ff302017-03-04 07:31:32 -08002364 def extend(self, conf):
James E. Blaird8e778f2015-12-22 14:09:20 -08002365 if isinstance(conf, UnparsedTenantConfig):
2366 self.pipelines.extend(conf.pipelines)
2367 self.jobs.extend(conf.jobs)
2368 self.project_templates.extend(conf.project_templates)
James E. Blairff555742017-02-19 11:34:27 -08002369 for k, v in conf.projects.items():
2370 self.projects.setdefault(k, []).extend(v)
James E. Blaira98340f2016-09-02 11:33:49 -07002371 self.nodesets.extend(conf.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002372 self.secrets.extend(conf.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002373 self.semaphores.extend(conf.semaphores)
James E. Blaird8e778f2015-12-22 14:09:20 -08002374 return
2375
2376 if not isinstance(conf, list):
2377 raise Exception("Configuration items must be in the form of "
2378 "a list of dictionaries (when parsing %s)" %
2379 (conf,))
James E. Blaircdab2032017-02-01 09:09:29 -08002380
James E. Blaird8e778f2015-12-22 14:09:20 -08002381 for item in conf:
2382 if not isinstance(item, dict):
2383 raise Exception("Configuration items must be in the form of "
2384 "a list of dictionaries (when parsing %s)" %
2385 (conf,))
2386 if len(item.keys()) > 1:
2387 raise Exception("Configuration item dictionaries must have "
2388 "a single key (when parsing %s)" %
2389 (conf,))
2390 key, value = item.items()[0]
James E. Blair66b274e2017-01-31 14:47:52 -08002391 if key == 'project':
James E. Blairff555742017-02-19 11:34:27 -08002392 name = value['name']
2393 self.projects.setdefault(name, []).append(value)
James E. Blair66b274e2017-01-31 14:47:52 -08002394 elif key == 'job':
James E. Blaird8e778f2015-12-22 14:09:20 -08002395 self.jobs.append(value)
2396 elif key == 'project-template':
2397 self.project_templates.append(value)
2398 elif key == 'pipeline':
2399 self.pipelines.append(value)
James E. Blaira98340f2016-09-02 11:33:49 -07002400 elif key == 'nodeset':
2401 self.nodesets.append(value)
James E. Blair01f83b72017-03-15 13:03:40 -07002402 elif key == 'secret':
2403 self.secrets.append(value)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002404 elif key == 'semaphore':
2405 self.semaphores.append(value)
James E. Blaird8e778f2015-12-22 14:09:20 -08002406 else:
Morgan Fainberg4245a422016-08-05 16:20:12 -07002407 raise Exception("Configuration item `%s` not recognized "
James E. Blaird8e778f2015-12-22 14:09:20 -08002408 "(when parsing %s)" %
Morgan Fainberg4245a422016-08-05 16:20:12 -07002409 (item, conf,))
James E. Blaird8e778f2015-12-22 14:09:20 -08002410
2411
James E. Blaireff88162013-07-01 12:44:14 -04002412class Layout(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002413 """Holds all of the Pipelines."""
2414
James E. Blaireff88162013-07-01 12:44:14 -04002415 def __init__(self):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002416 self.tenant = None
James E. Blairb97ed802015-12-21 15:55:35 -08002417 self.project_configs = {}
2418 self.project_templates = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07002419 self.pipelines = OrderedDict()
James E. Blair83005782015-12-11 14:46:03 -08002420 # This is a dictionary of name -> [jobs]. The first element
2421 # of the list is the first job added with that name. It is
2422 # the reference definition for a given job. Subsequent
2423 # elements are aspects of that job with different matchers
2424 # that override some attribute of the job. These aspects all
2425 # inherit from the reference definition.
James E. Blairfef88ec2016-11-30 08:38:27 -08002426 self.jobs = {'noop': [Job('noop')]}
James E. Blaira98340f2016-09-02 11:33:49 -07002427 self.nodesets = {}
James E. Blair01f83b72017-03-15 13:03:40 -07002428 self.secrets = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002429 self.semaphores = {}
James E. Blaireff88162013-07-01 12:44:14 -04002430
2431 def getJob(self, name):
2432 if name in self.jobs:
James E. Blair83005782015-12-11 14:46:03 -08002433 return self.jobs[name][0]
James E. Blairb97ed802015-12-21 15:55:35 -08002434 raise Exception("Job %s not defined" % (name,))
James E. Blair83005782015-12-11 14:46:03 -08002435
2436 def getJobs(self, name):
2437 return self.jobs.get(name, [])
2438
2439 def addJob(self, job):
James E. Blair4317e9f2016-07-15 10:05:47 -07002440 # We can have multiple variants of a job all with the same
2441 # name, but these variants must all be defined in the same repo.
James E. Blaircdab2032017-02-01 09:09:29 -08002442 prior_jobs = [j for j in self.getJobs(job.name) if
2443 j.source_context.project !=
2444 job.source_context.project]
James E. Blair4317e9f2016-07-15 10:05:47 -07002445 if prior_jobs:
2446 raise Exception("Job %s in %s is not permitted to shadow "
James E. Blaircdab2032017-02-01 09:09:29 -08002447 "job %s in %s" % (
2448 job,
2449 job.source_context.project,
2450 prior_jobs[0],
2451 prior_jobs[0].source_context.project))
James E. Blair4317e9f2016-07-15 10:05:47 -07002452
James E. Blair83005782015-12-11 14:46:03 -08002453 if job.name in self.jobs:
2454 self.jobs[job.name].append(job)
2455 else:
2456 self.jobs[job.name] = [job]
2457
James E. Blaira98340f2016-09-02 11:33:49 -07002458 def addNodeSet(self, nodeset):
2459 if nodeset.name in self.nodesets:
2460 raise Exception("NodeSet %s already defined" % (nodeset.name,))
2461 self.nodesets[nodeset.name] = nodeset
2462
James E. Blair01f83b72017-03-15 13:03:40 -07002463 def addSecret(self, secret):
2464 if secret.name in self.secrets:
2465 raise Exception("Secret %s already defined" % (secret.name,))
2466 self.secrets[secret.name] = secret
2467
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002468 def addSemaphore(self, semaphore):
2469 if semaphore.name in self.semaphores:
2470 raise Exception("Semaphore %s already defined" % (semaphore.name,))
2471 self.semaphores[semaphore.name] = semaphore
2472
James E. Blair83005782015-12-11 14:46:03 -08002473 def addPipeline(self, pipeline):
2474 self.pipelines[pipeline.name] = pipeline
James E. Blair59fdbac2015-12-07 17:08:06 -08002475
James E. Blairb97ed802015-12-21 15:55:35 -08002476 def addProjectTemplate(self, project_template):
2477 self.project_templates[project_template.name] = project_template
2478
James E. Blairf59f3cf2017-02-19 14:50:26 -08002479 def addProjectConfig(self, project_config):
James E. Blairb97ed802015-12-21 15:55:35 -08002480 self.project_configs[project_config.name] = project_config
James E. Blairb97ed802015-12-21 15:55:35 -08002481
James E. Blaird2348362017-03-17 13:59:35 -07002482 def _createJobGraph(self, item, job_list, job_graph):
2483 change = item.change
2484 pipeline = item.pipeline
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002485 for jobname in job_list.jobs:
2486 # This is the final job we are constructing
James E. Blaira7f51ca2017-02-07 16:01:26 -08002487 frozen_job = None
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002488 # Whether the change matches any globally defined variant
James E. Blaira7f51ca2017-02-07 16:01:26 -08002489 matched = False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002490 for variant in self.getJobs(jobname):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002491 if variant.changeMatches(change):
James E. Blaira7f51ca2017-02-07 16:01:26 -08002492 if frozen_job is None:
2493 frozen_job = variant.copy()
2494 frozen_job.setRun()
2495 else:
2496 frozen_job.applyVariant(variant)
2497 matched = True
2498 if not matched:
James E. Blair6e85c2b2016-11-21 16:47:01 -08002499 # A change must match at least one defined job variant
2500 # (that is to say that it must match more than just
2501 # the job that is defined in the tree).
2502 continue
James E. Blaira7f51ca2017-02-07 16:01:26 -08002503 # If the job does not allow auth inheritance, do not allow
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002504 # the project-pipeline variants to update its execution
James E. Blaira7f51ca2017-02-07 16:01:26 -08002505 # attributes.
James E. Blair8525e2b2017-03-15 14:05:47 -07002506 if frozen_job.auth and not frozen_job.auth.inherit:
James E. Blaira7f51ca2017-02-07 16:01:26 -08002507 frozen_job.final = True
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002508 # Whether the change matches any of the project pipeline
2509 # variants
2510 matched = False
2511 for variant in job_list.jobs[jobname]:
2512 if variant.changeMatches(change):
2513 frozen_job.applyVariant(variant)
2514 matched = True
2515 if not matched:
2516 # A change must match at least one project pipeline
2517 # job variant.
2518 continue
James E. Blairb3f5db12017-03-17 12:57:39 -07002519 if (frozen_job.allowed_projects and
2520 change.project.name not in frozen_job.allowed_projects):
2521 raise Exception("Project %s is not allowed to run job %s" %
2522 (change.project.name, frozen_job.name))
James E. Blaird2348362017-03-17 13:59:35 -07002523 if ((not pipeline.allow_secrets) and frozen_job.auth and
2524 frozen_job.auth.secrets):
2525 raise Exception("Pipeline %s does not allow jobs with "
2526 "secrets (job %s)" % (
2527 pipeline.name, frozen_job.name))
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002528 job_graph.addJob(frozen_job)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002529
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002530 def createJobGraph(self, item):
Paul Belanger160cb8e2016-11-11 19:04:24 -05002531 project_config = self.project_configs.get(
James E. Blair0ffa0102017-03-30 13:11:33 -07002532 item.change.project.canonical_name, None)
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002533 ret = JobGraph()
Paul Belanger15e3e202016-10-14 16:27:34 -04002534 # NOTE(pabelanger): It is possible for a foreign project not to have a
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002535 # configured pipeline, if so return an empty JobGraph.
Paul Belanger160cb8e2016-11-11 19:04:24 -05002536 if project_config and item.pipeline.name in project_config.pipelines:
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002537 project_job_list = \
2538 project_config.pipelines[item.pipeline.name].job_list
James E. Blaird2348362017-03-17 13:59:35 -07002539 self._createJobGraph(item, project_job_list, ret)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002540 return ret
2541
James E. Blair59fdbac2015-12-07 17:08:06 -08002542
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002543class Semaphore(object):
2544 def __init__(self, name, max=1):
2545 self.name = name
2546 self.max = int(max)
2547
2548
2549class SemaphoreHandler(object):
2550 log = logging.getLogger("zuul.SemaphoreHandler")
2551
2552 def __init__(self):
2553 self.semaphores = {}
2554
2555 def acquire(self, item, job):
2556 if not job.semaphore:
2557 return True
2558
2559 semaphore_key = job.semaphore
2560
2561 m = self.semaphores.get(semaphore_key)
2562 if not m:
2563 # The semaphore is not held, acquire it
2564 self._acquire(semaphore_key, item, job.name)
2565 return True
2566 if (item, job.name) in m:
2567 # This item already holds the semaphore
2568 return True
2569
2570 # semaphore is there, check max
2571 if len(m) < self._max_count(item, job.semaphore):
2572 self._acquire(semaphore_key, item, job.name)
2573 return True
2574
2575 return False
2576
2577 def release(self, item, job):
2578 if not job.semaphore:
2579 return
2580
2581 semaphore_key = job.semaphore
2582
2583 m = self.semaphores.get(semaphore_key)
2584 if not m:
2585 # The semaphore is not held, nothing to do
2586 self.log.error("Semaphore can not be released for %s "
2587 "because the semaphore is not held" %
2588 item)
2589 return
2590 if (item, job.name) in m:
2591 # This item is a holder of the semaphore
2592 self._release(semaphore_key, item, job.name)
2593 return
2594 self.log.error("Semaphore can not be released for %s "
2595 "which does not hold it" % item)
2596
2597 def _acquire(self, semaphore_key, item, job_name):
2598 self.log.debug("Semaphore acquire {semaphore}: job {job}, item {item}"
2599 .format(semaphore=semaphore_key,
2600 job=job_name,
2601 item=item))
2602 if semaphore_key not in self.semaphores:
2603 self.semaphores[semaphore_key] = []
2604 self.semaphores[semaphore_key].append((item, job_name))
2605
2606 def _release(self, semaphore_key, item, job_name):
2607 self.log.debug("Semaphore release {semaphore}: job {job}, item {item}"
2608 .format(semaphore=semaphore_key,
2609 job=job_name,
2610 item=item))
2611 sem_item = (item, job_name)
2612 if sem_item in self.semaphores[semaphore_key]:
2613 self.semaphores[semaphore_key].remove(sem_item)
2614
2615 # cleanup if there is no user of the semaphore anymore
2616 if len(self.semaphores[semaphore_key]) == 0:
2617 del self.semaphores[semaphore_key]
2618
2619 @staticmethod
2620 def _max_count(item, semaphore_name):
2621 if not item.current_build_set.layout:
2622 # This should not occur as the layout of the item must already be
2623 # built when acquiring or releasing a semaphore for a job.
2624 raise Exception("Item {} has no layout".format(item))
2625
2626 # find the right semaphore
2627 default_semaphore = Semaphore(semaphore_name, 1)
2628 semaphores = item.current_build_set.layout.semaphores
2629 return semaphores.get(semaphore_name, default_semaphore).max
2630
2631
James E. Blair59fdbac2015-12-07 17:08:06 -08002632class Tenant(object):
2633 def __init__(self, name):
2634 self.name = name
2635 self.layout = None
James E. Blair646322f2017-01-27 15:50:34 -08002636 # The unparsed configuration from the main zuul config for
2637 # this tenant.
2638 self.unparsed_config = None
James E. Blair109da3f2017-04-04 14:39:43 -07002639 # The list of projects from which we will read full
James E. Blair8a395f92017-03-30 11:15:33 -07002640 # configuration.
James E. Blair109da3f2017-04-04 14:39:43 -07002641 self.config_projects = []
2642 # The unparsed config from those projects.
2643 self.config_projects_config = None
2644 # The list of projects from which we will read untrusted
2645 # in-repo configuration.
2646 self.untrusted_projects = []
2647 # The unparsed config from those projects.
2648 self.untrusted_projects_config = None
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002649 self.semaphore_handler = SemaphoreHandler()
2650
James E. Blairc2a54fd2017-03-29 15:19:26 -07002651 # A mapping of project names to projects. project_name ->
2652 # VALUE where VALUE is a further dictionary of
2653 # canonical_hostname -> Project.
2654 self.projects = {}
2655 self.canonical_hostnames = set()
2656
2657 def _addProject(self, project):
2658 """Add a project to the project index
2659
2660 :arg Project project: The project to add.
2661 """
2662 self.canonical_hostnames.add(project.canonical_hostname)
2663 hostname_dict = self.projects.setdefault(project.name, {})
2664 if project.canonical_hostname in hostname_dict:
2665 raise Exception("Project %s is already in project index" %
2666 (project,))
2667 hostname_dict[project.canonical_hostname] = project
2668
2669 def getProject(self, name):
2670 """Return a project given its name.
2671
2672 :arg str name: The name of the project. It may be fully
2673 qualified (E.g., "git.example.com/subpath/project") or may
2674 contain only the project name name may be supplied (E.g.,
2675 "subpath/project").
2676
2677 :returns: A tuple (trusted, project) or (None, None) if the
2678 project is not found or ambiguous. The "trusted" boolean
2679 indicates whether or not the project is trusted by this
2680 tenant.
2681 :rtype: (bool, Project)
2682
2683 """
2684 path = name.split('/', 1)
2685 if path[0] in self.canonical_hostnames:
2686 hostname = path[0]
2687 project_name = path[1]
2688 else:
2689 hostname = None
2690 project_name = name
2691 hostname_dict = self.projects.get(project_name)
2692 project = None
2693 if hostname_dict:
2694 if hostname:
2695 project = hostname_dict.get(hostname)
2696 else:
2697 values = hostname_dict.values()
2698 if len(values) == 1:
2699 project = values[0]
2700 else:
2701 raise Exception("Project name '%s' is ambiguous, "
2702 "please fully qualify the project "
2703 "with a hostname" % (name,))
2704 if project is None:
2705 return (None, None)
James E. Blair109da3f2017-04-04 14:39:43 -07002706 if project in self.config_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002707 return (True, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002708 if project in self.untrusted_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002709 return (False, project)
2710 # This should never happen:
2711 raise Exception("Project %s is neither trusted nor untrusted" %
2712 (project,))
2713
James E. Blair109da3f2017-04-04 14:39:43 -07002714 def addConfigProject(self, project):
2715 self.config_projects.append(project)
James E. Blairc2a54fd2017-03-29 15:19:26 -07002716 self._addProject(project)
James E. Blair5ac93842017-01-20 06:47:34 -08002717
James E. Blair109da3f2017-04-04 14:39:43 -07002718 def addUntrustedProject(self, project):
2719 self.untrusted_projects.append(project)
James E. Blairc2a54fd2017-03-29 15:19:26 -07002720 self._addProject(project)
James E. Blair5ac93842017-01-20 06:47:34 -08002721
James E. Blair59fdbac2015-12-07 17:08:06 -08002722
2723class Abide(object):
2724 def __init__(self):
2725 self.tenants = OrderedDict()
James E. Blairce8a2132016-05-19 15:21:52 -07002726
2727
2728class JobTimeData(object):
2729 format = 'B10H10H10B'
2730 version = 0
2731
2732 def __init__(self, path):
2733 self.path = path
2734 self.success_times = [0 for x in range(10)]
2735 self.failure_times = [0 for x in range(10)]
2736 self.results = [0 for x in range(10)]
2737
2738 def load(self):
2739 if not os.path.exists(self.path):
2740 return
2741 with open(self.path) as f:
2742 data = struct.unpack(self.format, f.read())
2743 version = data[0]
2744 if version != self.version:
2745 raise Exception("Unkown data version")
2746 self.success_times = list(data[1:11])
2747 self.failure_times = list(data[11:21])
2748 self.results = list(data[21:32])
2749
2750 def save(self):
2751 tmpfile = self.path + '.tmp'
2752 data = [self.version]
2753 data.extend(self.success_times)
2754 data.extend(self.failure_times)
2755 data.extend(self.results)
2756 data = struct.pack(self.format, *data)
2757 with open(tmpfile, 'w') as f:
2758 f.write(data)
2759 os.rename(tmpfile, self.path)
2760
2761 def add(self, elapsed, result):
2762 elapsed = int(elapsed)
2763 if result == 'SUCCESS':
2764 self.success_times.append(elapsed)
2765 self.success_times.pop(0)
2766 result = 0
2767 else:
2768 self.failure_times.append(elapsed)
2769 self.failure_times.pop(0)
2770 result = 1
2771 self.results.append(result)
2772 self.results.pop(0)
2773
2774 def getEstimatedTime(self):
2775 times = [x for x in self.success_times if x]
2776 if times:
2777 return float(sum(times)) / len(times)
2778 return 0.0
2779
2780
2781class TimeDataBase(object):
2782 def __init__(self, root):
2783 self.root = root
2784 self.jobs = {}
2785
2786 def _getTD(self, name):
2787 td = self.jobs.get(name)
2788 if not td:
2789 td = JobTimeData(os.path.join(self.root, name))
2790 self.jobs[name] = td
2791 td.load()
2792 return td
2793
2794 def getEstimatedTime(self, name):
2795 return self._getTD(name).getEstimatedTime()
2796
2797 def update(self, name, elapsed, result):
2798 td = self._getTD(name)
2799 td.add(elapsed, result)
2800 td.save()