blob: b027c534fe332b3677fac92f7b2af08d57ee3a81 [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. Blair88e79c02017-07-07 13:36:54 -070023import urllib.parse
James E. Blair97043882017-09-06 15:51:17 -070024import textwrap
James E. Blair5a9918a2013-08-27 10:06:27 -070025
James E. Blaire74f5712017-09-29 15:14:31 -070026from zuul import change_matcher
27
James E. Blair19deff22013-08-25 13:17:35 -070028MERGER_MERGE = 1 # "git merge"
29MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
30MERGER_CHERRY_PICK = 3 # "git cherry-pick"
31
32MERGER_MAP = {
33 'merge': MERGER_MERGE,
34 'merge-resolve': MERGER_MERGE_RESOLVE,
35 'cherry-pick': MERGER_CHERRY_PICK,
36}
James E. Blairee743612012-05-29 14:49:32 -070037
James E. Blair64ed6f22013-07-10 14:07:23 -070038PRECEDENCE_NORMAL = 0
39PRECEDENCE_LOW = 1
40PRECEDENCE_HIGH = 2
41
42PRECEDENCE_MAP = {
43 None: PRECEDENCE_NORMAL,
44 'low': PRECEDENCE_LOW,
45 'normal': PRECEDENCE_NORMAL,
46 'high': PRECEDENCE_HIGH,
47}
48
Monty Taylor6dc5bc12017-09-29 15:47:31 -050049PRIORITY_MAP = {
50 PRECEDENCE_NORMAL: 200,
51 PRECEDENCE_LOW: 300,
52 PRECEDENCE_HIGH: 100,
53}
54
James E. Blair803e94f2017-01-06 09:18:59 -080055# Request states
56STATE_REQUESTED = 'requested'
57STATE_PENDING = 'pending'
58STATE_FULFILLED = 'fulfilled'
59STATE_FAILED = 'failed'
60REQUEST_STATES = set([STATE_REQUESTED,
61 STATE_PENDING,
62 STATE_FULFILLED,
63 STATE_FAILED])
64
65# Node states
66STATE_BUILDING = 'building'
67STATE_TESTING = 'testing'
68STATE_READY = 'ready'
69STATE_IN_USE = 'in-use'
70STATE_USED = 'used'
71STATE_HOLD = 'hold'
72STATE_DELETING = 'deleting'
73NODE_STATES = set([STATE_BUILDING,
74 STATE_TESTING,
75 STATE_READY,
76 STATE_IN_USE,
77 STATE_USED,
78 STATE_HOLD,
79 STATE_DELETING])
80
James E. Blair1e8dd892012-05-30 09:15:05 -070081
James E. Blairc32a8352017-10-11 16:27:50 -070082class NoMatchingParentError(Exception):
83 """A job referenced a parent, but that parent had no variants which
84 matched the current change."""
85 pass
86
87
Joshua Hesketh58419cb2017-02-24 13:09:22 -050088class Attributes(object):
89 """A class to hold attributes for string formatting."""
90
91 def __init__(self, **kw):
92 setattr(self, '__dict__', kw)
93
94
James E. Blair4aea70c2012-07-26 14:23:24 -070095class Pipeline(object):
James E. Blair6053de42017-04-05 11:27:11 -070096 """A configuration that ties together triggers, reporters and managers
Monty Taylor82dfd412016-07-29 12:01:28 -070097
98 Trigger
99 A description of which events should be processed
100
101 Manager
102 Responsible for enqueing and dequeing Changes
103
104 Reporter
105 Communicates success and failure results somewhere
Monty Taylora42a55b2016-07-29 07:53:33 -0700106 """
James E. Blair83005782015-12-11 14:46:03 -0800107 def __init__(self, name, layout):
James E. Blair4aea70c2012-07-26 14:23:24 -0700108 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800109 self.layout = layout
James E. Blair8dbd56a2012-12-22 10:55:10 -0800110 self.description = None
James E. Blair56370192013-01-14 15:47:28 -0800111 self.failure_message = None
Joshua Heskethb7179772014-01-30 23:30:46 +1100112 self.merge_failure_message = None
James E. Blair56370192013-01-14 15:47:28 -0800113 self.success_message = None
Joshua Hesketh3979e3e2014-03-04 11:21:10 +1100114 self.footer_message = None
James E. Blair60af7f42016-03-11 16:11:06 -0800115 self.start_message = None
James E. Blair8eb564a2017-08-10 09:21:41 -0700116 self.post_review = False
James E. Blair2fa50962013-01-30 21:50:41 -0800117 self.dequeue_on_new_patchset = True
James E. Blair17dd6772015-02-09 14:45:18 -0800118 self.ignore_dependencies = False
James E. Blair4aea70c2012-07-26 14:23:24 -0700119 self.manager = None
James E. Blaire0487072012-08-29 17:38:31 -0700120 self.queues = []
James E. Blair64ed6f22013-07-10 14:07:23 -0700121 self.precedence = PRECEDENCE_NORMAL
James E. Blair83005782015-12-11 14:46:03 -0800122 self.triggers = []
Joshua Hesketh352264b2015-08-11 23:42:08 +1000123 self.start_actions = []
124 self.success_actions = []
125 self.failure_actions = []
126 self.merge_failure_actions = []
127 self.disabled_actions = []
Joshua Hesketh89e829d2015-02-10 16:29:45 +1100128 self.disable_at = None
129 self._consecutive_failures = 0
130 self._disabled = False
Clark Boylan7603a372014-01-21 11:43:20 -0800131 self.window = None
132 self.window_floor = None
133 self.window_increase_type = None
134 self.window_increase_factor = None
135 self.window_decrease_type = None
136 self.window_decrease_factor = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700137
James E. Blair83005782015-12-11 14:46:03 -0800138 @property
139 def actions(self):
140 return (
141 self.start_actions +
142 self.success_actions +
143 self.failure_actions +
144 self.merge_failure_actions +
145 self.disabled_actions
146 )
147
James E. Blaird09c17a2012-08-07 09:23:14 -0700148 def __repr__(self):
149 return '<Pipeline %s>' % self.name
150
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100151 def getSafeAttributes(self):
152 return Attributes(name=self.name)
153
James E. Blair4aea70c2012-07-26 14:23:24 -0700154 def setManager(self, manager):
155 self.manager = manager
156
James E. Blaire0487072012-08-29 17:38:31 -0700157 def addQueue(self, queue):
158 self.queues.append(queue)
159
160 def getQueue(self, project):
161 for queue in self.queues:
162 if project in queue.projects:
163 return queue
164 return None
165
James E. Blairbfb8e042014-12-30 17:01:44 -0800166 def removeQueue(self, queue):
Tobias Henkel6b9390f2017-03-28 11:23:21 +0200167 if queue in self.queues:
168 self.queues.remove(queue)
James E. Blairbfb8e042014-12-30 17:01:44 -0800169
James E. Blaire0487072012-08-29 17:38:31 -0700170 def getChangesInQueue(self):
171 changes = []
172 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700173 changes.extend([x.change for x in shared_queue.queue])
James E. Blaire0487072012-08-29 17:38:31 -0700174 return changes
175
James E. Blairfee8d652013-06-07 08:57:52 -0700176 def getAllItems(self):
177 items = []
James E. Blaire0487072012-08-29 17:38:31 -0700178 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700179 items.extend(shared_queue.queue)
James E. Blairfee8d652013-06-07 08:57:52 -0700180 return items
James E. Blaire0487072012-08-29 17:38:31 -0700181
Tobias Henkelb4407fc2017-07-07 13:52:56 +0200182 def formatStatusJSON(self, websocket_url=None):
James E. Blair8dbd56a2012-12-22 10:55:10 -0800183 j_pipeline = dict(name=self.name,
184 description=self.description)
185 j_queues = []
186 j_pipeline['change_queues'] = j_queues
187 for queue in self.queues:
188 j_queue = dict(name=queue.name)
189 j_queues.append(j_queue)
190 j_queue['heads'] = []
Clark Boylanaf2476f2014-01-23 14:47:36 -0800191 j_queue['window'] = queue.window
James E. Blair972e3c72013-08-29 12:04:55 -0700192
193 j_changes = []
194 for e in queue.queue:
195 if not e.item_ahead:
196 if j_changes:
197 j_queue['heads'].append(j_changes)
198 j_changes = []
Tobias Henkelb4407fc2017-07-07 13:52:56 +0200199 j_changes.append(e.formatJSON(websocket_url))
James E. Blair972e3c72013-08-29 12:04:55 -0700200 if (len(j_changes) > 1 and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000201 (j_changes[-2]['remaining_time'] is not None) and
202 (j_changes[-1]['remaining_time'] is not None)):
James E. Blair972e3c72013-08-29 12:04:55 -0700203 j_changes[-1]['remaining_time'] = max(
204 j_changes[-2]['remaining_time'],
205 j_changes[-1]['remaining_time'])
206 if j_changes:
James E. Blair8dbd56a2012-12-22 10:55:10 -0800207 j_queue['heads'].append(j_changes)
208 return j_pipeline
209
James E. Blair4aea70c2012-07-26 14:23:24 -0700210
James E. Blairee743612012-05-29 14:49:32 -0700211class ChangeQueue(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700212 """A ChangeQueue contains Changes to be processed related projects.
213
Monty Taylor82dfd412016-07-29 12:01:28 -0700214 A Pipeline with a DependentPipelineManager has multiple parallel
215 ChangeQueues shared by different projects. For instance, there may a
216 ChangeQueue shared by interrelated projects foo and bar, and a second queue
217 for independent project baz.
Monty Taylora42a55b2016-07-29 07:53:33 -0700218
Monty Taylor82dfd412016-07-29 12:01:28 -0700219 A Pipeline with an IndependentPipelineManager puts every Change into its
220 own ChangeQueue
Monty Taylora42a55b2016-07-29 07:53:33 -0700221
222 The ChangeQueue Window is inspired by TCP windows and controlls how many
223 Changes in a given ChangeQueue will be considered active and ready to
224 be processed. If a Change succeeds, the Window is increased by
225 `window_increase_factor`. If a Change fails, the Window is decreased by
226 `window_decrease_factor`.
Jesse Keating78f544a2017-07-13 14:27:40 -0700227
228 A ChangeQueue may be a dynamically created queue, which may be removed
229 from a DependentPipelineManager once empty.
Monty Taylora42a55b2016-07-29 07:53:33 -0700230 """
James E. Blairbfb8e042014-12-30 17:01:44 -0800231 def __init__(self, pipeline, window=0, window_floor=1,
Clark Boylan7603a372014-01-21 11:43:20 -0800232 window_increase_type='linear', window_increase_factor=1,
James E. Blair0dcef7a2016-08-19 09:35:17 -0700233 window_decrease_type='exponential', window_decrease_factor=2,
Jesse Keating78f544a2017-07-13 14:27:40 -0700234 name=None, dynamic=False):
James E. Blair4aea70c2012-07-26 14:23:24 -0700235 self.pipeline = pipeline
James E. Blair0dcef7a2016-08-19 09:35:17 -0700236 if name:
237 self.name = name
238 else:
239 self.name = ''
James E. Blairee743612012-05-29 14:49:32 -0700240 self.projects = []
241 self._jobs = set()
242 self.queue = []
Clark Boylan7603a372014-01-21 11:43:20 -0800243 self.window = window
244 self.window_floor = window_floor
245 self.window_increase_type = window_increase_type
246 self.window_increase_factor = window_increase_factor
247 self.window_decrease_type = window_decrease_type
248 self.window_decrease_factor = window_decrease_factor
Jesse Keating78f544a2017-07-13 14:27:40 -0700249 self.dynamic = dynamic
James E. Blairee743612012-05-29 14:49:32 -0700250
James E. Blair9f9667e2012-06-12 17:51:08 -0700251 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700252 return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
James E. Blairee743612012-05-29 14:49:32 -0700253
254 def getJobs(self):
255 return self._jobs
256
257 def addProject(self, project):
258 if project not in self.projects:
259 self.projects.append(project)
James E. Blairc8a1e052014-02-25 09:29:26 -0800260
James E. Blair0dcef7a2016-08-19 09:35:17 -0700261 if not self.name:
262 self.name = project.name
James E. Blairee743612012-05-29 14:49:32 -0700263
264 def enqueueChange(self, change):
James E. Blairbfb8e042014-12-30 17:01:44 -0800265 item = QueueItem(self, change)
James E. Blaircdccd972013-07-01 12:10:22 -0700266 self.enqueueItem(item)
267 item.enqueue_time = time.time()
268 return item
269
270 def enqueueItem(self, item):
James E. Blair4a035d92014-01-23 13:10:48 -0800271 item.pipeline = self.pipeline
James E. Blairbfb8e042014-12-30 17:01:44 -0800272 item.queue = self
273 if self.queue:
James E. Blairfee8d652013-06-07 08:57:52 -0700274 item.item_ahead = self.queue[-1]
James E. Blair972e3c72013-08-29 12:04:55 -0700275 item.item_ahead.items_behind.append(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700276 self.queue.append(item)
James E. Blairee743612012-05-29 14:49:32 -0700277
James E. Blairfee8d652013-06-07 08:57:52 -0700278 def dequeueItem(self, item):
279 if item in self.queue:
280 self.queue.remove(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700281 if item.item_ahead:
James E. Blair972e3c72013-08-29 12:04:55 -0700282 item.item_ahead.items_behind.remove(item)
283 for item_behind in item.items_behind:
284 if item.item_ahead:
285 item.item_ahead.items_behind.append(item_behind)
286 item_behind.item_ahead = item.item_ahead
James E. Blairfee8d652013-06-07 08:57:52 -0700287 item.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700288 item.items_behind = []
James E. Blairfee8d652013-06-07 08:57:52 -0700289 item.dequeue_time = time.time()
James E. Blaire0487072012-08-29 17:38:31 -0700290
James E. Blair972e3c72013-08-29 12:04:55 -0700291 def moveItem(self, item, item_ahead):
James E. Blair972e3c72013-08-29 12:04:55 -0700292 if item.item_ahead == item_ahead:
293 return False
294 # Remove from current location
295 if item.item_ahead:
296 item.item_ahead.items_behind.remove(item)
297 for item_behind in item.items_behind:
298 if item.item_ahead:
299 item.item_ahead.items_behind.append(item_behind)
300 item_behind.item_ahead = item.item_ahead
301 # Add to new location
302 item.item_ahead = item_ahead
James E. Blair00451262013-09-20 11:40:17 -0700303 item.items_behind = []
James E. Blair972e3c72013-08-29 12:04:55 -0700304 if item.item_ahead:
305 item.item_ahead.items_behind.append(item)
306 return True
James E. Blairee743612012-05-29 14:49:32 -0700307
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800308 def isActionable(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800309 if self.window:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800310 return item in self.queue[:self.window]
Clark Boylan7603a372014-01-21 11:43:20 -0800311 else:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800312 return True
Clark Boylan7603a372014-01-21 11:43:20 -0800313
314 def increaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800315 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800316 if self.window_increase_type == 'linear':
317 self.window += self.window_increase_factor
318 elif self.window_increase_type == 'exponential':
319 self.window *= self.window_increase_factor
320
321 def decreaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800322 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800323 if self.window_decrease_type == 'linear':
324 self.window = max(
325 self.window_floor,
326 self.window - self.window_decrease_factor)
327 elif self.window_decrease_type == 'exponential':
328 self.window = max(
329 self.window_floor,
Morgan Fainbergfdae2252016-06-07 20:13:20 -0700330 int(self.window / self.window_decrease_factor))
James E. Blairee743612012-05-29 14:49:32 -0700331
James E. Blair1e8dd892012-05-30 09:15:05 -0700332
James E. Blair4aea70c2012-07-26 14:23:24 -0700333class Project(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700334 """A Project represents a git repository such as openstack/nova."""
335
James E. Blaircf440a22016-07-15 09:11:58 -0700336 # NOTE: Projects should only be instantiated via a Source object
337 # so that they are associated with and cached by their Connection.
338 # This makes a Project instance a unique identifier for a given
339 # project from a given source.
340
James E. Blair0a899752017-03-29 13:22:16 -0700341 def __init__(self, name, source, foreign=False):
James E. Blair4aea70c2012-07-26 14:23:24 -0700342 self.name = name
James E. Blair8a395f92017-03-30 11:15:33 -0700343 self.source = source
James E. Blair0a899752017-03-29 13:22:16 -0700344 self.connection_name = source.connection.connection_name
345 self.canonical_hostname = source.canonical_hostname
James E. Blairc2a54fd2017-03-29 15:19:26 -0700346 self.canonical_name = source.canonical_hostname + '/' + name
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000347 # foreign projects are those referenced in dependencies
348 # of layout projects, this should matter
349 # when deciding whether to enqueue their changes
James E. Blaircf440a22016-07-15 09:11:58 -0700350 # TODOv3 (jeblair): re-add support for foreign projects if needed
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000351 self.foreign = foreign
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700352 self.unparsed_config = None
James E. Blaire3162022017-02-20 16:47:27 -0500353 self.unparsed_branch_config = {} # branch -> UnparsedTenantConfig
James E. Blair4aea70c2012-07-26 14:23:24 -0700354
355 def __str__(self):
356 return self.name
357
358 def __repr__(self):
359 return '<Project %s>' % (self.name)
360
Clark Boylana7f724c2017-10-25 11:35:19 -0700361 def getSafeAttributes(self):
362 return Attributes(name=self.name)
363
James E. Blair4aea70c2012-07-26 14:23:24 -0700364
James E. Blair34776ee2016-08-25 13:53:54 -0700365class Node(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700366 """A single node for use by a job.
367
368 This may represent a request for a node, or an actual node
369 provided by Nodepool.
370 """
371
James E. Blair16d96a02017-06-08 11:32:56 -0700372 def __init__(self, name, label):
James E. Blair34776ee2016-08-25 13:53:54 -0700373 self.name = name
James E. Blair16d96a02017-06-08 11:32:56 -0700374 self.label = label
James E. Blaircbf43672017-01-04 14:33:41 -0800375 self.id = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800376 self.lock = None
David Shrewsburyffab07a2017-07-24 12:45:07 -0400377 self.hold_job = None
David Shrewsburyf9af9df2017-08-01 15:19:26 -0400378 self.comment = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800379 # Attributes from Nodepool
380 self._state = 'unknown'
381 self.state_time = time.time()
Monty Taylor56f61332017-04-11 05:38:12 -0500382 self.interface_ip = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800383 self.public_ipv4 = None
384 self.private_ipv4 = None
385 self.public_ipv6 = None
Tristan Cacqueray80954402017-05-28 00:33:55 +0000386 self.ssh_port = 22
James E. Blaircacdf2b2017-01-04 13:14:37 -0800387 self._keys = []
Paul Belanger30ba93a2017-03-16 16:28:10 -0400388 self.az = None
389 self.provider = None
390 self.region = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800391
392 @property
393 def state(self):
394 return self._state
395
396 @state.setter
397 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800398 if value not in NODE_STATES:
399 raise TypeError("'%s' is not a valid state" % value)
James E. Blaira38c28e2017-01-04 10:33:20 -0800400 self._state = value
401 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700402
403 def __repr__(self):
James E. Blair16d96a02017-06-08 11:32:56 -0700404 return '<Node %s %s:%s>' % (self.id, self.name, self.label)
James E. Blair34776ee2016-08-25 13:53:54 -0700405
James E. Blair0d952152017-02-07 17:14:44 -0800406 def __ne__(self, other):
407 return not self.__eq__(other)
408
409 def __eq__(self, other):
410 if not isinstance(other, Node):
411 return False
412 return (self.name == other.name and
James E. Blair16d96a02017-06-08 11:32:56 -0700413 self.label == other.label and
James E. Blair0d952152017-02-07 17:14:44 -0800414 self.id == other.id)
415
James E. Blaircacdf2b2017-01-04 13:14:37 -0800416 def toDict(self):
417 d = {}
418 d['state'] = self.state
David Shrewsburyffab07a2017-07-24 12:45:07 -0400419 d['hold_job'] = self.hold_job
David Shrewsburyf9af9df2017-08-01 15:19:26 -0400420 d['comment'] = self.comment
James E. Blaircacdf2b2017-01-04 13:14:37 -0800421 for k in self._keys:
422 d[k] = getattr(self, k)
423 return d
424
James E. Blaira38c28e2017-01-04 10:33:20 -0800425 def updateFromDict(self, data):
426 self._state = data['state']
James E. Blaircacdf2b2017-01-04 13:14:37 -0800427 keys = []
428 for k, v in data.items():
429 if k == 'state':
430 continue
431 keys.append(k)
432 setattr(self, k, v)
433 self._keys = keys
James E. Blaira38c28e2017-01-04 10:33:20 -0800434
James E. Blair34776ee2016-08-25 13:53:54 -0700435
Monty Taylor7b19ba72017-05-24 07:42:54 -0500436class Group(object):
437 """A logical group of nodes for use by a job.
438
439 A Group is a named set of node names that will be provided to
440 jobs in the inventory to describe logical units where some subset of tasks
441 run.
442 """
443
444 def __init__(self, name, nodes):
445 self.name = name
446 self.nodes = nodes
447
448 def __repr__(self):
449 return '<Group %s %s>' % (self.name, str(self.nodes))
450
451 def __ne__(self, other):
452 return not self.__eq__(other)
453
454 def __eq__(self, other):
455 if not isinstance(other, Group):
456 return False
457 return (self.name == other.name and
458 self.nodes == other.nodes)
459
460 def toDict(self):
461 return {
462 'name': self.name,
463 'nodes': self.nodes
464 }
465
466
James E. Blaira98340f2016-09-02 11:33:49 -0700467class NodeSet(object):
468 """A set of nodes.
469
470 In configuration, NodeSets are attributes of Jobs indicating that
471 a Job requires nodes matching this description.
472
473 They may appear as top-level configuration objects and be named,
474 or they may appears anonymously in in-line job definitions.
475 """
476
477 def __init__(self, name=None):
478 self.name = name or ''
479 self.nodes = OrderedDict()
Monty Taylor7b19ba72017-05-24 07:42:54 -0500480 self.groups = OrderedDict()
James E. Blaira98340f2016-09-02 11:33:49 -0700481
James E. Blair1774dd52017-02-03 10:52:32 -0800482 def __ne__(self, other):
483 return not self.__eq__(other)
484
485 def __eq__(self, other):
486 if not isinstance(other, NodeSet):
487 return False
488 return (self.name == other.name and
489 self.nodes == other.nodes)
490
James E. Blaircbf43672017-01-04 14:33:41 -0800491 def copy(self):
492 n = NodeSet(self.name)
493 for name, node in self.nodes.items():
James E. Blair16d96a02017-06-08 11:32:56 -0700494 n.addNode(Node(node.name, node.label))
Monty Taylor7b19ba72017-05-24 07:42:54 -0500495 for name, group in self.groups.items():
496 n.addGroup(Group(group.name, group.nodes[:]))
James E. Blaircbf43672017-01-04 14:33:41 -0800497 return n
498
James E. Blaira98340f2016-09-02 11:33:49 -0700499 def addNode(self, node):
500 if node.name in self.nodes:
501 raise Exception("Duplicate node in %s" % (self,))
502 self.nodes[node.name] = node
503
James E. Blair0eaad552016-09-02 12:09:54 -0700504 def getNodes(self):
Clint Byruma4471d12017-05-10 20:57:40 -0400505 return list(self.nodes.values())
James E. Blair0eaad552016-09-02 12:09:54 -0700506
Monty Taylor7b19ba72017-05-24 07:42:54 -0500507 def addGroup(self, group):
508 if group.name in self.groups:
509 raise Exception("Duplicate group in %s" % (self,))
510 self.groups[group.name] = group
511
512 def getGroups(self):
513 return list(self.groups.values())
514
James E. Blaira98340f2016-09-02 11:33:49 -0700515 def __repr__(self):
516 if self.name:
517 name = self.name + ' '
518 else:
519 name = ''
Monty Taylor7b19ba72017-05-24 07:42:54 -0500520 return '<NodeSet %s%s%s>' % (name, self.nodes, self.groups)
James E. Blaira98340f2016-09-02 11:33:49 -0700521
Tristan Cacqueray82f864b2017-08-01 05:54:42 +0000522 def __len__(self):
523 return len(self.nodes)
524
James E. Blaira98340f2016-09-02 11:33:49 -0700525
James E. Blair34776ee2016-08-25 13:53:54 -0700526class NodeRequest(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700527 """A request for a set of nodes."""
528
James E. Blair8b2a1472017-02-19 15:33:55 -0800529 def __init__(self, requestor, build_set, job, nodeset):
530 self.requestor = requestor
James E. Blair34776ee2016-08-25 13:53:54 -0700531 self.build_set = build_set
532 self.job = job
James E. Blair0eaad552016-09-02 12:09:54 -0700533 self.nodeset = nodeset
James E. Blair803e94f2017-01-06 09:18:59 -0800534 self._state = STATE_REQUESTED
James E. Blair4f1731b2017-10-10 18:11:42 -0700535 self.requested_time = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -0800536 self.state_time = time.time()
537 self.stat = None
538 self.uid = uuid4().hex
James E. Blaircbf43672017-01-04 14:33:41 -0800539 self.id = None
James E. Blaircbbce0d2017-05-19 07:28:29 -0700540 # Zuul internal flags (not stored in ZK so they are not
James E. Blaira38c28e2017-01-04 10:33:20 -0800541 # overwritten).
542 self.failed = False
James E. Blaircbbce0d2017-05-19 07:28:29 -0700543 self.canceled = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800544
545 @property
Monty Taylor6dc5bc12017-09-29 15:47:31 -0500546 def priority(self):
Monty Taylorb5882052017-09-29 19:12:52 -0500547 if self.build_set:
548 precedence = self.build_set.item.pipeline.precedence
549 else:
550 precedence = PRECEDENCE_NORMAL
551 return PRIORITY_MAP[precedence]
Monty Taylor6dc5bc12017-09-29 15:47:31 -0500552
553 @property
James E. Blair6ab79e02017-01-06 10:10:17 -0800554 def fulfilled(self):
555 return (self._state == STATE_FULFILLED) and not self.failed
556
557 @property
James E. Blairdce6cea2016-12-20 16:45:32 -0800558 def state(self):
559 return self._state
560
561 @state.setter
562 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800563 if value not in REQUEST_STATES:
564 raise TypeError("'%s' is not a valid state" % value)
James E. Blairdce6cea2016-12-20 16:45:32 -0800565 self._state = value
566 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700567
568 def __repr__(self):
James E. Blaircbf43672017-01-04 14:33:41 -0800569 return '<NodeRequest %s %s>' % (self.id, self.nodeset)
James E. Blair34776ee2016-08-25 13:53:54 -0700570
James E. Blairdce6cea2016-12-20 16:45:32 -0800571 def toDict(self):
572 d = {}
James E. Blair16d96a02017-06-08 11:32:56 -0700573 nodes = [n.label for n in self.nodeset.getNodes()]
James E. Blairdce6cea2016-12-20 16:45:32 -0800574 d['node_types'] = nodes
James E. Blair8b2a1472017-02-19 15:33:55 -0800575 d['requestor'] = self.requestor
James E. Blairdce6cea2016-12-20 16:45:32 -0800576 d['state'] = self.state
577 d['state_time'] = self.state_time
578 return d
579
580 def updateFromDict(self, data):
581 self._state = data['state']
582 self.state_time = data['state_time']
583
James E. Blair34776ee2016-08-25 13:53:54 -0700584
James E. Blair01f83b72017-03-15 13:03:40 -0700585class Secret(object):
586 """A collection of private data.
587
588 In configuration, Secrets are collections of private data in
589 key-value pair format. They are defined as top-level
590 configuration objects and then referenced by Jobs.
591
592 """
593
James E. Blair8525e2b2017-03-15 14:05:47 -0700594 def __init__(self, name, source_context):
James E. Blair01f83b72017-03-15 13:03:40 -0700595 self.name = name
James E. Blair8525e2b2017-03-15 14:05:47 -0700596 self.source_context = source_context
James E. Blair01f83b72017-03-15 13:03:40 -0700597 # The secret data may or may not be encrypted. This attribute
598 # is named 'secret_data' to make it easy to search for and
599 # spot where it is directly used.
600 self.secret_data = {}
601
602 def __ne__(self, other):
603 return not self.__eq__(other)
604
605 def __eq__(self, other):
606 if not isinstance(other, Secret):
607 return False
608 return (self.name == other.name and
James E. Blair8525e2b2017-03-15 14:05:47 -0700609 self.source_context == other.source_context and
James E. Blair01f83b72017-03-15 13:03:40 -0700610 self.secret_data == other.secret_data)
611
612 def __repr__(self):
613 return '<Secret %s>' % (self.name,)
614
James E. Blair18f86a32017-03-15 14:43:26 -0700615 def decrypt(self, private_key):
616 """Return a copy of this secret with any encrypted data decrypted.
617 Note that the original remains encrypted."""
618
619 r = copy.deepcopy(self)
620 decrypted_secret_data = {}
621 for k, v in r.secret_data.items():
622 if hasattr(v, 'decrypt'):
623 decrypted_secret_data[k] = v.decrypt(private_key)
624 else:
625 decrypted_secret_data[k] = v
626 r.secret_data = decrypted_secret_data
627 return r
628
James E. Blair01f83b72017-03-15 13:03:40 -0700629
James E. Blaircdab2032017-02-01 09:09:29 -0800630class SourceContext(object):
631 """A reference to the branch of a project in configuration.
632
633 Jobs and playbooks reference this to keep track of where they
634 originate."""
635
James E. Blair6f140c72017-03-03 10:32:07 -0800636 def __init__(self, project, branch, path, trusted):
James E. Blaircdab2032017-02-01 09:09:29 -0800637 self.project = project
638 self.branch = branch
James E. Blair6f140c72017-03-03 10:32:07 -0800639 self.path = path
Monty Taylore6562aa2017-02-20 07:37:39 -0500640 self.trusted = trusted
James E. Blair7edc25f2017-10-26 10:47:14 -0700641 self.implied_branch_matchers = None
James E. Blaircdab2032017-02-01 09:09:29 -0800642
James E. Blair6f140c72017-03-03 10:32:07 -0800643 def __str__(self):
644 return '%s/%s@%s' % (self.project, self.path, self.branch)
645
James E. Blaircdab2032017-02-01 09:09:29 -0800646 def __repr__(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800647 return '<SourceContext %s trusted:%s>' % (str(self),
648 self.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800649
James E. Blaira7f51ca2017-02-07 16:01:26 -0800650 def __deepcopy__(self, memo):
651 return self.copy()
652
653 def copy(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800654 return self.__class__(self.project, self.branch, self.path,
655 self.trusted)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800656
Tristan Cacqueraye50af2e2017-09-19 14:18:28 +0000657 def isSameProject(self, other):
658 if not isinstance(other, SourceContext):
659 return False
660 return (self.project == other.project and
661 self.branch == other.branch and
662 self.trusted == other.trusted)
663
James E. Blaircdab2032017-02-01 09:09:29 -0800664 def __ne__(self, other):
665 return not self.__eq__(other)
666
667 def __eq__(self, other):
668 if not isinstance(other, SourceContext):
669 return False
670 return (self.project == other.project and
671 self.branch == other.branch and
James E. Blair6f140c72017-03-03 10:32:07 -0800672 self.path == other.path and
Monty Taylore6562aa2017-02-20 07:37:39 -0500673 self.trusted == other.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800674
675
James E. Blair66b274e2017-01-31 14:47:52 -0800676class PlaybookContext(object):
James E. Blaircdab2032017-02-01 09:09:29 -0800677
James E. Blair66b274e2017-01-31 14:47:52 -0800678 """A reference to a playbook in the context of a project.
679
680 Jobs refer to objects of this class for their main, pre, and post
681 playbooks so that we can keep track of which repos and security
James E. Blair74a82cf2017-07-12 17:23:08 -0700682 contexts are needed in order to run them.
James E. Blair66b274e2017-01-31 14:47:52 -0800683
James E. Blair74a82cf2017-07-12 17:23:08 -0700684 We also keep a list of roles so that playbooks only run with the
685 roles which were defined at the point the playbook was defined.
686
687 """
688
James E. Blair892cca62017-08-09 11:36:58 -0700689 def __init__(self, source_context, path, roles, secrets):
James E. Blaircdab2032017-02-01 09:09:29 -0800690 self.source_context = source_context
James E. Blair66b274e2017-01-31 14:47:52 -0800691 self.path = path
James E. Blair74a82cf2017-07-12 17:23:08 -0700692 self.roles = roles
James E. Blair892cca62017-08-09 11:36:58 -0700693 self.secrets = secrets
James E. Blair66b274e2017-01-31 14:47:52 -0800694
695 def __repr__(self):
James E. Blaircdab2032017-02-01 09:09:29 -0800696 return '<PlaybookContext %s %s>' % (self.source_context,
697 self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800698
699 def __ne__(self, other):
700 return not self.__eq__(other)
701
702 def __eq__(self, other):
703 if not isinstance(other, PlaybookContext):
704 return False
James E. Blaircdab2032017-02-01 09:09:29 -0800705 return (self.source_context == other.source_context and
James E. Blair74a82cf2017-07-12 17:23:08 -0700706 self.path == other.path and
James E. Blair892cca62017-08-09 11:36:58 -0700707 self.roles == other.roles and
708 self.secrets == other.secrets)
James E. Blair66b274e2017-01-31 14:47:52 -0800709
James E. Blairc32a8352017-10-11 16:27:50 -0700710 def copy(self):
711 r = PlaybookContext(self.source_context,
712 self.path,
713 self.roles,
714 self.secrets)
715 return r
716
James E. Blair66b274e2017-01-31 14:47:52 -0800717 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400718 # Render to a dict to use in passing json to the executor
James E. Blair892cca62017-08-09 11:36:58 -0700719 secrets = {}
720 for secret in self.secrets:
721 secret_data = copy.deepcopy(secret.secret_data)
722 secrets[secret.name] = secret_data
James E. Blair66b274e2017-01-31 14:47:52 -0800723 return dict(
James E. Blaircdab2032017-02-01 09:09:29 -0800724 connection=self.source_context.project.connection_name,
725 project=self.source_context.project.name,
726 branch=self.source_context.branch,
Monty Taylore6562aa2017-02-20 07:37:39 -0500727 trusted=self.source_context.trusted,
James E. Blair74a82cf2017-07-12 17:23:08 -0700728 roles=[r.toDict() for r in self.roles],
James E. Blair892cca62017-08-09 11:36:58 -0700729 secrets=secrets,
James E. Blaircdab2032017-02-01 09:09:29 -0800730 path=self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800731
732
Monty Taylorb934c1a2017-06-16 19:31:47 -0500733class Role(object, metaclass=abc.ABCMeta):
James E. Blair5ac93842017-01-20 06:47:34 -0800734 """A reference to an ansible role."""
735
736 def __init__(self, target_name):
737 self.target_name = target_name
738
739 @abc.abstractmethod
740 def __repr__(self):
741 pass
742
743 def __ne__(self, other):
744 return not self.__eq__(other)
745
746 @abc.abstractmethod
747 def __eq__(self, other):
748 if not isinstance(other, Role):
749 return False
750 return (self.target_name == other.target_name)
751
752 @abc.abstractmethod
753 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400754 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800755 return dict(target_name=self.target_name)
756
757
758class ZuulRole(Role):
759 """A reference to an ansible role in a Zuul project."""
760
James E. Blairbb94dfa2017-07-11 07:45:19 -0700761 def __init__(self, target_name, connection_name, project_name,
762 implicit=False):
James E. Blair5ac93842017-01-20 06:47:34 -0800763 super(ZuulRole, self).__init__(target_name)
764 self.connection_name = connection_name
765 self.project_name = project_name
James E. Blairbb94dfa2017-07-11 07:45:19 -0700766 self.implicit = implicit
James E. Blair5ac93842017-01-20 06:47:34 -0800767
768 def __repr__(self):
769 return '<ZuulRole %s %s>' % (self.project_name, self.target_name)
770
Clint Byrumaf7438f2017-05-10 17:26:57 -0400771 __hash__ = object.__hash__
772
James E. Blair5ac93842017-01-20 06:47:34 -0800773 def __eq__(self, other):
774 if not isinstance(other, ZuulRole):
775 return False
James E. Blairbb94dfa2017-07-11 07:45:19 -0700776 # Implicit is not consulted for equality so that we can handle
777 # implicit to explicit conversions.
James E. Blair5ac93842017-01-20 06:47:34 -0800778 return (super(ZuulRole, self).__eq__(other) and
James E. Blair1b27f6a2017-07-14 14:09:07 -0700779 self.connection_name == other.connection_name and
James E. Blair6563e4b2017-04-28 08:14:48 -0700780 self.project_name == other.project_name)
James E. Blair5ac93842017-01-20 06:47:34 -0800781
782 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400783 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800784 d = super(ZuulRole, self).toDict()
785 d['type'] = 'zuul'
786 d['connection'] = self.connection_name
787 d['project'] = self.project_name
James E. Blairbb94dfa2017-07-11 07:45:19 -0700788 d['implicit'] = self.implicit
James E. Blair5ac93842017-01-20 06:47:34 -0800789 return d
790
791
James E. Blairee743612012-05-29 14:49:32 -0700792class Job(object):
James E. Blair66b274e2017-01-31 14:47:52 -0800793
James E. Blaira7f51ca2017-02-07 16:01:26 -0800794 """A Job represents the defintion of actions to perform.
795
James E. Blaird4ade8c2017-02-19 15:25:46 -0800796 A Job is an abstract configuration concept. It describes what,
797 where, and under what circumstances something should be run
798 (contrast this with Build which is a concrete single execution of
799 a Job).
800
James E. Blaira7f51ca2017-02-07 16:01:26 -0800801 NB: Do not modify attributes of this class, set them directly
802 (e.g., "job.run = ..." rather than "job.run.append(...)").
803 """
Monty Taylora42a55b2016-07-29 07:53:33 -0700804
James E. Blairc32a8352017-10-11 16:27:50 -0700805 BASE_JOB_MARKER = object()
806
James E. Blairee743612012-05-29 14:49:32 -0700807 def __init__(self, name):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800808 # These attributes may override even the final form of a job
809 # in the context of a project-pipeline. They can not affect
810 # the execution of the job, but only whether the job is run
811 # and how it is reported.
812 self.context_attributes = dict(
813 voting=True,
814 hold_following_changes=False,
James E. Blair1774dd52017-02-03 10:52:32 -0800815 failure_message=None,
816 success_message=None,
817 failure_url=None,
818 success_url=None,
819 # Matchers. These are separate so they can be individually
820 # overidden.
821 branch_matcher=None,
822 file_matcher=None,
823 irrelevant_file_matcher=None, # skip-if
James E. Blaira7f51ca2017-02-07 16:01:26 -0800824 tags=frozenset(),
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200825 dependencies=frozenset(),
James E. Blair1774dd52017-02-03 10:52:32 -0800826 )
827
James E. Blaira7f51ca2017-02-07 16:01:26 -0800828 # These attributes affect how the job is actually run and more
829 # care must be taken when overriding them. If a job is
830 # declared "final", these may not be overriden in a
831 # project-pipeline.
832 self.execution_attributes = dict(
James E. Blairc32a8352017-10-11 16:27:50 -0700833 parent=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800834 timeout=None,
James E. Blair490cf042017-02-24 23:07:21 -0500835 variables={},
James E. Blaira7f51ca2017-02-07 16:01:26 -0800836 nodeset=NodeSet(),
James E. Blaira7f51ca2017-02-07 16:01:26 -0800837 workspace=None,
838 pre_run=(),
839 post_run=(),
840 run=(),
Tobias Henkel9a0e1942017-03-20 16:16:02 +0100841 semaphore=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800842 attempts=3,
843 final=False,
James E. Blair5fc81922017-07-12 13:19:37 -0700844 roles=(),
James E. Blair912322f2017-05-23 13:11:25 -0700845 required_projects={},
James E. Blairb3f5db12017-03-17 12:57:39 -0700846 allowed_projects=None,
James E. Blair7391ecb2017-05-23 13:32:40 -0700847 override_branch=None,
James E. Blairedff2c22017-10-30 14:04:48 -0700848 override_checkout=None,
James E. Blair8eb564a2017-08-10 09:21:41 -0700849 post_review=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800850 )
851
852 # These are generally internal attributes which are not
853 # accessible via configuration.
854 self.other_attributes = dict(
855 name=None,
856 source_context=None,
James E. Blair167d6cd2017-09-29 14:24:42 -0700857 source_line=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800858 inheritance_path=(),
James E. Blair698703c2017-09-15 20:58:30 -0600859 parent_data=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800860 )
861
862 self.inheritable_attributes = {}
863 self.inheritable_attributes.update(self.context_attributes)
864 self.inheritable_attributes.update(self.execution_attributes)
865 self.attributes = {}
866 self.attributes.update(self.inheritable_attributes)
867 self.attributes.update(self.other_attributes)
868
James E. Blairee743612012-05-29 14:49:32 -0700869 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800870
James E. Blair66b274e2017-01-31 14:47:52 -0800871 def __ne__(self, other):
872 return not self.__eq__(other)
873
Paul Belangere22baea2016-11-03 16:59:27 -0400874 def __eq__(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800875 # Compare the name and all inheritable attributes to determine
876 # whether two jobs with the same name are identically
877 # configured. Useful upon reconfiguration.
878 if not isinstance(other, Job):
879 return False
880 if self.name != other.name:
881 return False
882 for k, v in self.attributes.items():
883 if getattr(self, k) != getattr(other, k):
884 return False
885 return True
James E. Blairee743612012-05-29 14:49:32 -0700886
Clint Byrumaf7438f2017-05-10 17:26:57 -0400887 __hash__ = object.__hash__
888
James E. Blairee743612012-05-29 14:49:32 -0700889 def __str__(self):
890 return self.name
891
892 def __repr__(self):
James E. Blair167d6cd2017-09-29 14:24:42 -0700893 return '<Job %s branches: %s source: %s#%s>' % (
894 self.name,
895 self.branch_matcher,
896 self.source_context,
897 self.source_line)
James E. Blair83005782015-12-11 14:46:03 -0800898
James E. Blaira7f51ca2017-02-07 16:01:26 -0800899 def __getattr__(self, name):
900 v = self.__dict__.get(name)
901 if v is None:
James E. Blairaf8b2082017-10-03 15:38:27 -0700902 return self.attributes[name]
James E. Blaira7f51ca2017-02-07 16:01:26 -0800903 return v
904
905 def _get(self, name):
906 return self.__dict__.get(name)
907
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100908 def getSafeAttributes(self):
909 return Attributes(name=self.name)
910
James E. Blairc32a8352017-10-11 16:27:50 -0700911 def isBase(self):
912 return self.parent is self.BASE_JOB_MARKER
913
914 def setBase(self):
915 self.inheritance_path = self.inheritance_path + (repr(self),)
916
James E. Blair5fc81922017-07-12 13:19:37 -0700917 def addRoles(self, roles):
James E. Blairbb94dfa2017-07-11 07:45:19 -0700918 newroles = []
919 # Start with a copy of the existing roles, but if any of them
920 # are implicit roles which are identified as explicit in the
921 # new roles list, replace them with the explicit version.
922 changed = False
923 for existing_role in self.roles:
924 if existing_role in roles:
925 new_role = roles[roles.index(existing_role)]
926 else:
927 new_role = None
928 if (new_role and
929 isinstance(new_role, ZuulRole) and
930 isinstance(existing_role, ZuulRole) and
931 existing_role.implicit and not new_role.implicit):
932 newroles.append(new_role)
933 changed = True
934 else:
935 newroles.append(existing_role)
936 # Now add the new roles.
James E. Blair4eec8282017-07-12 17:33:26 -0700937 for role in reversed(roles):
James E. Blair5fc81922017-07-12 13:19:37 -0700938 if role not in newroles:
James E. Blair4eec8282017-07-12 17:33:26 -0700939 newroles.insert(0, role)
James E. Blairbb94dfa2017-07-11 07:45:19 -0700940 changed = True
941 if changed:
942 self.roles = tuple(newroles)
James E. Blair5fc81922017-07-12 13:19:37 -0700943
James E. Blaire74f5712017-09-29 15:14:31 -0700944 def setBranchMatcher(self, branches):
945 # Set the branch matcher to match any of the supplied branches
946 matchers = []
947 for branch in branches:
948 matchers.append(change_matcher.BranchMatcher(branch))
949 self.branch_matcher = change_matcher.MatchAny(matchers)
950
James E. Blair490cf042017-02-24 23:07:21 -0500951 def updateVariables(self, other_vars):
James E. Blairaf8b2082017-10-03 15:38:27 -0700952 v = copy.deepcopy(self.variables)
James E. Blair490cf042017-02-24 23:07:21 -0500953 Job._deepUpdate(v, other_vars)
954 self.variables = v
955
James E. Blair698703c2017-09-15 20:58:30 -0600956 def updateParentData(self, other_vars):
957 # Update variables, but give the current values priority (used
958 # for job return data which is lower precedence than defined
959 # job vars).
960 v = self.parent_data or {}
961 Job._deepUpdate(v, other_vars)
962 # To avoid running afoul of checks that jobs don't set zuul
963 # variables, remove them from parent data here.
964 if 'zuul' in v:
965 del v['zuul']
966 self.parent_data = v
967 v = copy.deepcopy(self.parent_data)
968 Job._deepUpdate(v, self.variables)
969 self.variables = v
970
James E. Blair912322f2017-05-23 13:11:25 -0700971 def updateProjects(self, other_projects):
James E. Blairaf8b2082017-10-03 15:38:27 -0700972 required_projects = self.required_projects.copy()
973 required_projects.update(other_projects)
James E. Blair912322f2017-05-23 13:11:25 -0700974 self.required_projects = required_projects
James E. Blair27f3dfc2017-05-23 13:07:28 -0700975
James E. Blair490cf042017-02-24 23:07:21 -0500976 @staticmethod
977 def _deepUpdate(a, b):
978 # Merge nested dictionaries if possible, otherwise, overwrite
979 # the value in 'a' with the value in 'b'.
980 for k, bv in b.items():
981 av = a.get(k)
982 if isinstance(av, dict) and isinstance(bv, dict):
983 Job._deepUpdate(av, bv)
984 else:
985 a[k] = bv
986
James E. Blaira7f51ca2017-02-07 16:01:26 -0800987 def copy(self):
988 job = Job(self.name)
989 for k in self.attributes:
990 if self._get(k) is not None:
991 setattr(job, k, copy.deepcopy(self._get(k)))
992 return job
993
James E. Blairc32a8352017-10-11 16:27:50 -0700994 def freezePlaybooks(self, pblist):
995 """Take a list of playbooks, and return a copy of it updated with this
996 job's roles.
997
998 """
999
1000 ret = []
1001 for old_pb in pblist:
1002 pb = old_pb.copy()
1003 pb.roles = self.roles
1004 ret.append(pb)
1005 return tuple(ret)
1006
James E. Blaira7f51ca2017-02-07 16:01:26 -08001007 def applyVariant(self, other):
1008 """Copy the attributes which have been set on the other job to this
1009 job."""
James E. Blair83005782015-12-11 14:46:03 -08001010 if not isinstance(other, Job):
1011 raise Exception("Job unable to inherit from %s" % (other,))
James E. Blaira7f51ca2017-02-07 16:01:26 -08001012
1013 for k in self.execution_attributes:
1014 if (other._get(k) is not None and
1015 k not in set(['final'])):
1016 if self.final:
1017 raise Exception("Unable to modify final job %s attribute "
1018 "%s=%s with variant %s" % (
1019 repr(self), k, other._get(k),
1020 repr(other)))
James E. Blairc32a8352017-10-11 16:27:50 -07001021 if k not in set(['pre_run', 'run', 'post_run', 'roles',
1022 'variables', 'required_projects']):
1023 # TODO(jeblair): determine if deepcopy is required
James E. Blaira7f51ca2017-02-07 16:01:26 -08001024 setattr(self, k, copy.deepcopy(other._get(k)))
1025
1026 # Don't set final above so that we don't trip an error halfway
1027 # through assignment.
1028 if other.final != self.attributes['final']:
1029 self.final = other.final
1030
James E. Blairc32a8352017-10-11 16:27:50 -07001031 # We must update roles before any playbook contexts
James E. Blair5ac93842017-01-20 06:47:34 -08001032 if other._get('roles') is not None:
James E. Blair5fc81922017-07-12 13:19:37 -07001033 self.addRoles(other.roles)
James E. Blairc32a8352017-10-11 16:27:50 -07001034
James E. Blairc32a8352017-10-11 16:27:50 -07001035 if other._get('run') is not None:
1036 other_run = self.freezePlaybooks(other.run)
1037 self.run = other_run
1038 if other._get('pre_run') is not None:
1039 other_pre_run = self.freezePlaybooks(other.pre_run)
1040 self.pre_run = self.pre_run + other_pre_run
1041 if other._get('post_run') is not None:
1042 other_post_run = self.freezePlaybooks(other.post_run)
1043 self.post_run = other_post_run + self.post_run
James E. Blair490cf042017-02-24 23:07:21 -05001044 if other._get('variables') is not None:
1045 self.updateVariables(other.variables)
James E. Blair912322f2017-05-23 13:11:25 -07001046 if other._get('required_projects') is not None:
1047 self.updateProjects(other.required_projects)
James E. Blaira7f51ca2017-02-07 16:01:26 -08001048
1049 for k in self.context_attributes:
1050 if (other._get(k) is not None and
1051 k not in set(['tags'])):
1052 setattr(self, k, copy.deepcopy(other._get(k)))
1053
1054 if other._get('tags') is not None:
1055 self.tags = self.tags.union(other.tags)
1056
James E. Blairc32a8352017-10-11 16:27:50 -07001057 self.inheritance_path = self.inheritance_path + (repr(other),)
James E. Blairee743612012-05-29 14:49:32 -07001058
James E. Blaire421a232012-07-25 16:59:21 -07001059 def changeMatches(self, change):
James E. Blair83005782015-12-11 14:46:03 -08001060 if self.branch_matcher and not self.branch_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -08001061 return False
1062
James E. Blair83005782015-12-11 14:46:03 -08001063 if self.file_matcher and not self.file_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -08001064 return False
1065
James E. Blair83005782015-12-11 14:46:03 -08001066 # NB: This is a negative match.
1067 if (self.irrelevant_file_matcher and
1068 self.irrelevant_file_matcher.matches(change)):
Maru Newby3fe5f852015-01-13 04:22:14 +00001069 return False
1070
James E. Blair70c71582013-03-06 08:50:50 -08001071 return True
James E. Blaire5a847f2012-07-10 15:29:14 -07001072
James E. Blair1e8dd892012-05-30 09:15:05 -07001073
James E. Blair912322f2017-05-23 13:11:25 -07001074class JobProject(object):
James E. Blair27f3dfc2017-05-23 13:07:28 -07001075 """ A reference to a project from a job. """
1076
James E. Blairedff2c22017-10-30 14:04:48 -07001077 def __init__(self, project_name, override_branch=None,
1078 override_checkout=None):
James E. Blair27f3dfc2017-05-23 13:07:28 -07001079 self.project_name = project_name
1080 self.override_branch = override_branch
James E. Blairedff2c22017-10-30 14:04:48 -07001081 self.override_checkout = override_checkout
James E. Blair27f3dfc2017-05-23 13:07:28 -07001082
1083
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001084class JobList(object):
1085 """ A list of jobs in a project's pipeline. """
Monty Taylora42a55b2016-07-29 07:53:33 -07001086
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001087 def __init__(self):
1088 self.jobs = OrderedDict() # job.name -> [job, ...]
James E. Blair12748b52017-02-07 17:17:53 -08001089
James E. Blairee743612012-05-29 14:49:32 -07001090 def addJob(self, job):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001091 if job.name in self.jobs:
1092 self.jobs[job.name].append(job)
1093 else:
1094 self.jobs[job.name] = [job]
James E. Blairee743612012-05-29 14:49:32 -07001095
James E. Blaire74f5712017-09-29 15:14:31 -07001096 def inheritFrom(self, other, implied_branch):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001097 for jobname, jobs in other.jobs.items():
James E. Blaire74f5712017-09-29 15:14:31 -07001098 joblist = self.jobs.setdefault(jobname, [])
1099 for job in jobs:
1100 if not job.branch_matcher and implied_branch:
1101 job = job.copy()
1102 job.setBranchMatcher([implied_branch])
James E. Blair2a664502017-10-27 11:39:33 -07001103 if job not in joblist:
1104 joblist.append(job)
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001105
1106
1107class JobGraph(object):
1108 """ A JobGraph represents the dependency graph between Job."""
1109
1110 def __init__(self):
1111 self.jobs = OrderedDict() # job_name -> Job
1112 self._dependencies = {} # dependent_job_name -> set(parent_job_names)
1113
1114 def __repr__(self):
1115 return '<JobGraph %s>' % (self.jobs)
1116
1117 def addJob(self, job):
1118 # A graph must be created after the job list is frozen,
1119 # therefore we should only get one job with the same name.
1120 if job.name in self.jobs:
1121 raise Exception("Job %s already added" % (job.name,))
1122 self.jobs[job.name] = job
1123 # Append the dependency information
1124 self._dependencies.setdefault(job.name, set())
1125 try:
1126 for dependency in job.dependencies:
1127 # Make sure a circular dependency is never created
1128 ancestor_jobs = self._getParentJobNamesRecursively(
1129 dependency, soft=True)
1130 ancestor_jobs.add(dependency)
1131 if any((job.name == anc_job) for anc_job in ancestor_jobs):
1132 raise Exception("Dependency cycle detected in job %s" %
1133 (job.name,))
1134 self._dependencies[job.name].add(dependency)
1135 except Exception:
1136 del self.jobs[job.name]
1137 del self._dependencies[job.name]
1138 raise
1139
1140 def getJobs(self):
Clint Byruma4471d12017-05-10 20:57:40 -04001141 return list(self.jobs.values()) # Report in the order of layout cfg
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001142
1143 def _getDirectDependentJobs(self, parent_job):
1144 ret = set()
1145 for dependent_name, parent_names in self._dependencies.items():
1146 if parent_job in parent_names:
1147 ret.add(dependent_name)
1148 return ret
1149
1150 def getDependentJobsRecursively(self, parent_job):
1151 all_dependent_jobs = set()
1152 jobs_to_iterate = set([parent_job])
1153 while len(jobs_to_iterate) > 0:
1154 current_job = jobs_to_iterate.pop()
1155 current_dependent_jobs = self._getDirectDependentJobs(current_job)
1156 new_dependent_jobs = current_dependent_jobs - all_dependent_jobs
1157 jobs_to_iterate |= new_dependent_jobs
1158 all_dependent_jobs |= new_dependent_jobs
1159 return [self.jobs[name] for name in all_dependent_jobs]
1160
1161 def getParentJobsRecursively(self, dependent_job):
1162 return [self.jobs[name] for name in
1163 self._getParentJobNamesRecursively(dependent_job)]
1164
1165 def _getParentJobNamesRecursively(self, dependent_job, soft=False):
1166 all_parent_jobs = set()
1167 jobs_to_iterate = set([dependent_job])
1168 while len(jobs_to_iterate) > 0:
1169 current_job = jobs_to_iterate.pop()
1170 current_parent_jobs = self._dependencies.get(current_job)
1171 if current_parent_jobs is None:
1172 if soft:
1173 current_parent_jobs = set()
1174 else:
1175 raise Exception("Dependent job %s not found: " %
1176 (dependent_job,))
1177 new_parent_jobs = current_parent_jobs - all_parent_jobs
1178 jobs_to_iterate |= new_parent_jobs
1179 all_parent_jobs |= new_parent_jobs
1180 return all_parent_jobs
James E. Blairb97ed802015-12-21 15:55:35 -08001181
James E. Blair1e8dd892012-05-30 09:15:05 -07001182
James E. Blair4aea70c2012-07-26 14:23:24 -07001183class Build(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001184 """A Build is an instance of a single execution of a Job.
1185
1186 While a Job describes what to run, a Build describes an actual
1187 execution of that Job. Each build is associated with exactly one
1188 Job (related builds are grouped together in a BuildSet).
1189 """
Monty Taylora42a55b2016-07-29 07:53:33 -07001190
James E. Blair4aea70c2012-07-26 14:23:24 -07001191 def __init__(self, job, uuid):
1192 self.job = job
1193 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -07001194 self.url = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001195 self.result = None
James E. Blair196f61a2017-06-30 15:42:29 -07001196 self.result_data = {}
James E. Blair6f699732017-07-18 14:19:11 -07001197 self.error_detail = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001198 self.build_set = None
Paul Belanger174a8272017-03-14 13:20:10 -04001199 self.execute_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -08001200 self.start_time = None
1201 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -07001202 self.estimated_time = None
James E. Blair0aac4872013-08-23 14:02:38 -07001203 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -07001204 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -07001205 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +08001206 self.worker = Worker()
Timothy Chavezb2332082015-08-07 20:08:04 -05001207 self.node_labels = []
1208 self.node_name = None
James E. Blairee743612012-05-29 14:49:32 -07001209
1210 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +08001211 return ('<Build %s of %s on %s>' %
1212 (self.uuid, self.job.name, self.worker))
1213
James E. Blair3a098dd2017-10-04 14:37:29 -07001214 @property
1215 def pipeline(self):
1216 return self.build_set.item.pipeline
1217
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001218 def getSafeAttributes(self):
James E. Blair196f61a2017-06-30 15:42:29 -07001219 return Attributes(uuid=self.uuid,
1220 result=self.result,
James E. Blair6f699732017-07-18 14:19:11 -07001221 error_detail=self.error_detail,
James E. Blair196f61a2017-06-30 15:42:29 -07001222 result_data=self.result_data)
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001223
Joshua Heskethba8776a2014-01-12 14:35:40 +08001224
1225class Worker(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001226 """Information about the specific worker executing a Build."""
Joshua Heskethba8776a2014-01-12 14:35:40 +08001227 def __init__(self):
1228 self.name = "Unknown"
1229 self.hostname = None
Monty Taylor0dbe1592017-06-11 10:57:27 -05001230 self.log_port = None
Joshua Heskethba8776a2014-01-12 14:35:40 +08001231
1232 def updateFromData(self, data):
1233 """Update worker information if contained in the WORK_DATA response."""
1234 self.name = data.get('worker_name', self.name)
1235 self.hostname = data.get('worker_hostname', self.hostname)
Monty Taylor0dbe1592017-06-11 10:57:27 -05001236 self.log_port = data.get('worker_log_port', self.log_port)
Joshua Heskethba8776a2014-01-12 14:35:40 +08001237
1238 def __repr__(self):
1239 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -07001240
James E. Blair1e8dd892012-05-30 09:15:05 -07001241
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001242class RepoFiles(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001243 """RepoFiles holds config-file content for per-project job config.
1244
1245 When Zuul asks a merger to prepare a future multiple-repo state
1246 and collect Zuul configuration files so that we can dynamically
1247 load our configuration, this class provides cached access to that
1248 data for use by the Change which updated the config files and any
1249 changes that follow it in a ChangeQueue.
1250
1251 It is attached to a BuildSet since the content of Zuul
1252 configuration files can change with each new BuildSet.
1253 """
1254
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001255 def __init__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001256 self.connections = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001257
1258 def __repr__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001259 return '<RepoFiles %s>' % self.connections
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001260
1261 def setFiles(self, items):
James E. Blair2a535672017-04-27 12:03:15 -07001262 self.hostnames = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001263 for item in items:
James E. Blair2a535672017-04-27 12:03:15 -07001264 connection = self.connections.setdefault(
1265 item['connection'], {})
1266 project = connection.setdefault(item['project'], {})
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001267 branch = project.setdefault(item['branch'], {})
1268 branch.update(item['files'])
1269
James E. Blair2a535672017-04-27 12:03:15 -07001270 def getFile(self, connection_name, project_name, branch, fn):
1271 host = self.connections.get(connection_name, {})
1272 return host.get(project_name, {}).get(branch, {}).get(fn)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001273
1274
James E. Blair7e530ad2012-07-03 16:12:28 -07001275class BuildSet(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001276 """A collection of Builds for one specific potential future repository
1277 state.
Monty Taylora42a55b2016-07-29 07:53:33 -07001278
Paul Belanger174a8272017-03-14 13:20:10 -04001279 When Zuul executes Builds for a change, it creates a Build to
James E. Blaird4ade8c2017-02-19 15:25:46 -08001280 represent each execution of each job and a BuildSet to keep track
Paul Belanger174a8272017-03-14 13:20:10 -04001281 of all the Builds running for that Change. When Zuul re-executes
James E. Blaird4ade8c2017-02-19 15:25:46 -08001282 Builds for a Change with a different configuration, all of the
1283 running Builds in the BuildSet for that change are aborted, and a
1284 new BuildSet is created to hold the Builds for the Jobs being
1285 run with the new configuration.
1286
1287 A BuildSet also holds the UUID used to produce the Zuul Ref that
1288 builders check out.
1289
Monty Taylora42a55b2016-07-29 07:53:33 -07001290 """
James E. Blair4076e2b2014-01-28 12:42:20 -08001291 # Merge states:
1292 NEW = 1
1293 PENDING = 2
1294 COMPLETE = 3
1295
Antoine Musso9b229282014-08-18 23:45:43 +02001296 states_map = {
1297 1: 'NEW',
1298 2: 'PENDING',
1299 3: 'COMPLETE',
1300 }
1301
James E. Blairfee8d652013-06-07 08:57:52 -07001302 def __init__(self, item):
1303 self.item = item
James E. Blair7e530ad2012-07-03 16:12:28 -07001304 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -07001305 self.result = None
Jamie Lennox3f16de52017-05-09 14:24:11 +10001306 self.uuid = None
James E. Blair81515ad2012-10-01 18:29:08 -07001307 self.commit = None
James E. Blair9e5b8112017-10-19 08:12:24 -07001308 self.dependent_changes = None
James E. Blair1960d682017-04-28 15:44:14 -07001309 self.merger_items = None
James E. Blair973721f2012-08-15 10:19:43 -07001310 self.unable_to_merge = False
James E. Blaire53250c2017-03-01 14:34:36 -08001311 self.config_error = None # None or an error message string.
James E. Blair972e3c72013-08-29 12:04:55 -07001312 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -08001313 self.merge_state = self.NEW
James E. Blair0eaad552016-09-02 12:09:54 -07001314 self.nodesets = {} # job -> nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001315 self.node_requests = {} # job -> reqs
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001316 self.files = RepoFiles()
James E. Blair1960d682017-04-28 15:44:14 -07001317 self.repo_state = {}
Paul Belanger71d98172016-11-08 10:56:31 -05001318 self.tries = {}
James E. Blair7e530ad2012-07-03 16:12:28 -07001319
Jamie Lennox3f16de52017-05-09 14:24:11 +10001320 @property
1321 def ref(self):
1322 # NOTE(jamielennox): The concept of buildset ref is to be removed and a
1323 # buildset UUID identifier available instead. Currently the ref is
1324 # checked to see if the BuildSet has been configured.
1325 return 'Z' + self.uuid if self.uuid else None
1326
Antoine Musso9b229282014-08-18 23:45:43 +02001327 def __repr__(self):
1328 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
1329 self.item,
1330 len(self.builds),
1331 self.getStateName(self.merge_state))
1332
James E. Blair4886cc12012-07-18 15:39:41 -07001333 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -07001334 # The change isn't enqueued until after it's created
1335 # so we don't know what the other changes ahead will be
1336 # until jobs start.
James E. Blair9e5b8112017-10-19 08:12:24 -07001337 if not self.uuid:
1338 self.uuid = uuid4().hex
1339 if self.dependent_changes is None:
1340 items = [self.item]
James E. Blairfee8d652013-06-07 08:57:52 -07001341 next_item = self.item.item_ahead
1342 while next_item:
James E. Blair1960d682017-04-28 15:44:14 -07001343 items.append(next_item)
James E. Blairfee8d652013-06-07 08:57:52 -07001344 next_item = next_item.item_ahead
James E. Blair1960d682017-04-28 15:44:14 -07001345 items.reverse()
James E. Blair9e5b8112017-10-19 08:12:24 -07001346 self.dependent_changes = [i.change.toDict() for i in items]
James E. Blair1960d682017-04-28 15:44:14 -07001347 self.merger_items = [i.makeMergerItem() for i in items]
James E. Blair4886cc12012-07-18 15:39:41 -07001348
Antoine Musso9b229282014-08-18 23:45:43 +02001349 def getStateName(self, state_num):
1350 return self.states_map.get(
1351 state_num, 'UNKNOWN (%s)' % state_num)
1352
James E. Blair4886cc12012-07-18 15:39:41 -07001353 def addBuild(self, build):
1354 self.builds[build.job.name] = build
Paul Belanger71d98172016-11-08 10:56:31 -05001355 if build.job.name not in self.tries:
James E. Blair5aed1112016-12-15 09:18:05 -08001356 self.tries[build.job.name] = 1
James E. Blair4886cc12012-07-18 15:39:41 -07001357 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -07001358
James E. Blair4a28a882013-08-23 15:17:33 -07001359 def removeBuild(self, build):
Paul Belanger71d98172016-11-08 10:56:31 -05001360 self.tries[build.job.name] += 1
James E. Blair4a28a882013-08-23 15:17:33 -07001361 del self.builds[build.job.name]
1362
James E. Blair7e530ad2012-07-03 16:12:28 -07001363 def getBuild(self, job_name):
1364 return self.builds.get(job_name)
1365
James E. Blair11700c32012-07-05 17:50:05 -07001366 def getBuilds(self):
Clint Byrum1d0c7d12017-05-10 19:40:53 -07001367 keys = list(self.builds.keys())
James E. Blair11700c32012-07-05 17:50:05 -07001368 keys.sort()
1369 return [self.builds.get(x) for x in keys]
1370
James E. Blair0eaad552016-09-02 12:09:54 -07001371 def getJobNodeSet(self, job_name):
1372 # Return None if not provisioned; empty NodeSet if no nodes
1373 # required
1374 return self.nodesets.get(job_name)
James E. Blair8d692392016-04-08 17:47:58 -07001375
James E. Blaire18d4602017-01-05 11:17:28 -08001376 def removeJobNodeSet(self, job_name):
1377 if job_name not in self.nodesets:
1378 raise Exception("No job set for %s" % (job_name))
1379 del self.nodesets[job_name]
1380
James E. Blair8d692392016-04-08 17:47:58 -07001381 def setJobNodeRequest(self, job_name, req):
1382 if job_name in self.node_requests:
1383 raise Exception("Prior node request for %s" % (job_name))
1384 self.node_requests[job_name] = req
1385
1386 def getJobNodeRequest(self, job_name):
1387 return self.node_requests.get(job_name)
1388
James E. Blair0eaad552016-09-02 12:09:54 -07001389 def jobNodeRequestComplete(self, job_name, req, nodeset):
1390 if job_name in self.nodesets:
James E. Blair8d692392016-04-08 17:47:58 -07001391 raise Exception("Prior node request for %s" % (job_name))
James E. Blair0eaad552016-09-02 12:09:54 -07001392 self.nodesets[job_name] = nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001393 del self.node_requests[job_name]
1394
Paul Belanger71d98172016-11-08 10:56:31 -05001395 def getTries(self, job_name):
Clint Byrum804073b2017-05-10 17:47:45 -04001396 return self.tries.get(job_name, 0)
Paul Belanger71d98172016-11-08 10:56:31 -05001397
James E. Blair0ffa0102017-03-30 13:11:33 -07001398 def getMergeMode(self):
James E. Blair1960d682017-04-28 15:44:14 -07001399 # We may be called before this build set has a shadow layout
1400 # (ie, we are called to perform the merge to create that
1401 # layout). It's possible that the change we are merging will
1402 # update the merge-mode for the project, but there's not much
1403 # we can do about that here. Instead, do the best we can by
1404 # using the nearest shadow layout to determine the merge mode,
1405 # or if that fails, the current live layout, or if that fails,
1406 # use the default: merge-resolve.
1407 item = self.item
1408 layout = None
1409 while item:
James E. Blair29a24fd2017-10-02 15:04:56 -07001410 layout = item.layout
James E. Blair1960d682017-04-28 15:44:14 -07001411 if layout:
1412 break
1413 item = item.item_ahead
1414 if not layout:
1415 layout = self.item.pipeline.layout
1416 if layout:
James E. Blair0ffa0102017-03-30 13:11:33 -07001417 project = self.item.change.project
James E. Blair1960d682017-04-28 15:44:14 -07001418 project_config = layout.project_configs.get(
James E. Blair0ffa0102017-03-30 13:11:33 -07001419 project.canonical_name)
1420 if project_config:
1421 return project_config.merge_mode
1422 return MERGER_MERGE_RESOLVE
Adam Gandelman8bd57102016-12-02 12:58:42 -08001423
Jamie Lennox3f16de52017-05-09 14:24:11 +10001424 def getSafeAttributes(self):
1425 return Attributes(uuid=self.uuid)
1426
James E. Blair7e530ad2012-07-03 16:12:28 -07001427
James E. Blairfee8d652013-06-07 08:57:52 -07001428class QueueItem(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001429 """Represents the position of a Change in a ChangeQueue.
1430
1431 All Changes are enqueued into ChangeQueue in a QueueItem. The QueueItem
1432 holds the current `BuildSet` as well as all previous `BuildSets` that were
1433 produced for this `QueueItem`.
1434 """
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001435 log = logging.getLogger("zuul.QueueItem")
James E. Blair32663402012-06-01 10:04:18 -07001436
James E. Blairbfb8e042014-12-30 17:01:44 -08001437 def __init__(self, queue, change):
1438 self.pipeline = queue.pipeline
1439 self.queue = queue
Monty Taylor55d0f562017-05-17 14:30:21 -05001440 self.change = change # a ref
James E. Blaircaec0c52012-08-22 14:52:22 -07001441 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -07001442 self.current_build_set = BuildSet(self)
James E. Blairfee8d652013-06-07 08:57:52 -07001443 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -07001444 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -08001445 self.enqueue_time = None
1446 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -07001447 self.reported = False
Tobias Henkel9842bd72017-05-16 13:40:03 +02001448 self.reported_start = False
1449 self.quiet = False
James E. Blairbfb8e042014-12-30 17:01:44 -08001450 self.active = False # Whether an item is within an active window
1451 self.live = True # Whether an item is intended to be processed at all
James E. Blair29a24fd2017-10-02 15:04:56 -07001452 self.layout = None
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001453 self.job_graph = None
James E. Blaire5a847f2012-07-10 15:29:14 -07001454
James E. Blair972e3c72013-08-29 12:04:55 -07001455 def __repr__(self):
1456 if self.pipeline:
1457 pipeline = self.pipeline.name
1458 else:
1459 pipeline = None
1460 return '<QueueItem 0x%x for %s in %s>' % (
1461 id(self), self.change, pipeline)
1462
James E. Blairee743612012-05-29 14:49:32 -07001463 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -07001464 self.current_build_set = BuildSet(self)
James E. Blair29a24fd2017-10-02 15:04:56 -07001465 self.layout = None
James E. Blairc9455002017-09-06 09:22:19 -07001466 self.job_graph = None
James E. Blairee743612012-05-29 14:49:32 -07001467
1468 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -07001469 self.current_build_set.addBuild(build)
James E. Blairee743612012-05-29 14:49:32 -07001470
James E. Blair4a28a882013-08-23 15:17:33 -07001471 def removeBuild(self, build):
1472 self.current_build_set.removeBuild(build)
1473
James E. Blairfee8d652013-06-07 08:57:52 -07001474 def setReportedResult(self, result):
1475 self.current_build_set.result = result
1476
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001477 def freezeJobGraph(self):
James E. Blair83005782015-12-11 14:46:03 -08001478 """Find or create actual matching jobs for this item's change and
1479 store the resulting job tree."""
James E. Blair29a24fd2017-10-02 15:04:56 -07001480 job_graph = self.layout.createJobGraph(self)
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001481 for job in job_graph.getJobs():
1482 # Ensure that each jobs's dependencies are fully
1483 # accessible. This will raise an exception if not.
1484 job_graph.getParentJobsRecursively(job.name)
1485 self.job_graph = job_graph
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001486
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001487 def hasJobGraph(self):
1488 """Returns True if the item has a job graph."""
1489 return self.job_graph is not None
James E. Blair83005782015-12-11 14:46:03 -08001490
1491 def getJobs(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001492 if not self.live or not self.job_graph:
James E. Blair83005782015-12-11 14:46:03 -08001493 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001494 return self.job_graph.getJobs()
1495
1496 def getJob(self, name):
1497 if not self.job_graph:
1498 return None
1499 return self.job_graph.jobs.get(name)
James E. Blair83005782015-12-11 14:46:03 -08001500
James E. Blairdbfd3282016-07-21 10:46:19 -07001501 def haveAllJobsStarted(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001502 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001503 return False
1504 for job in self.getJobs():
1505 build = self.current_build_set.getBuild(job.name)
1506 if not build or not build.start_time:
1507 return False
1508 return True
1509
1510 def areAllJobsComplete(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001511 if (self.current_build_set.config_error or
1512 self.current_build_set.unable_to_merge):
1513 return True
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001514 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001515 return False
1516 for job in self.getJobs():
1517 build = self.current_build_set.getBuild(job.name)
1518 if not build or not build.result:
1519 return False
1520 return True
1521
1522 def didAllJobsSucceed(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001523 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001524 return False
1525 for job in self.getJobs():
1526 if not job.voting:
1527 continue
1528 build = self.current_build_set.getBuild(job.name)
1529 if not build:
1530 return False
1531 if build.result != 'SUCCESS':
1532 return False
1533 return True
1534
1535 def didAnyJobFail(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001536 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001537 return False
1538 for job in self.getJobs():
1539 if not job.voting:
1540 continue
1541 build = self.current_build_set.getBuild(job.name)
1542 if build and build.result and (build.result != 'SUCCESS'):
1543 return True
1544 return False
1545
1546 def didMergerFail(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001547 return self.current_build_set.unable_to_merge
1548
1549 def getConfigError(self):
1550 return self.current_build_set.config_error
James E. Blairdbfd3282016-07-21 10:46:19 -07001551
James E. Blair0d3e83b2017-06-05 13:51:57 -07001552 def wasDequeuedNeedingChange(self):
1553 return self.dequeued_needing_change
1554
James E. Blair8c2d5812017-10-06 09:29:21 -07001555 def includesConfigUpdates(self):
1556 includes_trusted = False
1557 includes_untrusted = False
1558 tenant = self.pipeline.layout.tenant
1559 item = self
1560 while item:
1561 if item.change.updatesConfig():
1562 (trusted, project) = tenant.getProject(
1563 item.change.project.canonical_name)
1564 if trusted:
1565 includes_trusted = True
1566 else:
1567 includes_untrusted = True
1568 if includes_trusted and includes_untrusted:
1569 # We're done early
1570 return (includes_trusted, includes_untrusted)
1571 item = item.item_ahead
1572 return (includes_trusted, includes_untrusted)
1573
James E. Blairdbfd3282016-07-21 10:46:19 -07001574 def isHoldingFollowingChanges(self):
1575 if not self.live:
1576 return False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001577 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001578 return False
1579 for job in self.getJobs():
1580 if not job.hold_following_changes:
1581 continue
1582 build = self.current_build_set.getBuild(job.name)
1583 if not build:
1584 return True
1585 if build.result != 'SUCCESS':
1586 return True
1587
1588 if not self.item_ahead:
1589 return False
1590 return self.item_ahead.isHoldingFollowingChanges()
1591
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001592 def findJobsToRun(self, semaphore_handler):
James E. Blairdbfd3282016-07-21 10:46:19 -07001593 torun = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001594 if not self.live:
1595 return []
1596 if not self.job_graph:
1597 return []
James E. Blair791b5392016-08-03 11:25:56 -07001598 if self.item_ahead:
1599 # Only run jobs if any 'hold' jobs on the change ahead
1600 # have completed successfully.
1601 if self.item_ahead.isHoldingFollowingChanges():
1602 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001603
1604 successful_job_names = set()
1605 jobs_not_started = set()
1606 for job in self.job_graph.getJobs():
1607 build = self.current_build_set.getBuild(job.name)
1608 if build:
1609 if build.result == 'SUCCESS':
1610 successful_job_names.add(job.name)
1611 else:
1612 jobs_not_started.add(job)
1613
James E. Blair698703c2017-09-15 20:58:30 -06001614 # Attempt to run jobs in the order they appear in
1615 # configuration.
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001616 for job in self.job_graph.getJobs():
1617 if job not in jobs_not_started:
1618 continue
1619 all_parent_jobs_successful = True
Tobias Henkela96c9b32017-10-22 12:38:06 +02001620 parent_builds_with_data = {}
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001621 for parent_job in self.job_graph.getParentJobsRecursively(
1622 job.name):
1623 if parent_job.name not in successful_job_names:
1624 all_parent_jobs_successful = False
1625 break
James E. Blair698703c2017-09-15 20:58:30 -06001626 parent_build = self.current_build_set.getBuild(parent_job.name)
1627 if parent_build.result_data:
Tobias Henkela96c9b32017-10-22 12:38:06 +02001628 parent_builds_with_data[parent_job.name] = parent_build
1629
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001630 if all_parent_jobs_successful:
Tobias Henkela96c9b32017-10-22 12:38:06 +02001631 # Iterate in reverse order over all jobs of the graph (which is
1632 # in sorted config order) and apply parent data of the jobs we
1633 # already found.
1634 if len(parent_builds_with_data) > 0:
1635 for parent_job in reversed(self.job_graph.getJobs()):
1636 parent_build = parent_builds_with_data.get(
1637 parent_job.name)
1638 if parent_build:
1639 job.updateParentData(parent_build.result_data)
1640
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001641 nodeset = self.current_build_set.getJobNodeSet(job.name)
1642 if nodeset is None:
1643 # The nodes for this job are not ready, skip
1644 # it for now.
James E. Blairdbfd3282016-07-21 10:46:19 -07001645 continue
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001646 if semaphore_handler.acquire(self, job):
1647 # If this job needs a semaphore, either acquire it or
1648 # make sure that we have it before running the job.
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001649 torun.append(job)
James E. Blairdbfd3282016-07-21 10:46:19 -07001650 return torun
1651
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001652 def findJobsToRequest(self):
James E. Blair6ab79e02017-01-06 10:10:17 -08001653 build_set = self.current_build_set
James E. Blairdbfd3282016-07-21 10:46:19 -07001654 toreq = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001655 if not self.live:
1656 return []
1657 if not self.job_graph:
1658 return []
James E. Blair6ab79e02017-01-06 10:10:17 -08001659 if self.item_ahead:
1660 if self.item_ahead.isHoldingFollowingChanges():
1661 return []
James E. Blairdbfd3282016-07-21 10:46:19 -07001662
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001663 successful_job_names = set()
1664 jobs_not_requested = set()
1665 for job in self.job_graph.getJobs():
1666 build = build_set.getBuild(job.name)
1667 if build and build.result == 'SUCCESS':
1668 successful_job_names.add(job.name)
1669 else:
1670 nodeset = build_set.getJobNodeSet(job.name)
1671 if nodeset is None:
1672 req = build_set.getJobNodeRequest(job.name)
1673 if req is None:
1674 jobs_not_requested.add(job)
1675
1676 # Attempt to request nodes for jobs in the order jobs appear
1677 # in configuration.
1678 for job in self.job_graph.getJobs():
1679 if job not in jobs_not_requested:
1680 continue
1681 all_parent_jobs_successful = True
1682 for parent_job in self.job_graph.getParentJobsRecursively(
1683 job.name):
1684 if parent_job.name not in successful_job_names:
1685 all_parent_jobs_successful = False
1686 break
1687 if all_parent_jobs_successful:
1688 toreq.append(job)
1689 return toreq
James E. Blairdbfd3282016-07-21 10:46:19 -07001690
1691 def setResult(self, build):
1692 if build.retry:
1693 self.removeBuild(build)
1694 elif build.result != 'SUCCESS':
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001695 for job in self.job_graph.getDependentJobsRecursively(
1696 build.job.name):
James E. Blairdbfd3282016-07-21 10:46:19 -07001697 fakebuild = Build(job, None)
1698 fakebuild.result = 'SKIPPED'
1699 self.addBuild(fakebuild)
1700
James E. Blair6ab79e02017-01-06 10:10:17 -08001701 def setNodeRequestFailure(self, job):
1702 fakebuild = Build(job, None)
1703 self.addBuild(fakebuild)
1704 fakebuild.result = 'NODE_FAILURE'
1705 self.setResult(fakebuild)
1706
James E. Blairdbfd3282016-07-21 10:46:19 -07001707 def setDequeuedNeedingChange(self):
1708 self.dequeued_needing_change = True
1709 self._setAllJobsSkipped()
1710
1711 def setUnableToMerge(self):
1712 self.current_build_set.unable_to_merge = True
1713 self._setAllJobsSkipped()
1714
James E. Blaire53250c2017-03-01 14:34:36 -08001715 def setConfigError(self, error):
1716 self.current_build_set.config_error = error
1717 self._setAllJobsSkipped()
1718
James E. Blairdbfd3282016-07-21 10:46:19 -07001719 def _setAllJobsSkipped(self):
1720 for job in self.getJobs():
1721 fakebuild = Build(job, None)
1722 fakebuild.result = 'SKIPPED'
1723 self.addBuild(fakebuild)
1724
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001725 def formatUrlPattern(self, url_pattern, job=None, build=None):
1726 url = None
1727 # Produce safe versions of objects which may be useful in
1728 # result formatting, but don't allow users to crawl through
1729 # the entire data structure where they might be able to access
1730 # secrets, etc.
1731 safe_change = self.change.getSafeAttributes()
1732 safe_pipeline = self.pipeline.getSafeAttributes()
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001733 safe_tenant = self.pipeline.layout.tenant.getSafeAttributes()
Jamie Lennox3f16de52017-05-09 14:24:11 +10001734 safe_buildset = self.current_build_set.getSafeAttributes()
K Jonathan Harkera7f390c2017-06-02 12:31:22 -07001735 safe_job = job.getSafeAttributes() if job else {}
1736 safe_build = build.getSafeAttributes() if build else {}
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001737 try:
1738 url = url_pattern.format(change=safe_change,
1739 pipeline=safe_pipeline,
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001740 tenant=safe_tenant,
Jamie Lennox3f16de52017-05-09 14:24:11 +10001741 buildset=safe_buildset,
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001742 job=safe_job,
1743 build=safe_build)
1744 except KeyError as e:
1745 self.log.error("Error while formatting url for job %s: unknown "
1746 "key %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001747 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001748 except AttributeError as e:
1749 self.log.error("Error while formatting url for job %s: unknown "
1750 "attribute %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001751 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001752 except Exception:
1753 self.log.exception("Error while formatting url for job %s with "
1754 "pattern %s:" % (job, url_pattern))
1755
1756 return url
1757
James E. Blair800e7ff2017-03-17 16:06:52 -07001758 def formatJobResult(self, job):
James E. Blairb7273ef2016-04-19 08:58:51 -07001759 build = self.current_build_set.getBuild(job.name)
1760 result = build.result
James E. Blair800e7ff2017-03-17 16:06:52 -07001761 pattern = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001762 if result == 'SUCCESS':
1763 if job.success_message:
1764 result = job.success_message
James E. Blaira5dba232016-08-08 15:53:24 -07001765 if job.success_url:
1766 pattern = job.success_url
Tobias Henkel077f2f32017-05-30 20:16:46 +02001767 else:
James E. Blairb7273ef2016-04-19 08:58:51 -07001768 if job.failure_message:
1769 result = job.failure_message
James E. Blaira5dba232016-08-08 15:53:24 -07001770 if job.failure_url:
1771 pattern = job.failure_url
James E. Blair88e79c02017-07-07 13:36:54 -07001772 url = None # The final URL
1773 default_url = build.result_data.get('zuul', {}).get('log_url')
James E. Blairb7273ef2016-04-19 08:58:51 -07001774 if pattern:
James E. Blair88e79c02017-07-07 13:36:54 -07001775 job_url = self.formatUrlPattern(pattern, job, build)
1776 else:
1777 job_url = None
1778 try:
1779 if job_url:
1780 u = urllib.parse.urlparse(job_url)
1781 if u.scheme:
1782 # The job success or failure url is absolute, so it's
1783 # our final url.
1784 url = job_url
1785 else:
1786 # We have a relative job url. Combine it with our
1787 # default url.
1788 if default_url:
1789 url = urllib.parse.urljoin(default_url, job_url)
1790 except Exception:
1791 self.log.exception("Error while parsing url for job %s:"
1792 % (job,))
James E. Blairb7273ef2016-04-19 08:58:51 -07001793 if not url:
James E. Blair88e79c02017-07-07 13:36:54 -07001794 url = default_url or build.url or job.name
James E. Blairb7273ef2016-04-19 08:58:51 -07001795 return (result, url)
1796
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001797 def formatJSON(self, websocket_url=None):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001798 ret = {}
1799 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -08001800 ret['live'] = self.live
Monty Taylor55d0f562017-05-17 14:30:21 -05001801 if hasattr(self.change, 'url') and self.change.url is not None:
1802 ret['url'] = self.change.url
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001803 else:
1804 ret['url'] = None
Monty Taylor55d0f562017-05-17 14:30:21 -05001805 ret['id'] = self.change._id()
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001806 if self.item_ahead:
1807 ret['item_ahead'] = self.item_ahead.change._id()
1808 else:
1809 ret['item_ahead'] = None
1810 ret['items_behind'] = [i.change._id() for i in self.items_behind]
1811 ret['failing_reasons'] = self.current_build_set.failing_reasons
1812 ret['zuul_ref'] = self.current_build_set.ref
Monty Taylor55d0f562017-05-17 14:30:21 -05001813 if self.change.project:
1814 ret['project'] = self.change.project.name
Ramy Asselin07cc33c2015-06-12 14:06:34 -07001815 else:
1816 # For cross-project dependencies with the depends-on
1817 # project not known to zuul, the project is None
1818 # Set it to a static value
1819 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001820 ret['enqueue_time'] = int(self.enqueue_time * 1000)
1821 ret['jobs'] = []
Monty Taylor55d0f562017-05-17 14:30:21 -05001822 if hasattr(self.change, 'owner'):
1823 ret['owner'] = self.change.owner
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001824 else:
1825 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001826 max_remaining = 0
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001827 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001828 now = time.time()
1829 build = self.current_build_set.getBuild(job.name)
1830 elapsed = None
1831 remaining = None
1832 result = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001833 build_url = None
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001834 finger_url = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001835 report_url = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001836 worker = None
1837 if build:
1838 result = build.result
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001839 finger_url = build.url
1840 # TODO(tobiash): add support for custom web root
1841 urlformat = 'static/stream.html?' \
1842 'uuid={build.uuid}&' \
1843 'logfile=console.log'
1844 if websocket_url:
1845 urlformat += '&websocket_url={websocket_url}'
1846 build_url = urlformat.format(
1847 build=build, websocket_url=websocket_url)
James E. Blair800e7ff2017-03-17 16:06:52 -07001848 (unused, report_url) = self.formatJobResult(job)
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001849 if build.start_time:
1850 if build.end_time:
1851 elapsed = int((build.end_time -
1852 build.start_time) * 1000)
1853 remaining = 0
1854 else:
1855 elapsed = int((now - build.start_time) * 1000)
1856 if build.estimated_time:
1857 remaining = max(
1858 int(build.estimated_time * 1000) - elapsed,
1859 0)
1860 worker = {
1861 'name': build.worker.name,
1862 'hostname': build.worker.hostname,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001863 }
1864 if remaining and remaining > max_remaining:
1865 max_remaining = remaining
1866
1867 ret['jobs'].append({
1868 'name': job.name,
Tobias Henkel65639f82017-07-10 10:25:42 +02001869 'dependencies': list(job.dependencies),
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001870 'elapsed_time': elapsed,
1871 'remaining_time': remaining,
James E. Blairb7273ef2016-04-19 08:58:51 -07001872 'url': build_url,
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001873 'finger_url': finger_url,
James E. Blairb7273ef2016-04-19 08:58:51 -07001874 'report_url': report_url,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001875 'result': result,
1876 'voting': job.voting,
1877 'uuid': build.uuid if build else None,
Paul Belanger174a8272017-03-14 13:20:10 -04001878 'execute_time': build.execute_time if build else None,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001879 'start_time': build.start_time if build else None,
1880 'end_time': build.end_time if build else None,
1881 'estimated_time': build.estimated_time if build else None,
1882 'pipeline': build.pipeline.name if build else None,
1883 'canceled': build.canceled if build else None,
1884 'retry': build.retry if build else None,
Timothy Chavezb2332082015-08-07 20:08:04 -05001885 'node_labels': build.node_labels if build else [],
1886 'node_name': build.node_name if build else None,
1887 'worker': worker,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001888 })
1889
James E. Blairdbfd3282016-07-21 10:46:19 -07001890 if self.haveAllJobsStarted():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001891 ret['remaining_time'] = max_remaining
1892 else:
1893 ret['remaining_time'] = None
1894 return ret
1895
1896 def formatStatus(self, indent=0, html=False):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001897 indent_str = ' ' * indent
1898 ret = ''
Monty Taylor55d0f562017-05-17 14:30:21 -05001899 if html and getattr(self.change, 'url', None) is not None:
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001900 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
1901 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001902 self.change.project.name,
1903 self.change.url,
1904 self.change._id())
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001905 else:
1906 ret += '%sProject %s change %s based on %s\n' % (
1907 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001908 self.change.project.name,
1909 self.change._id(),
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001910 self.item_ahead)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001911 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001912 build = self.current_build_set.getBuild(job.name)
1913 if build:
1914 result = build.result
1915 else:
1916 result = None
1917 job_name = job.name
1918 if not job.voting:
1919 voting = ' (non-voting)'
1920 else:
1921 voting = ''
1922 if html:
1923 if build:
1924 url = build.url
1925 else:
1926 url = None
1927 if url is not None:
1928 job_name = '<a href="%s">%s</a>' % (url, job_name)
1929 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
1930 ret += '\n'
1931 return ret
1932
James E. Blaira04b0792017-04-27 09:59:06 -07001933 def makeMergerItem(self):
1934 # Create a dictionary with all info about the item needed by
1935 # the merger.
1936 number = None
1937 patchset = None
1938 oldrev = None
1939 newrev = None
James E. Blair21037782017-07-19 11:56:55 -07001940 branch = None
James E. Blaira04b0792017-04-27 09:59:06 -07001941 if hasattr(self.change, 'number'):
1942 number = self.change.number
1943 patchset = self.change.patchset
James E. Blair21037782017-07-19 11:56:55 -07001944 if hasattr(self.change, 'newrev'):
James E. Blaira04b0792017-04-27 09:59:06 -07001945 oldrev = self.change.oldrev
1946 newrev = self.change.newrev
James E. Blair21037782017-07-19 11:56:55 -07001947 if hasattr(self.change, 'branch'):
1948 branch = self.change.branch
1949
James E. Blaira04b0792017-04-27 09:59:06 -07001950 source = self.change.project.source
1951 connection_name = source.connection.connection_name
James E. Blair2a535672017-04-27 12:03:15 -07001952 project = self.change.project
James E. Blaira04b0792017-04-27 09:59:06 -07001953
James E. Blair2a535672017-04-27 12:03:15 -07001954 return dict(project=project.name,
1955 connection=connection_name,
James E. Blaira04b0792017-04-27 09:59:06 -07001956 merge_mode=self.current_build_set.getMergeMode(),
James E. Blair247cab72017-07-20 16:52:36 -07001957 ref=self.change.ref,
James E. Blaira04b0792017-04-27 09:59:06 -07001958 branch=branch,
James E. Blair247cab72017-07-20 16:52:36 -07001959 buildset_uuid=self.current_build_set.uuid,
James E. Blaira04b0792017-04-27 09:59:06 -07001960 number=number,
1961 patchset=patchset,
1962 oldrev=oldrev,
1963 newrev=newrev,
1964 )
1965
James E. Blairfee8d652013-06-07 08:57:52 -07001966
Clint Byrumf8cc9902017-03-22 22:38:25 -07001967class Ref(object):
1968 """An existing state of a Project."""
James E. Blairfee8d652013-06-07 08:57:52 -07001969
1970 def __init__(self, project):
1971 self.project = project
Clint Byrumf8cc9902017-03-22 22:38:25 -07001972 self.ref = None
1973 self.oldrev = None
1974 self.newrev = None
Jesse Keating71a47ff2017-06-06 11:36:43 -07001975 self.files = []
1976
Clint Byrumf8cc9902017-03-22 22:38:25 -07001977 def _id(self):
1978 return self.newrev
1979
1980 def __repr__(self):
1981 rep = None
1982 if self.newrev == '0000000000000000000000000000000000000000':
James E. Blair21037782017-07-19 11:56:55 -07001983 rep = '<%s 0x%x deletes %s from %s' % (
1984 type(self).__name__,
1985 id(self), self.ref, self.oldrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001986 elif self.oldrev == '0000000000000000000000000000000000000000':
James E. Blair21037782017-07-19 11:56:55 -07001987 rep = '<%s 0x%x creates %s on %s>' % (
1988 type(self).__name__,
1989 id(self), self.ref, self.newrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001990 else:
1991 # Catch all
James E. Blair21037782017-07-19 11:56:55 -07001992 rep = '<%s 0x%x %s updated %s..%s>' % (
1993 type(self).__name__,
1994 id(self), self.ref, self.oldrev, self.newrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001995 return rep
1996
James E. Blairfee8d652013-06-07 08:57:52 -07001997 def equals(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001998 if (self.project == other.project
1999 and self.ref == other.ref
2000 and self.newrev == other.newrev):
2001 return True
2002 return False
James E. Blairfee8d652013-06-07 08:57:52 -07002003
2004 def isUpdateOf(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07002005 return False
James E. Blairfee8d652013-06-07 08:57:52 -07002006
2007 def filterJobs(self, jobs):
2008 return filter(lambda job: job.changeMatches(self), jobs)
2009
2010 def getRelatedChanges(self):
2011 return set()
2012
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002013 def updatesConfig(self):
Tristan Cacqueray829e6172017-06-13 06:49:36 +00002014 if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files or \
2015 [True for fn in self.files if fn.startswith("zuul.d/") or
2016 fn.startswith(".zuul.d/")]:
Jesse Keating71a47ff2017-06-06 11:36:43 -07002017 return True
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002018 return False
2019
Joshua Hesketh58419cb2017-02-24 13:09:22 -05002020 def getSafeAttributes(self):
2021 return Attributes(project=self.project,
2022 ref=self.ref,
2023 oldrev=self.oldrev,
2024 newrev=self.newrev)
2025
James E. Blair9e5b8112017-10-19 08:12:24 -07002026 def toDict(self):
2027 # Render to a dict to use in passing json to the executor
2028 d = dict()
2029 d['project'] = dict(
2030 name=self.project.name,
2031 short_name=self.project.name.split('/')[-1],
2032 canonical_hostname=self.project.canonical_hostname,
2033 canonical_name=self.project.canonical_name,
2034 src_dir=os.path.join('src', self.project.canonical_name),
2035 )
2036 return d
2037
James E. Blair1e8dd892012-05-30 09:15:05 -07002038
James E. Blair21037782017-07-19 11:56:55 -07002039class Branch(Ref):
2040 """An existing branch state for a Project."""
2041 def __init__(self, project):
2042 super(Branch, self).__init__(project)
2043 self.branch = None
2044
James E. Blair9e5b8112017-10-19 08:12:24 -07002045 def toDict(self):
2046 # Render to a dict to use in passing json to the executor
2047 d = super(Branch, self).toDict()
2048 d['branch'] = self.branch
2049 return d
2050
James E. Blair21037782017-07-19 11:56:55 -07002051
2052class Tag(Ref):
2053 """An existing tag state for a Project."""
2054 def __init__(self, project):
2055 super(Tag, self).__init__(project)
2056 self.tag = None
2057
2058
2059class Change(Branch):
Monty Taylora42a55b2016-07-29 07:53:33 -07002060 """A proposed new state for a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07002061 def __init__(self, project):
2062 super(Change, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -07002063 self.number = None
2064 self.url = None
2065 self.patchset = None
James E. Blair4aea70c2012-07-26 14:23:24 -07002066
James E. Blair6965a4b2014-12-16 17:19:04 -08002067 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -07002068 self.needed_by_changes = []
2069 self.is_current_patchset = True
2070 self.can_merge = False
2071 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -07002072 self.failed_to_merge = False
James E. Blair11041d22014-05-02 14:49:53 -07002073 self.open = None
2074 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05002075 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -07002076
Jan Hruban3b415922016-02-03 13:10:22 +01002077 self.source_event = None
2078
James E. Blair4aea70c2012-07-26 14:23:24 -07002079 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -07002080 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -07002081
2082 def __repr__(self):
2083 return '<Change 0x%x %s>' % (id(self), self._id())
2084
2085 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +08002086 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -07002087 return True
2088 return False
2089
James E. Blair2fa50962013-01-30 21:50:41 -08002090 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -08002091 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -07002092 (hasattr(other, 'patchset') and
2093 self.patchset is not None and
2094 other.patchset is not None and
2095 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -08002096 return True
2097 return False
2098
James E. Blairfee8d652013-06-07 08:57:52 -07002099 def getRelatedChanges(self):
2100 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -08002101 for c in self.needs_changes:
2102 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -07002103 for c in self.needed_by_changes:
2104 related.add(c)
2105 related.update(c.getRelatedChanges())
2106 return related
James E. Blair4aea70c2012-07-26 14:23:24 -07002107
Joshua Hesketh58419cb2017-02-24 13:09:22 -05002108 def getSafeAttributes(self):
2109 return Attributes(project=self.project,
2110 number=self.number,
2111 patchset=self.patchset)
2112
James E. Blair9e5b8112017-10-19 08:12:24 -07002113 def toDict(self):
2114 # Render to a dict to use in passing json to the executor
2115 d = super(Change, self).toDict()
2116 d['change'] = str(self.number)
2117 d['change_url'] = self.url
2118 d['patchset'] = str(self.patchset)
2119 return d
2120
James E. Blair4aea70c2012-07-26 14:23:24 -07002121
James E. Blairee743612012-05-29 14:49:32 -07002122class TriggerEvent(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002123 """Incoming event from an external system."""
James E. Blairee743612012-05-29 14:49:32 -07002124 def __init__(self):
James E. Blairaad3ae22017-05-18 14:11:29 -07002125 # TODO(jeblair): further reduce this list
James E. Blairee743612012-05-29 14:49:32 -07002126 self.data = None
James E. Blair32663402012-06-01 10:04:18 -07002127 # common
James E. Blairee743612012-05-29 14:49:32 -07002128 self.type = None
Jesse Keating71a47ff2017-06-06 11:36:43 -07002129 self.branch_updated = False
James E. Blair72facdc2017-08-17 10:29:12 -07002130 self.branch_created = False
2131 self.branch_deleted = False
James E. Blair247cab72017-07-20 16:52:36 -07002132 self.ref = None
Paul Belangerbaca3132016-11-04 12:49:54 -04002133 # For management events (eg: enqueue / promote)
2134 self.tenant_name = None
James E. Blair6f284b42017-03-31 14:14:41 -07002135 self.project_hostname = None
James E. Blairee743612012-05-29 14:49:32 -07002136 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -07002137 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +01002138 # Representation of the user account that performed the event.
2139 self.account = None
James E. Blair32663402012-06-01 10:04:18 -07002140 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -07002141 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -07002142 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -07002143 self.patch_number = None
James E. Blairee743612012-05-29 14:49:32 -07002144 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07002145 self.comment = None
Jesse Keating5c05a9f2017-01-12 14:44:58 -08002146 self.state = None
James E. Blair32663402012-06-01 10:04:18 -07002147 # ref-updated
James E. Blair32663402012-06-01 10:04:18 -07002148 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07002149 self.newrev = None
James E. Blairad28e912013-11-27 10:43:22 -08002150 # For events that arrive with a destination pipeline (eg, from
2151 # an admin command, etc):
2152 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07002153
James E. Blair6f284b42017-03-31 14:14:41 -07002154 @property
2155 def canonical_project_name(self):
2156 return self.project_hostname + '/' + self.project_name
2157
Jan Hruban324ca5b2015-11-05 19:28:54 +01002158 def isPatchsetCreated(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01002159 return False
2160
2161 def isChangeAbandoned(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01002162 return False
2163
James E. Blair1e8dd892012-05-30 09:15:05 -07002164
James E. Blair9c17dbf2014-06-23 14:21:58 -07002165class BaseFilter(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002166 """Base Class for filtering which Changes and Events to process."""
James E. Blairaad3ae22017-05-18 14:11:29 -07002167 pass
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002168
James E. Blair9c17dbf2014-06-23 14:21:58 -07002169
2170class EventFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002171 """Allows a Pipeline to only respond to certain events."""
James E. Blairaad3ae22017-05-18 14:11:29 -07002172 def __init__(self, trigger):
2173 super(EventFilter, self).__init__()
James E. Blairc0dedf82014-08-06 09:37:52 -07002174 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07002175
James E. Blairaad3ae22017-05-18 14:11:29 -07002176 def matches(self, event, ref):
2177 # TODO(jeblair): consider removing ref argument
James E. Blairee743612012-05-29 14:49:32 -07002178 return True
James E. Blaireff88162013-07-01 12:44:14 -04002179
2180
James E. Blairaad3ae22017-05-18 14:11:29 -07002181class RefFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002182 """Allows a Manager to only enqueue Changes that meet certain criteria."""
Jesse Keating8c2eb572017-05-30 17:31:45 -07002183 def __init__(self, connection_name):
James E. Blairaad3ae22017-05-18 14:11:29 -07002184 super(RefFilter, self).__init__()
Jesse Keating8c2eb572017-05-30 17:31:45 -07002185 self.connection_name = connection_name
James E. Blair11041d22014-05-02 14:49:53 -07002186
2187 def matches(self, change):
James E. Blair11041d22014-05-02 14:49:53 -07002188 return True
2189
2190
James E. Blairb97ed802015-12-21 15:55:35 -08002191class ProjectPipelineConfig(object):
2192 # Represents a project cofiguration in the context of a pipeline
2193 def __init__(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002194 self.job_list = JobList()
James E. Blairb97ed802015-12-21 15:55:35 -08002195 self.queue_name = None
Adam Gandelman8bd57102016-12-02 12:58:42 -08002196 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08002197
2198
James E. Blair08d9b782017-06-29 14:22:48 -07002199class TenantProjectConfig(object):
2200 """A project in the context of a tenant.
2201
2202 A Project is globally unique in the system, however, when used in
2203 a tenant, some metadata about the project local to the tenant is
2204 stored in a TenantProjectConfig.
2205 """
2206
2207 def __init__(self, project):
2208 self.project = project
2209 self.load_classes = set()
James E. Blair6459db12017-06-29 14:57:20 -07002210 self.shadow_projects = set()
James E. Blairdaaf3262017-10-23 13:51:48 -07002211 self.branches = []
Tobias Henkeleca46202017-08-02 20:27:10 +02002212 # The tenant's default setting of exclude_unprotected_branches will
2213 # be overridden by this one if not None.
2214 self.exclude_unprotected_branches = None
2215
James E. Blair08d9b782017-06-29 14:22:48 -07002216
James E. Blairb97ed802015-12-21 15:55:35 -08002217class ProjectConfig(object):
2218 # Represents a project cofiguration
James E. Blair2a664502017-10-27 11:39:33 -07002219 def __init__(self, name, source_context=None):
James E. Blairb97ed802015-12-21 15:55:35 -08002220 self.name = name
James E. Blair2a664502017-10-27 11:39:33 -07002221 # If this is a template, it will have a source_context, but
2222 # not if it is a project definition.
2223 self.source_context = source_context
Adam Gandelman8bd57102016-12-02 12:58:42 -08002224 self.merge_mode = None
James E. Blaire74f5712017-09-29 15:14:31 -07002225 # The default branch for the project (usually master).
James E. Blair040b6502017-05-23 10:18:21 -07002226 self.default_branch = None
James E. Blairb97ed802015-12-21 15:55:35 -08002227 self.pipelines = {}
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002228 self.private_key_file = None
James E. Blairb97ed802015-12-21 15:55:35 -08002229
2230
James E. Blair97043882017-09-06 15:51:17 -07002231class ConfigItemNotListError(Exception):
2232 def __init__(self):
2233 message = textwrap.dedent("""\
2234 Configuration file is not a list. Each zuul.yaml configuration
2235 file must be a list of items, for example:
2236
2237 - job:
2238 name: foo
2239
2240 - project:
2241 name: bar
2242
2243 Ensure that every item starts with "- " so that it is parsed as a
2244 YAML list.
2245 """)
2246 super(ConfigItemNotListError, self).__init__(message)
2247
2248
2249class ConfigItemNotDictError(Exception):
2250 def __init__(self):
2251 message = textwrap.dedent("""\
2252 Configuration item is not a dictionary. Each zuul.yaml
2253 configuration file must be a list of dictionaries, for
2254 example:
2255
2256 - job:
2257 name: foo
2258
2259 - project:
2260 name: bar
2261
2262 Ensure that every item in the list is a dictionary with one
2263 key (in this example, 'job' and 'project').
2264 """)
2265 super(ConfigItemNotDictError, self).__init__(message)
2266
2267
2268class ConfigItemMultipleKeysError(Exception):
2269 def __init__(self):
2270 message = textwrap.dedent("""\
2271 Configuration item has more than one key. Each zuul.yaml
2272 configuration file must be a list of dictionaries with a
2273 single key, for example:
2274
2275 - job:
2276 name: foo
2277
2278 - project:
2279 name: bar
2280
2281 Ensure that every item in the list is a dictionary with only
2282 one key (in this example, 'job' and 'project'). This error
2283 may be caused by insufficient indentation of the keys under
2284 the configuration item ('name' in this example).
2285 """)
2286 super(ConfigItemMultipleKeysError, self).__init__(message)
2287
2288
2289class ConfigItemUnknownError(Exception):
2290 def __init__(self):
2291 message = textwrap.dedent("""\
2292 Configuration item not recognized. Each zuul.yaml
2293 configuration file must be a list of dictionaries, for
2294 example:
2295
2296 - job:
2297 name: foo
2298
2299 - project:
2300 name: bar
2301
2302 The dictionary keys must match one of the configuration item
2303 types recognized by zuul (for example, 'job' or 'project').
2304 """)
2305 super(ConfigItemUnknownError, self).__init__(message)
2306
2307
James E. Blaird8e778f2015-12-22 14:09:20 -08002308class UnparsedAbideConfig(object):
James E. Blair08d9b782017-06-29 14:22:48 -07002309
Monty Taylora42a55b2016-07-29 07:53:33 -07002310 """A collection of yaml lists that has not yet been parsed into objects.
2311
2312 An Abide is a collection of tenants.
2313 """
2314
James E. Blaird8e778f2015-12-22 14:09:20 -08002315 def __init__(self):
2316 self.tenants = []
2317
2318 def extend(self, conf):
2319 if isinstance(conf, UnparsedAbideConfig):
2320 self.tenants.extend(conf.tenants)
2321 return
2322
2323 if not isinstance(conf, list):
James E. Blair97043882017-09-06 15:51:17 -07002324 raise ConfigItemNotListError()
2325
James E. Blaird8e778f2015-12-22 14:09:20 -08002326 for item in conf:
2327 if not isinstance(item, dict):
James E. Blair97043882017-09-06 15:51:17 -07002328 raise ConfigItemNotDictError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002329 if len(item.keys()) > 1:
James E. Blair97043882017-09-06 15:51:17 -07002330 raise ConfigItemMultipleKeysError()
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002331 key, value = list(item.items())[0]
James E. Blaird8e778f2015-12-22 14:09:20 -08002332 if key == 'tenant':
2333 self.tenants.append(value)
2334 else:
James E. Blair97043882017-09-06 15:51:17 -07002335 raise ConfigItemUnknownError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002336
2337
2338class UnparsedTenantConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002339 """A collection of yaml lists that has not yet been parsed into objects."""
2340
James E. Blaird8e778f2015-12-22 14:09:20 -08002341 def __init__(self):
James E. Blair7edc25f2017-10-26 10:47:14 -07002342 self.pragmas = []
James E. Blaird8e778f2015-12-22 14:09:20 -08002343 self.pipelines = []
2344 self.jobs = []
2345 self.project_templates = []
James E. Blairff555742017-02-19 11:34:27 -08002346 self.projects = {}
James E. Blaira98340f2016-09-02 11:33:49 -07002347 self.nodesets = []
James E. Blair01f83b72017-03-15 13:03:40 -07002348 self.secrets = []
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002349 self.semaphores = []
James E. Blaird8e778f2015-12-22 14:09:20 -08002350
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002351 def copy(self):
2352 r = UnparsedTenantConfig()
James E. Blair7edc25f2017-10-26 10:47:14 -07002353 r.pragmas = copy.deepcopy(self.pragmas)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002354 r.pipelines = copy.deepcopy(self.pipelines)
2355 r.jobs = copy.deepcopy(self.jobs)
2356 r.project_templates = copy.deepcopy(self.project_templates)
2357 r.projects = copy.deepcopy(self.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07002358 r.nodesets = copy.deepcopy(self.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002359 r.secrets = copy.deepcopy(self.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002360 r.semaphores = copy.deepcopy(self.semaphores)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002361 return r
2362
James E. Blairec7ff302017-03-04 07:31:32 -08002363 def extend(self, conf):
James E. Blaird8e778f2015-12-22 14:09:20 -08002364 if isinstance(conf, UnparsedTenantConfig):
James E. Blair7edc25f2017-10-26 10:47:14 -07002365 self.pragmas.extend(conf.pragmas)
James E. Blaird8e778f2015-12-22 14:09:20 -08002366 self.pipelines.extend(conf.pipelines)
2367 self.jobs.extend(conf.jobs)
2368 self.project_templates.extend(conf.project_templates)
James E. Blairff555742017-02-19 11:34:27 -08002369 for k, v in conf.projects.items():
2370 self.projects.setdefault(k, []).extend(v)
James E. Blaira98340f2016-09-02 11:33:49 -07002371 self.nodesets.extend(conf.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002372 self.secrets.extend(conf.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002373 self.semaphores.extend(conf.semaphores)
James E. Blaird8e778f2015-12-22 14:09:20 -08002374 return
2375
2376 if not isinstance(conf, list):
James E. Blair97043882017-09-06 15:51:17 -07002377 raise ConfigItemNotListError()
James E. Blaircdab2032017-02-01 09:09:29 -08002378
James E. Blaird8e778f2015-12-22 14:09:20 -08002379 for item in conf:
2380 if not isinstance(item, dict):
James E. Blair97043882017-09-06 15:51:17 -07002381 raise ConfigItemNotDictError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002382 if len(item.keys()) > 1:
James E. Blair97043882017-09-06 15:51:17 -07002383 raise ConfigItemMultipleKeysError()
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002384 key, value = list(item.items())[0]
James E. Blair66b274e2017-01-31 14:47:52 -08002385 if key == 'project':
James E. Blairff555742017-02-19 11:34:27 -08002386 name = value['name']
2387 self.projects.setdefault(name, []).append(value)
James E. Blair66b274e2017-01-31 14:47:52 -08002388 elif key == 'job':
James E. Blaird8e778f2015-12-22 14:09:20 -08002389 self.jobs.append(value)
2390 elif key == 'project-template':
2391 self.project_templates.append(value)
2392 elif key == 'pipeline':
2393 self.pipelines.append(value)
James E. Blaira98340f2016-09-02 11:33:49 -07002394 elif key == 'nodeset':
2395 self.nodesets.append(value)
James E. Blair01f83b72017-03-15 13:03:40 -07002396 elif key == 'secret':
2397 self.secrets.append(value)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002398 elif key == 'semaphore':
2399 self.semaphores.append(value)
James E. Blair7edc25f2017-10-26 10:47:14 -07002400 elif key == 'pragma':
2401 self.pragmas.append(value)
James E. Blaird8e778f2015-12-22 14:09:20 -08002402 else:
James E. Blair97043882017-09-06 15:51:17 -07002403 raise ConfigItemUnknownError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002404
2405
James E. Blaireff88162013-07-01 12:44:14 -04002406class Layout(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002407 """Holds all of the Pipelines."""
2408
James E. Blair6459db12017-06-29 14:57:20 -07002409 def __init__(self, tenant):
James E. Blair8fe53b42017-10-18 16:58:38 -07002410 self.uuid = uuid4().hex
James E. Blair6459db12017-06-29 14:57:20 -07002411 self.tenant = tenant
James E. Blairb97ed802015-12-21 15:55:35 -08002412 self.project_configs = {}
2413 self.project_templates = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07002414 self.pipelines = OrderedDict()
James E. Blair83005782015-12-11 14:46:03 -08002415 # This is a dictionary of name -> [jobs]. The first element
2416 # of the list is the first job added with that name. It is
2417 # the reference definition for a given job. Subsequent
2418 # elements are aspects of that job with different matchers
2419 # that override some attribute of the job. These aspects all
2420 # inherit from the reference definition.
James E. Blairc32a8352017-10-11 16:27:50 -07002421 noop = Job('noop')
2422 noop.parent = noop.BASE_JOB_MARKER
James E. Blair00292672017-10-26 15:29:39 -07002423 noop.run = 'noop.yaml'
James E. Blairc32a8352017-10-11 16:27:50 -07002424 self.jobs = {'noop': [noop]}
James E. Blaira98340f2016-09-02 11:33:49 -07002425 self.nodesets = {}
James E. Blair01f83b72017-03-15 13:03:40 -07002426 self.secrets = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002427 self.semaphores = {}
James E. Blaireff88162013-07-01 12:44:14 -04002428
2429 def getJob(self, name):
2430 if name in self.jobs:
James E. Blair83005782015-12-11 14:46:03 -08002431 return self.jobs[name][0]
James E. Blairb97ed802015-12-21 15:55:35 -08002432 raise Exception("Job %s not defined" % (name,))
James E. Blair83005782015-12-11 14:46:03 -08002433
James E. Blair2bab6e72017-08-07 09:52:45 -07002434 def hasJob(self, name):
2435 return name in self.jobs
2436
James E. Blair83005782015-12-11 14:46:03 -08002437 def getJobs(self, name):
2438 return self.jobs.get(name, [])
2439
2440 def addJob(self, job):
James E. Blair4317e9f2016-07-15 10:05:47 -07002441 # We can have multiple variants of a job all with the same
2442 # name, but these variants must all be defined in the same repo.
James E. Blaircdab2032017-02-01 09:09:29 -08002443 prior_jobs = [j for j in self.getJobs(job.name) if
2444 j.source_context.project !=
2445 job.source_context.project]
James E. Blair6459db12017-06-29 14:57:20 -07002446 # Unless the repo is permitted to shadow another. If so, and
2447 # the job we are adding is from a repo that is permitted to
2448 # shadow the one with the older jobs, skip adding this job.
2449 job_project = job.source_context.project
2450 job_tpc = self.tenant.project_configs[job_project.canonical_name]
2451 skip_add = False
2452 for prior_job in prior_jobs[:]:
2453 prior_project = prior_job.source_context.project
2454 if prior_project in job_tpc.shadow_projects:
2455 prior_jobs.remove(prior_job)
2456 skip_add = True
2457
James E. Blair4317e9f2016-07-15 10:05:47 -07002458 if prior_jobs:
2459 raise Exception("Job %s in %s is not permitted to shadow "
James E. Blaircdab2032017-02-01 09:09:29 -08002460 "job %s in %s" % (
2461 job,
2462 job.source_context.project,
2463 prior_jobs[0],
2464 prior_jobs[0].source_context.project))
James E. Blair6459db12017-06-29 14:57:20 -07002465 if skip_add:
2466 return False
James E. Blair83005782015-12-11 14:46:03 -08002467 if job.name in self.jobs:
2468 self.jobs[job.name].append(job)
2469 else:
2470 self.jobs[job.name] = [job]
James E. Blair6459db12017-06-29 14:57:20 -07002471 return True
James E. Blair83005782015-12-11 14:46:03 -08002472
James E. Blaira98340f2016-09-02 11:33:49 -07002473 def addNodeSet(self, nodeset):
2474 if nodeset.name in self.nodesets:
2475 raise Exception("NodeSet %s already defined" % (nodeset.name,))
2476 self.nodesets[nodeset.name] = nodeset
2477
James E. Blair01f83b72017-03-15 13:03:40 -07002478 def addSecret(self, secret):
2479 if secret.name in self.secrets:
2480 raise Exception("Secret %s already defined" % (secret.name,))
2481 self.secrets[secret.name] = secret
2482
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002483 def addSemaphore(self, semaphore):
2484 if semaphore.name in self.semaphores:
2485 raise Exception("Semaphore %s already defined" % (semaphore.name,))
2486 self.semaphores[semaphore.name] = semaphore
2487
James E. Blair83005782015-12-11 14:46:03 -08002488 def addPipeline(self, pipeline):
2489 self.pipelines[pipeline.name] = pipeline
James E. Blair59fdbac2015-12-07 17:08:06 -08002490
James E. Blairb97ed802015-12-21 15:55:35 -08002491 def addProjectTemplate(self, project_template):
James E. Blair2a664502017-10-27 11:39:33 -07002492 template = self.project_templates.get(project_template.name)
2493 if template:
2494 if (project_template.source_context.project !=
2495 template.source_context.project):
2496 raise Exception("Project template %s is already defined" %
2497 (project_template.name,))
2498 for pipeline in project_template.pipelines:
2499 template.pipelines[pipeline].job_list.\
2500 inheritFrom(project_template.pipelines[pipeline].job_list,
2501 None)
2502 else:
2503 self.project_templates[project_template.name] = project_template
James E. Blairb97ed802015-12-21 15:55:35 -08002504
James E. Blairf59f3cf2017-02-19 14:50:26 -08002505 def addProjectConfig(self, project_config):
James E. Blairb97ed802015-12-21 15:55:35 -08002506 self.project_configs[project_config.name] = project_config
James E. Blairb97ed802015-12-21 15:55:35 -08002507
James E. Blairc32a8352017-10-11 16:27:50 -07002508 def collectJobs(self, jobname, change, path=None, jobs=None, stack=None):
2509 if stack is None:
2510 stack = []
2511 if jobs is None:
2512 jobs = []
2513 if path is None:
2514 path = []
2515 path.append(jobname)
2516 matched = False
2517 for variant in self.getJobs(jobname):
2518 if not variant.changeMatches(change):
2519 continue
2520 if not variant.isBase():
2521 parent = variant.parent
2522 if not jobs and parent is None:
2523 parent = self.tenant.default_base_job
2524 else:
2525 parent = None
2526 if parent and parent not in path:
2527 if parent in stack:
2528 raise Exception("Dependency cycle in jobs: %s" % stack)
2529 self.collectJobs(parent, change, path, jobs, stack + [jobname])
2530 matched = True
2531 jobs.append(variant)
2532 if not matched:
2533 raise NoMatchingParentError()
2534 return jobs
2535
James E. Blaird2348362017-03-17 13:59:35 -07002536 def _createJobGraph(self, item, job_list, job_graph):
2537 change = item.change
2538 pipeline = item.pipeline
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002539 for jobname in job_list.jobs:
2540 # This is the final job we are constructing
James E. Blaira7f51ca2017-02-07 16:01:26 -08002541 frozen_job = None
James E. Blairc32a8352017-10-11 16:27:50 -07002542 try:
2543 variants = self.collectJobs(jobname, change)
2544 except NoMatchingParentError:
2545 variants = None
2546 if not variants:
James E. Blair6e85c2b2016-11-21 16:47:01 -08002547 # A change must match at least one defined job variant
2548 # (that is to say that it must match more than just
2549 # the job that is defined in the tree).
2550 continue
James E. Blairc32a8352017-10-11 16:27:50 -07002551 for variant in variants:
2552 if frozen_job is None:
2553 frozen_job = variant.copy()
2554 frozen_job.setBase()
2555 else:
2556 frozen_job.applyVariant(variant)
2557 frozen_job.name = variant.name
James E. Blairc32a8352017-10-11 16:27:50 -07002558 frozen_job.name = jobname
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002559 # Whether the change matches any of the project pipeline
2560 # variants
2561 matched = False
2562 for variant in job_list.jobs[jobname]:
2563 if variant.changeMatches(change):
2564 frozen_job.applyVariant(variant)
2565 matched = True
2566 if not matched:
2567 # A change must match at least one project pipeline
2568 # job variant.
2569 continue
James E. Blairb3f5db12017-03-17 12:57:39 -07002570 if (frozen_job.allowed_projects and
2571 change.project.name not in frozen_job.allowed_projects):
2572 raise Exception("Project %s is not allowed to run job %s" %
2573 (change.project.name, frozen_job.name))
James E. Blair8eb564a2017-08-10 09:21:41 -07002574 if ((not pipeline.post_review) and frozen_job.post_review):
2575 raise Exception("Pre-review pipeline %s does not allow "
2576 "post-review job %s" % (
James E. Blaird2348362017-03-17 13:59:35 -07002577 pipeline.name, frozen_job.name))
James E. Blair00292672017-10-26 15:29:39 -07002578 if not frozen_job.run:
2579 raise Exception("Job %s does not specify a run playbook" % (
2580 frozen_job.name,))
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002581 job_graph.addJob(frozen_job)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002582
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002583 def createJobGraph(self, item):
Paul Belanger15e3e202016-10-14 16:27:34 -04002584 # NOTE(pabelanger): It is possible for a foreign project not to have a
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002585 # configured pipeline, if so return an empty JobGraph.
James E. Blairc9455002017-09-06 09:22:19 -07002586 ret = JobGraph()
2587 ppc = self.getProjectPipelineConfig(item.change.project,
2588 item.pipeline)
2589 if ppc:
2590 self._createJobGraph(item, ppc.job_list, ret)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002591 return ret
2592
James E. Blairc9455002017-09-06 09:22:19 -07002593 def getProjectPipelineConfig(self, project, pipeline):
2594 project_config = self.project_configs.get(
2595 project.canonical_name, None)
2596 if not project_config:
2597 return None
2598 return project_config.pipelines.get(pipeline.name, None)
James E. Blair0d3e83b2017-06-05 13:51:57 -07002599
James E. Blair59fdbac2015-12-07 17:08:06 -08002600
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002601class Semaphore(object):
2602 def __init__(self, name, max=1):
2603 self.name = name
2604 self.max = int(max)
2605
2606
2607class SemaphoreHandler(object):
2608 log = logging.getLogger("zuul.SemaphoreHandler")
2609
2610 def __init__(self):
2611 self.semaphores = {}
2612
2613 def acquire(self, item, job):
2614 if not job.semaphore:
2615 return True
2616
2617 semaphore_key = job.semaphore
2618
2619 m = self.semaphores.get(semaphore_key)
2620 if not m:
2621 # The semaphore is not held, acquire it
2622 self._acquire(semaphore_key, item, job.name)
2623 return True
2624 if (item, job.name) in m:
2625 # This item already holds the semaphore
2626 return True
2627
2628 # semaphore is there, check max
2629 if len(m) < self._max_count(item, job.semaphore):
2630 self._acquire(semaphore_key, item, job.name)
2631 return True
2632
2633 return False
2634
2635 def release(self, item, job):
2636 if not job.semaphore:
2637 return
2638
2639 semaphore_key = job.semaphore
2640
2641 m = self.semaphores.get(semaphore_key)
2642 if not m:
2643 # The semaphore is not held, nothing to do
2644 self.log.error("Semaphore can not be released for %s "
2645 "because the semaphore is not held" %
2646 item)
2647 return
2648 if (item, job.name) in m:
2649 # This item is a holder of the semaphore
2650 self._release(semaphore_key, item, job.name)
2651 return
2652 self.log.error("Semaphore can not be released for %s "
2653 "which does not hold it" % item)
2654
2655 def _acquire(self, semaphore_key, item, job_name):
2656 self.log.debug("Semaphore acquire {semaphore}: job {job}, item {item}"
2657 .format(semaphore=semaphore_key,
2658 job=job_name,
2659 item=item))
2660 if semaphore_key not in self.semaphores:
2661 self.semaphores[semaphore_key] = []
2662 self.semaphores[semaphore_key].append((item, job_name))
2663
2664 def _release(self, semaphore_key, item, job_name):
2665 self.log.debug("Semaphore release {semaphore}: job {job}, item {item}"
2666 .format(semaphore=semaphore_key,
2667 job=job_name,
2668 item=item))
2669 sem_item = (item, job_name)
2670 if sem_item in self.semaphores[semaphore_key]:
2671 self.semaphores[semaphore_key].remove(sem_item)
2672
2673 # cleanup if there is no user of the semaphore anymore
2674 if len(self.semaphores[semaphore_key]) == 0:
2675 del self.semaphores[semaphore_key]
2676
2677 @staticmethod
2678 def _max_count(item, semaphore_name):
James E. Blair29a24fd2017-10-02 15:04:56 -07002679 if not item.layout:
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002680 # This should not occur as the layout of the item must already be
2681 # built when acquiring or releasing a semaphore for a job.
2682 raise Exception("Item {} has no layout".format(item))
2683
2684 # find the right semaphore
2685 default_semaphore = Semaphore(semaphore_name, 1)
James E. Blair29a24fd2017-10-02 15:04:56 -07002686 semaphores = item.layout.semaphores
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002687 return semaphores.get(semaphore_name, default_semaphore).max
2688
2689
James E. Blair59fdbac2015-12-07 17:08:06 -08002690class Tenant(object):
2691 def __init__(self, name):
2692 self.name = name
Tristan Cacqueray82f864b2017-08-01 05:54:42 +00002693 self.max_nodes_per_job = 5
Tristan Cacquerayc98bff72017-09-10 15:25:26 +00002694 self.max_job_timeout = 10800
Tobias Henkeleca46202017-08-02 20:27:10 +02002695 self.exclude_unprotected_branches = False
James E. Blair2bab6e72017-08-07 09:52:45 -07002696 self.default_base_job = None
James E. Blair59fdbac2015-12-07 17:08:06 -08002697 self.layout = None
James E. Blair646322f2017-01-27 15:50:34 -08002698 # The unparsed configuration from the main zuul config for
2699 # this tenant.
2700 self.unparsed_config = None
James E. Blair109da3f2017-04-04 14:39:43 -07002701 # The list of projects from which we will read full
James E. Blair8a395f92017-03-30 11:15:33 -07002702 # configuration.
James E. Blair109da3f2017-04-04 14:39:43 -07002703 self.config_projects = []
2704 # The unparsed config from those projects.
2705 self.config_projects_config = None
2706 # The list of projects from which we will read untrusted
2707 # in-repo configuration.
2708 self.untrusted_projects = []
2709 # The unparsed config from those projects.
2710 self.untrusted_projects_config = None
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002711 self.semaphore_handler = SemaphoreHandler()
James E. Blair08d9b782017-06-29 14:22:48 -07002712 # Metadata about projects for this tenant
2713 # canonical project name -> TenantProjectConfig
2714 self.project_configs = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002715
James E. Blairc2a54fd2017-03-29 15:19:26 -07002716 # A mapping of project names to projects. project_name ->
2717 # VALUE where VALUE is a further dictionary of
2718 # canonical_hostname -> Project.
2719 self.projects = {}
2720 self.canonical_hostnames = set()
2721
James E. Blair08d9b782017-06-29 14:22:48 -07002722 def _addProject(self, tpc):
James E. Blairc2a54fd2017-03-29 15:19:26 -07002723 """Add a project to the project index
2724
James E. Blair08d9b782017-06-29 14:22:48 -07002725 :arg TenantProjectConfig tpc: The TenantProjectConfig (with
2726 associated project) to add.
2727
James E. Blairc2a54fd2017-03-29 15:19:26 -07002728 """
James E. Blair08d9b782017-06-29 14:22:48 -07002729 project = tpc.project
James E. Blairc2a54fd2017-03-29 15:19:26 -07002730 self.canonical_hostnames.add(project.canonical_hostname)
2731 hostname_dict = self.projects.setdefault(project.name, {})
2732 if project.canonical_hostname in hostname_dict:
2733 raise Exception("Project %s is already in project index" %
2734 (project,))
2735 hostname_dict[project.canonical_hostname] = project
James E. Blair08d9b782017-06-29 14:22:48 -07002736 self.project_configs[project.canonical_name] = tpc
James E. Blairc2a54fd2017-03-29 15:19:26 -07002737
2738 def getProject(self, name):
2739 """Return a project given its name.
2740
2741 :arg str name: The name of the project. It may be fully
2742 qualified (E.g., "git.example.com/subpath/project") or may
2743 contain only the project name name may be supplied (E.g.,
2744 "subpath/project").
2745
2746 :returns: A tuple (trusted, project) or (None, None) if the
2747 project is not found or ambiguous. The "trusted" boolean
2748 indicates whether or not the project is trusted by this
2749 tenant.
2750 :rtype: (bool, Project)
2751
2752 """
2753 path = name.split('/', 1)
2754 if path[0] in self.canonical_hostnames:
2755 hostname = path[0]
2756 project_name = path[1]
2757 else:
2758 hostname = None
2759 project_name = name
2760 hostname_dict = self.projects.get(project_name)
2761 project = None
2762 if hostname_dict:
2763 if hostname:
2764 project = hostname_dict.get(hostname)
2765 else:
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002766 values = list(hostname_dict.values())
James E. Blairc2a54fd2017-03-29 15:19:26 -07002767 if len(values) == 1:
2768 project = values[0]
2769 else:
2770 raise Exception("Project name '%s' is ambiguous, "
2771 "please fully qualify the project "
2772 "with a hostname" % (name,))
2773 if project is None:
2774 return (None, None)
James E. Blair109da3f2017-04-04 14:39:43 -07002775 if project in self.config_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002776 return (True, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002777 if project in self.untrusted_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002778 return (False, project)
2779 # This should never happen:
2780 raise Exception("Project %s is neither trusted nor untrusted" %
2781 (project,))
2782
James E. Blairdaaf3262017-10-23 13:51:48 -07002783 def getProjectBranches(self, project):
2784 """Return a project's branches (filtered by this tenant config)
2785
2786 :arg Project project: The project object.
2787
2788 :returns: A list of branch names.
2789 :rtype: [str]
2790
2791 """
2792 tpc = self.project_configs[project.canonical_name]
2793 return tpc.branches
2794
James E. Blair08d9b782017-06-29 14:22:48 -07002795 def addConfigProject(self, tpc):
2796 self.config_projects.append(tpc.project)
2797 self._addProject(tpc)
James E. Blair5ac93842017-01-20 06:47:34 -08002798
James E. Blair08d9b782017-06-29 14:22:48 -07002799 def addUntrustedProject(self, tpc):
2800 self.untrusted_projects.append(tpc.project)
2801 self._addProject(tpc)
James E. Blair5ac93842017-01-20 06:47:34 -08002802
Jamie Lennoxbf997c82017-05-10 09:58:40 +10002803 def getSafeAttributes(self):
2804 return Attributes(name=self.name)
2805
James E. Blair59fdbac2015-12-07 17:08:06 -08002806
2807class Abide(object):
2808 def __init__(self):
2809 self.tenants = OrderedDict()
James E. Blairce8a2132016-05-19 15:21:52 -07002810
2811
2812class JobTimeData(object):
2813 format = 'B10H10H10B'
2814 version = 0
2815
2816 def __init__(self, path):
2817 self.path = path
2818 self.success_times = [0 for x in range(10)]
2819 self.failure_times = [0 for x in range(10)]
2820 self.results = [0 for x in range(10)]
2821
2822 def load(self):
2823 if not os.path.exists(self.path):
2824 return
Clint Byruma4471d12017-05-10 20:57:40 -04002825 with open(self.path, 'rb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002826 data = struct.unpack(self.format, f.read())
2827 version = data[0]
2828 if version != self.version:
2829 raise Exception("Unkown data version")
2830 self.success_times = list(data[1:11])
2831 self.failure_times = list(data[11:21])
2832 self.results = list(data[21:32])
2833
2834 def save(self):
2835 tmpfile = self.path + '.tmp'
2836 data = [self.version]
2837 data.extend(self.success_times)
2838 data.extend(self.failure_times)
2839 data.extend(self.results)
2840 data = struct.pack(self.format, *data)
Clint Byruma4471d12017-05-10 20:57:40 -04002841 with open(tmpfile, 'wb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002842 f.write(data)
2843 os.rename(tmpfile, self.path)
2844
2845 def add(self, elapsed, result):
2846 elapsed = int(elapsed)
2847 if result == 'SUCCESS':
2848 self.success_times.append(elapsed)
2849 self.success_times.pop(0)
2850 result = 0
2851 else:
2852 self.failure_times.append(elapsed)
2853 self.failure_times.pop(0)
2854 result = 1
2855 self.results.append(result)
2856 self.results.pop(0)
2857
2858 def getEstimatedTime(self):
2859 times = [x for x in self.success_times if x]
2860 if times:
2861 return float(sum(times)) / len(times)
2862 return 0.0
2863
2864
2865class TimeDataBase(object):
2866 def __init__(self, root):
2867 self.root = root
James E. Blairce8a2132016-05-19 15:21:52 -07002868
James E. Blairae0f23c2017-09-13 10:55:15 -06002869 def _getTD(self, build):
2870 if hasattr(build.build_set.item.change, 'branch'):
2871 branch = build.build_set.item.change.branch
2872 else:
2873 branch = ''
2874
2875 dir_path = os.path.join(
2876 self.root,
2877 build.build_set.item.pipeline.layout.tenant.name,
2878 build.build_set.item.change.project.canonical_name,
2879 branch)
2880 if not os.path.exists(dir_path):
2881 os.makedirs(dir_path)
2882 path = os.path.join(dir_path, build.job.name)
2883
2884 td = JobTimeData(path)
2885 td.load()
James E. Blairce8a2132016-05-19 15:21:52 -07002886 return td
2887
2888 def getEstimatedTime(self, name):
2889 return self._getTD(name).getEstimatedTime()
2890
James E. Blairae0f23c2017-09-13 10:55:15 -06002891 def update(self, build, elapsed, result):
2892 td = self._getTD(build)
James E. Blairce8a2132016-05-19 15:21:52 -07002893 td.add(elapsed, result)
2894 td.save()