blob: 11fa3839503737c30fc81d7afee090f023c9194b [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
Jamie Lennoxd4006d62017-04-06 10:34:04 +1000391 self.username = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800392
393 @property
394 def state(self):
395 return self._state
396
397 @state.setter
398 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800399 if value not in NODE_STATES:
400 raise TypeError("'%s' is not a valid state" % value)
James E. Blaira38c28e2017-01-04 10:33:20 -0800401 self._state = value
402 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700403
404 def __repr__(self):
James E. Blair16d96a02017-06-08 11:32:56 -0700405 return '<Node %s %s:%s>' % (self.id, self.name, self.label)
James E. Blair34776ee2016-08-25 13:53:54 -0700406
James E. Blair0d952152017-02-07 17:14:44 -0800407 def __ne__(self, other):
408 return not self.__eq__(other)
409
410 def __eq__(self, other):
411 if not isinstance(other, Node):
412 return False
413 return (self.name == other.name and
James E. Blair16d96a02017-06-08 11:32:56 -0700414 self.label == other.label and
James E. Blair0d952152017-02-07 17:14:44 -0800415 self.id == other.id)
416
James E. Blaircacdf2b2017-01-04 13:14:37 -0800417 def toDict(self):
418 d = {}
419 d['state'] = self.state
David Shrewsburyffab07a2017-07-24 12:45:07 -0400420 d['hold_job'] = self.hold_job
David Shrewsburyf9af9df2017-08-01 15:19:26 -0400421 d['comment'] = self.comment
James E. Blaircacdf2b2017-01-04 13:14:37 -0800422 for k in self._keys:
423 d[k] = getattr(self, k)
424 return d
425
James E. Blaira38c28e2017-01-04 10:33:20 -0800426 def updateFromDict(self, data):
427 self._state = data['state']
James E. Blaircacdf2b2017-01-04 13:14:37 -0800428 keys = []
429 for k, v in data.items():
430 if k == 'state':
431 continue
432 keys.append(k)
433 setattr(self, k, v)
434 self._keys = keys
James E. Blaira38c28e2017-01-04 10:33:20 -0800435
James E. Blair34776ee2016-08-25 13:53:54 -0700436
Monty Taylor7b19ba72017-05-24 07:42:54 -0500437class Group(object):
438 """A logical group of nodes for use by a job.
439
440 A Group is a named set of node names that will be provided to
441 jobs in the inventory to describe logical units where some subset of tasks
442 run.
443 """
444
445 def __init__(self, name, nodes):
446 self.name = name
447 self.nodes = nodes
448
449 def __repr__(self):
450 return '<Group %s %s>' % (self.name, str(self.nodes))
451
452 def __ne__(self, other):
453 return not self.__eq__(other)
454
455 def __eq__(self, other):
456 if not isinstance(other, Group):
457 return False
458 return (self.name == other.name and
459 self.nodes == other.nodes)
460
461 def toDict(self):
462 return {
463 'name': self.name,
464 'nodes': self.nodes
465 }
466
467
James E. Blaira98340f2016-09-02 11:33:49 -0700468class NodeSet(object):
469 """A set of nodes.
470
471 In configuration, NodeSets are attributes of Jobs indicating that
472 a Job requires nodes matching this description.
473
474 They may appear as top-level configuration objects and be named,
475 or they may appears anonymously in in-line job definitions.
476 """
477
478 def __init__(self, name=None):
479 self.name = name or ''
480 self.nodes = OrderedDict()
Monty Taylor7b19ba72017-05-24 07:42:54 -0500481 self.groups = OrderedDict()
James E. Blaira98340f2016-09-02 11:33:49 -0700482
James E. Blair1774dd52017-02-03 10:52:32 -0800483 def __ne__(self, other):
484 return not self.__eq__(other)
485
486 def __eq__(self, other):
487 if not isinstance(other, NodeSet):
488 return False
489 return (self.name == other.name and
490 self.nodes == other.nodes)
491
James E. Blaircbf43672017-01-04 14:33:41 -0800492 def copy(self):
493 n = NodeSet(self.name)
494 for name, node in self.nodes.items():
James E. Blair16d96a02017-06-08 11:32:56 -0700495 n.addNode(Node(node.name, node.label))
Monty Taylor7b19ba72017-05-24 07:42:54 -0500496 for name, group in self.groups.items():
497 n.addGroup(Group(group.name, group.nodes[:]))
James E. Blaircbf43672017-01-04 14:33:41 -0800498 return n
499
James E. Blaira98340f2016-09-02 11:33:49 -0700500 def addNode(self, node):
501 if node.name in self.nodes:
502 raise Exception("Duplicate node in %s" % (self,))
503 self.nodes[node.name] = node
504
James E. Blair0eaad552016-09-02 12:09:54 -0700505 def getNodes(self):
Clint Byruma4471d12017-05-10 20:57:40 -0400506 return list(self.nodes.values())
James E. Blair0eaad552016-09-02 12:09:54 -0700507
Monty Taylor7b19ba72017-05-24 07:42:54 -0500508 def addGroup(self, group):
509 if group.name in self.groups:
510 raise Exception("Duplicate group in %s" % (self,))
511 self.groups[group.name] = group
512
513 def getGroups(self):
514 return list(self.groups.values())
515
James E. Blaira98340f2016-09-02 11:33:49 -0700516 def __repr__(self):
517 if self.name:
518 name = self.name + ' '
519 else:
520 name = ''
Monty Taylor7b19ba72017-05-24 07:42:54 -0500521 return '<NodeSet %s%s%s>' % (name, self.nodes, self.groups)
James E. Blaira98340f2016-09-02 11:33:49 -0700522
Tristan Cacqueray82f864b2017-08-01 05:54:42 +0000523 def __len__(self):
524 return len(self.nodes)
525
James E. Blaira98340f2016-09-02 11:33:49 -0700526
James E. Blair34776ee2016-08-25 13:53:54 -0700527class NodeRequest(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700528 """A request for a set of nodes."""
529
James E. Blair8b2a1472017-02-19 15:33:55 -0800530 def __init__(self, requestor, build_set, job, nodeset):
531 self.requestor = requestor
James E. Blair34776ee2016-08-25 13:53:54 -0700532 self.build_set = build_set
533 self.job = job
James E. Blair0eaad552016-09-02 12:09:54 -0700534 self.nodeset = nodeset
James E. Blair803e94f2017-01-06 09:18:59 -0800535 self._state = STATE_REQUESTED
James E. Blair4f1731b2017-10-10 18:11:42 -0700536 self.requested_time = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -0800537 self.state_time = time.time()
538 self.stat = None
539 self.uid = uuid4().hex
James E. Blaircbf43672017-01-04 14:33:41 -0800540 self.id = None
James E. Blaircbbce0d2017-05-19 07:28:29 -0700541 # Zuul internal flags (not stored in ZK so they are not
James E. Blaira38c28e2017-01-04 10:33:20 -0800542 # overwritten).
543 self.failed = False
James E. Blaircbbce0d2017-05-19 07:28:29 -0700544 self.canceled = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800545
546 @property
Monty Taylor6dc5bc12017-09-29 15:47:31 -0500547 def priority(self):
Monty Taylorb5882052017-09-29 19:12:52 -0500548 if self.build_set:
549 precedence = self.build_set.item.pipeline.precedence
550 else:
551 precedence = PRECEDENCE_NORMAL
552 return PRIORITY_MAP[precedence]
Monty Taylor6dc5bc12017-09-29 15:47:31 -0500553
554 @property
James E. Blair6ab79e02017-01-06 10:10:17 -0800555 def fulfilled(self):
556 return (self._state == STATE_FULFILLED) and not self.failed
557
558 @property
James E. Blairdce6cea2016-12-20 16:45:32 -0800559 def state(self):
560 return self._state
561
562 @state.setter
563 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800564 if value not in REQUEST_STATES:
565 raise TypeError("'%s' is not a valid state" % value)
James E. Blairdce6cea2016-12-20 16:45:32 -0800566 self._state = value
567 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700568
569 def __repr__(self):
James E. Blaircbf43672017-01-04 14:33:41 -0800570 return '<NodeRequest %s %s>' % (self.id, self.nodeset)
James E. Blair34776ee2016-08-25 13:53:54 -0700571
James E. Blairdce6cea2016-12-20 16:45:32 -0800572 def toDict(self):
573 d = {}
James E. Blair16d96a02017-06-08 11:32:56 -0700574 nodes = [n.label for n in self.nodeset.getNodes()]
James E. Blairdce6cea2016-12-20 16:45:32 -0800575 d['node_types'] = nodes
James E. Blair8b2a1472017-02-19 15:33:55 -0800576 d['requestor'] = self.requestor
James E. Blairdce6cea2016-12-20 16:45:32 -0800577 d['state'] = self.state
578 d['state_time'] = self.state_time
579 return d
580
581 def updateFromDict(self, data):
582 self._state = data['state']
583 self.state_time = data['state_time']
584
James E. Blair34776ee2016-08-25 13:53:54 -0700585
James E. Blair01f83b72017-03-15 13:03:40 -0700586class Secret(object):
587 """A collection of private data.
588
589 In configuration, Secrets are collections of private data in
590 key-value pair format. They are defined as top-level
591 configuration objects and then referenced by Jobs.
592
593 """
594
James E. Blair8525e2b2017-03-15 14:05:47 -0700595 def __init__(self, name, source_context):
James E. Blair01f83b72017-03-15 13:03:40 -0700596 self.name = name
James E. Blair8525e2b2017-03-15 14:05:47 -0700597 self.source_context = source_context
James E. Blair01f83b72017-03-15 13:03:40 -0700598 # The secret data may or may not be encrypted. This attribute
599 # is named 'secret_data' to make it easy to search for and
600 # spot where it is directly used.
601 self.secret_data = {}
602
603 def __ne__(self, other):
604 return not self.__eq__(other)
605
606 def __eq__(self, other):
607 if not isinstance(other, Secret):
608 return False
609 return (self.name == other.name and
James E. Blair8525e2b2017-03-15 14:05:47 -0700610 self.source_context == other.source_context and
James E. Blair01f83b72017-03-15 13:03:40 -0700611 self.secret_data == other.secret_data)
612
613 def __repr__(self):
614 return '<Secret %s>' % (self.name,)
615
James E. Blair18f86a32017-03-15 14:43:26 -0700616 def decrypt(self, private_key):
617 """Return a copy of this secret with any encrypted data decrypted.
618 Note that the original remains encrypted."""
619
620 r = copy.deepcopy(self)
621 decrypted_secret_data = {}
622 for k, v in r.secret_data.items():
623 if hasattr(v, 'decrypt'):
624 decrypted_secret_data[k] = v.decrypt(private_key)
625 else:
626 decrypted_secret_data[k] = v
627 r.secret_data = decrypted_secret_data
628 return r
629
James E. Blair01f83b72017-03-15 13:03:40 -0700630
James E. Blaircdab2032017-02-01 09:09:29 -0800631class SourceContext(object):
632 """A reference to the branch of a project in configuration.
633
634 Jobs and playbooks reference this to keep track of where they
635 originate."""
636
James E. Blair6f140c72017-03-03 10:32:07 -0800637 def __init__(self, project, branch, path, trusted):
James E. Blaircdab2032017-02-01 09:09:29 -0800638 self.project = project
639 self.branch = branch
James E. Blair6f140c72017-03-03 10:32:07 -0800640 self.path = path
Monty Taylore6562aa2017-02-20 07:37:39 -0500641 self.trusted = trusted
James E. Blair7edc25f2017-10-26 10:47:14 -0700642 self.implied_branch_matchers = None
James E. Blaircdab2032017-02-01 09:09:29 -0800643
James E. Blair6f140c72017-03-03 10:32:07 -0800644 def __str__(self):
645 return '%s/%s@%s' % (self.project, self.path, self.branch)
646
James E. Blaircdab2032017-02-01 09:09:29 -0800647 def __repr__(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800648 return '<SourceContext %s trusted:%s>' % (str(self),
649 self.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800650
James E. Blaira7f51ca2017-02-07 16:01:26 -0800651 def __deepcopy__(self, memo):
652 return self.copy()
653
654 def copy(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800655 return self.__class__(self.project, self.branch, self.path,
656 self.trusted)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800657
Tristan Cacqueraye50af2e2017-09-19 14:18:28 +0000658 def isSameProject(self, other):
659 if not isinstance(other, SourceContext):
660 return False
661 return (self.project == other.project and
662 self.branch == other.branch and
663 self.trusted == other.trusted)
664
James E. Blaircdab2032017-02-01 09:09:29 -0800665 def __ne__(self, other):
666 return not self.__eq__(other)
667
668 def __eq__(self, other):
669 if not isinstance(other, SourceContext):
670 return False
671 return (self.project == other.project and
672 self.branch == other.branch and
James E. Blair6f140c72017-03-03 10:32:07 -0800673 self.path == other.path and
Monty Taylore6562aa2017-02-20 07:37:39 -0500674 self.trusted == other.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800675
676
James E. Blair66b274e2017-01-31 14:47:52 -0800677class PlaybookContext(object):
James E. Blaircdab2032017-02-01 09:09:29 -0800678
James E. Blair66b274e2017-01-31 14:47:52 -0800679 """A reference to a playbook in the context of a project.
680
681 Jobs refer to objects of this class for their main, pre, and post
682 playbooks so that we can keep track of which repos and security
James E. Blair74a82cf2017-07-12 17:23:08 -0700683 contexts are needed in order to run them.
James E. Blair66b274e2017-01-31 14:47:52 -0800684
James E. Blair74a82cf2017-07-12 17:23:08 -0700685 We also keep a list of roles so that playbooks only run with the
686 roles which were defined at the point the playbook was defined.
687
688 """
689
James E. Blair892cca62017-08-09 11:36:58 -0700690 def __init__(self, source_context, path, roles, secrets):
James E. Blaircdab2032017-02-01 09:09:29 -0800691 self.source_context = source_context
James E. Blair66b274e2017-01-31 14:47:52 -0800692 self.path = path
James E. Blair74a82cf2017-07-12 17:23:08 -0700693 self.roles = roles
James E. Blair892cca62017-08-09 11:36:58 -0700694 self.secrets = secrets
James E. Blair66b274e2017-01-31 14:47:52 -0800695
696 def __repr__(self):
James E. Blaircdab2032017-02-01 09:09:29 -0800697 return '<PlaybookContext %s %s>' % (self.source_context,
698 self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800699
700 def __ne__(self, other):
701 return not self.__eq__(other)
702
703 def __eq__(self, other):
704 if not isinstance(other, PlaybookContext):
705 return False
James E. Blaircdab2032017-02-01 09:09:29 -0800706 return (self.source_context == other.source_context and
James E. Blair74a82cf2017-07-12 17:23:08 -0700707 self.path == other.path and
James E. Blair892cca62017-08-09 11:36:58 -0700708 self.roles == other.roles and
709 self.secrets == other.secrets)
James E. Blair66b274e2017-01-31 14:47:52 -0800710
James E. Blairc32a8352017-10-11 16:27:50 -0700711 def copy(self):
712 r = PlaybookContext(self.source_context,
713 self.path,
714 self.roles,
715 self.secrets)
716 return r
717
James E. Blair66b274e2017-01-31 14:47:52 -0800718 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400719 # Render to a dict to use in passing json to the executor
James E. Blair892cca62017-08-09 11:36:58 -0700720 secrets = {}
721 for secret in self.secrets:
722 secret_data = copy.deepcopy(secret.secret_data)
723 secrets[secret.name] = secret_data
James E. Blair66b274e2017-01-31 14:47:52 -0800724 return dict(
James E. Blaircdab2032017-02-01 09:09:29 -0800725 connection=self.source_context.project.connection_name,
726 project=self.source_context.project.name,
727 branch=self.source_context.branch,
Monty Taylore6562aa2017-02-20 07:37:39 -0500728 trusted=self.source_context.trusted,
James E. Blair74a82cf2017-07-12 17:23:08 -0700729 roles=[r.toDict() for r in self.roles],
James E. Blair892cca62017-08-09 11:36:58 -0700730 secrets=secrets,
James E. Blaircdab2032017-02-01 09:09:29 -0800731 path=self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800732
733
Monty Taylorb934c1a2017-06-16 19:31:47 -0500734class Role(object, metaclass=abc.ABCMeta):
James E. Blair5ac93842017-01-20 06:47:34 -0800735 """A reference to an ansible role."""
736
737 def __init__(self, target_name):
738 self.target_name = target_name
739
740 @abc.abstractmethod
741 def __repr__(self):
742 pass
743
744 def __ne__(self, other):
745 return not self.__eq__(other)
746
747 @abc.abstractmethod
748 def __eq__(self, other):
749 if not isinstance(other, Role):
750 return False
751 return (self.target_name == other.target_name)
752
753 @abc.abstractmethod
754 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400755 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800756 return dict(target_name=self.target_name)
757
758
759class ZuulRole(Role):
760 """A reference to an ansible role in a Zuul project."""
761
James E. Blairbb94dfa2017-07-11 07:45:19 -0700762 def __init__(self, target_name, connection_name, project_name,
763 implicit=False):
James E. Blair5ac93842017-01-20 06:47:34 -0800764 super(ZuulRole, self).__init__(target_name)
765 self.connection_name = connection_name
766 self.project_name = project_name
James E. Blairbb94dfa2017-07-11 07:45:19 -0700767 self.implicit = implicit
James E. Blair5ac93842017-01-20 06:47:34 -0800768
769 def __repr__(self):
770 return '<ZuulRole %s %s>' % (self.project_name, self.target_name)
771
Clint Byrumaf7438f2017-05-10 17:26:57 -0400772 __hash__ = object.__hash__
773
James E. Blair5ac93842017-01-20 06:47:34 -0800774 def __eq__(self, other):
775 if not isinstance(other, ZuulRole):
776 return False
James E. Blairbb94dfa2017-07-11 07:45:19 -0700777 # Implicit is not consulted for equality so that we can handle
778 # implicit to explicit conversions.
James E. Blair5ac93842017-01-20 06:47:34 -0800779 return (super(ZuulRole, self).__eq__(other) and
James E. Blair1b27f6a2017-07-14 14:09:07 -0700780 self.connection_name == other.connection_name and
James E. Blair6563e4b2017-04-28 08:14:48 -0700781 self.project_name == other.project_name)
James E. Blair5ac93842017-01-20 06:47:34 -0800782
783 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400784 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800785 d = super(ZuulRole, self).toDict()
786 d['type'] = 'zuul'
787 d['connection'] = self.connection_name
788 d['project'] = self.project_name
James E. Blairbb94dfa2017-07-11 07:45:19 -0700789 d['implicit'] = self.implicit
James E. Blair5ac93842017-01-20 06:47:34 -0800790 return d
791
792
James E. Blairee743612012-05-29 14:49:32 -0700793class Job(object):
James E. Blair66b274e2017-01-31 14:47:52 -0800794
James E. Blaira7f51ca2017-02-07 16:01:26 -0800795 """A Job represents the defintion of actions to perform.
796
James E. Blaird4ade8c2017-02-19 15:25:46 -0800797 A Job is an abstract configuration concept. It describes what,
798 where, and under what circumstances something should be run
799 (contrast this with Build which is a concrete single execution of
800 a Job).
801
James E. Blaira7f51ca2017-02-07 16:01:26 -0800802 NB: Do not modify attributes of this class, set them directly
803 (e.g., "job.run = ..." rather than "job.run.append(...)").
804 """
Monty Taylora42a55b2016-07-29 07:53:33 -0700805
James E. Blairc32a8352017-10-11 16:27:50 -0700806 BASE_JOB_MARKER = object()
807
James E. Blairee743612012-05-29 14:49:32 -0700808 def __init__(self, name):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800809 # These attributes may override even the final form of a job
810 # in the context of a project-pipeline. They can not affect
811 # the execution of the job, but only whether the job is run
812 # and how it is reported.
813 self.context_attributes = dict(
814 voting=True,
815 hold_following_changes=False,
James E. Blair1774dd52017-02-03 10:52:32 -0800816 failure_message=None,
817 success_message=None,
818 failure_url=None,
819 success_url=None,
820 # Matchers. These are separate so they can be individually
821 # overidden.
822 branch_matcher=None,
823 file_matcher=None,
824 irrelevant_file_matcher=None, # skip-if
James E. Blaira7f51ca2017-02-07 16:01:26 -0800825 tags=frozenset(),
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200826 dependencies=frozenset(),
James E. Blair1774dd52017-02-03 10:52:32 -0800827 )
828
James E. Blaira7f51ca2017-02-07 16:01:26 -0800829 # These attributes affect how the job is actually run and more
830 # care must be taken when overriding them. If a job is
831 # declared "final", these may not be overriden in a
832 # project-pipeline.
833 self.execution_attributes = dict(
James E. Blairc32a8352017-10-11 16:27:50 -0700834 parent=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800835 timeout=None,
James E. Blair490cf042017-02-24 23:07:21 -0500836 variables={},
James E. Blaira7f51ca2017-02-07 16:01:26 -0800837 nodeset=NodeSet(),
James E. Blaira7f51ca2017-02-07 16:01:26 -0800838 workspace=None,
839 pre_run=(),
840 post_run=(),
841 run=(),
Tobias Henkel9a0e1942017-03-20 16:16:02 +0100842 semaphore=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800843 attempts=3,
844 final=False,
James E. Blair5fc81922017-07-12 13:19:37 -0700845 roles=(),
James E. Blair912322f2017-05-23 13:11:25 -0700846 required_projects={},
James E. Blairb3f5db12017-03-17 12:57:39 -0700847 allowed_projects=None,
James E. Blair7391ecb2017-05-23 13:32:40 -0700848 override_branch=None,
James E. Blairedff2c22017-10-30 14:04:48 -0700849 override_checkout=None,
James E. Blair8eb564a2017-08-10 09:21:41 -0700850 post_review=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800851 )
852
853 # These are generally internal attributes which are not
854 # accessible via configuration.
855 self.other_attributes = dict(
856 name=None,
857 source_context=None,
James E. Blair167d6cd2017-09-29 14:24:42 -0700858 source_line=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800859 inheritance_path=(),
James E. Blair698703c2017-09-15 20:58:30 -0600860 parent_data=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800861 )
862
863 self.inheritable_attributes = {}
864 self.inheritable_attributes.update(self.context_attributes)
865 self.inheritable_attributes.update(self.execution_attributes)
866 self.attributes = {}
867 self.attributes.update(self.inheritable_attributes)
868 self.attributes.update(self.other_attributes)
869
James E. Blairee743612012-05-29 14:49:32 -0700870 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800871
James E. Blair66b274e2017-01-31 14:47:52 -0800872 def __ne__(self, other):
873 return not self.__eq__(other)
874
Paul Belangere22baea2016-11-03 16:59:27 -0400875 def __eq__(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800876 # Compare the name and all inheritable attributes to determine
877 # whether two jobs with the same name are identically
878 # configured. Useful upon reconfiguration.
879 if not isinstance(other, Job):
880 return False
881 if self.name != other.name:
882 return False
883 for k, v in self.attributes.items():
884 if getattr(self, k) != getattr(other, k):
885 return False
886 return True
James E. Blairee743612012-05-29 14:49:32 -0700887
Clint Byrumaf7438f2017-05-10 17:26:57 -0400888 __hash__ = object.__hash__
889
James E. Blairee743612012-05-29 14:49:32 -0700890 def __str__(self):
891 return self.name
892
893 def __repr__(self):
James E. Blair167d6cd2017-09-29 14:24:42 -0700894 return '<Job %s branches: %s source: %s#%s>' % (
895 self.name,
896 self.branch_matcher,
897 self.source_context,
898 self.source_line)
James E. Blair83005782015-12-11 14:46:03 -0800899
James E. Blaira7f51ca2017-02-07 16:01:26 -0800900 def __getattr__(self, name):
901 v = self.__dict__.get(name)
902 if v is None:
James E. Blairaf8b2082017-10-03 15:38:27 -0700903 return self.attributes[name]
James E. Blaira7f51ca2017-02-07 16:01:26 -0800904 return v
905
906 def _get(self, name):
907 return self.__dict__.get(name)
908
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100909 def getSafeAttributes(self):
910 return Attributes(name=self.name)
911
James E. Blairc32a8352017-10-11 16:27:50 -0700912 def isBase(self):
913 return self.parent is self.BASE_JOB_MARKER
914
915 def setBase(self):
916 self.inheritance_path = self.inheritance_path + (repr(self),)
917
James E. Blair5fc81922017-07-12 13:19:37 -0700918 def addRoles(self, roles):
James E. Blairbb94dfa2017-07-11 07:45:19 -0700919 newroles = []
920 # Start with a copy of the existing roles, but if any of them
921 # are implicit roles which are identified as explicit in the
922 # new roles list, replace them with the explicit version.
923 changed = False
924 for existing_role in self.roles:
925 if existing_role in roles:
926 new_role = roles[roles.index(existing_role)]
927 else:
928 new_role = None
929 if (new_role and
930 isinstance(new_role, ZuulRole) and
931 isinstance(existing_role, ZuulRole) and
932 existing_role.implicit and not new_role.implicit):
933 newroles.append(new_role)
934 changed = True
935 else:
936 newroles.append(existing_role)
937 # Now add the new roles.
James E. Blair4eec8282017-07-12 17:33:26 -0700938 for role in reversed(roles):
James E. Blair5fc81922017-07-12 13:19:37 -0700939 if role not in newroles:
James E. Blair4eec8282017-07-12 17:33:26 -0700940 newroles.insert(0, role)
James E. Blairbb94dfa2017-07-11 07:45:19 -0700941 changed = True
942 if changed:
943 self.roles = tuple(newroles)
James E. Blair5fc81922017-07-12 13:19:37 -0700944
James E. Blaire74f5712017-09-29 15:14:31 -0700945 def setBranchMatcher(self, branches):
946 # Set the branch matcher to match any of the supplied branches
947 matchers = []
948 for branch in branches:
949 matchers.append(change_matcher.BranchMatcher(branch))
950 self.branch_matcher = change_matcher.MatchAny(matchers)
951
James E. Blaire36d1a32017-11-28 13:33:38 -0800952 def getSimpleBranchMatcher(self):
953 # If the job has a simple branch matcher, return it; otherwise None.
954 if not self.branch_matcher:
955 return None
956 m = self.branch_matcher
957 if not isinstance(m, change_matcher.AbstractMatcherCollection):
958 return None
959 if len(m.matchers) != 1:
960 return None
961 m = m.matchers[0]
962 if not isinstance(m, change_matcher.BranchMatcher):
963 return None
964 return m._regex
965
966 def addBranchMatcher(self, branch):
967 # Add a branch matcher that combines as a boolean *and* with
968 # existing branch matchers, if any.
969 matchers = [change_matcher.BranchMatcher(branch)]
970 if self.branch_matcher:
971 matchers.append(self.branch_matcher)
972 self.branch_matcher = change_matcher.MatchAll(matchers)
973
James E. Blair490cf042017-02-24 23:07:21 -0500974 def updateVariables(self, other_vars):
James E. Blairaf8b2082017-10-03 15:38:27 -0700975 v = copy.deepcopy(self.variables)
James E. Blair490cf042017-02-24 23:07:21 -0500976 Job._deepUpdate(v, other_vars)
977 self.variables = v
978
James E. Blair698703c2017-09-15 20:58:30 -0600979 def updateParentData(self, other_vars):
980 # Update variables, but give the current values priority (used
981 # for job return data which is lower precedence than defined
982 # job vars).
983 v = self.parent_data or {}
984 Job._deepUpdate(v, other_vars)
985 # To avoid running afoul of checks that jobs don't set zuul
986 # variables, remove them from parent data here.
987 if 'zuul' in v:
988 del v['zuul']
989 self.parent_data = v
990 v = copy.deepcopy(self.parent_data)
991 Job._deepUpdate(v, self.variables)
992 self.variables = v
993
James E. Blair912322f2017-05-23 13:11:25 -0700994 def updateProjects(self, other_projects):
James E. Blairaf8b2082017-10-03 15:38:27 -0700995 required_projects = self.required_projects.copy()
996 required_projects.update(other_projects)
James E. Blair912322f2017-05-23 13:11:25 -0700997 self.required_projects = required_projects
James E. Blair27f3dfc2017-05-23 13:07:28 -0700998
James E. Blair490cf042017-02-24 23:07:21 -0500999 @staticmethod
1000 def _deepUpdate(a, b):
1001 # Merge nested dictionaries if possible, otherwise, overwrite
1002 # the value in 'a' with the value in 'b'.
1003 for k, bv in b.items():
1004 av = a.get(k)
1005 if isinstance(av, dict) and isinstance(bv, dict):
1006 Job._deepUpdate(av, bv)
1007 else:
1008 a[k] = bv
1009
James E. Blaira7f51ca2017-02-07 16:01:26 -08001010 def copy(self):
1011 job = Job(self.name)
1012 for k in self.attributes:
1013 if self._get(k) is not None:
1014 setattr(job, k, copy.deepcopy(self._get(k)))
1015 return job
1016
James E. Blairc32a8352017-10-11 16:27:50 -07001017 def freezePlaybooks(self, pblist):
1018 """Take a list of playbooks, and return a copy of it updated with this
1019 job's roles.
1020
1021 """
1022
1023 ret = []
1024 for old_pb in pblist:
1025 pb = old_pb.copy()
1026 pb.roles = self.roles
1027 ret.append(pb)
1028 return tuple(ret)
1029
James E. Blaira7f51ca2017-02-07 16:01:26 -08001030 def applyVariant(self, other):
1031 """Copy the attributes which have been set on the other job to this
1032 job."""
James E. Blair83005782015-12-11 14:46:03 -08001033 if not isinstance(other, Job):
1034 raise Exception("Job unable to inherit from %s" % (other,))
James E. Blaira7f51ca2017-02-07 16:01:26 -08001035
1036 for k in self.execution_attributes:
1037 if (other._get(k) is not None and
1038 k not in set(['final'])):
1039 if self.final:
1040 raise Exception("Unable to modify final job %s attribute "
1041 "%s=%s with variant %s" % (
1042 repr(self), k, other._get(k),
1043 repr(other)))
James E. Blairc32a8352017-10-11 16:27:50 -07001044 if k not in set(['pre_run', 'run', 'post_run', 'roles',
1045 'variables', 'required_projects']):
1046 # TODO(jeblair): determine if deepcopy is required
James E. Blaira7f51ca2017-02-07 16:01:26 -08001047 setattr(self, k, copy.deepcopy(other._get(k)))
1048
1049 # Don't set final above so that we don't trip an error halfway
1050 # through assignment.
1051 if other.final != self.attributes['final']:
1052 self.final = other.final
1053
James E. Blairc32a8352017-10-11 16:27:50 -07001054 # We must update roles before any playbook contexts
James E. Blair5ac93842017-01-20 06:47:34 -08001055 if other._get('roles') is not None:
James E. Blair5fc81922017-07-12 13:19:37 -07001056 self.addRoles(other.roles)
James E. Blairc32a8352017-10-11 16:27:50 -07001057
James E. Blairc32a8352017-10-11 16:27:50 -07001058 if other._get('run') is not None:
1059 other_run = self.freezePlaybooks(other.run)
1060 self.run = other_run
1061 if other._get('pre_run') is not None:
1062 other_pre_run = self.freezePlaybooks(other.pre_run)
1063 self.pre_run = self.pre_run + other_pre_run
1064 if other._get('post_run') is not None:
1065 other_post_run = self.freezePlaybooks(other.post_run)
1066 self.post_run = other_post_run + self.post_run
James E. Blair490cf042017-02-24 23:07:21 -05001067 if other._get('variables') is not None:
1068 self.updateVariables(other.variables)
James E. Blair912322f2017-05-23 13:11:25 -07001069 if other._get('required_projects') is not None:
1070 self.updateProjects(other.required_projects)
James E. Blaira7f51ca2017-02-07 16:01:26 -08001071
1072 for k in self.context_attributes:
1073 if (other._get(k) is not None and
1074 k not in set(['tags'])):
1075 setattr(self, k, copy.deepcopy(other._get(k)))
1076
1077 if other._get('tags') is not None:
1078 self.tags = self.tags.union(other.tags)
1079
James E. Blairc32a8352017-10-11 16:27:50 -07001080 self.inheritance_path = self.inheritance_path + (repr(other),)
James E. Blairee743612012-05-29 14:49:32 -07001081
James E. Blaire421a232012-07-25 16:59:21 -07001082 def changeMatches(self, change):
James E. Blair83005782015-12-11 14:46:03 -08001083 if self.branch_matcher and not self.branch_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -08001084 return False
1085
James E. Blair83005782015-12-11 14:46:03 -08001086 if self.file_matcher and not self.file_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -08001087 return False
1088
James E. Blair83005782015-12-11 14:46:03 -08001089 # NB: This is a negative match.
1090 if (self.irrelevant_file_matcher and
1091 self.irrelevant_file_matcher.matches(change)):
Maru Newby3fe5f852015-01-13 04:22:14 +00001092 return False
1093
James E. Blair70c71582013-03-06 08:50:50 -08001094 return True
James E. Blaire5a847f2012-07-10 15:29:14 -07001095
James E. Blair1e8dd892012-05-30 09:15:05 -07001096
James E. Blair912322f2017-05-23 13:11:25 -07001097class JobProject(object):
James E. Blair27f3dfc2017-05-23 13:07:28 -07001098 """ A reference to a project from a job. """
1099
James E. Blairedff2c22017-10-30 14:04:48 -07001100 def __init__(self, project_name, override_branch=None,
1101 override_checkout=None):
James E. Blair27f3dfc2017-05-23 13:07:28 -07001102 self.project_name = project_name
1103 self.override_branch = override_branch
James E. Blairedff2c22017-10-30 14:04:48 -07001104 self.override_checkout = override_checkout
James E. Blair27f3dfc2017-05-23 13:07:28 -07001105
1106
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001107class JobList(object):
1108 """ A list of jobs in a project's pipeline. """
Monty Taylora42a55b2016-07-29 07:53:33 -07001109
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001110 def __init__(self):
1111 self.jobs = OrderedDict() # job.name -> [job, ...]
James E. Blair12748b52017-02-07 17:17:53 -08001112
James E. Blairee743612012-05-29 14:49:32 -07001113 def addJob(self, job):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001114 if job.name in self.jobs:
1115 self.jobs[job.name].append(job)
1116 else:
1117 self.jobs[job.name] = [job]
James E. Blairee743612012-05-29 14:49:32 -07001118
James E. Blaire74f5712017-09-29 15:14:31 -07001119 def inheritFrom(self, other, implied_branch):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001120 for jobname, jobs in other.jobs.items():
James E. Blaire74f5712017-09-29 15:14:31 -07001121 joblist = self.jobs.setdefault(jobname, [])
1122 for job in jobs:
James E. Blaire36d1a32017-11-28 13:33:38 -08001123 if implied_branch:
1124 # If setting an implied branch and the current
1125 # branch matcher is a simple match for a different
1126 # branch, then simply do not add this job. If it
1127 # is absent, set it to the implied branch.
1128 # Otherwise, combine it with the implied branch to
1129 # ensure that it still only affects this branch
1130 # (whatever else it may do).
1131 simple_branch = job.getSimpleBranchMatcher()
1132 if simple_branch and simple_branch != implied_branch:
1133 # Job is for a different branch, don't add it.
1134 continue
1135 if not simple_branch:
1136 # The branch matcher could be complex, or
1137 # missing. Add our implied matcher.
1138 job = job.copy()
1139 job.addBranchMatcher(implied_branch)
1140 # Otherwise we have a simple branch matcher which
1141 # is the same as our implied branch, the job can
1142 # be added as-is.
James E. Blair2a664502017-10-27 11:39:33 -07001143 if job not in joblist:
1144 joblist.append(job)
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001145
1146
1147class JobGraph(object):
1148 """ A JobGraph represents the dependency graph between Job."""
1149
1150 def __init__(self):
1151 self.jobs = OrderedDict() # job_name -> Job
1152 self._dependencies = {} # dependent_job_name -> set(parent_job_names)
1153
1154 def __repr__(self):
1155 return '<JobGraph %s>' % (self.jobs)
1156
1157 def addJob(self, job):
1158 # A graph must be created after the job list is frozen,
1159 # therefore we should only get one job with the same name.
1160 if job.name in self.jobs:
1161 raise Exception("Job %s already added" % (job.name,))
1162 self.jobs[job.name] = job
1163 # Append the dependency information
1164 self._dependencies.setdefault(job.name, set())
1165 try:
1166 for dependency in job.dependencies:
1167 # Make sure a circular dependency is never created
1168 ancestor_jobs = self._getParentJobNamesRecursively(
1169 dependency, soft=True)
1170 ancestor_jobs.add(dependency)
1171 if any((job.name == anc_job) for anc_job in ancestor_jobs):
1172 raise Exception("Dependency cycle detected in job %s" %
1173 (job.name,))
1174 self._dependencies[job.name].add(dependency)
1175 except Exception:
1176 del self.jobs[job.name]
1177 del self._dependencies[job.name]
1178 raise
1179
1180 def getJobs(self):
Clint Byruma4471d12017-05-10 20:57:40 -04001181 return list(self.jobs.values()) # Report in the order of layout cfg
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001182
1183 def _getDirectDependentJobs(self, parent_job):
1184 ret = set()
1185 for dependent_name, parent_names in self._dependencies.items():
1186 if parent_job in parent_names:
1187 ret.add(dependent_name)
1188 return ret
1189
1190 def getDependentJobsRecursively(self, parent_job):
1191 all_dependent_jobs = set()
1192 jobs_to_iterate = set([parent_job])
1193 while len(jobs_to_iterate) > 0:
1194 current_job = jobs_to_iterate.pop()
1195 current_dependent_jobs = self._getDirectDependentJobs(current_job)
1196 new_dependent_jobs = current_dependent_jobs - all_dependent_jobs
1197 jobs_to_iterate |= new_dependent_jobs
1198 all_dependent_jobs |= new_dependent_jobs
1199 return [self.jobs[name] for name in all_dependent_jobs]
1200
1201 def getParentJobsRecursively(self, dependent_job):
1202 return [self.jobs[name] for name in
1203 self._getParentJobNamesRecursively(dependent_job)]
1204
1205 def _getParentJobNamesRecursively(self, dependent_job, soft=False):
1206 all_parent_jobs = set()
1207 jobs_to_iterate = set([dependent_job])
1208 while len(jobs_to_iterate) > 0:
1209 current_job = jobs_to_iterate.pop()
1210 current_parent_jobs = self._dependencies.get(current_job)
1211 if current_parent_jobs is None:
1212 if soft:
1213 current_parent_jobs = set()
1214 else:
1215 raise Exception("Dependent job %s not found: " %
1216 (dependent_job,))
1217 new_parent_jobs = current_parent_jobs - all_parent_jobs
1218 jobs_to_iterate |= new_parent_jobs
1219 all_parent_jobs |= new_parent_jobs
1220 return all_parent_jobs
James E. Blairb97ed802015-12-21 15:55:35 -08001221
James E. Blair1e8dd892012-05-30 09:15:05 -07001222
James E. Blair4aea70c2012-07-26 14:23:24 -07001223class Build(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001224 """A Build is an instance of a single execution of a Job.
1225
1226 While a Job describes what to run, a Build describes an actual
1227 execution of that Job. Each build is associated with exactly one
1228 Job (related builds are grouped together in a BuildSet).
1229 """
Monty Taylora42a55b2016-07-29 07:53:33 -07001230
James E. Blair4aea70c2012-07-26 14:23:24 -07001231 def __init__(self, job, uuid):
1232 self.job = job
1233 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -07001234 self.url = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001235 self.result = None
James E. Blair196f61a2017-06-30 15:42:29 -07001236 self.result_data = {}
James E. Blair6f699732017-07-18 14:19:11 -07001237 self.error_detail = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001238 self.build_set = None
Paul Belanger174a8272017-03-14 13:20:10 -04001239 self.execute_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -08001240 self.start_time = None
1241 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -07001242 self.estimated_time = None
James E. Blair0aac4872013-08-23 14:02:38 -07001243 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -07001244 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -07001245 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +08001246 self.worker = Worker()
Timothy Chavezb2332082015-08-07 20:08:04 -05001247 self.node_labels = []
1248 self.node_name = None
James E. Blair8fd207b2017-11-30 13:44:38 -08001249 self.nodeset = None
James E. Blairee743612012-05-29 14:49:32 -07001250
1251 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +08001252 return ('<Build %s of %s on %s>' %
1253 (self.uuid, self.job.name, self.worker))
1254
James E. Blair3a098dd2017-10-04 14:37:29 -07001255 @property
1256 def pipeline(self):
1257 return self.build_set.item.pipeline
1258
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001259 def getSafeAttributes(self):
James E. Blair196f61a2017-06-30 15:42:29 -07001260 return Attributes(uuid=self.uuid,
1261 result=self.result,
James E. Blair6f699732017-07-18 14:19:11 -07001262 error_detail=self.error_detail,
James E. Blair196f61a2017-06-30 15:42:29 -07001263 result_data=self.result_data)
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001264
Joshua Heskethba8776a2014-01-12 14:35:40 +08001265
1266class Worker(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001267 """Information about the specific worker executing a Build."""
Joshua Heskethba8776a2014-01-12 14:35:40 +08001268 def __init__(self):
1269 self.name = "Unknown"
1270 self.hostname = None
Monty Taylor0dbe1592017-06-11 10:57:27 -05001271 self.log_port = None
Joshua Heskethba8776a2014-01-12 14:35:40 +08001272
1273 def updateFromData(self, data):
1274 """Update worker information if contained in the WORK_DATA response."""
1275 self.name = data.get('worker_name', self.name)
1276 self.hostname = data.get('worker_hostname', self.hostname)
Monty Taylor0dbe1592017-06-11 10:57:27 -05001277 self.log_port = data.get('worker_log_port', self.log_port)
Joshua Heskethba8776a2014-01-12 14:35:40 +08001278
1279 def __repr__(self):
1280 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -07001281
James E. Blair1e8dd892012-05-30 09:15:05 -07001282
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001283class RepoFiles(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001284 """RepoFiles holds config-file content for per-project job config.
1285
1286 When Zuul asks a merger to prepare a future multiple-repo state
1287 and collect Zuul configuration files so that we can dynamically
1288 load our configuration, this class provides cached access to that
1289 data for use by the Change which updated the config files and any
1290 changes that follow it in a ChangeQueue.
1291
1292 It is attached to a BuildSet since the content of Zuul
1293 configuration files can change with each new BuildSet.
1294 """
1295
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001296 def __init__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001297 self.connections = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001298
1299 def __repr__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001300 return '<RepoFiles %s>' % self.connections
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001301
1302 def setFiles(self, items):
James E. Blair2a535672017-04-27 12:03:15 -07001303 self.hostnames = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001304 for item in items:
James E. Blair2a535672017-04-27 12:03:15 -07001305 connection = self.connections.setdefault(
1306 item['connection'], {})
1307 project = connection.setdefault(item['project'], {})
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001308 branch = project.setdefault(item['branch'], {})
1309 branch.update(item['files'])
1310
James E. Blair2a535672017-04-27 12:03:15 -07001311 def getFile(self, connection_name, project_name, branch, fn):
1312 host = self.connections.get(connection_name, {})
1313 return host.get(project_name, {}).get(branch, {}).get(fn)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001314
1315
James E. Blair7e530ad2012-07-03 16:12:28 -07001316class BuildSet(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001317 """A collection of Builds for one specific potential future repository
1318 state.
Monty Taylora42a55b2016-07-29 07:53:33 -07001319
Paul Belanger174a8272017-03-14 13:20:10 -04001320 When Zuul executes Builds for a change, it creates a Build to
James E. Blaird4ade8c2017-02-19 15:25:46 -08001321 represent each execution of each job and a BuildSet to keep track
Paul Belanger174a8272017-03-14 13:20:10 -04001322 of all the Builds running for that Change. When Zuul re-executes
James E. Blaird4ade8c2017-02-19 15:25:46 -08001323 Builds for a Change with a different configuration, all of the
1324 running Builds in the BuildSet for that change are aborted, and a
1325 new BuildSet is created to hold the Builds for the Jobs being
1326 run with the new configuration.
1327
1328 A BuildSet also holds the UUID used to produce the Zuul Ref that
1329 builders check out.
1330
Monty Taylora42a55b2016-07-29 07:53:33 -07001331 """
James E. Blair4076e2b2014-01-28 12:42:20 -08001332 # Merge states:
1333 NEW = 1
1334 PENDING = 2
1335 COMPLETE = 3
1336
Antoine Musso9b229282014-08-18 23:45:43 +02001337 states_map = {
1338 1: 'NEW',
1339 2: 'PENDING',
1340 3: 'COMPLETE',
1341 }
1342
James E. Blairfee8d652013-06-07 08:57:52 -07001343 def __init__(self, item):
1344 self.item = item
James E. Blair7e530ad2012-07-03 16:12:28 -07001345 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -07001346 self.result = None
Jamie Lennox3f16de52017-05-09 14:24:11 +10001347 self.uuid = None
James E. Blair81515ad2012-10-01 18:29:08 -07001348 self.commit = None
James E. Blair9e5b8112017-10-19 08:12:24 -07001349 self.dependent_changes = None
James E. Blair1960d682017-04-28 15:44:14 -07001350 self.merger_items = None
James E. Blair973721f2012-08-15 10:19:43 -07001351 self.unable_to_merge = False
James E. Blaire53250c2017-03-01 14:34:36 -08001352 self.config_error = None # None or an error message string.
James E. Blair972e3c72013-08-29 12:04:55 -07001353 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -08001354 self.merge_state = self.NEW
James E. Blair0eaad552016-09-02 12:09:54 -07001355 self.nodesets = {} # job -> nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001356 self.node_requests = {} # job -> reqs
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001357 self.files = RepoFiles()
James E. Blair1960d682017-04-28 15:44:14 -07001358 self.repo_state = {}
Paul Belanger71d98172016-11-08 10:56:31 -05001359 self.tries = {}
James E. Blair7e530ad2012-07-03 16:12:28 -07001360
Jamie Lennox3f16de52017-05-09 14:24:11 +10001361 @property
1362 def ref(self):
1363 # NOTE(jamielennox): The concept of buildset ref is to be removed and a
1364 # buildset UUID identifier available instead. Currently the ref is
1365 # checked to see if the BuildSet has been configured.
1366 return 'Z' + self.uuid if self.uuid else None
1367
Antoine Musso9b229282014-08-18 23:45:43 +02001368 def __repr__(self):
1369 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
1370 self.item,
1371 len(self.builds),
1372 self.getStateName(self.merge_state))
1373
James E. Blair4886cc12012-07-18 15:39:41 -07001374 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -07001375 # The change isn't enqueued until after it's created
1376 # so we don't know what the other changes ahead will be
1377 # until jobs start.
James E. Blair9e5b8112017-10-19 08:12:24 -07001378 if not self.uuid:
1379 self.uuid = uuid4().hex
1380 if self.dependent_changes is None:
1381 items = [self.item]
James E. Blairfee8d652013-06-07 08:57:52 -07001382 next_item = self.item.item_ahead
1383 while next_item:
James E. Blair1960d682017-04-28 15:44:14 -07001384 items.append(next_item)
James E. Blairfee8d652013-06-07 08:57:52 -07001385 next_item = next_item.item_ahead
James E. Blair1960d682017-04-28 15:44:14 -07001386 items.reverse()
James E. Blair9e5b8112017-10-19 08:12:24 -07001387 self.dependent_changes = [i.change.toDict() for i in items]
James E. Blair1960d682017-04-28 15:44:14 -07001388 self.merger_items = [i.makeMergerItem() for i in items]
James E. Blair4886cc12012-07-18 15:39:41 -07001389
Antoine Musso9b229282014-08-18 23:45:43 +02001390 def getStateName(self, state_num):
1391 return self.states_map.get(
1392 state_num, 'UNKNOWN (%s)' % state_num)
1393
James E. Blair4886cc12012-07-18 15:39:41 -07001394 def addBuild(self, build):
1395 self.builds[build.job.name] = build
Paul Belanger71d98172016-11-08 10:56:31 -05001396 if build.job.name not in self.tries:
James E. Blair5aed1112016-12-15 09:18:05 -08001397 self.tries[build.job.name] = 1
James E. Blair4886cc12012-07-18 15:39:41 -07001398 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -07001399
James E. Blair4a28a882013-08-23 15:17:33 -07001400 def removeBuild(self, build):
Paul Belanger71d98172016-11-08 10:56:31 -05001401 self.tries[build.job.name] += 1
James E. Blair4a28a882013-08-23 15:17:33 -07001402 del self.builds[build.job.name]
1403
James E. Blair7e530ad2012-07-03 16:12:28 -07001404 def getBuild(self, job_name):
1405 return self.builds.get(job_name)
1406
James E. Blair11700c32012-07-05 17:50:05 -07001407 def getBuilds(self):
Clint Byrum1d0c7d12017-05-10 19:40:53 -07001408 keys = list(self.builds.keys())
James E. Blair11700c32012-07-05 17:50:05 -07001409 keys.sort()
1410 return [self.builds.get(x) for x in keys]
1411
James E. Blair0eaad552016-09-02 12:09:54 -07001412 def getJobNodeSet(self, job_name):
1413 # Return None if not provisioned; empty NodeSet if no nodes
1414 # required
1415 return self.nodesets.get(job_name)
James E. Blair8d692392016-04-08 17:47:58 -07001416
James E. Blaire18d4602017-01-05 11:17:28 -08001417 def removeJobNodeSet(self, job_name):
1418 if job_name not in self.nodesets:
1419 raise Exception("No job set for %s" % (job_name))
1420 del self.nodesets[job_name]
1421
James E. Blair8d692392016-04-08 17:47:58 -07001422 def setJobNodeRequest(self, job_name, req):
1423 if job_name in self.node_requests:
1424 raise Exception("Prior node request for %s" % (job_name))
1425 self.node_requests[job_name] = req
1426
1427 def getJobNodeRequest(self, job_name):
1428 return self.node_requests.get(job_name)
1429
James E. Blair0eaad552016-09-02 12:09:54 -07001430 def jobNodeRequestComplete(self, job_name, req, nodeset):
1431 if job_name in self.nodesets:
James E. Blair8d692392016-04-08 17:47:58 -07001432 raise Exception("Prior node request for %s" % (job_name))
James E. Blair0eaad552016-09-02 12:09:54 -07001433 self.nodesets[job_name] = nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001434 del self.node_requests[job_name]
1435
Paul Belanger71d98172016-11-08 10:56:31 -05001436 def getTries(self, job_name):
Clint Byrum804073b2017-05-10 17:47:45 -04001437 return self.tries.get(job_name, 0)
Paul Belanger71d98172016-11-08 10:56:31 -05001438
James E. Blair0ffa0102017-03-30 13:11:33 -07001439 def getMergeMode(self):
James E. Blair1960d682017-04-28 15:44:14 -07001440 # We may be called before this build set has a shadow layout
1441 # (ie, we are called to perform the merge to create that
1442 # layout). It's possible that the change we are merging will
1443 # update the merge-mode for the project, but there's not much
1444 # we can do about that here. Instead, do the best we can by
1445 # using the nearest shadow layout to determine the merge mode,
1446 # or if that fails, the current live layout, or if that fails,
1447 # use the default: merge-resolve.
1448 item = self.item
1449 layout = None
1450 while item:
James E. Blair29a24fd2017-10-02 15:04:56 -07001451 layout = item.layout
James E. Blair1960d682017-04-28 15:44:14 -07001452 if layout:
1453 break
1454 item = item.item_ahead
1455 if not layout:
1456 layout = self.item.pipeline.layout
1457 if layout:
James E. Blair0ffa0102017-03-30 13:11:33 -07001458 project = self.item.change.project
James E. Blair1960d682017-04-28 15:44:14 -07001459 project_config = layout.project_configs.get(
James E. Blair0ffa0102017-03-30 13:11:33 -07001460 project.canonical_name)
1461 if project_config:
1462 return project_config.merge_mode
1463 return MERGER_MERGE_RESOLVE
Adam Gandelman8bd57102016-12-02 12:58:42 -08001464
Jamie Lennox3f16de52017-05-09 14:24:11 +10001465 def getSafeAttributes(self):
1466 return Attributes(uuid=self.uuid)
1467
James E. Blair7e530ad2012-07-03 16:12:28 -07001468
James E. Blairfee8d652013-06-07 08:57:52 -07001469class QueueItem(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001470 """Represents the position of a Change in a ChangeQueue.
1471
1472 All Changes are enqueued into ChangeQueue in a QueueItem. The QueueItem
1473 holds the current `BuildSet` as well as all previous `BuildSets` that were
1474 produced for this `QueueItem`.
1475 """
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001476 log = logging.getLogger("zuul.QueueItem")
James E. Blair32663402012-06-01 10:04:18 -07001477
James E. Blairbfb8e042014-12-30 17:01:44 -08001478 def __init__(self, queue, change):
1479 self.pipeline = queue.pipeline
1480 self.queue = queue
Monty Taylor55d0f562017-05-17 14:30:21 -05001481 self.change = change # a ref
James E. Blaircaec0c52012-08-22 14:52:22 -07001482 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -07001483 self.current_build_set = BuildSet(self)
James E. Blairfee8d652013-06-07 08:57:52 -07001484 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -07001485 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -08001486 self.enqueue_time = None
1487 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -07001488 self.reported = False
Tobias Henkel9842bd72017-05-16 13:40:03 +02001489 self.reported_start = False
1490 self.quiet = False
James E. Blairbfb8e042014-12-30 17:01:44 -08001491 self.active = False # Whether an item is within an active window
1492 self.live = True # Whether an item is intended to be processed at all
James E. Blair29a24fd2017-10-02 15:04:56 -07001493 self.layout = None
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001494 self.job_graph = None
James E. Blaire5a847f2012-07-10 15:29:14 -07001495
James E. Blair972e3c72013-08-29 12:04:55 -07001496 def __repr__(self):
1497 if self.pipeline:
1498 pipeline = self.pipeline.name
1499 else:
1500 pipeline = None
1501 return '<QueueItem 0x%x for %s in %s>' % (
1502 id(self), self.change, pipeline)
1503
James E. Blairee743612012-05-29 14:49:32 -07001504 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -07001505 self.current_build_set = BuildSet(self)
James E. Blair29a24fd2017-10-02 15:04:56 -07001506 self.layout = None
James E. Blairc9455002017-09-06 09:22:19 -07001507 self.job_graph = None
James E. Blairee743612012-05-29 14:49:32 -07001508
1509 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -07001510 self.current_build_set.addBuild(build)
James E. Blairee743612012-05-29 14:49:32 -07001511
James E. Blair4a28a882013-08-23 15:17:33 -07001512 def removeBuild(self, build):
1513 self.current_build_set.removeBuild(build)
1514
James E. Blairfee8d652013-06-07 08:57:52 -07001515 def setReportedResult(self, result):
1516 self.current_build_set.result = result
1517
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001518 def freezeJobGraph(self):
James E. Blair83005782015-12-11 14:46:03 -08001519 """Find or create actual matching jobs for this item's change and
1520 store the resulting job tree."""
James E. Blair29a24fd2017-10-02 15:04:56 -07001521 job_graph = self.layout.createJobGraph(self)
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001522 for job in job_graph.getJobs():
1523 # Ensure that each jobs's dependencies are fully
1524 # accessible. This will raise an exception if not.
1525 job_graph.getParentJobsRecursively(job.name)
1526 self.job_graph = job_graph
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001527
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001528 def hasJobGraph(self):
1529 """Returns True if the item has a job graph."""
1530 return self.job_graph is not None
James E. Blair83005782015-12-11 14:46:03 -08001531
1532 def getJobs(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001533 if not self.live or not self.job_graph:
James E. Blair83005782015-12-11 14:46:03 -08001534 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001535 return self.job_graph.getJobs()
1536
1537 def getJob(self, name):
1538 if not self.job_graph:
1539 return None
1540 return self.job_graph.jobs.get(name)
James E. Blair83005782015-12-11 14:46:03 -08001541
James E. Blairdbfd3282016-07-21 10:46:19 -07001542 def haveAllJobsStarted(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001543 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001544 return False
1545 for job in self.getJobs():
1546 build = self.current_build_set.getBuild(job.name)
1547 if not build or not build.start_time:
1548 return False
1549 return True
1550
1551 def areAllJobsComplete(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001552 if (self.current_build_set.config_error or
1553 self.current_build_set.unable_to_merge):
1554 return True
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001555 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001556 return False
1557 for job in self.getJobs():
1558 build = self.current_build_set.getBuild(job.name)
1559 if not build or not build.result:
1560 return False
1561 return True
1562
1563 def didAllJobsSucceed(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001564 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001565 return False
1566 for job in self.getJobs():
1567 if not job.voting:
1568 continue
1569 build = self.current_build_set.getBuild(job.name)
1570 if not build:
1571 return False
1572 if build.result != 'SUCCESS':
1573 return False
1574 return True
1575
1576 def didAnyJobFail(self):
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.voting:
1581 continue
1582 build = self.current_build_set.getBuild(job.name)
1583 if build and build.result and (build.result != 'SUCCESS'):
1584 return True
1585 return False
1586
1587 def didMergerFail(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001588 return self.current_build_set.unable_to_merge
1589
1590 def getConfigError(self):
1591 return self.current_build_set.config_error
James E. Blairdbfd3282016-07-21 10:46:19 -07001592
James E. Blair0d3e83b2017-06-05 13:51:57 -07001593 def wasDequeuedNeedingChange(self):
1594 return self.dequeued_needing_change
1595
James E. Blair8c2d5812017-10-06 09:29:21 -07001596 def includesConfigUpdates(self):
1597 includes_trusted = False
1598 includes_untrusted = False
1599 tenant = self.pipeline.layout.tenant
1600 item = self
1601 while item:
1602 if item.change.updatesConfig():
1603 (trusted, project) = tenant.getProject(
1604 item.change.project.canonical_name)
1605 if trusted:
1606 includes_trusted = True
1607 else:
1608 includes_untrusted = True
1609 if includes_trusted and includes_untrusted:
1610 # We're done early
1611 return (includes_trusted, includes_untrusted)
1612 item = item.item_ahead
1613 return (includes_trusted, includes_untrusted)
1614
James E. Blairdbfd3282016-07-21 10:46:19 -07001615 def isHoldingFollowingChanges(self):
1616 if not self.live:
1617 return False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001618 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001619 return False
1620 for job in self.getJobs():
1621 if not job.hold_following_changes:
1622 continue
1623 build = self.current_build_set.getBuild(job.name)
1624 if not build:
1625 return True
1626 if build.result != 'SUCCESS':
1627 return True
1628
1629 if not self.item_ahead:
1630 return False
1631 return self.item_ahead.isHoldingFollowingChanges()
1632
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001633 def findJobsToRun(self, semaphore_handler):
James E. Blairdbfd3282016-07-21 10:46:19 -07001634 torun = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001635 if not self.live:
1636 return []
1637 if not self.job_graph:
1638 return []
James E. Blair791b5392016-08-03 11:25:56 -07001639 if self.item_ahead:
1640 # Only run jobs if any 'hold' jobs on the change ahead
1641 # have completed successfully.
1642 if self.item_ahead.isHoldingFollowingChanges():
1643 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001644
1645 successful_job_names = set()
1646 jobs_not_started = set()
1647 for job in self.job_graph.getJobs():
1648 build = self.current_build_set.getBuild(job.name)
1649 if build:
1650 if build.result == 'SUCCESS':
1651 successful_job_names.add(job.name)
1652 else:
1653 jobs_not_started.add(job)
1654
James E. Blair698703c2017-09-15 20:58:30 -06001655 # Attempt to run jobs in the order they appear in
1656 # configuration.
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001657 for job in self.job_graph.getJobs():
1658 if job not in jobs_not_started:
1659 continue
1660 all_parent_jobs_successful = True
Tobias Henkela96c9b32017-10-22 12:38:06 +02001661 parent_builds_with_data = {}
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001662 for parent_job in self.job_graph.getParentJobsRecursively(
1663 job.name):
1664 if parent_job.name not in successful_job_names:
1665 all_parent_jobs_successful = False
1666 break
James E. Blair698703c2017-09-15 20:58:30 -06001667 parent_build = self.current_build_set.getBuild(parent_job.name)
1668 if parent_build.result_data:
Tobias Henkela96c9b32017-10-22 12:38:06 +02001669 parent_builds_with_data[parent_job.name] = parent_build
1670
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001671 if all_parent_jobs_successful:
Tobias Henkela96c9b32017-10-22 12:38:06 +02001672 # Iterate in reverse order over all jobs of the graph (which is
1673 # in sorted config order) and apply parent data of the jobs we
1674 # already found.
1675 if len(parent_builds_with_data) > 0:
1676 for parent_job in reversed(self.job_graph.getJobs()):
1677 parent_build = parent_builds_with_data.get(
1678 parent_job.name)
1679 if parent_build:
1680 job.updateParentData(parent_build.result_data)
1681
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001682 nodeset = self.current_build_set.getJobNodeSet(job.name)
1683 if nodeset is None:
1684 # The nodes for this job are not ready, skip
1685 # it for now.
James E. Blairdbfd3282016-07-21 10:46:19 -07001686 continue
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001687 if semaphore_handler.acquire(self, job):
1688 # If this job needs a semaphore, either acquire it or
1689 # make sure that we have it before running the job.
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001690 torun.append(job)
James E. Blairdbfd3282016-07-21 10:46:19 -07001691 return torun
1692
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001693 def findJobsToRequest(self):
James E. Blair6ab79e02017-01-06 10:10:17 -08001694 build_set = self.current_build_set
James E. Blairdbfd3282016-07-21 10:46:19 -07001695 toreq = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001696 if not self.live:
1697 return []
1698 if not self.job_graph:
1699 return []
James E. Blair6ab79e02017-01-06 10:10:17 -08001700 if self.item_ahead:
1701 if self.item_ahead.isHoldingFollowingChanges():
1702 return []
James E. Blairdbfd3282016-07-21 10:46:19 -07001703
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001704 successful_job_names = set()
1705 jobs_not_requested = set()
1706 for job in self.job_graph.getJobs():
1707 build = build_set.getBuild(job.name)
1708 if build and build.result == 'SUCCESS':
1709 successful_job_names.add(job.name)
1710 else:
1711 nodeset = build_set.getJobNodeSet(job.name)
1712 if nodeset is None:
1713 req = build_set.getJobNodeRequest(job.name)
1714 if req is None:
1715 jobs_not_requested.add(job)
1716
1717 # Attempt to request nodes for jobs in the order jobs appear
1718 # in configuration.
1719 for job in self.job_graph.getJobs():
1720 if job not in jobs_not_requested:
1721 continue
1722 all_parent_jobs_successful = True
1723 for parent_job in self.job_graph.getParentJobsRecursively(
1724 job.name):
1725 if parent_job.name not in successful_job_names:
1726 all_parent_jobs_successful = False
1727 break
1728 if all_parent_jobs_successful:
1729 toreq.append(job)
1730 return toreq
James E. Blairdbfd3282016-07-21 10:46:19 -07001731
1732 def setResult(self, build):
1733 if build.retry:
1734 self.removeBuild(build)
1735 elif build.result != 'SUCCESS':
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001736 for job in self.job_graph.getDependentJobsRecursively(
1737 build.job.name):
James E. Blairdbfd3282016-07-21 10:46:19 -07001738 fakebuild = Build(job, None)
1739 fakebuild.result = 'SKIPPED'
1740 self.addBuild(fakebuild)
1741
James E. Blair6ab79e02017-01-06 10:10:17 -08001742 def setNodeRequestFailure(self, job):
1743 fakebuild = Build(job, None)
1744 self.addBuild(fakebuild)
1745 fakebuild.result = 'NODE_FAILURE'
1746 self.setResult(fakebuild)
1747
James E. Blairdbfd3282016-07-21 10:46:19 -07001748 def setDequeuedNeedingChange(self):
1749 self.dequeued_needing_change = True
1750 self._setAllJobsSkipped()
1751
1752 def setUnableToMerge(self):
1753 self.current_build_set.unable_to_merge = True
1754 self._setAllJobsSkipped()
1755
James E. Blaire53250c2017-03-01 14:34:36 -08001756 def setConfigError(self, error):
1757 self.current_build_set.config_error = error
1758 self._setAllJobsSkipped()
1759
James E. Blairdbfd3282016-07-21 10:46:19 -07001760 def _setAllJobsSkipped(self):
1761 for job in self.getJobs():
1762 fakebuild = Build(job, None)
1763 fakebuild.result = 'SKIPPED'
1764 self.addBuild(fakebuild)
1765
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001766 def formatUrlPattern(self, url_pattern, job=None, build=None):
1767 url = None
1768 # Produce safe versions of objects which may be useful in
1769 # result formatting, but don't allow users to crawl through
1770 # the entire data structure where they might be able to access
1771 # secrets, etc.
1772 safe_change = self.change.getSafeAttributes()
1773 safe_pipeline = self.pipeline.getSafeAttributes()
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001774 safe_tenant = self.pipeline.layout.tenant.getSafeAttributes()
Jamie Lennox3f16de52017-05-09 14:24:11 +10001775 safe_buildset = self.current_build_set.getSafeAttributes()
K Jonathan Harkera7f390c2017-06-02 12:31:22 -07001776 safe_job = job.getSafeAttributes() if job else {}
1777 safe_build = build.getSafeAttributes() if build else {}
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001778 try:
1779 url = url_pattern.format(change=safe_change,
1780 pipeline=safe_pipeline,
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001781 tenant=safe_tenant,
Jamie Lennox3f16de52017-05-09 14:24:11 +10001782 buildset=safe_buildset,
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001783 job=safe_job,
1784 build=safe_build)
1785 except KeyError as e:
1786 self.log.error("Error while formatting url for job %s: unknown "
1787 "key %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001788 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001789 except AttributeError as e:
1790 self.log.error("Error while formatting url for job %s: unknown "
1791 "attribute %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001792 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001793 except Exception:
1794 self.log.exception("Error while formatting url for job %s with "
1795 "pattern %s:" % (job, url_pattern))
1796
1797 return url
1798
James E. Blair800e7ff2017-03-17 16:06:52 -07001799 def formatJobResult(self, job):
James E. Blairb7273ef2016-04-19 08:58:51 -07001800 build = self.current_build_set.getBuild(job.name)
1801 result = build.result
James E. Blair800e7ff2017-03-17 16:06:52 -07001802 pattern = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001803 if result == 'SUCCESS':
1804 if job.success_message:
1805 result = job.success_message
James E. Blaira5dba232016-08-08 15:53:24 -07001806 if job.success_url:
1807 pattern = job.success_url
Tobias Henkel077f2f32017-05-30 20:16:46 +02001808 else:
James E. Blairb7273ef2016-04-19 08:58:51 -07001809 if job.failure_message:
1810 result = job.failure_message
James E. Blaira5dba232016-08-08 15:53:24 -07001811 if job.failure_url:
1812 pattern = job.failure_url
James E. Blair88e79c02017-07-07 13:36:54 -07001813 url = None # The final URL
1814 default_url = build.result_data.get('zuul', {}).get('log_url')
James E. Blairb7273ef2016-04-19 08:58:51 -07001815 if pattern:
James E. Blair88e79c02017-07-07 13:36:54 -07001816 job_url = self.formatUrlPattern(pattern, job, build)
1817 else:
1818 job_url = None
1819 try:
1820 if job_url:
1821 u = urllib.parse.urlparse(job_url)
1822 if u.scheme:
1823 # The job success or failure url is absolute, so it's
1824 # our final url.
1825 url = job_url
1826 else:
1827 # We have a relative job url. Combine it with our
1828 # default url.
1829 if default_url:
1830 url = urllib.parse.urljoin(default_url, job_url)
1831 except Exception:
1832 self.log.exception("Error while parsing url for job %s:"
1833 % (job,))
James E. Blairb7273ef2016-04-19 08:58:51 -07001834 if not url:
James E. Blair88e79c02017-07-07 13:36:54 -07001835 url = default_url or build.url or job.name
James E. Blairb7273ef2016-04-19 08:58:51 -07001836 return (result, url)
1837
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001838 def formatJSON(self, websocket_url=None):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001839 ret = {}
1840 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -08001841 ret['live'] = self.live
Monty Taylor55d0f562017-05-17 14:30:21 -05001842 if hasattr(self.change, 'url') and self.change.url is not None:
1843 ret['url'] = self.change.url
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001844 else:
1845 ret['url'] = None
Monty Taylor55d0f562017-05-17 14:30:21 -05001846 ret['id'] = self.change._id()
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001847 if self.item_ahead:
1848 ret['item_ahead'] = self.item_ahead.change._id()
1849 else:
1850 ret['item_ahead'] = None
1851 ret['items_behind'] = [i.change._id() for i in self.items_behind]
1852 ret['failing_reasons'] = self.current_build_set.failing_reasons
1853 ret['zuul_ref'] = self.current_build_set.ref
Monty Taylor55d0f562017-05-17 14:30:21 -05001854 if self.change.project:
1855 ret['project'] = self.change.project.name
Ramy Asselin07cc33c2015-06-12 14:06:34 -07001856 else:
1857 # For cross-project dependencies with the depends-on
1858 # project not known to zuul, the project is None
1859 # Set it to a static value
1860 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001861 ret['enqueue_time'] = int(self.enqueue_time * 1000)
1862 ret['jobs'] = []
Monty Taylor55d0f562017-05-17 14:30:21 -05001863 if hasattr(self.change, 'owner'):
1864 ret['owner'] = self.change.owner
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001865 else:
1866 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001867 max_remaining = 0
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001868 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001869 now = time.time()
1870 build = self.current_build_set.getBuild(job.name)
1871 elapsed = None
1872 remaining = None
1873 result = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001874 build_url = None
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001875 finger_url = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001876 report_url = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001877 worker = None
1878 if build:
1879 result = build.result
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001880 finger_url = build.url
1881 # TODO(tobiash): add support for custom web root
1882 urlformat = 'static/stream.html?' \
1883 'uuid={build.uuid}&' \
1884 'logfile=console.log'
1885 if websocket_url:
1886 urlformat += '&websocket_url={websocket_url}'
1887 build_url = urlformat.format(
1888 build=build, websocket_url=websocket_url)
James E. Blair800e7ff2017-03-17 16:06:52 -07001889 (unused, report_url) = self.formatJobResult(job)
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001890 if build.start_time:
1891 if build.end_time:
1892 elapsed = int((build.end_time -
1893 build.start_time) * 1000)
1894 remaining = 0
1895 else:
1896 elapsed = int((now - build.start_time) * 1000)
1897 if build.estimated_time:
1898 remaining = max(
1899 int(build.estimated_time * 1000) - elapsed,
1900 0)
1901 worker = {
1902 'name': build.worker.name,
1903 'hostname': build.worker.hostname,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001904 }
1905 if remaining and remaining > max_remaining:
1906 max_remaining = remaining
1907
1908 ret['jobs'].append({
1909 'name': job.name,
Tobias Henkel65639f82017-07-10 10:25:42 +02001910 'dependencies': list(job.dependencies),
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001911 'elapsed_time': elapsed,
1912 'remaining_time': remaining,
James E. Blairb7273ef2016-04-19 08:58:51 -07001913 'url': build_url,
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001914 'finger_url': finger_url,
James E. Blairb7273ef2016-04-19 08:58:51 -07001915 'report_url': report_url,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001916 'result': result,
1917 'voting': job.voting,
1918 'uuid': build.uuid if build else None,
Paul Belanger174a8272017-03-14 13:20:10 -04001919 'execute_time': build.execute_time if build else None,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001920 'start_time': build.start_time if build else None,
1921 'end_time': build.end_time if build else None,
1922 'estimated_time': build.estimated_time if build else None,
1923 'pipeline': build.pipeline.name if build else None,
1924 'canceled': build.canceled if build else None,
1925 'retry': build.retry if build else None,
Timothy Chavezb2332082015-08-07 20:08:04 -05001926 'node_labels': build.node_labels if build else [],
1927 'node_name': build.node_name if build else None,
1928 'worker': worker,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001929 })
1930
James E. Blairdbfd3282016-07-21 10:46:19 -07001931 if self.haveAllJobsStarted():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001932 ret['remaining_time'] = max_remaining
1933 else:
1934 ret['remaining_time'] = None
1935 return ret
1936
1937 def formatStatus(self, indent=0, html=False):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001938 indent_str = ' ' * indent
1939 ret = ''
Monty Taylor55d0f562017-05-17 14:30:21 -05001940 if html and getattr(self.change, 'url', None) is not None:
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001941 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
1942 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001943 self.change.project.name,
1944 self.change.url,
1945 self.change._id())
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001946 else:
1947 ret += '%sProject %s change %s based on %s\n' % (
1948 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001949 self.change.project.name,
1950 self.change._id(),
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001951 self.item_ahead)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001952 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001953 build = self.current_build_set.getBuild(job.name)
1954 if build:
1955 result = build.result
1956 else:
1957 result = None
1958 job_name = job.name
1959 if not job.voting:
1960 voting = ' (non-voting)'
1961 else:
1962 voting = ''
1963 if html:
1964 if build:
1965 url = build.url
1966 else:
1967 url = None
1968 if url is not None:
1969 job_name = '<a href="%s">%s</a>' % (url, job_name)
1970 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
1971 ret += '\n'
1972 return ret
1973
James E. Blaira04b0792017-04-27 09:59:06 -07001974 def makeMergerItem(self):
1975 # Create a dictionary with all info about the item needed by
1976 # the merger.
1977 number = None
1978 patchset = None
1979 oldrev = None
1980 newrev = None
James E. Blair21037782017-07-19 11:56:55 -07001981 branch = None
James E. Blaira04b0792017-04-27 09:59:06 -07001982 if hasattr(self.change, 'number'):
1983 number = self.change.number
1984 patchset = self.change.patchset
James E. Blair21037782017-07-19 11:56:55 -07001985 if hasattr(self.change, 'newrev'):
James E. Blaira04b0792017-04-27 09:59:06 -07001986 oldrev = self.change.oldrev
1987 newrev = self.change.newrev
James E. Blair21037782017-07-19 11:56:55 -07001988 if hasattr(self.change, 'branch'):
1989 branch = self.change.branch
1990
James E. Blaira04b0792017-04-27 09:59:06 -07001991 source = self.change.project.source
1992 connection_name = source.connection.connection_name
James E. Blair2a535672017-04-27 12:03:15 -07001993 project = self.change.project
James E. Blaira04b0792017-04-27 09:59:06 -07001994
James E. Blair2a535672017-04-27 12:03:15 -07001995 return dict(project=project.name,
1996 connection=connection_name,
James E. Blaira04b0792017-04-27 09:59:06 -07001997 merge_mode=self.current_build_set.getMergeMode(),
James E. Blair247cab72017-07-20 16:52:36 -07001998 ref=self.change.ref,
James E. Blaira04b0792017-04-27 09:59:06 -07001999 branch=branch,
James E. Blair247cab72017-07-20 16:52:36 -07002000 buildset_uuid=self.current_build_set.uuid,
James E. Blaira04b0792017-04-27 09:59:06 -07002001 number=number,
2002 patchset=patchset,
2003 oldrev=oldrev,
2004 newrev=newrev,
2005 )
2006
James E. Blairfee8d652013-06-07 08:57:52 -07002007
Clint Byrumf8cc9902017-03-22 22:38:25 -07002008class Ref(object):
2009 """An existing state of a Project."""
James E. Blairfee8d652013-06-07 08:57:52 -07002010
2011 def __init__(self, project):
2012 self.project = project
Clint Byrumf8cc9902017-03-22 22:38:25 -07002013 self.ref = None
2014 self.oldrev = None
2015 self.newrev = None
Jesse Keating71a47ff2017-06-06 11:36:43 -07002016 self.files = []
2017
Clint Byrumf8cc9902017-03-22 22:38:25 -07002018 def _id(self):
2019 return self.newrev
2020
2021 def __repr__(self):
2022 rep = None
2023 if self.newrev == '0000000000000000000000000000000000000000':
James E. Blair21037782017-07-19 11:56:55 -07002024 rep = '<%s 0x%x deletes %s from %s' % (
2025 type(self).__name__,
2026 id(self), self.ref, self.oldrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07002027 elif self.oldrev == '0000000000000000000000000000000000000000':
James E. Blair21037782017-07-19 11:56:55 -07002028 rep = '<%s 0x%x creates %s on %s>' % (
2029 type(self).__name__,
2030 id(self), self.ref, self.newrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07002031 else:
2032 # Catch all
James E. Blair21037782017-07-19 11:56:55 -07002033 rep = '<%s 0x%x %s updated %s..%s>' % (
2034 type(self).__name__,
2035 id(self), self.ref, self.oldrev, self.newrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07002036 return rep
2037
James E. Blairfee8d652013-06-07 08:57:52 -07002038 def equals(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07002039 if (self.project == other.project
2040 and self.ref == other.ref
2041 and self.newrev == other.newrev):
2042 return True
2043 return False
James E. Blairfee8d652013-06-07 08:57:52 -07002044
2045 def isUpdateOf(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07002046 return False
James E. Blairfee8d652013-06-07 08:57:52 -07002047
2048 def filterJobs(self, jobs):
2049 return filter(lambda job: job.changeMatches(self), jobs)
2050
2051 def getRelatedChanges(self):
2052 return set()
2053
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002054 def updatesConfig(self):
Tristan Cacqueray829e6172017-06-13 06:49:36 +00002055 if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files or \
2056 [True for fn in self.files if fn.startswith("zuul.d/") or
2057 fn.startswith(".zuul.d/")]:
Jesse Keating71a47ff2017-06-06 11:36:43 -07002058 return True
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002059 return False
2060
Joshua Hesketh58419cb2017-02-24 13:09:22 -05002061 def getSafeAttributes(self):
2062 return Attributes(project=self.project,
2063 ref=self.ref,
2064 oldrev=self.oldrev,
2065 newrev=self.newrev)
2066
James E. Blair9e5b8112017-10-19 08:12:24 -07002067 def toDict(self):
2068 # Render to a dict to use in passing json to the executor
2069 d = dict()
2070 d['project'] = dict(
2071 name=self.project.name,
2072 short_name=self.project.name.split('/')[-1],
2073 canonical_hostname=self.project.canonical_hostname,
2074 canonical_name=self.project.canonical_name,
2075 src_dir=os.path.join('src', self.project.canonical_name),
2076 )
2077 return d
2078
James E. Blair1e8dd892012-05-30 09:15:05 -07002079
James E. Blair21037782017-07-19 11:56:55 -07002080class Branch(Ref):
2081 """An existing branch state for a Project."""
2082 def __init__(self, project):
2083 super(Branch, self).__init__(project)
2084 self.branch = None
2085
James E. Blair9e5b8112017-10-19 08:12:24 -07002086 def toDict(self):
2087 # Render to a dict to use in passing json to the executor
2088 d = super(Branch, self).toDict()
2089 d['branch'] = self.branch
2090 return d
2091
James E. Blair21037782017-07-19 11:56:55 -07002092
2093class Tag(Ref):
2094 """An existing tag state for a Project."""
2095 def __init__(self, project):
2096 super(Tag, self).__init__(project)
2097 self.tag = None
2098
2099
2100class Change(Branch):
Monty Taylora42a55b2016-07-29 07:53:33 -07002101 """A proposed new state for a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07002102 def __init__(self, project):
2103 super(Change, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -07002104 self.number = None
2105 self.url = None
2106 self.patchset = None
James E. Blair4aea70c2012-07-26 14:23:24 -07002107
James E. Blair6965a4b2014-12-16 17:19:04 -08002108 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -07002109 self.needed_by_changes = []
2110 self.is_current_patchset = True
2111 self.can_merge = False
2112 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -07002113 self.failed_to_merge = False
James E. Blair11041d22014-05-02 14:49:53 -07002114 self.open = None
2115 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05002116 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -07002117
Jan Hruban3b415922016-02-03 13:10:22 +01002118 self.source_event = None
2119
James E. Blair4aea70c2012-07-26 14:23:24 -07002120 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -07002121 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -07002122
2123 def __repr__(self):
2124 return '<Change 0x%x %s>' % (id(self), self._id())
2125
2126 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +08002127 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -07002128 return True
2129 return False
2130
James E. Blair2fa50962013-01-30 21:50:41 -08002131 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -08002132 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -07002133 (hasattr(other, 'patchset') and
2134 self.patchset is not None and
2135 other.patchset is not None and
2136 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -08002137 return True
2138 return False
2139
James E. Blairfee8d652013-06-07 08:57:52 -07002140 def getRelatedChanges(self):
2141 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -08002142 for c in self.needs_changes:
2143 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -07002144 for c in self.needed_by_changes:
2145 related.add(c)
2146 related.update(c.getRelatedChanges())
2147 return related
James E. Blair4aea70c2012-07-26 14:23:24 -07002148
Joshua Hesketh58419cb2017-02-24 13:09:22 -05002149 def getSafeAttributes(self):
2150 return Attributes(project=self.project,
2151 number=self.number,
2152 patchset=self.patchset)
2153
James E. Blair9e5b8112017-10-19 08:12:24 -07002154 def toDict(self):
2155 # Render to a dict to use in passing json to the executor
2156 d = super(Change, self).toDict()
2157 d['change'] = str(self.number)
2158 d['change_url'] = self.url
2159 d['patchset'] = str(self.patchset)
2160 return d
2161
James E. Blair4aea70c2012-07-26 14:23:24 -07002162
James E. Blairee743612012-05-29 14:49:32 -07002163class TriggerEvent(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002164 """Incoming event from an external system."""
James E. Blairee743612012-05-29 14:49:32 -07002165 def __init__(self):
James E. Blairaad3ae22017-05-18 14:11:29 -07002166 # TODO(jeblair): further reduce this list
James E. Blairee743612012-05-29 14:49:32 -07002167 self.data = None
James E. Blair32663402012-06-01 10:04:18 -07002168 # common
James E. Blairee743612012-05-29 14:49:32 -07002169 self.type = None
Jesse Keating71a47ff2017-06-06 11:36:43 -07002170 self.branch_updated = False
James E. Blair72facdc2017-08-17 10:29:12 -07002171 self.branch_created = False
2172 self.branch_deleted = False
James E. Blair247cab72017-07-20 16:52:36 -07002173 self.ref = None
Paul Belangerbaca3132016-11-04 12:49:54 -04002174 # For management events (eg: enqueue / promote)
2175 self.tenant_name = None
James E. Blair6f284b42017-03-31 14:14:41 -07002176 self.project_hostname = None
James E. Blairee743612012-05-29 14:49:32 -07002177 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -07002178 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +01002179 # Representation of the user account that performed the event.
2180 self.account = None
James E. Blair32663402012-06-01 10:04:18 -07002181 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -07002182 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -07002183 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -07002184 self.patch_number = None
James E. Blairee743612012-05-29 14:49:32 -07002185 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07002186 self.comment = None
Jesse Keating5c05a9f2017-01-12 14:44:58 -08002187 self.state = None
James E. Blair32663402012-06-01 10:04:18 -07002188 # ref-updated
James E. Blair32663402012-06-01 10:04:18 -07002189 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07002190 self.newrev = None
James E. Blairad28e912013-11-27 10:43:22 -08002191 # For events that arrive with a destination pipeline (eg, from
2192 # an admin command, etc):
2193 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07002194
James E. Blair6f284b42017-03-31 14:14:41 -07002195 @property
2196 def canonical_project_name(self):
2197 return self.project_hostname + '/' + self.project_name
2198
Jan Hruban324ca5b2015-11-05 19:28:54 +01002199 def isPatchsetCreated(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01002200 return False
2201
2202 def isChangeAbandoned(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01002203 return False
2204
James E. Blair1e8dd892012-05-30 09:15:05 -07002205
James E. Blair9c17dbf2014-06-23 14:21:58 -07002206class BaseFilter(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002207 """Base Class for filtering which Changes and Events to process."""
James E. Blairaad3ae22017-05-18 14:11:29 -07002208 pass
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002209
James E. Blair9c17dbf2014-06-23 14:21:58 -07002210
2211class EventFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002212 """Allows a Pipeline to only respond to certain events."""
James E. Blairaad3ae22017-05-18 14:11:29 -07002213 def __init__(self, trigger):
2214 super(EventFilter, self).__init__()
James E. Blairc0dedf82014-08-06 09:37:52 -07002215 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07002216
James E. Blairaad3ae22017-05-18 14:11:29 -07002217 def matches(self, event, ref):
2218 # TODO(jeblair): consider removing ref argument
James E. Blairee743612012-05-29 14:49:32 -07002219 return True
James E. Blaireff88162013-07-01 12:44:14 -04002220
2221
James E. Blairaad3ae22017-05-18 14:11:29 -07002222class RefFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002223 """Allows a Manager to only enqueue Changes that meet certain criteria."""
Jesse Keating8c2eb572017-05-30 17:31:45 -07002224 def __init__(self, connection_name):
James E. Blairaad3ae22017-05-18 14:11:29 -07002225 super(RefFilter, self).__init__()
Jesse Keating8c2eb572017-05-30 17:31:45 -07002226 self.connection_name = connection_name
James E. Blair11041d22014-05-02 14:49:53 -07002227
2228 def matches(self, change):
James E. Blair11041d22014-05-02 14:49:53 -07002229 return True
2230
2231
James E. Blairb97ed802015-12-21 15:55:35 -08002232class ProjectPipelineConfig(object):
2233 # Represents a project cofiguration in the context of a pipeline
2234 def __init__(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002235 self.job_list = JobList()
James E. Blairb97ed802015-12-21 15:55:35 -08002236 self.queue_name = None
Adam Gandelman8bd57102016-12-02 12:58:42 -08002237 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08002238
2239
James E. Blair08d9b782017-06-29 14:22:48 -07002240class TenantProjectConfig(object):
2241 """A project in the context of a tenant.
2242
2243 A Project is globally unique in the system, however, when used in
2244 a tenant, some metadata about the project local to the tenant is
2245 stored in a TenantProjectConfig.
2246 """
2247
2248 def __init__(self, project):
2249 self.project = project
2250 self.load_classes = set()
James E. Blair6459db12017-06-29 14:57:20 -07002251 self.shadow_projects = set()
James E. Blairdaaf3262017-10-23 13:51:48 -07002252 self.branches = []
Tobias Henkeleca46202017-08-02 20:27:10 +02002253 # The tenant's default setting of exclude_unprotected_branches will
2254 # be overridden by this one if not None.
2255 self.exclude_unprotected_branches = None
2256
James E. Blair08d9b782017-06-29 14:22:48 -07002257
James E. Blairb97ed802015-12-21 15:55:35 -08002258class ProjectConfig(object):
2259 # Represents a project cofiguration
James E. Blair2a664502017-10-27 11:39:33 -07002260 def __init__(self, name, source_context=None):
James E. Blairb97ed802015-12-21 15:55:35 -08002261 self.name = name
James E. Blair2a664502017-10-27 11:39:33 -07002262 # If this is a template, it will have a source_context, but
2263 # not if it is a project definition.
2264 self.source_context = source_context
Adam Gandelman8bd57102016-12-02 12:58:42 -08002265 self.merge_mode = None
James E. Blaire74f5712017-09-29 15:14:31 -07002266 # The default branch for the project (usually master).
James E. Blair040b6502017-05-23 10:18:21 -07002267 self.default_branch = None
James E. Blairb97ed802015-12-21 15:55:35 -08002268 self.pipelines = {}
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002269 self.private_key_file = None
James E. Blairb97ed802015-12-21 15:55:35 -08002270
2271
James E. Blair97043882017-09-06 15:51:17 -07002272class ConfigItemNotListError(Exception):
2273 def __init__(self):
2274 message = textwrap.dedent("""\
2275 Configuration file is not a list. Each zuul.yaml configuration
2276 file must be a list of items, for example:
2277
2278 - job:
2279 name: foo
2280
2281 - project:
2282 name: bar
2283
2284 Ensure that every item starts with "- " so that it is parsed as a
2285 YAML list.
2286 """)
2287 super(ConfigItemNotListError, self).__init__(message)
2288
2289
2290class ConfigItemNotDictError(Exception):
2291 def __init__(self):
2292 message = textwrap.dedent("""\
2293 Configuration item is not a dictionary. Each zuul.yaml
2294 configuration file must be a list of dictionaries, for
2295 example:
2296
2297 - job:
2298 name: foo
2299
2300 - project:
2301 name: bar
2302
2303 Ensure that every item in the list is a dictionary with one
2304 key (in this example, 'job' and 'project').
2305 """)
2306 super(ConfigItemNotDictError, self).__init__(message)
2307
2308
2309class ConfigItemMultipleKeysError(Exception):
2310 def __init__(self):
2311 message = textwrap.dedent("""\
2312 Configuration item has more than one key. Each zuul.yaml
2313 configuration file must be a list of dictionaries with a
2314 single key, for example:
2315
2316 - job:
2317 name: foo
2318
2319 - project:
2320 name: bar
2321
2322 Ensure that every item in the list is a dictionary with only
2323 one key (in this example, 'job' and 'project'). This error
2324 may be caused by insufficient indentation of the keys under
2325 the configuration item ('name' in this example).
2326 """)
2327 super(ConfigItemMultipleKeysError, self).__init__(message)
2328
2329
2330class ConfigItemUnknownError(Exception):
2331 def __init__(self):
2332 message = textwrap.dedent("""\
2333 Configuration item not recognized. Each zuul.yaml
2334 configuration file must be a list of dictionaries, for
2335 example:
2336
2337 - job:
2338 name: foo
2339
2340 - project:
2341 name: bar
2342
2343 The dictionary keys must match one of the configuration item
2344 types recognized by zuul (for example, 'job' or 'project').
2345 """)
2346 super(ConfigItemUnknownError, self).__init__(message)
2347
2348
James E. Blaird8e778f2015-12-22 14:09:20 -08002349class UnparsedAbideConfig(object):
James E. Blair08d9b782017-06-29 14:22:48 -07002350
Monty Taylora42a55b2016-07-29 07:53:33 -07002351 """A collection of yaml lists that has not yet been parsed into objects.
2352
2353 An Abide is a collection of tenants.
2354 """
2355
James E. Blaird8e778f2015-12-22 14:09:20 -08002356 def __init__(self):
2357 self.tenants = []
2358
2359 def extend(self, conf):
2360 if isinstance(conf, UnparsedAbideConfig):
2361 self.tenants.extend(conf.tenants)
2362 return
2363
2364 if not isinstance(conf, list):
James E. Blair97043882017-09-06 15:51:17 -07002365 raise ConfigItemNotListError()
2366
James E. Blaird8e778f2015-12-22 14:09:20 -08002367 for item in conf:
2368 if not isinstance(item, dict):
James E. Blair97043882017-09-06 15:51:17 -07002369 raise ConfigItemNotDictError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002370 if len(item.keys()) > 1:
James E. Blair97043882017-09-06 15:51:17 -07002371 raise ConfigItemMultipleKeysError()
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002372 key, value = list(item.items())[0]
James E. Blaird8e778f2015-12-22 14:09:20 -08002373 if key == 'tenant':
2374 self.tenants.append(value)
2375 else:
James E. Blair97043882017-09-06 15:51:17 -07002376 raise ConfigItemUnknownError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002377
2378
2379class UnparsedTenantConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002380 """A collection of yaml lists that has not yet been parsed into objects."""
2381
James E. Blaird8e778f2015-12-22 14:09:20 -08002382 def __init__(self):
James E. Blair7edc25f2017-10-26 10:47:14 -07002383 self.pragmas = []
James E. Blaird8e778f2015-12-22 14:09:20 -08002384 self.pipelines = []
2385 self.jobs = []
2386 self.project_templates = []
James E. Blairff555742017-02-19 11:34:27 -08002387 self.projects = {}
James E. Blaira98340f2016-09-02 11:33:49 -07002388 self.nodesets = []
James E. Blair01f83b72017-03-15 13:03:40 -07002389 self.secrets = []
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002390 self.semaphores = []
James E. Blaird8e778f2015-12-22 14:09:20 -08002391
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002392 def copy(self):
2393 r = UnparsedTenantConfig()
James E. Blair7edc25f2017-10-26 10:47:14 -07002394 r.pragmas = copy.deepcopy(self.pragmas)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002395 r.pipelines = copy.deepcopy(self.pipelines)
2396 r.jobs = copy.deepcopy(self.jobs)
2397 r.project_templates = copy.deepcopy(self.project_templates)
2398 r.projects = copy.deepcopy(self.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07002399 r.nodesets = copy.deepcopy(self.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002400 r.secrets = copy.deepcopy(self.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002401 r.semaphores = copy.deepcopy(self.semaphores)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002402 return r
2403
James E. Blairec7ff302017-03-04 07:31:32 -08002404 def extend(self, conf):
James E. Blaird8e778f2015-12-22 14:09:20 -08002405 if isinstance(conf, UnparsedTenantConfig):
James E. Blair7edc25f2017-10-26 10:47:14 -07002406 self.pragmas.extend(conf.pragmas)
James E. Blaird8e778f2015-12-22 14:09:20 -08002407 self.pipelines.extend(conf.pipelines)
2408 self.jobs.extend(conf.jobs)
2409 self.project_templates.extend(conf.project_templates)
James E. Blairff555742017-02-19 11:34:27 -08002410 for k, v in conf.projects.items():
2411 self.projects.setdefault(k, []).extend(v)
James E. Blaira98340f2016-09-02 11:33:49 -07002412 self.nodesets.extend(conf.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002413 self.secrets.extend(conf.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002414 self.semaphores.extend(conf.semaphores)
James E. Blaird8e778f2015-12-22 14:09:20 -08002415 return
2416
2417 if not isinstance(conf, list):
James E. Blair97043882017-09-06 15:51:17 -07002418 raise ConfigItemNotListError()
James E. Blaircdab2032017-02-01 09:09:29 -08002419
James E. Blaird8e778f2015-12-22 14:09:20 -08002420 for item in conf:
2421 if not isinstance(item, dict):
James E. Blair97043882017-09-06 15:51:17 -07002422 raise ConfigItemNotDictError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002423 if len(item.keys()) > 1:
James E. Blair97043882017-09-06 15:51:17 -07002424 raise ConfigItemMultipleKeysError()
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002425 key, value = list(item.items())[0]
James E. Blair66b274e2017-01-31 14:47:52 -08002426 if key == 'project':
James E. Blairff555742017-02-19 11:34:27 -08002427 name = value['name']
2428 self.projects.setdefault(name, []).append(value)
James E. Blair66b274e2017-01-31 14:47:52 -08002429 elif key == 'job':
James E. Blaird8e778f2015-12-22 14:09:20 -08002430 self.jobs.append(value)
2431 elif key == 'project-template':
2432 self.project_templates.append(value)
2433 elif key == 'pipeline':
2434 self.pipelines.append(value)
James E. Blaira98340f2016-09-02 11:33:49 -07002435 elif key == 'nodeset':
2436 self.nodesets.append(value)
James E. Blair01f83b72017-03-15 13:03:40 -07002437 elif key == 'secret':
2438 self.secrets.append(value)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002439 elif key == 'semaphore':
2440 self.semaphores.append(value)
James E. Blair7edc25f2017-10-26 10:47:14 -07002441 elif key == 'pragma':
2442 self.pragmas.append(value)
James E. Blaird8e778f2015-12-22 14:09:20 -08002443 else:
James E. Blair97043882017-09-06 15:51:17 -07002444 raise ConfigItemUnknownError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002445
2446
James E. Blaireff88162013-07-01 12:44:14 -04002447class Layout(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002448 """Holds all of the Pipelines."""
2449
James E. Blair6459db12017-06-29 14:57:20 -07002450 def __init__(self, tenant):
James E. Blair8fe53b42017-10-18 16:58:38 -07002451 self.uuid = uuid4().hex
James E. Blair6459db12017-06-29 14:57:20 -07002452 self.tenant = tenant
James E. Blairb97ed802015-12-21 15:55:35 -08002453 self.project_configs = {}
2454 self.project_templates = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07002455 self.pipelines = OrderedDict()
James E. Blair83005782015-12-11 14:46:03 -08002456 # This is a dictionary of name -> [jobs]. The first element
2457 # of the list is the first job added with that name. It is
2458 # the reference definition for a given job. Subsequent
2459 # elements are aspects of that job with different matchers
2460 # that override some attribute of the job. These aspects all
2461 # inherit from the reference definition.
James E. Blairc32a8352017-10-11 16:27:50 -07002462 noop = Job('noop')
2463 noop.parent = noop.BASE_JOB_MARKER
James E. Blair00292672017-10-26 15:29:39 -07002464 noop.run = 'noop.yaml'
James E. Blairc32a8352017-10-11 16:27:50 -07002465 self.jobs = {'noop': [noop]}
James E. Blaira98340f2016-09-02 11:33:49 -07002466 self.nodesets = {}
James E. Blair01f83b72017-03-15 13:03:40 -07002467 self.secrets = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002468 self.semaphores = {}
James E. Blaireff88162013-07-01 12:44:14 -04002469
2470 def getJob(self, name):
2471 if name in self.jobs:
James E. Blair83005782015-12-11 14:46:03 -08002472 return self.jobs[name][0]
James E. Blairb97ed802015-12-21 15:55:35 -08002473 raise Exception("Job %s not defined" % (name,))
James E. Blair83005782015-12-11 14:46:03 -08002474
James E. Blair2bab6e72017-08-07 09:52:45 -07002475 def hasJob(self, name):
2476 return name in self.jobs
2477
James E. Blair83005782015-12-11 14:46:03 -08002478 def getJobs(self, name):
2479 return self.jobs.get(name, [])
2480
2481 def addJob(self, job):
James E. Blair4317e9f2016-07-15 10:05:47 -07002482 # We can have multiple variants of a job all with the same
2483 # name, but these variants must all be defined in the same repo.
James E. Blaircdab2032017-02-01 09:09:29 -08002484 prior_jobs = [j for j in self.getJobs(job.name) if
2485 j.source_context.project !=
2486 job.source_context.project]
James E. Blair6459db12017-06-29 14:57:20 -07002487 # Unless the repo is permitted to shadow another. If so, and
2488 # the job we are adding is from a repo that is permitted to
2489 # shadow the one with the older jobs, skip adding this job.
2490 job_project = job.source_context.project
2491 job_tpc = self.tenant.project_configs[job_project.canonical_name]
2492 skip_add = False
2493 for prior_job in prior_jobs[:]:
2494 prior_project = prior_job.source_context.project
2495 if prior_project in job_tpc.shadow_projects:
2496 prior_jobs.remove(prior_job)
2497 skip_add = True
2498
James E. Blair4317e9f2016-07-15 10:05:47 -07002499 if prior_jobs:
2500 raise Exception("Job %s in %s is not permitted to shadow "
James E. Blaircdab2032017-02-01 09:09:29 -08002501 "job %s in %s" % (
2502 job,
2503 job.source_context.project,
2504 prior_jobs[0],
2505 prior_jobs[0].source_context.project))
James E. Blair6459db12017-06-29 14:57:20 -07002506 if skip_add:
2507 return False
James E. Blair83005782015-12-11 14:46:03 -08002508 if job.name in self.jobs:
2509 self.jobs[job.name].append(job)
2510 else:
2511 self.jobs[job.name] = [job]
James E. Blair6459db12017-06-29 14:57:20 -07002512 return True
James E. Blair83005782015-12-11 14:46:03 -08002513
James E. Blaira98340f2016-09-02 11:33:49 -07002514 def addNodeSet(self, nodeset):
2515 if nodeset.name in self.nodesets:
2516 raise Exception("NodeSet %s already defined" % (nodeset.name,))
2517 self.nodesets[nodeset.name] = nodeset
2518
James E. Blair01f83b72017-03-15 13:03:40 -07002519 def addSecret(self, secret):
2520 if secret.name in self.secrets:
2521 raise Exception("Secret %s already defined" % (secret.name,))
2522 self.secrets[secret.name] = secret
2523
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002524 def addSemaphore(self, semaphore):
2525 if semaphore.name in self.semaphores:
2526 raise Exception("Semaphore %s already defined" % (semaphore.name,))
2527 self.semaphores[semaphore.name] = semaphore
2528
James E. Blair83005782015-12-11 14:46:03 -08002529 def addPipeline(self, pipeline):
2530 self.pipelines[pipeline.name] = pipeline
James E. Blair59fdbac2015-12-07 17:08:06 -08002531
James E. Blairb97ed802015-12-21 15:55:35 -08002532 def addProjectTemplate(self, project_template):
James E. Blair2a664502017-10-27 11:39:33 -07002533 template = self.project_templates.get(project_template.name)
2534 if template:
2535 if (project_template.source_context.project !=
2536 template.source_context.project):
2537 raise Exception("Project template %s is already defined" %
2538 (project_template.name,))
2539 for pipeline in project_template.pipelines:
2540 template.pipelines[pipeline].job_list.\
2541 inheritFrom(project_template.pipelines[pipeline].job_list,
2542 None)
2543 else:
2544 self.project_templates[project_template.name] = project_template
James E. Blairb97ed802015-12-21 15:55:35 -08002545
James E. Blairf59f3cf2017-02-19 14:50:26 -08002546 def addProjectConfig(self, project_config):
James E. Blairb97ed802015-12-21 15:55:35 -08002547 self.project_configs[project_config.name] = project_config
James E. Blairb97ed802015-12-21 15:55:35 -08002548
James E. Blairc32a8352017-10-11 16:27:50 -07002549 def collectJobs(self, jobname, change, path=None, jobs=None, stack=None):
2550 if stack is None:
2551 stack = []
2552 if jobs is None:
2553 jobs = []
2554 if path is None:
2555 path = []
2556 path.append(jobname)
2557 matched = False
2558 for variant in self.getJobs(jobname):
2559 if not variant.changeMatches(change):
2560 continue
2561 if not variant.isBase():
2562 parent = variant.parent
2563 if not jobs and parent is None:
2564 parent = self.tenant.default_base_job
2565 else:
2566 parent = None
2567 if parent and parent not in path:
2568 if parent in stack:
2569 raise Exception("Dependency cycle in jobs: %s" % stack)
2570 self.collectJobs(parent, change, path, jobs, stack + [jobname])
2571 matched = True
2572 jobs.append(variant)
2573 if not matched:
2574 raise NoMatchingParentError()
2575 return jobs
2576
James E. Blaird2348362017-03-17 13:59:35 -07002577 def _createJobGraph(self, item, job_list, job_graph):
2578 change = item.change
2579 pipeline = item.pipeline
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002580 for jobname in job_list.jobs:
2581 # This is the final job we are constructing
James E. Blaira7f51ca2017-02-07 16:01:26 -08002582 frozen_job = None
James E. Blairc32a8352017-10-11 16:27:50 -07002583 try:
2584 variants = self.collectJobs(jobname, change)
2585 except NoMatchingParentError:
2586 variants = None
2587 if not variants:
James E. Blair6e85c2b2016-11-21 16:47:01 -08002588 # A change must match at least one defined job variant
2589 # (that is to say that it must match more than just
2590 # the job that is defined in the tree).
2591 continue
James E. Blairc32a8352017-10-11 16:27:50 -07002592 for variant in variants:
2593 if frozen_job is None:
2594 frozen_job = variant.copy()
2595 frozen_job.setBase()
2596 else:
2597 frozen_job.applyVariant(variant)
2598 frozen_job.name = variant.name
James E. Blairc32a8352017-10-11 16:27:50 -07002599 frozen_job.name = jobname
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002600 # Whether the change matches any of the project pipeline
2601 # variants
2602 matched = False
2603 for variant in job_list.jobs[jobname]:
2604 if variant.changeMatches(change):
2605 frozen_job.applyVariant(variant)
2606 matched = True
2607 if not matched:
2608 # A change must match at least one project pipeline
2609 # job variant.
2610 continue
James E. Blairb3f5db12017-03-17 12:57:39 -07002611 if (frozen_job.allowed_projects and
2612 change.project.name not in frozen_job.allowed_projects):
2613 raise Exception("Project %s is not allowed to run job %s" %
2614 (change.project.name, frozen_job.name))
James E. Blair8eb564a2017-08-10 09:21:41 -07002615 if ((not pipeline.post_review) and frozen_job.post_review):
2616 raise Exception("Pre-review pipeline %s does not allow "
2617 "post-review job %s" % (
James E. Blaird2348362017-03-17 13:59:35 -07002618 pipeline.name, frozen_job.name))
James E. Blair00292672017-10-26 15:29:39 -07002619 if not frozen_job.run:
2620 raise Exception("Job %s does not specify a run playbook" % (
2621 frozen_job.name,))
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002622 job_graph.addJob(frozen_job)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002623
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002624 def createJobGraph(self, item):
Paul Belanger15e3e202016-10-14 16:27:34 -04002625 # NOTE(pabelanger): It is possible for a foreign project not to have a
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002626 # configured pipeline, if so return an empty JobGraph.
James E. Blairc9455002017-09-06 09:22:19 -07002627 ret = JobGraph()
2628 ppc = self.getProjectPipelineConfig(item.change.project,
2629 item.pipeline)
2630 if ppc:
2631 self._createJobGraph(item, ppc.job_list, ret)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002632 return ret
2633
James E. Blairc9455002017-09-06 09:22:19 -07002634 def getProjectPipelineConfig(self, project, pipeline):
2635 project_config = self.project_configs.get(
2636 project.canonical_name, None)
2637 if not project_config:
2638 return None
2639 return project_config.pipelines.get(pipeline.name, None)
James E. Blair0d3e83b2017-06-05 13:51:57 -07002640
James E. Blair59fdbac2015-12-07 17:08:06 -08002641
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002642class Semaphore(object):
2643 def __init__(self, name, max=1):
2644 self.name = name
2645 self.max = int(max)
2646
2647
2648class SemaphoreHandler(object):
2649 log = logging.getLogger("zuul.SemaphoreHandler")
2650
2651 def __init__(self):
2652 self.semaphores = {}
2653
2654 def acquire(self, item, job):
2655 if not job.semaphore:
2656 return True
2657
2658 semaphore_key = job.semaphore
2659
2660 m = self.semaphores.get(semaphore_key)
2661 if not m:
2662 # The semaphore is not held, acquire it
2663 self._acquire(semaphore_key, item, job.name)
2664 return True
2665 if (item, job.name) in m:
2666 # This item already holds the semaphore
2667 return True
2668
2669 # semaphore is there, check max
2670 if len(m) < self._max_count(item, job.semaphore):
2671 self._acquire(semaphore_key, item, job.name)
2672 return True
2673
2674 return False
2675
2676 def release(self, item, job):
2677 if not job.semaphore:
2678 return
2679
2680 semaphore_key = job.semaphore
2681
2682 m = self.semaphores.get(semaphore_key)
2683 if not m:
2684 # The semaphore is not held, nothing to do
2685 self.log.error("Semaphore can not be released for %s "
2686 "because the semaphore is not held" %
2687 item)
2688 return
2689 if (item, job.name) in m:
2690 # This item is a holder of the semaphore
2691 self._release(semaphore_key, item, job.name)
2692 return
2693 self.log.error("Semaphore can not be released for %s "
2694 "which does not hold it" % item)
2695
2696 def _acquire(self, semaphore_key, item, job_name):
2697 self.log.debug("Semaphore acquire {semaphore}: job {job}, item {item}"
2698 .format(semaphore=semaphore_key,
2699 job=job_name,
2700 item=item))
2701 if semaphore_key not in self.semaphores:
2702 self.semaphores[semaphore_key] = []
2703 self.semaphores[semaphore_key].append((item, job_name))
2704
2705 def _release(self, semaphore_key, item, job_name):
2706 self.log.debug("Semaphore release {semaphore}: job {job}, item {item}"
2707 .format(semaphore=semaphore_key,
2708 job=job_name,
2709 item=item))
2710 sem_item = (item, job_name)
2711 if sem_item in self.semaphores[semaphore_key]:
2712 self.semaphores[semaphore_key].remove(sem_item)
2713
2714 # cleanup if there is no user of the semaphore anymore
2715 if len(self.semaphores[semaphore_key]) == 0:
2716 del self.semaphores[semaphore_key]
2717
2718 @staticmethod
2719 def _max_count(item, semaphore_name):
James E. Blair29a24fd2017-10-02 15:04:56 -07002720 if not item.layout:
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002721 # This should not occur as the layout of the item must already be
2722 # built when acquiring or releasing a semaphore for a job.
2723 raise Exception("Item {} has no layout".format(item))
2724
2725 # find the right semaphore
2726 default_semaphore = Semaphore(semaphore_name, 1)
James E. Blair29a24fd2017-10-02 15:04:56 -07002727 semaphores = item.layout.semaphores
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002728 return semaphores.get(semaphore_name, default_semaphore).max
2729
2730
James E. Blair59fdbac2015-12-07 17:08:06 -08002731class Tenant(object):
2732 def __init__(self, name):
2733 self.name = name
Tristan Cacqueray82f864b2017-08-01 05:54:42 +00002734 self.max_nodes_per_job = 5
Tristan Cacquerayc98bff72017-09-10 15:25:26 +00002735 self.max_job_timeout = 10800
Tobias Henkeleca46202017-08-02 20:27:10 +02002736 self.exclude_unprotected_branches = False
James E. Blair2bab6e72017-08-07 09:52:45 -07002737 self.default_base_job = None
James E. Blair59fdbac2015-12-07 17:08:06 -08002738 self.layout = None
James E. Blair646322f2017-01-27 15:50:34 -08002739 # The unparsed configuration from the main zuul config for
2740 # this tenant.
2741 self.unparsed_config = None
James E. Blair109da3f2017-04-04 14:39:43 -07002742 # The list of projects from which we will read full
James E. Blair8a395f92017-03-30 11:15:33 -07002743 # configuration.
James E. Blair109da3f2017-04-04 14:39:43 -07002744 self.config_projects = []
2745 # The unparsed config from those projects.
2746 self.config_projects_config = None
2747 # The list of projects from which we will read untrusted
2748 # in-repo configuration.
2749 self.untrusted_projects = []
2750 # The unparsed config from those projects.
2751 self.untrusted_projects_config = None
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002752 self.semaphore_handler = SemaphoreHandler()
James E. Blair08d9b782017-06-29 14:22:48 -07002753 # Metadata about projects for this tenant
2754 # canonical project name -> TenantProjectConfig
2755 self.project_configs = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002756
James E. Blairc2a54fd2017-03-29 15:19:26 -07002757 # A mapping of project names to projects. project_name ->
2758 # VALUE where VALUE is a further dictionary of
2759 # canonical_hostname -> Project.
2760 self.projects = {}
2761 self.canonical_hostnames = set()
2762
James E. Blair08d9b782017-06-29 14:22:48 -07002763 def _addProject(self, tpc):
James E. Blairc2a54fd2017-03-29 15:19:26 -07002764 """Add a project to the project index
2765
James E. Blair08d9b782017-06-29 14:22:48 -07002766 :arg TenantProjectConfig tpc: The TenantProjectConfig (with
2767 associated project) to add.
2768
James E. Blairc2a54fd2017-03-29 15:19:26 -07002769 """
James E. Blair08d9b782017-06-29 14:22:48 -07002770 project = tpc.project
James E. Blairc2a54fd2017-03-29 15:19:26 -07002771 self.canonical_hostnames.add(project.canonical_hostname)
2772 hostname_dict = self.projects.setdefault(project.name, {})
2773 if project.canonical_hostname in hostname_dict:
2774 raise Exception("Project %s is already in project index" %
2775 (project,))
2776 hostname_dict[project.canonical_hostname] = project
James E. Blair08d9b782017-06-29 14:22:48 -07002777 self.project_configs[project.canonical_name] = tpc
James E. Blairc2a54fd2017-03-29 15:19:26 -07002778
2779 def getProject(self, name):
2780 """Return a project given its name.
2781
2782 :arg str name: The name of the project. It may be fully
2783 qualified (E.g., "git.example.com/subpath/project") or may
2784 contain only the project name name may be supplied (E.g.,
2785 "subpath/project").
2786
2787 :returns: A tuple (trusted, project) or (None, None) if the
2788 project is not found or ambiguous. The "trusted" boolean
2789 indicates whether or not the project is trusted by this
2790 tenant.
2791 :rtype: (bool, Project)
2792
2793 """
2794 path = name.split('/', 1)
2795 if path[0] in self.canonical_hostnames:
2796 hostname = path[0]
2797 project_name = path[1]
2798 else:
2799 hostname = None
2800 project_name = name
2801 hostname_dict = self.projects.get(project_name)
2802 project = None
2803 if hostname_dict:
2804 if hostname:
2805 project = hostname_dict.get(hostname)
2806 else:
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002807 values = list(hostname_dict.values())
James E. Blairc2a54fd2017-03-29 15:19:26 -07002808 if len(values) == 1:
2809 project = values[0]
2810 else:
2811 raise Exception("Project name '%s' is ambiguous, "
2812 "please fully qualify the project "
2813 "with a hostname" % (name,))
2814 if project is None:
2815 return (None, None)
James E. Blair109da3f2017-04-04 14:39:43 -07002816 if project in self.config_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002817 return (True, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002818 if project in self.untrusted_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002819 return (False, project)
2820 # This should never happen:
2821 raise Exception("Project %s is neither trusted nor untrusted" %
2822 (project,))
2823
James E. Blairdaaf3262017-10-23 13:51:48 -07002824 def getProjectBranches(self, project):
2825 """Return a project's branches (filtered by this tenant config)
2826
2827 :arg Project project: The project object.
2828
2829 :returns: A list of branch names.
2830 :rtype: [str]
2831
2832 """
2833 tpc = self.project_configs[project.canonical_name]
2834 return tpc.branches
2835
James E. Blair08d9b782017-06-29 14:22:48 -07002836 def addConfigProject(self, tpc):
2837 self.config_projects.append(tpc.project)
2838 self._addProject(tpc)
James E. Blair5ac93842017-01-20 06:47:34 -08002839
James E. Blair08d9b782017-06-29 14:22:48 -07002840 def addUntrustedProject(self, tpc):
2841 self.untrusted_projects.append(tpc.project)
2842 self._addProject(tpc)
James E. Blair5ac93842017-01-20 06:47:34 -08002843
Jamie Lennoxbf997c82017-05-10 09:58:40 +10002844 def getSafeAttributes(self):
2845 return Attributes(name=self.name)
2846
James E. Blair59fdbac2015-12-07 17:08:06 -08002847
2848class Abide(object):
2849 def __init__(self):
2850 self.tenants = OrderedDict()
James E. Blairce8a2132016-05-19 15:21:52 -07002851
2852
2853class JobTimeData(object):
2854 format = 'B10H10H10B'
2855 version = 0
2856
2857 def __init__(self, path):
2858 self.path = path
2859 self.success_times = [0 for x in range(10)]
2860 self.failure_times = [0 for x in range(10)]
2861 self.results = [0 for x in range(10)]
2862
2863 def load(self):
2864 if not os.path.exists(self.path):
2865 return
Clint Byruma4471d12017-05-10 20:57:40 -04002866 with open(self.path, 'rb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002867 data = struct.unpack(self.format, f.read())
2868 version = data[0]
2869 if version != self.version:
2870 raise Exception("Unkown data version")
2871 self.success_times = list(data[1:11])
2872 self.failure_times = list(data[11:21])
2873 self.results = list(data[21:32])
2874
2875 def save(self):
2876 tmpfile = self.path + '.tmp'
2877 data = [self.version]
2878 data.extend(self.success_times)
2879 data.extend(self.failure_times)
2880 data.extend(self.results)
2881 data = struct.pack(self.format, *data)
Clint Byruma4471d12017-05-10 20:57:40 -04002882 with open(tmpfile, 'wb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002883 f.write(data)
2884 os.rename(tmpfile, self.path)
2885
2886 def add(self, elapsed, result):
2887 elapsed = int(elapsed)
2888 if result == 'SUCCESS':
2889 self.success_times.append(elapsed)
2890 self.success_times.pop(0)
2891 result = 0
2892 else:
2893 self.failure_times.append(elapsed)
2894 self.failure_times.pop(0)
2895 result = 1
2896 self.results.append(result)
2897 self.results.pop(0)
2898
2899 def getEstimatedTime(self):
2900 times = [x for x in self.success_times if x]
2901 if times:
2902 return float(sum(times)) / len(times)
2903 return 0.0
2904
2905
2906class TimeDataBase(object):
2907 def __init__(self, root):
2908 self.root = root
James E. Blairce8a2132016-05-19 15:21:52 -07002909
James E. Blairae0f23c2017-09-13 10:55:15 -06002910 def _getTD(self, build):
2911 if hasattr(build.build_set.item.change, 'branch'):
2912 branch = build.build_set.item.change.branch
2913 else:
2914 branch = ''
2915
2916 dir_path = os.path.join(
2917 self.root,
2918 build.build_set.item.pipeline.layout.tenant.name,
2919 build.build_set.item.change.project.canonical_name,
2920 branch)
2921 if not os.path.exists(dir_path):
2922 os.makedirs(dir_path)
2923 path = os.path.join(dir_path, build.job.name)
2924
2925 td = JobTimeData(path)
2926 td.load()
James E. Blairce8a2132016-05-19 15:21:52 -07002927 return td
2928
2929 def getEstimatedTime(self, name):
2930 return self._getTD(name).getEstimatedTime()
2931
James E. Blairae0f23c2017-09-13 10:55:15 -06002932 def update(self, build, elapsed, result):
2933 td = self._getTD(build)
James E. Blairce8a2132016-05-19 15:21:52 -07002934 td.add(elapsed, result)
2935 td.save()