blob: 6c2a59c4077a601b0ffb1cb07a122d738295b038 [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
Joshua Hesketh58419cb2017-02-24 13:09:22 -050082class Attributes(object):
83 """A class to hold attributes for string formatting."""
84
85 def __init__(self, **kw):
86 setattr(self, '__dict__', kw)
87
88
James E. Blair4aea70c2012-07-26 14:23:24 -070089class Pipeline(object):
James E. Blair6053de42017-04-05 11:27:11 -070090 """A configuration that ties together triggers, reporters and managers
Monty Taylor82dfd412016-07-29 12:01:28 -070091
92 Trigger
93 A description of which events should be processed
94
95 Manager
96 Responsible for enqueing and dequeing Changes
97
98 Reporter
99 Communicates success and failure results somewhere
Monty Taylora42a55b2016-07-29 07:53:33 -0700100 """
James E. Blair83005782015-12-11 14:46:03 -0800101 def __init__(self, name, layout):
James E. Blair4aea70c2012-07-26 14:23:24 -0700102 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800103 self.layout = layout
James E. Blair8dbd56a2012-12-22 10:55:10 -0800104 self.description = None
James E. Blair56370192013-01-14 15:47:28 -0800105 self.failure_message = None
Joshua Heskethb7179772014-01-30 23:30:46 +1100106 self.merge_failure_message = None
James E. Blair56370192013-01-14 15:47:28 -0800107 self.success_message = None
Joshua Hesketh3979e3e2014-03-04 11:21:10 +1100108 self.footer_message = None
James E. Blair60af7f42016-03-11 16:11:06 -0800109 self.start_message = None
James E. Blair8eb564a2017-08-10 09:21:41 -0700110 self.post_review = False
James E. Blair2fa50962013-01-30 21:50:41 -0800111 self.dequeue_on_new_patchset = True
James E. Blair17dd6772015-02-09 14:45:18 -0800112 self.ignore_dependencies = False
James E. Blair4aea70c2012-07-26 14:23:24 -0700113 self.manager = None
James E. Blaire0487072012-08-29 17:38:31 -0700114 self.queues = []
James E. Blair64ed6f22013-07-10 14:07:23 -0700115 self.precedence = PRECEDENCE_NORMAL
James E. Blair83005782015-12-11 14:46:03 -0800116 self.triggers = []
Joshua Hesketh352264b2015-08-11 23:42:08 +1000117 self.start_actions = []
118 self.success_actions = []
119 self.failure_actions = []
120 self.merge_failure_actions = []
121 self.disabled_actions = []
Joshua Hesketh89e829d2015-02-10 16:29:45 +1100122 self.disable_at = None
123 self._consecutive_failures = 0
124 self._disabled = False
Clark Boylan7603a372014-01-21 11:43:20 -0800125 self.window = None
126 self.window_floor = None
127 self.window_increase_type = None
128 self.window_increase_factor = None
129 self.window_decrease_type = None
130 self.window_decrease_factor = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700131
James E. Blair83005782015-12-11 14:46:03 -0800132 @property
133 def actions(self):
134 return (
135 self.start_actions +
136 self.success_actions +
137 self.failure_actions +
138 self.merge_failure_actions +
139 self.disabled_actions
140 )
141
James E. Blaird09c17a2012-08-07 09:23:14 -0700142 def __repr__(self):
143 return '<Pipeline %s>' % self.name
144
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100145 def getSafeAttributes(self):
146 return Attributes(name=self.name)
147
James E. Blair4aea70c2012-07-26 14:23:24 -0700148 def setManager(self, manager):
149 self.manager = manager
150
James E. Blaire0487072012-08-29 17:38:31 -0700151 def addQueue(self, queue):
152 self.queues.append(queue)
153
154 def getQueue(self, project):
155 for queue in self.queues:
156 if project in queue.projects:
157 return queue
158 return None
159
James E. Blairbfb8e042014-12-30 17:01:44 -0800160 def removeQueue(self, queue):
Tobias Henkel6b9390f2017-03-28 11:23:21 +0200161 if queue in self.queues:
162 self.queues.remove(queue)
James E. Blairbfb8e042014-12-30 17:01:44 -0800163
James E. Blaire0487072012-08-29 17:38:31 -0700164 def getChangesInQueue(self):
165 changes = []
166 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700167 changes.extend([x.change for x in shared_queue.queue])
James E. Blaire0487072012-08-29 17:38:31 -0700168 return changes
169
James E. Blairfee8d652013-06-07 08:57:52 -0700170 def getAllItems(self):
171 items = []
James E. Blaire0487072012-08-29 17:38:31 -0700172 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700173 items.extend(shared_queue.queue)
James E. Blairfee8d652013-06-07 08:57:52 -0700174 return items
James E. Blaire0487072012-08-29 17:38:31 -0700175
Tobias Henkelb4407fc2017-07-07 13:52:56 +0200176 def formatStatusJSON(self, websocket_url=None):
James E. Blair8dbd56a2012-12-22 10:55:10 -0800177 j_pipeline = dict(name=self.name,
178 description=self.description)
179 j_queues = []
180 j_pipeline['change_queues'] = j_queues
181 for queue in self.queues:
182 j_queue = dict(name=queue.name)
183 j_queues.append(j_queue)
184 j_queue['heads'] = []
Clark Boylanaf2476f2014-01-23 14:47:36 -0800185 j_queue['window'] = queue.window
James E. Blair972e3c72013-08-29 12:04:55 -0700186
187 j_changes = []
188 for e in queue.queue:
189 if not e.item_ahead:
190 if j_changes:
191 j_queue['heads'].append(j_changes)
192 j_changes = []
Tobias Henkelb4407fc2017-07-07 13:52:56 +0200193 j_changes.append(e.formatJSON(websocket_url))
James E. Blair972e3c72013-08-29 12:04:55 -0700194 if (len(j_changes) > 1 and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000195 (j_changes[-2]['remaining_time'] is not None) and
196 (j_changes[-1]['remaining_time'] is not None)):
James E. Blair972e3c72013-08-29 12:04:55 -0700197 j_changes[-1]['remaining_time'] = max(
198 j_changes[-2]['remaining_time'],
199 j_changes[-1]['remaining_time'])
200 if j_changes:
James E. Blair8dbd56a2012-12-22 10:55:10 -0800201 j_queue['heads'].append(j_changes)
202 return j_pipeline
203
James E. Blair4aea70c2012-07-26 14:23:24 -0700204
James E. Blairee743612012-05-29 14:49:32 -0700205class ChangeQueue(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700206 """A ChangeQueue contains Changes to be processed related projects.
207
Monty Taylor82dfd412016-07-29 12:01:28 -0700208 A Pipeline with a DependentPipelineManager has multiple parallel
209 ChangeQueues shared by different projects. For instance, there may a
210 ChangeQueue shared by interrelated projects foo and bar, and a second queue
211 for independent project baz.
Monty Taylora42a55b2016-07-29 07:53:33 -0700212
Monty Taylor82dfd412016-07-29 12:01:28 -0700213 A Pipeline with an IndependentPipelineManager puts every Change into its
214 own ChangeQueue
Monty Taylora42a55b2016-07-29 07:53:33 -0700215
216 The ChangeQueue Window is inspired by TCP windows and controlls how many
217 Changes in a given ChangeQueue will be considered active and ready to
218 be processed. If a Change succeeds, the Window is increased by
219 `window_increase_factor`. If a Change fails, the Window is decreased by
220 `window_decrease_factor`.
Jesse Keating78f544a2017-07-13 14:27:40 -0700221
222 A ChangeQueue may be a dynamically created queue, which may be removed
223 from a DependentPipelineManager once empty.
Monty Taylora42a55b2016-07-29 07:53:33 -0700224 """
James E. Blairbfb8e042014-12-30 17:01:44 -0800225 def __init__(self, pipeline, window=0, window_floor=1,
Clark Boylan7603a372014-01-21 11:43:20 -0800226 window_increase_type='linear', window_increase_factor=1,
James E. Blair0dcef7a2016-08-19 09:35:17 -0700227 window_decrease_type='exponential', window_decrease_factor=2,
Jesse Keating78f544a2017-07-13 14:27:40 -0700228 name=None, dynamic=False):
James E. Blair4aea70c2012-07-26 14:23:24 -0700229 self.pipeline = pipeline
James E. Blair0dcef7a2016-08-19 09:35:17 -0700230 if name:
231 self.name = name
232 else:
233 self.name = ''
James E. Blairee743612012-05-29 14:49:32 -0700234 self.projects = []
235 self._jobs = set()
236 self.queue = []
Clark Boylan7603a372014-01-21 11:43:20 -0800237 self.window = window
238 self.window_floor = window_floor
239 self.window_increase_type = window_increase_type
240 self.window_increase_factor = window_increase_factor
241 self.window_decrease_type = window_decrease_type
242 self.window_decrease_factor = window_decrease_factor
Jesse Keating78f544a2017-07-13 14:27:40 -0700243 self.dynamic = dynamic
James E. Blairee743612012-05-29 14:49:32 -0700244
James E. Blair9f9667e2012-06-12 17:51:08 -0700245 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700246 return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
James E. Blairee743612012-05-29 14:49:32 -0700247
248 def getJobs(self):
249 return self._jobs
250
251 def addProject(self, project):
252 if project not in self.projects:
253 self.projects.append(project)
James E. Blairc8a1e052014-02-25 09:29:26 -0800254
James E. Blair0dcef7a2016-08-19 09:35:17 -0700255 if not self.name:
256 self.name = project.name
James E. Blairee743612012-05-29 14:49:32 -0700257
258 def enqueueChange(self, change):
James E. Blairbfb8e042014-12-30 17:01:44 -0800259 item = QueueItem(self, change)
James E. Blaircdccd972013-07-01 12:10:22 -0700260 self.enqueueItem(item)
261 item.enqueue_time = time.time()
262 return item
263
264 def enqueueItem(self, item):
James E. Blair4a035d92014-01-23 13:10:48 -0800265 item.pipeline = self.pipeline
James E. Blairbfb8e042014-12-30 17:01:44 -0800266 item.queue = self
267 if self.queue:
James E. Blairfee8d652013-06-07 08:57:52 -0700268 item.item_ahead = self.queue[-1]
James E. Blair972e3c72013-08-29 12:04:55 -0700269 item.item_ahead.items_behind.append(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700270 self.queue.append(item)
James E. Blairee743612012-05-29 14:49:32 -0700271
James E. Blairfee8d652013-06-07 08:57:52 -0700272 def dequeueItem(self, item):
273 if item in self.queue:
274 self.queue.remove(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700275 if item.item_ahead:
James E. Blair972e3c72013-08-29 12:04:55 -0700276 item.item_ahead.items_behind.remove(item)
277 for item_behind in item.items_behind:
278 if item.item_ahead:
279 item.item_ahead.items_behind.append(item_behind)
280 item_behind.item_ahead = item.item_ahead
James E. Blairfee8d652013-06-07 08:57:52 -0700281 item.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700282 item.items_behind = []
James E. Blairfee8d652013-06-07 08:57:52 -0700283 item.dequeue_time = time.time()
James E. Blaire0487072012-08-29 17:38:31 -0700284
James E. Blair972e3c72013-08-29 12:04:55 -0700285 def moveItem(self, item, item_ahead):
James E. Blair972e3c72013-08-29 12:04:55 -0700286 if item.item_ahead == item_ahead:
287 return False
288 # Remove from current location
289 if item.item_ahead:
290 item.item_ahead.items_behind.remove(item)
291 for item_behind in item.items_behind:
292 if item.item_ahead:
293 item.item_ahead.items_behind.append(item_behind)
294 item_behind.item_ahead = item.item_ahead
295 # Add to new location
296 item.item_ahead = item_ahead
James E. Blair00451262013-09-20 11:40:17 -0700297 item.items_behind = []
James E. Blair972e3c72013-08-29 12:04:55 -0700298 if item.item_ahead:
299 item.item_ahead.items_behind.append(item)
300 return True
James E. Blairee743612012-05-29 14:49:32 -0700301
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800302 def isActionable(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800303 if self.window:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800304 return item in self.queue[:self.window]
Clark Boylan7603a372014-01-21 11:43:20 -0800305 else:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800306 return True
Clark Boylan7603a372014-01-21 11:43:20 -0800307
308 def increaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800309 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800310 if self.window_increase_type == 'linear':
311 self.window += self.window_increase_factor
312 elif self.window_increase_type == 'exponential':
313 self.window *= self.window_increase_factor
314
315 def decreaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800316 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800317 if self.window_decrease_type == 'linear':
318 self.window = max(
319 self.window_floor,
320 self.window - self.window_decrease_factor)
321 elif self.window_decrease_type == 'exponential':
322 self.window = max(
323 self.window_floor,
Morgan Fainbergfdae2252016-06-07 20:13:20 -0700324 int(self.window / self.window_decrease_factor))
James E. Blairee743612012-05-29 14:49:32 -0700325
James E. Blair1e8dd892012-05-30 09:15:05 -0700326
James E. Blair4aea70c2012-07-26 14:23:24 -0700327class Project(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700328 """A Project represents a git repository such as openstack/nova."""
329
James E. Blaircf440a22016-07-15 09:11:58 -0700330 # NOTE: Projects should only be instantiated via a Source object
331 # so that they are associated with and cached by their Connection.
332 # This makes a Project instance a unique identifier for a given
333 # project from a given source.
334
James E. Blair0a899752017-03-29 13:22:16 -0700335 def __init__(self, name, source, foreign=False):
James E. Blair4aea70c2012-07-26 14:23:24 -0700336 self.name = name
James E. Blair8a395f92017-03-30 11:15:33 -0700337 self.source = source
James E. Blair0a899752017-03-29 13:22:16 -0700338 self.connection_name = source.connection.connection_name
339 self.canonical_hostname = source.canonical_hostname
James E. Blairc2a54fd2017-03-29 15:19:26 -0700340 self.canonical_name = source.canonical_hostname + '/' + name
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000341 # foreign projects are those referenced in dependencies
342 # of layout projects, this should matter
343 # when deciding whether to enqueue their changes
James E. Blaircf440a22016-07-15 09:11:58 -0700344 # TODOv3 (jeblair): re-add support for foreign projects if needed
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000345 self.foreign = foreign
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700346 self.unparsed_config = None
James E. Blaire3162022017-02-20 16:47:27 -0500347 self.unparsed_branch_config = {} # branch -> UnparsedTenantConfig
James E. Blair4aea70c2012-07-26 14:23:24 -0700348
349 def __str__(self):
350 return self.name
351
352 def __repr__(self):
353 return '<Project %s>' % (self.name)
354
355
James E. Blair34776ee2016-08-25 13:53:54 -0700356class Node(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700357 """A single node for use by a job.
358
359 This may represent a request for a node, or an actual node
360 provided by Nodepool.
361 """
362
James E. Blair16d96a02017-06-08 11:32:56 -0700363 def __init__(self, name, label):
James E. Blair34776ee2016-08-25 13:53:54 -0700364 self.name = name
James E. Blair16d96a02017-06-08 11:32:56 -0700365 self.label = label
James E. Blaircbf43672017-01-04 14:33:41 -0800366 self.id = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800367 self.lock = None
David Shrewsburyffab07a2017-07-24 12:45:07 -0400368 self.hold_job = None
David Shrewsburyf9af9df2017-08-01 15:19:26 -0400369 self.comment = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800370 # Attributes from Nodepool
371 self._state = 'unknown'
372 self.state_time = time.time()
Monty Taylor56f61332017-04-11 05:38:12 -0500373 self.interface_ip = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800374 self.public_ipv4 = None
375 self.private_ipv4 = None
376 self.public_ipv6 = None
Tristan Cacqueray80954402017-05-28 00:33:55 +0000377 self.ssh_port = 22
James E. Blaircacdf2b2017-01-04 13:14:37 -0800378 self._keys = []
Paul Belanger30ba93a2017-03-16 16:28:10 -0400379 self.az = None
380 self.provider = None
381 self.region = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800382
383 @property
384 def state(self):
385 return self._state
386
387 @state.setter
388 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800389 if value not in NODE_STATES:
390 raise TypeError("'%s' is not a valid state" % value)
James E. Blaira38c28e2017-01-04 10:33:20 -0800391 self._state = value
392 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700393
394 def __repr__(self):
James E. Blair16d96a02017-06-08 11:32:56 -0700395 return '<Node %s %s:%s>' % (self.id, self.name, self.label)
James E. Blair34776ee2016-08-25 13:53:54 -0700396
James E. Blair0d952152017-02-07 17:14:44 -0800397 def __ne__(self, other):
398 return not self.__eq__(other)
399
400 def __eq__(self, other):
401 if not isinstance(other, Node):
402 return False
403 return (self.name == other.name and
James E. Blair16d96a02017-06-08 11:32:56 -0700404 self.label == other.label and
James E. Blair0d952152017-02-07 17:14:44 -0800405 self.id == other.id)
406
James E. Blaircacdf2b2017-01-04 13:14:37 -0800407 def toDict(self):
408 d = {}
409 d['state'] = self.state
David Shrewsburyffab07a2017-07-24 12:45:07 -0400410 d['hold_job'] = self.hold_job
David Shrewsburyf9af9df2017-08-01 15:19:26 -0400411 d['comment'] = self.comment
James E. Blaircacdf2b2017-01-04 13:14:37 -0800412 for k in self._keys:
413 d[k] = getattr(self, k)
414 return d
415
James E. Blaira38c28e2017-01-04 10:33:20 -0800416 def updateFromDict(self, data):
417 self._state = data['state']
James E. Blaircacdf2b2017-01-04 13:14:37 -0800418 keys = []
419 for k, v in data.items():
420 if k == 'state':
421 continue
422 keys.append(k)
423 setattr(self, k, v)
424 self._keys = keys
James E. Blaira38c28e2017-01-04 10:33:20 -0800425
James E. Blair34776ee2016-08-25 13:53:54 -0700426
Monty Taylor7b19ba72017-05-24 07:42:54 -0500427class Group(object):
428 """A logical group of nodes for use by a job.
429
430 A Group is a named set of node names that will be provided to
431 jobs in the inventory to describe logical units where some subset of tasks
432 run.
433 """
434
435 def __init__(self, name, nodes):
436 self.name = name
437 self.nodes = nodes
438
439 def __repr__(self):
440 return '<Group %s %s>' % (self.name, str(self.nodes))
441
442 def __ne__(self, other):
443 return not self.__eq__(other)
444
445 def __eq__(self, other):
446 if not isinstance(other, Group):
447 return False
448 return (self.name == other.name and
449 self.nodes == other.nodes)
450
451 def toDict(self):
452 return {
453 'name': self.name,
454 'nodes': self.nodes
455 }
456
457
James E. Blaira98340f2016-09-02 11:33:49 -0700458class NodeSet(object):
459 """A set of nodes.
460
461 In configuration, NodeSets are attributes of Jobs indicating that
462 a Job requires nodes matching this description.
463
464 They may appear as top-level configuration objects and be named,
465 or they may appears anonymously in in-line job definitions.
466 """
467
468 def __init__(self, name=None):
469 self.name = name or ''
470 self.nodes = OrderedDict()
Monty Taylor7b19ba72017-05-24 07:42:54 -0500471 self.groups = OrderedDict()
James E. Blaira98340f2016-09-02 11:33:49 -0700472
James E. Blair1774dd52017-02-03 10:52:32 -0800473 def __ne__(self, other):
474 return not self.__eq__(other)
475
476 def __eq__(self, other):
477 if not isinstance(other, NodeSet):
478 return False
479 return (self.name == other.name and
480 self.nodes == other.nodes)
481
James E. Blaircbf43672017-01-04 14:33:41 -0800482 def copy(self):
483 n = NodeSet(self.name)
484 for name, node in self.nodes.items():
James E. Blair16d96a02017-06-08 11:32:56 -0700485 n.addNode(Node(node.name, node.label))
Monty Taylor7b19ba72017-05-24 07:42:54 -0500486 for name, group in self.groups.items():
487 n.addGroup(Group(group.name, group.nodes[:]))
James E. Blaircbf43672017-01-04 14:33:41 -0800488 return n
489
James E. Blaira98340f2016-09-02 11:33:49 -0700490 def addNode(self, node):
491 if node.name in self.nodes:
492 raise Exception("Duplicate node in %s" % (self,))
493 self.nodes[node.name] = node
494
James E. Blair0eaad552016-09-02 12:09:54 -0700495 def getNodes(self):
Clint Byruma4471d12017-05-10 20:57:40 -0400496 return list(self.nodes.values())
James E. Blair0eaad552016-09-02 12:09:54 -0700497
Monty Taylor7b19ba72017-05-24 07:42:54 -0500498 def addGroup(self, group):
499 if group.name in self.groups:
500 raise Exception("Duplicate group in %s" % (self,))
501 self.groups[group.name] = group
502
503 def getGroups(self):
504 return list(self.groups.values())
505
James E. Blaira98340f2016-09-02 11:33:49 -0700506 def __repr__(self):
507 if self.name:
508 name = self.name + ' '
509 else:
510 name = ''
Monty Taylor7b19ba72017-05-24 07:42:54 -0500511 return '<NodeSet %s%s%s>' % (name, self.nodes, self.groups)
James E. Blaira98340f2016-09-02 11:33:49 -0700512
Tristan Cacqueray82f864b2017-08-01 05:54:42 +0000513 def __len__(self):
514 return len(self.nodes)
515
James E. Blaira98340f2016-09-02 11:33:49 -0700516
James E. Blair34776ee2016-08-25 13:53:54 -0700517class NodeRequest(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700518 """A request for a set of nodes."""
519
James E. Blair8b2a1472017-02-19 15:33:55 -0800520 def __init__(self, requestor, build_set, job, nodeset):
521 self.requestor = requestor
James E. Blair34776ee2016-08-25 13:53:54 -0700522 self.build_set = build_set
523 self.job = job
James E. Blair0eaad552016-09-02 12:09:54 -0700524 self.nodeset = nodeset
James E. Blair803e94f2017-01-06 09:18:59 -0800525 self._state = STATE_REQUESTED
James E. Blairdce6cea2016-12-20 16:45:32 -0800526 self.state_time = time.time()
527 self.stat = None
528 self.uid = uuid4().hex
James E. Blaircbf43672017-01-04 14:33:41 -0800529 self.id = None
James E. Blaircbbce0d2017-05-19 07:28:29 -0700530 # Zuul internal flags (not stored in ZK so they are not
James E. Blaira38c28e2017-01-04 10:33:20 -0800531 # overwritten).
532 self.failed = False
James E. Blaircbbce0d2017-05-19 07:28:29 -0700533 self.canceled = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800534
535 @property
Monty Taylor6dc5bc12017-09-29 15:47:31 -0500536 def priority(self):
Monty Taylorb5882052017-09-29 19:12:52 -0500537 if self.build_set:
538 precedence = self.build_set.item.pipeline.precedence
539 else:
540 precedence = PRECEDENCE_NORMAL
541 return PRIORITY_MAP[precedence]
Monty Taylor6dc5bc12017-09-29 15:47:31 -0500542
543 @property
James E. Blair6ab79e02017-01-06 10:10:17 -0800544 def fulfilled(self):
545 return (self._state == STATE_FULFILLED) and not self.failed
546
547 @property
James E. Blairdce6cea2016-12-20 16:45:32 -0800548 def state(self):
549 return self._state
550
551 @state.setter
552 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800553 if value not in REQUEST_STATES:
554 raise TypeError("'%s' is not a valid state" % value)
James E. Blairdce6cea2016-12-20 16:45:32 -0800555 self._state = value
556 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700557
558 def __repr__(self):
James E. Blaircbf43672017-01-04 14:33:41 -0800559 return '<NodeRequest %s %s>' % (self.id, self.nodeset)
James E. Blair34776ee2016-08-25 13:53:54 -0700560
James E. Blairdce6cea2016-12-20 16:45:32 -0800561 def toDict(self):
562 d = {}
James E. Blair16d96a02017-06-08 11:32:56 -0700563 nodes = [n.label for n in self.nodeset.getNodes()]
James E. Blairdce6cea2016-12-20 16:45:32 -0800564 d['node_types'] = nodes
James E. Blair8b2a1472017-02-19 15:33:55 -0800565 d['requestor'] = self.requestor
James E. Blairdce6cea2016-12-20 16:45:32 -0800566 d['state'] = self.state
567 d['state_time'] = self.state_time
568 return d
569
570 def updateFromDict(self, data):
571 self._state = data['state']
572 self.state_time = data['state_time']
573
James E. Blair34776ee2016-08-25 13:53:54 -0700574
James E. Blair01f83b72017-03-15 13:03:40 -0700575class Secret(object):
576 """A collection of private data.
577
578 In configuration, Secrets are collections of private data in
579 key-value pair format. They are defined as top-level
580 configuration objects and then referenced by Jobs.
581
582 """
583
James E. Blair8525e2b2017-03-15 14:05:47 -0700584 def __init__(self, name, source_context):
James E. Blair01f83b72017-03-15 13:03:40 -0700585 self.name = name
James E. Blair8525e2b2017-03-15 14:05:47 -0700586 self.source_context = source_context
James E. Blair01f83b72017-03-15 13:03:40 -0700587 # The secret data may or may not be encrypted. This attribute
588 # is named 'secret_data' to make it easy to search for and
589 # spot where it is directly used.
590 self.secret_data = {}
591
592 def __ne__(self, other):
593 return not self.__eq__(other)
594
595 def __eq__(self, other):
596 if not isinstance(other, Secret):
597 return False
598 return (self.name == other.name and
James E. Blair8525e2b2017-03-15 14:05:47 -0700599 self.source_context == other.source_context and
James E. Blair01f83b72017-03-15 13:03:40 -0700600 self.secret_data == other.secret_data)
601
602 def __repr__(self):
603 return '<Secret %s>' % (self.name,)
604
James E. Blair18f86a32017-03-15 14:43:26 -0700605 def decrypt(self, private_key):
606 """Return a copy of this secret with any encrypted data decrypted.
607 Note that the original remains encrypted."""
608
609 r = copy.deepcopy(self)
610 decrypted_secret_data = {}
611 for k, v in r.secret_data.items():
612 if hasattr(v, 'decrypt'):
613 decrypted_secret_data[k] = v.decrypt(private_key)
614 else:
615 decrypted_secret_data[k] = v
616 r.secret_data = decrypted_secret_data
617 return r
618
James E. Blair01f83b72017-03-15 13:03:40 -0700619
James E. Blaircdab2032017-02-01 09:09:29 -0800620class SourceContext(object):
621 """A reference to the branch of a project in configuration.
622
623 Jobs and playbooks reference this to keep track of where they
624 originate."""
625
James E. Blair6f140c72017-03-03 10:32:07 -0800626 def __init__(self, project, branch, path, trusted):
James E. Blaircdab2032017-02-01 09:09:29 -0800627 self.project = project
628 self.branch = branch
James E. Blair6f140c72017-03-03 10:32:07 -0800629 self.path = path
Monty Taylore6562aa2017-02-20 07:37:39 -0500630 self.trusted = trusted
James E. Blaircdab2032017-02-01 09:09:29 -0800631
James E. Blair6f140c72017-03-03 10:32:07 -0800632 def __str__(self):
633 return '%s/%s@%s' % (self.project, self.path, self.branch)
634
James E. Blaircdab2032017-02-01 09:09:29 -0800635 def __repr__(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800636 return '<SourceContext %s trusted:%s>' % (str(self),
637 self.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800638
James E. Blaira7f51ca2017-02-07 16:01:26 -0800639 def __deepcopy__(self, memo):
640 return self.copy()
641
642 def copy(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800643 return self.__class__(self.project, self.branch, self.path,
644 self.trusted)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800645
Tristan Cacqueraye50af2e2017-09-19 14:18:28 +0000646 def isSameProject(self, other):
647 if not isinstance(other, SourceContext):
648 return False
649 return (self.project == other.project and
650 self.branch == other.branch and
651 self.trusted == other.trusted)
652
James E. Blaircdab2032017-02-01 09:09:29 -0800653 def __ne__(self, other):
654 return not self.__eq__(other)
655
656 def __eq__(self, other):
657 if not isinstance(other, SourceContext):
658 return False
659 return (self.project == other.project and
660 self.branch == other.branch and
James E. Blair6f140c72017-03-03 10:32:07 -0800661 self.path == other.path and
Monty Taylore6562aa2017-02-20 07:37:39 -0500662 self.trusted == other.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800663
664
James E. Blair66b274e2017-01-31 14:47:52 -0800665class PlaybookContext(object):
James E. Blaircdab2032017-02-01 09:09:29 -0800666
James E. Blair66b274e2017-01-31 14:47:52 -0800667 """A reference to a playbook in the context of a project.
668
669 Jobs refer to objects of this class for their main, pre, and post
670 playbooks so that we can keep track of which repos and security
James E. Blair74a82cf2017-07-12 17:23:08 -0700671 contexts are needed in order to run them.
James E. Blair66b274e2017-01-31 14:47:52 -0800672
James E. Blair74a82cf2017-07-12 17:23:08 -0700673 We also keep a list of roles so that playbooks only run with the
674 roles which were defined at the point the playbook was defined.
675
676 """
677
James E. Blair892cca62017-08-09 11:36:58 -0700678 def __init__(self, source_context, path, roles, secrets):
James E. Blaircdab2032017-02-01 09:09:29 -0800679 self.source_context = source_context
James E. Blair66b274e2017-01-31 14:47:52 -0800680 self.path = path
James E. Blair74a82cf2017-07-12 17:23:08 -0700681 self.roles = roles
James E. Blair892cca62017-08-09 11:36:58 -0700682 self.secrets = secrets
James E. Blair66b274e2017-01-31 14:47:52 -0800683
684 def __repr__(self):
James E. Blaircdab2032017-02-01 09:09:29 -0800685 return '<PlaybookContext %s %s>' % (self.source_context,
686 self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800687
688 def __ne__(self, other):
689 return not self.__eq__(other)
690
691 def __eq__(self, other):
692 if not isinstance(other, PlaybookContext):
693 return False
James E. Blaircdab2032017-02-01 09:09:29 -0800694 return (self.source_context == other.source_context and
James E. Blair74a82cf2017-07-12 17:23:08 -0700695 self.path == other.path and
James E. Blair892cca62017-08-09 11:36:58 -0700696 self.roles == other.roles and
697 self.secrets == other.secrets)
James E. Blair66b274e2017-01-31 14:47:52 -0800698
699 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400700 # Render to a dict to use in passing json to the executor
James E. Blair892cca62017-08-09 11:36:58 -0700701 secrets = {}
702 for secret in self.secrets:
703 secret_data = copy.deepcopy(secret.secret_data)
704 secrets[secret.name] = secret_data
James E. Blair66b274e2017-01-31 14:47:52 -0800705 return dict(
James E. Blaircdab2032017-02-01 09:09:29 -0800706 connection=self.source_context.project.connection_name,
707 project=self.source_context.project.name,
708 branch=self.source_context.branch,
Monty Taylore6562aa2017-02-20 07:37:39 -0500709 trusted=self.source_context.trusted,
James E. Blair74a82cf2017-07-12 17:23:08 -0700710 roles=[r.toDict() for r in self.roles],
James E. Blair892cca62017-08-09 11:36:58 -0700711 secrets=secrets,
James E. Blaircdab2032017-02-01 09:09:29 -0800712 path=self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800713
714
Monty Taylorb934c1a2017-06-16 19:31:47 -0500715class Role(object, metaclass=abc.ABCMeta):
James E. Blair5ac93842017-01-20 06:47:34 -0800716 """A reference to an ansible role."""
717
718 def __init__(self, target_name):
719 self.target_name = target_name
720
721 @abc.abstractmethod
722 def __repr__(self):
723 pass
724
725 def __ne__(self, other):
726 return not self.__eq__(other)
727
728 @abc.abstractmethod
729 def __eq__(self, other):
730 if not isinstance(other, Role):
731 return False
732 return (self.target_name == other.target_name)
733
734 @abc.abstractmethod
735 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400736 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800737 return dict(target_name=self.target_name)
738
739
740class ZuulRole(Role):
741 """A reference to an ansible role in a Zuul project."""
742
James E. Blairbb94dfa2017-07-11 07:45:19 -0700743 def __init__(self, target_name, connection_name, project_name,
744 implicit=False):
James E. Blair5ac93842017-01-20 06:47:34 -0800745 super(ZuulRole, self).__init__(target_name)
746 self.connection_name = connection_name
747 self.project_name = project_name
James E. Blairbb94dfa2017-07-11 07:45:19 -0700748 self.implicit = implicit
James E. Blair5ac93842017-01-20 06:47:34 -0800749
750 def __repr__(self):
751 return '<ZuulRole %s %s>' % (self.project_name, self.target_name)
752
Clint Byrumaf7438f2017-05-10 17:26:57 -0400753 __hash__ = object.__hash__
754
James E. Blair5ac93842017-01-20 06:47:34 -0800755 def __eq__(self, other):
756 if not isinstance(other, ZuulRole):
757 return False
James E. Blairbb94dfa2017-07-11 07:45:19 -0700758 # Implicit is not consulted for equality so that we can handle
759 # implicit to explicit conversions.
James E. Blair5ac93842017-01-20 06:47:34 -0800760 return (super(ZuulRole, self).__eq__(other) and
James E. Blair1b27f6a2017-07-14 14:09:07 -0700761 self.connection_name == other.connection_name and
James E. Blair6563e4b2017-04-28 08:14:48 -0700762 self.project_name == other.project_name)
James E. Blair5ac93842017-01-20 06:47:34 -0800763
764 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400765 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800766 d = super(ZuulRole, self).toDict()
767 d['type'] = 'zuul'
768 d['connection'] = self.connection_name
769 d['project'] = self.project_name
James E. Blairbb94dfa2017-07-11 07:45:19 -0700770 d['implicit'] = self.implicit
James E. Blair5ac93842017-01-20 06:47:34 -0800771 return d
772
773
James E. Blairee743612012-05-29 14:49:32 -0700774class Job(object):
James E. Blair66b274e2017-01-31 14:47:52 -0800775
James E. Blaira7f51ca2017-02-07 16:01:26 -0800776 """A Job represents the defintion of actions to perform.
777
James E. Blaird4ade8c2017-02-19 15:25:46 -0800778 A Job is an abstract configuration concept. It describes what,
779 where, and under what circumstances something should be run
780 (contrast this with Build which is a concrete single execution of
781 a Job).
782
James E. Blaira7f51ca2017-02-07 16:01:26 -0800783 NB: Do not modify attributes of this class, set them directly
784 (e.g., "job.run = ..." rather than "job.run.append(...)").
785 """
Monty Taylora42a55b2016-07-29 07:53:33 -0700786
James E. Blairee743612012-05-29 14:49:32 -0700787 def __init__(self, name):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800788 # These attributes may override even the final form of a job
789 # in the context of a project-pipeline. They can not affect
790 # the execution of the job, but only whether the job is run
791 # and how it is reported.
792 self.context_attributes = dict(
793 voting=True,
794 hold_following_changes=False,
James E. Blair1774dd52017-02-03 10:52:32 -0800795 failure_message=None,
796 success_message=None,
797 failure_url=None,
798 success_url=None,
799 # Matchers. These are separate so they can be individually
800 # overidden.
801 branch_matcher=None,
802 file_matcher=None,
803 irrelevant_file_matcher=None, # skip-if
James E. Blaira7f51ca2017-02-07 16:01:26 -0800804 tags=frozenset(),
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200805 dependencies=frozenset(),
James E. Blair1774dd52017-02-03 10:52:32 -0800806 )
807
James E. Blaira7f51ca2017-02-07 16:01:26 -0800808 # These attributes affect how the job is actually run and more
809 # care must be taken when overriding them. If a job is
810 # declared "final", these may not be overriden in a
811 # project-pipeline.
812 self.execution_attributes = dict(
813 timeout=None,
James E. Blair490cf042017-02-24 23:07:21 -0500814 variables={},
James E. Blaira7f51ca2017-02-07 16:01:26 -0800815 nodeset=NodeSet(),
James E. Blaira7f51ca2017-02-07 16:01:26 -0800816 workspace=None,
817 pre_run=(),
818 post_run=(),
819 run=(),
820 implied_run=(),
Tobias Henkel9a0e1942017-03-20 16:16:02 +0100821 semaphore=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800822 attempts=3,
823 final=False,
James E. Blair5fc81922017-07-12 13:19:37 -0700824 roles=(),
James E. Blair912322f2017-05-23 13:11:25 -0700825 required_projects={},
James E. Blairb3f5db12017-03-17 12:57:39 -0700826 allowed_projects=None,
James E. Blair7391ecb2017-05-23 13:32:40 -0700827 override_branch=None,
James E. Blair8eb564a2017-08-10 09:21:41 -0700828 post_review=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800829 )
830
831 # These are generally internal attributes which are not
832 # accessible via configuration.
833 self.other_attributes = dict(
834 name=None,
835 source_context=None,
James E. Blair167d6cd2017-09-29 14:24:42 -0700836 source_line=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800837 inheritance_path=(),
838 )
839
840 self.inheritable_attributes = {}
841 self.inheritable_attributes.update(self.context_attributes)
842 self.inheritable_attributes.update(self.execution_attributes)
843 self.attributes = {}
844 self.attributes.update(self.inheritable_attributes)
845 self.attributes.update(self.other_attributes)
846
James E. Blairee743612012-05-29 14:49:32 -0700847 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800848
James E. Blair66b274e2017-01-31 14:47:52 -0800849 def __ne__(self, other):
850 return not self.__eq__(other)
851
Paul Belangere22baea2016-11-03 16:59:27 -0400852 def __eq__(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800853 # Compare the name and all inheritable attributes to determine
854 # whether two jobs with the same name are identically
855 # configured. Useful upon reconfiguration.
856 if not isinstance(other, Job):
857 return False
858 if self.name != other.name:
859 return False
860 for k, v in self.attributes.items():
861 if getattr(self, k) != getattr(other, k):
862 return False
863 return True
James E. Blairee743612012-05-29 14:49:32 -0700864
Clint Byrumaf7438f2017-05-10 17:26:57 -0400865 __hash__ = object.__hash__
866
James E. Blairee743612012-05-29 14:49:32 -0700867 def __str__(self):
868 return self.name
869
870 def __repr__(self):
James E. Blair167d6cd2017-09-29 14:24:42 -0700871 return '<Job %s branches: %s source: %s#%s>' % (
872 self.name,
873 self.branch_matcher,
874 self.source_context,
875 self.source_line)
James E. Blair83005782015-12-11 14:46:03 -0800876
James E. Blaira7f51ca2017-02-07 16:01:26 -0800877 def __getattr__(self, name):
878 v = self.__dict__.get(name)
879 if v is None:
James E. Blairaf8b2082017-10-03 15:38:27 -0700880 return self.attributes[name]
James E. Blaira7f51ca2017-02-07 16:01:26 -0800881 return v
882
883 def _get(self, name):
884 return self.__dict__.get(name)
885
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100886 def getSafeAttributes(self):
887 return Attributes(name=self.name)
888
James E. Blaira7f51ca2017-02-07 16:01:26 -0800889 def setRun(self):
890 if not self.run:
891 self.run = self.implied_run
892
James E. Blair5fc81922017-07-12 13:19:37 -0700893 def addRoles(self, roles):
James E. Blairbb94dfa2017-07-11 07:45:19 -0700894 newroles = []
895 # Start with a copy of the existing roles, but if any of them
896 # are implicit roles which are identified as explicit in the
897 # new roles list, replace them with the explicit version.
898 changed = False
899 for existing_role in self.roles:
900 if existing_role in roles:
901 new_role = roles[roles.index(existing_role)]
902 else:
903 new_role = None
904 if (new_role and
905 isinstance(new_role, ZuulRole) and
906 isinstance(existing_role, ZuulRole) and
907 existing_role.implicit and not new_role.implicit):
908 newroles.append(new_role)
909 changed = True
910 else:
911 newroles.append(existing_role)
912 # Now add the new roles.
James E. Blair4eec8282017-07-12 17:33:26 -0700913 for role in reversed(roles):
James E. Blair5fc81922017-07-12 13:19:37 -0700914 if role not in newroles:
James E. Blair4eec8282017-07-12 17:33:26 -0700915 newroles.insert(0, role)
James E. Blairbb94dfa2017-07-11 07:45:19 -0700916 changed = True
917 if changed:
918 self.roles = tuple(newroles)
James E. Blair5fc81922017-07-12 13:19:37 -0700919
James E. Blaire74f5712017-09-29 15:14:31 -0700920 def setBranchMatcher(self, branches):
921 # Set the branch matcher to match any of the supplied branches
922 matchers = []
923 for branch in branches:
924 matchers.append(change_matcher.BranchMatcher(branch))
925 self.branch_matcher = change_matcher.MatchAny(matchers)
926
James E. Blair490cf042017-02-24 23:07:21 -0500927 def updateVariables(self, other_vars):
James E. Blairaf8b2082017-10-03 15:38:27 -0700928 v = copy.deepcopy(self.variables)
James E. Blair490cf042017-02-24 23:07:21 -0500929 Job._deepUpdate(v, other_vars)
930 self.variables = v
931
James E. Blair912322f2017-05-23 13:11:25 -0700932 def updateProjects(self, other_projects):
James E. Blairaf8b2082017-10-03 15:38:27 -0700933 required_projects = self.required_projects.copy()
934 required_projects.update(other_projects)
James E. Blair912322f2017-05-23 13:11:25 -0700935 self.required_projects = required_projects
James E. Blair27f3dfc2017-05-23 13:07:28 -0700936
James E. Blair490cf042017-02-24 23:07:21 -0500937 @staticmethod
938 def _deepUpdate(a, b):
939 # Merge nested dictionaries if possible, otherwise, overwrite
940 # the value in 'a' with the value in 'b'.
941 for k, bv in b.items():
942 av = a.get(k)
943 if isinstance(av, dict) and isinstance(bv, dict):
944 Job._deepUpdate(av, bv)
945 else:
946 a[k] = bv
947
James E. Blaira7f51ca2017-02-07 16:01:26 -0800948 def inheritFrom(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800949 """Copy the inheritable attributes which have been set on the other
950 job to this job."""
James E. Blaira7f51ca2017-02-07 16:01:26 -0800951 if not isinstance(other, Job):
952 raise Exception("Job unable to inherit from %s" % (other,))
953
Tobias Henkel83167622017-06-30 19:45:03 +0200954 if other.final:
955 raise Exception("Unable to inherit from final job %s" %
956 (repr(other),))
957
James E. Blaira7f51ca2017-02-07 16:01:26 -0800958 # copy all attributes
959 for k in self.inheritable_attributes:
James E. Blair892cca62017-08-09 11:36:58 -0700960 if (other._get(k) is not None):
James E. Blairaf8b2082017-10-03 15:38:27 -0700961 setattr(self, k, getattr(other, k))
James E. Blaira7f51ca2017-02-07 16:01:26 -0800962
963 msg = 'inherit from %s' % (repr(other),)
964 self.inheritance_path = other.inheritance_path + (msg,)
965
966 def copy(self):
967 job = Job(self.name)
968 for k in self.attributes:
969 if self._get(k) is not None:
970 setattr(job, k, copy.deepcopy(self._get(k)))
971 return job
972
973 def applyVariant(self, other):
974 """Copy the attributes which have been set on the other job to this
975 job."""
James E. Blair83005782015-12-11 14:46:03 -0800976
977 if not isinstance(other, Job):
978 raise Exception("Job unable to inherit from %s" % (other,))
James E. Blaira7f51ca2017-02-07 16:01:26 -0800979
980 for k in self.execution_attributes:
981 if (other._get(k) is not None and
982 k not in set(['final'])):
983 if self.final:
984 raise Exception("Unable to modify final job %s attribute "
985 "%s=%s with variant %s" % (
986 repr(self), k, other._get(k),
987 repr(other)))
James E. Blair27f3dfc2017-05-23 13:07:28 -0700988 if k not in set(['pre_run', 'post_run', 'roles', 'variables',
James E. Blair912322f2017-05-23 13:11:25 -0700989 'required_projects']):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800990 setattr(self, k, copy.deepcopy(other._get(k)))
991
992 # Don't set final above so that we don't trip an error halfway
993 # through assignment.
994 if other.final != self.attributes['final']:
995 self.final = other.final
996
997 if other._get('pre_run') is not None:
998 self.pre_run = self.pre_run + other.pre_run
999 if other._get('post_run') is not None:
1000 self.post_run = other.post_run + self.post_run
James E. Blair5ac93842017-01-20 06:47:34 -08001001 if other._get('roles') is not None:
James E. Blair5fc81922017-07-12 13:19:37 -07001002 self.addRoles(other.roles)
James E. Blair490cf042017-02-24 23:07:21 -05001003 if other._get('variables') is not None:
1004 self.updateVariables(other.variables)
James E. Blair912322f2017-05-23 13:11:25 -07001005 if other._get('required_projects') is not None:
1006 self.updateProjects(other.required_projects)
James E. Blaira7f51ca2017-02-07 16:01:26 -08001007
1008 for k in self.context_attributes:
1009 if (other._get(k) is not None and
1010 k not in set(['tags'])):
1011 setattr(self, k, copy.deepcopy(other._get(k)))
1012
1013 if other._get('tags') is not None:
1014 self.tags = self.tags.union(other.tags)
1015
1016 msg = 'apply variant %s' % (repr(other),)
1017 self.inheritance_path = self.inheritance_path + (msg,)
James E. Blairee743612012-05-29 14:49:32 -07001018
James E. Blaire421a232012-07-25 16:59:21 -07001019 def changeMatches(self, change):
James E. Blair83005782015-12-11 14:46:03 -08001020 if self.branch_matcher and not self.branch_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -08001021 return False
1022
James E. Blair83005782015-12-11 14:46:03 -08001023 if self.file_matcher and not self.file_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -08001024 return False
1025
James E. Blair83005782015-12-11 14:46:03 -08001026 # NB: This is a negative match.
1027 if (self.irrelevant_file_matcher and
1028 self.irrelevant_file_matcher.matches(change)):
Maru Newby3fe5f852015-01-13 04:22:14 +00001029 return False
1030
James E. Blair70c71582013-03-06 08:50:50 -08001031 return True
James E. Blaire5a847f2012-07-10 15:29:14 -07001032
James E. Blair1e8dd892012-05-30 09:15:05 -07001033
James E. Blair912322f2017-05-23 13:11:25 -07001034class JobProject(object):
James E. Blair27f3dfc2017-05-23 13:07:28 -07001035 """ A reference to a project from a job. """
1036
1037 def __init__(self, project_name, override_branch=None):
1038 self.project_name = project_name
1039 self.override_branch = override_branch
1040
1041
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001042class JobList(object):
1043 """ A list of jobs in a project's pipeline. """
Monty Taylora42a55b2016-07-29 07:53:33 -07001044
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001045 def __init__(self):
1046 self.jobs = OrderedDict() # job.name -> [job, ...]
James E. Blair12748b52017-02-07 17:17:53 -08001047
James E. Blairee743612012-05-29 14:49:32 -07001048 def addJob(self, job):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001049 if job.name in self.jobs:
1050 self.jobs[job.name].append(job)
1051 else:
1052 self.jobs[job.name] = [job]
James E. Blairee743612012-05-29 14:49:32 -07001053
James E. Blaire74f5712017-09-29 15:14:31 -07001054 def inheritFrom(self, other, implied_branch):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001055 for jobname, jobs in other.jobs.items():
James E. Blaire74f5712017-09-29 15:14:31 -07001056 joblist = self.jobs.setdefault(jobname, [])
1057 for job in jobs:
1058 if not job.branch_matcher and implied_branch:
1059 job = job.copy()
1060 job.setBranchMatcher([implied_branch])
1061 joblist.append(job)
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001062
1063
1064class JobGraph(object):
1065 """ A JobGraph represents the dependency graph between Job."""
1066
1067 def __init__(self):
1068 self.jobs = OrderedDict() # job_name -> Job
1069 self._dependencies = {} # dependent_job_name -> set(parent_job_names)
1070
1071 def __repr__(self):
1072 return '<JobGraph %s>' % (self.jobs)
1073
1074 def addJob(self, job):
1075 # A graph must be created after the job list is frozen,
1076 # therefore we should only get one job with the same name.
1077 if job.name in self.jobs:
1078 raise Exception("Job %s already added" % (job.name,))
1079 self.jobs[job.name] = job
1080 # Append the dependency information
1081 self._dependencies.setdefault(job.name, set())
1082 try:
1083 for dependency in job.dependencies:
1084 # Make sure a circular dependency is never created
1085 ancestor_jobs = self._getParentJobNamesRecursively(
1086 dependency, soft=True)
1087 ancestor_jobs.add(dependency)
1088 if any((job.name == anc_job) for anc_job in ancestor_jobs):
1089 raise Exception("Dependency cycle detected in job %s" %
1090 (job.name,))
1091 self._dependencies[job.name].add(dependency)
1092 except Exception:
1093 del self.jobs[job.name]
1094 del self._dependencies[job.name]
1095 raise
1096
1097 def getJobs(self):
Clint Byruma4471d12017-05-10 20:57:40 -04001098 return list(self.jobs.values()) # Report in the order of layout cfg
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001099
1100 def _getDirectDependentJobs(self, parent_job):
1101 ret = set()
1102 for dependent_name, parent_names in self._dependencies.items():
1103 if parent_job in parent_names:
1104 ret.add(dependent_name)
1105 return ret
1106
1107 def getDependentJobsRecursively(self, parent_job):
1108 all_dependent_jobs = set()
1109 jobs_to_iterate = set([parent_job])
1110 while len(jobs_to_iterate) > 0:
1111 current_job = jobs_to_iterate.pop()
1112 current_dependent_jobs = self._getDirectDependentJobs(current_job)
1113 new_dependent_jobs = current_dependent_jobs - all_dependent_jobs
1114 jobs_to_iterate |= new_dependent_jobs
1115 all_dependent_jobs |= new_dependent_jobs
1116 return [self.jobs[name] for name in all_dependent_jobs]
1117
1118 def getParentJobsRecursively(self, dependent_job):
1119 return [self.jobs[name] for name in
1120 self._getParentJobNamesRecursively(dependent_job)]
1121
1122 def _getParentJobNamesRecursively(self, dependent_job, soft=False):
1123 all_parent_jobs = set()
1124 jobs_to_iterate = set([dependent_job])
1125 while len(jobs_to_iterate) > 0:
1126 current_job = jobs_to_iterate.pop()
1127 current_parent_jobs = self._dependencies.get(current_job)
1128 if current_parent_jobs is None:
1129 if soft:
1130 current_parent_jobs = set()
1131 else:
1132 raise Exception("Dependent job %s not found: " %
1133 (dependent_job,))
1134 new_parent_jobs = current_parent_jobs - all_parent_jobs
1135 jobs_to_iterate |= new_parent_jobs
1136 all_parent_jobs |= new_parent_jobs
1137 return all_parent_jobs
James E. Blairb97ed802015-12-21 15:55:35 -08001138
James E. Blair1e8dd892012-05-30 09:15:05 -07001139
James E. Blair4aea70c2012-07-26 14:23:24 -07001140class Build(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001141 """A Build is an instance of a single execution of a Job.
1142
1143 While a Job describes what to run, a Build describes an actual
1144 execution of that Job. Each build is associated with exactly one
1145 Job (related builds are grouped together in a BuildSet).
1146 """
Monty Taylora42a55b2016-07-29 07:53:33 -07001147
James E. Blair4aea70c2012-07-26 14:23:24 -07001148 def __init__(self, job, uuid):
1149 self.job = job
1150 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -07001151 self.url = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001152 self.result = None
James E. Blair196f61a2017-06-30 15:42:29 -07001153 self.result_data = {}
James E. Blair6f699732017-07-18 14:19:11 -07001154 self.error_detail = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001155 self.build_set = None
Paul Belanger174a8272017-03-14 13:20:10 -04001156 self.execute_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -08001157 self.start_time = None
1158 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -07001159 self.estimated_time = None
James E. Blair0aac4872013-08-23 14:02:38 -07001160 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -07001161 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -07001162 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +08001163 self.worker = Worker()
Timothy Chavezb2332082015-08-07 20:08:04 -05001164 self.node_labels = []
1165 self.node_name = None
James E. Blairee743612012-05-29 14:49:32 -07001166
1167 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +08001168 return ('<Build %s of %s on %s>' %
1169 (self.uuid, self.job.name, self.worker))
1170
James E. Blair3a098dd2017-10-04 14:37:29 -07001171 @property
1172 def pipeline(self):
1173 return self.build_set.item.pipeline
1174
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001175 def getSafeAttributes(self):
James E. Blair196f61a2017-06-30 15:42:29 -07001176 return Attributes(uuid=self.uuid,
1177 result=self.result,
James E. Blair6f699732017-07-18 14:19:11 -07001178 error_detail=self.error_detail,
James E. Blair196f61a2017-06-30 15:42:29 -07001179 result_data=self.result_data)
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001180
Joshua Heskethba8776a2014-01-12 14:35:40 +08001181
1182class Worker(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001183 """Information about the specific worker executing a Build."""
Joshua Heskethba8776a2014-01-12 14:35:40 +08001184 def __init__(self):
1185 self.name = "Unknown"
1186 self.hostname = None
Monty Taylor0dbe1592017-06-11 10:57:27 -05001187 self.log_port = None
Joshua Heskethba8776a2014-01-12 14:35:40 +08001188
1189 def updateFromData(self, data):
1190 """Update worker information if contained in the WORK_DATA response."""
1191 self.name = data.get('worker_name', self.name)
1192 self.hostname = data.get('worker_hostname', self.hostname)
Monty Taylor0dbe1592017-06-11 10:57:27 -05001193 self.log_port = data.get('worker_log_port', self.log_port)
Joshua Heskethba8776a2014-01-12 14:35:40 +08001194
1195 def __repr__(self):
1196 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -07001197
James E. Blair1e8dd892012-05-30 09:15:05 -07001198
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001199class RepoFiles(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001200 """RepoFiles holds config-file content for per-project job config.
1201
1202 When Zuul asks a merger to prepare a future multiple-repo state
1203 and collect Zuul configuration files so that we can dynamically
1204 load our configuration, this class provides cached access to that
1205 data for use by the Change which updated the config files and any
1206 changes that follow it in a ChangeQueue.
1207
1208 It is attached to a BuildSet since the content of Zuul
1209 configuration files can change with each new BuildSet.
1210 """
1211
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001212 def __init__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001213 self.connections = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001214
1215 def __repr__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001216 return '<RepoFiles %s>' % self.connections
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001217
1218 def setFiles(self, items):
James E. Blair2a535672017-04-27 12:03:15 -07001219 self.hostnames = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001220 for item in items:
James E. Blair2a535672017-04-27 12:03:15 -07001221 connection = self.connections.setdefault(
1222 item['connection'], {})
1223 project = connection.setdefault(item['project'], {})
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001224 branch = project.setdefault(item['branch'], {})
1225 branch.update(item['files'])
1226
James E. Blair2a535672017-04-27 12:03:15 -07001227 def getFile(self, connection_name, project_name, branch, fn):
1228 host = self.connections.get(connection_name, {})
1229 return host.get(project_name, {}).get(branch, {}).get(fn)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001230
1231
James E. Blair7e530ad2012-07-03 16:12:28 -07001232class BuildSet(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001233 """A collection of Builds for one specific potential future repository
1234 state.
Monty Taylora42a55b2016-07-29 07:53:33 -07001235
Paul Belanger174a8272017-03-14 13:20:10 -04001236 When Zuul executes Builds for a change, it creates a Build to
James E. Blaird4ade8c2017-02-19 15:25:46 -08001237 represent each execution of each job and a BuildSet to keep track
Paul Belanger174a8272017-03-14 13:20:10 -04001238 of all the Builds running for that Change. When Zuul re-executes
James E. Blaird4ade8c2017-02-19 15:25:46 -08001239 Builds for a Change with a different configuration, all of the
1240 running Builds in the BuildSet for that change are aborted, and a
1241 new BuildSet is created to hold the Builds for the Jobs being
1242 run with the new configuration.
1243
1244 A BuildSet also holds the UUID used to produce the Zuul Ref that
1245 builders check out.
1246
Monty Taylora42a55b2016-07-29 07:53:33 -07001247 """
James E. Blair4076e2b2014-01-28 12:42:20 -08001248 # Merge states:
1249 NEW = 1
1250 PENDING = 2
1251 COMPLETE = 3
1252
Antoine Musso9b229282014-08-18 23:45:43 +02001253 states_map = {
1254 1: 'NEW',
1255 2: 'PENDING',
1256 3: 'COMPLETE',
1257 }
1258
James E. Blairfee8d652013-06-07 08:57:52 -07001259 def __init__(self, item):
1260 self.item = item
James E. Blair7e530ad2012-07-03 16:12:28 -07001261 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -07001262 self.result = None
1263 self.next_build_set = None
1264 self.previous_build_set = None
Jamie Lennox3f16de52017-05-09 14:24:11 +10001265 self.uuid = None
James E. Blair81515ad2012-10-01 18:29:08 -07001266 self.commit = None
James E. Blair1960d682017-04-28 15:44:14 -07001267 self.dependent_items = None
1268 self.merger_items = None
James E. Blair973721f2012-08-15 10:19:43 -07001269 self.unable_to_merge = False
James E. Blaire53250c2017-03-01 14:34:36 -08001270 self.config_error = None # None or an error message string.
James E. Blair972e3c72013-08-29 12:04:55 -07001271 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -08001272 self.merge_state = self.NEW
James E. Blair0eaad552016-09-02 12:09:54 -07001273 self.nodesets = {} # job -> nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001274 self.node_requests = {} # job -> reqs
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001275 self.files = RepoFiles()
James E. Blair1960d682017-04-28 15:44:14 -07001276 self.repo_state = {}
Paul Belanger71d98172016-11-08 10:56:31 -05001277 self.tries = {}
James E. Blair7e530ad2012-07-03 16:12:28 -07001278
Jamie Lennox3f16de52017-05-09 14:24:11 +10001279 @property
1280 def ref(self):
1281 # NOTE(jamielennox): The concept of buildset ref is to be removed and a
1282 # buildset UUID identifier available instead. Currently the ref is
1283 # checked to see if the BuildSet has been configured.
1284 return 'Z' + self.uuid if self.uuid else None
1285
Antoine Musso9b229282014-08-18 23:45:43 +02001286 def __repr__(self):
1287 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
1288 self.item,
1289 len(self.builds),
1290 self.getStateName(self.merge_state))
1291
James E. Blair4886cc12012-07-18 15:39:41 -07001292 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -07001293 # The change isn't enqueued until after it's created
1294 # so we don't know what the other changes ahead will be
1295 # until jobs start.
James E. Blair1960d682017-04-28 15:44:14 -07001296 if self.dependent_items is None:
1297 items = []
James E. Blairfee8d652013-06-07 08:57:52 -07001298 next_item = self.item.item_ahead
1299 while next_item:
James E. Blair1960d682017-04-28 15:44:14 -07001300 items.append(next_item)
James E. Blairfee8d652013-06-07 08:57:52 -07001301 next_item = next_item.item_ahead
James E. Blair1960d682017-04-28 15:44:14 -07001302 self.dependent_items = items
Jamie Lennox3f16de52017-05-09 14:24:11 +10001303 if not self.uuid:
1304 self.uuid = uuid4().hex
James E. Blair1960d682017-04-28 15:44:14 -07001305 if self.merger_items is None:
1306 items = [self.item] + self.dependent_items
1307 items.reverse()
1308 self.merger_items = [i.makeMergerItem() for i in items]
James E. Blair4886cc12012-07-18 15:39:41 -07001309
Antoine Musso9b229282014-08-18 23:45:43 +02001310 def getStateName(self, state_num):
1311 return self.states_map.get(
1312 state_num, 'UNKNOWN (%s)' % state_num)
1313
James E. Blair4886cc12012-07-18 15:39:41 -07001314 def addBuild(self, build):
1315 self.builds[build.job.name] = build
Paul Belanger71d98172016-11-08 10:56:31 -05001316 if build.job.name not in self.tries:
James E. Blair5aed1112016-12-15 09:18:05 -08001317 self.tries[build.job.name] = 1
James E. Blair4886cc12012-07-18 15:39:41 -07001318 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -07001319
James E. Blair4a28a882013-08-23 15:17:33 -07001320 def removeBuild(self, build):
Paul Belanger71d98172016-11-08 10:56:31 -05001321 self.tries[build.job.name] += 1
James E. Blair4a28a882013-08-23 15:17:33 -07001322 del self.builds[build.job.name]
1323
James E. Blair7e530ad2012-07-03 16:12:28 -07001324 def getBuild(self, job_name):
1325 return self.builds.get(job_name)
1326
James E. Blair11700c32012-07-05 17:50:05 -07001327 def getBuilds(self):
Clint Byrum1d0c7d12017-05-10 19:40:53 -07001328 keys = list(self.builds.keys())
James E. Blair11700c32012-07-05 17:50:05 -07001329 keys.sort()
1330 return [self.builds.get(x) for x in keys]
1331
James E. Blair0eaad552016-09-02 12:09:54 -07001332 def getJobNodeSet(self, job_name):
1333 # Return None if not provisioned; empty NodeSet if no nodes
1334 # required
1335 return self.nodesets.get(job_name)
James E. Blair8d692392016-04-08 17:47:58 -07001336
James E. Blaire18d4602017-01-05 11:17:28 -08001337 def removeJobNodeSet(self, job_name):
1338 if job_name not in self.nodesets:
1339 raise Exception("No job set for %s" % (job_name))
1340 del self.nodesets[job_name]
1341
James E. Blair8d692392016-04-08 17:47:58 -07001342 def setJobNodeRequest(self, job_name, req):
1343 if job_name in self.node_requests:
1344 raise Exception("Prior node request for %s" % (job_name))
1345 self.node_requests[job_name] = req
1346
1347 def getJobNodeRequest(self, job_name):
1348 return self.node_requests.get(job_name)
1349
James E. Blair0eaad552016-09-02 12:09:54 -07001350 def jobNodeRequestComplete(self, job_name, req, nodeset):
1351 if job_name in self.nodesets:
James E. Blair8d692392016-04-08 17:47:58 -07001352 raise Exception("Prior node request for %s" % (job_name))
James E. Blair0eaad552016-09-02 12:09:54 -07001353 self.nodesets[job_name] = nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001354 del self.node_requests[job_name]
1355
Paul Belanger71d98172016-11-08 10:56:31 -05001356 def getTries(self, job_name):
Clint Byrum804073b2017-05-10 17:47:45 -04001357 return self.tries.get(job_name, 0)
Paul Belanger71d98172016-11-08 10:56:31 -05001358
James E. Blair0ffa0102017-03-30 13:11:33 -07001359 def getMergeMode(self):
James E. Blair1960d682017-04-28 15:44:14 -07001360 # We may be called before this build set has a shadow layout
1361 # (ie, we are called to perform the merge to create that
1362 # layout). It's possible that the change we are merging will
1363 # update the merge-mode for the project, but there's not much
1364 # we can do about that here. Instead, do the best we can by
1365 # using the nearest shadow layout to determine the merge mode,
1366 # or if that fails, the current live layout, or if that fails,
1367 # use the default: merge-resolve.
1368 item = self.item
1369 layout = None
1370 while item:
James E. Blair29a24fd2017-10-02 15:04:56 -07001371 layout = item.layout
James E. Blair1960d682017-04-28 15:44:14 -07001372 if layout:
1373 break
1374 item = item.item_ahead
1375 if not layout:
1376 layout = self.item.pipeline.layout
1377 if layout:
James E. Blair0ffa0102017-03-30 13:11:33 -07001378 project = self.item.change.project
James E. Blair1960d682017-04-28 15:44:14 -07001379 project_config = layout.project_configs.get(
James E. Blair0ffa0102017-03-30 13:11:33 -07001380 project.canonical_name)
1381 if project_config:
1382 return project_config.merge_mode
1383 return MERGER_MERGE_RESOLVE
Adam Gandelman8bd57102016-12-02 12:58:42 -08001384
Jamie Lennox3f16de52017-05-09 14:24:11 +10001385 def getSafeAttributes(self):
1386 return Attributes(uuid=self.uuid)
1387
James E. Blair7e530ad2012-07-03 16:12:28 -07001388
James E. Blairfee8d652013-06-07 08:57:52 -07001389class QueueItem(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001390 """Represents the position of a Change in a ChangeQueue.
1391
1392 All Changes are enqueued into ChangeQueue in a QueueItem. The QueueItem
1393 holds the current `BuildSet` as well as all previous `BuildSets` that were
1394 produced for this `QueueItem`.
1395 """
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001396 log = logging.getLogger("zuul.QueueItem")
James E. Blair32663402012-06-01 10:04:18 -07001397
James E. Blairbfb8e042014-12-30 17:01:44 -08001398 def __init__(self, queue, change):
1399 self.pipeline = queue.pipeline
1400 self.queue = queue
Monty Taylor55d0f562017-05-17 14:30:21 -05001401 self.change = change # a ref
James E. Blair7e530ad2012-07-03 16:12:28 -07001402 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -07001403 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -07001404 self.current_build_set = BuildSet(self)
1405 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -07001406 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -07001407 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -08001408 self.enqueue_time = None
1409 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -07001410 self.reported = False
Tobias Henkel9842bd72017-05-16 13:40:03 +02001411 self.reported_start = False
1412 self.quiet = False
James E. Blairbfb8e042014-12-30 17:01:44 -08001413 self.active = False # Whether an item is within an active window
1414 self.live = True # Whether an item is intended to be processed at all
James E. Blair29a24fd2017-10-02 15:04:56 -07001415 self.layout = None
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001416 self.job_graph = None
James E. Blaire5a847f2012-07-10 15:29:14 -07001417
James E. Blair972e3c72013-08-29 12:04:55 -07001418 def __repr__(self):
1419 if self.pipeline:
1420 pipeline = self.pipeline.name
1421 else:
1422 pipeline = None
1423 return '<QueueItem 0x%x for %s in %s>' % (
1424 id(self), self.change, pipeline)
1425
James E. Blairee743612012-05-29 14:49:32 -07001426 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -07001427 old = self.current_build_set
1428 self.current_build_set.result = 'CANCELED'
1429 self.current_build_set = BuildSet(self)
1430 old.next_build_set = self.current_build_set
1431 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -07001432 self.build_sets.append(self.current_build_set)
James E. Blair29a24fd2017-10-02 15:04:56 -07001433 self.layout = None
James E. Blairc9455002017-09-06 09:22:19 -07001434 self.job_graph = None
James E. Blairee743612012-05-29 14:49:32 -07001435
1436 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -07001437 self.current_build_set.addBuild(build)
James E. Blairee743612012-05-29 14:49:32 -07001438
James E. Blair4a28a882013-08-23 15:17:33 -07001439 def removeBuild(self, build):
1440 self.current_build_set.removeBuild(build)
1441
James E. Blairfee8d652013-06-07 08:57:52 -07001442 def setReportedResult(self, result):
1443 self.current_build_set.result = result
1444
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001445 def freezeJobGraph(self):
James E. Blair83005782015-12-11 14:46:03 -08001446 """Find or create actual matching jobs for this item's change and
1447 store the resulting job tree."""
James E. Blair29a24fd2017-10-02 15:04:56 -07001448 job_graph = self.layout.createJobGraph(self)
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001449 for job in job_graph.getJobs():
1450 # Ensure that each jobs's dependencies are fully
1451 # accessible. This will raise an exception if not.
1452 job_graph.getParentJobsRecursively(job.name)
1453 self.job_graph = job_graph
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001454
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001455 def hasJobGraph(self):
1456 """Returns True if the item has a job graph."""
1457 return self.job_graph is not None
James E. Blair83005782015-12-11 14:46:03 -08001458
1459 def getJobs(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001460 if not self.live or not self.job_graph:
James E. Blair83005782015-12-11 14:46:03 -08001461 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001462 return self.job_graph.getJobs()
1463
1464 def getJob(self, name):
1465 if not self.job_graph:
1466 return None
1467 return self.job_graph.jobs.get(name)
James E. Blair83005782015-12-11 14:46:03 -08001468
James E. Blairdbfd3282016-07-21 10:46:19 -07001469 def haveAllJobsStarted(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001470 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001471 return False
1472 for job in self.getJobs():
1473 build = self.current_build_set.getBuild(job.name)
1474 if not build or not build.start_time:
1475 return False
1476 return True
1477
1478 def areAllJobsComplete(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001479 if (self.current_build_set.config_error or
1480 self.current_build_set.unable_to_merge):
1481 return True
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001482 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001483 return False
1484 for job in self.getJobs():
1485 build = self.current_build_set.getBuild(job.name)
1486 if not build or not build.result:
1487 return False
1488 return True
1489
1490 def didAllJobsSucceed(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001491 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001492 return False
1493 for job in self.getJobs():
1494 if not job.voting:
1495 continue
1496 build = self.current_build_set.getBuild(job.name)
1497 if not build:
1498 return False
1499 if build.result != 'SUCCESS':
1500 return False
1501 return True
1502
1503 def didAnyJobFail(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001504 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001505 return False
1506 for job in self.getJobs():
1507 if not job.voting:
1508 continue
1509 build = self.current_build_set.getBuild(job.name)
1510 if build and build.result and (build.result != 'SUCCESS'):
1511 return True
1512 return False
1513
1514 def didMergerFail(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001515 return self.current_build_set.unable_to_merge
1516
1517 def getConfigError(self):
1518 return self.current_build_set.config_error
James E. Blairdbfd3282016-07-21 10:46:19 -07001519
James E. Blair0d3e83b2017-06-05 13:51:57 -07001520 def wasDequeuedNeedingChange(self):
1521 return self.dequeued_needing_change
1522
James E. Blairdbfd3282016-07-21 10:46:19 -07001523 def isHoldingFollowingChanges(self):
1524 if not self.live:
1525 return False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001526 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001527 return False
1528 for job in self.getJobs():
1529 if not job.hold_following_changes:
1530 continue
1531 build = self.current_build_set.getBuild(job.name)
1532 if not build:
1533 return True
1534 if build.result != 'SUCCESS':
1535 return True
1536
1537 if not self.item_ahead:
1538 return False
1539 return self.item_ahead.isHoldingFollowingChanges()
1540
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001541 def findJobsToRun(self, semaphore_handler):
James E. Blairdbfd3282016-07-21 10:46:19 -07001542 torun = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001543 if not self.live:
1544 return []
1545 if not self.job_graph:
1546 return []
James E. Blair791b5392016-08-03 11:25:56 -07001547 if self.item_ahead:
1548 # Only run jobs if any 'hold' jobs on the change ahead
1549 # have completed successfully.
1550 if self.item_ahead.isHoldingFollowingChanges():
1551 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001552
1553 successful_job_names = set()
1554 jobs_not_started = set()
1555 for job in self.job_graph.getJobs():
1556 build = self.current_build_set.getBuild(job.name)
1557 if build:
1558 if build.result == 'SUCCESS':
1559 successful_job_names.add(job.name)
1560 else:
1561 jobs_not_started.add(job)
1562
1563 # Attempt to request nodes for jobs in the order jobs appear
1564 # in configuration.
1565 for job in self.job_graph.getJobs():
1566 if job not in jobs_not_started:
1567 continue
1568 all_parent_jobs_successful = True
1569 for parent_job in self.job_graph.getParentJobsRecursively(
1570 job.name):
1571 if parent_job.name not in successful_job_names:
1572 all_parent_jobs_successful = False
1573 break
1574 if all_parent_jobs_successful:
1575 nodeset = self.current_build_set.getJobNodeSet(job.name)
1576 if nodeset is None:
1577 # The nodes for this job are not ready, skip
1578 # it for now.
James E. Blairdbfd3282016-07-21 10:46:19 -07001579 continue
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001580 if semaphore_handler.acquire(self, job):
1581 # If this job needs a semaphore, either acquire it or
1582 # make sure that we have it before running the job.
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001583 torun.append(job)
James E. Blairdbfd3282016-07-21 10:46:19 -07001584 return torun
1585
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001586 def findJobsToRequest(self):
James E. Blair6ab79e02017-01-06 10:10:17 -08001587 build_set = self.current_build_set
James E. Blairdbfd3282016-07-21 10:46:19 -07001588 toreq = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001589 if not self.live:
1590 return []
1591 if not self.job_graph:
1592 return []
James E. Blair6ab79e02017-01-06 10:10:17 -08001593 if self.item_ahead:
1594 if self.item_ahead.isHoldingFollowingChanges():
1595 return []
James E. Blairdbfd3282016-07-21 10:46:19 -07001596
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001597 successful_job_names = set()
1598 jobs_not_requested = set()
1599 for job in self.job_graph.getJobs():
1600 build = build_set.getBuild(job.name)
1601 if build and build.result == 'SUCCESS':
1602 successful_job_names.add(job.name)
1603 else:
1604 nodeset = build_set.getJobNodeSet(job.name)
1605 if nodeset is None:
1606 req = build_set.getJobNodeRequest(job.name)
1607 if req is None:
1608 jobs_not_requested.add(job)
1609
1610 # Attempt to request nodes for jobs in the order jobs appear
1611 # in configuration.
1612 for job in self.job_graph.getJobs():
1613 if job not in jobs_not_requested:
1614 continue
1615 all_parent_jobs_successful = True
1616 for parent_job in self.job_graph.getParentJobsRecursively(
1617 job.name):
1618 if parent_job.name not in successful_job_names:
1619 all_parent_jobs_successful = False
1620 break
1621 if all_parent_jobs_successful:
1622 toreq.append(job)
1623 return toreq
James E. Blairdbfd3282016-07-21 10:46:19 -07001624
1625 def setResult(self, build):
1626 if build.retry:
1627 self.removeBuild(build)
1628 elif build.result != 'SUCCESS':
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001629 for job in self.job_graph.getDependentJobsRecursively(
1630 build.job.name):
James E. Blairdbfd3282016-07-21 10:46:19 -07001631 fakebuild = Build(job, None)
1632 fakebuild.result = 'SKIPPED'
1633 self.addBuild(fakebuild)
1634
James E. Blair6ab79e02017-01-06 10:10:17 -08001635 def setNodeRequestFailure(self, job):
1636 fakebuild = Build(job, None)
1637 self.addBuild(fakebuild)
1638 fakebuild.result = 'NODE_FAILURE'
1639 self.setResult(fakebuild)
1640
James E. Blairdbfd3282016-07-21 10:46:19 -07001641 def setDequeuedNeedingChange(self):
1642 self.dequeued_needing_change = True
1643 self._setAllJobsSkipped()
1644
1645 def setUnableToMerge(self):
1646 self.current_build_set.unable_to_merge = True
1647 self._setAllJobsSkipped()
1648
James E. Blaire53250c2017-03-01 14:34:36 -08001649 def setConfigError(self, error):
1650 self.current_build_set.config_error = error
1651 self._setAllJobsSkipped()
1652
James E. Blairdbfd3282016-07-21 10:46:19 -07001653 def _setAllJobsSkipped(self):
1654 for job in self.getJobs():
1655 fakebuild = Build(job, None)
1656 fakebuild.result = 'SKIPPED'
1657 self.addBuild(fakebuild)
1658
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001659 def formatUrlPattern(self, url_pattern, job=None, build=None):
1660 url = None
1661 # Produce safe versions of objects which may be useful in
1662 # result formatting, but don't allow users to crawl through
1663 # the entire data structure where they might be able to access
1664 # secrets, etc.
1665 safe_change = self.change.getSafeAttributes()
1666 safe_pipeline = self.pipeline.getSafeAttributes()
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001667 safe_tenant = self.pipeline.layout.tenant.getSafeAttributes()
Jamie Lennox3f16de52017-05-09 14:24:11 +10001668 safe_buildset = self.current_build_set.getSafeAttributes()
K Jonathan Harkera7f390c2017-06-02 12:31:22 -07001669 safe_job = job.getSafeAttributes() if job else {}
1670 safe_build = build.getSafeAttributes() if build else {}
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001671 try:
1672 url = url_pattern.format(change=safe_change,
1673 pipeline=safe_pipeline,
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001674 tenant=safe_tenant,
Jamie Lennox3f16de52017-05-09 14:24:11 +10001675 buildset=safe_buildset,
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001676 job=safe_job,
1677 build=safe_build)
1678 except KeyError as e:
1679 self.log.error("Error while formatting url for job %s: unknown "
1680 "key %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001681 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001682 except AttributeError as e:
1683 self.log.error("Error while formatting url for job %s: unknown "
1684 "attribute %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001685 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001686 except Exception:
1687 self.log.exception("Error while formatting url for job %s with "
1688 "pattern %s:" % (job, url_pattern))
1689
1690 return url
1691
James E. Blair800e7ff2017-03-17 16:06:52 -07001692 def formatJobResult(self, job):
James E. Blairb7273ef2016-04-19 08:58:51 -07001693 build = self.current_build_set.getBuild(job.name)
1694 result = build.result
James E. Blair800e7ff2017-03-17 16:06:52 -07001695 pattern = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001696 if result == 'SUCCESS':
1697 if job.success_message:
1698 result = job.success_message
James E. Blaira5dba232016-08-08 15:53:24 -07001699 if job.success_url:
1700 pattern = job.success_url
Tobias Henkel077f2f32017-05-30 20:16:46 +02001701 else:
James E. Blairb7273ef2016-04-19 08:58:51 -07001702 if job.failure_message:
1703 result = job.failure_message
James E. Blaira5dba232016-08-08 15:53:24 -07001704 if job.failure_url:
1705 pattern = job.failure_url
James E. Blair88e79c02017-07-07 13:36:54 -07001706 url = None # The final URL
1707 default_url = build.result_data.get('zuul', {}).get('log_url')
James E. Blairb7273ef2016-04-19 08:58:51 -07001708 if pattern:
James E. Blair88e79c02017-07-07 13:36:54 -07001709 job_url = self.formatUrlPattern(pattern, job, build)
1710 else:
1711 job_url = None
1712 try:
1713 if job_url:
1714 u = urllib.parse.urlparse(job_url)
1715 if u.scheme:
1716 # The job success or failure url is absolute, so it's
1717 # our final url.
1718 url = job_url
1719 else:
1720 # We have a relative job url. Combine it with our
1721 # default url.
1722 if default_url:
1723 url = urllib.parse.urljoin(default_url, job_url)
1724 except Exception:
1725 self.log.exception("Error while parsing url for job %s:"
1726 % (job,))
James E. Blairb7273ef2016-04-19 08:58:51 -07001727 if not url:
James E. Blair88e79c02017-07-07 13:36:54 -07001728 url = default_url or build.url or job.name
James E. Blairb7273ef2016-04-19 08:58:51 -07001729 return (result, url)
1730
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001731 def formatJSON(self, websocket_url=None):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001732 ret = {}
1733 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -08001734 ret['live'] = self.live
Monty Taylor55d0f562017-05-17 14:30:21 -05001735 if hasattr(self.change, 'url') and self.change.url is not None:
1736 ret['url'] = self.change.url
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001737 else:
1738 ret['url'] = None
Monty Taylor55d0f562017-05-17 14:30:21 -05001739 ret['id'] = self.change._id()
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001740 if self.item_ahead:
1741 ret['item_ahead'] = self.item_ahead.change._id()
1742 else:
1743 ret['item_ahead'] = None
1744 ret['items_behind'] = [i.change._id() for i in self.items_behind]
1745 ret['failing_reasons'] = self.current_build_set.failing_reasons
1746 ret['zuul_ref'] = self.current_build_set.ref
Monty Taylor55d0f562017-05-17 14:30:21 -05001747 if self.change.project:
1748 ret['project'] = self.change.project.name
Ramy Asselin07cc33c2015-06-12 14:06:34 -07001749 else:
1750 # For cross-project dependencies with the depends-on
1751 # project not known to zuul, the project is None
1752 # Set it to a static value
1753 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001754 ret['enqueue_time'] = int(self.enqueue_time * 1000)
1755 ret['jobs'] = []
Monty Taylor55d0f562017-05-17 14:30:21 -05001756 if hasattr(self.change, 'owner'):
1757 ret['owner'] = self.change.owner
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001758 else:
1759 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001760 max_remaining = 0
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001761 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001762 now = time.time()
1763 build = self.current_build_set.getBuild(job.name)
1764 elapsed = None
1765 remaining = None
1766 result = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001767 build_url = None
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001768 finger_url = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001769 report_url = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001770 worker = None
1771 if build:
1772 result = build.result
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001773 finger_url = build.url
1774 # TODO(tobiash): add support for custom web root
1775 urlformat = 'static/stream.html?' \
1776 'uuid={build.uuid}&' \
1777 'logfile=console.log'
1778 if websocket_url:
1779 urlformat += '&websocket_url={websocket_url}'
1780 build_url = urlformat.format(
1781 build=build, websocket_url=websocket_url)
James E. Blair800e7ff2017-03-17 16:06:52 -07001782 (unused, report_url) = self.formatJobResult(job)
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001783 if build.start_time:
1784 if build.end_time:
1785 elapsed = int((build.end_time -
1786 build.start_time) * 1000)
1787 remaining = 0
1788 else:
1789 elapsed = int((now - build.start_time) * 1000)
1790 if build.estimated_time:
1791 remaining = max(
1792 int(build.estimated_time * 1000) - elapsed,
1793 0)
1794 worker = {
1795 'name': build.worker.name,
1796 'hostname': build.worker.hostname,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001797 }
1798 if remaining and remaining > max_remaining:
1799 max_remaining = remaining
1800
1801 ret['jobs'].append({
1802 'name': job.name,
Tobias Henkel65639f82017-07-10 10:25:42 +02001803 'dependencies': list(job.dependencies),
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001804 'elapsed_time': elapsed,
1805 'remaining_time': remaining,
James E. Blairb7273ef2016-04-19 08:58:51 -07001806 'url': build_url,
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001807 'finger_url': finger_url,
James E. Blairb7273ef2016-04-19 08:58:51 -07001808 'report_url': report_url,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001809 'result': result,
1810 'voting': job.voting,
1811 'uuid': build.uuid if build else None,
Paul Belanger174a8272017-03-14 13:20:10 -04001812 'execute_time': build.execute_time if build else None,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001813 'start_time': build.start_time if build else None,
1814 'end_time': build.end_time if build else None,
1815 'estimated_time': build.estimated_time if build else None,
1816 'pipeline': build.pipeline.name if build else None,
1817 'canceled': build.canceled if build else None,
1818 'retry': build.retry if build else None,
Timothy Chavezb2332082015-08-07 20:08:04 -05001819 'node_labels': build.node_labels if build else [],
1820 'node_name': build.node_name if build else None,
1821 'worker': worker,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001822 })
1823
James E. Blairdbfd3282016-07-21 10:46:19 -07001824 if self.haveAllJobsStarted():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001825 ret['remaining_time'] = max_remaining
1826 else:
1827 ret['remaining_time'] = None
1828 return ret
1829
1830 def formatStatus(self, indent=0, html=False):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001831 indent_str = ' ' * indent
1832 ret = ''
Monty Taylor55d0f562017-05-17 14:30:21 -05001833 if html and getattr(self.change, 'url', None) is not None:
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001834 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
1835 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001836 self.change.project.name,
1837 self.change.url,
1838 self.change._id())
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001839 else:
1840 ret += '%sProject %s change %s based on %s\n' % (
1841 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001842 self.change.project.name,
1843 self.change._id(),
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001844 self.item_ahead)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001845 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001846 build = self.current_build_set.getBuild(job.name)
1847 if build:
1848 result = build.result
1849 else:
1850 result = None
1851 job_name = job.name
1852 if not job.voting:
1853 voting = ' (non-voting)'
1854 else:
1855 voting = ''
1856 if html:
1857 if build:
1858 url = build.url
1859 else:
1860 url = None
1861 if url is not None:
1862 job_name = '<a href="%s">%s</a>' % (url, job_name)
1863 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
1864 ret += '\n'
1865 return ret
1866
James E. Blaira04b0792017-04-27 09:59:06 -07001867 def makeMergerItem(self):
1868 # Create a dictionary with all info about the item needed by
1869 # the merger.
1870 number = None
1871 patchset = None
1872 oldrev = None
1873 newrev = None
James E. Blair21037782017-07-19 11:56:55 -07001874 branch = None
James E. Blaira04b0792017-04-27 09:59:06 -07001875 if hasattr(self.change, 'number'):
1876 number = self.change.number
1877 patchset = self.change.patchset
James E. Blair21037782017-07-19 11:56:55 -07001878 if hasattr(self.change, 'newrev'):
James E. Blaira04b0792017-04-27 09:59:06 -07001879 oldrev = self.change.oldrev
1880 newrev = self.change.newrev
James E. Blair21037782017-07-19 11:56:55 -07001881 if hasattr(self.change, 'branch'):
1882 branch = self.change.branch
1883
James E. Blaira04b0792017-04-27 09:59:06 -07001884 source = self.change.project.source
1885 connection_name = source.connection.connection_name
James E. Blair2a535672017-04-27 12:03:15 -07001886 project = self.change.project
James E. Blaira04b0792017-04-27 09:59:06 -07001887
James E. Blair2a535672017-04-27 12:03:15 -07001888 return dict(project=project.name,
1889 connection=connection_name,
James E. Blaira04b0792017-04-27 09:59:06 -07001890 merge_mode=self.current_build_set.getMergeMode(),
James E. Blair247cab72017-07-20 16:52:36 -07001891 ref=self.change.ref,
James E. Blaira04b0792017-04-27 09:59:06 -07001892 branch=branch,
James E. Blair247cab72017-07-20 16:52:36 -07001893 buildset_uuid=self.current_build_set.uuid,
James E. Blaira04b0792017-04-27 09:59:06 -07001894 number=number,
1895 patchset=patchset,
1896 oldrev=oldrev,
1897 newrev=newrev,
1898 )
1899
James E. Blairfee8d652013-06-07 08:57:52 -07001900
Clint Byrumf8cc9902017-03-22 22:38:25 -07001901class Ref(object):
1902 """An existing state of a Project."""
James E. Blairfee8d652013-06-07 08:57:52 -07001903
1904 def __init__(self, project):
1905 self.project = project
Clint Byrumf8cc9902017-03-22 22:38:25 -07001906 self.ref = None
1907 self.oldrev = None
1908 self.newrev = None
Jesse Keating71a47ff2017-06-06 11:36:43 -07001909 self.files = []
1910
Clint Byrumf8cc9902017-03-22 22:38:25 -07001911 def _id(self):
1912 return self.newrev
1913
1914 def __repr__(self):
1915 rep = None
1916 if self.newrev == '0000000000000000000000000000000000000000':
James E. Blair21037782017-07-19 11:56:55 -07001917 rep = '<%s 0x%x deletes %s from %s' % (
1918 type(self).__name__,
1919 id(self), self.ref, self.oldrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001920 elif self.oldrev == '0000000000000000000000000000000000000000':
James E. Blair21037782017-07-19 11:56:55 -07001921 rep = '<%s 0x%x creates %s on %s>' % (
1922 type(self).__name__,
1923 id(self), self.ref, self.newrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001924 else:
1925 # Catch all
James E. Blair21037782017-07-19 11:56:55 -07001926 rep = '<%s 0x%x %s updated %s..%s>' % (
1927 type(self).__name__,
1928 id(self), self.ref, self.oldrev, self.newrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001929 return rep
1930
James E. Blairfee8d652013-06-07 08:57:52 -07001931 def equals(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001932 if (self.project == other.project
1933 and self.ref == other.ref
1934 and self.newrev == other.newrev):
1935 return True
1936 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001937
1938 def isUpdateOf(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001939 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001940
1941 def filterJobs(self, jobs):
1942 return filter(lambda job: job.changeMatches(self), jobs)
1943
1944 def getRelatedChanges(self):
1945 return set()
1946
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001947 def updatesConfig(self):
Tristan Cacqueray829e6172017-06-13 06:49:36 +00001948 if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files or \
1949 [True for fn in self.files if fn.startswith("zuul.d/") or
1950 fn.startswith(".zuul.d/")]:
Jesse Keating71a47ff2017-06-06 11:36:43 -07001951 return True
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001952 return False
1953
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001954 def getSafeAttributes(self):
1955 return Attributes(project=self.project,
1956 ref=self.ref,
1957 oldrev=self.oldrev,
1958 newrev=self.newrev)
1959
James E. Blair1e8dd892012-05-30 09:15:05 -07001960
James E. Blair21037782017-07-19 11:56:55 -07001961class Branch(Ref):
1962 """An existing branch state for a Project."""
1963 def __init__(self, project):
1964 super(Branch, self).__init__(project)
1965 self.branch = None
1966
1967
1968class Tag(Ref):
1969 """An existing tag state for a Project."""
1970 def __init__(self, project):
1971 super(Tag, self).__init__(project)
1972 self.tag = None
1973
1974
1975class Change(Branch):
Monty Taylora42a55b2016-07-29 07:53:33 -07001976 """A proposed new state for a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001977 def __init__(self, project):
1978 super(Change, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -07001979 self.number = None
1980 self.url = None
1981 self.patchset = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001982
James E. Blair6965a4b2014-12-16 17:19:04 -08001983 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -07001984 self.needed_by_changes = []
1985 self.is_current_patchset = True
1986 self.can_merge = False
1987 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -07001988 self.failed_to_merge = False
James E. Blair11041d22014-05-02 14:49:53 -07001989 self.open = None
1990 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001991 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001992
Jan Hruban3b415922016-02-03 13:10:22 +01001993 self.source_event = None
1994
James E. Blair4aea70c2012-07-26 14:23:24 -07001995 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -07001996 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -07001997
1998 def __repr__(self):
1999 return '<Change 0x%x %s>' % (id(self), self._id())
2000
2001 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +08002002 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -07002003 return True
2004 return False
2005
James E. Blair2fa50962013-01-30 21:50:41 -08002006 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -08002007 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -07002008 (hasattr(other, 'patchset') and
2009 self.patchset is not None and
2010 other.patchset is not None and
2011 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -08002012 return True
2013 return False
2014
James E. Blairfee8d652013-06-07 08:57:52 -07002015 def getRelatedChanges(self):
2016 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -08002017 for c in self.needs_changes:
2018 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -07002019 for c in self.needed_by_changes:
2020 related.add(c)
2021 related.update(c.getRelatedChanges())
2022 return related
James E. Blair4aea70c2012-07-26 14:23:24 -07002023
Joshua Hesketh58419cb2017-02-24 13:09:22 -05002024 def getSafeAttributes(self):
2025 return Attributes(project=self.project,
2026 number=self.number,
2027 patchset=self.patchset)
2028
James E. Blair4aea70c2012-07-26 14:23:24 -07002029
James E. Blairee743612012-05-29 14:49:32 -07002030class TriggerEvent(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002031 """Incoming event from an external system."""
James E. Blairee743612012-05-29 14:49:32 -07002032 def __init__(self):
James E. Blairaad3ae22017-05-18 14:11:29 -07002033 # TODO(jeblair): further reduce this list
James E. Blairee743612012-05-29 14:49:32 -07002034 self.data = None
James E. Blair32663402012-06-01 10:04:18 -07002035 # common
James E. Blairee743612012-05-29 14:49:32 -07002036 self.type = None
Jesse Keating71a47ff2017-06-06 11:36:43 -07002037 self.branch_updated = False
James E. Blair72facdc2017-08-17 10:29:12 -07002038 self.branch_created = False
2039 self.branch_deleted = False
James E. Blair247cab72017-07-20 16:52:36 -07002040 self.ref = None
Paul Belangerbaca3132016-11-04 12:49:54 -04002041 # For management events (eg: enqueue / promote)
2042 self.tenant_name = None
James E. Blair6f284b42017-03-31 14:14:41 -07002043 self.project_hostname = None
James E. Blairee743612012-05-29 14:49:32 -07002044 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -07002045 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +01002046 # Representation of the user account that performed the event.
2047 self.account = None
James E. Blair32663402012-06-01 10:04:18 -07002048 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -07002049 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -07002050 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -07002051 self.patch_number = None
James E. Blairee743612012-05-29 14:49:32 -07002052 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07002053 self.comment = None
Jesse Keating5c05a9f2017-01-12 14:44:58 -08002054 self.state = None
James E. Blair32663402012-06-01 10:04:18 -07002055 # ref-updated
James E. Blair32663402012-06-01 10:04:18 -07002056 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07002057 self.newrev = None
James E. Blairad28e912013-11-27 10:43:22 -08002058 # For events that arrive with a destination pipeline (eg, from
2059 # an admin command, etc):
2060 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07002061
James E. Blair6f284b42017-03-31 14:14:41 -07002062 @property
2063 def canonical_project_name(self):
2064 return self.project_hostname + '/' + self.project_name
2065
Jan Hruban324ca5b2015-11-05 19:28:54 +01002066 def isPatchsetCreated(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01002067 return False
2068
2069 def isChangeAbandoned(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01002070 return False
2071
James E. Blair1e8dd892012-05-30 09:15:05 -07002072
James E. Blair9c17dbf2014-06-23 14:21:58 -07002073class BaseFilter(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002074 """Base Class for filtering which Changes and Events to process."""
James E. Blairaad3ae22017-05-18 14:11:29 -07002075 pass
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002076
James E. Blair9c17dbf2014-06-23 14:21:58 -07002077
2078class EventFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002079 """Allows a Pipeline to only respond to certain events."""
James E. Blairaad3ae22017-05-18 14:11:29 -07002080 def __init__(self, trigger):
2081 super(EventFilter, self).__init__()
James E. Blairc0dedf82014-08-06 09:37:52 -07002082 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07002083
James E. Blairaad3ae22017-05-18 14:11:29 -07002084 def matches(self, event, ref):
2085 # TODO(jeblair): consider removing ref argument
James E. Blairee743612012-05-29 14:49:32 -07002086 return True
James E. Blaireff88162013-07-01 12:44:14 -04002087
2088
James E. Blairaad3ae22017-05-18 14:11:29 -07002089class RefFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002090 """Allows a Manager to only enqueue Changes that meet certain criteria."""
Jesse Keating8c2eb572017-05-30 17:31:45 -07002091 def __init__(self, connection_name):
James E. Blairaad3ae22017-05-18 14:11:29 -07002092 super(RefFilter, self).__init__()
Jesse Keating8c2eb572017-05-30 17:31:45 -07002093 self.connection_name = connection_name
James E. Blair11041d22014-05-02 14:49:53 -07002094
2095 def matches(self, change):
James E. Blair11041d22014-05-02 14:49:53 -07002096 return True
2097
2098
James E. Blairb97ed802015-12-21 15:55:35 -08002099class ProjectPipelineConfig(object):
2100 # Represents a project cofiguration in the context of a pipeline
2101 def __init__(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002102 self.job_list = JobList()
James E. Blairb97ed802015-12-21 15:55:35 -08002103 self.queue_name = None
Adam Gandelman8bd57102016-12-02 12:58:42 -08002104 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08002105
2106
James E. Blair08d9b782017-06-29 14:22:48 -07002107class TenantProjectConfig(object):
2108 """A project in the context of a tenant.
2109
2110 A Project is globally unique in the system, however, when used in
2111 a tenant, some metadata about the project local to the tenant is
2112 stored in a TenantProjectConfig.
2113 """
2114
2115 def __init__(self, project):
2116 self.project = project
2117 self.load_classes = set()
James E. Blair6459db12017-06-29 14:57:20 -07002118 self.shadow_projects = set()
James E. Blair08d9b782017-06-29 14:22:48 -07002119
Tobias Henkeleca46202017-08-02 20:27:10 +02002120 # The tenant's default setting of exclude_unprotected_branches will
2121 # be overridden by this one if not None.
2122 self.exclude_unprotected_branches = None
2123
James E. Blair08d9b782017-06-29 14:22:48 -07002124
James E. Blairb97ed802015-12-21 15:55:35 -08002125class ProjectConfig(object):
2126 # Represents a project cofiguration
2127 def __init__(self, name):
2128 self.name = name
Adam Gandelman8bd57102016-12-02 12:58:42 -08002129 self.merge_mode = None
James E. Blaire74f5712017-09-29 15:14:31 -07002130 # The default branch for the project (usually master).
James E. Blair040b6502017-05-23 10:18:21 -07002131 self.default_branch = None
James E. Blairb97ed802015-12-21 15:55:35 -08002132 self.pipelines = {}
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002133 self.private_key_file = None
James E. Blairb97ed802015-12-21 15:55:35 -08002134
2135
James E. Blair97043882017-09-06 15:51:17 -07002136class ConfigItemNotListError(Exception):
2137 def __init__(self):
2138 message = textwrap.dedent("""\
2139 Configuration file is not a list. Each zuul.yaml configuration
2140 file must be a list of items, for example:
2141
2142 - job:
2143 name: foo
2144
2145 - project:
2146 name: bar
2147
2148 Ensure that every item starts with "- " so that it is parsed as a
2149 YAML list.
2150 """)
2151 super(ConfigItemNotListError, self).__init__(message)
2152
2153
2154class ConfigItemNotDictError(Exception):
2155 def __init__(self):
2156 message = textwrap.dedent("""\
2157 Configuration item is not a dictionary. Each zuul.yaml
2158 configuration file must be a list of dictionaries, for
2159 example:
2160
2161 - job:
2162 name: foo
2163
2164 - project:
2165 name: bar
2166
2167 Ensure that every item in the list is a dictionary with one
2168 key (in this example, 'job' and 'project').
2169 """)
2170 super(ConfigItemNotDictError, self).__init__(message)
2171
2172
2173class ConfigItemMultipleKeysError(Exception):
2174 def __init__(self):
2175 message = textwrap.dedent("""\
2176 Configuration item has more than one key. Each zuul.yaml
2177 configuration file must be a list of dictionaries with a
2178 single key, for example:
2179
2180 - job:
2181 name: foo
2182
2183 - project:
2184 name: bar
2185
2186 Ensure that every item in the list is a dictionary with only
2187 one key (in this example, 'job' and 'project'). This error
2188 may be caused by insufficient indentation of the keys under
2189 the configuration item ('name' in this example).
2190 """)
2191 super(ConfigItemMultipleKeysError, self).__init__(message)
2192
2193
2194class ConfigItemUnknownError(Exception):
2195 def __init__(self):
2196 message = textwrap.dedent("""\
2197 Configuration item not recognized. Each zuul.yaml
2198 configuration file must be a list of dictionaries, for
2199 example:
2200
2201 - job:
2202 name: foo
2203
2204 - project:
2205 name: bar
2206
2207 The dictionary keys must match one of the configuration item
2208 types recognized by zuul (for example, 'job' or 'project').
2209 """)
2210 super(ConfigItemUnknownError, self).__init__(message)
2211
2212
James E. Blaird8e778f2015-12-22 14:09:20 -08002213class UnparsedAbideConfig(object):
James E. Blair08d9b782017-06-29 14:22:48 -07002214
Monty Taylora42a55b2016-07-29 07:53:33 -07002215 """A collection of yaml lists that has not yet been parsed into objects.
2216
2217 An Abide is a collection of tenants.
2218 """
2219
James E. Blaird8e778f2015-12-22 14:09:20 -08002220 def __init__(self):
2221 self.tenants = []
2222
2223 def extend(self, conf):
2224 if isinstance(conf, UnparsedAbideConfig):
2225 self.tenants.extend(conf.tenants)
2226 return
2227
2228 if not isinstance(conf, list):
James E. Blair97043882017-09-06 15:51:17 -07002229 raise ConfigItemNotListError()
2230
James E. Blaird8e778f2015-12-22 14:09:20 -08002231 for item in conf:
2232 if not isinstance(item, dict):
James E. Blair97043882017-09-06 15:51:17 -07002233 raise ConfigItemNotDictError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002234 if len(item.keys()) > 1:
James E. Blair97043882017-09-06 15:51:17 -07002235 raise ConfigItemMultipleKeysError()
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002236 key, value = list(item.items())[0]
James E. Blaird8e778f2015-12-22 14:09:20 -08002237 if key == 'tenant':
2238 self.tenants.append(value)
2239 else:
James E. Blair97043882017-09-06 15:51:17 -07002240 raise ConfigItemUnknownError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002241
2242
2243class UnparsedTenantConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002244 """A collection of yaml lists that has not yet been parsed into objects."""
2245
James E. Blaird8e778f2015-12-22 14:09:20 -08002246 def __init__(self):
2247 self.pipelines = []
2248 self.jobs = []
2249 self.project_templates = []
James E. Blairff555742017-02-19 11:34:27 -08002250 self.projects = {}
James E. Blaira98340f2016-09-02 11:33:49 -07002251 self.nodesets = []
James E. Blair01f83b72017-03-15 13:03:40 -07002252 self.secrets = []
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002253 self.semaphores = []
James E. Blaird8e778f2015-12-22 14:09:20 -08002254
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002255 def copy(self):
2256 r = UnparsedTenantConfig()
2257 r.pipelines = copy.deepcopy(self.pipelines)
2258 r.jobs = copy.deepcopy(self.jobs)
2259 r.project_templates = copy.deepcopy(self.project_templates)
2260 r.projects = copy.deepcopy(self.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07002261 r.nodesets = copy.deepcopy(self.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002262 r.secrets = copy.deepcopy(self.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002263 r.semaphores = copy.deepcopy(self.semaphores)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002264 return r
2265
James E. Blairec7ff302017-03-04 07:31:32 -08002266 def extend(self, conf):
James E. Blaird8e778f2015-12-22 14:09:20 -08002267 if isinstance(conf, UnparsedTenantConfig):
2268 self.pipelines.extend(conf.pipelines)
2269 self.jobs.extend(conf.jobs)
2270 self.project_templates.extend(conf.project_templates)
James E. Blairff555742017-02-19 11:34:27 -08002271 for k, v in conf.projects.items():
2272 self.projects.setdefault(k, []).extend(v)
James E. Blaira98340f2016-09-02 11:33:49 -07002273 self.nodesets.extend(conf.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002274 self.secrets.extend(conf.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002275 self.semaphores.extend(conf.semaphores)
James E. Blaird8e778f2015-12-22 14:09:20 -08002276 return
2277
2278 if not isinstance(conf, list):
James E. Blair97043882017-09-06 15:51:17 -07002279 raise ConfigItemNotListError()
James E. Blaircdab2032017-02-01 09:09:29 -08002280
James E. Blaird8e778f2015-12-22 14:09:20 -08002281 for item in conf:
2282 if not isinstance(item, dict):
James E. Blair97043882017-09-06 15:51:17 -07002283 raise ConfigItemNotDictError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002284 if len(item.keys()) > 1:
James E. Blair97043882017-09-06 15:51:17 -07002285 raise ConfigItemMultipleKeysError()
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002286 key, value = list(item.items())[0]
James E. Blair66b274e2017-01-31 14:47:52 -08002287 if key == 'project':
James E. Blairff555742017-02-19 11:34:27 -08002288 name = value['name']
2289 self.projects.setdefault(name, []).append(value)
James E. Blair66b274e2017-01-31 14:47:52 -08002290 elif key == 'job':
James E. Blaird8e778f2015-12-22 14:09:20 -08002291 self.jobs.append(value)
2292 elif key == 'project-template':
2293 self.project_templates.append(value)
2294 elif key == 'pipeline':
2295 self.pipelines.append(value)
James E. Blaira98340f2016-09-02 11:33:49 -07002296 elif key == 'nodeset':
2297 self.nodesets.append(value)
James E. Blair01f83b72017-03-15 13:03:40 -07002298 elif key == 'secret':
2299 self.secrets.append(value)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002300 elif key == 'semaphore':
2301 self.semaphores.append(value)
James E. Blaird8e778f2015-12-22 14:09:20 -08002302 else:
James E. Blair97043882017-09-06 15:51:17 -07002303 raise ConfigItemUnknownError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002304
2305
James E. Blaireff88162013-07-01 12:44:14 -04002306class Layout(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002307 """Holds all of the Pipelines."""
2308
James E. Blair6459db12017-06-29 14:57:20 -07002309 def __init__(self, tenant):
2310 self.tenant = tenant
James E. Blairb97ed802015-12-21 15:55:35 -08002311 self.project_configs = {}
2312 self.project_templates = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07002313 self.pipelines = OrderedDict()
James E. Blair83005782015-12-11 14:46:03 -08002314 # This is a dictionary of name -> [jobs]. The first element
2315 # of the list is the first job added with that name. It is
2316 # the reference definition for a given job. Subsequent
2317 # elements are aspects of that job with different matchers
2318 # that override some attribute of the job. These aspects all
2319 # inherit from the reference definition.
James E. Blairfef88ec2016-11-30 08:38:27 -08002320 self.jobs = {'noop': [Job('noop')]}
James E. Blaira98340f2016-09-02 11:33:49 -07002321 self.nodesets = {}
James E. Blair01f83b72017-03-15 13:03:40 -07002322 self.secrets = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002323 self.semaphores = {}
James E. Blaireff88162013-07-01 12:44:14 -04002324
2325 def getJob(self, name):
2326 if name in self.jobs:
James E. Blair83005782015-12-11 14:46:03 -08002327 return self.jobs[name][0]
James E. Blairb97ed802015-12-21 15:55:35 -08002328 raise Exception("Job %s not defined" % (name,))
James E. Blair83005782015-12-11 14:46:03 -08002329
James E. Blair2bab6e72017-08-07 09:52:45 -07002330 def hasJob(self, name):
2331 return name in self.jobs
2332
James E. Blair83005782015-12-11 14:46:03 -08002333 def getJobs(self, name):
2334 return self.jobs.get(name, [])
2335
2336 def addJob(self, job):
James E. Blair4317e9f2016-07-15 10:05:47 -07002337 # We can have multiple variants of a job all with the same
2338 # name, but these variants must all be defined in the same repo.
James E. Blaircdab2032017-02-01 09:09:29 -08002339 prior_jobs = [j for j in self.getJobs(job.name) if
2340 j.source_context.project !=
2341 job.source_context.project]
James E. Blair6459db12017-06-29 14:57:20 -07002342 # Unless the repo is permitted to shadow another. If so, and
2343 # the job we are adding is from a repo that is permitted to
2344 # shadow the one with the older jobs, skip adding this job.
2345 job_project = job.source_context.project
2346 job_tpc = self.tenant.project_configs[job_project.canonical_name]
2347 skip_add = False
2348 for prior_job in prior_jobs[:]:
2349 prior_project = prior_job.source_context.project
2350 if prior_project in job_tpc.shadow_projects:
2351 prior_jobs.remove(prior_job)
2352 skip_add = True
2353
James E. Blair4317e9f2016-07-15 10:05:47 -07002354 if prior_jobs:
2355 raise Exception("Job %s in %s is not permitted to shadow "
James E. Blaircdab2032017-02-01 09:09:29 -08002356 "job %s in %s" % (
2357 job,
2358 job.source_context.project,
2359 prior_jobs[0],
2360 prior_jobs[0].source_context.project))
James E. Blair6459db12017-06-29 14:57:20 -07002361 if skip_add:
2362 return False
James E. Blair83005782015-12-11 14:46:03 -08002363 if job.name in self.jobs:
2364 self.jobs[job.name].append(job)
2365 else:
2366 self.jobs[job.name] = [job]
James E. Blair6459db12017-06-29 14:57:20 -07002367 return True
James E. Blair83005782015-12-11 14:46:03 -08002368
James E. Blaira98340f2016-09-02 11:33:49 -07002369 def addNodeSet(self, nodeset):
2370 if nodeset.name in self.nodesets:
2371 raise Exception("NodeSet %s already defined" % (nodeset.name,))
2372 self.nodesets[nodeset.name] = nodeset
2373
James E. Blair01f83b72017-03-15 13:03:40 -07002374 def addSecret(self, secret):
2375 if secret.name in self.secrets:
2376 raise Exception("Secret %s already defined" % (secret.name,))
2377 self.secrets[secret.name] = secret
2378
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002379 def addSemaphore(self, semaphore):
2380 if semaphore.name in self.semaphores:
2381 raise Exception("Semaphore %s already defined" % (semaphore.name,))
2382 self.semaphores[semaphore.name] = semaphore
2383
James E. Blair83005782015-12-11 14:46:03 -08002384 def addPipeline(self, pipeline):
2385 self.pipelines[pipeline.name] = pipeline
James E. Blair59fdbac2015-12-07 17:08:06 -08002386
James E. Blairb97ed802015-12-21 15:55:35 -08002387 def addProjectTemplate(self, project_template):
2388 self.project_templates[project_template.name] = project_template
2389
James E. Blairf59f3cf2017-02-19 14:50:26 -08002390 def addProjectConfig(self, project_config):
James E. Blairb97ed802015-12-21 15:55:35 -08002391 self.project_configs[project_config.name] = project_config
James E. Blairb97ed802015-12-21 15:55:35 -08002392
James E. Blaird2348362017-03-17 13:59:35 -07002393 def _createJobGraph(self, item, job_list, job_graph):
2394 change = item.change
2395 pipeline = item.pipeline
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002396 for jobname in job_list.jobs:
2397 # This is the final job we are constructing
James E. Blaira7f51ca2017-02-07 16:01:26 -08002398 frozen_job = None
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002399 # Whether the change matches any globally defined variant
James E. Blaira7f51ca2017-02-07 16:01:26 -08002400 matched = False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002401 for variant in self.getJobs(jobname):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002402 if variant.changeMatches(change):
James E. Blaira7f51ca2017-02-07 16:01:26 -08002403 if frozen_job is None:
2404 frozen_job = variant.copy()
2405 frozen_job.setRun()
2406 else:
2407 frozen_job.applyVariant(variant)
2408 matched = True
2409 if not matched:
James E. Blair6e85c2b2016-11-21 16:47:01 -08002410 # A change must match at least one defined job variant
2411 # (that is to say that it must match more than just
2412 # the job that is defined in the tree).
2413 continue
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002414 # Whether the change matches any of the project pipeline
2415 # variants
2416 matched = False
2417 for variant in job_list.jobs[jobname]:
2418 if variant.changeMatches(change):
2419 frozen_job.applyVariant(variant)
2420 matched = True
2421 if not matched:
2422 # A change must match at least one project pipeline
2423 # job variant.
2424 continue
James E. Blairb3f5db12017-03-17 12:57:39 -07002425 if (frozen_job.allowed_projects and
2426 change.project.name not in frozen_job.allowed_projects):
2427 raise Exception("Project %s is not allowed to run job %s" %
2428 (change.project.name, frozen_job.name))
James E. Blair8eb564a2017-08-10 09:21:41 -07002429 if ((not pipeline.post_review) and frozen_job.post_review):
2430 raise Exception("Pre-review pipeline %s does not allow "
2431 "post-review job %s" % (
James E. Blaird2348362017-03-17 13:59:35 -07002432 pipeline.name, frozen_job.name))
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002433 job_graph.addJob(frozen_job)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002434
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002435 def createJobGraph(self, item):
Paul Belanger15e3e202016-10-14 16:27:34 -04002436 # NOTE(pabelanger): It is possible for a foreign project not to have a
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002437 # configured pipeline, if so return an empty JobGraph.
James E. Blairc9455002017-09-06 09:22:19 -07002438 ret = JobGraph()
2439 ppc = self.getProjectPipelineConfig(item.change.project,
2440 item.pipeline)
2441 if ppc:
2442 self._createJobGraph(item, ppc.job_list, ret)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002443 return ret
2444
James E. Blairc9455002017-09-06 09:22:19 -07002445 def getProjectPipelineConfig(self, project, pipeline):
2446 project_config = self.project_configs.get(
2447 project.canonical_name, None)
2448 if not project_config:
2449 return None
2450 return project_config.pipelines.get(pipeline.name, None)
James E. Blair0d3e83b2017-06-05 13:51:57 -07002451
James E. Blair59fdbac2015-12-07 17:08:06 -08002452
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002453class Semaphore(object):
2454 def __init__(self, name, max=1):
2455 self.name = name
2456 self.max = int(max)
2457
2458
2459class SemaphoreHandler(object):
2460 log = logging.getLogger("zuul.SemaphoreHandler")
2461
2462 def __init__(self):
2463 self.semaphores = {}
2464
2465 def acquire(self, item, job):
2466 if not job.semaphore:
2467 return True
2468
2469 semaphore_key = job.semaphore
2470
2471 m = self.semaphores.get(semaphore_key)
2472 if not m:
2473 # The semaphore is not held, acquire it
2474 self._acquire(semaphore_key, item, job.name)
2475 return True
2476 if (item, job.name) in m:
2477 # This item already holds the semaphore
2478 return True
2479
2480 # semaphore is there, check max
2481 if len(m) < self._max_count(item, job.semaphore):
2482 self._acquire(semaphore_key, item, job.name)
2483 return True
2484
2485 return False
2486
2487 def release(self, item, job):
2488 if not job.semaphore:
2489 return
2490
2491 semaphore_key = job.semaphore
2492
2493 m = self.semaphores.get(semaphore_key)
2494 if not m:
2495 # The semaphore is not held, nothing to do
2496 self.log.error("Semaphore can not be released for %s "
2497 "because the semaphore is not held" %
2498 item)
2499 return
2500 if (item, job.name) in m:
2501 # This item is a holder of the semaphore
2502 self._release(semaphore_key, item, job.name)
2503 return
2504 self.log.error("Semaphore can not be released for %s "
2505 "which does not hold it" % item)
2506
2507 def _acquire(self, semaphore_key, item, job_name):
2508 self.log.debug("Semaphore acquire {semaphore}: job {job}, item {item}"
2509 .format(semaphore=semaphore_key,
2510 job=job_name,
2511 item=item))
2512 if semaphore_key not in self.semaphores:
2513 self.semaphores[semaphore_key] = []
2514 self.semaphores[semaphore_key].append((item, job_name))
2515
2516 def _release(self, semaphore_key, item, job_name):
2517 self.log.debug("Semaphore release {semaphore}: job {job}, item {item}"
2518 .format(semaphore=semaphore_key,
2519 job=job_name,
2520 item=item))
2521 sem_item = (item, job_name)
2522 if sem_item in self.semaphores[semaphore_key]:
2523 self.semaphores[semaphore_key].remove(sem_item)
2524
2525 # cleanup if there is no user of the semaphore anymore
2526 if len(self.semaphores[semaphore_key]) == 0:
2527 del self.semaphores[semaphore_key]
2528
2529 @staticmethod
2530 def _max_count(item, semaphore_name):
James E. Blair29a24fd2017-10-02 15:04:56 -07002531 if not item.layout:
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002532 # This should not occur as the layout of the item must already be
2533 # built when acquiring or releasing a semaphore for a job.
2534 raise Exception("Item {} has no layout".format(item))
2535
2536 # find the right semaphore
2537 default_semaphore = Semaphore(semaphore_name, 1)
James E. Blair29a24fd2017-10-02 15:04:56 -07002538 semaphores = item.layout.semaphores
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002539 return semaphores.get(semaphore_name, default_semaphore).max
2540
2541
James E. Blair59fdbac2015-12-07 17:08:06 -08002542class Tenant(object):
2543 def __init__(self, name):
2544 self.name = name
Tristan Cacqueray82f864b2017-08-01 05:54:42 +00002545 self.max_nodes_per_job = 5
Tristan Cacquerayc98bff72017-09-10 15:25:26 +00002546 self.max_job_timeout = 10800
Tobias Henkeleca46202017-08-02 20:27:10 +02002547 self.exclude_unprotected_branches = False
James E. Blair2bab6e72017-08-07 09:52:45 -07002548 self.default_base_job = None
James E. Blair59fdbac2015-12-07 17:08:06 -08002549 self.layout = None
James E. Blair646322f2017-01-27 15:50:34 -08002550 # The unparsed configuration from the main zuul config for
2551 # this tenant.
2552 self.unparsed_config = None
James E. Blair109da3f2017-04-04 14:39:43 -07002553 # The list of projects from which we will read full
James E. Blair8a395f92017-03-30 11:15:33 -07002554 # configuration.
James E. Blair109da3f2017-04-04 14:39:43 -07002555 self.config_projects = []
2556 # The unparsed config from those projects.
2557 self.config_projects_config = None
2558 # The list of projects from which we will read untrusted
2559 # in-repo configuration.
2560 self.untrusted_projects = []
2561 # The unparsed config from those projects.
2562 self.untrusted_projects_config = None
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002563 self.semaphore_handler = SemaphoreHandler()
James E. Blair08d9b782017-06-29 14:22:48 -07002564 # Metadata about projects for this tenant
2565 # canonical project name -> TenantProjectConfig
2566 self.project_configs = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002567
James E. Blairc2a54fd2017-03-29 15:19:26 -07002568 # A mapping of project names to projects. project_name ->
2569 # VALUE where VALUE is a further dictionary of
2570 # canonical_hostname -> Project.
2571 self.projects = {}
2572 self.canonical_hostnames = set()
2573
James E. Blair08d9b782017-06-29 14:22:48 -07002574 def _addProject(self, tpc):
James E. Blairc2a54fd2017-03-29 15:19:26 -07002575 """Add a project to the project index
2576
James E. Blair08d9b782017-06-29 14:22:48 -07002577 :arg TenantProjectConfig tpc: The TenantProjectConfig (with
2578 associated project) to add.
2579
James E. Blairc2a54fd2017-03-29 15:19:26 -07002580 """
James E. Blair08d9b782017-06-29 14:22:48 -07002581 project = tpc.project
James E. Blairc2a54fd2017-03-29 15:19:26 -07002582 self.canonical_hostnames.add(project.canonical_hostname)
2583 hostname_dict = self.projects.setdefault(project.name, {})
2584 if project.canonical_hostname in hostname_dict:
2585 raise Exception("Project %s is already in project index" %
2586 (project,))
2587 hostname_dict[project.canonical_hostname] = project
James E. Blair08d9b782017-06-29 14:22:48 -07002588 self.project_configs[project.canonical_name] = tpc
James E. Blairc2a54fd2017-03-29 15:19:26 -07002589
2590 def getProject(self, name):
2591 """Return a project given its name.
2592
2593 :arg str name: The name of the project. It may be fully
2594 qualified (E.g., "git.example.com/subpath/project") or may
2595 contain only the project name name may be supplied (E.g.,
2596 "subpath/project").
2597
2598 :returns: A tuple (trusted, project) or (None, None) if the
2599 project is not found or ambiguous. The "trusted" boolean
2600 indicates whether or not the project is trusted by this
2601 tenant.
2602 :rtype: (bool, Project)
2603
2604 """
2605 path = name.split('/', 1)
2606 if path[0] in self.canonical_hostnames:
2607 hostname = path[0]
2608 project_name = path[1]
2609 else:
2610 hostname = None
2611 project_name = name
2612 hostname_dict = self.projects.get(project_name)
2613 project = None
2614 if hostname_dict:
2615 if hostname:
2616 project = hostname_dict.get(hostname)
2617 else:
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002618 values = list(hostname_dict.values())
James E. Blairc2a54fd2017-03-29 15:19:26 -07002619 if len(values) == 1:
2620 project = values[0]
2621 else:
2622 raise Exception("Project name '%s' is ambiguous, "
2623 "please fully qualify the project "
2624 "with a hostname" % (name,))
2625 if project is None:
2626 return (None, None)
James E. Blair109da3f2017-04-04 14:39:43 -07002627 if project in self.config_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002628 return (True, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002629 if project in self.untrusted_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002630 return (False, project)
2631 # This should never happen:
2632 raise Exception("Project %s is neither trusted nor untrusted" %
2633 (project,))
2634
James E. Blair08d9b782017-06-29 14:22:48 -07002635 def addConfigProject(self, tpc):
2636 self.config_projects.append(tpc.project)
2637 self._addProject(tpc)
James E. Blair5ac93842017-01-20 06:47:34 -08002638
James E. Blair08d9b782017-06-29 14:22:48 -07002639 def addUntrustedProject(self, tpc):
2640 self.untrusted_projects.append(tpc.project)
2641 self._addProject(tpc)
James E. Blair5ac93842017-01-20 06:47:34 -08002642
Jamie Lennoxbf997c82017-05-10 09:58:40 +10002643 def getSafeAttributes(self):
2644 return Attributes(name=self.name)
2645
James E. Blair59fdbac2015-12-07 17:08:06 -08002646
2647class Abide(object):
2648 def __init__(self):
2649 self.tenants = OrderedDict()
James E. Blairce8a2132016-05-19 15:21:52 -07002650
2651
2652class JobTimeData(object):
2653 format = 'B10H10H10B'
2654 version = 0
2655
2656 def __init__(self, path):
2657 self.path = path
2658 self.success_times = [0 for x in range(10)]
2659 self.failure_times = [0 for x in range(10)]
2660 self.results = [0 for x in range(10)]
2661
2662 def load(self):
2663 if not os.path.exists(self.path):
2664 return
Clint Byruma4471d12017-05-10 20:57:40 -04002665 with open(self.path, 'rb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002666 data = struct.unpack(self.format, f.read())
2667 version = data[0]
2668 if version != self.version:
2669 raise Exception("Unkown data version")
2670 self.success_times = list(data[1:11])
2671 self.failure_times = list(data[11:21])
2672 self.results = list(data[21:32])
2673
2674 def save(self):
2675 tmpfile = self.path + '.tmp'
2676 data = [self.version]
2677 data.extend(self.success_times)
2678 data.extend(self.failure_times)
2679 data.extend(self.results)
2680 data = struct.pack(self.format, *data)
Clint Byruma4471d12017-05-10 20:57:40 -04002681 with open(tmpfile, 'wb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002682 f.write(data)
2683 os.rename(tmpfile, self.path)
2684
2685 def add(self, elapsed, result):
2686 elapsed = int(elapsed)
2687 if result == 'SUCCESS':
2688 self.success_times.append(elapsed)
2689 self.success_times.pop(0)
2690 result = 0
2691 else:
2692 self.failure_times.append(elapsed)
2693 self.failure_times.pop(0)
2694 result = 1
2695 self.results.append(result)
2696 self.results.pop(0)
2697
2698 def getEstimatedTime(self):
2699 times = [x for x in self.success_times if x]
2700 if times:
2701 return float(sum(times)) / len(times)
2702 return 0.0
2703
2704
2705class TimeDataBase(object):
2706 def __init__(self, root):
2707 self.root = root
James E. Blairce8a2132016-05-19 15:21:52 -07002708
James E. Blairae0f23c2017-09-13 10:55:15 -06002709 def _getTD(self, build):
2710 if hasattr(build.build_set.item.change, 'branch'):
2711 branch = build.build_set.item.change.branch
2712 else:
2713 branch = ''
2714
2715 dir_path = os.path.join(
2716 self.root,
2717 build.build_set.item.pipeline.layout.tenant.name,
2718 build.build_set.item.change.project.canonical_name,
2719 branch)
2720 if not os.path.exists(dir_path):
2721 os.makedirs(dir_path)
2722 path = os.path.join(dir_path, build.job.name)
2723
2724 td = JobTimeData(path)
2725 td.load()
James E. Blairce8a2132016-05-19 15:21:52 -07002726 return td
2727
2728 def getEstimatedTime(self, name):
2729 return self._getTD(name).getEstimatedTime()
2730
James E. Blairae0f23c2017-09-13 10:55:15 -06002731 def update(self, build, elapsed, result):
2732 td = self._getTD(build)
James E. Blairce8a2132016-05-19 15:21:52 -07002733 td.add(elapsed, result)
2734 td.save()