blob: dc04e5938cfc2f7ad141806bd530e41cfeb3a0ee [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
Tristan Cacqueraye7410af2017-06-19 04:32:08 +000016from collections import OrderedDict
James E. Blair1b265312014-06-24 09:35:21 -070017import copy
Tobias Henkel9a0e1942017-03-20 16:16:02 +010018import logging
James E. Blairce8a2132016-05-19 15:21:52 -070019import os
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 -070023
James E. Blair19deff22013-08-25 13:17:35 -070024MERGER_MERGE = 1 # "git merge"
25MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
26MERGER_CHERRY_PICK = 3 # "git cherry-pick"
27
28MERGER_MAP = {
29 'merge': MERGER_MERGE,
30 'merge-resolve': MERGER_MERGE_RESOLVE,
31 'cherry-pick': MERGER_CHERRY_PICK,
32}
James E. Blairee743612012-05-29 14:49:32 -070033
James E. Blair64ed6f22013-07-10 14:07:23 -070034PRECEDENCE_NORMAL = 0
35PRECEDENCE_LOW = 1
36PRECEDENCE_HIGH = 2
37
38PRECEDENCE_MAP = {
39 None: PRECEDENCE_NORMAL,
40 'low': PRECEDENCE_LOW,
41 'normal': PRECEDENCE_NORMAL,
42 'high': PRECEDENCE_HIGH,
43}
44
James E. Blair803e94f2017-01-06 09:18:59 -080045# Request states
46STATE_REQUESTED = 'requested'
47STATE_PENDING = 'pending'
48STATE_FULFILLED = 'fulfilled'
49STATE_FAILED = 'failed'
50REQUEST_STATES = set([STATE_REQUESTED,
51 STATE_PENDING,
52 STATE_FULFILLED,
53 STATE_FAILED])
54
55# Node states
56STATE_BUILDING = 'building'
57STATE_TESTING = 'testing'
58STATE_READY = 'ready'
59STATE_IN_USE = 'in-use'
60STATE_USED = 'used'
61STATE_HOLD = 'hold'
62STATE_DELETING = 'deleting'
63NODE_STATES = set([STATE_BUILDING,
64 STATE_TESTING,
65 STATE_READY,
66 STATE_IN_USE,
67 STATE_USED,
68 STATE_HOLD,
69 STATE_DELETING])
70
James E. Blair1e8dd892012-05-30 09:15:05 -070071
Joshua Hesketh58419cb2017-02-24 13:09:22 -050072class Attributes(object):
73 """A class to hold attributes for string formatting."""
74
75 def __init__(self, **kw):
76 setattr(self, '__dict__', kw)
77
78
James E. Blair4aea70c2012-07-26 14:23:24 -070079class Pipeline(object):
James E. Blair6053de42017-04-05 11:27:11 -070080 """A configuration that ties together triggers, reporters and managers
Monty Taylor82dfd412016-07-29 12:01:28 -070081
82 Trigger
83 A description of which events should be processed
84
85 Manager
86 Responsible for enqueing and dequeing Changes
87
88 Reporter
89 Communicates success and failure results somewhere
Monty Taylora42a55b2016-07-29 07:53:33 -070090 """
James E. Blair83005782015-12-11 14:46:03 -080091 def __init__(self, name, layout):
James E. Blair4aea70c2012-07-26 14:23:24 -070092 self.name = name
James E. Blair83005782015-12-11 14:46:03 -080093 self.layout = layout
James E. Blair8dbd56a2012-12-22 10:55:10 -080094 self.description = None
James E. Blair56370192013-01-14 15:47:28 -080095 self.failure_message = None
Joshua Heskethb7179772014-01-30 23:30:46 +110096 self.merge_failure_message = None
James E. Blair56370192013-01-14 15:47:28 -080097 self.success_message = None
Joshua Hesketh3979e3e2014-03-04 11:21:10 +110098 self.footer_message = None
James E. Blair60af7f42016-03-11 16:11:06 -080099 self.start_message = None
James E. Blaird2348362017-03-17 13:59:35 -0700100 self.allow_secrets = False
James E. Blair2fa50962013-01-30 21:50:41 -0800101 self.dequeue_on_new_patchset = True
James E. Blair17dd6772015-02-09 14:45:18 -0800102 self.ignore_dependencies = False
James E. Blair4aea70c2012-07-26 14:23:24 -0700103 self.manager = None
James E. Blaire0487072012-08-29 17:38:31 -0700104 self.queues = []
James E. Blair64ed6f22013-07-10 14:07:23 -0700105 self.precedence = PRECEDENCE_NORMAL
James E. Blair83005782015-12-11 14:46:03 -0800106 self.triggers = []
Joshua Hesketh352264b2015-08-11 23:42:08 +1000107 self.start_actions = []
108 self.success_actions = []
109 self.failure_actions = []
110 self.merge_failure_actions = []
111 self.disabled_actions = []
Joshua Hesketh89e829d2015-02-10 16:29:45 +1100112 self.disable_at = None
113 self._consecutive_failures = 0
114 self._disabled = False
Clark Boylan7603a372014-01-21 11:43:20 -0800115 self.window = None
116 self.window_floor = None
117 self.window_increase_type = None
118 self.window_increase_factor = None
119 self.window_decrease_type = None
120 self.window_decrease_factor = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700121
James E. Blair83005782015-12-11 14:46:03 -0800122 @property
123 def actions(self):
124 return (
125 self.start_actions +
126 self.success_actions +
127 self.failure_actions +
128 self.merge_failure_actions +
129 self.disabled_actions
130 )
131
James E. Blaird09c17a2012-08-07 09:23:14 -0700132 def __repr__(self):
133 return '<Pipeline %s>' % self.name
134
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100135 def getSafeAttributes(self):
136 return Attributes(name=self.name)
137
James E. Blair4aea70c2012-07-26 14:23:24 -0700138 def setManager(self, manager):
139 self.manager = manager
140
James E. Blaire0487072012-08-29 17:38:31 -0700141 def addQueue(self, queue):
142 self.queues.append(queue)
143
144 def getQueue(self, project):
145 for queue in self.queues:
146 if project in queue.projects:
147 return queue
148 return None
149
James E. Blairbfb8e042014-12-30 17:01:44 -0800150 def removeQueue(self, queue):
Tobias Henkel6b9390f2017-03-28 11:23:21 +0200151 if queue in self.queues:
152 self.queues.remove(queue)
James E. Blairbfb8e042014-12-30 17:01:44 -0800153
James E. Blaire0487072012-08-29 17:38:31 -0700154 def getChangesInQueue(self):
155 changes = []
156 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700157 changes.extend([x.change for x in shared_queue.queue])
James E. Blaire0487072012-08-29 17:38:31 -0700158 return changes
159
James E. Blairfee8d652013-06-07 08:57:52 -0700160 def getAllItems(self):
161 items = []
James E. Blaire0487072012-08-29 17:38:31 -0700162 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700163 items.extend(shared_queue.queue)
James E. Blairfee8d652013-06-07 08:57:52 -0700164 return items
James E. Blaire0487072012-08-29 17:38:31 -0700165
James E. Blair800e7ff2017-03-17 16:06:52 -0700166 def formatStatusJSON(self):
James E. Blair8dbd56a2012-12-22 10:55:10 -0800167 j_pipeline = dict(name=self.name,
168 description=self.description)
169 j_queues = []
170 j_pipeline['change_queues'] = j_queues
171 for queue in self.queues:
172 j_queue = dict(name=queue.name)
173 j_queues.append(j_queue)
174 j_queue['heads'] = []
Clark Boylanaf2476f2014-01-23 14:47:36 -0800175 j_queue['window'] = queue.window
James E. Blair972e3c72013-08-29 12:04:55 -0700176
177 j_changes = []
178 for e in queue.queue:
179 if not e.item_ahead:
180 if j_changes:
181 j_queue['heads'].append(j_changes)
182 j_changes = []
James E. Blair800e7ff2017-03-17 16:06:52 -0700183 j_changes.append(e.formatJSON())
James E. Blair972e3c72013-08-29 12:04:55 -0700184 if (len(j_changes) > 1 and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000185 (j_changes[-2]['remaining_time'] is not None) and
186 (j_changes[-1]['remaining_time'] is not None)):
James E. Blair972e3c72013-08-29 12:04:55 -0700187 j_changes[-1]['remaining_time'] = max(
188 j_changes[-2]['remaining_time'],
189 j_changes[-1]['remaining_time'])
190 if j_changes:
James E. Blair8dbd56a2012-12-22 10:55:10 -0800191 j_queue['heads'].append(j_changes)
192 return j_pipeline
193
James E. Blair4aea70c2012-07-26 14:23:24 -0700194
James E. Blairee743612012-05-29 14:49:32 -0700195class ChangeQueue(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700196 """A ChangeQueue contains Changes to be processed related projects.
197
Monty Taylor82dfd412016-07-29 12:01:28 -0700198 A Pipeline with a DependentPipelineManager has multiple parallel
199 ChangeQueues shared by different projects. For instance, there may a
200 ChangeQueue shared by interrelated projects foo and bar, and a second queue
201 for independent project baz.
Monty Taylora42a55b2016-07-29 07:53:33 -0700202
Monty Taylor82dfd412016-07-29 12:01:28 -0700203 A Pipeline with an IndependentPipelineManager puts every Change into its
204 own ChangeQueue
Monty Taylora42a55b2016-07-29 07:53:33 -0700205
206 The ChangeQueue Window is inspired by TCP windows and controlls how many
207 Changes in a given ChangeQueue will be considered active and ready to
208 be processed. If a Change succeeds, the Window is increased by
209 `window_increase_factor`. If a Change fails, the Window is decreased by
210 `window_decrease_factor`.
211 """
James E. Blairbfb8e042014-12-30 17:01:44 -0800212 def __init__(self, pipeline, window=0, window_floor=1,
Clark Boylan7603a372014-01-21 11:43:20 -0800213 window_increase_type='linear', window_increase_factor=1,
James E. Blair0dcef7a2016-08-19 09:35:17 -0700214 window_decrease_type='exponential', window_decrease_factor=2,
215 name=None):
James E. Blair4aea70c2012-07-26 14:23:24 -0700216 self.pipeline = pipeline
James E. Blair0dcef7a2016-08-19 09:35:17 -0700217 if name:
218 self.name = name
219 else:
220 self.name = ''
James E. Blairee743612012-05-29 14:49:32 -0700221 self.projects = []
222 self._jobs = set()
223 self.queue = []
Clark Boylan7603a372014-01-21 11:43:20 -0800224 self.window = window
225 self.window_floor = window_floor
226 self.window_increase_type = window_increase_type
227 self.window_increase_factor = window_increase_factor
228 self.window_decrease_type = window_decrease_type
229 self.window_decrease_factor = window_decrease_factor
James E. Blairee743612012-05-29 14:49:32 -0700230
James E. Blair9f9667e2012-06-12 17:51:08 -0700231 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700232 return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
James E. Blairee743612012-05-29 14:49:32 -0700233
234 def getJobs(self):
235 return self._jobs
236
237 def addProject(self, project):
238 if project not in self.projects:
239 self.projects.append(project)
James E. Blairc8a1e052014-02-25 09:29:26 -0800240
James E. Blair0dcef7a2016-08-19 09:35:17 -0700241 if not self.name:
242 self.name = project.name
James E. Blairee743612012-05-29 14:49:32 -0700243
244 def enqueueChange(self, change):
James E. Blairbfb8e042014-12-30 17:01:44 -0800245 item = QueueItem(self, change)
James E. Blaircdccd972013-07-01 12:10:22 -0700246 self.enqueueItem(item)
247 item.enqueue_time = time.time()
248 return item
249
250 def enqueueItem(self, item):
James E. Blair4a035d92014-01-23 13:10:48 -0800251 item.pipeline = self.pipeline
James E. Blairbfb8e042014-12-30 17:01:44 -0800252 item.queue = self
253 if self.queue:
James E. Blairfee8d652013-06-07 08:57:52 -0700254 item.item_ahead = self.queue[-1]
James E. Blair972e3c72013-08-29 12:04:55 -0700255 item.item_ahead.items_behind.append(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700256 self.queue.append(item)
James E. Blairee743612012-05-29 14:49:32 -0700257
James E. Blairfee8d652013-06-07 08:57:52 -0700258 def dequeueItem(self, item):
259 if item in self.queue:
260 self.queue.remove(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700261 if item.item_ahead:
James E. Blair972e3c72013-08-29 12:04:55 -0700262 item.item_ahead.items_behind.remove(item)
263 for item_behind in item.items_behind:
264 if item.item_ahead:
265 item.item_ahead.items_behind.append(item_behind)
266 item_behind.item_ahead = item.item_ahead
James E. Blairfee8d652013-06-07 08:57:52 -0700267 item.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700268 item.items_behind = []
James E. Blairfee8d652013-06-07 08:57:52 -0700269 item.dequeue_time = time.time()
James E. Blaire0487072012-08-29 17:38:31 -0700270
James E. Blair972e3c72013-08-29 12:04:55 -0700271 def moveItem(self, item, item_ahead):
James E. Blair972e3c72013-08-29 12:04:55 -0700272 if item.item_ahead == item_ahead:
273 return False
274 # Remove from current location
275 if item.item_ahead:
276 item.item_ahead.items_behind.remove(item)
277 for item_behind in item.items_behind:
278 if item.item_ahead:
279 item.item_ahead.items_behind.append(item_behind)
280 item_behind.item_ahead = item.item_ahead
281 # Add to new location
282 item.item_ahead = item_ahead
James E. Blair00451262013-09-20 11:40:17 -0700283 item.items_behind = []
James E. Blair972e3c72013-08-29 12:04:55 -0700284 if item.item_ahead:
285 item.item_ahead.items_behind.append(item)
286 return True
James E. Blairee743612012-05-29 14:49:32 -0700287
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800288 def isActionable(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800289 if self.window:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800290 return item in self.queue[:self.window]
Clark Boylan7603a372014-01-21 11:43:20 -0800291 else:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800292 return True
Clark Boylan7603a372014-01-21 11:43:20 -0800293
294 def increaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800295 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800296 if self.window_increase_type == 'linear':
297 self.window += self.window_increase_factor
298 elif self.window_increase_type == 'exponential':
299 self.window *= self.window_increase_factor
300
301 def decreaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800302 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800303 if self.window_decrease_type == 'linear':
304 self.window = max(
305 self.window_floor,
306 self.window - self.window_decrease_factor)
307 elif self.window_decrease_type == 'exponential':
308 self.window = max(
309 self.window_floor,
Morgan Fainbergfdae2252016-06-07 20:13:20 -0700310 int(self.window / self.window_decrease_factor))
James E. Blairee743612012-05-29 14:49:32 -0700311
James E. Blair1e8dd892012-05-30 09:15:05 -0700312
James E. Blair4aea70c2012-07-26 14:23:24 -0700313class Project(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700314 """A Project represents a git repository such as openstack/nova."""
315
James E. Blaircf440a22016-07-15 09:11:58 -0700316 # NOTE: Projects should only be instantiated via a Source object
317 # so that they are associated with and cached by their Connection.
318 # This makes a Project instance a unique identifier for a given
319 # project from a given source.
320
James E. Blair0a899752017-03-29 13:22:16 -0700321 def __init__(self, name, source, foreign=False):
James E. Blair4aea70c2012-07-26 14:23:24 -0700322 self.name = name
James E. Blair8a395f92017-03-30 11:15:33 -0700323 self.source = source
James E. Blair0a899752017-03-29 13:22:16 -0700324 self.connection_name = source.connection.connection_name
325 self.canonical_hostname = source.canonical_hostname
James E. Blairc2a54fd2017-03-29 15:19:26 -0700326 self.canonical_name = source.canonical_hostname + '/' + name
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000327 # foreign projects are those referenced in dependencies
328 # of layout projects, this should matter
329 # when deciding whether to enqueue their changes
James E. Blaircf440a22016-07-15 09:11:58 -0700330 # TODOv3 (jeblair): re-add support for foreign projects if needed
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000331 self.foreign = foreign
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700332 self.unparsed_config = None
James E. Blaire3162022017-02-20 16:47:27 -0500333 self.unparsed_branch_config = {} # branch -> UnparsedTenantConfig
James E. Blairac180ce2017-06-07 19:31:02 -0700334 # Configuration object classes to include or exclude when
335 # loading zuul config files.
336 self.load_classes = frozenset()
James E. Blair4aea70c2012-07-26 14:23:24 -0700337
338 def __str__(self):
339 return self.name
340
341 def __repr__(self):
342 return '<Project %s>' % (self.name)
343
344
James E. Blair34776ee2016-08-25 13:53:54 -0700345class Node(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700346 """A single node for use by a job.
347
348 This may represent a request for a node, or an actual node
349 provided by Nodepool.
350 """
351
James E. Blair16d96a02017-06-08 11:32:56 -0700352 def __init__(self, name, label):
James E. Blair34776ee2016-08-25 13:53:54 -0700353 self.name = name
James E. Blair16d96a02017-06-08 11:32:56 -0700354 self.label = label
James E. Blaircbf43672017-01-04 14:33:41 -0800355 self.id = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800356 self.lock = None
357 # Attributes from Nodepool
358 self._state = 'unknown'
359 self.state_time = time.time()
Monty Taylor56f61332017-04-11 05:38:12 -0500360 self.interface_ip = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800361 self.public_ipv4 = None
362 self.private_ipv4 = None
363 self.public_ipv6 = None
James E. Blaircacdf2b2017-01-04 13:14:37 -0800364 self._keys = []
Paul Belanger30ba93a2017-03-16 16:28:10 -0400365 self.az = None
366 self.provider = None
367 self.region = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800368
369 @property
370 def state(self):
371 return self._state
372
373 @state.setter
374 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800375 if value not in NODE_STATES:
376 raise TypeError("'%s' is not a valid state" % value)
James E. Blaira38c28e2017-01-04 10:33:20 -0800377 self._state = value
378 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700379
380 def __repr__(self):
James E. Blair16d96a02017-06-08 11:32:56 -0700381 return '<Node %s %s:%s>' % (self.id, self.name, self.label)
James E. Blair34776ee2016-08-25 13:53:54 -0700382
James E. Blair0d952152017-02-07 17:14:44 -0800383 def __ne__(self, other):
384 return not self.__eq__(other)
385
386 def __eq__(self, other):
387 if not isinstance(other, Node):
388 return False
389 return (self.name == other.name and
James E. Blair16d96a02017-06-08 11:32:56 -0700390 self.label == other.label and
James E. Blair0d952152017-02-07 17:14:44 -0800391 self.id == other.id)
392
James E. Blaircacdf2b2017-01-04 13:14:37 -0800393 def toDict(self):
394 d = {}
395 d['state'] = self.state
396 for k in self._keys:
397 d[k] = getattr(self, k)
398 return d
399
James E. Blaira38c28e2017-01-04 10:33:20 -0800400 def updateFromDict(self, data):
401 self._state = data['state']
James E. Blaircacdf2b2017-01-04 13:14:37 -0800402 keys = []
403 for k, v in data.items():
404 if k == 'state':
405 continue
406 keys.append(k)
407 setattr(self, k, v)
408 self._keys = keys
James E. Blaira38c28e2017-01-04 10:33:20 -0800409
James E. Blair34776ee2016-08-25 13:53:54 -0700410
Monty Taylor7b19ba72017-05-24 07:42:54 -0500411class Group(object):
412 """A logical group of nodes for use by a job.
413
414 A Group is a named set of node names that will be provided to
415 jobs in the inventory to describe logical units where some subset of tasks
416 run.
417 """
418
419 def __init__(self, name, nodes):
420 self.name = name
421 self.nodes = nodes
422
423 def __repr__(self):
424 return '<Group %s %s>' % (self.name, str(self.nodes))
425
426 def __ne__(self, other):
427 return not self.__eq__(other)
428
429 def __eq__(self, other):
430 if not isinstance(other, Group):
431 return False
432 return (self.name == other.name and
433 self.nodes == other.nodes)
434
435 def toDict(self):
436 return {
437 'name': self.name,
438 'nodes': self.nodes
439 }
440
441
James E. Blaira98340f2016-09-02 11:33:49 -0700442class NodeSet(object):
443 """A set of nodes.
444
445 In configuration, NodeSets are attributes of Jobs indicating that
446 a Job requires nodes matching this description.
447
448 They may appear as top-level configuration objects and be named,
449 or they may appears anonymously in in-line job definitions.
450 """
451
452 def __init__(self, name=None):
453 self.name = name or ''
454 self.nodes = OrderedDict()
Monty Taylor7b19ba72017-05-24 07:42:54 -0500455 self.groups = OrderedDict()
James E. Blaira98340f2016-09-02 11:33:49 -0700456
James E. Blair1774dd52017-02-03 10:52:32 -0800457 def __ne__(self, other):
458 return not self.__eq__(other)
459
460 def __eq__(self, other):
461 if not isinstance(other, NodeSet):
462 return False
463 return (self.name == other.name and
464 self.nodes == other.nodes)
465
James E. Blaircbf43672017-01-04 14:33:41 -0800466 def copy(self):
467 n = NodeSet(self.name)
468 for name, node in self.nodes.items():
James E. Blair16d96a02017-06-08 11:32:56 -0700469 n.addNode(Node(node.name, node.label))
Monty Taylor7b19ba72017-05-24 07:42:54 -0500470 for name, group in self.groups.items():
471 n.addGroup(Group(group.name, group.nodes[:]))
James E. Blaircbf43672017-01-04 14:33:41 -0800472 return n
473
James E. Blaira98340f2016-09-02 11:33:49 -0700474 def addNode(self, node):
475 if node.name in self.nodes:
476 raise Exception("Duplicate node in %s" % (self,))
477 self.nodes[node.name] = node
478
James E. Blair0eaad552016-09-02 12:09:54 -0700479 def getNodes(self):
Clint Byruma4471d12017-05-10 20:57:40 -0400480 return list(self.nodes.values())
James E. Blair0eaad552016-09-02 12:09:54 -0700481
Monty Taylor7b19ba72017-05-24 07:42:54 -0500482 def addGroup(self, group):
483 if group.name in self.groups:
484 raise Exception("Duplicate group in %s" % (self,))
485 self.groups[group.name] = group
486
487 def getGroups(self):
488 return list(self.groups.values())
489
James E. Blaira98340f2016-09-02 11:33:49 -0700490 def __repr__(self):
491 if self.name:
492 name = self.name + ' '
493 else:
494 name = ''
Monty Taylor7b19ba72017-05-24 07:42:54 -0500495 return '<NodeSet %s%s%s>' % (name, self.nodes, self.groups)
James E. Blaira98340f2016-09-02 11:33:49 -0700496
497
James E. Blair34776ee2016-08-25 13:53:54 -0700498class NodeRequest(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700499 """A request for a set of nodes."""
500
James E. Blair8b2a1472017-02-19 15:33:55 -0800501 def __init__(self, requestor, build_set, job, nodeset):
502 self.requestor = requestor
James E. Blair34776ee2016-08-25 13:53:54 -0700503 self.build_set = build_set
504 self.job = job
James E. Blair0eaad552016-09-02 12:09:54 -0700505 self.nodeset = nodeset
James E. Blair803e94f2017-01-06 09:18:59 -0800506 self._state = STATE_REQUESTED
James E. Blairdce6cea2016-12-20 16:45:32 -0800507 self.state_time = time.time()
508 self.stat = None
509 self.uid = uuid4().hex
James E. Blaircbf43672017-01-04 14:33:41 -0800510 self.id = None
James E. Blaircbbce0d2017-05-19 07:28:29 -0700511 # Zuul internal flags (not stored in ZK so they are not
James E. Blaira38c28e2017-01-04 10:33:20 -0800512 # overwritten).
513 self.failed = False
James E. Blaircbbce0d2017-05-19 07:28:29 -0700514 self.canceled = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800515
516 @property
James E. Blair6ab79e02017-01-06 10:10:17 -0800517 def fulfilled(self):
518 return (self._state == STATE_FULFILLED) and not self.failed
519
520 @property
James E. Blairdce6cea2016-12-20 16:45:32 -0800521 def state(self):
522 return self._state
523
524 @state.setter
525 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800526 if value not in REQUEST_STATES:
527 raise TypeError("'%s' is not a valid state" % value)
James E. Blairdce6cea2016-12-20 16:45:32 -0800528 self._state = value
529 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700530
531 def __repr__(self):
James E. Blaircbf43672017-01-04 14:33:41 -0800532 return '<NodeRequest %s %s>' % (self.id, self.nodeset)
James E. Blair34776ee2016-08-25 13:53:54 -0700533
James E. Blairdce6cea2016-12-20 16:45:32 -0800534 def toDict(self):
535 d = {}
James E. Blair16d96a02017-06-08 11:32:56 -0700536 nodes = [n.label for n in self.nodeset.getNodes()]
James E. Blairdce6cea2016-12-20 16:45:32 -0800537 d['node_types'] = nodes
James E. Blair8b2a1472017-02-19 15:33:55 -0800538 d['requestor'] = self.requestor
James E. Blairdce6cea2016-12-20 16:45:32 -0800539 d['state'] = self.state
540 d['state_time'] = self.state_time
541 return d
542
543 def updateFromDict(self, data):
544 self._state = data['state']
545 self.state_time = data['state_time']
546
James E. Blair34776ee2016-08-25 13:53:54 -0700547
James E. Blair01f83b72017-03-15 13:03:40 -0700548class Secret(object):
549 """A collection of private data.
550
551 In configuration, Secrets are collections of private data in
552 key-value pair format. They are defined as top-level
553 configuration objects and then referenced by Jobs.
554
555 """
556
James E. Blair8525e2b2017-03-15 14:05:47 -0700557 def __init__(self, name, source_context):
James E. Blair01f83b72017-03-15 13:03:40 -0700558 self.name = name
James E. Blair8525e2b2017-03-15 14:05:47 -0700559 self.source_context = source_context
James E. Blair01f83b72017-03-15 13:03:40 -0700560 # The secret data may or may not be encrypted. This attribute
561 # is named 'secret_data' to make it easy to search for and
562 # spot where it is directly used.
563 self.secret_data = {}
564
565 def __ne__(self, other):
566 return not self.__eq__(other)
567
568 def __eq__(self, other):
569 if not isinstance(other, Secret):
570 return False
571 return (self.name == other.name and
James E. Blair8525e2b2017-03-15 14:05:47 -0700572 self.source_context == other.source_context and
James E. Blair01f83b72017-03-15 13:03:40 -0700573 self.secret_data == other.secret_data)
574
575 def __repr__(self):
576 return '<Secret %s>' % (self.name,)
577
James E. Blair18f86a32017-03-15 14:43:26 -0700578 def decrypt(self, private_key):
579 """Return a copy of this secret with any encrypted data decrypted.
580 Note that the original remains encrypted."""
581
582 r = copy.deepcopy(self)
583 decrypted_secret_data = {}
584 for k, v in r.secret_data.items():
585 if hasattr(v, 'decrypt'):
586 decrypted_secret_data[k] = v.decrypt(private_key)
587 else:
588 decrypted_secret_data[k] = v
589 r.secret_data = decrypted_secret_data
590 return r
591
James E. Blair01f83b72017-03-15 13:03:40 -0700592
James E. Blaircdab2032017-02-01 09:09:29 -0800593class SourceContext(object):
594 """A reference to the branch of a project in configuration.
595
596 Jobs and playbooks reference this to keep track of where they
597 originate."""
598
James E. Blair6f140c72017-03-03 10:32:07 -0800599 def __init__(self, project, branch, path, trusted):
James E. Blaircdab2032017-02-01 09:09:29 -0800600 self.project = project
601 self.branch = branch
James E. Blair6f140c72017-03-03 10:32:07 -0800602 self.path = path
Monty Taylore6562aa2017-02-20 07:37:39 -0500603 self.trusted = trusted
James E. Blaircdab2032017-02-01 09:09:29 -0800604
James E. Blair6f140c72017-03-03 10:32:07 -0800605 def __str__(self):
606 return '%s/%s@%s' % (self.project, self.path, self.branch)
607
James E. Blaircdab2032017-02-01 09:09:29 -0800608 def __repr__(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800609 return '<SourceContext %s trusted:%s>' % (str(self),
610 self.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800611
James E. Blaira7f51ca2017-02-07 16:01:26 -0800612 def __deepcopy__(self, memo):
613 return self.copy()
614
615 def copy(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800616 return self.__class__(self.project, self.branch, self.path,
617 self.trusted)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800618
James E. Blaircdab2032017-02-01 09:09:29 -0800619 def __ne__(self, other):
620 return not self.__eq__(other)
621
622 def __eq__(self, other):
623 if not isinstance(other, SourceContext):
624 return False
625 return (self.project == other.project and
626 self.branch == other.branch and
James E. Blair6f140c72017-03-03 10:32:07 -0800627 self.path == other.path and
Monty Taylore6562aa2017-02-20 07:37:39 -0500628 self.trusted == other.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800629
630
James E. Blair66b274e2017-01-31 14:47:52 -0800631class PlaybookContext(object):
James E. Blaircdab2032017-02-01 09:09:29 -0800632
James E. Blair66b274e2017-01-31 14:47:52 -0800633 """A reference to a playbook in the context of a project.
634
635 Jobs refer to objects of this class for their main, pre, and post
636 playbooks so that we can keep track of which repos and security
637 contexts are needed in order to run them."""
638
James E. Blaircdab2032017-02-01 09:09:29 -0800639 def __init__(self, source_context, path):
640 self.source_context = source_context
James E. Blair66b274e2017-01-31 14:47:52 -0800641 self.path = path
James E. Blair66b274e2017-01-31 14:47:52 -0800642
643 def __repr__(self):
James E. Blaircdab2032017-02-01 09:09:29 -0800644 return '<PlaybookContext %s %s>' % (self.source_context,
645 self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800646
647 def __ne__(self, other):
648 return not self.__eq__(other)
649
650 def __eq__(self, other):
651 if not isinstance(other, PlaybookContext):
652 return False
James E. Blaircdab2032017-02-01 09:09:29 -0800653 return (self.source_context == other.source_context and
654 self.path == other.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800655
656 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400657 # Render to a dict to use in passing json to the executor
James E. Blair66b274e2017-01-31 14:47:52 -0800658 return dict(
James E. Blaircdab2032017-02-01 09:09:29 -0800659 connection=self.source_context.project.connection_name,
660 project=self.source_context.project.name,
661 branch=self.source_context.branch,
Monty Taylore6562aa2017-02-20 07:37:39 -0500662 trusted=self.source_context.trusted,
James E. Blaircdab2032017-02-01 09:09:29 -0800663 path=self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800664
665
Monty Taylorb934c1a2017-06-16 19:31:47 -0500666class Role(object, metaclass=abc.ABCMeta):
James E. Blair5ac93842017-01-20 06:47:34 -0800667 """A reference to an ansible role."""
668
669 def __init__(self, target_name):
670 self.target_name = target_name
671
672 @abc.abstractmethod
673 def __repr__(self):
674 pass
675
676 def __ne__(self, other):
677 return not self.__eq__(other)
678
679 @abc.abstractmethod
680 def __eq__(self, other):
681 if not isinstance(other, Role):
682 return False
683 return (self.target_name == other.target_name)
684
685 @abc.abstractmethod
686 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400687 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800688 return dict(target_name=self.target_name)
689
690
691class ZuulRole(Role):
692 """A reference to an ansible role in a Zuul project."""
693
James E. Blair6563e4b2017-04-28 08:14:48 -0700694 def __init__(self, target_name, connection_name, project_name):
James E. Blair5ac93842017-01-20 06:47:34 -0800695 super(ZuulRole, self).__init__(target_name)
696 self.connection_name = connection_name
697 self.project_name = project_name
James E. Blair5ac93842017-01-20 06:47:34 -0800698
699 def __repr__(self):
700 return '<ZuulRole %s %s>' % (self.project_name, self.target_name)
701
Clint Byrumaf7438f2017-05-10 17:26:57 -0400702 __hash__ = object.__hash__
703
James E. Blair5ac93842017-01-20 06:47:34 -0800704 def __eq__(self, other):
705 if not isinstance(other, ZuulRole):
706 return False
707 return (super(ZuulRole, self).__eq__(other) and
708 self.connection_name == other.connection_name,
James E. Blair6563e4b2017-04-28 08:14:48 -0700709 self.project_name == other.project_name)
James E. Blair5ac93842017-01-20 06:47:34 -0800710
711 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400712 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800713 d = super(ZuulRole, self).toDict()
714 d['type'] = 'zuul'
715 d['connection'] = self.connection_name
716 d['project'] = self.project_name
James E. Blair5ac93842017-01-20 06:47:34 -0800717 return d
718
719
James E. Blair8525e2b2017-03-15 14:05:47 -0700720class AuthContext(object):
721 """The authentication information for a job.
722
723 Authentication information (both the actual data and metadata such
724 as whether it should be inherited) for a job is grouped together
725 in this object.
726 """
727
728 def __init__(self, inherit=False):
729 self.inherit = inherit
730 self.secrets = []
731
732 def __ne__(self, other):
733 return not self.__eq__(other)
734
735 def __eq__(self, other):
736 if not isinstance(other, AuthContext):
737 return False
738 return (self.inherit == other.inherit and
739 self.secrets == other.secrets)
740
741
James E. Blairee743612012-05-29 14:49:32 -0700742class Job(object):
James E. Blair66b274e2017-01-31 14:47:52 -0800743
James E. Blaira7f51ca2017-02-07 16:01:26 -0800744 """A Job represents the defintion of actions to perform.
745
James E. Blaird4ade8c2017-02-19 15:25:46 -0800746 A Job is an abstract configuration concept. It describes what,
747 where, and under what circumstances something should be run
748 (contrast this with Build which is a concrete single execution of
749 a Job).
750
James E. Blaira7f51ca2017-02-07 16:01:26 -0800751 NB: Do not modify attributes of this class, set them directly
752 (e.g., "job.run = ..." rather than "job.run.append(...)").
753 """
Monty Taylora42a55b2016-07-29 07:53:33 -0700754
James E. Blairee743612012-05-29 14:49:32 -0700755 def __init__(self, name):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800756 # These attributes may override even the final form of a job
757 # in the context of a project-pipeline. They can not affect
758 # the execution of the job, but only whether the job is run
759 # and how it is reported.
760 self.context_attributes = dict(
761 voting=True,
762 hold_following_changes=False,
James E. Blair1774dd52017-02-03 10:52:32 -0800763 failure_message=None,
764 success_message=None,
765 failure_url=None,
766 success_url=None,
767 # Matchers. These are separate so they can be individually
768 # overidden.
769 branch_matcher=None,
770 file_matcher=None,
771 irrelevant_file_matcher=None, # skip-if
James E. Blaira7f51ca2017-02-07 16:01:26 -0800772 tags=frozenset(),
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200773 dependencies=frozenset(),
James E. Blair1774dd52017-02-03 10:52:32 -0800774 )
775
James E. Blaira7f51ca2017-02-07 16:01:26 -0800776 # These attributes affect how the job is actually run and more
777 # care must be taken when overriding them. If a job is
778 # declared "final", these may not be overriden in a
779 # project-pipeline.
780 self.execution_attributes = dict(
781 timeout=None,
James E. Blair490cf042017-02-24 23:07:21 -0500782 variables={},
James E. Blaira7f51ca2017-02-07 16:01:26 -0800783 nodeset=NodeSet(),
James E. Blair8525e2b2017-03-15 14:05:47 -0700784 auth=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800785 workspace=None,
786 pre_run=(),
787 post_run=(),
788 run=(),
789 implied_run=(),
Tobias Henkel9a0e1942017-03-20 16:16:02 +0100790 semaphore=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800791 attempts=3,
792 final=False,
James E. Blair5ac93842017-01-20 06:47:34 -0800793 roles=frozenset(),
James E. Blair912322f2017-05-23 13:11:25 -0700794 required_projects={},
James E. Blairb3f5db12017-03-17 12:57:39 -0700795 allowed_projects=None,
James E. Blair7391ecb2017-05-23 13:32:40 -0700796 override_branch=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800797 )
798
799 # These are generally internal attributes which are not
800 # accessible via configuration.
801 self.other_attributes = dict(
802 name=None,
803 source_context=None,
804 inheritance_path=(),
805 )
806
807 self.inheritable_attributes = {}
808 self.inheritable_attributes.update(self.context_attributes)
809 self.inheritable_attributes.update(self.execution_attributes)
810 self.attributes = {}
811 self.attributes.update(self.inheritable_attributes)
812 self.attributes.update(self.other_attributes)
813
James E. Blairee743612012-05-29 14:49:32 -0700814 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800815
James E. Blair66b274e2017-01-31 14:47:52 -0800816 def __ne__(self, other):
817 return not self.__eq__(other)
818
Paul Belangere22baea2016-11-03 16:59:27 -0400819 def __eq__(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800820 # Compare the name and all inheritable attributes to determine
821 # whether two jobs with the same name are identically
822 # configured. Useful upon reconfiguration.
823 if not isinstance(other, Job):
824 return False
825 if self.name != other.name:
826 return False
827 for k, v in self.attributes.items():
828 if getattr(self, k) != getattr(other, k):
829 return False
830 return True
James E. Blairee743612012-05-29 14:49:32 -0700831
Clint Byrumaf7438f2017-05-10 17:26:57 -0400832 __hash__ = object.__hash__
833
James E. Blairee743612012-05-29 14:49:32 -0700834 def __str__(self):
835 return self.name
836
837 def __repr__(self):
James E. Blair72ae9da2017-02-03 14:21:30 -0800838 return '<Job %s branches: %s source: %s>' % (self.name,
839 self.branch_matcher,
840 self.source_context)
James E. Blair83005782015-12-11 14:46:03 -0800841
James E. Blaira7f51ca2017-02-07 16:01:26 -0800842 def __getattr__(self, name):
843 v = self.__dict__.get(name)
844 if v is None:
845 return copy.deepcopy(self.attributes[name])
846 return v
847
848 def _get(self, name):
849 return self.__dict__.get(name)
850
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100851 def getSafeAttributes(self):
852 return Attributes(name=self.name)
853
James E. Blaira7f51ca2017-02-07 16:01:26 -0800854 def setRun(self):
855 if not self.run:
856 self.run = self.implied_run
857
James E. Blair490cf042017-02-24 23:07:21 -0500858 def updateVariables(self, other_vars):
859 v = self.variables
860 Job._deepUpdate(v, other_vars)
861 self.variables = v
862
James E. Blair912322f2017-05-23 13:11:25 -0700863 def updateProjects(self, other_projects):
864 required_projects = self.required_projects
865 Job._deepUpdate(required_projects, other_projects)
866 self.required_projects = required_projects
James E. Blair27f3dfc2017-05-23 13:07:28 -0700867
James E. Blair490cf042017-02-24 23:07:21 -0500868 @staticmethod
869 def _deepUpdate(a, b):
870 # Merge nested dictionaries if possible, otherwise, overwrite
871 # the value in 'a' with the value in 'b'.
872 for k, bv in b.items():
873 av = a.get(k)
874 if isinstance(av, dict) and isinstance(bv, dict):
875 Job._deepUpdate(av, bv)
876 else:
877 a[k] = bv
878
James E. Blaira7f51ca2017-02-07 16:01:26 -0800879 def inheritFrom(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800880 """Copy the inheritable attributes which have been set on the other
881 job to this job."""
James E. Blaira7f51ca2017-02-07 16:01:26 -0800882 if not isinstance(other, Job):
883 raise Exception("Job unable to inherit from %s" % (other,))
884
885 do_not_inherit = set()
James E. Blair8525e2b2017-03-15 14:05:47 -0700886 if other.auth and not other.auth.inherit:
James E. Blaira7f51ca2017-02-07 16:01:26 -0800887 do_not_inherit.add('auth')
888
889 # copy all attributes
890 for k in self.inheritable_attributes:
891 if (other._get(k) is not None and k not in do_not_inherit):
892 setattr(self, k, copy.deepcopy(getattr(other, k)))
893
894 msg = 'inherit from %s' % (repr(other),)
895 self.inheritance_path = other.inheritance_path + (msg,)
896
897 def copy(self):
898 job = Job(self.name)
899 for k in self.attributes:
900 if self._get(k) is not None:
901 setattr(job, k, copy.deepcopy(self._get(k)))
902 return job
903
904 def applyVariant(self, other):
905 """Copy the attributes which have been set on the other job to this
906 job."""
James E. Blair83005782015-12-11 14:46:03 -0800907
908 if not isinstance(other, Job):
909 raise Exception("Job unable to inherit from %s" % (other,))
James E. Blaira7f51ca2017-02-07 16:01:26 -0800910
911 for k in self.execution_attributes:
912 if (other._get(k) is not None and
913 k not in set(['final'])):
914 if self.final:
915 raise Exception("Unable to modify final job %s attribute "
916 "%s=%s with variant %s" % (
917 repr(self), k, other._get(k),
918 repr(other)))
James E. Blair27f3dfc2017-05-23 13:07:28 -0700919 if k not in set(['pre_run', 'post_run', 'roles', 'variables',
James E. Blair912322f2017-05-23 13:11:25 -0700920 'required_projects']):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800921 setattr(self, k, copy.deepcopy(other._get(k)))
922
923 # Don't set final above so that we don't trip an error halfway
924 # through assignment.
925 if other.final != self.attributes['final']:
926 self.final = other.final
927
928 if other._get('pre_run') is not None:
929 self.pre_run = self.pre_run + other.pre_run
930 if other._get('post_run') is not None:
931 self.post_run = other.post_run + self.post_run
James E. Blair5ac93842017-01-20 06:47:34 -0800932 if other._get('roles') is not None:
933 self.roles = self.roles.union(other.roles)
James E. Blair490cf042017-02-24 23:07:21 -0500934 if other._get('variables') is not None:
935 self.updateVariables(other.variables)
James E. Blair912322f2017-05-23 13:11:25 -0700936 if other._get('required_projects') is not None:
937 self.updateProjects(other.required_projects)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800938
939 for k in self.context_attributes:
940 if (other._get(k) is not None and
941 k not in set(['tags'])):
942 setattr(self, k, copy.deepcopy(other._get(k)))
943
944 if other._get('tags') is not None:
945 self.tags = self.tags.union(other.tags)
946
947 msg = 'apply variant %s' % (repr(other),)
948 self.inheritance_path = self.inheritance_path + (msg,)
James E. Blairee743612012-05-29 14:49:32 -0700949
James E. Blaire421a232012-07-25 16:59:21 -0700950 def changeMatches(self, change):
James E. Blair83005782015-12-11 14:46:03 -0800951 if self.branch_matcher and not self.branch_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800952 return False
953
James E. Blair83005782015-12-11 14:46:03 -0800954 if self.file_matcher and not self.file_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800955 return False
956
James E. Blair83005782015-12-11 14:46:03 -0800957 # NB: This is a negative match.
958 if (self.irrelevant_file_matcher and
959 self.irrelevant_file_matcher.matches(change)):
Maru Newby3fe5f852015-01-13 04:22:14 +0000960 return False
961
James E. Blair70c71582013-03-06 08:50:50 -0800962 return True
James E. Blaire5a847f2012-07-10 15:29:14 -0700963
James E. Blair1e8dd892012-05-30 09:15:05 -0700964
James E. Blair912322f2017-05-23 13:11:25 -0700965class JobProject(object):
James E. Blair27f3dfc2017-05-23 13:07:28 -0700966 """ A reference to a project from a job. """
967
968 def __init__(self, project_name, override_branch=None):
969 self.project_name = project_name
970 self.override_branch = override_branch
971
972
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200973class JobList(object):
974 """ A list of jobs in a project's pipeline. """
Monty Taylora42a55b2016-07-29 07:53:33 -0700975
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200976 def __init__(self):
977 self.jobs = OrderedDict() # job.name -> [job, ...]
James E. Blair12748b52017-02-07 17:17:53 -0800978
James E. Blairee743612012-05-29 14:49:32 -0700979 def addJob(self, job):
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200980 if job.name in self.jobs:
981 self.jobs[job.name].append(job)
982 else:
983 self.jobs[job.name] = [job]
James E. Blairee743612012-05-29 14:49:32 -0700984
James E. Blaira7f51ca2017-02-07 16:01:26 -0800985 def inheritFrom(self, other):
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200986 for jobname, jobs in other.jobs.items():
987 if jobname in self.jobs:
Jesse Keatingd1f434a2017-05-16 20:28:35 -0700988 self.jobs[jobname].extend(jobs)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800989 else:
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200990 self.jobs[jobname] = jobs
991
992
993class JobGraph(object):
994 """ A JobGraph represents the dependency graph between Job."""
995
996 def __init__(self):
997 self.jobs = OrderedDict() # job_name -> Job
998 self._dependencies = {} # dependent_job_name -> set(parent_job_names)
999
1000 def __repr__(self):
1001 return '<JobGraph %s>' % (self.jobs)
1002
1003 def addJob(self, job):
1004 # A graph must be created after the job list is frozen,
1005 # therefore we should only get one job with the same name.
1006 if job.name in self.jobs:
1007 raise Exception("Job %s already added" % (job.name,))
1008 self.jobs[job.name] = job
1009 # Append the dependency information
1010 self._dependencies.setdefault(job.name, set())
1011 try:
1012 for dependency in job.dependencies:
1013 # Make sure a circular dependency is never created
1014 ancestor_jobs = self._getParentJobNamesRecursively(
1015 dependency, soft=True)
1016 ancestor_jobs.add(dependency)
1017 if any((job.name == anc_job) for anc_job in ancestor_jobs):
1018 raise Exception("Dependency cycle detected in job %s" %
1019 (job.name,))
1020 self._dependencies[job.name].add(dependency)
1021 except Exception:
1022 del self.jobs[job.name]
1023 del self._dependencies[job.name]
1024 raise
1025
1026 def getJobs(self):
Clint Byruma4471d12017-05-10 20:57:40 -04001027 return list(self.jobs.values()) # Report in the order of layout cfg
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001028
1029 def _getDirectDependentJobs(self, parent_job):
1030 ret = set()
1031 for dependent_name, parent_names in self._dependencies.items():
1032 if parent_job in parent_names:
1033 ret.add(dependent_name)
1034 return ret
1035
1036 def getDependentJobsRecursively(self, parent_job):
1037 all_dependent_jobs = set()
1038 jobs_to_iterate = set([parent_job])
1039 while len(jobs_to_iterate) > 0:
1040 current_job = jobs_to_iterate.pop()
1041 current_dependent_jobs = self._getDirectDependentJobs(current_job)
1042 new_dependent_jobs = current_dependent_jobs - all_dependent_jobs
1043 jobs_to_iterate |= new_dependent_jobs
1044 all_dependent_jobs |= new_dependent_jobs
1045 return [self.jobs[name] for name in all_dependent_jobs]
1046
1047 def getParentJobsRecursively(self, dependent_job):
1048 return [self.jobs[name] for name in
1049 self._getParentJobNamesRecursively(dependent_job)]
1050
1051 def _getParentJobNamesRecursively(self, dependent_job, soft=False):
1052 all_parent_jobs = set()
1053 jobs_to_iterate = set([dependent_job])
1054 while len(jobs_to_iterate) > 0:
1055 current_job = jobs_to_iterate.pop()
1056 current_parent_jobs = self._dependencies.get(current_job)
1057 if current_parent_jobs is None:
1058 if soft:
1059 current_parent_jobs = set()
1060 else:
1061 raise Exception("Dependent job %s not found: " %
1062 (dependent_job,))
1063 new_parent_jobs = current_parent_jobs - all_parent_jobs
1064 jobs_to_iterate |= new_parent_jobs
1065 all_parent_jobs |= new_parent_jobs
1066 return all_parent_jobs
James E. Blairb97ed802015-12-21 15:55:35 -08001067
James E. Blair1e8dd892012-05-30 09:15:05 -07001068
James E. Blair4aea70c2012-07-26 14:23:24 -07001069class Build(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001070 """A Build is an instance of a single execution of a Job.
1071
1072 While a Job describes what to run, a Build describes an actual
1073 execution of that Job. Each build is associated with exactly one
1074 Job (related builds are grouped together in a BuildSet).
1075 """
Monty Taylora42a55b2016-07-29 07:53:33 -07001076
James E. Blair4aea70c2012-07-26 14:23:24 -07001077 def __init__(self, job, uuid):
1078 self.job = job
1079 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -07001080 self.url = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001081 self.result = None
1082 self.build_set = None
Paul Belanger174a8272017-03-14 13:20:10 -04001083 self.execute_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -08001084 self.start_time = None
1085 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -07001086 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -07001087 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -07001088 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -07001089 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -07001090 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +08001091 self.worker = Worker()
Timothy Chavezb2332082015-08-07 20:08:04 -05001092 self.node_labels = []
1093 self.node_name = None
James E. Blairee743612012-05-29 14:49:32 -07001094
1095 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +08001096 return ('<Build %s of %s on %s>' %
1097 (self.uuid, self.job.name, self.worker))
1098
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001099 def getSafeAttributes(self):
1100 return Attributes(uuid=self.uuid)
1101
Joshua Heskethba8776a2014-01-12 14:35:40 +08001102
1103class Worker(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001104 """Information about the specific worker executing a Build."""
Joshua Heskethba8776a2014-01-12 14:35:40 +08001105 def __init__(self):
1106 self.name = "Unknown"
1107 self.hostname = None
Monty Taylor0dbe1592017-06-11 10:57:27 -05001108 self.log_port = None
Joshua Heskethba8776a2014-01-12 14:35:40 +08001109
1110 def updateFromData(self, data):
1111 """Update worker information if contained in the WORK_DATA response."""
1112 self.name = data.get('worker_name', self.name)
1113 self.hostname = data.get('worker_hostname', self.hostname)
Monty Taylor0dbe1592017-06-11 10:57:27 -05001114 self.log_port = data.get('worker_log_port', self.log_port)
Joshua Heskethba8776a2014-01-12 14:35:40 +08001115
1116 def __repr__(self):
1117 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -07001118
James E. Blair1e8dd892012-05-30 09:15:05 -07001119
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001120class RepoFiles(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001121 """RepoFiles holds config-file content for per-project job config.
1122
1123 When Zuul asks a merger to prepare a future multiple-repo state
1124 and collect Zuul configuration files so that we can dynamically
1125 load our configuration, this class provides cached access to that
1126 data for use by the Change which updated the config files and any
1127 changes that follow it in a ChangeQueue.
1128
1129 It is attached to a BuildSet since the content of Zuul
1130 configuration files can change with each new BuildSet.
1131 """
1132
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001133 def __init__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001134 self.connections = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001135
1136 def __repr__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001137 return '<RepoFiles %s>' % self.connections
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001138
1139 def setFiles(self, items):
James E. Blair2a535672017-04-27 12:03:15 -07001140 self.hostnames = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001141 for item in items:
James E. Blair2a535672017-04-27 12:03:15 -07001142 connection = self.connections.setdefault(
1143 item['connection'], {})
1144 project = connection.setdefault(item['project'], {})
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001145 branch = project.setdefault(item['branch'], {})
1146 branch.update(item['files'])
1147
James E. Blair2a535672017-04-27 12:03:15 -07001148 def getFile(self, connection_name, project_name, branch, fn):
1149 host = self.connections.get(connection_name, {})
1150 return host.get(project_name, {}).get(branch, {}).get(fn)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001151
1152
James E. Blair7e530ad2012-07-03 16:12:28 -07001153class BuildSet(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001154 """A collection of Builds for one specific potential future repository
1155 state.
Monty Taylora42a55b2016-07-29 07:53:33 -07001156
Paul Belanger174a8272017-03-14 13:20:10 -04001157 When Zuul executes Builds for a change, it creates a Build to
James E. Blaird4ade8c2017-02-19 15:25:46 -08001158 represent each execution of each job and a BuildSet to keep track
Paul Belanger174a8272017-03-14 13:20:10 -04001159 of all the Builds running for that Change. When Zuul re-executes
James E. Blaird4ade8c2017-02-19 15:25:46 -08001160 Builds for a Change with a different configuration, all of the
1161 running Builds in the BuildSet for that change are aborted, and a
1162 new BuildSet is created to hold the Builds for the Jobs being
1163 run with the new configuration.
1164
1165 A BuildSet also holds the UUID used to produce the Zuul Ref that
1166 builders check out.
1167
Monty Taylora42a55b2016-07-29 07:53:33 -07001168 """
James E. Blair4076e2b2014-01-28 12:42:20 -08001169 # Merge states:
1170 NEW = 1
1171 PENDING = 2
1172 COMPLETE = 3
1173
Antoine Musso9b229282014-08-18 23:45:43 +02001174 states_map = {
1175 1: 'NEW',
1176 2: 'PENDING',
1177 3: 'COMPLETE',
1178 }
1179
James E. Blairfee8d652013-06-07 08:57:52 -07001180 def __init__(self, item):
1181 self.item = item
James E. Blair7e530ad2012-07-03 16:12:28 -07001182 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -07001183 self.result = None
1184 self.next_build_set = None
1185 self.previous_build_set = None
Jamie Lennox3f16de52017-05-09 14:24:11 +10001186 self.uuid = None
James E. Blair81515ad2012-10-01 18:29:08 -07001187 self.commit = None
James E. Blair4076e2b2014-01-28 12:42:20 -08001188 self.zuul_url = None
James E. Blair1960d682017-04-28 15:44:14 -07001189 self.dependent_items = None
1190 self.merger_items = None
James E. Blair973721f2012-08-15 10:19:43 -07001191 self.unable_to_merge = False
James E. Blaire53250c2017-03-01 14:34:36 -08001192 self.config_error = None # None or an error message string.
James E. Blair972e3c72013-08-29 12:04:55 -07001193 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -08001194 self.merge_state = self.NEW
James E. Blair0eaad552016-09-02 12:09:54 -07001195 self.nodesets = {} # job -> nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001196 self.node_requests = {} # job -> reqs
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001197 self.files = RepoFiles()
James E. Blair1960d682017-04-28 15:44:14 -07001198 self.repo_state = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001199 self.layout = None
Paul Belanger71d98172016-11-08 10:56:31 -05001200 self.tries = {}
James E. Blair7e530ad2012-07-03 16:12:28 -07001201
Jamie Lennox3f16de52017-05-09 14:24:11 +10001202 @property
1203 def ref(self):
1204 # NOTE(jamielennox): The concept of buildset ref is to be removed and a
1205 # buildset UUID identifier available instead. Currently the ref is
1206 # checked to see if the BuildSet has been configured.
1207 return 'Z' + self.uuid if self.uuid else None
1208
Antoine Musso9b229282014-08-18 23:45:43 +02001209 def __repr__(self):
1210 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
1211 self.item,
1212 len(self.builds),
1213 self.getStateName(self.merge_state))
1214
James E. Blair4886cc12012-07-18 15:39:41 -07001215 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -07001216 # The change isn't enqueued until after it's created
1217 # so we don't know what the other changes ahead will be
1218 # until jobs start.
James E. Blair1960d682017-04-28 15:44:14 -07001219 if self.dependent_items is None:
1220 items = []
James E. Blairfee8d652013-06-07 08:57:52 -07001221 next_item = self.item.item_ahead
1222 while next_item:
James E. Blair1960d682017-04-28 15:44:14 -07001223 items.append(next_item)
James E. Blairfee8d652013-06-07 08:57:52 -07001224 next_item = next_item.item_ahead
James E. Blair1960d682017-04-28 15:44:14 -07001225 self.dependent_items = items
Jamie Lennox3f16de52017-05-09 14:24:11 +10001226 if not self.uuid:
1227 self.uuid = uuid4().hex
James E. Blair1960d682017-04-28 15:44:14 -07001228 if self.merger_items is None:
1229 items = [self.item] + self.dependent_items
1230 items.reverse()
1231 self.merger_items = [i.makeMergerItem() for i in items]
James E. Blair4886cc12012-07-18 15:39:41 -07001232
Antoine Musso9b229282014-08-18 23:45:43 +02001233 def getStateName(self, state_num):
1234 return self.states_map.get(
1235 state_num, 'UNKNOWN (%s)' % state_num)
1236
James E. Blair4886cc12012-07-18 15:39:41 -07001237 def addBuild(self, build):
1238 self.builds[build.job.name] = build
Paul Belanger71d98172016-11-08 10:56:31 -05001239 if build.job.name not in self.tries:
James E. Blair5aed1112016-12-15 09:18:05 -08001240 self.tries[build.job.name] = 1
James E. Blair4886cc12012-07-18 15:39:41 -07001241 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -07001242
James E. Blair4a28a882013-08-23 15:17:33 -07001243 def removeBuild(self, build):
Paul Belanger71d98172016-11-08 10:56:31 -05001244 self.tries[build.job.name] += 1
James E. Blair4a28a882013-08-23 15:17:33 -07001245 del self.builds[build.job.name]
1246
James E. Blair7e530ad2012-07-03 16:12:28 -07001247 def getBuild(self, job_name):
1248 return self.builds.get(job_name)
1249
James E. Blair11700c32012-07-05 17:50:05 -07001250 def getBuilds(self):
Clint Byrum1d0c7d12017-05-10 19:40:53 -07001251 keys = list(self.builds.keys())
James E. Blair11700c32012-07-05 17:50:05 -07001252 keys.sort()
1253 return [self.builds.get(x) for x in keys]
1254
James E. Blair0eaad552016-09-02 12:09:54 -07001255 def getJobNodeSet(self, job_name):
1256 # Return None if not provisioned; empty NodeSet if no nodes
1257 # required
1258 return self.nodesets.get(job_name)
James E. Blair8d692392016-04-08 17:47:58 -07001259
James E. Blaire18d4602017-01-05 11:17:28 -08001260 def removeJobNodeSet(self, job_name):
1261 if job_name not in self.nodesets:
1262 raise Exception("No job set for %s" % (job_name))
1263 del self.nodesets[job_name]
1264
James E. Blair8d692392016-04-08 17:47:58 -07001265 def setJobNodeRequest(self, job_name, req):
1266 if job_name in self.node_requests:
1267 raise Exception("Prior node request for %s" % (job_name))
1268 self.node_requests[job_name] = req
1269
1270 def getJobNodeRequest(self, job_name):
1271 return self.node_requests.get(job_name)
1272
James E. Blair0eaad552016-09-02 12:09:54 -07001273 def jobNodeRequestComplete(self, job_name, req, nodeset):
1274 if job_name in self.nodesets:
James E. Blair8d692392016-04-08 17:47:58 -07001275 raise Exception("Prior node request for %s" % (job_name))
James E. Blair0eaad552016-09-02 12:09:54 -07001276 self.nodesets[job_name] = nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001277 del self.node_requests[job_name]
1278
Paul Belanger71d98172016-11-08 10:56:31 -05001279 def getTries(self, job_name):
Clint Byrum804073b2017-05-10 17:47:45 -04001280 return self.tries.get(job_name, 0)
Paul Belanger71d98172016-11-08 10:56:31 -05001281
James E. Blair0ffa0102017-03-30 13:11:33 -07001282 def getMergeMode(self):
James E. Blair1960d682017-04-28 15:44:14 -07001283 # We may be called before this build set has a shadow layout
1284 # (ie, we are called to perform the merge to create that
1285 # layout). It's possible that the change we are merging will
1286 # update the merge-mode for the project, but there's not much
1287 # we can do about that here. Instead, do the best we can by
1288 # using the nearest shadow layout to determine the merge mode,
1289 # or if that fails, the current live layout, or if that fails,
1290 # use the default: merge-resolve.
1291 item = self.item
1292 layout = None
1293 while item:
1294 layout = item.current_build_set.layout
1295 if layout:
1296 break
1297 item = item.item_ahead
1298 if not layout:
1299 layout = self.item.pipeline.layout
1300 if layout:
James E. Blair0ffa0102017-03-30 13:11:33 -07001301 project = self.item.change.project
James E. Blair1960d682017-04-28 15:44:14 -07001302 project_config = layout.project_configs.get(
James E. Blair0ffa0102017-03-30 13:11:33 -07001303 project.canonical_name)
1304 if project_config:
1305 return project_config.merge_mode
1306 return MERGER_MERGE_RESOLVE
Adam Gandelman8bd57102016-12-02 12:58:42 -08001307
Jamie Lennox3f16de52017-05-09 14:24:11 +10001308 def getSafeAttributes(self):
1309 return Attributes(uuid=self.uuid)
1310
James E. Blair7e530ad2012-07-03 16:12:28 -07001311
James E. Blairfee8d652013-06-07 08:57:52 -07001312class QueueItem(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001313 """Represents the position of a Change in a ChangeQueue.
1314
1315 All Changes are enqueued into ChangeQueue in a QueueItem. The QueueItem
1316 holds the current `BuildSet` as well as all previous `BuildSets` that were
1317 produced for this `QueueItem`.
1318 """
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001319 log = logging.getLogger("zuul.QueueItem")
James E. Blair32663402012-06-01 10:04:18 -07001320
James E. Blairbfb8e042014-12-30 17:01:44 -08001321 def __init__(self, queue, change):
1322 self.pipeline = queue.pipeline
1323 self.queue = queue
Monty Taylor55d0f562017-05-17 14:30:21 -05001324 self.change = change # a ref
James E. Blair7e530ad2012-07-03 16:12:28 -07001325 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -07001326 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -07001327 self.current_build_set = BuildSet(self)
1328 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -07001329 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -07001330 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -08001331 self.enqueue_time = None
1332 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -07001333 self.reported = False
Tobias Henkel9842bd72017-05-16 13:40:03 +02001334 self.reported_start = False
1335 self.quiet = False
James E. Blairbfb8e042014-12-30 17:01:44 -08001336 self.active = False # Whether an item is within an active window
1337 self.live = True # Whether an item is intended to be processed at all
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001338 self.job_graph = None
James E. Blaire5a847f2012-07-10 15:29:14 -07001339
James E. Blair972e3c72013-08-29 12:04:55 -07001340 def __repr__(self):
1341 if self.pipeline:
1342 pipeline = self.pipeline.name
1343 else:
1344 pipeline = None
1345 return '<QueueItem 0x%x for %s in %s>' % (
1346 id(self), self.change, pipeline)
1347
James E. Blairee743612012-05-29 14:49:32 -07001348 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -07001349 old = self.current_build_set
1350 self.current_build_set.result = 'CANCELED'
1351 self.current_build_set = BuildSet(self)
1352 old.next_build_set = self.current_build_set
1353 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -07001354 self.build_sets.append(self.current_build_set)
James E. Blairee743612012-05-29 14:49:32 -07001355
1356 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -07001357 self.current_build_set.addBuild(build)
James E. Blair66eeebf2013-07-27 17:44:32 -07001358 build.pipeline = self.pipeline
James E. Blairee743612012-05-29 14:49:32 -07001359
James E. Blair4a28a882013-08-23 15:17:33 -07001360 def removeBuild(self, build):
1361 self.current_build_set.removeBuild(build)
1362
James E. Blairfee8d652013-06-07 08:57:52 -07001363 def setReportedResult(self, result):
1364 self.current_build_set.result = result
1365
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001366 def freezeJobGraph(self):
James E. Blair83005782015-12-11 14:46:03 -08001367 """Find or create actual matching jobs for this item's change and
1368 store the resulting job tree."""
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001369 layout = self.current_build_set.layout
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001370 job_graph = layout.createJobGraph(self)
1371 for job in job_graph.getJobs():
1372 # Ensure that each jobs's dependencies are fully
1373 # accessible. This will raise an exception if not.
1374 job_graph.getParentJobsRecursively(job.name)
1375 self.job_graph = job_graph
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001376
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001377 def hasJobGraph(self):
1378 """Returns True if the item has a job graph."""
1379 return self.job_graph is not None
James E. Blair83005782015-12-11 14:46:03 -08001380
1381 def getJobs(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001382 if not self.live or not self.job_graph:
James E. Blair83005782015-12-11 14:46:03 -08001383 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001384 return self.job_graph.getJobs()
1385
1386 def getJob(self, name):
1387 if not self.job_graph:
1388 return None
1389 return self.job_graph.jobs.get(name)
James E. Blair83005782015-12-11 14:46:03 -08001390
James E. Blairdbfd3282016-07-21 10:46:19 -07001391 def haveAllJobsStarted(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001392 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001393 return False
1394 for job in self.getJobs():
1395 build = self.current_build_set.getBuild(job.name)
1396 if not build or not build.start_time:
1397 return False
1398 return True
1399
1400 def areAllJobsComplete(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001401 if (self.current_build_set.config_error or
1402 self.current_build_set.unable_to_merge):
1403 return True
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001404 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001405 return False
1406 for job in self.getJobs():
1407 build = self.current_build_set.getBuild(job.name)
1408 if not build or not build.result:
1409 return False
1410 return True
1411
1412 def didAllJobsSucceed(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001413 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001414 return False
1415 for job in self.getJobs():
1416 if not job.voting:
1417 continue
1418 build = self.current_build_set.getBuild(job.name)
1419 if not build:
1420 return False
1421 if build.result != 'SUCCESS':
1422 return False
1423 return True
1424
1425 def didAnyJobFail(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001426 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001427 return False
1428 for job in self.getJobs():
1429 if not job.voting:
1430 continue
1431 build = self.current_build_set.getBuild(job.name)
1432 if build and build.result and (build.result != 'SUCCESS'):
1433 return True
1434 return False
1435
1436 def didMergerFail(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001437 return self.current_build_set.unable_to_merge
1438
1439 def getConfigError(self):
1440 return self.current_build_set.config_error
James E. Blairdbfd3282016-07-21 10:46:19 -07001441
James E. Blair0d3e83b2017-06-05 13:51:57 -07001442 def wasDequeuedNeedingChange(self):
1443 return self.dequeued_needing_change
1444
James E. Blairdbfd3282016-07-21 10:46:19 -07001445 def isHoldingFollowingChanges(self):
1446 if not self.live:
1447 return False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001448 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001449 return False
1450 for job in self.getJobs():
1451 if not job.hold_following_changes:
1452 continue
1453 build = self.current_build_set.getBuild(job.name)
1454 if not build:
1455 return True
1456 if build.result != 'SUCCESS':
1457 return True
1458
1459 if not self.item_ahead:
1460 return False
1461 return self.item_ahead.isHoldingFollowingChanges()
1462
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001463 def findJobsToRun(self, semaphore_handler):
James E. Blairdbfd3282016-07-21 10:46:19 -07001464 torun = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001465 if not self.live:
1466 return []
1467 if not self.job_graph:
1468 return []
James E. Blair791b5392016-08-03 11:25:56 -07001469 if self.item_ahead:
1470 # Only run jobs if any 'hold' jobs on the change ahead
1471 # have completed successfully.
1472 if self.item_ahead.isHoldingFollowingChanges():
1473 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001474
1475 successful_job_names = set()
1476 jobs_not_started = set()
1477 for job in self.job_graph.getJobs():
1478 build = self.current_build_set.getBuild(job.name)
1479 if build:
1480 if build.result == 'SUCCESS':
1481 successful_job_names.add(job.name)
1482 else:
1483 jobs_not_started.add(job)
1484
1485 # Attempt to request nodes for jobs in the order jobs appear
1486 # in configuration.
1487 for job in self.job_graph.getJobs():
1488 if job not in jobs_not_started:
1489 continue
1490 all_parent_jobs_successful = True
1491 for parent_job in self.job_graph.getParentJobsRecursively(
1492 job.name):
1493 if parent_job.name not in successful_job_names:
1494 all_parent_jobs_successful = False
1495 break
1496 if all_parent_jobs_successful:
1497 nodeset = self.current_build_set.getJobNodeSet(job.name)
1498 if nodeset is None:
1499 # The nodes for this job are not ready, skip
1500 # it for now.
James E. Blairdbfd3282016-07-21 10:46:19 -07001501 continue
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001502 if semaphore_handler.acquire(self, job):
1503 # If this job needs a semaphore, either acquire it or
1504 # make sure that we have it before running the job.
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001505 torun.append(job)
James E. Blairdbfd3282016-07-21 10:46:19 -07001506 return torun
1507
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001508 def findJobsToRequest(self):
James E. Blair6ab79e02017-01-06 10:10:17 -08001509 build_set = self.current_build_set
James E. Blairdbfd3282016-07-21 10:46:19 -07001510 toreq = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001511 if not self.live:
1512 return []
1513 if not self.job_graph:
1514 return []
James E. Blair6ab79e02017-01-06 10:10:17 -08001515 if self.item_ahead:
1516 if self.item_ahead.isHoldingFollowingChanges():
1517 return []
James E. Blairdbfd3282016-07-21 10:46:19 -07001518
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001519 successful_job_names = set()
1520 jobs_not_requested = set()
1521 for job in self.job_graph.getJobs():
1522 build = build_set.getBuild(job.name)
1523 if build and build.result == 'SUCCESS':
1524 successful_job_names.add(job.name)
1525 else:
1526 nodeset = build_set.getJobNodeSet(job.name)
1527 if nodeset is None:
1528 req = build_set.getJobNodeRequest(job.name)
1529 if req is None:
1530 jobs_not_requested.add(job)
1531
1532 # Attempt to request nodes for jobs in the order jobs appear
1533 # in configuration.
1534 for job in self.job_graph.getJobs():
1535 if job not in jobs_not_requested:
1536 continue
1537 all_parent_jobs_successful = True
1538 for parent_job in self.job_graph.getParentJobsRecursively(
1539 job.name):
1540 if parent_job.name not in successful_job_names:
1541 all_parent_jobs_successful = False
1542 break
1543 if all_parent_jobs_successful:
1544 toreq.append(job)
1545 return toreq
James E. Blairdbfd3282016-07-21 10:46:19 -07001546
1547 def setResult(self, build):
1548 if build.retry:
1549 self.removeBuild(build)
1550 elif build.result != 'SUCCESS':
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001551 for job in self.job_graph.getDependentJobsRecursively(
1552 build.job.name):
James E. Blairdbfd3282016-07-21 10:46:19 -07001553 fakebuild = Build(job, None)
1554 fakebuild.result = 'SKIPPED'
1555 self.addBuild(fakebuild)
1556
James E. Blair6ab79e02017-01-06 10:10:17 -08001557 def setNodeRequestFailure(self, job):
1558 fakebuild = Build(job, None)
1559 self.addBuild(fakebuild)
1560 fakebuild.result = 'NODE_FAILURE'
1561 self.setResult(fakebuild)
1562
James E. Blairdbfd3282016-07-21 10:46:19 -07001563 def setDequeuedNeedingChange(self):
1564 self.dequeued_needing_change = True
1565 self._setAllJobsSkipped()
1566
1567 def setUnableToMerge(self):
1568 self.current_build_set.unable_to_merge = True
1569 self._setAllJobsSkipped()
1570
James E. Blaire53250c2017-03-01 14:34:36 -08001571 def setConfigError(self, error):
1572 self.current_build_set.config_error = error
1573 self._setAllJobsSkipped()
1574
James E. Blairdbfd3282016-07-21 10:46:19 -07001575 def _setAllJobsSkipped(self):
1576 for job in self.getJobs():
1577 fakebuild = Build(job, None)
1578 fakebuild.result = 'SKIPPED'
1579 self.addBuild(fakebuild)
1580
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001581 def formatUrlPattern(self, url_pattern, job=None, build=None):
1582 url = None
1583 # Produce safe versions of objects which may be useful in
1584 # result formatting, but don't allow users to crawl through
1585 # the entire data structure where they might be able to access
1586 # secrets, etc.
1587 safe_change = self.change.getSafeAttributes()
1588 safe_pipeline = self.pipeline.getSafeAttributes()
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001589 safe_tenant = self.pipeline.layout.tenant.getSafeAttributes()
Jamie Lennox3f16de52017-05-09 14:24:11 +10001590 safe_buildset = self.current_build_set.getSafeAttributes()
K Jonathan Harkera7f390c2017-06-02 12:31:22 -07001591 safe_job = job.getSafeAttributes() if job else {}
1592 safe_build = build.getSafeAttributes() if build else {}
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001593 try:
1594 url = url_pattern.format(change=safe_change,
1595 pipeline=safe_pipeline,
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001596 tenant=safe_tenant,
Jamie Lennox3f16de52017-05-09 14:24:11 +10001597 buildset=safe_buildset,
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001598 job=safe_job,
1599 build=safe_build)
1600 except KeyError as e:
1601 self.log.error("Error while formatting url for job %s: unknown "
1602 "key %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001603 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001604 except AttributeError as e:
1605 self.log.error("Error while formatting url for job %s: unknown "
1606 "attribute %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001607 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001608 except Exception:
1609 self.log.exception("Error while formatting url for job %s with "
1610 "pattern %s:" % (job, url_pattern))
1611
1612 return url
1613
James E. Blair800e7ff2017-03-17 16:06:52 -07001614 def formatJobResult(self, job):
James E. Blairb7273ef2016-04-19 08:58:51 -07001615 build = self.current_build_set.getBuild(job.name)
1616 result = build.result
James E. Blair800e7ff2017-03-17 16:06:52 -07001617 pattern = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001618 if result == 'SUCCESS':
1619 if job.success_message:
1620 result = job.success_message
James E. Blaira5dba232016-08-08 15:53:24 -07001621 if job.success_url:
1622 pattern = job.success_url
James E. Blairb7273ef2016-04-19 08:58:51 -07001623 elif result == 'FAILURE':
1624 if job.failure_message:
1625 result = job.failure_message
James E. Blaira5dba232016-08-08 15:53:24 -07001626 if job.failure_url:
1627 pattern = job.failure_url
James E. Blairb7273ef2016-04-19 08:58:51 -07001628 url = None
1629 if pattern:
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001630 url = self.formatUrlPattern(pattern, job, build)
James E. Blairb7273ef2016-04-19 08:58:51 -07001631 if not url:
1632 url = build.url or job.name
1633 return (result, url)
1634
James E. Blair800e7ff2017-03-17 16:06:52 -07001635 def formatJSON(self):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001636 ret = {}
1637 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -08001638 ret['live'] = self.live
Monty Taylor55d0f562017-05-17 14:30:21 -05001639 if hasattr(self.change, 'url') and self.change.url is not None:
1640 ret['url'] = self.change.url
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001641 else:
1642 ret['url'] = None
Monty Taylor55d0f562017-05-17 14:30:21 -05001643 ret['id'] = self.change._id()
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001644 if self.item_ahead:
1645 ret['item_ahead'] = self.item_ahead.change._id()
1646 else:
1647 ret['item_ahead'] = None
1648 ret['items_behind'] = [i.change._id() for i in self.items_behind]
1649 ret['failing_reasons'] = self.current_build_set.failing_reasons
1650 ret['zuul_ref'] = self.current_build_set.ref
Monty Taylor55d0f562017-05-17 14:30:21 -05001651 if self.change.project:
1652 ret['project'] = self.change.project.name
Ramy Asselin07cc33c2015-06-12 14:06:34 -07001653 else:
1654 # For cross-project dependencies with the depends-on
1655 # project not known to zuul, the project is None
1656 # Set it to a static value
1657 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001658 ret['enqueue_time'] = int(self.enqueue_time * 1000)
1659 ret['jobs'] = []
Monty Taylor55d0f562017-05-17 14:30:21 -05001660 if hasattr(self.change, 'owner'):
1661 ret['owner'] = self.change.owner
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001662 else:
1663 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001664 max_remaining = 0
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001665 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001666 now = time.time()
1667 build = self.current_build_set.getBuild(job.name)
1668 elapsed = None
1669 remaining = None
1670 result = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001671 build_url = None
1672 report_url = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001673 worker = None
1674 if build:
1675 result = build.result
James E. Blairb7273ef2016-04-19 08:58:51 -07001676 build_url = build.url
James E. Blair800e7ff2017-03-17 16:06:52 -07001677 (unused, report_url) = self.formatJobResult(job)
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001678 if build.start_time:
1679 if build.end_time:
1680 elapsed = int((build.end_time -
1681 build.start_time) * 1000)
1682 remaining = 0
1683 else:
1684 elapsed = int((now - build.start_time) * 1000)
1685 if build.estimated_time:
1686 remaining = max(
1687 int(build.estimated_time * 1000) - elapsed,
1688 0)
1689 worker = {
1690 'name': build.worker.name,
1691 'hostname': build.worker.hostname,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001692 }
1693 if remaining and remaining > max_remaining:
1694 max_remaining = remaining
1695
1696 ret['jobs'].append({
1697 'name': job.name,
1698 'elapsed_time': elapsed,
1699 'remaining_time': remaining,
James E. Blairb7273ef2016-04-19 08:58:51 -07001700 'url': build_url,
1701 'report_url': report_url,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001702 'result': result,
1703 'voting': job.voting,
1704 'uuid': build.uuid if build else None,
Paul Belanger174a8272017-03-14 13:20:10 -04001705 'execute_time': build.execute_time if build else None,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001706 'start_time': build.start_time if build else None,
1707 'end_time': build.end_time if build else None,
1708 'estimated_time': build.estimated_time if build else None,
1709 'pipeline': build.pipeline.name if build else None,
1710 'canceled': build.canceled if build else None,
1711 'retry': build.retry if build else None,
Timothy Chavezb2332082015-08-07 20:08:04 -05001712 'node_labels': build.node_labels if build else [],
1713 'node_name': build.node_name if build else None,
1714 'worker': worker,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001715 })
1716
James E. Blairdbfd3282016-07-21 10:46:19 -07001717 if self.haveAllJobsStarted():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001718 ret['remaining_time'] = max_remaining
1719 else:
1720 ret['remaining_time'] = None
1721 return ret
1722
1723 def formatStatus(self, indent=0, html=False):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001724 indent_str = ' ' * indent
1725 ret = ''
Monty Taylor55d0f562017-05-17 14:30:21 -05001726 if html and getattr(self.change, 'url', None) is not None:
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001727 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
1728 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001729 self.change.project.name,
1730 self.change.url,
1731 self.change._id())
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001732 else:
1733 ret += '%sProject %s change %s based on %s\n' % (
1734 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001735 self.change.project.name,
1736 self.change._id(),
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001737 self.item_ahead)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001738 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001739 build = self.current_build_set.getBuild(job.name)
1740 if build:
1741 result = build.result
1742 else:
1743 result = None
1744 job_name = job.name
1745 if not job.voting:
1746 voting = ' (non-voting)'
1747 else:
1748 voting = ''
1749 if html:
1750 if build:
1751 url = build.url
1752 else:
1753 url = None
1754 if url is not None:
1755 job_name = '<a href="%s">%s</a>' % (url, job_name)
1756 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
1757 ret += '\n'
1758 return ret
1759
James E. Blaira04b0792017-04-27 09:59:06 -07001760 def makeMergerItem(self):
1761 # Create a dictionary with all info about the item needed by
1762 # the merger.
1763 number = None
1764 patchset = None
1765 oldrev = None
1766 newrev = None
1767 refspec = None
1768 if hasattr(self.change, 'number'):
1769 number = self.change.number
1770 patchset = self.change.patchset
1771 refspec = self.change.refspec
1772 branch = self.change.branch
1773 elif hasattr(self.change, 'newrev'):
1774 oldrev = self.change.oldrev
1775 newrev = self.change.newrev
1776 branch = self.change.ref
1777 else:
1778 oldrev = None
1779 newrev = None
1780 branch = None
1781 source = self.change.project.source
1782 connection_name = source.connection.connection_name
James E. Blair2a535672017-04-27 12:03:15 -07001783 project = self.change.project
James E. Blaira04b0792017-04-27 09:59:06 -07001784
James E. Blair2a535672017-04-27 12:03:15 -07001785 return dict(project=project.name,
1786 connection=connection_name,
James E. Blaira04b0792017-04-27 09:59:06 -07001787 merge_mode=self.current_build_set.getMergeMode(),
1788 refspec=refspec,
1789 branch=branch,
1790 ref=self.current_build_set.ref,
1791 number=number,
1792 patchset=patchset,
1793 oldrev=oldrev,
1794 newrev=newrev,
1795 )
1796
James E. Blairfee8d652013-06-07 08:57:52 -07001797
Clint Byrumf8cc9902017-03-22 22:38:25 -07001798class Ref(object):
1799 """An existing state of a Project."""
James E. Blairfee8d652013-06-07 08:57:52 -07001800
1801 def __init__(self, project):
1802 self.project = project
Clint Byrumf8cc9902017-03-22 22:38:25 -07001803 self.ref = None
1804 self.oldrev = None
1805 self.newrev = None
James E. Blairfee8d652013-06-07 08:57:52 -07001806
Jesse Keating71a47ff2017-06-06 11:36:43 -07001807 self.files = []
1808
Joshua Hesketh36c3fa52014-01-22 11:40:52 +11001809 def getBasePath(self):
1810 base_path = ''
Clint Byrumf8cc9902017-03-22 22:38:25 -07001811 if hasattr(self, 'ref'):
Joshua Hesketh36c3fa52014-01-22 11:40:52 +11001812 base_path = "%s/%s" % (self.newrev[:2], self.newrev)
1813
1814 return base_path
1815
Clint Byrumf8cc9902017-03-22 22:38:25 -07001816 def _id(self):
1817 return self.newrev
1818
1819 def __repr__(self):
1820 rep = None
1821 if self.newrev == '0000000000000000000000000000000000000000':
1822 rep = '<Ref 0x%x deletes %s from %s' % (
1823 id(self), self.ref, self.oldrev)
1824 elif self.oldrev == '0000000000000000000000000000000000000000':
1825 rep = '<Ref 0x%x creates %s on %s>' % (
1826 id(self), self.ref, self.newrev)
1827 else:
1828 # Catch all
1829 rep = '<Ref 0x%x %s updated %s..%s>' % (
1830 id(self), self.ref, self.oldrev, self.newrev)
1831
1832 return rep
1833
James E. Blairfee8d652013-06-07 08:57:52 -07001834 def equals(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001835 if (self.project == other.project
1836 and self.ref == other.ref
1837 and self.newrev == other.newrev):
1838 return True
1839 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001840
1841 def isUpdateOf(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001842 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001843
1844 def filterJobs(self, jobs):
1845 return filter(lambda job: job.changeMatches(self), jobs)
1846
1847 def getRelatedChanges(self):
1848 return set()
1849
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001850 def updatesConfig(self):
Jesse Keating71a47ff2017-06-06 11:36:43 -07001851 if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files:
1852 return True
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001853 return False
1854
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001855 def getSafeAttributes(self):
1856 return Attributes(project=self.project,
1857 ref=self.ref,
1858 oldrev=self.oldrev,
1859 newrev=self.newrev)
1860
James E. Blair1e8dd892012-05-30 09:15:05 -07001861
Clint Byrumf8cc9902017-03-22 22:38:25 -07001862class Change(Ref):
Monty Taylora42a55b2016-07-29 07:53:33 -07001863 """A proposed new state for a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001864 def __init__(self, project):
1865 super(Change, self).__init__(project)
1866 self.branch = None
1867 self.number = None
1868 self.url = None
1869 self.patchset = None
1870 self.refspec = None
1871
James E. Blair6965a4b2014-12-16 17:19:04 -08001872 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -07001873 self.needed_by_changes = []
1874 self.is_current_patchset = True
1875 self.can_merge = False
1876 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -07001877 self.failed_to_merge = False
James E. Blair11041d22014-05-02 14:49:53 -07001878 self.open = None
1879 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001880 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001881
Jan Hruban3b415922016-02-03 13:10:22 +01001882 self.source_event = None
1883
James E. Blair4aea70c2012-07-26 14:23:24 -07001884 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -07001885 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -07001886
1887 def __repr__(self):
1888 return '<Change 0x%x %s>' % (id(self), self._id())
1889
Clint Byrumf8cc9902017-03-22 22:38:25 -07001890 def getBasePath(self):
1891 if hasattr(self, 'refspec'):
1892 return "%s/%s/%s" % (
Gregory Haynes4fc12542015-04-22 20:38:06 -07001893 str(self.number)[-2:], self.number, self.patchset)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001894 return super(Change, self).getBasePath()
1895
James E. Blair4aea70c2012-07-26 14:23:24 -07001896 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +08001897 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -07001898 return True
1899 return False
1900
James E. Blair2fa50962013-01-30 21:50:41 -08001901 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -08001902 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -07001903 (hasattr(other, 'patchset') and
1904 self.patchset is not None and
1905 other.patchset is not None and
1906 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -08001907 return True
1908 return False
1909
James E. Blairfee8d652013-06-07 08:57:52 -07001910 def getRelatedChanges(self):
1911 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -08001912 for c in self.needs_changes:
1913 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -07001914 for c in self.needed_by_changes:
1915 related.add(c)
1916 related.update(c.getRelatedChanges())
1917 return related
James E. Blair4aea70c2012-07-26 14:23:24 -07001918
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001919 def getSafeAttributes(self):
1920 return Attributes(project=self.project,
1921 number=self.number,
1922 patchset=self.patchset)
1923
James E. Blair4aea70c2012-07-26 14:23:24 -07001924
James E. Blairee743612012-05-29 14:49:32 -07001925class TriggerEvent(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001926 """Incoming event from an external system."""
James E. Blairee743612012-05-29 14:49:32 -07001927 def __init__(self):
James E. Blairaad3ae22017-05-18 14:11:29 -07001928 # TODO(jeblair): further reduce this list
James E. Blairee743612012-05-29 14:49:32 -07001929 self.data = None
James E. Blair32663402012-06-01 10:04:18 -07001930 # common
James E. Blairee743612012-05-29 14:49:32 -07001931 self.type = None
Jesse Keating71a47ff2017-06-06 11:36:43 -07001932 self.branch_updated = False
Paul Belangerbaca3132016-11-04 12:49:54 -04001933 # For management events (eg: enqueue / promote)
1934 self.tenant_name = None
James E. Blair6f284b42017-03-31 14:14:41 -07001935 self.project_hostname = None
James E. Blairee743612012-05-29 14:49:32 -07001936 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -07001937 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +01001938 # Representation of the user account that performed the event.
1939 self.account = None
James E. Blair32663402012-06-01 10:04:18 -07001940 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -07001941 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -07001942 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -07001943 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -07001944 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -07001945 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07001946 self.comment = None
Jesse Keating5c05a9f2017-01-12 14:44:58 -08001947 self.state = None
James E. Blair32663402012-06-01 10:04:18 -07001948 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -07001949 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -07001950 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07001951 self.newrev = None
James E. Blairad28e912013-11-27 10:43:22 -08001952 # For events that arrive with a destination pipeline (eg, from
1953 # an admin command, etc):
1954 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07001955
James E. Blair6f284b42017-03-31 14:14:41 -07001956 @property
1957 def canonical_project_name(self):
1958 return self.project_hostname + '/' + self.project_name
1959
Jan Hruban324ca5b2015-11-05 19:28:54 +01001960 def isPatchsetCreated(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01001961 return False
1962
1963 def isChangeAbandoned(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01001964 return False
1965
James E. Blair1e8dd892012-05-30 09:15:05 -07001966
James E. Blair9c17dbf2014-06-23 14:21:58 -07001967class BaseFilter(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001968 """Base Class for filtering which Changes and Events to process."""
James E. Blairaad3ae22017-05-18 14:11:29 -07001969 pass
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001970
James E. Blair9c17dbf2014-06-23 14:21:58 -07001971
1972class EventFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07001973 """Allows a Pipeline to only respond to certain events."""
James E. Blairaad3ae22017-05-18 14:11:29 -07001974 def __init__(self, trigger):
1975 super(EventFilter, self).__init__()
James E. Blairc0dedf82014-08-06 09:37:52 -07001976 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07001977
James E. Blairaad3ae22017-05-18 14:11:29 -07001978 def matches(self, event, ref):
1979 # TODO(jeblair): consider removing ref argument
James E. Blairee743612012-05-29 14:49:32 -07001980 return True
James E. Blaireff88162013-07-01 12:44:14 -04001981
1982
James E. Blairaad3ae22017-05-18 14:11:29 -07001983class RefFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07001984 """Allows a Manager to only enqueue Changes that meet certain criteria."""
Jesse Keating8c2eb572017-05-30 17:31:45 -07001985 def __init__(self, connection_name):
James E. Blairaad3ae22017-05-18 14:11:29 -07001986 super(RefFilter, self).__init__()
Jesse Keating8c2eb572017-05-30 17:31:45 -07001987 self.connection_name = connection_name
James E. Blair11041d22014-05-02 14:49:53 -07001988
1989 def matches(self, change):
James E. Blair11041d22014-05-02 14:49:53 -07001990 return True
1991
1992
James E. Blairb97ed802015-12-21 15:55:35 -08001993class ProjectPipelineConfig(object):
1994 # Represents a project cofiguration in the context of a pipeline
1995 def __init__(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001996 self.job_list = JobList()
James E. Blairb97ed802015-12-21 15:55:35 -08001997 self.queue_name = None
Adam Gandelman8bd57102016-12-02 12:58:42 -08001998 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08001999
2000
2001class ProjectConfig(object):
2002 # Represents a project cofiguration
2003 def __init__(self, name):
2004 self.name = name
Adam Gandelman8bd57102016-12-02 12:58:42 -08002005 self.merge_mode = None
James E. Blair040b6502017-05-23 10:18:21 -07002006 self.default_branch = None
James E. Blairb97ed802015-12-21 15:55:35 -08002007 self.pipelines = {}
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002008 self.private_key_file = None
James E. Blairb97ed802015-12-21 15:55:35 -08002009
2010
James E. Blaird8e778f2015-12-22 14:09:20 -08002011class UnparsedAbideConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002012 """A collection of yaml lists that has not yet been parsed into objects.
2013
2014 An Abide is a collection of tenants.
2015 """
2016
James E. Blaird8e778f2015-12-22 14:09:20 -08002017 def __init__(self):
2018 self.tenants = []
2019
2020 def extend(self, conf):
2021 if isinstance(conf, UnparsedAbideConfig):
2022 self.tenants.extend(conf.tenants)
2023 return
2024
2025 if not isinstance(conf, list):
2026 raise Exception("Configuration items must be in the form of "
2027 "a list of dictionaries (when parsing %s)" %
2028 (conf,))
2029 for item in conf:
2030 if not isinstance(item, dict):
2031 raise Exception("Configuration items must be in the form of "
2032 "a list of dictionaries (when parsing %s)" %
2033 (conf,))
2034 if len(item.keys()) > 1:
2035 raise Exception("Configuration item dictionaries must have "
2036 "a single key (when parsing %s)" %
2037 (conf,))
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002038 key, value = list(item.items())[0]
James E. Blaird8e778f2015-12-22 14:09:20 -08002039 if key == 'tenant':
2040 self.tenants.append(value)
2041 else:
2042 raise Exception("Configuration item not recognized "
2043 "(when parsing %s)" %
2044 (conf,))
2045
2046
2047class UnparsedTenantConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002048 """A collection of yaml lists that has not yet been parsed into objects."""
2049
James E. Blaird8e778f2015-12-22 14:09:20 -08002050 def __init__(self):
2051 self.pipelines = []
2052 self.jobs = []
2053 self.project_templates = []
James E. Blairff555742017-02-19 11:34:27 -08002054 self.projects = {}
James E. Blaira98340f2016-09-02 11:33:49 -07002055 self.nodesets = []
James E. Blair01f83b72017-03-15 13:03:40 -07002056 self.secrets = []
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002057 self.semaphores = []
James E. Blaird8e778f2015-12-22 14:09:20 -08002058
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002059 def copy(self):
2060 r = UnparsedTenantConfig()
2061 r.pipelines = copy.deepcopy(self.pipelines)
2062 r.jobs = copy.deepcopy(self.jobs)
2063 r.project_templates = copy.deepcopy(self.project_templates)
2064 r.projects = copy.deepcopy(self.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07002065 r.nodesets = copy.deepcopy(self.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002066 r.secrets = copy.deepcopy(self.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002067 r.semaphores = copy.deepcopy(self.semaphores)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002068 return r
2069
James E. Blairec7ff302017-03-04 07:31:32 -08002070 def extend(self, conf):
James E. Blaird8e778f2015-12-22 14:09:20 -08002071 if isinstance(conf, UnparsedTenantConfig):
2072 self.pipelines.extend(conf.pipelines)
2073 self.jobs.extend(conf.jobs)
2074 self.project_templates.extend(conf.project_templates)
James E. Blairff555742017-02-19 11:34:27 -08002075 for k, v in conf.projects.items():
2076 self.projects.setdefault(k, []).extend(v)
James E. Blaira98340f2016-09-02 11:33:49 -07002077 self.nodesets.extend(conf.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002078 self.secrets.extend(conf.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002079 self.semaphores.extend(conf.semaphores)
James E. Blaird8e778f2015-12-22 14:09:20 -08002080 return
2081
2082 if not isinstance(conf, list):
2083 raise Exception("Configuration items must be in the form of "
2084 "a list of dictionaries (when parsing %s)" %
2085 (conf,))
James E. Blaircdab2032017-02-01 09:09:29 -08002086
James E. Blaird8e778f2015-12-22 14:09:20 -08002087 for item in conf:
2088 if not isinstance(item, dict):
2089 raise Exception("Configuration items must be in the form of "
2090 "a list of dictionaries (when parsing %s)" %
2091 (conf,))
2092 if len(item.keys()) > 1:
2093 raise Exception("Configuration item dictionaries must have "
2094 "a single key (when parsing %s)" %
2095 (conf,))
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002096 key, value = list(item.items())[0]
James E. Blair66b274e2017-01-31 14:47:52 -08002097 if key == 'project':
James E. Blairff555742017-02-19 11:34:27 -08002098 name = value['name']
2099 self.projects.setdefault(name, []).append(value)
James E. Blair66b274e2017-01-31 14:47:52 -08002100 elif key == 'job':
James E. Blaird8e778f2015-12-22 14:09:20 -08002101 self.jobs.append(value)
2102 elif key == 'project-template':
2103 self.project_templates.append(value)
2104 elif key == 'pipeline':
2105 self.pipelines.append(value)
James E. Blaira98340f2016-09-02 11:33:49 -07002106 elif key == 'nodeset':
2107 self.nodesets.append(value)
James E. Blair01f83b72017-03-15 13:03:40 -07002108 elif key == 'secret':
2109 self.secrets.append(value)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002110 elif key == 'semaphore':
2111 self.semaphores.append(value)
James E. Blaird8e778f2015-12-22 14:09:20 -08002112 else:
Morgan Fainberg4245a422016-08-05 16:20:12 -07002113 raise Exception("Configuration item `%s` not recognized "
James E. Blaird8e778f2015-12-22 14:09:20 -08002114 "(when parsing %s)" %
Morgan Fainberg4245a422016-08-05 16:20:12 -07002115 (item, conf,))
James E. Blaird8e778f2015-12-22 14:09:20 -08002116
2117
James E. Blaireff88162013-07-01 12:44:14 -04002118class Layout(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002119 """Holds all of the Pipelines."""
2120
James E. Blaireff88162013-07-01 12:44:14 -04002121 def __init__(self):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002122 self.tenant = None
James E. Blairb97ed802015-12-21 15:55:35 -08002123 self.project_configs = {}
2124 self.project_templates = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07002125 self.pipelines = OrderedDict()
James E. Blair83005782015-12-11 14:46:03 -08002126 # This is a dictionary of name -> [jobs]. The first element
2127 # of the list is the first job added with that name. It is
2128 # the reference definition for a given job. Subsequent
2129 # elements are aspects of that job with different matchers
2130 # that override some attribute of the job. These aspects all
2131 # inherit from the reference definition.
James E. Blairfef88ec2016-11-30 08:38:27 -08002132 self.jobs = {'noop': [Job('noop')]}
James E. Blaira98340f2016-09-02 11:33:49 -07002133 self.nodesets = {}
James E. Blair01f83b72017-03-15 13:03:40 -07002134 self.secrets = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002135 self.semaphores = {}
James E. Blaireff88162013-07-01 12:44:14 -04002136
2137 def getJob(self, name):
2138 if name in self.jobs:
James E. Blair83005782015-12-11 14:46:03 -08002139 return self.jobs[name][0]
James E. Blairb97ed802015-12-21 15:55:35 -08002140 raise Exception("Job %s not defined" % (name,))
James E. Blair83005782015-12-11 14:46:03 -08002141
2142 def getJobs(self, name):
2143 return self.jobs.get(name, [])
2144
2145 def addJob(self, job):
James E. Blair4317e9f2016-07-15 10:05:47 -07002146 # We can have multiple variants of a job all with the same
2147 # name, but these variants must all be defined in the same repo.
James E. Blaircdab2032017-02-01 09:09:29 -08002148 prior_jobs = [j for j in self.getJobs(job.name) if
2149 j.source_context.project !=
2150 job.source_context.project]
James E. Blair4317e9f2016-07-15 10:05:47 -07002151 if prior_jobs:
2152 raise Exception("Job %s in %s is not permitted to shadow "
James E. Blaircdab2032017-02-01 09:09:29 -08002153 "job %s in %s" % (
2154 job,
2155 job.source_context.project,
2156 prior_jobs[0],
2157 prior_jobs[0].source_context.project))
James E. Blair4317e9f2016-07-15 10:05:47 -07002158
James E. Blair83005782015-12-11 14:46:03 -08002159 if job.name in self.jobs:
2160 self.jobs[job.name].append(job)
2161 else:
2162 self.jobs[job.name] = [job]
2163
James E. Blaira98340f2016-09-02 11:33:49 -07002164 def addNodeSet(self, nodeset):
2165 if nodeset.name in self.nodesets:
2166 raise Exception("NodeSet %s already defined" % (nodeset.name,))
2167 self.nodesets[nodeset.name] = nodeset
2168
James E. Blair01f83b72017-03-15 13:03:40 -07002169 def addSecret(self, secret):
2170 if secret.name in self.secrets:
2171 raise Exception("Secret %s already defined" % (secret.name,))
2172 self.secrets[secret.name] = secret
2173
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002174 def addSemaphore(self, semaphore):
2175 if semaphore.name in self.semaphores:
2176 raise Exception("Semaphore %s already defined" % (semaphore.name,))
2177 self.semaphores[semaphore.name] = semaphore
2178
James E. Blair83005782015-12-11 14:46:03 -08002179 def addPipeline(self, pipeline):
2180 self.pipelines[pipeline.name] = pipeline
James E. Blair59fdbac2015-12-07 17:08:06 -08002181
James E. Blairb97ed802015-12-21 15:55:35 -08002182 def addProjectTemplate(self, project_template):
2183 self.project_templates[project_template.name] = project_template
2184
James E. Blairf59f3cf2017-02-19 14:50:26 -08002185 def addProjectConfig(self, project_config):
James E. Blairb97ed802015-12-21 15:55:35 -08002186 self.project_configs[project_config.name] = project_config
James E. Blairb97ed802015-12-21 15:55:35 -08002187
James E. Blaird2348362017-03-17 13:59:35 -07002188 def _createJobGraph(self, item, job_list, job_graph):
2189 change = item.change
2190 pipeline = item.pipeline
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002191 for jobname in job_list.jobs:
2192 # This is the final job we are constructing
James E. Blaira7f51ca2017-02-07 16:01:26 -08002193 frozen_job = None
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002194 # Whether the change matches any globally defined variant
James E. Blaira7f51ca2017-02-07 16:01:26 -08002195 matched = False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002196 for variant in self.getJobs(jobname):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002197 if variant.changeMatches(change):
James E. Blaira7f51ca2017-02-07 16:01:26 -08002198 if frozen_job is None:
2199 frozen_job = variant.copy()
2200 frozen_job.setRun()
2201 else:
2202 frozen_job.applyVariant(variant)
2203 matched = True
2204 if not matched:
James E. Blair6e85c2b2016-11-21 16:47:01 -08002205 # A change must match at least one defined job variant
2206 # (that is to say that it must match more than just
2207 # the job that is defined in the tree).
2208 continue
James E. Blaira7f51ca2017-02-07 16:01:26 -08002209 # If the job does not allow auth inheritance, do not allow
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002210 # the project-pipeline variants to update its execution
James E. Blaira7f51ca2017-02-07 16:01:26 -08002211 # attributes.
James E. Blair8525e2b2017-03-15 14:05:47 -07002212 if frozen_job.auth and not frozen_job.auth.inherit:
James E. Blaira7f51ca2017-02-07 16:01:26 -08002213 frozen_job.final = True
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002214 # Whether the change matches any of the project pipeline
2215 # variants
2216 matched = False
2217 for variant in job_list.jobs[jobname]:
2218 if variant.changeMatches(change):
2219 frozen_job.applyVariant(variant)
2220 matched = True
2221 if not matched:
2222 # A change must match at least one project pipeline
2223 # job variant.
2224 continue
James E. Blairb3f5db12017-03-17 12:57:39 -07002225 if (frozen_job.allowed_projects and
2226 change.project.name not in frozen_job.allowed_projects):
2227 raise Exception("Project %s is not allowed to run job %s" %
2228 (change.project.name, frozen_job.name))
James E. Blaird2348362017-03-17 13:59:35 -07002229 if ((not pipeline.allow_secrets) and frozen_job.auth and
2230 frozen_job.auth.secrets):
2231 raise Exception("Pipeline %s does not allow jobs with "
2232 "secrets (job %s)" % (
2233 pipeline.name, frozen_job.name))
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002234 job_graph.addJob(frozen_job)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002235
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002236 def createJobGraph(self, item):
Paul Belanger160cb8e2016-11-11 19:04:24 -05002237 project_config = self.project_configs.get(
James E. Blair0ffa0102017-03-30 13:11:33 -07002238 item.change.project.canonical_name, None)
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002239 ret = JobGraph()
Paul Belanger15e3e202016-10-14 16:27:34 -04002240 # NOTE(pabelanger): It is possible for a foreign project not to have a
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002241 # configured pipeline, if so return an empty JobGraph.
Paul Belanger160cb8e2016-11-11 19:04:24 -05002242 if project_config and item.pipeline.name in project_config.pipelines:
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002243 project_job_list = \
2244 project_config.pipelines[item.pipeline.name].job_list
James E. Blaird2348362017-03-17 13:59:35 -07002245 self._createJobGraph(item, project_job_list, ret)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002246 return ret
2247
James E. Blair0d3e83b2017-06-05 13:51:57 -07002248 def hasProject(self, project):
2249 return project.canonical_name in self.project_configs
2250
James E. Blair59fdbac2015-12-07 17:08:06 -08002251
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002252class Semaphore(object):
2253 def __init__(self, name, max=1):
2254 self.name = name
2255 self.max = int(max)
2256
2257
2258class SemaphoreHandler(object):
2259 log = logging.getLogger("zuul.SemaphoreHandler")
2260
2261 def __init__(self):
2262 self.semaphores = {}
2263
2264 def acquire(self, item, job):
2265 if not job.semaphore:
2266 return True
2267
2268 semaphore_key = job.semaphore
2269
2270 m = self.semaphores.get(semaphore_key)
2271 if not m:
2272 # The semaphore is not held, acquire it
2273 self._acquire(semaphore_key, item, job.name)
2274 return True
2275 if (item, job.name) in m:
2276 # This item already holds the semaphore
2277 return True
2278
2279 # semaphore is there, check max
2280 if len(m) < self._max_count(item, job.semaphore):
2281 self._acquire(semaphore_key, item, job.name)
2282 return True
2283
2284 return False
2285
2286 def release(self, item, job):
2287 if not job.semaphore:
2288 return
2289
2290 semaphore_key = job.semaphore
2291
2292 m = self.semaphores.get(semaphore_key)
2293 if not m:
2294 # The semaphore is not held, nothing to do
2295 self.log.error("Semaphore can not be released for %s "
2296 "because the semaphore is not held" %
2297 item)
2298 return
2299 if (item, job.name) in m:
2300 # This item is a holder of the semaphore
2301 self._release(semaphore_key, item, job.name)
2302 return
2303 self.log.error("Semaphore can not be released for %s "
2304 "which does not hold it" % item)
2305
2306 def _acquire(self, semaphore_key, item, job_name):
2307 self.log.debug("Semaphore acquire {semaphore}: job {job}, item {item}"
2308 .format(semaphore=semaphore_key,
2309 job=job_name,
2310 item=item))
2311 if semaphore_key not in self.semaphores:
2312 self.semaphores[semaphore_key] = []
2313 self.semaphores[semaphore_key].append((item, job_name))
2314
2315 def _release(self, semaphore_key, item, job_name):
2316 self.log.debug("Semaphore release {semaphore}: job {job}, item {item}"
2317 .format(semaphore=semaphore_key,
2318 job=job_name,
2319 item=item))
2320 sem_item = (item, job_name)
2321 if sem_item in self.semaphores[semaphore_key]:
2322 self.semaphores[semaphore_key].remove(sem_item)
2323
2324 # cleanup if there is no user of the semaphore anymore
2325 if len(self.semaphores[semaphore_key]) == 0:
2326 del self.semaphores[semaphore_key]
2327
2328 @staticmethod
2329 def _max_count(item, semaphore_name):
2330 if not item.current_build_set.layout:
2331 # This should not occur as the layout of the item must already be
2332 # built when acquiring or releasing a semaphore for a job.
2333 raise Exception("Item {} has no layout".format(item))
2334
2335 # find the right semaphore
2336 default_semaphore = Semaphore(semaphore_name, 1)
2337 semaphores = item.current_build_set.layout.semaphores
2338 return semaphores.get(semaphore_name, default_semaphore).max
2339
2340
James E. Blair59fdbac2015-12-07 17:08:06 -08002341class Tenant(object):
2342 def __init__(self, name):
2343 self.name = name
2344 self.layout = None
James E. Blair646322f2017-01-27 15:50:34 -08002345 # The unparsed configuration from the main zuul config for
2346 # this tenant.
2347 self.unparsed_config = None
James E. Blair109da3f2017-04-04 14:39:43 -07002348 # The list of projects from which we will read full
James E. Blair8a395f92017-03-30 11:15:33 -07002349 # configuration.
James E. Blair109da3f2017-04-04 14:39:43 -07002350 self.config_projects = []
2351 # The unparsed config from those projects.
2352 self.config_projects_config = None
2353 # The list of projects from which we will read untrusted
2354 # in-repo configuration.
2355 self.untrusted_projects = []
2356 # The unparsed config from those projects.
2357 self.untrusted_projects_config = None
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002358 self.semaphore_handler = SemaphoreHandler()
2359
James E. Blairc2a54fd2017-03-29 15:19:26 -07002360 # A mapping of project names to projects. project_name ->
2361 # VALUE where VALUE is a further dictionary of
2362 # canonical_hostname -> Project.
2363 self.projects = {}
2364 self.canonical_hostnames = set()
2365
2366 def _addProject(self, project):
2367 """Add a project to the project index
2368
2369 :arg Project project: The project to add.
2370 """
2371 self.canonical_hostnames.add(project.canonical_hostname)
2372 hostname_dict = self.projects.setdefault(project.name, {})
2373 if project.canonical_hostname in hostname_dict:
2374 raise Exception("Project %s is already in project index" %
2375 (project,))
2376 hostname_dict[project.canonical_hostname] = project
2377
2378 def getProject(self, name):
2379 """Return a project given its name.
2380
2381 :arg str name: The name of the project. It may be fully
2382 qualified (E.g., "git.example.com/subpath/project") or may
2383 contain only the project name name may be supplied (E.g.,
2384 "subpath/project").
2385
2386 :returns: A tuple (trusted, project) or (None, None) if the
2387 project is not found or ambiguous. The "trusted" boolean
2388 indicates whether or not the project is trusted by this
2389 tenant.
2390 :rtype: (bool, Project)
2391
2392 """
2393 path = name.split('/', 1)
2394 if path[0] in self.canonical_hostnames:
2395 hostname = path[0]
2396 project_name = path[1]
2397 else:
2398 hostname = None
2399 project_name = name
2400 hostname_dict = self.projects.get(project_name)
2401 project = None
2402 if hostname_dict:
2403 if hostname:
2404 project = hostname_dict.get(hostname)
2405 else:
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002406 values = list(hostname_dict.values())
James E. Blairc2a54fd2017-03-29 15:19:26 -07002407 if len(values) == 1:
2408 project = values[0]
2409 else:
2410 raise Exception("Project name '%s' is ambiguous, "
2411 "please fully qualify the project "
2412 "with a hostname" % (name,))
2413 if project is None:
2414 return (None, None)
James E. Blair109da3f2017-04-04 14:39:43 -07002415 if project in self.config_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002416 return (True, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002417 if project in self.untrusted_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002418 return (False, project)
2419 # This should never happen:
2420 raise Exception("Project %s is neither trusted nor untrusted" %
2421 (project,))
2422
James E. Blair109da3f2017-04-04 14:39:43 -07002423 def addConfigProject(self, project):
2424 self.config_projects.append(project)
James E. Blairc2a54fd2017-03-29 15:19:26 -07002425 self._addProject(project)
James E. Blair5ac93842017-01-20 06:47:34 -08002426
James E. Blair109da3f2017-04-04 14:39:43 -07002427 def addUntrustedProject(self, project):
2428 self.untrusted_projects.append(project)
James E. Blairc2a54fd2017-03-29 15:19:26 -07002429 self._addProject(project)
James E. Blair5ac93842017-01-20 06:47:34 -08002430
Jamie Lennoxbf997c82017-05-10 09:58:40 +10002431 def getSafeAttributes(self):
2432 return Attributes(name=self.name)
2433
James E. Blair59fdbac2015-12-07 17:08:06 -08002434
2435class Abide(object):
2436 def __init__(self):
2437 self.tenants = OrderedDict()
James E. Blairce8a2132016-05-19 15:21:52 -07002438
2439
2440class JobTimeData(object):
2441 format = 'B10H10H10B'
2442 version = 0
2443
2444 def __init__(self, path):
2445 self.path = path
2446 self.success_times = [0 for x in range(10)]
2447 self.failure_times = [0 for x in range(10)]
2448 self.results = [0 for x in range(10)]
2449
2450 def load(self):
2451 if not os.path.exists(self.path):
2452 return
Clint Byruma4471d12017-05-10 20:57:40 -04002453 with open(self.path, 'rb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002454 data = struct.unpack(self.format, f.read())
2455 version = data[0]
2456 if version != self.version:
2457 raise Exception("Unkown data version")
2458 self.success_times = list(data[1:11])
2459 self.failure_times = list(data[11:21])
2460 self.results = list(data[21:32])
2461
2462 def save(self):
2463 tmpfile = self.path + '.tmp'
2464 data = [self.version]
2465 data.extend(self.success_times)
2466 data.extend(self.failure_times)
2467 data.extend(self.results)
2468 data = struct.pack(self.format, *data)
Clint Byruma4471d12017-05-10 20:57:40 -04002469 with open(tmpfile, 'wb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002470 f.write(data)
2471 os.rename(tmpfile, self.path)
2472
2473 def add(self, elapsed, result):
2474 elapsed = int(elapsed)
2475 if result == 'SUCCESS':
2476 self.success_times.append(elapsed)
2477 self.success_times.pop(0)
2478 result = 0
2479 else:
2480 self.failure_times.append(elapsed)
2481 self.failure_times.pop(0)
2482 result = 1
2483 self.results.append(result)
2484 self.results.pop(0)
2485
2486 def getEstimatedTime(self):
2487 times = [x for x in self.success_times if x]
2488 if times:
2489 return float(sum(times)) / len(times)
2490 return 0.0
2491
2492
2493class TimeDataBase(object):
2494 def __init__(self, root):
2495 self.root = root
2496 self.jobs = {}
2497
2498 def _getTD(self, name):
2499 td = self.jobs.get(name)
2500 if not td:
2501 td = JobTimeData(os.path.join(self.root, name))
2502 self.jobs[name] = td
2503 td.load()
2504 return td
2505
2506 def getEstimatedTime(self, name):
2507 return self._getTD(name).getEstimatedTime()
2508
2509 def update(self, name, elapsed, result):
2510 td = self._getTD(name)
2511 td.add(elapsed, result)
2512 td.save()