blob: 7b0f4d62add262c63b0d1a28b2b477162aeb4e0d [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. Blair4f1731b2017-10-10 18:11:42 -0700526 self.requested_time = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -0800527 self.state_time = time.time()
528 self.stat = None
529 self.uid = uuid4().hex
James E. Blaircbf43672017-01-04 14:33:41 -0800530 self.id = None
James E. Blaircbbce0d2017-05-19 07:28:29 -0700531 # Zuul internal flags (not stored in ZK so they are not
James E. Blaira38c28e2017-01-04 10:33:20 -0800532 # overwritten).
533 self.failed = False
James E. Blaircbbce0d2017-05-19 07:28:29 -0700534 self.canceled = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800535
536 @property
Monty Taylor6dc5bc12017-09-29 15:47:31 -0500537 def priority(self):
Monty Taylorb5882052017-09-29 19:12:52 -0500538 if self.build_set:
539 precedence = self.build_set.item.pipeline.precedence
540 else:
541 precedence = PRECEDENCE_NORMAL
542 return PRIORITY_MAP[precedence]
Monty Taylor6dc5bc12017-09-29 15:47:31 -0500543
544 @property
James E. Blair6ab79e02017-01-06 10:10:17 -0800545 def fulfilled(self):
546 return (self._state == STATE_FULFILLED) and not self.failed
547
548 @property
James E. Blairdce6cea2016-12-20 16:45:32 -0800549 def state(self):
550 return self._state
551
552 @state.setter
553 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800554 if value not in REQUEST_STATES:
555 raise TypeError("'%s' is not a valid state" % value)
James E. Blairdce6cea2016-12-20 16:45:32 -0800556 self._state = value
557 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700558
559 def __repr__(self):
James E. Blaircbf43672017-01-04 14:33:41 -0800560 return '<NodeRequest %s %s>' % (self.id, self.nodeset)
James E. Blair34776ee2016-08-25 13:53:54 -0700561
James E. Blairdce6cea2016-12-20 16:45:32 -0800562 def toDict(self):
563 d = {}
James E. Blair16d96a02017-06-08 11:32:56 -0700564 nodes = [n.label for n in self.nodeset.getNodes()]
James E. Blairdce6cea2016-12-20 16:45:32 -0800565 d['node_types'] = nodes
James E. Blair8b2a1472017-02-19 15:33:55 -0800566 d['requestor'] = self.requestor
James E. Blairdce6cea2016-12-20 16:45:32 -0800567 d['state'] = self.state
568 d['state_time'] = self.state_time
569 return d
570
571 def updateFromDict(self, data):
572 self._state = data['state']
573 self.state_time = data['state_time']
574
James E. Blair34776ee2016-08-25 13:53:54 -0700575
James E. Blair01f83b72017-03-15 13:03:40 -0700576class Secret(object):
577 """A collection of private data.
578
579 In configuration, Secrets are collections of private data in
580 key-value pair format. They are defined as top-level
581 configuration objects and then referenced by Jobs.
582
583 """
584
James E. Blair8525e2b2017-03-15 14:05:47 -0700585 def __init__(self, name, source_context):
James E. Blair01f83b72017-03-15 13:03:40 -0700586 self.name = name
James E. Blair8525e2b2017-03-15 14:05:47 -0700587 self.source_context = source_context
James E. Blair01f83b72017-03-15 13:03:40 -0700588 # The secret data may or may not be encrypted. This attribute
589 # is named 'secret_data' to make it easy to search for and
590 # spot where it is directly used.
591 self.secret_data = {}
592
593 def __ne__(self, other):
594 return not self.__eq__(other)
595
596 def __eq__(self, other):
597 if not isinstance(other, Secret):
598 return False
599 return (self.name == other.name and
James E. Blair8525e2b2017-03-15 14:05:47 -0700600 self.source_context == other.source_context and
James E. Blair01f83b72017-03-15 13:03:40 -0700601 self.secret_data == other.secret_data)
602
603 def __repr__(self):
604 return '<Secret %s>' % (self.name,)
605
James E. Blair18f86a32017-03-15 14:43:26 -0700606 def decrypt(self, private_key):
607 """Return a copy of this secret with any encrypted data decrypted.
608 Note that the original remains encrypted."""
609
610 r = copy.deepcopy(self)
611 decrypted_secret_data = {}
612 for k, v in r.secret_data.items():
613 if hasattr(v, 'decrypt'):
614 decrypted_secret_data[k] = v.decrypt(private_key)
615 else:
616 decrypted_secret_data[k] = v
617 r.secret_data = decrypted_secret_data
618 return r
619
James E. Blair01f83b72017-03-15 13:03:40 -0700620
James E. Blaircdab2032017-02-01 09:09:29 -0800621class SourceContext(object):
622 """A reference to the branch of a project in configuration.
623
624 Jobs and playbooks reference this to keep track of where they
625 originate."""
626
James E. Blair6f140c72017-03-03 10:32:07 -0800627 def __init__(self, project, branch, path, trusted):
James E. Blaircdab2032017-02-01 09:09:29 -0800628 self.project = project
629 self.branch = branch
James E. Blair6f140c72017-03-03 10:32:07 -0800630 self.path = path
Monty Taylore6562aa2017-02-20 07:37:39 -0500631 self.trusted = trusted
James E. Blaircdab2032017-02-01 09:09:29 -0800632
James E. Blair6f140c72017-03-03 10:32:07 -0800633 def __str__(self):
634 return '%s/%s@%s' % (self.project, self.path, self.branch)
635
James E. Blaircdab2032017-02-01 09:09:29 -0800636 def __repr__(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800637 return '<SourceContext %s trusted:%s>' % (str(self),
638 self.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800639
James E. Blaira7f51ca2017-02-07 16:01:26 -0800640 def __deepcopy__(self, memo):
641 return self.copy()
642
643 def copy(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800644 return self.__class__(self.project, self.branch, self.path,
645 self.trusted)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800646
Tristan Cacqueraye50af2e2017-09-19 14:18:28 +0000647 def isSameProject(self, other):
648 if not isinstance(other, SourceContext):
649 return False
650 return (self.project == other.project and
651 self.branch == other.branch and
652 self.trusted == other.trusted)
653
James E. Blaircdab2032017-02-01 09:09:29 -0800654 def __ne__(self, other):
655 return not self.__eq__(other)
656
657 def __eq__(self, other):
658 if not isinstance(other, SourceContext):
659 return False
660 return (self.project == other.project and
661 self.branch == other.branch and
James E. Blair6f140c72017-03-03 10:32:07 -0800662 self.path == other.path and
Monty Taylore6562aa2017-02-20 07:37:39 -0500663 self.trusted == other.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800664
665
James E. Blair66b274e2017-01-31 14:47:52 -0800666class PlaybookContext(object):
James E. Blaircdab2032017-02-01 09:09:29 -0800667
James E. Blair66b274e2017-01-31 14:47:52 -0800668 """A reference to a playbook in the context of a project.
669
670 Jobs refer to objects of this class for their main, pre, and post
671 playbooks so that we can keep track of which repos and security
James E. Blair74a82cf2017-07-12 17:23:08 -0700672 contexts are needed in order to run them.
James E. Blair66b274e2017-01-31 14:47:52 -0800673
James E. Blair74a82cf2017-07-12 17:23:08 -0700674 We also keep a list of roles so that playbooks only run with the
675 roles which were defined at the point the playbook was defined.
676
677 """
678
James E. Blair892cca62017-08-09 11:36:58 -0700679 def __init__(self, source_context, path, roles, secrets):
James E. Blaircdab2032017-02-01 09:09:29 -0800680 self.source_context = source_context
James E. Blair66b274e2017-01-31 14:47:52 -0800681 self.path = path
James E. Blair74a82cf2017-07-12 17:23:08 -0700682 self.roles = roles
James E. Blair892cca62017-08-09 11:36:58 -0700683 self.secrets = secrets
James E. Blair66b274e2017-01-31 14:47:52 -0800684
685 def __repr__(self):
James E. Blaircdab2032017-02-01 09:09:29 -0800686 return '<PlaybookContext %s %s>' % (self.source_context,
687 self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800688
689 def __ne__(self, other):
690 return not self.__eq__(other)
691
692 def __eq__(self, other):
693 if not isinstance(other, PlaybookContext):
694 return False
James E. Blaircdab2032017-02-01 09:09:29 -0800695 return (self.source_context == other.source_context and
James E. Blair74a82cf2017-07-12 17:23:08 -0700696 self.path == other.path and
James E. Blair892cca62017-08-09 11:36:58 -0700697 self.roles == other.roles and
698 self.secrets == other.secrets)
James E. Blair66b274e2017-01-31 14:47:52 -0800699
700 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400701 # Render to a dict to use in passing json to the executor
James E. Blair892cca62017-08-09 11:36:58 -0700702 secrets = {}
703 for secret in self.secrets:
704 secret_data = copy.deepcopy(secret.secret_data)
705 secrets[secret.name] = secret_data
James E. Blair66b274e2017-01-31 14:47:52 -0800706 return dict(
James E. Blaircdab2032017-02-01 09:09:29 -0800707 connection=self.source_context.project.connection_name,
708 project=self.source_context.project.name,
709 branch=self.source_context.branch,
Monty Taylore6562aa2017-02-20 07:37:39 -0500710 trusted=self.source_context.trusted,
James E. Blair74a82cf2017-07-12 17:23:08 -0700711 roles=[r.toDict() for r in self.roles],
James E. Blair892cca62017-08-09 11:36:58 -0700712 secrets=secrets,
James E. Blaircdab2032017-02-01 09:09:29 -0800713 path=self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800714
715
Monty Taylorb934c1a2017-06-16 19:31:47 -0500716class Role(object, metaclass=abc.ABCMeta):
James E. Blair5ac93842017-01-20 06:47:34 -0800717 """A reference to an ansible role."""
718
719 def __init__(self, target_name):
720 self.target_name = target_name
721
722 @abc.abstractmethod
723 def __repr__(self):
724 pass
725
726 def __ne__(self, other):
727 return not self.__eq__(other)
728
729 @abc.abstractmethod
730 def __eq__(self, other):
731 if not isinstance(other, Role):
732 return False
733 return (self.target_name == other.target_name)
734
735 @abc.abstractmethod
736 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400737 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800738 return dict(target_name=self.target_name)
739
740
741class ZuulRole(Role):
742 """A reference to an ansible role in a Zuul project."""
743
James E. Blairbb94dfa2017-07-11 07:45:19 -0700744 def __init__(self, target_name, connection_name, project_name,
745 implicit=False):
James E. Blair5ac93842017-01-20 06:47:34 -0800746 super(ZuulRole, self).__init__(target_name)
747 self.connection_name = connection_name
748 self.project_name = project_name
James E. Blairbb94dfa2017-07-11 07:45:19 -0700749 self.implicit = implicit
James E. Blair5ac93842017-01-20 06:47:34 -0800750
751 def __repr__(self):
752 return '<ZuulRole %s %s>' % (self.project_name, self.target_name)
753
Clint Byrumaf7438f2017-05-10 17:26:57 -0400754 __hash__ = object.__hash__
755
James E. Blair5ac93842017-01-20 06:47:34 -0800756 def __eq__(self, other):
757 if not isinstance(other, ZuulRole):
758 return False
James E. Blairbb94dfa2017-07-11 07:45:19 -0700759 # Implicit is not consulted for equality so that we can handle
760 # implicit to explicit conversions.
James E. Blair5ac93842017-01-20 06:47:34 -0800761 return (super(ZuulRole, self).__eq__(other) and
James E. Blair1b27f6a2017-07-14 14:09:07 -0700762 self.connection_name == other.connection_name and
James E. Blair6563e4b2017-04-28 08:14:48 -0700763 self.project_name == other.project_name)
James E. Blair5ac93842017-01-20 06:47:34 -0800764
765 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400766 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800767 d = super(ZuulRole, self).toDict()
768 d['type'] = 'zuul'
769 d['connection'] = self.connection_name
770 d['project'] = self.project_name
James E. Blairbb94dfa2017-07-11 07:45:19 -0700771 d['implicit'] = self.implicit
James E. Blair5ac93842017-01-20 06:47:34 -0800772 return d
773
774
James E. Blairee743612012-05-29 14:49:32 -0700775class Job(object):
James E. Blair66b274e2017-01-31 14:47:52 -0800776
James E. Blaira7f51ca2017-02-07 16:01:26 -0800777 """A Job represents the defintion of actions to perform.
778
James E. Blaird4ade8c2017-02-19 15:25:46 -0800779 A Job is an abstract configuration concept. It describes what,
780 where, and under what circumstances something should be run
781 (contrast this with Build which is a concrete single execution of
782 a Job).
783
James E. Blaira7f51ca2017-02-07 16:01:26 -0800784 NB: Do not modify attributes of this class, set them directly
785 (e.g., "job.run = ..." rather than "job.run.append(...)").
786 """
Monty Taylora42a55b2016-07-29 07:53:33 -0700787
James E. Blairee743612012-05-29 14:49:32 -0700788 def __init__(self, name):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800789 # These attributes may override even the final form of a job
790 # in the context of a project-pipeline. They can not affect
791 # the execution of the job, but only whether the job is run
792 # and how it is reported.
793 self.context_attributes = dict(
794 voting=True,
795 hold_following_changes=False,
James E. Blair1774dd52017-02-03 10:52:32 -0800796 failure_message=None,
797 success_message=None,
798 failure_url=None,
799 success_url=None,
800 # Matchers. These are separate so they can be individually
801 # overidden.
802 branch_matcher=None,
803 file_matcher=None,
804 irrelevant_file_matcher=None, # skip-if
James E. Blaira7f51ca2017-02-07 16:01:26 -0800805 tags=frozenset(),
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200806 dependencies=frozenset(),
James E. Blair1774dd52017-02-03 10:52:32 -0800807 )
808
James E. Blaira7f51ca2017-02-07 16:01:26 -0800809 # These attributes affect how the job is actually run and more
810 # care must be taken when overriding them. If a job is
811 # declared "final", these may not be overriden in a
812 # project-pipeline.
813 self.execution_attributes = dict(
814 timeout=None,
James E. Blair490cf042017-02-24 23:07:21 -0500815 variables={},
James E. Blaira7f51ca2017-02-07 16:01:26 -0800816 nodeset=NodeSet(),
James E. Blaira7f51ca2017-02-07 16:01:26 -0800817 workspace=None,
818 pre_run=(),
819 post_run=(),
820 run=(),
821 implied_run=(),
Tobias Henkel9a0e1942017-03-20 16:16:02 +0100822 semaphore=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800823 attempts=3,
824 final=False,
James E. Blair5fc81922017-07-12 13:19:37 -0700825 roles=(),
James E. Blair912322f2017-05-23 13:11:25 -0700826 required_projects={},
James E. Blairb3f5db12017-03-17 12:57:39 -0700827 allowed_projects=None,
James E. Blair7391ecb2017-05-23 13:32:40 -0700828 override_branch=None,
James E. Blair8eb564a2017-08-10 09:21:41 -0700829 post_review=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800830 )
831
832 # These are generally internal attributes which are not
833 # accessible via configuration.
834 self.other_attributes = dict(
835 name=None,
836 source_context=None,
James E. Blair167d6cd2017-09-29 14:24:42 -0700837 source_line=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800838 inheritance_path=(),
James E. Blair698703c2017-09-15 20:58:30 -0600839 parent_data=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800840 )
841
842 self.inheritable_attributes = {}
843 self.inheritable_attributes.update(self.context_attributes)
844 self.inheritable_attributes.update(self.execution_attributes)
845 self.attributes = {}
846 self.attributes.update(self.inheritable_attributes)
847 self.attributes.update(self.other_attributes)
848
James E. Blairee743612012-05-29 14:49:32 -0700849 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800850
James E. Blair66b274e2017-01-31 14:47:52 -0800851 def __ne__(self, other):
852 return not self.__eq__(other)
853
Paul Belangere22baea2016-11-03 16:59:27 -0400854 def __eq__(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800855 # Compare the name and all inheritable attributes to determine
856 # whether two jobs with the same name are identically
857 # configured. Useful upon reconfiguration.
858 if not isinstance(other, Job):
859 return False
860 if self.name != other.name:
861 return False
862 for k, v in self.attributes.items():
863 if getattr(self, k) != getattr(other, k):
864 return False
865 return True
James E. Blairee743612012-05-29 14:49:32 -0700866
Clint Byrumaf7438f2017-05-10 17:26:57 -0400867 __hash__ = object.__hash__
868
James E. Blairee743612012-05-29 14:49:32 -0700869 def __str__(self):
870 return self.name
871
872 def __repr__(self):
James E. Blair167d6cd2017-09-29 14:24:42 -0700873 return '<Job %s branches: %s source: %s#%s>' % (
874 self.name,
875 self.branch_matcher,
876 self.source_context,
877 self.source_line)
James E. Blair83005782015-12-11 14:46:03 -0800878
James E. Blaira7f51ca2017-02-07 16:01:26 -0800879 def __getattr__(self, name):
880 v = self.__dict__.get(name)
881 if v is None:
James E. Blairaf8b2082017-10-03 15:38:27 -0700882 return self.attributes[name]
James E. Blaira7f51ca2017-02-07 16:01:26 -0800883 return v
884
885 def _get(self, name):
886 return self.__dict__.get(name)
887
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100888 def getSafeAttributes(self):
889 return Attributes(name=self.name)
890
James E. Blaira7f51ca2017-02-07 16:01:26 -0800891 def setRun(self):
James E. Blair11bd5ee2017-10-18 09:29:42 -0700892 msg = 'self %s' % (repr(self),)
893 self.inheritance_path = self.inheritance_path + (msg,)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800894 if not self.run:
895 self.run = self.implied_run
896
James E. Blair5fc81922017-07-12 13:19:37 -0700897 def addRoles(self, roles):
James E. Blairbb94dfa2017-07-11 07:45:19 -0700898 newroles = []
899 # Start with a copy of the existing roles, but if any of them
900 # are implicit roles which are identified as explicit in the
901 # new roles list, replace them with the explicit version.
902 changed = False
903 for existing_role in self.roles:
904 if existing_role in roles:
905 new_role = roles[roles.index(existing_role)]
906 else:
907 new_role = None
908 if (new_role and
909 isinstance(new_role, ZuulRole) and
910 isinstance(existing_role, ZuulRole) and
911 existing_role.implicit and not new_role.implicit):
912 newroles.append(new_role)
913 changed = True
914 else:
915 newroles.append(existing_role)
916 # Now add the new roles.
James E. Blair4eec8282017-07-12 17:33:26 -0700917 for role in reversed(roles):
James E. Blair5fc81922017-07-12 13:19:37 -0700918 if role not in newroles:
James E. Blair4eec8282017-07-12 17:33:26 -0700919 newroles.insert(0, role)
James E. Blairbb94dfa2017-07-11 07:45:19 -0700920 changed = True
921 if changed:
922 self.roles = tuple(newroles)
James E. Blair5fc81922017-07-12 13:19:37 -0700923
James E. Blaire74f5712017-09-29 15:14:31 -0700924 def setBranchMatcher(self, branches):
925 # Set the branch matcher to match any of the supplied branches
926 matchers = []
927 for branch in branches:
928 matchers.append(change_matcher.BranchMatcher(branch))
929 self.branch_matcher = change_matcher.MatchAny(matchers)
930
James E. Blair490cf042017-02-24 23:07:21 -0500931 def updateVariables(self, other_vars):
James E. Blairaf8b2082017-10-03 15:38:27 -0700932 v = copy.deepcopy(self.variables)
James E. Blair490cf042017-02-24 23:07:21 -0500933 Job._deepUpdate(v, other_vars)
934 self.variables = v
935
James E. Blair698703c2017-09-15 20:58:30 -0600936 def updateParentData(self, other_vars):
937 # Update variables, but give the current values priority (used
938 # for job return data which is lower precedence than defined
939 # job vars).
940 v = self.parent_data or {}
941 Job._deepUpdate(v, other_vars)
942 # To avoid running afoul of checks that jobs don't set zuul
943 # variables, remove them from parent data here.
944 if 'zuul' in v:
945 del v['zuul']
946 self.parent_data = v
947 v = copy.deepcopy(self.parent_data)
948 Job._deepUpdate(v, self.variables)
949 self.variables = v
950
James E. Blair912322f2017-05-23 13:11:25 -0700951 def updateProjects(self, other_projects):
James E. Blairaf8b2082017-10-03 15:38:27 -0700952 required_projects = self.required_projects.copy()
953 required_projects.update(other_projects)
James E. Blair912322f2017-05-23 13:11:25 -0700954 self.required_projects = required_projects
James E. Blair27f3dfc2017-05-23 13:07:28 -0700955
James E. Blair490cf042017-02-24 23:07:21 -0500956 @staticmethod
957 def _deepUpdate(a, b):
958 # Merge nested dictionaries if possible, otherwise, overwrite
959 # the value in 'a' with the value in 'b'.
960 for k, bv in b.items():
961 av = a.get(k)
962 if isinstance(av, dict) and isinstance(bv, dict):
963 Job._deepUpdate(av, bv)
964 else:
965 a[k] = bv
966
James E. Blaira7f51ca2017-02-07 16:01:26 -0800967 def inheritFrom(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800968 """Copy the inheritable attributes which have been set on the other
969 job to this job."""
James E. Blaira7f51ca2017-02-07 16:01:26 -0800970 if not isinstance(other, Job):
971 raise Exception("Job unable to inherit from %s" % (other,))
972
Tobias Henkel83167622017-06-30 19:45:03 +0200973 if other.final:
974 raise Exception("Unable to inherit from final job %s" %
975 (repr(other),))
976
James E. Blaira7f51ca2017-02-07 16:01:26 -0800977 # copy all attributes
978 for k in self.inheritable_attributes:
James E. Blair892cca62017-08-09 11:36:58 -0700979 if (other._get(k) is not None):
James E. Blairaf8b2082017-10-03 15:38:27 -0700980 setattr(self, k, getattr(other, k))
James E. Blaira7f51ca2017-02-07 16:01:26 -0800981
982 msg = 'inherit from %s' % (repr(other),)
983 self.inheritance_path = other.inheritance_path + (msg,)
984
985 def copy(self):
986 job = Job(self.name)
987 for k in self.attributes:
988 if self._get(k) is not None:
989 setattr(job, k, copy.deepcopy(self._get(k)))
990 return job
991
992 def applyVariant(self, other):
993 """Copy the attributes which have been set on the other job to this
994 job."""
James E. Blair83005782015-12-11 14:46:03 -0800995
996 if not isinstance(other, Job):
997 raise Exception("Job unable to inherit from %s" % (other,))
James E. Blaira7f51ca2017-02-07 16:01:26 -0800998
999 for k in self.execution_attributes:
1000 if (other._get(k) is not None and
1001 k not in set(['final'])):
1002 if self.final:
1003 raise Exception("Unable to modify final job %s attribute "
1004 "%s=%s with variant %s" % (
1005 repr(self), k, other._get(k),
1006 repr(other)))
James E. Blair27f3dfc2017-05-23 13:07:28 -07001007 if k not in set(['pre_run', 'post_run', 'roles', 'variables',
James E. Blair912322f2017-05-23 13:11:25 -07001008 'required_projects']):
James E. Blaira7f51ca2017-02-07 16:01:26 -08001009 setattr(self, k, copy.deepcopy(other._get(k)))
1010
1011 # Don't set final above so that we don't trip an error halfway
1012 # through assignment.
1013 if other.final != self.attributes['final']:
1014 self.final = other.final
1015
1016 if other._get('pre_run') is not None:
1017 self.pre_run = self.pre_run + other.pre_run
1018 if other._get('post_run') is not None:
1019 self.post_run = other.post_run + self.post_run
James E. Blair5ac93842017-01-20 06:47:34 -08001020 if other._get('roles') is not None:
James E. Blair5fc81922017-07-12 13:19:37 -07001021 self.addRoles(other.roles)
James E. Blair490cf042017-02-24 23:07:21 -05001022 if other._get('variables') is not None:
1023 self.updateVariables(other.variables)
James E. Blair912322f2017-05-23 13:11:25 -07001024 if other._get('required_projects') is not None:
1025 self.updateProjects(other.required_projects)
James E. Blaira7f51ca2017-02-07 16:01:26 -08001026
1027 for k in self.context_attributes:
1028 if (other._get(k) is not None and
1029 k not in set(['tags'])):
1030 setattr(self, k, copy.deepcopy(other._get(k)))
1031
1032 if other._get('tags') is not None:
1033 self.tags = self.tags.union(other.tags)
1034
1035 msg = 'apply variant %s' % (repr(other),)
1036 self.inheritance_path = self.inheritance_path + (msg,)
James E. Blairee743612012-05-29 14:49:32 -07001037
James E. Blaire421a232012-07-25 16:59:21 -07001038 def changeMatches(self, change):
James E. Blair83005782015-12-11 14:46:03 -08001039 if self.branch_matcher and not self.branch_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -08001040 return False
1041
James E. Blair83005782015-12-11 14:46:03 -08001042 if self.file_matcher and not self.file_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -08001043 return False
1044
James E. Blair83005782015-12-11 14:46:03 -08001045 # NB: This is a negative match.
1046 if (self.irrelevant_file_matcher and
1047 self.irrelevant_file_matcher.matches(change)):
Maru Newby3fe5f852015-01-13 04:22:14 +00001048 return False
1049
James E. Blair70c71582013-03-06 08:50:50 -08001050 return True
James E. Blaire5a847f2012-07-10 15:29:14 -07001051
James E. Blair1e8dd892012-05-30 09:15:05 -07001052
James E. Blair912322f2017-05-23 13:11:25 -07001053class JobProject(object):
James E. Blair27f3dfc2017-05-23 13:07:28 -07001054 """ A reference to a project from a job. """
1055
1056 def __init__(self, project_name, override_branch=None):
1057 self.project_name = project_name
1058 self.override_branch = override_branch
1059
1060
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001061class JobList(object):
1062 """ A list of jobs in a project's pipeline. """
Monty Taylora42a55b2016-07-29 07:53:33 -07001063
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001064 def __init__(self):
1065 self.jobs = OrderedDict() # job.name -> [job, ...]
James E. Blair12748b52017-02-07 17:17:53 -08001066
James E. Blairee743612012-05-29 14:49:32 -07001067 def addJob(self, job):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001068 if job.name in self.jobs:
1069 self.jobs[job.name].append(job)
1070 else:
1071 self.jobs[job.name] = [job]
James E. Blairee743612012-05-29 14:49:32 -07001072
James E. Blaire74f5712017-09-29 15:14:31 -07001073 def inheritFrom(self, other, implied_branch):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001074 for jobname, jobs in other.jobs.items():
James E. Blaire74f5712017-09-29 15:14:31 -07001075 joblist = self.jobs.setdefault(jobname, [])
1076 for job in jobs:
1077 if not job.branch_matcher and implied_branch:
1078 job = job.copy()
1079 job.setBranchMatcher([implied_branch])
1080 joblist.append(job)
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001081
1082
1083class JobGraph(object):
1084 """ A JobGraph represents the dependency graph between Job."""
1085
1086 def __init__(self):
1087 self.jobs = OrderedDict() # job_name -> Job
1088 self._dependencies = {} # dependent_job_name -> set(parent_job_names)
1089
1090 def __repr__(self):
1091 return '<JobGraph %s>' % (self.jobs)
1092
1093 def addJob(self, job):
1094 # A graph must be created after the job list is frozen,
1095 # therefore we should only get one job with the same name.
1096 if job.name in self.jobs:
1097 raise Exception("Job %s already added" % (job.name,))
1098 self.jobs[job.name] = job
1099 # Append the dependency information
1100 self._dependencies.setdefault(job.name, set())
1101 try:
1102 for dependency in job.dependencies:
1103 # Make sure a circular dependency is never created
1104 ancestor_jobs = self._getParentJobNamesRecursively(
1105 dependency, soft=True)
1106 ancestor_jobs.add(dependency)
1107 if any((job.name == anc_job) for anc_job in ancestor_jobs):
1108 raise Exception("Dependency cycle detected in job %s" %
1109 (job.name,))
1110 self._dependencies[job.name].add(dependency)
1111 except Exception:
1112 del self.jobs[job.name]
1113 del self._dependencies[job.name]
1114 raise
1115
1116 def getJobs(self):
Clint Byruma4471d12017-05-10 20:57:40 -04001117 return list(self.jobs.values()) # Report in the order of layout cfg
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001118
1119 def _getDirectDependentJobs(self, parent_job):
1120 ret = set()
1121 for dependent_name, parent_names in self._dependencies.items():
1122 if parent_job in parent_names:
1123 ret.add(dependent_name)
1124 return ret
1125
1126 def getDependentJobsRecursively(self, parent_job):
1127 all_dependent_jobs = set()
1128 jobs_to_iterate = set([parent_job])
1129 while len(jobs_to_iterate) > 0:
1130 current_job = jobs_to_iterate.pop()
1131 current_dependent_jobs = self._getDirectDependentJobs(current_job)
1132 new_dependent_jobs = current_dependent_jobs - all_dependent_jobs
1133 jobs_to_iterate |= new_dependent_jobs
1134 all_dependent_jobs |= new_dependent_jobs
1135 return [self.jobs[name] for name in all_dependent_jobs]
1136
1137 def getParentJobsRecursively(self, dependent_job):
1138 return [self.jobs[name] for name in
1139 self._getParentJobNamesRecursively(dependent_job)]
1140
1141 def _getParentJobNamesRecursively(self, dependent_job, soft=False):
1142 all_parent_jobs = set()
1143 jobs_to_iterate = set([dependent_job])
1144 while len(jobs_to_iterate) > 0:
1145 current_job = jobs_to_iterate.pop()
1146 current_parent_jobs = self._dependencies.get(current_job)
1147 if current_parent_jobs is None:
1148 if soft:
1149 current_parent_jobs = set()
1150 else:
1151 raise Exception("Dependent job %s not found: " %
1152 (dependent_job,))
1153 new_parent_jobs = current_parent_jobs - all_parent_jobs
1154 jobs_to_iterate |= new_parent_jobs
1155 all_parent_jobs |= new_parent_jobs
1156 return all_parent_jobs
James E. Blairb97ed802015-12-21 15:55:35 -08001157
James E. Blair1e8dd892012-05-30 09:15:05 -07001158
James E. Blair4aea70c2012-07-26 14:23:24 -07001159class Build(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001160 """A Build is an instance of a single execution of a Job.
1161
1162 While a Job describes what to run, a Build describes an actual
1163 execution of that Job. Each build is associated with exactly one
1164 Job (related builds are grouped together in a BuildSet).
1165 """
Monty Taylora42a55b2016-07-29 07:53:33 -07001166
James E. Blair4aea70c2012-07-26 14:23:24 -07001167 def __init__(self, job, uuid):
1168 self.job = job
1169 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -07001170 self.url = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001171 self.result = None
James E. Blair196f61a2017-06-30 15:42:29 -07001172 self.result_data = {}
James E. Blair6f699732017-07-18 14:19:11 -07001173 self.error_detail = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001174 self.build_set = None
Paul Belanger174a8272017-03-14 13:20:10 -04001175 self.execute_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -08001176 self.start_time = None
1177 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -07001178 self.estimated_time = None
James E. Blair0aac4872013-08-23 14:02:38 -07001179 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -07001180 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -07001181 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +08001182 self.worker = Worker()
Timothy Chavezb2332082015-08-07 20:08:04 -05001183 self.node_labels = []
1184 self.node_name = None
James E. Blairee743612012-05-29 14:49:32 -07001185
1186 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +08001187 return ('<Build %s of %s on %s>' %
1188 (self.uuid, self.job.name, self.worker))
1189
James E. Blair3a098dd2017-10-04 14:37:29 -07001190 @property
1191 def pipeline(self):
1192 return self.build_set.item.pipeline
1193
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001194 def getSafeAttributes(self):
James E. Blair196f61a2017-06-30 15:42:29 -07001195 return Attributes(uuid=self.uuid,
1196 result=self.result,
James E. Blair6f699732017-07-18 14:19:11 -07001197 error_detail=self.error_detail,
James E. Blair196f61a2017-06-30 15:42:29 -07001198 result_data=self.result_data)
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001199
Joshua Heskethba8776a2014-01-12 14:35:40 +08001200
1201class Worker(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001202 """Information about the specific worker executing a Build."""
Joshua Heskethba8776a2014-01-12 14:35:40 +08001203 def __init__(self):
1204 self.name = "Unknown"
1205 self.hostname = None
Monty Taylor0dbe1592017-06-11 10:57:27 -05001206 self.log_port = None
Joshua Heskethba8776a2014-01-12 14:35:40 +08001207
1208 def updateFromData(self, data):
1209 """Update worker information if contained in the WORK_DATA response."""
1210 self.name = data.get('worker_name', self.name)
1211 self.hostname = data.get('worker_hostname', self.hostname)
Monty Taylor0dbe1592017-06-11 10:57:27 -05001212 self.log_port = data.get('worker_log_port', self.log_port)
Joshua Heskethba8776a2014-01-12 14:35:40 +08001213
1214 def __repr__(self):
1215 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -07001216
James E. Blair1e8dd892012-05-30 09:15:05 -07001217
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001218class RepoFiles(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001219 """RepoFiles holds config-file content for per-project job config.
1220
1221 When Zuul asks a merger to prepare a future multiple-repo state
1222 and collect Zuul configuration files so that we can dynamically
1223 load our configuration, this class provides cached access to that
1224 data for use by the Change which updated the config files and any
1225 changes that follow it in a ChangeQueue.
1226
1227 It is attached to a BuildSet since the content of Zuul
1228 configuration files can change with each new BuildSet.
1229 """
1230
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001231 def __init__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001232 self.connections = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001233
1234 def __repr__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001235 return '<RepoFiles %s>' % self.connections
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001236
1237 def setFiles(self, items):
James E. Blair2a535672017-04-27 12:03:15 -07001238 self.hostnames = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001239 for item in items:
James E. Blair2a535672017-04-27 12:03:15 -07001240 connection = self.connections.setdefault(
1241 item['connection'], {})
1242 project = connection.setdefault(item['project'], {})
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001243 branch = project.setdefault(item['branch'], {})
1244 branch.update(item['files'])
1245
James E. Blair2a535672017-04-27 12:03:15 -07001246 def getFile(self, connection_name, project_name, branch, fn):
1247 host = self.connections.get(connection_name, {})
1248 return host.get(project_name, {}).get(branch, {}).get(fn)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001249
1250
James E. Blair7e530ad2012-07-03 16:12:28 -07001251class BuildSet(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001252 """A collection of Builds for one specific potential future repository
1253 state.
Monty Taylora42a55b2016-07-29 07:53:33 -07001254
Paul Belanger174a8272017-03-14 13:20:10 -04001255 When Zuul executes Builds for a change, it creates a Build to
James E. Blaird4ade8c2017-02-19 15:25:46 -08001256 represent each execution of each job and a BuildSet to keep track
Paul Belanger174a8272017-03-14 13:20:10 -04001257 of all the Builds running for that Change. When Zuul re-executes
James E. Blaird4ade8c2017-02-19 15:25:46 -08001258 Builds for a Change with a different configuration, all of the
1259 running Builds in the BuildSet for that change are aborted, and a
1260 new BuildSet is created to hold the Builds for the Jobs being
1261 run with the new configuration.
1262
1263 A BuildSet also holds the UUID used to produce the Zuul Ref that
1264 builders check out.
1265
Monty Taylora42a55b2016-07-29 07:53:33 -07001266 """
James E. Blair4076e2b2014-01-28 12:42:20 -08001267 # Merge states:
1268 NEW = 1
1269 PENDING = 2
1270 COMPLETE = 3
1271
Antoine Musso9b229282014-08-18 23:45:43 +02001272 states_map = {
1273 1: 'NEW',
1274 2: 'PENDING',
1275 3: 'COMPLETE',
1276 }
1277
James E. Blairfee8d652013-06-07 08:57:52 -07001278 def __init__(self, item):
1279 self.item = item
James E. Blair7e530ad2012-07-03 16:12:28 -07001280 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -07001281 self.result = None
Jamie Lennox3f16de52017-05-09 14:24:11 +10001282 self.uuid = None
James E. Blair81515ad2012-10-01 18:29:08 -07001283 self.commit = None
James E. Blair9e5b8112017-10-19 08:12:24 -07001284 self.dependent_changes = None
James E. Blair1960d682017-04-28 15:44:14 -07001285 self.merger_items = None
James E. Blair973721f2012-08-15 10:19:43 -07001286 self.unable_to_merge = False
James E. Blaire53250c2017-03-01 14:34:36 -08001287 self.config_error = None # None or an error message string.
James E. Blair972e3c72013-08-29 12:04:55 -07001288 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -08001289 self.merge_state = self.NEW
James E. Blair0eaad552016-09-02 12:09:54 -07001290 self.nodesets = {} # job -> nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001291 self.node_requests = {} # job -> reqs
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001292 self.files = RepoFiles()
James E. Blair1960d682017-04-28 15:44:14 -07001293 self.repo_state = {}
Paul Belanger71d98172016-11-08 10:56:31 -05001294 self.tries = {}
James E. Blair7e530ad2012-07-03 16:12:28 -07001295
Jamie Lennox3f16de52017-05-09 14:24:11 +10001296 @property
1297 def ref(self):
1298 # NOTE(jamielennox): The concept of buildset ref is to be removed and a
1299 # buildset UUID identifier available instead. Currently the ref is
1300 # checked to see if the BuildSet has been configured.
1301 return 'Z' + self.uuid if self.uuid else None
1302
Antoine Musso9b229282014-08-18 23:45:43 +02001303 def __repr__(self):
1304 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
1305 self.item,
1306 len(self.builds),
1307 self.getStateName(self.merge_state))
1308
James E. Blair4886cc12012-07-18 15:39:41 -07001309 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -07001310 # The change isn't enqueued until after it's created
1311 # so we don't know what the other changes ahead will be
1312 # until jobs start.
James E. Blair9e5b8112017-10-19 08:12:24 -07001313 if not self.uuid:
1314 self.uuid = uuid4().hex
1315 if self.dependent_changes is None:
1316 items = [self.item]
James E. Blairfee8d652013-06-07 08:57:52 -07001317 next_item = self.item.item_ahead
1318 while next_item:
James E. Blair1960d682017-04-28 15:44:14 -07001319 items.append(next_item)
James E. Blairfee8d652013-06-07 08:57:52 -07001320 next_item = next_item.item_ahead
James E. Blair1960d682017-04-28 15:44:14 -07001321 items.reverse()
James E. Blair9e5b8112017-10-19 08:12:24 -07001322 self.dependent_changes = [i.change.toDict() for i in items]
James E. Blair1960d682017-04-28 15:44:14 -07001323 self.merger_items = [i.makeMergerItem() for i in items]
James E. Blair4886cc12012-07-18 15:39:41 -07001324
Antoine Musso9b229282014-08-18 23:45:43 +02001325 def getStateName(self, state_num):
1326 return self.states_map.get(
1327 state_num, 'UNKNOWN (%s)' % state_num)
1328
James E. Blair4886cc12012-07-18 15:39:41 -07001329 def addBuild(self, build):
1330 self.builds[build.job.name] = build
Paul Belanger71d98172016-11-08 10:56:31 -05001331 if build.job.name not in self.tries:
James E. Blair5aed1112016-12-15 09:18:05 -08001332 self.tries[build.job.name] = 1
James E. Blair4886cc12012-07-18 15:39:41 -07001333 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -07001334
James E. Blair4a28a882013-08-23 15:17:33 -07001335 def removeBuild(self, build):
Paul Belanger71d98172016-11-08 10:56:31 -05001336 self.tries[build.job.name] += 1
James E. Blair4a28a882013-08-23 15:17:33 -07001337 del self.builds[build.job.name]
1338
James E. Blair7e530ad2012-07-03 16:12:28 -07001339 def getBuild(self, job_name):
1340 return self.builds.get(job_name)
1341
James E. Blair11700c32012-07-05 17:50:05 -07001342 def getBuilds(self):
Clint Byrum1d0c7d12017-05-10 19:40:53 -07001343 keys = list(self.builds.keys())
James E. Blair11700c32012-07-05 17:50:05 -07001344 keys.sort()
1345 return [self.builds.get(x) for x in keys]
1346
James E. Blair0eaad552016-09-02 12:09:54 -07001347 def getJobNodeSet(self, job_name):
1348 # Return None if not provisioned; empty NodeSet if no nodes
1349 # required
1350 return self.nodesets.get(job_name)
James E. Blair8d692392016-04-08 17:47:58 -07001351
James E. Blaire18d4602017-01-05 11:17:28 -08001352 def removeJobNodeSet(self, job_name):
1353 if job_name not in self.nodesets:
1354 raise Exception("No job set for %s" % (job_name))
1355 del self.nodesets[job_name]
1356
James E. Blair8d692392016-04-08 17:47:58 -07001357 def setJobNodeRequest(self, job_name, req):
1358 if job_name in self.node_requests:
1359 raise Exception("Prior node request for %s" % (job_name))
1360 self.node_requests[job_name] = req
1361
1362 def getJobNodeRequest(self, job_name):
1363 return self.node_requests.get(job_name)
1364
James E. Blair0eaad552016-09-02 12:09:54 -07001365 def jobNodeRequestComplete(self, job_name, req, nodeset):
1366 if job_name in self.nodesets:
James E. Blair8d692392016-04-08 17:47:58 -07001367 raise Exception("Prior node request for %s" % (job_name))
James E. Blair0eaad552016-09-02 12:09:54 -07001368 self.nodesets[job_name] = nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001369 del self.node_requests[job_name]
1370
Paul Belanger71d98172016-11-08 10:56:31 -05001371 def getTries(self, job_name):
Clint Byrum804073b2017-05-10 17:47:45 -04001372 return self.tries.get(job_name, 0)
Paul Belanger71d98172016-11-08 10:56:31 -05001373
James E. Blair0ffa0102017-03-30 13:11:33 -07001374 def getMergeMode(self):
James E. Blair1960d682017-04-28 15:44:14 -07001375 # We may be called before this build set has a shadow layout
1376 # (ie, we are called to perform the merge to create that
1377 # layout). It's possible that the change we are merging will
1378 # update the merge-mode for the project, but there's not much
1379 # we can do about that here. Instead, do the best we can by
1380 # using the nearest shadow layout to determine the merge mode,
1381 # or if that fails, the current live layout, or if that fails,
1382 # use the default: merge-resolve.
1383 item = self.item
1384 layout = None
1385 while item:
James E. Blair29a24fd2017-10-02 15:04:56 -07001386 layout = item.layout
James E. Blair1960d682017-04-28 15:44:14 -07001387 if layout:
1388 break
1389 item = item.item_ahead
1390 if not layout:
1391 layout = self.item.pipeline.layout
1392 if layout:
James E. Blair0ffa0102017-03-30 13:11:33 -07001393 project = self.item.change.project
James E. Blair1960d682017-04-28 15:44:14 -07001394 project_config = layout.project_configs.get(
James E. Blair0ffa0102017-03-30 13:11:33 -07001395 project.canonical_name)
1396 if project_config:
1397 return project_config.merge_mode
1398 return MERGER_MERGE_RESOLVE
Adam Gandelman8bd57102016-12-02 12:58:42 -08001399
Jamie Lennox3f16de52017-05-09 14:24:11 +10001400 def getSafeAttributes(self):
1401 return Attributes(uuid=self.uuid)
1402
James E. Blair7e530ad2012-07-03 16:12:28 -07001403
James E. Blairfee8d652013-06-07 08:57:52 -07001404class QueueItem(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001405 """Represents the position of a Change in a ChangeQueue.
1406
1407 All Changes are enqueued into ChangeQueue in a QueueItem. The QueueItem
1408 holds the current `BuildSet` as well as all previous `BuildSets` that were
1409 produced for this `QueueItem`.
1410 """
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001411 log = logging.getLogger("zuul.QueueItem")
James E. Blair32663402012-06-01 10:04:18 -07001412
James E. Blairbfb8e042014-12-30 17:01:44 -08001413 def __init__(self, queue, change):
1414 self.pipeline = queue.pipeline
1415 self.queue = queue
Monty Taylor55d0f562017-05-17 14:30:21 -05001416 self.change = change # a ref
James E. Blaircaec0c52012-08-22 14:52:22 -07001417 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -07001418 self.current_build_set = BuildSet(self)
James E. Blairfee8d652013-06-07 08:57:52 -07001419 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -07001420 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -08001421 self.enqueue_time = None
1422 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -07001423 self.reported = False
Tobias Henkel9842bd72017-05-16 13:40:03 +02001424 self.reported_start = False
1425 self.quiet = False
James E. Blairbfb8e042014-12-30 17:01:44 -08001426 self.active = False # Whether an item is within an active window
1427 self.live = True # Whether an item is intended to be processed at all
James E. Blair29a24fd2017-10-02 15:04:56 -07001428 self.layout = None
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001429 self.job_graph = None
James E. Blaire5a847f2012-07-10 15:29:14 -07001430
James E. Blair972e3c72013-08-29 12:04:55 -07001431 def __repr__(self):
1432 if self.pipeline:
1433 pipeline = self.pipeline.name
1434 else:
1435 pipeline = None
1436 return '<QueueItem 0x%x for %s in %s>' % (
1437 id(self), self.change, pipeline)
1438
James E. Blairee743612012-05-29 14:49:32 -07001439 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -07001440 self.current_build_set = BuildSet(self)
James E. Blair29a24fd2017-10-02 15:04:56 -07001441 self.layout = None
James E. Blairc9455002017-09-06 09:22:19 -07001442 self.job_graph = None
James E. Blairee743612012-05-29 14:49:32 -07001443
1444 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -07001445 self.current_build_set.addBuild(build)
James E. Blairee743612012-05-29 14:49:32 -07001446
James E. Blair4a28a882013-08-23 15:17:33 -07001447 def removeBuild(self, build):
1448 self.current_build_set.removeBuild(build)
1449
James E. Blairfee8d652013-06-07 08:57:52 -07001450 def setReportedResult(self, result):
1451 self.current_build_set.result = result
1452
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001453 def freezeJobGraph(self):
James E. Blair83005782015-12-11 14:46:03 -08001454 """Find or create actual matching jobs for this item's change and
1455 store the resulting job tree."""
James E. Blair29a24fd2017-10-02 15:04:56 -07001456 job_graph = self.layout.createJobGraph(self)
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001457 for job in job_graph.getJobs():
1458 # Ensure that each jobs's dependencies are fully
1459 # accessible. This will raise an exception if not.
1460 job_graph.getParentJobsRecursively(job.name)
1461 self.job_graph = job_graph
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001462
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001463 def hasJobGraph(self):
1464 """Returns True if the item has a job graph."""
1465 return self.job_graph is not None
James E. Blair83005782015-12-11 14:46:03 -08001466
1467 def getJobs(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001468 if not self.live or not self.job_graph:
James E. Blair83005782015-12-11 14:46:03 -08001469 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001470 return self.job_graph.getJobs()
1471
1472 def getJob(self, name):
1473 if not self.job_graph:
1474 return None
1475 return self.job_graph.jobs.get(name)
James E. Blair83005782015-12-11 14:46:03 -08001476
James E. Blairdbfd3282016-07-21 10:46:19 -07001477 def haveAllJobsStarted(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001478 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001479 return False
1480 for job in self.getJobs():
1481 build = self.current_build_set.getBuild(job.name)
1482 if not build or not build.start_time:
1483 return False
1484 return True
1485
1486 def areAllJobsComplete(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001487 if (self.current_build_set.config_error or
1488 self.current_build_set.unable_to_merge):
1489 return True
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001490 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001491 return False
1492 for job in self.getJobs():
1493 build = self.current_build_set.getBuild(job.name)
1494 if not build or not build.result:
1495 return False
1496 return True
1497
1498 def didAllJobsSucceed(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001499 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001500 return False
1501 for job in self.getJobs():
1502 if not job.voting:
1503 continue
1504 build = self.current_build_set.getBuild(job.name)
1505 if not build:
1506 return False
1507 if build.result != 'SUCCESS':
1508 return False
1509 return True
1510
1511 def didAnyJobFail(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001512 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001513 return False
1514 for job in self.getJobs():
1515 if not job.voting:
1516 continue
1517 build = self.current_build_set.getBuild(job.name)
1518 if build and build.result and (build.result != 'SUCCESS'):
1519 return True
1520 return False
1521
1522 def didMergerFail(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001523 return self.current_build_set.unable_to_merge
1524
1525 def getConfigError(self):
1526 return self.current_build_set.config_error
James E. Blairdbfd3282016-07-21 10:46:19 -07001527
James E. Blair0d3e83b2017-06-05 13:51:57 -07001528 def wasDequeuedNeedingChange(self):
1529 return self.dequeued_needing_change
1530
James E. Blair8c2d5812017-10-06 09:29:21 -07001531 def includesConfigUpdates(self):
1532 includes_trusted = False
1533 includes_untrusted = False
1534 tenant = self.pipeline.layout.tenant
1535 item = self
1536 while item:
1537 if item.change.updatesConfig():
1538 (trusted, project) = tenant.getProject(
1539 item.change.project.canonical_name)
1540 if trusted:
1541 includes_trusted = True
1542 else:
1543 includes_untrusted = True
1544 if includes_trusted and includes_untrusted:
1545 # We're done early
1546 return (includes_trusted, includes_untrusted)
1547 item = item.item_ahead
1548 return (includes_trusted, includes_untrusted)
1549
James E. Blairdbfd3282016-07-21 10:46:19 -07001550 def isHoldingFollowingChanges(self):
1551 if not self.live:
1552 return False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001553 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001554 return False
1555 for job in self.getJobs():
1556 if not job.hold_following_changes:
1557 continue
1558 build = self.current_build_set.getBuild(job.name)
1559 if not build:
1560 return True
1561 if build.result != 'SUCCESS':
1562 return True
1563
1564 if not self.item_ahead:
1565 return False
1566 return self.item_ahead.isHoldingFollowingChanges()
1567
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001568 def findJobsToRun(self, semaphore_handler):
James E. Blairdbfd3282016-07-21 10:46:19 -07001569 torun = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001570 if not self.live:
1571 return []
1572 if not self.job_graph:
1573 return []
James E. Blair791b5392016-08-03 11:25:56 -07001574 if self.item_ahead:
1575 # Only run jobs if any 'hold' jobs on the change ahead
1576 # have completed successfully.
1577 if self.item_ahead.isHoldingFollowingChanges():
1578 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001579
1580 successful_job_names = set()
1581 jobs_not_started = set()
1582 for job in self.job_graph.getJobs():
1583 build = self.current_build_set.getBuild(job.name)
1584 if build:
1585 if build.result == 'SUCCESS':
1586 successful_job_names.add(job.name)
1587 else:
1588 jobs_not_started.add(job)
1589
James E. Blair698703c2017-09-15 20:58:30 -06001590 # Attempt to run jobs in the order they appear in
1591 # configuration.
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001592 for job in self.job_graph.getJobs():
1593 if job not in jobs_not_started:
1594 continue
1595 all_parent_jobs_successful = True
Tobias Henkela96c9b32017-10-22 12:38:06 +02001596 parent_builds_with_data = {}
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001597 for parent_job in self.job_graph.getParentJobsRecursively(
1598 job.name):
1599 if parent_job.name not in successful_job_names:
1600 all_parent_jobs_successful = False
1601 break
James E. Blair698703c2017-09-15 20:58:30 -06001602 parent_build = self.current_build_set.getBuild(parent_job.name)
1603 if parent_build.result_data:
Tobias Henkela96c9b32017-10-22 12:38:06 +02001604 parent_builds_with_data[parent_job.name] = parent_build
1605
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001606 if all_parent_jobs_successful:
Tobias Henkela96c9b32017-10-22 12:38:06 +02001607 # Iterate in reverse order over all jobs of the graph (which is
1608 # in sorted config order) and apply parent data of the jobs we
1609 # already found.
1610 if len(parent_builds_with_data) > 0:
1611 for parent_job in reversed(self.job_graph.getJobs()):
1612 parent_build = parent_builds_with_data.get(
1613 parent_job.name)
1614 if parent_build:
1615 job.updateParentData(parent_build.result_data)
1616
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001617 nodeset = self.current_build_set.getJobNodeSet(job.name)
1618 if nodeset is None:
1619 # The nodes for this job are not ready, skip
1620 # it for now.
James E. Blairdbfd3282016-07-21 10:46:19 -07001621 continue
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001622 if semaphore_handler.acquire(self, job):
1623 # If this job needs a semaphore, either acquire it or
1624 # make sure that we have it before running the job.
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001625 torun.append(job)
James E. Blairdbfd3282016-07-21 10:46:19 -07001626 return torun
1627
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001628 def findJobsToRequest(self):
James E. Blair6ab79e02017-01-06 10:10:17 -08001629 build_set = self.current_build_set
James E. Blairdbfd3282016-07-21 10:46:19 -07001630 toreq = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001631 if not self.live:
1632 return []
1633 if not self.job_graph:
1634 return []
James E. Blair6ab79e02017-01-06 10:10:17 -08001635 if self.item_ahead:
1636 if self.item_ahead.isHoldingFollowingChanges():
1637 return []
James E. Blairdbfd3282016-07-21 10:46:19 -07001638
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001639 successful_job_names = set()
1640 jobs_not_requested = set()
1641 for job in self.job_graph.getJobs():
1642 build = build_set.getBuild(job.name)
1643 if build and build.result == 'SUCCESS':
1644 successful_job_names.add(job.name)
1645 else:
1646 nodeset = build_set.getJobNodeSet(job.name)
1647 if nodeset is None:
1648 req = build_set.getJobNodeRequest(job.name)
1649 if req is None:
1650 jobs_not_requested.add(job)
1651
1652 # Attempt to request nodes for jobs in the order jobs appear
1653 # in configuration.
1654 for job in self.job_graph.getJobs():
1655 if job not in jobs_not_requested:
1656 continue
1657 all_parent_jobs_successful = True
1658 for parent_job in self.job_graph.getParentJobsRecursively(
1659 job.name):
1660 if parent_job.name not in successful_job_names:
1661 all_parent_jobs_successful = False
1662 break
1663 if all_parent_jobs_successful:
1664 toreq.append(job)
1665 return toreq
James E. Blairdbfd3282016-07-21 10:46:19 -07001666
1667 def setResult(self, build):
1668 if build.retry:
1669 self.removeBuild(build)
1670 elif build.result != 'SUCCESS':
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001671 for job in self.job_graph.getDependentJobsRecursively(
1672 build.job.name):
James E. Blairdbfd3282016-07-21 10:46:19 -07001673 fakebuild = Build(job, None)
1674 fakebuild.result = 'SKIPPED'
1675 self.addBuild(fakebuild)
1676
James E. Blair6ab79e02017-01-06 10:10:17 -08001677 def setNodeRequestFailure(self, job):
1678 fakebuild = Build(job, None)
1679 self.addBuild(fakebuild)
1680 fakebuild.result = 'NODE_FAILURE'
1681 self.setResult(fakebuild)
1682
James E. Blairdbfd3282016-07-21 10:46:19 -07001683 def setDequeuedNeedingChange(self):
1684 self.dequeued_needing_change = True
1685 self._setAllJobsSkipped()
1686
1687 def setUnableToMerge(self):
1688 self.current_build_set.unable_to_merge = True
1689 self._setAllJobsSkipped()
1690
James E. Blaire53250c2017-03-01 14:34:36 -08001691 def setConfigError(self, error):
1692 self.current_build_set.config_error = error
1693 self._setAllJobsSkipped()
1694
James E. Blairdbfd3282016-07-21 10:46:19 -07001695 def _setAllJobsSkipped(self):
1696 for job in self.getJobs():
1697 fakebuild = Build(job, None)
1698 fakebuild.result = 'SKIPPED'
1699 self.addBuild(fakebuild)
1700
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001701 def formatUrlPattern(self, url_pattern, job=None, build=None):
1702 url = None
1703 # Produce safe versions of objects which may be useful in
1704 # result formatting, but don't allow users to crawl through
1705 # the entire data structure where they might be able to access
1706 # secrets, etc.
1707 safe_change = self.change.getSafeAttributes()
1708 safe_pipeline = self.pipeline.getSafeAttributes()
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001709 safe_tenant = self.pipeline.layout.tenant.getSafeAttributes()
Jamie Lennox3f16de52017-05-09 14:24:11 +10001710 safe_buildset = self.current_build_set.getSafeAttributes()
K Jonathan Harkera7f390c2017-06-02 12:31:22 -07001711 safe_job = job.getSafeAttributes() if job else {}
1712 safe_build = build.getSafeAttributes() if build else {}
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001713 try:
1714 url = url_pattern.format(change=safe_change,
1715 pipeline=safe_pipeline,
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001716 tenant=safe_tenant,
Jamie Lennox3f16de52017-05-09 14:24:11 +10001717 buildset=safe_buildset,
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001718 job=safe_job,
1719 build=safe_build)
1720 except KeyError as e:
1721 self.log.error("Error while formatting url for job %s: unknown "
1722 "key %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001723 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001724 except AttributeError as e:
1725 self.log.error("Error while formatting url for job %s: unknown "
1726 "attribute %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001727 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001728 except Exception:
1729 self.log.exception("Error while formatting url for job %s with "
1730 "pattern %s:" % (job, url_pattern))
1731
1732 return url
1733
James E. Blair800e7ff2017-03-17 16:06:52 -07001734 def formatJobResult(self, job):
James E. Blairb7273ef2016-04-19 08:58:51 -07001735 build = self.current_build_set.getBuild(job.name)
1736 result = build.result
James E. Blair800e7ff2017-03-17 16:06:52 -07001737 pattern = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001738 if result == 'SUCCESS':
1739 if job.success_message:
1740 result = job.success_message
James E. Blaira5dba232016-08-08 15:53:24 -07001741 if job.success_url:
1742 pattern = job.success_url
Tobias Henkel077f2f32017-05-30 20:16:46 +02001743 else:
James E. Blairb7273ef2016-04-19 08:58:51 -07001744 if job.failure_message:
1745 result = job.failure_message
James E. Blaira5dba232016-08-08 15:53:24 -07001746 if job.failure_url:
1747 pattern = job.failure_url
James E. Blair88e79c02017-07-07 13:36:54 -07001748 url = None # The final URL
1749 default_url = build.result_data.get('zuul', {}).get('log_url')
James E. Blairb7273ef2016-04-19 08:58:51 -07001750 if pattern:
James E. Blair88e79c02017-07-07 13:36:54 -07001751 job_url = self.formatUrlPattern(pattern, job, build)
1752 else:
1753 job_url = None
1754 try:
1755 if job_url:
1756 u = urllib.parse.urlparse(job_url)
1757 if u.scheme:
1758 # The job success or failure url is absolute, so it's
1759 # our final url.
1760 url = job_url
1761 else:
1762 # We have a relative job url. Combine it with our
1763 # default url.
1764 if default_url:
1765 url = urllib.parse.urljoin(default_url, job_url)
1766 except Exception:
1767 self.log.exception("Error while parsing url for job %s:"
1768 % (job,))
James E. Blairb7273ef2016-04-19 08:58:51 -07001769 if not url:
James E. Blair88e79c02017-07-07 13:36:54 -07001770 url = default_url or build.url or job.name
James E. Blairb7273ef2016-04-19 08:58:51 -07001771 return (result, url)
1772
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001773 def formatJSON(self, websocket_url=None):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001774 ret = {}
1775 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -08001776 ret['live'] = self.live
Monty Taylor55d0f562017-05-17 14:30:21 -05001777 if hasattr(self.change, 'url') and self.change.url is not None:
1778 ret['url'] = self.change.url
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001779 else:
1780 ret['url'] = None
Monty Taylor55d0f562017-05-17 14:30:21 -05001781 ret['id'] = self.change._id()
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001782 if self.item_ahead:
1783 ret['item_ahead'] = self.item_ahead.change._id()
1784 else:
1785 ret['item_ahead'] = None
1786 ret['items_behind'] = [i.change._id() for i in self.items_behind]
1787 ret['failing_reasons'] = self.current_build_set.failing_reasons
1788 ret['zuul_ref'] = self.current_build_set.ref
Monty Taylor55d0f562017-05-17 14:30:21 -05001789 if self.change.project:
1790 ret['project'] = self.change.project.name
Ramy Asselin07cc33c2015-06-12 14:06:34 -07001791 else:
1792 # For cross-project dependencies with the depends-on
1793 # project not known to zuul, the project is None
1794 # Set it to a static value
1795 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001796 ret['enqueue_time'] = int(self.enqueue_time * 1000)
1797 ret['jobs'] = []
Monty Taylor55d0f562017-05-17 14:30:21 -05001798 if hasattr(self.change, 'owner'):
1799 ret['owner'] = self.change.owner
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001800 else:
1801 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001802 max_remaining = 0
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001803 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001804 now = time.time()
1805 build = self.current_build_set.getBuild(job.name)
1806 elapsed = None
1807 remaining = None
1808 result = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001809 build_url = None
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001810 finger_url = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001811 report_url = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001812 worker = None
1813 if build:
1814 result = build.result
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001815 finger_url = build.url
1816 # TODO(tobiash): add support for custom web root
1817 urlformat = 'static/stream.html?' \
1818 'uuid={build.uuid}&' \
1819 'logfile=console.log'
1820 if websocket_url:
1821 urlformat += '&websocket_url={websocket_url}'
1822 build_url = urlformat.format(
1823 build=build, websocket_url=websocket_url)
James E. Blair800e7ff2017-03-17 16:06:52 -07001824 (unused, report_url) = self.formatJobResult(job)
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001825 if build.start_time:
1826 if build.end_time:
1827 elapsed = int((build.end_time -
1828 build.start_time) * 1000)
1829 remaining = 0
1830 else:
1831 elapsed = int((now - build.start_time) * 1000)
1832 if build.estimated_time:
1833 remaining = max(
1834 int(build.estimated_time * 1000) - elapsed,
1835 0)
1836 worker = {
1837 'name': build.worker.name,
1838 'hostname': build.worker.hostname,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001839 }
1840 if remaining and remaining > max_remaining:
1841 max_remaining = remaining
1842
1843 ret['jobs'].append({
1844 'name': job.name,
Tobias Henkel65639f82017-07-10 10:25:42 +02001845 'dependencies': list(job.dependencies),
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001846 'elapsed_time': elapsed,
1847 'remaining_time': remaining,
James E. Blairb7273ef2016-04-19 08:58:51 -07001848 'url': build_url,
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001849 'finger_url': finger_url,
James E. Blairb7273ef2016-04-19 08:58:51 -07001850 'report_url': report_url,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001851 'result': result,
1852 'voting': job.voting,
1853 'uuid': build.uuid if build else None,
Paul Belanger174a8272017-03-14 13:20:10 -04001854 'execute_time': build.execute_time if build else None,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001855 'start_time': build.start_time if build else None,
1856 'end_time': build.end_time if build else None,
1857 'estimated_time': build.estimated_time if build else None,
1858 'pipeline': build.pipeline.name if build else None,
1859 'canceled': build.canceled if build else None,
1860 'retry': build.retry if build else None,
Timothy Chavezb2332082015-08-07 20:08:04 -05001861 'node_labels': build.node_labels if build else [],
1862 'node_name': build.node_name if build else None,
1863 'worker': worker,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001864 })
1865
James E. Blairdbfd3282016-07-21 10:46:19 -07001866 if self.haveAllJobsStarted():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001867 ret['remaining_time'] = max_remaining
1868 else:
1869 ret['remaining_time'] = None
1870 return ret
1871
1872 def formatStatus(self, indent=0, html=False):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001873 indent_str = ' ' * indent
1874 ret = ''
Monty Taylor55d0f562017-05-17 14:30:21 -05001875 if html and getattr(self.change, 'url', None) is not None:
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001876 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
1877 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001878 self.change.project.name,
1879 self.change.url,
1880 self.change._id())
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001881 else:
1882 ret += '%sProject %s change %s based on %s\n' % (
1883 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001884 self.change.project.name,
1885 self.change._id(),
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001886 self.item_ahead)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001887 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001888 build = self.current_build_set.getBuild(job.name)
1889 if build:
1890 result = build.result
1891 else:
1892 result = None
1893 job_name = job.name
1894 if not job.voting:
1895 voting = ' (non-voting)'
1896 else:
1897 voting = ''
1898 if html:
1899 if build:
1900 url = build.url
1901 else:
1902 url = None
1903 if url is not None:
1904 job_name = '<a href="%s">%s</a>' % (url, job_name)
1905 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
1906 ret += '\n'
1907 return ret
1908
James E. Blaira04b0792017-04-27 09:59:06 -07001909 def makeMergerItem(self):
1910 # Create a dictionary with all info about the item needed by
1911 # the merger.
1912 number = None
1913 patchset = None
1914 oldrev = None
1915 newrev = None
James E. Blair21037782017-07-19 11:56:55 -07001916 branch = None
James E. Blaira04b0792017-04-27 09:59:06 -07001917 if hasattr(self.change, 'number'):
1918 number = self.change.number
1919 patchset = self.change.patchset
James E. Blair21037782017-07-19 11:56:55 -07001920 if hasattr(self.change, 'newrev'):
James E. Blaira04b0792017-04-27 09:59:06 -07001921 oldrev = self.change.oldrev
1922 newrev = self.change.newrev
James E. Blair21037782017-07-19 11:56:55 -07001923 if hasattr(self.change, 'branch'):
1924 branch = self.change.branch
1925
James E. Blaira04b0792017-04-27 09:59:06 -07001926 source = self.change.project.source
1927 connection_name = source.connection.connection_name
James E. Blair2a535672017-04-27 12:03:15 -07001928 project = self.change.project
James E. Blaira04b0792017-04-27 09:59:06 -07001929
James E. Blair2a535672017-04-27 12:03:15 -07001930 return dict(project=project.name,
1931 connection=connection_name,
James E. Blaira04b0792017-04-27 09:59:06 -07001932 merge_mode=self.current_build_set.getMergeMode(),
James E. Blair247cab72017-07-20 16:52:36 -07001933 ref=self.change.ref,
James E. Blaira04b0792017-04-27 09:59:06 -07001934 branch=branch,
James E. Blair247cab72017-07-20 16:52:36 -07001935 buildset_uuid=self.current_build_set.uuid,
James E. Blaira04b0792017-04-27 09:59:06 -07001936 number=number,
1937 patchset=patchset,
1938 oldrev=oldrev,
1939 newrev=newrev,
1940 )
1941
James E. Blairfee8d652013-06-07 08:57:52 -07001942
Clint Byrumf8cc9902017-03-22 22:38:25 -07001943class Ref(object):
1944 """An existing state of a Project."""
James E. Blairfee8d652013-06-07 08:57:52 -07001945
1946 def __init__(self, project):
1947 self.project = project
Clint Byrumf8cc9902017-03-22 22:38:25 -07001948 self.ref = None
1949 self.oldrev = None
1950 self.newrev = None
Jesse Keating71a47ff2017-06-06 11:36:43 -07001951 self.files = []
1952
Clint Byrumf8cc9902017-03-22 22:38:25 -07001953 def _id(self):
1954 return self.newrev
1955
1956 def __repr__(self):
1957 rep = None
1958 if self.newrev == '0000000000000000000000000000000000000000':
James E. Blair21037782017-07-19 11:56:55 -07001959 rep = '<%s 0x%x deletes %s from %s' % (
1960 type(self).__name__,
1961 id(self), self.ref, self.oldrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001962 elif self.oldrev == '0000000000000000000000000000000000000000':
James E. Blair21037782017-07-19 11:56:55 -07001963 rep = '<%s 0x%x creates %s on %s>' % (
1964 type(self).__name__,
1965 id(self), self.ref, self.newrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001966 else:
1967 # Catch all
James E. Blair21037782017-07-19 11:56:55 -07001968 rep = '<%s 0x%x %s updated %s..%s>' % (
1969 type(self).__name__,
1970 id(self), self.ref, self.oldrev, self.newrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001971 return rep
1972
James E. Blairfee8d652013-06-07 08:57:52 -07001973 def equals(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001974 if (self.project == other.project
1975 and self.ref == other.ref
1976 and self.newrev == other.newrev):
1977 return True
1978 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001979
1980 def isUpdateOf(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001981 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001982
1983 def filterJobs(self, jobs):
1984 return filter(lambda job: job.changeMatches(self), jobs)
1985
1986 def getRelatedChanges(self):
1987 return set()
1988
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001989 def updatesConfig(self):
Tristan Cacqueray829e6172017-06-13 06:49:36 +00001990 if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files or \
1991 [True for fn in self.files if fn.startswith("zuul.d/") or
1992 fn.startswith(".zuul.d/")]:
Jesse Keating71a47ff2017-06-06 11:36:43 -07001993 return True
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001994 return False
1995
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001996 def getSafeAttributes(self):
1997 return Attributes(project=self.project,
1998 ref=self.ref,
1999 oldrev=self.oldrev,
2000 newrev=self.newrev)
2001
James E. Blair9e5b8112017-10-19 08:12:24 -07002002 def toDict(self):
2003 # Render to a dict to use in passing json to the executor
2004 d = dict()
2005 d['project'] = dict(
2006 name=self.project.name,
2007 short_name=self.project.name.split('/')[-1],
2008 canonical_hostname=self.project.canonical_hostname,
2009 canonical_name=self.project.canonical_name,
2010 src_dir=os.path.join('src', self.project.canonical_name),
2011 )
2012 return d
2013
James E. Blair1e8dd892012-05-30 09:15:05 -07002014
James E. Blair21037782017-07-19 11:56:55 -07002015class Branch(Ref):
2016 """An existing branch state for a Project."""
2017 def __init__(self, project):
2018 super(Branch, self).__init__(project)
2019 self.branch = None
2020
James E. Blair9e5b8112017-10-19 08:12:24 -07002021 def toDict(self):
2022 # Render to a dict to use in passing json to the executor
2023 d = super(Branch, self).toDict()
2024 d['branch'] = self.branch
2025 return d
2026
James E. Blair21037782017-07-19 11:56:55 -07002027
2028class Tag(Ref):
2029 """An existing tag state for a Project."""
2030 def __init__(self, project):
2031 super(Tag, self).__init__(project)
2032 self.tag = None
2033
2034
2035class Change(Branch):
Monty Taylora42a55b2016-07-29 07:53:33 -07002036 """A proposed new state for a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07002037 def __init__(self, project):
2038 super(Change, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -07002039 self.number = None
2040 self.url = None
2041 self.patchset = None
James E. Blair4aea70c2012-07-26 14:23:24 -07002042
James E. Blair6965a4b2014-12-16 17:19:04 -08002043 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -07002044 self.needed_by_changes = []
2045 self.is_current_patchset = True
2046 self.can_merge = False
2047 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -07002048 self.failed_to_merge = False
James E. Blair11041d22014-05-02 14:49:53 -07002049 self.open = None
2050 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05002051 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -07002052
Jan Hruban3b415922016-02-03 13:10:22 +01002053 self.source_event = None
2054
James E. Blair4aea70c2012-07-26 14:23:24 -07002055 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -07002056 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -07002057
2058 def __repr__(self):
2059 return '<Change 0x%x %s>' % (id(self), self._id())
2060
2061 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +08002062 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -07002063 return True
2064 return False
2065
James E. Blair2fa50962013-01-30 21:50:41 -08002066 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -08002067 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -07002068 (hasattr(other, 'patchset') and
2069 self.patchset is not None and
2070 other.patchset is not None and
2071 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -08002072 return True
2073 return False
2074
James E. Blairfee8d652013-06-07 08:57:52 -07002075 def getRelatedChanges(self):
2076 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -08002077 for c in self.needs_changes:
2078 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -07002079 for c in self.needed_by_changes:
2080 related.add(c)
2081 related.update(c.getRelatedChanges())
2082 return related
James E. Blair4aea70c2012-07-26 14:23:24 -07002083
Joshua Hesketh58419cb2017-02-24 13:09:22 -05002084 def getSafeAttributes(self):
2085 return Attributes(project=self.project,
2086 number=self.number,
2087 patchset=self.patchset)
2088
James E. Blair9e5b8112017-10-19 08:12:24 -07002089 def toDict(self):
2090 # Render to a dict to use in passing json to the executor
2091 d = super(Change, self).toDict()
2092 d['change'] = str(self.number)
2093 d['change_url'] = self.url
2094 d['patchset'] = str(self.patchset)
2095 return d
2096
James E. Blair4aea70c2012-07-26 14:23:24 -07002097
James E. Blairee743612012-05-29 14:49:32 -07002098class TriggerEvent(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002099 """Incoming event from an external system."""
James E. Blairee743612012-05-29 14:49:32 -07002100 def __init__(self):
James E. Blairaad3ae22017-05-18 14:11:29 -07002101 # TODO(jeblair): further reduce this list
James E. Blairee743612012-05-29 14:49:32 -07002102 self.data = None
James E. Blair32663402012-06-01 10:04:18 -07002103 # common
James E. Blairee743612012-05-29 14:49:32 -07002104 self.type = None
Jesse Keating71a47ff2017-06-06 11:36:43 -07002105 self.branch_updated = False
James E. Blair72facdc2017-08-17 10:29:12 -07002106 self.branch_created = False
2107 self.branch_deleted = False
James E. Blair247cab72017-07-20 16:52:36 -07002108 self.ref = None
Paul Belangerbaca3132016-11-04 12:49:54 -04002109 # For management events (eg: enqueue / promote)
2110 self.tenant_name = None
James E. Blair6f284b42017-03-31 14:14:41 -07002111 self.project_hostname = None
James E. Blairee743612012-05-29 14:49:32 -07002112 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -07002113 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +01002114 # Representation of the user account that performed the event.
2115 self.account = None
James E. Blair32663402012-06-01 10:04:18 -07002116 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -07002117 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -07002118 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -07002119 self.patch_number = None
James E. Blairee743612012-05-29 14:49:32 -07002120 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07002121 self.comment = None
Jesse Keating5c05a9f2017-01-12 14:44:58 -08002122 self.state = None
James E. Blair32663402012-06-01 10:04:18 -07002123 # ref-updated
James E. Blair32663402012-06-01 10:04:18 -07002124 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07002125 self.newrev = None
James E. Blairad28e912013-11-27 10:43:22 -08002126 # For events that arrive with a destination pipeline (eg, from
2127 # an admin command, etc):
2128 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07002129
James E. Blair6f284b42017-03-31 14:14:41 -07002130 @property
2131 def canonical_project_name(self):
2132 return self.project_hostname + '/' + self.project_name
2133
Jan Hruban324ca5b2015-11-05 19:28:54 +01002134 def isPatchsetCreated(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01002135 return False
2136
2137 def isChangeAbandoned(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01002138 return False
2139
James E. Blair1e8dd892012-05-30 09:15:05 -07002140
James E. Blair9c17dbf2014-06-23 14:21:58 -07002141class BaseFilter(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002142 """Base Class for filtering which Changes and Events to process."""
James E. Blairaad3ae22017-05-18 14:11:29 -07002143 pass
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002144
James E. Blair9c17dbf2014-06-23 14:21:58 -07002145
2146class EventFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002147 """Allows a Pipeline to only respond to certain events."""
James E. Blairaad3ae22017-05-18 14:11:29 -07002148 def __init__(self, trigger):
2149 super(EventFilter, self).__init__()
James E. Blairc0dedf82014-08-06 09:37:52 -07002150 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07002151
James E. Blairaad3ae22017-05-18 14:11:29 -07002152 def matches(self, event, ref):
2153 # TODO(jeblair): consider removing ref argument
James E. Blairee743612012-05-29 14:49:32 -07002154 return True
James E. Blaireff88162013-07-01 12:44:14 -04002155
2156
James E. Blairaad3ae22017-05-18 14:11:29 -07002157class RefFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002158 """Allows a Manager to only enqueue Changes that meet certain criteria."""
Jesse Keating8c2eb572017-05-30 17:31:45 -07002159 def __init__(self, connection_name):
James E. Blairaad3ae22017-05-18 14:11:29 -07002160 super(RefFilter, self).__init__()
Jesse Keating8c2eb572017-05-30 17:31:45 -07002161 self.connection_name = connection_name
James E. Blair11041d22014-05-02 14:49:53 -07002162
2163 def matches(self, change):
James E. Blair11041d22014-05-02 14:49:53 -07002164 return True
2165
2166
James E. Blairb97ed802015-12-21 15:55:35 -08002167class ProjectPipelineConfig(object):
2168 # Represents a project cofiguration in the context of a pipeline
2169 def __init__(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002170 self.job_list = JobList()
James E. Blairb97ed802015-12-21 15:55:35 -08002171 self.queue_name = None
Adam Gandelman8bd57102016-12-02 12:58:42 -08002172 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08002173
2174
James E. Blair08d9b782017-06-29 14:22:48 -07002175class TenantProjectConfig(object):
2176 """A project in the context of a tenant.
2177
2178 A Project is globally unique in the system, however, when used in
2179 a tenant, some metadata about the project local to the tenant is
2180 stored in a TenantProjectConfig.
2181 """
2182
2183 def __init__(self, project):
2184 self.project = project
2185 self.load_classes = set()
James E. Blair6459db12017-06-29 14:57:20 -07002186 self.shadow_projects = set()
James E. Blairdaaf3262017-10-23 13:51:48 -07002187 self.branches = []
Tobias Henkeleca46202017-08-02 20:27:10 +02002188 # The tenant's default setting of exclude_unprotected_branches will
2189 # be overridden by this one if not None.
2190 self.exclude_unprotected_branches = None
2191
James E. Blair08d9b782017-06-29 14:22:48 -07002192
James E. Blairb97ed802015-12-21 15:55:35 -08002193class ProjectConfig(object):
2194 # Represents a project cofiguration
2195 def __init__(self, name):
2196 self.name = name
Adam Gandelman8bd57102016-12-02 12:58:42 -08002197 self.merge_mode = None
James E. Blaire74f5712017-09-29 15:14:31 -07002198 # The default branch for the project (usually master).
James E. Blair040b6502017-05-23 10:18:21 -07002199 self.default_branch = None
James E. Blairb97ed802015-12-21 15:55:35 -08002200 self.pipelines = {}
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002201 self.private_key_file = None
James E. Blairb97ed802015-12-21 15:55:35 -08002202
2203
James E. Blair97043882017-09-06 15:51:17 -07002204class ConfigItemNotListError(Exception):
2205 def __init__(self):
2206 message = textwrap.dedent("""\
2207 Configuration file is not a list. Each zuul.yaml configuration
2208 file must be a list of items, for example:
2209
2210 - job:
2211 name: foo
2212
2213 - project:
2214 name: bar
2215
2216 Ensure that every item starts with "- " so that it is parsed as a
2217 YAML list.
2218 """)
2219 super(ConfigItemNotListError, self).__init__(message)
2220
2221
2222class ConfigItemNotDictError(Exception):
2223 def __init__(self):
2224 message = textwrap.dedent("""\
2225 Configuration item is not a dictionary. Each zuul.yaml
2226 configuration file must be a list of dictionaries, for
2227 example:
2228
2229 - job:
2230 name: foo
2231
2232 - project:
2233 name: bar
2234
2235 Ensure that every item in the list is a dictionary with one
2236 key (in this example, 'job' and 'project').
2237 """)
2238 super(ConfigItemNotDictError, self).__init__(message)
2239
2240
2241class ConfigItemMultipleKeysError(Exception):
2242 def __init__(self):
2243 message = textwrap.dedent("""\
2244 Configuration item has more than one key. Each zuul.yaml
2245 configuration file must be a list of dictionaries with a
2246 single key, for example:
2247
2248 - job:
2249 name: foo
2250
2251 - project:
2252 name: bar
2253
2254 Ensure that every item in the list is a dictionary with only
2255 one key (in this example, 'job' and 'project'). This error
2256 may be caused by insufficient indentation of the keys under
2257 the configuration item ('name' in this example).
2258 """)
2259 super(ConfigItemMultipleKeysError, self).__init__(message)
2260
2261
2262class ConfigItemUnknownError(Exception):
2263 def __init__(self):
2264 message = textwrap.dedent("""\
2265 Configuration item not recognized. Each zuul.yaml
2266 configuration file must be a list of dictionaries, for
2267 example:
2268
2269 - job:
2270 name: foo
2271
2272 - project:
2273 name: bar
2274
2275 The dictionary keys must match one of the configuration item
2276 types recognized by zuul (for example, 'job' or 'project').
2277 """)
2278 super(ConfigItemUnknownError, self).__init__(message)
2279
2280
James E. Blaird8e778f2015-12-22 14:09:20 -08002281class UnparsedAbideConfig(object):
James E. Blair08d9b782017-06-29 14:22:48 -07002282
Monty Taylora42a55b2016-07-29 07:53:33 -07002283 """A collection of yaml lists that has not yet been parsed into objects.
2284
2285 An Abide is a collection of tenants.
2286 """
2287
James E. Blaird8e778f2015-12-22 14:09:20 -08002288 def __init__(self):
2289 self.tenants = []
2290
2291 def extend(self, conf):
2292 if isinstance(conf, UnparsedAbideConfig):
2293 self.tenants.extend(conf.tenants)
2294 return
2295
2296 if not isinstance(conf, list):
James E. Blair97043882017-09-06 15:51:17 -07002297 raise ConfigItemNotListError()
2298
James E. Blaird8e778f2015-12-22 14:09:20 -08002299 for item in conf:
2300 if not isinstance(item, dict):
James E. Blair97043882017-09-06 15:51:17 -07002301 raise ConfigItemNotDictError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002302 if len(item.keys()) > 1:
James E. Blair97043882017-09-06 15:51:17 -07002303 raise ConfigItemMultipleKeysError()
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002304 key, value = list(item.items())[0]
James E. Blaird8e778f2015-12-22 14:09:20 -08002305 if key == 'tenant':
2306 self.tenants.append(value)
2307 else:
James E. Blair97043882017-09-06 15:51:17 -07002308 raise ConfigItemUnknownError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002309
2310
2311class UnparsedTenantConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002312 """A collection of yaml lists that has not yet been parsed into objects."""
2313
James E. Blaird8e778f2015-12-22 14:09:20 -08002314 def __init__(self):
2315 self.pipelines = []
2316 self.jobs = []
2317 self.project_templates = []
James E. Blairff555742017-02-19 11:34:27 -08002318 self.projects = {}
James E. Blaira98340f2016-09-02 11:33:49 -07002319 self.nodesets = []
James E. Blair01f83b72017-03-15 13:03:40 -07002320 self.secrets = []
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002321 self.semaphores = []
James E. Blaird8e778f2015-12-22 14:09:20 -08002322
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002323 def copy(self):
2324 r = UnparsedTenantConfig()
2325 r.pipelines = copy.deepcopy(self.pipelines)
2326 r.jobs = copy.deepcopy(self.jobs)
2327 r.project_templates = copy.deepcopy(self.project_templates)
2328 r.projects = copy.deepcopy(self.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07002329 r.nodesets = copy.deepcopy(self.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002330 r.secrets = copy.deepcopy(self.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002331 r.semaphores = copy.deepcopy(self.semaphores)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002332 return r
2333
James E. Blairec7ff302017-03-04 07:31:32 -08002334 def extend(self, conf):
James E. Blaird8e778f2015-12-22 14:09:20 -08002335 if isinstance(conf, UnparsedTenantConfig):
2336 self.pipelines.extend(conf.pipelines)
2337 self.jobs.extend(conf.jobs)
2338 self.project_templates.extend(conf.project_templates)
James E. Blairff555742017-02-19 11:34:27 -08002339 for k, v in conf.projects.items():
2340 self.projects.setdefault(k, []).extend(v)
James E. Blaira98340f2016-09-02 11:33:49 -07002341 self.nodesets.extend(conf.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002342 self.secrets.extend(conf.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002343 self.semaphores.extend(conf.semaphores)
James E. Blaird8e778f2015-12-22 14:09:20 -08002344 return
2345
2346 if not isinstance(conf, list):
James E. Blair97043882017-09-06 15:51:17 -07002347 raise ConfigItemNotListError()
James E. Blaircdab2032017-02-01 09:09:29 -08002348
James E. Blaird8e778f2015-12-22 14:09:20 -08002349 for item in conf:
2350 if not isinstance(item, dict):
James E. Blair97043882017-09-06 15:51:17 -07002351 raise ConfigItemNotDictError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002352 if len(item.keys()) > 1:
James E. Blair97043882017-09-06 15:51:17 -07002353 raise ConfigItemMultipleKeysError()
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002354 key, value = list(item.items())[0]
James E. Blair66b274e2017-01-31 14:47:52 -08002355 if key == 'project':
James E. Blairff555742017-02-19 11:34:27 -08002356 name = value['name']
2357 self.projects.setdefault(name, []).append(value)
James E. Blair66b274e2017-01-31 14:47:52 -08002358 elif key == 'job':
James E. Blaird8e778f2015-12-22 14:09:20 -08002359 self.jobs.append(value)
2360 elif key == 'project-template':
2361 self.project_templates.append(value)
2362 elif key == 'pipeline':
2363 self.pipelines.append(value)
James E. Blaira98340f2016-09-02 11:33:49 -07002364 elif key == 'nodeset':
2365 self.nodesets.append(value)
James E. Blair01f83b72017-03-15 13:03:40 -07002366 elif key == 'secret':
2367 self.secrets.append(value)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002368 elif key == 'semaphore':
2369 self.semaphores.append(value)
James E. Blaird8e778f2015-12-22 14:09:20 -08002370 else:
James E. Blair97043882017-09-06 15:51:17 -07002371 raise ConfigItemUnknownError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002372
2373
James E. Blaireff88162013-07-01 12:44:14 -04002374class Layout(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002375 """Holds all of the Pipelines."""
2376
James E. Blair6459db12017-06-29 14:57:20 -07002377 def __init__(self, tenant):
James E. Blair8fe53b42017-10-18 16:58:38 -07002378 self.uuid = uuid4().hex
James E. Blair6459db12017-06-29 14:57:20 -07002379 self.tenant = tenant
James E. Blairb97ed802015-12-21 15:55:35 -08002380 self.project_configs = {}
2381 self.project_templates = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07002382 self.pipelines = OrderedDict()
James E. Blair83005782015-12-11 14:46:03 -08002383 # This is a dictionary of name -> [jobs]. The first element
2384 # of the list is the first job added with that name. It is
2385 # the reference definition for a given job. Subsequent
2386 # elements are aspects of that job with different matchers
2387 # that override some attribute of the job. These aspects all
2388 # inherit from the reference definition.
James E. Blairfef88ec2016-11-30 08:38:27 -08002389 self.jobs = {'noop': [Job('noop')]}
James E. Blaira98340f2016-09-02 11:33:49 -07002390 self.nodesets = {}
James E. Blair01f83b72017-03-15 13:03:40 -07002391 self.secrets = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002392 self.semaphores = {}
James E. Blaireff88162013-07-01 12:44:14 -04002393
2394 def getJob(self, name):
2395 if name in self.jobs:
James E. Blair83005782015-12-11 14:46:03 -08002396 return self.jobs[name][0]
James E. Blairb97ed802015-12-21 15:55:35 -08002397 raise Exception("Job %s not defined" % (name,))
James E. Blair83005782015-12-11 14:46:03 -08002398
James E. Blair2bab6e72017-08-07 09:52:45 -07002399 def hasJob(self, name):
2400 return name in self.jobs
2401
James E. Blair83005782015-12-11 14:46:03 -08002402 def getJobs(self, name):
2403 return self.jobs.get(name, [])
2404
2405 def addJob(self, job):
James E. Blair4317e9f2016-07-15 10:05:47 -07002406 # We can have multiple variants of a job all with the same
2407 # name, but these variants must all be defined in the same repo.
James E. Blaircdab2032017-02-01 09:09:29 -08002408 prior_jobs = [j for j in self.getJobs(job.name) if
2409 j.source_context.project !=
2410 job.source_context.project]
James E. Blair6459db12017-06-29 14:57:20 -07002411 # Unless the repo is permitted to shadow another. If so, and
2412 # the job we are adding is from a repo that is permitted to
2413 # shadow the one with the older jobs, skip adding this job.
2414 job_project = job.source_context.project
2415 job_tpc = self.tenant.project_configs[job_project.canonical_name]
2416 skip_add = False
2417 for prior_job in prior_jobs[:]:
2418 prior_project = prior_job.source_context.project
2419 if prior_project in job_tpc.shadow_projects:
2420 prior_jobs.remove(prior_job)
2421 skip_add = True
2422
James E. Blair4317e9f2016-07-15 10:05:47 -07002423 if prior_jobs:
2424 raise Exception("Job %s in %s is not permitted to shadow "
James E. Blaircdab2032017-02-01 09:09:29 -08002425 "job %s in %s" % (
2426 job,
2427 job.source_context.project,
2428 prior_jobs[0],
2429 prior_jobs[0].source_context.project))
James E. Blair6459db12017-06-29 14:57:20 -07002430 if skip_add:
2431 return False
James E. Blair83005782015-12-11 14:46:03 -08002432 if job.name in self.jobs:
2433 self.jobs[job.name].append(job)
2434 else:
2435 self.jobs[job.name] = [job]
James E. Blair6459db12017-06-29 14:57:20 -07002436 return True
James E. Blair83005782015-12-11 14:46:03 -08002437
James E. Blaira98340f2016-09-02 11:33:49 -07002438 def addNodeSet(self, nodeset):
2439 if nodeset.name in self.nodesets:
2440 raise Exception("NodeSet %s already defined" % (nodeset.name,))
2441 self.nodesets[nodeset.name] = nodeset
2442
James E. Blair01f83b72017-03-15 13:03:40 -07002443 def addSecret(self, secret):
2444 if secret.name in self.secrets:
2445 raise Exception("Secret %s already defined" % (secret.name,))
2446 self.secrets[secret.name] = secret
2447
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002448 def addSemaphore(self, semaphore):
2449 if semaphore.name in self.semaphores:
2450 raise Exception("Semaphore %s already defined" % (semaphore.name,))
2451 self.semaphores[semaphore.name] = semaphore
2452
James E. Blair83005782015-12-11 14:46:03 -08002453 def addPipeline(self, pipeline):
2454 self.pipelines[pipeline.name] = pipeline
James E. Blair59fdbac2015-12-07 17:08:06 -08002455
James E. Blairb97ed802015-12-21 15:55:35 -08002456 def addProjectTemplate(self, project_template):
James E. Blair6bc10482017-10-20 11:28:53 -07002457 if project_template.name in self.project_templates:
2458 # TODO(jeblair): issue a warning to the logs on loading
2459 # the config, and an error when this hits in a proposed
2460 # change.
2461 return
James E. Blairb97ed802015-12-21 15:55:35 -08002462 self.project_templates[project_template.name] = project_template
2463
James E. Blairf59f3cf2017-02-19 14:50:26 -08002464 def addProjectConfig(self, project_config):
James E. Blairb97ed802015-12-21 15:55:35 -08002465 self.project_configs[project_config.name] = project_config
James E. Blairb97ed802015-12-21 15:55:35 -08002466
James E. Blaird2348362017-03-17 13:59:35 -07002467 def _createJobGraph(self, item, job_list, job_graph):
2468 change = item.change
2469 pipeline = item.pipeline
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002470 for jobname in job_list.jobs:
2471 # This is the final job we are constructing
James E. Blaira7f51ca2017-02-07 16:01:26 -08002472 frozen_job = None
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002473 # Whether the change matches any globally defined variant
James E. Blaira7f51ca2017-02-07 16:01:26 -08002474 matched = False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002475 for variant in self.getJobs(jobname):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002476 if variant.changeMatches(change):
James E. Blaira7f51ca2017-02-07 16:01:26 -08002477 if frozen_job is None:
2478 frozen_job = variant.copy()
2479 frozen_job.setRun()
2480 else:
2481 frozen_job.applyVariant(variant)
2482 matched = True
2483 if not matched:
James E. Blair6e85c2b2016-11-21 16:47:01 -08002484 # A change must match at least one defined job variant
2485 # (that is to say that it must match more than just
2486 # the job that is defined in the tree).
2487 continue
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002488 # Whether the change matches any of the project pipeline
2489 # variants
2490 matched = False
2491 for variant in job_list.jobs[jobname]:
2492 if variant.changeMatches(change):
2493 frozen_job.applyVariant(variant)
2494 matched = True
2495 if not matched:
2496 # A change must match at least one project pipeline
2497 # job variant.
2498 continue
James E. Blairb3f5db12017-03-17 12:57:39 -07002499 if (frozen_job.allowed_projects and
2500 change.project.name not in frozen_job.allowed_projects):
2501 raise Exception("Project %s is not allowed to run job %s" %
2502 (change.project.name, frozen_job.name))
James E. Blair8eb564a2017-08-10 09:21:41 -07002503 if ((not pipeline.post_review) and frozen_job.post_review):
2504 raise Exception("Pre-review pipeline %s does not allow "
2505 "post-review job %s" % (
James E. Blaird2348362017-03-17 13:59:35 -07002506 pipeline.name, frozen_job.name))
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002507 job_graph.addJob(frozen_job)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002508
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002509 def createJobGraph(self, item):
Paul Belanger15e3e202016-10-14 16:27:34 -04002510 # NOTE(pabelanger): It is possible for a foreign project not to have a
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002511 # configured pipeline, if so return an empty JobGraph.
James E. Blairc9455002017-09-06 09:22:19 -07002512 ret = JobGraph()
2513 ppc = self.getProjectPipelineConfig(item.change.project,
2514 item.pipeline)
2515 if ppc:
2516 self._createJobGraph(item, ppc.job_list, ret)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002517 return ret
2518
James E. Blairc9455002017-09-06 09:22:19 -07002519 def getProjectPipelineConfig(self, project, pipeline):
2520 project_config = self.project_configs.get(
2521 project.canonical_name, None)
2522 if not project_config:
2523 return None
2524 return project_config.pipelines.get(pipeline.name, None)
James E. Blair0d3e83b2017-06-05 13:51:57 -07002525
James E. Blair59fdbac2015-12-07 17:08:06 -08002526
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002527class Semaphore(object):
2528 def __init__(self, name, max=1):
2529 self.name = name
2530 self.max = int(max)
2531
2532
2533class SemaphoreHandler(object):
2534 log = logging.getLogger("zuul.SemaphoreHandler")
2535
2536 def __init__(self):
2537 self.semaphores = {}
2538
2539 def acquire(self, item, job):
2540 if not job.semaphore:
2541 return True
2542
2543 semaphore_key = job.semaphore
2544
2545 m = self.semaphores.get(semaphore_key)
2546 if not m:
2547 # The semaphore is not held, acquire it
2548 self._acquire(semaphore_key, item, job.name)
2549 return True
2550 if (item, job.name) in m:
2551 # This item already holds the semaphore
2552 return True
2553
2554 # semaphore is there, check max
2555 if len(m) < self._max_count(item, job.semaphore):
2556 self._acquire(semaphore_key, item, job.name)
2557 return True
2558
2559 return False
2560
2561 def release(self, item, job):
2562 if not job.semaphore:
2563 return
2564
2565 semaphore_key = job.semaphore
2566
2567 m = self.semaphores.get(semaphore_key)
2568 if not m:
2569 # The semaphore is not held, nothing to do
2570 self.log.error("Semaphore can not be released for %s "
2571 "because the semaphore is not held" %
2572 item)
2573 return
2574 if (item, job.name) in m:
2575 # This item is a holder of the semaphore
2576 self._release(semaphore_key, item, job.name)
2577 return
2578 self.log.error("Semaphore can not be released for %s "
2579 "which does not hold it" % item)
2580
2581 def _acquire(self, semaphore_key, item, job_name):
2582 self.log.debug("Semaphore acquire {semaphore}: job {job}, item {item}"
2583 .format(semaphore=semaphore_key,
2584 job=job_name,
2585 item=item))
2586 if semaphore_key not in self.semaphores:
2587 self.semaphores[semaphore_key] = []
2588 self.semaphores[semaphore_key].append((item, job_name))
2589
2590 def _release(self, semaphore_key, item, job_name):
2591 self.log.debug("Semaphore release {semaphore}: job {job}, item {item}"
2592 .format(semaphore=semaphore_key,
2593 job=job_name,
2594 item=item))
2595 sem_item = (item, job_name)
2596 if sem_item in self.semaphores[semaphore_key]:
2597 self.semaphores[semaphore_key].remove(sem_item)
2598
2599 # cleanup if there is no user of the semaphore anymore
2600 if len(self.semaphores[semaphore_key]) == 0:
2601 del self.semaphores[semaphore_key]
2602
2603 @staticmethod
2604 def _max_count(item, semaphore_name):
James E. Blair29a24fd2017-10-02 15:04:56 -07002605 if not item.layout:
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002606 # This should not occur as the layout of the item must already be
2607 # built when acquiring or releasing a semaphore for a job.
2608 raise Exception("Item {} has no layout".format(item))
2609
2610 # find the right semaphore
2611 default_semaphore = Semaphore(semaphore_name, 1)
James E. Blair29a24fd2017-10-02 15:04:56 -07002612 semaphores = item.layout.semaphores
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002613 return semaphores.get(semaphore_name, default_semaphore).max
2614
2615
James E. Blair59fdbac2015-12-07 17:08:06 -08002616class Tenant(object):
2617 def __init__(self, name):
2618 self.name = name
Tristan Cacqueray82f864b2017-08-01 05:54:42 +00002619 self.max_nodes_per_job = 5
Tristan Cacquerayc98bff72017-09-10 15:25:26 +00002620 self.max_job_timeout = 10800
Tobias Henkeleca46202017-08-02 20:27:10 +02002621 self.exclude_unprotected_branches = False
James E. Blair2bab6e72017-08-07 09:52:45 -07002622 self.default_base_job = None
James E. Blair59fdbac2015-12-07 17:08:06 -08002623 self.layout = None
James E. Blair646322f2017-01-27 15:50:34 -08002624 # The unparsed configuration from the main zuul config for
2625 # this tenant.
2626 self.unparsed_config = None
James E. Blair109da3f2017-04-04 14:39:43 -07002627 # The list of projects from which we will read full
James E. Blair8a395f92017-03-30 11:15:33 -07002628 # configuration.
James E. Blair109da3f2017-04-04 14:39:43 -07002629 self.config_projects = []
2630 # The unparsed config from those projects.
2631 self.config_projects_config = None
2632 # The list of projects from which we will read untrusted
2633 # in-repo configuration.
2634 self.untrusted_projects = []
2635 # The unparsed config from those projects.
2636 self.untrusted_projects_config = None
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002637 self.semaphore_handler = SemaphoreHandler()
James E. Blair08d9b782017-06-29 14:22:48 -07002638 # Metadata about projects for this tenant
2639 # canonical project name -> TenantProjectConfig
2640 self.project_configs = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002641
James E. Blairc2a54fd2017-03-29 15:19:26 -07002642 # A mapping of project names to projects. project_name ->
2643 # VALUE where VALUE is a further dictionary of
2644 # canonical_hostname -> Project.
2645 self.projects = {}
2646 self.canonical_hostnames = set()
2647
James E. Blair08d9b782017-06-29 14:22:48 -07002648 def _addProject(self, tpc):
James E. Blairc2a54fd2017-03-29 15:19:26 -07002649 """Add a project to the project index
2650
James E. Blair08d9b782017-06-29 14:22:48 -07002651 :arg TenantProjectConfig tpc: The TenantProjectConfig (with
2652 associated project) to add.
2653
James E. Blairc2a54fd2017-03-29 15:19:26 -07002654 """
James E. Blair08d9b782017-06-29 14:22:48 -07002655 project = tpc.project
James E. Blairc2a54fd2017-03-29 15:19:26 -07002656 self.canonical_hostnames.add(project.canonical_hostname)
2657 hostname_dict = self.projects.setdefault(project.name, {})
2658 if project.canonical_hostname in hostname_dict:
2659 raise Exception("Project %s is already in project index" %
2660 (project,))
2661 hostname_dict[project.canonical_hostname] = project
James E. Blair08d9b782017-06-29 14:22:48 -07002662 self.project_configs[project.canonical_name] = tpc
James E. Blairc2a54fd2017-03-29 15:19:26 -07002663
2664 def getProject(self, name):
2665 """Return a project given its name.
2666
2667 :arg str name: The name of the project. It may be fully
2668 qualified (E.g., "git.example.com/subpath/project") or may
2669 contain only the project name name may be supplied (E.g.,
2670 "subpath/project").
2671
2672 :returns: A tuple (trusted, project) or (None, None) if the
2673 project is not found or ambiguous. The "trusted" boolean
2674 indicates whether or not the project is trusted by this
2675 tenant.
2676 :rtype: (bool, Project)
2677
2678 """
2679 path = name.split('/', 1)
2680 if path[0] in self.canonical_hostnames:
2681 hostname = path[0]
2682 project_name = path[1]
2683 else:
2684 hostname = None
2685 project_name = name
2686 hostname_dict = self.projects.get(project_name)
2687 project = None
2688 if hostname_dict:
2689 if hostname:
2690 project = hostname_dict.get(hostname)
2691 else:
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002692 values = list(hostname_dict.values())
James E. Blairc2a54fd2017-03-29 15:19:26 -07002693 if len(values) == 1:
2694 project = values[0]
2695 else:
2696 raise Exception("Project name '%s' is ambiguous, "
2697 "please fully qualify the project "
2698 "with a hostname" % (name,))
2699 if project is None:
2700 return (None, None)
James E. Blair109da3f2017-04-04 14:39:43 -07002701 if project in self.config_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002702 return (True, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002703 if project in self.untrusted_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002704 return (False, project)
2705 # This should never happen:
2706 raise Exception("Project %s is neither trusted nor untrusted" %
2707 (project,))
2708
James E. Blairdaaf3262017-10-23 13:51:48 -07002709 def getProjectBranches(self, project):
2710 """Return a project's branches (filtered by this tenant config)
2711
2712 :arg Project project: The project object.
2713
2714 :returns: A list of branch names.
2715 :rtype: [str]
2716
2717 """
2718 tpc = self.project_configs[project.canonical_name]
2719 return tpc.branches
2720
James E. Blair08d9b782017-06-29 14:22:48 -07002721 def addConfigProject(self, tpc):
2722 self.config_projects.append(tpc.project)
2723 self._addProject(tpc)
James E. Blair5ac93842017-01-20 06:47:34 -08002724
James E. Blair08d9b782017-06-29 14:22:48 -07002725 def addUntrustedProject(self, tpc):
2726 self.untrusted_projects.append(tpc.project)
2727 self._addProject(tpc)
James E. Blair5ac93842017-01-20 06:47:34 -08002728
Jamie Lennoxbf997c82017-05-10 09:58:40 +10002729 def getSafeAttributes(self):
2730 return Attributes(name=self.name)
2731
James E. Blair59fdbac2015-12-07 17:08:06 -08002732
2733class Abide(object):
2734 def __init__(self):
2735 self.tenants = OrderedDict()
James E. Blairce8a2132016-05-19 15:21:52 -07002736
2737
2738class JobTimeData(object):
2739 format = 'B10H10H10B'
2740 version = 0
2741
2742 def __init__(self, path):
2743 self.path = path
2744 self.success_times = [0 for x in range(10)]
2745 self.failure_times = [0 for x in range(10)]
2746 self.results = [0 for x in range(10)]
2747
2748 def load(self):
2749 if not os.path.exists(self.path):
2750 return
Clint Byruma4471d12017-05-10 20:57:40 -04002751 with open(self.path, 'rb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002752 data = struct.unpack(self.format, f.read())
2753 version = data[0]
2754 if version != self.version:
2755 raise Exception("Unkown data version")
2756 self.success_times = list(data[1:11])
2757 self.failure_times = list(data[11:21])
2758 self.results = list(data[21:32])
2759
2760 def save(self):
2761 tmpfile = self.path + '.tmp'
2762 data = [self.version]
2763 data.extend(self.success_times)
2764 data.extend(self.failure_times)
2765 data.extend(self.results)
2766 data = struct.pack(self.format, *data)
Clint Byruma4471d12017-05-10 20:57:40 -04002767 with open(tmpfile, 'wb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002768 f.write(data)
2769 os.rename(tmpfile, self.path)
2770
2771 def add(self, elapsed, result):
2772 elapsed = int(elapsed)
2773 if result == 'SUCCESS':
2774 self.success_times.append(elapsed)
2775 self.success_times.pop(0)
2776 result = 0
2777 else:
2778 self.failure_times.append(elapsed)
2779 self.failure_times.pop(0)
2780 result = 1
2781 self.results.append(result)
2782 self.results.pop(0)
2783
2784 def getEstimatedTime(self):
2785 times = [x for x in self.success_times if x]
2786 if times:
2787 return float(sum(times)) / len(times)
2788 return 0.0
2789
2790
2791class TimeDataBase(object):
2792 def __init__(self, root):
2793 self.root = root
James E. Blairce8a2132016-05-19 15:21:52 -07002794
James E. Blairae0f23c2017-09-13 10:55:15 -06002795 def _getTD(self, build):
2796 if hasattr(build.build_set.item.change, 'branch'):
2797 branch = build.build_set.item.change.branch
2798 else:
2799 branch = ''
2800
2801 dir_path = os.path.join(
2802 self.root,
2803 build.build_set.item.pipeline.layout.tenant.name,
2804 build.build_set.item.change.project.canonical_name,
2805 branch)
2806 if not os.path.exists(dir_path):
2807 os.makedirs(dir_path)
2808 path = os.path.join(dir_path, build.job.name)
2809
2810 td = JobTimeData(path)
2811 td.load()
James E. Blairce8a2132016-05-19 15:21:52 -07002812 return td
2813
2814 def getEstimatedTime(self, name):
2815 return self._getTD(name).getEstimatedTime()
2816
James E. Blairae0f23c2017-09-13 10:55:15 -06002817 def update(self, build, elapsed, result):
2818 td = self._getTD(build)
James E. Blairce8a2132016-05-19 15:21:52 -07002819 td.add(elapsed, result)
2820 td.save()