blob: ed77864f9069ef7f0ffacc88658342ddf93f0a49 [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. Blair5a9918a2013-08-27 10:06:27 -070024
James E. Blair19deff22013-08-25 13:17:35 -070025MERGER_MERGE = 1 # "git merge"
26MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
27MERGER_CHERRY_PICK = 3 # "git cherry-pick"
28
29MERGER_MAP = {
30 'merge': MERGER_MERGE,
31 'merge-resolve': MERGER_MERGE_RESOLVE,
32 'cherry-pick': MERGER_CHERRY_PICK,
33}
James E. Blairee743612012-05-29 14:49:32 -070034
James E. Blair64ed6f22013-07-10 14:07:23 -070035PRECEDENCE_NORMAL = 0
36PRECEDENCE_LOW = 1
37PRECEDENCE_HIGH = 2
38
39PRECEDENCE_MAP = {
40 None: PRECEDENCE_NORMAL,
41 'low': PRECEDENCE_LOW,
42 'normal': PRECEDENCE_NORMAL,
43 'high': PRECEDENCE_HIGH,
44}
45
James E. Blair803e94f2017-01-06 09:18:59 -080046# Request states
47STATE_REQUESTED = 'requested'
48STATE_PENDING = 'pending'
49STATE_FULFILLED = 'fulfilled'
50STATE_FAILED = 'failed'
51REQUEST_STATES = set([STATE_REQUESTED,
52 STATE_PENDING,
53 STATE_FULFILLED,
54 STATE_FAILED])
55
56# Node states
57STATE_BUILDING = 'building'
58STATE_TESTING = 'testing'
59STATE_READY = 'ready'
60STATE_IN_USE = 'in-use'
61STATE_USED = 'used'
62STATE_HOLD = 'hold'
63STATE_DELETING = 'deleting'
64NODE_STATES = set([STATE_BUILDING,
65 STATE_TESTING,
66 STATE_READY,
67 STATE_IN_USE,
68 STATE_USED,
69 STATE_HOLD,
70 STATE_DELETING])
71
James E. Blair1e8dd892012-05-30 09:15:05 -070072
Joshua Hesketh58419cb2017-02-24 13:09:22 -050073class Attributes(object):
74 """A class to hold attributes for string formatting."""
75
76 def __init__(self, **kw):
77 setattr(self, '__dict__', kw)
78
79
James E. Blair4aea70c2012-07-26 14:23:24 -070080class Pipeline(object):
James E. Blair6053de42017-04-05 11:27:11 -070081 """A configuration that ties together triggers, reporters and managers
Monty Taylor82dfd412016-07-29 12:01:28 -070082
83 Trigger
84 A description of which events should be processed
85
86 Manager
87 Responsible for enqueing and dequeing Changes
88
89 Reporter
90 Communicates success and failure results somewhere
Monty Taylora42a55b2016-07-29 07:53:33 -070091 """
James E. Blair83005782015-12-11 14:46:03 -080092 def __init__(self, name, layout):
James E. Blair4aea70c2012-07-26 14:23:24 -070093 self.name = name
James E. Blair83005782015-12-11 14:46:03 -080094 self.layout = layout
James E. Blair8dbd56a2012-12-22 10:55:10 -080095 self.description = None
James E. Blair56370192013-01-14 15:47:28 -080096 self.failure_message = None
Joshua Heskethb7179772014-01-30 23:30:46 +110097 self.merge_failure_message = None
James E. Blair56370192013-01-14 15:47:28 -080098 self.success_message = None
Joshua Hesketh3979e3e2014-03-04 11:21:10 +110099 self.footer_message = None
James E. Blair60af7f42016-03-11 16:11:06 -0800100 self.start_message = None
James E. Blaird2348362017-03-17 13:59:35 -0700101 self.allow_secrets = False
James E. Blair2fa50962013-01-30 21:50:41 -0800102 self.dequeue_on_new_patchset = True
James E. Blair17dd6772015-02-09 14:45:18 -0800103 self.ignore_dependencies = False
James E. Blair4aea70c2012-07-26 14:23:24 -0700104 self.manager = None
James E. Blaire0487072012-08-29 17:38:31 -0700105 self.queues = []
James E. Blair64ed6f22013-07-10 14:07:23 -0700106 self.precedence = PRECEDENCE_NORMAL
James E. Blair83005782015-12-11 14:46:03 -0800107 self.triggers = []
Joshua Hesketh352264b2015-08-11 23:42:08 +1000108 self.start_actions = []
109 self.success_actions = []
110 self.failure_actions = []
111 self.merge_failure_actions = []
112 self.disabled_actions = []
Joshua Hesketh89e829d2015-02-10 16:29:45 +1100113 self.disable_at = None
114 self._consecutive_failures = 0
115 self._disabled = False
Clark Boylan7603a372014-01-21 11:43:20 -0800116 self.window = None
117 self.window_floor = None
118 self.window_increase_type = None
119 self.window_increase_factor = None
120 self.window_decrease_type = None
121 self.window_decrease_factor = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700122
James E. Blair83005782015-12-11 14:46:03 -0800123 @property
124 def actions(self):
125 return (
126 self.start_actions +
127 self.success_actions +
128 self.failure_actions +
129 self.merge_failure_actions +
130 self.disabled_actions
131 )
132
James E. Blaird09c17a2012-08-07 09:23:14 -0700133 def __repr__(self):
134 return '<Pipeline %s>' % self.name
135
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100136 def getSafeAttributes(self):
137 return Attributes(name=self.name)
138
James E. Blair4aea70c2012-07-26 14:23:24 -0700139 def setManager(self, manager):
140 self.manager = manager
141
James E. Blaire0487072012-08-29 17:38:31 -0700142 def addQueue(self, queue):
143 self.queues.append(queue)
144
145 def getQueue(self, project):
146 for queue in self.queues:
147 if project in queue.projects:
148 return queue
149 return None
150
James E. Blairbfb8e042014-12-30 17:01:44 -0800151 def removeQueue(self, queue):
Tobias Henkel6b9390f2017-03-28 11:23:21 +0200152 if queue in self.queues:
153 self.queues.remove(queue)
James E. Blairbfb8e042014-12-30 17:01:44 -0800154
James E. Blaire0487072012-08-29 17:38:31 -0700155 def getChangesInQueue(self):
156 changes = []
157 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700158 changes.extend([x.change for x in shared_queue.queue])
James E. Blaire0487072012-08-29 17:38:31 -0700159 return changes
160
James E. Blairfee8d652013-06-07 08:57:52 -0700161 def getAllItems(self):
162 items = []
James E. Blaire0487072012-08-29 17:38:31 -0700163 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700164 items.extend(shared_queue.queue)
James E. Blairfee8d652013-06-07 08:57:52 -0700165 return items
James E. Blaire0487072012-08-29 17:38:31 -0700166
Tobias Henkelb4407fc2017-07-07 13:52:56 +0200167 def formatStatusJSON(self, websocket_url=None):
James E. Blair8dbd56a2012-12-22 10:55:10 -0800168 j_pipeline = dict(name=self.name,
169 description=self.description)
170 j_queues = []
171 j_pipeline['change_queues'] = j_queues
172 for queue in self.queues:
173 j_queue = dict(name=queue.name)
174 j_queues.append(j_queue)
175 j_queue['heads'] = []
Clark Boylanaf2476f2014-01-23 14:47:36 -0800176 j_queue['window'] = queue.window
James E. Blair972e3c72013-08-29 12:04:55 -0700177
178 j_changes = []
179 for e in queue.queue:
180 if not e.item_ahead:
181 if j_changes:
182 j_queue['heads'].append(j_changes)
183 j_changes = []
Tobias Henkelb4407fc2017-07-07 13:52:56 +0200184 j_changes.append(e.formatJSON(websocket_url))
James E. Blair972e3c72013-08-29 12:04:55 -0700185 if (len(j_changes) > 1 and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000186 (j_changes[-2]['remaining_time'] is not None) and
187 (j_changes[-1]['remaining_time'] is not None)):
James E. Blair972e3c72013-08-29 12:04:55 -0700188 j_changes[-1]['remaining_time'] = max(
189 j_changes[-2]['remaining_time'],
190 j_changes[-1]['remaining_time'])
191 if j_changes:
James E. Blair8dbd56a2012-12-22 10:55:10 -0800192 j_queue['heads'].append(j_changes)
193 return j_pipeline
194
James E. Blair4aea70c2012-07-26 14:23:24 -0700195
James E. Blairee743612012-05-29 14:49:32 -0700196class ChangeQueue(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700197 """A ChangeQueue contains Changes to be processed related projects.
198
Monty Taylor82dfd412016-07-29 12:01:28 -0700199 A Pipeline with a DependentPipelineManager has multiple parallel
200 ChangeQueues shared by different projects. For instance, there may a
201 ChangeQueue shared by interrelated projects foo and bar, and a second queue
202 for independent project baz.
Monty Taylora42a55b2016-07-29 07:53:33 -0700203
Monty Taylor82dfd412016-07-29 12:01:28 -0700204 A Pipeline with an IndependentPipelineManager puts every Change into its
205 own ChangeQueue
Monty Taylora42a55b2016-07-29 07:53:33 -0700206
207 The ChangeQueue Window is inspired by TCP windows and controlls how many
208 Changes in a given ChangeQueue will be considered active and ready to
209 be processed. If a Change succeeds, the Window is increased by
210 `window_increase_factor`. If a Change fails, the Window is decreased by
211 `window_decrease_factor`.
Jesse Keating78f544a2017-07-13 14:27:40 -0700212
213 A ChangeQueue may be a dynamically created queue, which may be removed
214 from a DependentPipelineManager once empty.
Monty Taylora42a55b2016-07-29 07:53:33 -0700215 """
James E. Blairbfb8e042014-12-30 17:01:44 -0800216 def __init__(self, pipeline, window=0, window_floor=1,
Clark Boylan7603a372014-01-21 11:43:20 -0800217 window_increase_type='linear', window_increase_factor=1,
James E. Blair0dcef7a2016-08-19 09:35:17 -0700218 window_decrease_type='exponential', window_decrease_factor=2,
Jesse Keating78f544a2017-07-13 14:27:40 -0700219 name=None, dynamic=False):
James E. Blair4aea70c2012-07-26 14:23:24 -0700220 self.pipeline = pipeline
James E. Blair0dcef7a2016-08-19 09:35:17 -0700221 if name:
222 self.name = name
223 else:
224 self.name = ''
James E. Blairee743612012-05-29 14:49:32 -0700225 self.projects = []
226 self._jobs = set()
227 self.queue = []
Clark Boylan7603a372014-01-21 11:43:20 -0800228 self.window = window
229 self.window_floor = window_floor
230 self.window_increase_type = window_increase_type
231 self.window_increase_factor = window_increase_factor
232 self.window_decrease_type = window_decrease_type
233 self.window_decrease_factor = window_decrease_factor
Jesse Keating78f544a2017-07-13 14:27:40 -0700234 self.dynamic = dynamic
James E. Blairee743612012-05-29 14:49:32 -0700235
James E. Blair9f9667e2012-06-12 17:51:08 -0700236 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700237 return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
James E. Blairee743612012-05-29 14:49:32 -0700238
239 def getJobs(self):
240 return self._jobs
241
242 def addProject(self, project):
243 if project not in self.projects:
244 self.projects.append(project)
James E. Blairc8a1e052014-02-25 09:29:26 -0800245
James E. Blair0dcef7a2016-08-19 09:35:17 -0700246 if not self.name:
247 self.name = project.name
James E. Blairee743612012-05-29 14:49:32 -0700248
249 def enqueueChange(self, change):
James E. Blairbfb8e042014-12-30 17:01:44 -0800250 item = QueueItem(self, change)
James E. Blaircdccd972013-07-01 12:10:22 -0700251 self.enqueueItem(item)
252 item.enqueue_time = time.time()
253 return item
254
255 def enqueueItem(self, item):
James E. Blair4a035d92014-01-23 13:10:48 -0800256 item.pipeline = self.pipeline
James E. Blairbfb8e042014-12-30 17:01:44 -0800257 item.queue = self
258 if self.queue:
James E. Blairfee8d652013-06-07 08:57:52 -0700259 item.item_ahead = self.queue[-1]
James E. Blair972e3c72013-08-29 12:04:55 -0700260 item.item_ahead.items_behind.append(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700261 self.queue.append(item)
James E. Blairee743612012-05-29 14:49:32 -0700262
James E. Blairfee8d652013-06-07 08:57:52 -0700263 def dequeueItem(self, item):
264 if item in self.queue:
265 self.queue.remove(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700266 if item.item_ahead:
James E. Blair972e3c72013-08-29 12:04:55 -0700267 item.item_ahead.items_behind.remove(item)
268 for item_behind in item.items_behind:
269 if item.item_ahead:
270 item.item_ahead.items_behind.append(item_behind)
271 item_behind.item_ahead = item.item_ahead
James E. Blairfee8d652013-06-07 08:57:52 -0700272 item.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700273 item.items_behind = []
James E. Blairfee8d652013-06-07 08:57:52 -0700274 item.dequeue_time = time.time()
James E. Blaire0487072012-08-29 17:38:31 -0700275
James E. Blair972e3c72013-08-29 12:04:55 -0700276 def moveItem(self, item, item_ahead):
James E. Blair972e3c72013-08-29 12:04:55 -0700277 if item.item_ahead == item_ahead:
278 return False
279 # Remove from current location
280 if item.item_ahead:
281 item.item_ahead.items_behind.remove(item)
282 for item_behind in item.items_behind:
283 if item.item_ahead:
284 item.item_ahead.items_behind.append(item_behind)
285 item_behind.item_ahead = item.item_ahead
286 # Add to new location
287 item.item_ahead = item_ahead
James E. Blair00451262013-09-20 11:40:17 -0700288 item.items_behind = []
James E. Blair972e3c72013-08-29 12:04:55 -0700289 if item.item_ahead:
290 item.item_ahead.items_behind.append(item)
291 return True
James E. Blairee743612012-05-29 14:49:32 -0700292
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800293 def isActionable(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800294 if self.window:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800295 return item in self.queue[:self.window]
Clark Boylan7603a372014-01-21 11:43:20 -0800296 else:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800297 return True
Clark Boylan7603a372014-01-21 11:43:20 -0800298
299 def increaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800300 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800301 if self.window_increase_type == 'linear':
302 self.window += self.window_increase_factor
303 elif self.window_increase_type == 'exponential':
304 self.window *= self.window_increase_factor
305
306 def decreaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800307 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800308 if self.window_decrease_type == 'linear':
309 self.window = max(
310 self.window_floor,
311 self.window - self.window_decrease_factor)
312 elif self.window_decrease_type == 'exponential':
313 self.window = max(
314 self.window_floor,
Morgan Fainbergfdae2252016-06-07 20:13:20 -0700315 int(self.window / self.window_decrease_factor))
James E. Blairee743612012-05-29 14:49:32 -0700316
James E. Blair1e8dd892012-05-30 09:15:05 -0700317
James E. Blair4aea70c2012-07-26 14:23:24 -0700318class Project(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700319 """A Project represents a git repository such as openstack/nova."""
320
James E. Blaircf440a22016-07-15 09:11:58 -0700321 # NOTE: Projects should only be instantiated via a Source object
322 # so that they are associated with and cached by their Connection.
323 # This makes a Project instance a unique identifier for a given
324 # project from a given source.
325
James E. Blair0a899752017-03-29 13:22:16 -0700326 def __init__(self, name, source, foreign=False):
James E. Blair4aea70c2012-07-26 14:23:24 -0700327 self.name = name
James E. Blair8a395f92017-03-30 11:15:33 -0700328 self.source = source
James E. Blair0a899752017-03-29 13:22:16 -0700329 self.connection_name = source.connection.connection_name
330 self.canonical_hostname = source.canonical_hostname
James E. Blairc2a54fd2017-03-29 15:19:26 -0700331 self.canonical_name = source.canonical_hostname + '/' + name
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000332 # foreign projects are those referenced in dependencies
333 # of layout projects, this should matter
334 # when deciding whether to enqueue their changes
James E. Blaircf440a22016-07-15 09:11:58 -0700335 # TODOv3 (jeblair): re-add support for foreign projects if needed
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000336 self.foreign = foreign
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700337 self.unparsed_config = None
James E. Blaire3162022017-02-20 16:47:27 -0500338 self.unparsed_branch_config = {} # branch -> UnparsedTenantConfig
James E. Blair4aea70c2012-07-26 14:23:24 -0700339
340 def __str__(self):
341 return self.name
342
343 def __repr__(self):
344 return '<Project %s>' % (self.name)
345
346
James E. Blair34776ee2016-08-25 13:53:54 -0700347class Node(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700348 """A single node for use by a job.
349
350 This may represent a request for a node, or an actual node
351 provided by Nodepool.
352 """
353
James E. Blair16d96a02017-06-08 11:32:56 -0700354 def __init__(self, name, label):
James E. Blair34776ee2016-08-25 13:53:54 -0700355 self.name = name
James E. Blair16d96a02017-06-08 11:32:56 -0700356 self.label = label
James E. Blaircbf43672017-01-04 14:33:41 -0800357 self.id = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800358 self.lock = None
359 # Attributes from Nodepool
360 self._state = 'unknown'
361 self.state_time = time.time()
Monty Taylor56f61332017-04-11 05:38:12 -0500362 self.interface_ip = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800363 self.public_ipv4 = None
364 self.private_ipv4 = None
365 self.public_ipv6 = None
Tristan Cacqueray80954402017-05-28 00:33:55 +0000366 self.ssh_port = 22
James E. Blaircacdf2b2017-01-04 13:14:37 -0800367 self._keys = []
Paul Belanger30ba93a2017-03-16 16:28:10 -0400368 self.az = None
369 self.provider = None
370 self.region = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800371
372 @property
373 def state(self):
374 return self._state
375
376 @state.setter
377 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800378 if value not in NODE_STATES:
379 raise TypeError("'%s' is not a valid state" % value)
James E. Blaira38c28e2017-01-04 10:33:20 -0800380 self._state = value
381 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700382
383 def __repr__(self):
James E. Blair16d96a02017-06-08 11:32:56 -0700384 return '<Node %s %s:%s>' % (self.id, self.name, self.label)
James E. Blair34776ee2016-08-25 13:53:54 -0700385
James E. Blair0d952152017-02-07 17:14:44 -0800386 def __ne__(self, other):
387 return not self.__eq__(other)
388
389 def __eq__(self, other):
390 if not isinstance(other, Node):
391 return False
392 return (self.name == other.name and
James E. Blair16d96a02017-06-08 11:32:56 -0700393 self.label == other.label and
James E. Blair0d952152017-02-07 17:14:44 -0800394 self.id == other.id)
395
James E. Blaircacdf2b2017-01-04 13:14:37 -0800396 def toDict(self):
397 d = {}
398 d['state'] = self.state
399 for k in self._keys:
400 d[k] = getattr(self, k)
401 return d
402
James E. Blaira38c28e2017-01-04 10:33:20 -0800403 def updateFromDict(self, data):
404 self._state = data['state']
James E. Blaircacdf2b2017-01-04 13:14:37 -0800405 keys = []
406 for k, v in data.items():
407 if k == 'state':
408 continue
409 keys.append(k)
410 setattr(self, k, v)
411 self._keys = keys
James E. Blaira38c28e2017-01-04 10:33:20 -0800412
James E. Blair34776ee2016-08-25 13:53:54 -0700413
Monty Taylor7b19ba72017-05-24 07:42:54 -0500414class Group(object):
415 """A logical group of nodes for use by a job.
416
417 A Group is a named set of node names that will be provided to
418 jobs in the inventory to describe logical units where some subset of tasks
419 run.
420 """
421
422 def __init__(self, name, nodes):
423 self.name = name
424 self.nodes = nodes
425
426 def __repr__(self):
427 return '<Group %s %s>' % (self.name, str(self.nodes))
428
429 def __ne__(self, other):
430 return not self.__eq__(other)
431
432 def __eq__(self, other):
433 if not isinstance(other, Group):
434 return False
435 return (self.name == other.name and
436 self.nodes == other.nodes)
437
438 def toDict(self):
439 return {
440 'name': self.name,
441 'nodes': self.nodes
442 }
443
444
James E. Blaira98340f2016-09-02 11:33:49 -0700445class NodeSet(object):
446 """A set of nodes.
447
448 In configuration, NodeSets are attributes of Jobs indicating that
449 a Job requires nodes matching this description.
450
451 They may appear as top-level configuration objects and be named,
452 or they may appears anonymously in in-line job definitions.
453 """
454
455 def __init__(self, name=None):
456 self.name = name or ''
457 self.nodes = OrderedDict()
Monty Taylor7b19ba72017-05-24 07:42:54 -0500458 self.groups = OrderedDict()
James E. Blaira98340f2016-09-02 11:33:49 -0700459
James E. Blair1774dd52017-02-03 10:52:32 -0800460 def __ne__(self, other):
461 return not self.__eq__(other)
462
463 def __eq__(self, other):
464 if not isinstance(other, NodeSet):
465 return False
466 return (self.name == other.name and
467 self.nodes == other.nodes)
468
James E. Blaircbf43672017-01-04 14:33:41 -0800469 def copy(self):
470 n = NodeSet(self.name)
471 for name, node in self.nodes.items():
James E. Blair16d96a02017-06-08 11:32:56 -0700472 n.addNode(Node(node.name, node.label))
Monty Taylor7b19ba72017-05-24 07:42:54 -0500473 for name, group in self.groups.items():
474 n.addGroup(Group(group.name, group.nodes[:]))
James E. Blaircbf43672017-01-04 14:33:41 -0800475 return n
476
James E. Blaira98340f2016-09-02 11:33:49 -0700477 def addNode(self, node):
478 if node.name in self.nodes:
479 raise Exception("Duplicate node in %s" % (self,))
480 self.nodes[node.name] = node
481
James E. Blair0eaad552016-09-02 12:09:54 -0700482 def getNodes(self):
Clint Byruma4471d12017-05-10 20:57:40 -0400483 return list(self.nodes.values())
James E. Blair0eaad552016-09-02 12:09:54 -0700484
Monty Taylor7b19ba72017-05-24 07:42:54 -0500485 def addGroup(self, group):
486 if group.name in self.groups:
487 raise Exception("Duplicate group in %s" % (self,))
488 self.groups[group.name] = group
489
490 def getGroups(self):
491 return list(self.groups.values())
492
James E. Blaira98340f2016-09-02 11:33:49 -0700493 def __repr__(self):
494 if self.name:
495 name = self.name + ' '
496 else:
497 name = ''
Monty Taylor7b19ba72017-05-24 07:42:54 -0500498 return '<NodeSet %s%s%s>' % (name, self.nodes, self.groups)
James E. Blaira98340f2016-09-02 11:33:49 -0700499
500
James E. Blair34776ee2016-08-25 13:53:54 -0700501class NodeRequest(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700502 """A request for a set of nodes."""
503
James E. Blair8b2a1472017-02-19 15:33:55 -0800504 def __init__(self, requestor, build_set, job, nodeset):
505 self.requestor = requestor
James E. Blair34776ee2016-08-25 13:53:54 -0700506 self.build_set = build_set
507 self.job = job
James E. Blair0eaad552016-09-02 12:09:54 -0700508 self.nodeset = nodeset
James E. Blair803e94f2017-01-06 09:18:59 -0800509 self._state = STATE_REQUESTED
James E. Blairdce6cea2016-12-20 16:45:32 -0800510 self.state_time = time.time()
511 self.stat = None
512 self.uid = uuid4().hex
James E. Blaircbf43672017-01-04 14:33:41 -0800513 self.id = None
James E. Blaircbbce0d2017-05-19 07:28:29 -0700514 # Zuul internal flags (not stored in ZK so they are not
James E. Blaira38c28e2017-01-04 10:33:20 -0800515 # overwritten).
516 self.failed = False
James E. Blaircbbce0d2017-05-19 07:28:29 -0700517 self.canceled = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800518
519 @property
James E. Blair6ab79e02017-01-06 10:10:17 -0800520 def fulfilled(self):
521 return (self._state == STATE_FULFILLED) and not self.failed
522
523 @property
James E. Blairdce6cea2016-12-20 16:45:32 -0800524 def state(self):
525 return self._state
526
527 @state.setter
528 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800529 if value not in REQUEST_STATES:
530 raise TypeError("'%s' is not a valid state" % value)
James E. Blairdce6cea2016-12-20 16:45:32 -0800531 self._state = value
532 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700533
534 def __repr__(self):
James E. Blaircbf43672017-01-04 14:33:41 -0800535 return '<NodeRequest %s %s>' % (self.id, self.nodeset)
James E. Blair34776ee2016-08-25 13:53:54 -0700536
James E. Blairdce6cea2016-12-20 16:45:32 -0800537 def toDict(self):
538 d = {}
James E. Blair16d96a02017-06-08 11:32:56 -0700539 nodes = [n.label for n in self.nodeset.getNodes()]
James E. Blairdce6cea2016-12-20 16:45:32 -0800540 d['node_types'] = nodes
James E. Blair8b2a1472017-02-19 15:33:55 -0800541 d['requestor'] = self.requestor
James E. Blairdce6cea2016-12-20 16:45:32 -0800542 d['state'] = self.state
543 d['state_time'] = self.state_time
544 return d
545
546 def updateFromDict(self, data):
547 self._state = data['state']
548 self.state_time = data['state_time']
549
James E. Blair34776ee2016-08-25 13:53:54 -0700550
James E. Blair01f83b72017-03-15 13:03:40 -0700551class Secret(object):
552 """A collection of private data.
553
554 In configuration, Secrets are collections of private data in
555 key-value pair format. They are defined as top-level
556 configuration objects and then referenced by Jobs.
557
558 """
559
James E. Blair8525e2b2017-03-15 14:05:47 -0700560 def __init__(self, name, source_context):
James E. Blair01f83b72017-03-15 13:03:40 -0700561 self.name = name
James E. Blair8525e2b2017-03-15 14:05:47 -0700562 self.source_context = source_context
James E. Blair01f83b72017-03-15 13:03:40 -0700563 # The secret data may or may not be encrypted. This attribute
564 # is named 'secret_data' to make it easy to search for and
565 # spot where it is directly used.
566 self.secret_data = {}
567
568 def __ne__(self, other):
569 return not self.__eq__(other)
570
571 def __eq__(self, other):
572 if not isinstance(other, Secret):
573 return False
574 return (self.name == other.name and
James E. Blair8525e2b2017-03-15 14:05:47 -0700575 self.source_context == other.source_context and
James E. Blair01f83b72017-03-15 13:03:40 -0700576 self.secret_data == other.secret_data)
577
578 def __repr__(self):
579 return '<Secret %s>' % (self.name,)
580
James E. Blair18f86a32017-03-15 14:43:26 -0700581 def decrypt(self, private_key):
582 """Return a copy of this secret with any encrypted data decrypted.
583 Note that the original remains encrypted."""
584
585 r = copy.deepcopy(self)
586 decrypted_secret_data = {}
587 for k, v in r.secret_data.items():
588 if hasattr(v, 'decrypt'):
589 decrypted_secret_data[k] = v.decrypt(private_key)
590 else:
591 decrypted_secret_data[k] = v
592 r.secret_data = decrypted_secret_data
593 return r
594
James E. Blair01f83b72017-03-15 13:03:40 -0700595
James E. Blaircdab2032017-02-01 09:09:29 -0800596class SourceContext(object):
597 """A reference to the branch of a project in configuration.
598
599 Jobs and playbooks reference this to keep track of where they
600 originate."""
601
James E. Blair6f140c72017-03-03 10:32:07 -0800602 def __init__(self, project, branch, path, trusted):
James E. Blaircdab2032017-02-01 09:09:29 -0800603 self.project = project
604 self.branch = branch
James E. Blair6f140c72017-03-03 10:32:07 -0800605 self.path = path
Monty Taylore6562aa2017-02-20 07:37:39 -0500606 self.trusted = trusted
James E. Blaircdab2032017-02-01 09:09:29 -0800607
James E. Blair6f140c72017-03-03 10:32:07 -0800608 def __str__(self):
609 return '%s/%s@%s' % (self.project, self.path, self.branch)
610
James E. Blaircdab2032017-02-01 09:09:29 -0800611 def __repr__(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800612 return '<SourceContext %s trusted:%s>' % (str(self),
613 self.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800614
James E. Blaira7f51ca2017-02-07 16:01:26 -0800615 def __deepcopy__(self, memo):
616 return self.copy()
617
618 def copy(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800619 return self.__class__(self.project, self.branch, self.path,
620 self.trusted)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800621
James E. Blaircdab2032017-02-01 09:09:29 -0800622 def __ne__(self, other):
623 return not self.__eq__(other)
624
625 def __eq__(self, other):
626 if not isinstance(other, SourceContext):
627 return False
628 return (self.project == other.project and
629 self.branch == other.branch and
James E. Blair6f140c72017-03-03 10:32:07 -0800630 self.path == other.path and
Monty Taylore6562aa2017-02-20 07:37:39 -0500631 self.trusted == other.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800632
633
James E. Blair66b274e2017-01-31 14:47:52 -0800634class PlaybookContext(object):
James E. Blaircdab2032017-02-01 09:09:29 -0800635
James E. Blair66b274e2017-01-31 14:47:52 -0800636 """A reference to a playbook in the context of a project.
637
638 Jobs refer to objects of this class for their main, pre, and post
639 playbooks so that we can keep track of which repos and security
James E. Blair74a82cf2017-07-12 17:23:08 -0700640 contexts are needed in order to run them.
James E. Blair66b274e2017-01-31 14:47:52 -0800641
James E. Blair74a82cf2017-07-12 17:23:08 -0700642 We also keep a list of roles so that playbooks only run with the
643 roles which were defined at the point the playbook was defined.
644
645 """
646
647 def __init__(self, source_context, path, roles):
James E. Blaircdab2032017-02-01 09:09:29 -0800648 self.source_context = source_context
James E. Blair66b274e2017-01-31 14:47:52 -0800649 self.path = path
James E. Blair74a82cf2017-07-12 17:23:08 -0700650 self.roles = roles
James E. Blair66b274e2017-01-31 14:47:52 -0800651
652 def __repr__(self):
James E. Blaircdab2032017-02-01 09:09:29 -0800653 return '<PlaybookContext %s %s>' % (self.source_context,
654 self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800655
656 def __ne__(self, other):
657 return not self.__eq__(other)
658
659 def __eq__(self, other):
660 if not isinstance(other, PlaybookContext):
661 return False
James E. Blaircdab2032017-02-01 09:09:29 -0800662 return (self.source_context == other.source_context and
James E. Blair74a82cf2017-07-12 17:23:08 -0700663 self.path == other.path and
664 self.roles == other.roles)
James E. Blair66b274e2017-01-31 14:47:52 -0800665
666 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400667 # Render to a dict to use in passing json to the executor
James E. Blair66b274e2017-01-31 14:47:52 -0800668 return dict(
James E. Blaircdab2032017-02-01 09:09:29 -0800669 connection=self.source_context.project.connection_name,
670 project=self.source_context.project.name,
671 branch=self.source_context.branch,
Monty Taylore6562aa2017-02-20 07:37:39 -0500672 trusted=self.source_context.trusted,
James E. Blair74a82cf2017-07-12 17:23:08 -0700673 roles=[r.toDict() for r in self.roles],
James E. Blaircdab2032017-02-01 09:09:29 -0800674 path=self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800675
676
Monty Taylorb934c1a2017-06-16 19:31:47 -0500677class Role(object, metaclass=abc.ABCMeta):
James E. Blair5ac93842017-01-20 06:47:34 -0800678 """A reference to an ansible role."""
679
680 def __init__(self, target_name):
681 self.target_name = target_name
682
683 @abc.abstractmethod
684 def __repr__(self):
685 pass
686
687 def __ne__(self, other):
688 return not self.__eq__(other)
689
690 @abc.abstractmethod
691 def __eq__(self, other):
692 if not isinstance(other, Role):
693 return False
694 return (self.target_name == other.target_name)
695
696 @abc.abstractmethod
697 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400698 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800699 return dict(target_name=self.target_name)
700
701
702class ZuulRole(Role):
703 """A reference to an ansible role in a Zuul project."""
704
James E. Blairbb94dfa2017-07-11 07:45:19 -0700705 def __init__(self, target_name, connection_name, project_name,
706 implicit=False):
James E. Blair5ac93842017-01-20 06:47:34 -0800707 super(ZuulRole, self).__init__(target_name)
708 self.connection_name = connection_name
709 self.project_name = project_name
James E. Blairbb94dfa2017-07-11 07:45:19 -0700710 self.implicit = implicit
James E. Blair5ac93842017-01-20 06:47:34 -0800711
712 def __repr__(self):
713 return '<ZuulRole %s %s>' % (self.project_name, self.target_name)
714
Clint Byrumaf7438f2017-05-10 17:26:57 -0400715 __hash__ = object.__hash__
716
James E. Blair5ac93842017-01-20 06:47:34 -0800717 def __eq__(self, other):
718 if not isinstance(other, ZuulRole):
719 return False
James E. Blairbb94dfa2017-07-11 07:45:19 -0700720 # Implicit is not consulted for equality so that we can handle
721 # implicit to explicit conversions.
James E. Blair5ac93842017-01-20 06:47:34 -0800722 return (super(ZuulRole, self).__eq__(other) and
James E. Blair1b27f6a2017-07-14 14:09:07 -0700723 self.connection_name == other.connection_name and
James E. Blair6563e4b2017-04-28 08:14:48 -0700724 self.project_name == other.project_name)
James E. Blair5ac93842017-01-20 06:47:34 -0800725
726 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400727 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800728 d = super(ZuulRole, self).toDict()
729 d['type'] = 'zuul'
730 d['connection'] = self.connection_name
731 d['project'] = self.project_name
James E. Blairbb94dfa2017-07-11 07:45:19 -0700732 d['implicit'] = self.implicit
James E. Blair5ac93842017-01-20 06:47:34 -0800733 return d
734
735
James E. Blair8525e2b2017-03-15 14:05:47 -0700736class AuthContext(object):
737 """The authentication information for a job.
738
739 Authentication information (both the actual data and metadata such
740 as whether it should be inherited) for a job is grouped together
741 in this object.
742 """
743
744 def __init__(self, inherit=False):
745 self.inherit = inherit
746 self.secrets = []
747
748 def __ne__(self, other):
749 return not self.__eq__(other)
750
751 def __eq__(self, other):
752 if not isinstance(other, AuthContext):
753 return False
754 return (self.inherit == other.inherit and
755 self.secrets == other.secrets)
756
757
James E. Blairee743612012-05-29 14:49:32 -0700758class Job(object):
James E. Blair66b274e2017-01-31 14:47:52 -0800759
James E. Blaira7f51ca2017-02-07 16:01:26 -0800760 """A Job represents the defintion of actions to perform.
761
James E. Blaird4ade8c2017-02-19 15:25:46 -0800762 A Job is an abstract configuration concept. It describes what,
763 where, and under what circumstances something should be run
764 (contrast this with Build which is a concrete single execution of
765 a Job).
766
James E. Blaira7f51ca2017-02-07 16:01:26 -0800767 NB: Do not modify attributes of this class, set them directly
768 (e.g., "job.run = ..." rather than "job.run.append(...)").
769 """
Monty Taylora42a55b2016-07-29 07:53:33 -0700770
James E. Blairee743612012-05-29 14:49:32 -0700771 def __init__(self, name):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800772 # These attributes may override even the final form of a job
773 # in the context of a project-pipeline. They can not affect
774 # the execution of the job, but only whether the job is run
775 # and how it is reported.
776 self.context_attributes = dict(
777 voting=True,
778 hold_following_changes=False,
James E. Blair1774dd52017-02-03 10:52:32 -0800779 failure_message=None,
780 success_message=None,
781 failure_url=None,
782 success_url=None,
783 # Matchers. These are separate so they can be individually
784 # overidden.
785 branch_matcher=None,
786 file_matcher=None,
787 irrelevant_file_matcher=None, # skip-if
James E. Blaira7f51ca2017-02-07 16:01:26 -0800788 tags=frozenset(),
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200789 dependencies=frozenset(),
James E. Blair1774dd52017-02-03 10:52:32 -0800790 )
791
James E. Blaira7f51ca2017-02-07 16:01:26 -0800792 # These attributes affect how the job is actually run and more
793 # care must be taken when overriding them. If a job is
794 # declared "final", these may not be overriden in a
795 # project-pipeline.
796 self.execution_attributes = dict(
797 timeout=None,
James E. Blair490cf042017-02-24 23:07:21 -0500798 variables={},
James E. Blaira7f51ca2017-02-07 16:01:26 -0800799 nodeset=NodeSet(),
James E. Blair8525e2b2017-03-15 14:05:47 -0700800 auth=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800801 workspace=None,
802 pre_run=(),
803 post_run=(),
804 run=(),
805 implied_run=(),
Tobias Henkel9a0e1942017-03-20 16:16:02 +0100806 semaphore=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800807 attempts=3,
808 final=False,
James E. Blair5fc81922017-07-12 13:19:37 -0700809 roles=(),
James E. Blair912322f2017-05-23 13:11:25 -0700810 required_projects={},
James E. Blairb3f5db12017-03-17 12:57:39 -0700811 allowed_projects=None,
James E. Blair7391ecb2017-05-23 13:32:40 -0700812 override_branch=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800813 )
814
815 # These are generally internal attributes which are not
816 # accessible via configuration.
817 self.other_attributes = dict(
818 name=None,
819 source_context=None,
820 inheritance_path=(),
821 )
822
823 self.inheritable_attributes = {}
824 self.inheritable_attributes.update(self.context_attributes)
825 self.inheritable_attributes.update(self.execution_attributes)
826 self.attributes = {}
827 self.attributes.update(self.inheritable_attributes)
828 self.attributes.update(self.other_attributes)
829
James E. Blairee743612012-05-29 14:49:32 -0700830 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800831
James E. Blair66b274e2017-01-31 14:47:52 -0800832 def __ne__(self, other):
833 return not self.__eq__(other)
834
Paul Belangere22baea2016-11-03 16:59:27 -0400835 def __eq__(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800836 # Compare the name and all inheritable attributes to determine
837 # whether two jobs with the same name are identically
838 # configured. Useful upon reconfiguration.
839 if not isinstance(other, Job):
840 return False
841 if self.name != other.name:
842 return False
843 for k, v in self.attributes.items():
844 if getattr(self, k) != getattr(other, k):
845 return False
846 return True
James E. Blairee743612012-05-29 14:49:32 -0700847
Clint Byrumaf7438f2017-05-10 17:26:57 -0400848 __hash__ = object.__hash__
849
James E. Blairee743612012-05-29 14:49:32 -0700850 def __str__(self):
851 return self.name
852
853 def __repr__(self):
James E. Blair72ae9da2017-02-03 14:21:30 -0800854 return '<Job %s branches: %s source: %s>' % (self.name,
855 self.branch_matcher,
856 self.source_context)
James E. Blair83005782015-12-11 14:46:03 -0800857
James E. Blaira7f51ca2017-02-07 16:01:26 -0800858 def __getattr__(self, name):
859 v = self.__dict__.get(name)
860 if v is None:
861 return copy.deepcopy(self.attributes[name])
862 return v
863
864 def _get(self, name):
865 return self.__dict__.get(name)
866
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100867 def getSafeAttributes(self):
868 return Attributes(name=self.name)
869
James E. Blaira7f51ca2017-02-07 16:01:26 -0800870 def setRun(self):
871 if not self.run:
872 self.run = self.implied_run
873
James E. Blair5fc81922017-07-12 13:19:37 -0700874 def addRoles(self, roles):
James E. Blairbb94dfa2017-07-11 07:45:19 -0700875 newroles = []
876 # Start with a copy of the existing roles, but if any of them
877 # are implicit roles which are identified as explicit in the
878 # new roles list, replace them with the explicit version.
879 changed = False
880 for existing_role in self.roles:
881 if existing_role in roles:
882 new_role = roles[roles.index(existing_role)]
883 else:
884 new_role = None
885 if (new_role and
886 isinstance(new_role, ZuulRole) and
887 isinstance(existing_role, ZuulRole) and
888 existing_role.implicit and not new_role.implicit):
889 newroles.append(new_role)
890 changed = True
891 else:
892 newroles.append(existing_role)
893 # Now add the new roles.
James E. Blair4eec8282017-07-12 17:33:26 -0700894 for role in reversed(roles):
James E. Blair5fc81922017-07-12 13:19:37 -0700895 if role not in newroles:
James E. Blair4eec8282017-07-12 17:33:26 -0700896 newroles.insert(0, role)
James E. Blairbb94dfa2017-07-11 07:45:19 -0700897 changed = True
898 if changed:
899 self.roles = tuple(newroles)
James E. Blair5fc81922017-07-12 13:19:37 -0700900
James E. Blair490cf042017-02-24 23:07:21 -0500901 def updateVariables(self, other_vars):
902 v = self.variables
903 Job._deepUpdate(v, other_vars)
904 self.variables = v
905
James E. Blair912322f2017-05-23 13:11:25 -0700906 def updateProjects(self, other_projects):
907 required_projects = self.required_projects
908 Job._deepUpdate(required_projects, other_projects)
909 self.required_projects = required_projects
James E. Blair27f3dfc2017-05-23 13:07:28 -0700910
James E. Blair490cf042017-02-24 23:07:21 -0500911 @staticmethod
912 def _deepUpdate(a, b):
913 # Merge nested dictionaries if possible, otherwise, overwrite
914 # the value in 'a' with the value in 'b'.
915 for k, bv in b.items():
916 av = a.get(k)
917 if isinstance(av, dict) and isinstance(bv, dict):
918 Job._deepUpdate(av, bv)
919 else:
920 a[k] = bv
921
James E. Blaira7f51ca2017-02-07 16:01:26 -0800922 def inheritFrom(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800923 """Copy the inheritable attributes which have been set on the other
924 job to this job."""
James E. Blaira7f51ca2017-02-07 16:01:26 -0800925 if not isinstance(other, Job):
926 raise Exception("Job unable to inherit from %s" % (other,))
927
928 do_not_inherit = set()
James E. Blair8525e2b2017-03-15 14:05:47 -0700929 if other.auth and not other.auth.inherit:
James E. Blaira7f51ca2017-02-07 16:01:26 -0800930 do_not_inherit.add('auth')
931
932 # copy all attributes
933 for k in self.inheritable_attributes:
934 if (other._get(k) is not None and k not in do_not_inherit):
935 setattr(self, k, copy.deepcopy(getattr(other, k)))
936
937 msg = 'inherit from %s' % (repr(other),)
938 self.inheritance_path = other.inheritance_path + (msg,)
939
940 def copy(self):
941 job = Job(self.name)
942 for k in self.attributes:
943 if self._get(k) is not None:
944 setattr(job, k, copy.deepcopy(self._get(k)))
945 return job
946
947 def applyVariant(self, other):
948 """Copy the attributes which have been set on the other job to this
949 job."""
James E. Blair83005782015-12-11 14:46:03 -0800950
951 if not isinstance(other, Job):
952 raise Exception("Job unable to inherit from %s" % (other,))
James E. Blaira7f51ca2017-02-07 16:01:26 -0800953
954 for k in self.execution_attributes:
955 if (other._get(k) is not None and
956 k not in set(['final'])):
957 if self.final:
958 raise Exception("Unable to modify final job %s attribute "
959 "%s=%s with variant %s" % (
960 repr(self), k, other._get(k),
961 repr(other)))
James E. Blair27f3dfc2017-05-23 13:07:28 -0700962 if k not in set(['pre_run', 'post_run', 'roles', 'variables',
James E. Blair912322f2017-05-23 13:11:25 -0700963 'required_projects']):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800964 setattr(self, k, copy.deepcopy(other._get(k)))
965
966 # Don't set final above so that we don't trip an error halfway
967 # through assignment.
968 if other.final != self.attributes['final']:
969 self.final = other.final
970
971 if other._get('pre_run') is not None:
972 self.pre_run = self.pre_run + other.pre_run
973 if other._get('post_run') is not None:
974 self.post_run = other.post_run + self.post_run
James E. Blair5ac93842017-01-20 06:47:34 -0800975 if other._get('roles') is not None:
James E. Blair5fc81922017-07-12 13:19:37 -0700976 self.addRoles(other.roles)
James E. Blair490cf042017-02-24 23:07:21 -0500977 if other._get('variables') is not None:
978 self.updateVariables(other.variables)
James E. Blair912322f2017-05-23 13:11:25 -0700979 if other._get('required_projects') is not None:
980 self.updateProjects(other.required_projects)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800981
982 for k in self.context_attributes:
983 if (other._get(k) is not None and
984 k not in set(['tags'])):
985 setattr(self, k, copy.deepcopy(other._get(k)))
986
987 if other._get('tags') is not None:
988 self.tags = self.tags.union(other.tags)
989
990 msg = 'apply variant %s' % (repr(other),)
991 self.inheritance_path = self.inheritance_path + (msg,)
James E. Blairee743612012-05-29 14:49:32 -0700992
James E. Blaire421a232012-07-25 16:59:21 -0700993 def changeMatches(self, change):
James E. Blair83005782015-12-11 14:46:03 -0800994 if self.branch_matcher and not self.branch_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800995 return False
996
James E. Blair83005782015-12-11 14:46:03 -0800997 if self.file_matcher and not self.file_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800998 return False
999
James E. Blair83005782015-12-11 14:46:03 -08001000 # NB: This is a negative match.
1001 if (self.irrelevant_file_matcher and
1002 self.irrelevant_file_matcher.matches(change)):
Maru Newby3fe5f852015-01-13 04:22:14 +00001003 return False
1004
James E. Blair70c71582013-03-06 08:50:50 -08001005 return True
James E. Blaire5a847f2012-07-10 15:29:14 -07001006
James E. Blair1e8dd892012-05-30 09:15:05 -07001007
James E. Blair912322f2017-05-23 13:11:25 -07001008class JobProject(object):
James E. Blair27f3dfc2017-05-23 13:07:28 -07001009 """ A reference to a project from a job. """
1010
1011 def __init__(self, project_name, override_branch=None):
1012 self.project_name = project_name
1013 self.override_branch = override_branch
1014
1015
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001016class JobList(object):
1017 """ A list of jobs in a project's pipeline. """
Monty Taylora42a55b2016-07-29 07:53:33 -07001018
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001019 def __init__(self):
1020 self.jobs = OrderedDict() # job.name -> [job, ...]
James E. Blair12748b52017-02-07 17:17:53 -08001021
James E. Blairee743612012-05-29 14:49:32 -07001022 def addJob(self, job):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001023 if job.name in self.jobs:
1024 self.jobs[job.name].append(job)
1025 else:
1026 self.jobs[job.name] = [job]
James E. Blairee743612012-05-29 14:49:32 -07001027
James E. Blaira7f51ca2017-02-07 16:01:26 -08001028 def inheritFrom(self, other):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001029 for jobname, jobs in other.jobs.items():
1030 if jobname in self.jobs:
Jesse Keatingd1f434a2017-05-16 20:28:35 -07001031 self.jobs[jobname].extend(jobs)
James E. Blaira7f51ca2017-02-07 16:01:26 -08001032 else:
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001033 self.jobs[jobname] = jobs
1034
1035
1036class JobGraph(object):
1037 """ A JobGraph represents the dependency graph between Job."""
1038
1039 def __init__(self):
1040 self.jobs = OrderedDict() # job_name -> Job
1041 self._dependencies = {} # dependent_job_name -> set(parent_job_names)
1042
1043 def __repr__(self):
1044 return '<JobGraph %s>' % (self.jobs)
1045
1046 def addJob(self, job):
1047 # A graph must be created after the job list is frozen,
1048 # therefore we should only get one job with the same name.
1049 if job.name in self.jobs:
1050 raise Exception("Job %s already added" % (job.name,))
1051 self.jobs[job.name] = job
1052 # Append the dependency information
1053 self._dependencies.setdefault(job.name, set())
1054 try:
1055 for dependency in job.dependencies:
1056 # Make sure a circular dependency is never created
1057 ancestor_jobs = self._getParentJobNamesRecursively(
1058 dependency, soft=True)
1059 ancestor_jobs.add(dependency)
1060 if any((job.name == anc_job) for anc_job in ancestor_jobs):
1061 raise Exception("Dependency cycle detected in job %s" %
1062 (job.name,))
1063 self._dependencies[job.name].add(dependency)
1064 except Exception:
1065 del self.jobs[job.name]
1066 del self._dependencies[job.name]
1067 raise
1068
1069 def getJobs(self):
Clint Byruma4471d12017-05-10 20:57:40 -04001070 return list(self.jobs.values()) # Report in the order of layout cfg
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001071
1072 def _getDirectDependentJobs(self, parent_job):
1073 ret = set()
1074 for dependent_name, parent_names in self._dependencies.items():
1075 if parent_job in parent_names:
1076 ret.add(dependent_name)
1077 return ret
1078
1079 def getDependentJobsRecursively(self, parent_job):
1080 all_dependent_jobs = set()
1081 jobs_to_iterate = set([parent_job])
1082 while len(jobs_to_iterate) > 0:
1083 current_job = jobs_to_iterate.pop()
1084 current_dependent_jobs = self._getDirectDependentJobs(current_job)
1085 new_dependent_jobs = current_dependent_jobs - all_dependent_jobs
1086 jobs_to_iterate |= new_dependent_jobs
1087 all_dependent_jobs |= new_dependent_jobs
1088 return [self.jobs[name] for name in all_dependent_jobs]
1089
1090 def getParentJobsRecursively(self, dependent_job):
1091 return [self.jobs[name] for name in
1092 self._getParentJobNamesRecursively(dependent_job)]
1093
1094 def _getParentJobNamesRecursively(self, dependent_job, soft=False):
1095 all_parent_jobs = set()
1096 jobs_to_iterate = set([dependent_job])
1097 while len(jobs_to_iterate) > 0:
1098 current_job = jobs_to_iterate.pop()
1099 current_parent_jobs = self._dependencies.get(current_job)
1100 if current_parent_jobs is None:
1101 if soft:
1102 current_parent_jobs = set()
1103 else:
1104 raise Exception("Dependent job %s not found: " %
1105 (dependent_job,))
1106 new_parent_jobs = current_parent_jobs - all_parent_jobs
1107 jobs_to_iterate |= new_parent_jobs
1108 all_parent_jobs |= new_parent_jobs
1109 return all_parent_jobs
James E. Blairb97ed802015-12-21 15:55:35 -08001110
James E. Blair1e8dd892012-05-30 09:15:05 -07001111
James E. Blair4aea70c2012-07-26 14:23:24 -07001112class Build(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001113 """A Build is an instance of a single execution of a Job.
1114
1115 While a Job describes what to run, a Build describes an actual
1116 execution of that Job. Each build is associated with exactly one
1117 Job (related builds are grouped together in a BuildSet).
1118 """
Monty Taylora42a55b2016-07-29 07:53:33 -07001119
James E. Blair4aea70c2012-07-26 14:23:24 -07001120 def __init__(self, job, uuid):
1121 self.job = job
1122 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -07001123 self.url = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001124 self.result = None
James E. Blair196f61a2017-06-30 15:42:29 -07001125 self.result_data = {}
James E. Blair6f699732017-07-18 14:19:11 -07001126 self.error_detail = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001127 self.build_set = None
Paul Belanger174a8272017-03-14 13:20:10 -04001128 self.execute_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -08001129 self.start_time = None
1130 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -07001131 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -07001132 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -07001133 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -07001134 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -07001135 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +08001136 self.worker = Worker()
Timothy Chavezb2332082015-08-07 20:08:04 -05001137 self.node_labels = []
1138 self.node_name = None
James E. Blairee743612012-05-29 14:49:32 -07001139
1140 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +08001141 return ('<Build %s of %s on %s>' %
1142 (self.uuid, self.job.name, self.worker))
1143
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001144 def getSafeAttributes(self):
James E. Blair196f61a2017-06-30 15:42:29 -07001145 return Attributes(uuid=self.uuid,
1146 result=self.result,
James E. Blair6f699732017-07-18 14:19:11 -07001147 error_detail=self.error_detail,
James E. Blair196f61a2017-06-30 15:42:29 -07001148 result_data=self.result_data)
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001149
Joshua Heskethba8776a2014-01-12 14:35:40 +08001150
1151class Worker(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001152 """Information about the specific worker executing a Build."""
Joshua Heskethba8776a2014-01-12 14:35:40 +08001153 def __init__(self):
1154 self.name = "Unknown"
1155 self.hostname = None
Monty Taylor0dbe1592017-06-11 10:57:27 -05001156 self.log_port = None
Joshua Heskethba8776a2014-01-12 14:35:40 +08001157
1158 def updateFromData(self, data):
1159 """Update worker information if contained in the WORK_DATA response."""
1160 self.name = data.get('worker_name', self.name)
1161 self.hostname = data.get('worker_hostname', self.hostname)
Monty Taylor0dbe1592017-06-11 10:57:27 -05001162 self.log_port = data.get('worker_log_port', self.log_port)
Joshua Heskethba8776a2014-01-12 14:35:40 +08001163
1164 def __repr__(self):
1165 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -07001166
James E. Blair1e8dd892012-05-30 09:15:05 -07001167
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001168class RepoFiles(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001169 """RepoFiles holds config-file content for per-project job config.
1170
1171 When Zuul asks a merger to prepare a future multiple-repo state
1172 and collect Zuul configuration files so that we can dynamically
1173 load our configuration, this class provides cached access to that
1174 data for use by the Change which updated the config files and any
1175 changes that follow it in a ChangeQueue.
1176
1177 It is attached to a BuildSet since the content of Zuul
1178 configuration files can change with each new BuildSet.
1179 """
1180
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001181 def __init__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001182 self.connections = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001183
1184 def __repr__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001185 return '<RepoFiles %s>' % self.connections
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001186
1187 def setFiles(self, items):
James E. Blair2a535672017-04-27 12:03:15 -07001188 self.hostnames = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001189 for item in items:
James E. Blair2a535672017-04-27 12:03:15 -07001190 connection = self.connections.setdefault(
1191 item['connection'], {})
1192 project = connection.setdefault(item['project'], {})
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001193 branch = project.setdefault(item['branch'], {})
1194 branch.update(item['files'])
1195
James E. Blair2a535672017-04-27 12:03:15 -07001196 def getFile(self, connection_name, project_name, branch, fn):
1197 host = self.connections.get(connection_name, {})
1198 return host.get(project_name, {}).get(branch, {}).get(fn)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001199
1200
James E. Blair7e530ad2012-07-03 16:12:28 -07001201class BuildSet(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001202 """A collection of Builds for one specific potential future repository
1203 state.
Monty Taylora42a55b2016-07-29 07:53:33 -07001204
Paul Belanger174a8272017-03-14 13:20:10 -04001205 When Zuul executes Builds for a change, it creates a Build to
James E. Blaird4ade8c2017-02-19 15:25:46 -08001206 represent each execution of each job and a BuildSet to keep track
Paul Belanger174a8272017-03-14 13:20:10 -04001207 of all the Builds running for that Change. When Zuul re-executes
James E. Blaird4ade8c2017-02-19 15:25:46 -08001208 Builds for a Change with a different configuration, all of the
1209 running Builds in the BuildSet for that change are aborted, and a
1210 new BuildSet is created to hold the Builds for the Jobs being
1211 run with the new configuration.
1212
1213 A BuildSet also holds the UUID used to produce the Zuul Ref that
1214 builders check out.
1215
Monty Taylora42a55b2016-07-29 07:53:33 -07001216 """
James E. Blair4076e2b2014-01-28 12:42:20 -08001217 # Merge states:
1218 NEW = 1
1219 PENDING = 2
1220 COMPLETE = 3
1221
Antoine Musso9b229282014-08-18 23:45:43 +02001222 states_map = {
1223 1: 'NEW',
1224 2: 'PENDING',
1225 3: 'COMPLETE',
1226 }
1227
James E. Blairfee8d652013-06-07 08:57:52 -07001228 def __init__(self, item):
1229 self.item = item
James E. Blair7e530ad2012-07-03 16:12:28 -07001230 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -07001231 self.result = None
1232 self.next_build_set = None
1233 self.previous_build_set = None
Jamie Lennox3f16de52017-05-09 14:24:11 +10001234 self.uuid = None
James E. Blair81515ad2012-10-01 18:29:08 -07001235 self.commit = None
James E. Blair4076e2b2014-01-28 12:42:20 -08001236 self.zuul_url = None
James E. Blair1960d682017-04-28 15:44:14 -07001237 self.dependent_items = None
1238 self.merger_items = None
James E. Blair973721f2012-08-15 10:19:43 -07001239 self.unable_to_merge = False
James E. Blaire53250c2017-03-01 14:34:36 -08001240 self.config_error = None # None or an error message string.
James E. Blair972e3c72013-08-29 12:04:55 -07001241 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -08001242 self.merge_state = self.NEW
James E. Blair0eaad552016-09-02 12:09:54 -07001243 self.nodesets = {} # job -> nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001244 self.node_requests = {} # job -> reqs
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001245 self.files = RepoFiles()
James E. Blair1960d682017-04-28 15:44:14 -07001246 self.repo_state = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001247 self.layout = None
Paul Belanger71d98172016-11-08 10:56:31 -05001248 self.tries = {}
James E. Blair7e530ad2012-07-03 16:12:28 -07001249
Jamie Lennox3f16de52017-05-09 14:24:11 +10001250 @property
1251 def ref(self):
1252 # NOTE(jamielennox): The concept of buildset ref is to be removed and a
1253 # buildset UUID identifier available instead. Currently the ref is
1254 # checked to see if the BuildSet has been configured.
1255 return 'Z' + self.uuid if self.uuid else None
1256
Antoine Musso9b229282014-08-18 23:45:43 +02001257 def __repr__(self):
1258 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
1259 self.item,
1260 len(self.builds),
1261 self.getStateName(self.merge_state))
1262
James E. Blair4886cc12012-07-18 15:39:41 -07001263 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -07001264 # The change isn't enqueued until after it's created
1265 # so we don't know what the other changes ahead will be
1266 # until jobs start.
James E. Blair1960d682017-04-28 15:44:14 -07001267 if self.dependent_items is None:
1268 items = []
James E. Blairfee8d652013-06-07 08:57:52 -07001269 next_item = self.item.item_ahead
1270 while next_item:
James E. Blair1960d682017-04-28 15:44:14 -07001271 items.append(next_item)
James E. Blairfee8d652013-06-07 08:57:52 -07001272 next_item = next_item.item_ahead
James E. Blair1960d682017-04-28 15:44:14 -07001273 self.dependent_items = items
Jamie Lennox3f16de52017-05-09 14:24:11 +10001274 if not self.uuid:
1275 self.uuid = uuid4().hex
James E. Blair1960d682017-04-28 15:44:14 -07001276 if self.merger_items is None:
1277 items = [self.item] + self.dependent_items
1278 items.reverse()
1279 self.merger_items = [i.makeMergerItem() for i in items]
James E. Blair4886cc12012-07-18 15:39:41 -07001280
Antoine Musso9b229282014-08-18 23:45:43 +02001281 def getStateName(self, state_num):
1282 return self.states_map.get(
1283 state_num, 'UNKNOWN (%s)' % state_num)
1284
James E. Blair4886cc12012-07-18 15:39:41 -07001285 def addBuild(self, build):
1286 self.builds[build.job.name] = build
Paul Belanger71d98172016-11-08 10:56:31 -05001287 if build.job.name not in self.tries:
James E. Blair5aed1112016-12-15 09:18:05 -08001288 self.tries[build.job.name] = 1
James E. Blair4886cc12012-07-18 15:39:41 -07001289 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -07001290
James E. Blair4a28a882013-08-23 15:17:33 -07001291 def removeBuild(self, build):
Paul Belanger71d98172016-11-08 10:56:31 -05001292 self.tries[build.job.name] += 1
James E. Blair4a28a882013-08-23 15:17:33 -07001293 del self.builds[build.job.name]
1294
James E. Blair7e530ad2012-07-03 16:12:28 -07001295 def getBuild(self, job_name):
1296 return self.builds.get(job_name)
1297
James E. Blair11700c32012-07-05 17:50:05 -07001298 def getBuilds(self):
Clint Byrum1d0c7d12017-05-10 19:40:53 -07001299 keys = list(self.builds.keys())
James E. Blair11700c32012-07-05 17:50:05 -07001300 keys.sort()
1301 return [self.builds.get(x) for x in keys]
1302
James E. Blair0eaad552016-09-02 12:09:54 -07001303 def getJobNodeSet(self, job_name):
1304 # Return None if not provisioned; empty NodeSet if no nodes
1305 # required
1306 return self.nodesets.get(job_name)
James E. Blair8d692392016-04-08 17:47:58 -07001307
James E. Blaire18d4602017-01-05 11:17:28 -08001308 def removeJobNodeSet(self, job_name):
1309 if job_name not in self.nodesets:
1310 raise Exception("No job set for %s" % (job_name))
1311 del self.nodesets[job_name]
1312
James E. Blair8d692392016-04-08 17:47:58 -07001313 def setJobNodeRequest(self, job_name, req):
1314 if job_name in self.node_requests:
1315 raise Exception("Prior node request for %s" % (job_name))
1316 self.node_requests[job_name] = req
1317
1318 def getJobNodeRequest(self, job_name):
1319 return self.node_requests.get(job_name)
1320
James E. Blair0eaad552016-09-02 12:09:54 -07001321 def jobNodeRequestComplete(self, job_name, req, nodeset):
1322 if job_name in self.nodesets:
James E. Blair8d692392016-04-08 17:47:58 -07001323 raise Exception("Prior node request for %s" % (job_name))
James E. Blair0eaad552016-09-02 12:09:54 -07001324 self.nodesets[job_name] = nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001325 del self.node_requests[job_name]
1326
Paul Belanger71d98172016-11-08 10:56:31 -05001327 def getTries(self, job_name):
Clint Byrum804073b2017-05-10 17:47:45 -04001328 return self.tries.get(job_name, 0)
Paul Belanger71d98172016-11-08 10:56:31 -05001329
James E. Blair0ffa0102017-03-30 13:11:33 -07001330 def getMergeMode(self):
James E. Blair1960d682017-04-28 15:44:14 -07001331 # We may be called before this build set has a shadow layout
1332 # (ie, we are called to perform the merge to create that
1333 # layout). It's possible that the change we are merging will
1334 # update the merge-mode for the project, but there's not much
1335 # we can do about that here. Instead, do the best we can by
1336 # using the nearest shadow layout to determine the merge mode,
1337 # or if that fails, the current live layout, or if that fails,
1338 # use the default: merge-resolve.
1339 item = self.item
1340 layout = None
1341 while item:
1342 layout = item.current_build_set.layout
1343 if layout:
1344 break
1345 item = item.item_ahead
1346 if not layout:
1347 layout = self.item.pipeline.layout
1348 if layout:
James E. Blair0ffa0102017-03-30 13:11:33 -07001349 project = self.item.change.project
James E. Blair1960d682017-04-28 15:44:14 -07001350 project_config = layout.project_configs.get(
James E. Blair0ffa0102017-03-30 13:11:33 -07001351 project.canonical_name)
1352 if project_config:
1353 return project_config.merge_mode
1354 return MERGER_MERGE_RESOLVE
Adam Gandelman8bd57102016-12-02 12:58:42 -08001355
Jamie Lennox3f16de52017-05-09 14:24:11 +10001356 def getSafeAttributes(self):
1357 return Attributes(uuid=self.uuid)
1358
James E. Blair7e530ad2012-07-03 16:12:28 -07001359
James E. Blairfee8d652013-06-07 08:57:52 -07001360class QueueItem(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001361 """Represents the position of a Change in a ChangeQueue.
1362
1363 All Changes are enqueued into ChangeQueue in a QueueItem. The QueueItem
1364 holds the current `BuildSet` as well as all previous `BuildSets` that were
1365 produced for this `QueueItem`.
1366 """
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001367 log = logging.getLogger("zuul.QueueItem")
James E. Blair32663402012-06-01 10:04:18 -07001368
James E. Blairbfb8e042014-12-30 17:01:44 -08001369 def __init__(self, queue, change):
1370 self.pipeline = queue.pipeline
1371 self.queue = queue
Monty Taylor55d0f562017-05-17 14:30:21 -05001372 self.change = change # a ref
James E. Blair7e530ad2012-07-03 16:12:28 -07001373 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -07001374 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -07001375 self.current_build_set = BuildSet(self)
1376 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -07001377 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -07001378 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -08001379 self.enqueue_time = None
1380 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -07001381 self.reported = False
Tobias Henkel9842bd72017-05-16 13:40:03 +02001382 self.reported_start = False
1383 self.quiet = False
James E. Blairbfb8e042014-12-30 17:01:44 -08001384 self.active = False # Whether an item is within an active window
1385 self.live = True # Whether an item is intended to be processed at all
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001386 self.job_graph = None
James E. Blaire5a847f2012-07-10 15:29:14 -07001387
James E. Blair972e3c72013-08-29 12:04:55 -07001388 def __repr__(self):
1389 if self.pipeline:
1390 pipeline = self.pipeline.name
1391 else:
1392 pipeline = None
1393 return '<QueueItem 0x%x for %s in %s>' % (
1394 id(self), self.change, pipeline)
1395
James E. Blairee743612012-05-29 14:49:32 -07001396 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -07001397 old = self.current_build_set
1398 self.current_build_set.result = 'CANCELED'
1399 self.current_build_set = BuildSet(self)
1400 old.next_build_set = self.current_build_set
1401 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -07001402 self.build_sets.append(self.current_build_set)
James E. Blairee743612012-05-29 14:49:32 -07001403
1404 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -07001405 self.current_build_set.addBuild(build)
James E. Blair66eeebf2013-07-27 17:44:32 -07001406 build.pipeline = self.pipeline
James E. Blairee743612012-05-29 14:49:32 -07001407
James E. Blair4a28a882013-08-23 15:17:33 -07001408 def removeBuild(self, build):
1409 self.current_build_set.removeBuild(build)
1410
James E. Blairfee8d652013-06-07 08:57:52 -07001411 def setReportedResult(self, result):
1412 self.current_build_set.result = result
1413
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001414 def freezeJobGraph(self):
James E. Blair83005782015-12-11 14:46:03 -08001415 """Find or create actual matching jobs for this item's change and
1416 store the resulting job tree."""
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001417 layout = self.current_build_set.layout
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001418 job_graph = layout.createJobGraph(self)
1419 for job in job_graph.getJobs():
1420 # Ensure that each jobs's dependencies are fully
1421 # accessible. This will raise an exception if not.
1422 job_graph.getParentJobsRecursively(job.name)
1423 self.job_graph = job_graph
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001424
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001425 def hasJobGraph(self):
1426 """Returns True if the item has a job graph."""
1427 return self.job_graph is not None
James E. Blair83005782015-12-11 14:46:03 -08001428
1429 def getJobs(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001430 if not self.live or not self.job_graph:
James E. Blair83005782015-12-11 14:46:03 -08001431 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001432 return self.job_graph.getJobs()
1433
1434 def getJob(self, name):
1435 if not self.job_graph:
1436 return None
1437 return self.job_graph.jobs.get(name)
James E. Blair83005782015-12-11 14:46:03 -08001438
James E. Blairdbfd3282016-07-21 10:46:19 -07001439 def haveAllJobsStarted(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001440 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001441 return False
1442 for job in self.getJobs():
1443 build = self.current_build_set.getBuild(job.name)
1444 if not build or not build.start_time:
1445 return False
1446 return True
1447
1448 def areAllJobsComplete(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001449 if (self.current_build_set.config_error or
1450 self.current_build_set.unable_to_merge):
1451 return True
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001452 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001453 return False
1454 for job in self.getJobs():
1455 build = self.current_build_set.getBuild(job.name)
1456 if not build or not build.result:
1457 return False
1458 return True
1459
1460 def didAllJobsSucceed(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001461 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001462 return False
1463 for job in self.getJobs():
1464 if not job.voting:
1465 continue
1466 build = self.current_build_set.getBuild(job.name)
1467 if not build:
1468 return False
1469 if build.result != 'SUCCESS':
1470 return False
1471 return True
1472
1473 def didAnyJobFail(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001474 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001475 return False
1476 for job in self.getJobs():
1477 if not job.voting:
1478 continue
1479 build = self.current_build_set.getBuild(job.name)
1480 if build and build.result and (build.result != 'SUCCESS'):
1481 return True
1482 return False
1483
1484 def didMergerFail(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001485 return self.current_build_set.unable_to_merge
1486
1487 def getConfigError(self):
1488 return self.current_build_set.config_error
James E. Blairdbfd3282016-07-21 10:46:19 -07001489
James E. Blair0d3e83b2017-06-05 13:51:57 -07001490 def wasDequeuedNeedingChange(self):
1491 return self.dequeued_needing_change
1492
James E. Blairdbfd3282016-07-21 10:46:19 -07001493 def isHoldingFollowingChanges(self):
1494 if not self.live:
1495 return False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001496 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001497 return False
1498 for job in self.getJobs():
1499 if not job.hold_following_changes:
1500 continue
1501 build = self.current_build_set.getBuild(job.name)
1502 if not build:
1503 return True
1504 if build.result != 'SUCCESS':
1505 return True
1506
1507 if not self.item_ahead:
1508 return False
1509 return self.item_ahead.isHoldingFollowingChanges()
1510
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001511 def findJobsToRun(self, semaphore_handler):
James E. Blairdbfd3282016-07-21 10:46:19 -07001512 torun = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001513 if not self.live:
1514 return []
1515 if not self.job_graph:
1516 return []
James E. Blair791b5392016-08-03 11:25:56 -07001517 if self.item_ahead:
1518 # Only run jobs if any 'hold' jobs on the change ahead
1519 # have completed successfully.
1520 if self.item_ahead.isHoldingFollowingChanges():
1521 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001522
1523 successful_job_names = set()
1524 jobs_not_started = set()
1525 for job in self.job_graph.getJobs():
1526 build = self.current_build_set.getBuild(job.name)
1527 if build:
1528 if build.result == 'SUCCESS':
1529 successful_job_names.add(job.name)
1530 else:
1531 jobs_not_started.add(job)
1532
1533 # Attempt to request nodes for jobs in the order jobs appear
1534 # in configuration.
1535 for job in self.job_graph.getJobs():
1536 if job not in jobs_not_started:
1537 continue
1538 all_parent_jobs_successful = True
1539 for parent_job in self.job_graph.getParentJobsRecursively(
1540 job.name):
1541 if parent_job.name not in successful_job_names:
1542 all_parent_jobs_successful = False
1543 break
1544 if all_parent_jobs_successful:
1545 nodeset = self.current_build_set.getJobNodeSet(job.name)
1546 if nodeset is None:
1547 # The nodes for this job are not ready, skip
1548 # it for now.
James E. Blairdbfd3282016-07-21 10:46:19 -07001549 continue
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001550 if semaphore_handler.acquire(self, job):
1551 # If this job needs a semaphore, either acquire it or
1552 # make sure that we have it before running the job.
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001553 torun.append(job)
James E. Blairdbfd3282016-07-21 10:46:19 -07001554 return torun
1555
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001556 def findJobsToRequest(self):
James E. Blair6ab79e02017-01-06 10:10:17 -08001557 build_set = self.current_build_set
James E. Blairdbfd3282016-07-21 10:46:19 -07001558 toreq = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001559 if not self.live:
1560 return []
1561 if not self.job_graph:
1562 return []
James E. Blair6ab79e02017-01-06 10:10:17 -08001563 if self.item_ahead:
1564 if self.item_ahead.isHoldingFollowingChanges():
1565 return []
James E. Blairdbfd3282016-07-21 10:46:19 -07001566
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001567 successful_job_names = set()
1568 jobs_not_requested = set()
1569 for job in self.job_graph.getJobs():
1570 build = build_set.getBuild(job.name)
1571 if build and build.result == 'SUCCESS':
1572 successful_job_names.add(job.name)
1573 else:
1574 nodeset = build_set.getJobNodeSet(job.name)
1575 if nodeset is None:
1576 req = build_set.getJobNodeRequest(job.name)
1577 if req is None:
1578 jobs_not_requested.add(job)
1579
1580 # Attempt to request nodes for jobs in the order jobs appear
1581 # in configuration.
1582 for job in self.job_graph.getJobs():
1583 if job not in jobs_not_requested:
1584 continue
1585 all_parent_jobs_successful = True
1586 for parent_job in self.job_graph.getParentJobsRecursively(
1587 job.name):
1588 if parent_job.name not in successful_job_names:
1589 all_parent_jobs_successful = False
1590 break
1591 if all_parent_jobs_successful:
1592 toreq.append(job)
1593 return toreq
James E. Blairdbfd3282016-07-21 10:46:19 -07001594
1595 def setResult(self, build):
1596 if build.retry:
1597 self.removeBuild(build)
1598 elif build.result != 'SUCCESS':
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001599 for job in self.job_graph.getDependentJobsRecursively(
1600 build.job.name):
James E. Blairdbfd3282016-07-21 10:46:19 -07001601 fakebuild = Build(job, None)
1602 fakebuild.result = 'SKIPPED'
1603 self.addBuild(fakebuild)
1604
James E. Blair6ab79e02017-01-06 10:10:17 -08001605 def setNodeRequestFailure(self, job):
1606 fakebuild = Build(job, None)
1607 self.addBuild(fakebuild)
1608 fakebuild.result = 'NODE_FAILURE'
1609 self.setResult(fakebuild)
1610
James E. Blairdbfd3282016-07-21 10:46:19 -07001611 def setDequeuedNeedingChange(self):
1612 self.dequeued_needing_change = True
1613 self._setAllJobsSkipped()
1614
1615 def setUnableToMerge(self):
1616 self.current_build_set.unable_to_merge = True
1617 self._setAllJobsSkipped()
1618
James E. Blaire53250c2017-03-01 14:34:36 -08001619 def setConfigError(self, error):
1620 self.current_build_set.config_error = error
1621 self._setAllJobsSkipped()
1622
James E. Blairdbfd3282016-07-21 10:46:19 -07001623 def _setAllJobsSkipped(self):
1624 for job in self.getJobs():
1625 fakebuild = Build(job, None)
1626 fakebuild.result = 'SKIPPED'
1627 self.addBuild(fakebuild)
1628
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001629 def formatUrlPattern(self, url_pattern, job=None, build=None):
1630 url = None
1631 # Produce safe versions of objects which may be useful in
1632 # result formatting, but don't allow users to crawl through
1633 # the entire data structure where they might be able to access
1634 # secrets, etc.
1635 safe_change = self.change.getSafeAttributes()
1636 safe_pipeline = self.pipeline.getSafeAttributes()
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001637 safe_tenant = self.pipeline.layout.tenant.getSafeAttributes()
Jamie Lennox3f16de52017-05-09 14:24:11 +10001638 safe_buildset = self.current_build_set.getSafeAttributes()
K Jonathan Harkera7f390c2017-06-02 12:31:22 -07001639 safe_job = job.getSafeAttributes() if job else {}
1640 safe_build = build.getSafeAttributes() if build else {}
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001641 try:
1642 url = url_pattern.format(change=safe_change,
1643 pipeline=safe_pipeline,
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001644 tenant=safe_tenant,
Jamie Lennox3f16de52017-05-09 14:24:11 +10001645 buildset=safe_buildset,
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001646 job=safe_job,
1647 build=safe_build)
1648 except KeyError as e:
1649 self.log.error("Error while formatting url for job %s: unknown "
1650 "key %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001651 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001652 except AttributeError as e:
1653 self.log.error("Error while formatting url for job %s: unknown "
1654 "attribute %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001655 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001656 except Exception:
1657 self.log.exception("Error while formatting url for job %s with "
1658 "pattern %s:" % (job, url_pattern))
1659
1660 return url
1661
James E. Blair800e7ff2017-03-17 16:06:52 -07001662 def formatJobResult(self, job):
James E. Blairb7273ef2016-04-19 08:58:51 -07001663 build = self.current_build_set.getBuild(job.name)
1664 result = build.result
James E. Blair800e7ff2017-03-17 16:06:52 -07001665 pattern = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001666 if result == 'SUCCESS':
1667 if job.success_message:
1668 result = job.success_message
James E. Blaira5dba232016-08-08 15:53:24 -07001669 if job.success_url:
1670 pattern = job.success_url
Tobias Henkel077f2f32017-05-30 20:16:46 +02001671 else:
James E. Blairb7273ef2016-04-19 08:58:51 -07001672 if job.failure_message:
1673 result = job.failure_message
James E. Blaira5dba232016-08-08 15:53:24 -07001674 if job.failure_url:
1675 pattern = job.failure_url
James E. Blair88e79c02017-07-07 13:36:54 -07001676 url = None # The final URL
1677 default_url = build.result_data.get('zuul', {}).get('log_url')
James E. Blairb7273ef2016-04-19 08:58:51 -07001678 if pattern:
James E. Blair88e79c02017-07-07 13:36:54 -07001679 job_url = self.formatUrlPattern(pattern, job, build)
1680 else:
1681 job_url = None
1682 try:
1683 if job_url:
1684 u = urllib.parse.urlparse(job_url)
1685 if u.scheme:
1686 # The job success or failure url is absolute, so it's
1687 # our final url.
1688 url = job_url
1689 else:
1690 # We have a relative job url. Combine it with our
1691 # default url.
1692 if default_url:
1693 url = urllib.parse.urljoin(default_url, job_url)
1694 except Exception:
1695 self.log.exception("Error while parsing url for job %s:"
1696 % (job,))
James E. Blairb7273ef2016-04-19 08:58:51 -07001697 if not url:
James E. Blair88e79c02017-07-07 13:36:54 -07001698 url = default_url or build.url or job.name
James E. Blairb7273ef2016-04-19 08:58:51 -07001699 return (result, url)
1700
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001701 def formatJSON(self, websocket_url=None):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001702 ret = {}
1703 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -08001704 ret['live'] = self.live
Monty Taylor55d0f562017-05-17 14:30:21 -05001705 if hasattr(self.change, 'url') and self.change.url is not None:
1706 ret['url'] = self.change.url
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001707 else:
1708 ret['url'] = None
Monty Taylor55d0f562017-05-17 14:30:21 -05001709 ret['id'] = self.change._id()
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001710 if self.item_ahead:
1711 ret['item_ahead'] = self.item_ahead.change._id()
1712 else:
1713 ret['item_ahead'] = None
1714 ret['items_behind'] = [i.change._id() for i in self.items_behind]
1715 ret['failing_reasons'] = self.current_build_set.failing_reasons
1716 ret['zuul_ref'] = self.current_build_set.ref
Monty Taylor55d0f562017-05-17 14:30:21 -05001717 if self.change.project:
1718 ret['project'] = self.change.project.name
Ramy Asselin07cc33c2015-06-12 14:06:34 -07001719 else:
1720 # For cross-project dependencies with the depends-on
1721 # project not known to zuul, the project is None
1722 # Set it to a static value
1723 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001724 ret['enqueue_time'] = int(self.enqueue_time * 1000)
1725 ret['jobs'] = []
Monty Taylor55d0f562017-05-17 14:30:21 -05001726 if hasattr(self.change, 'owner'):
1727 ret['owner'] = self.change.owner
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001728 else:
1729 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001730 max_remaining = 0
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001731 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001732 now = time.time()
1733 build = self.current_build_set.getBuild(job.name)
1734 elapsed = None
1735 remaining = None
1736 result = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001737 build_url = None
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001738 finger_url = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001739 report_url = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001740 worker = None
1741 if build:
1742 result = build.result
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001743 finger_url = build.url
1744 # TODO(tobiash): add support for custom web root
1745 urlformat = 'static/stream.html?' \
1746 'uuid={build.uuid}&' \
1747 'logfile=console.log'
1748 if websocket_url:
1749 urlformat += '&websocket_url={websocket_url}'
1750 build_url = urlformat.format(
1751 build=build, websocket_url=websocket_url)
James E. Blair800e7ff2017-03-17 16:06:52 -07001752 (unused, report_url) = self.formatJobResult(job)
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001753 if build.start_time:
1754 if build.end_time:
1755 elapsed = int((build.end_time -
1756 build.start_time) * 1000)
1757 remaining = 0
1758 else:
1759 elapsed = int((now - build.start_time) * 1000)
1760 if build.estimated_time:
1761 remaining = max(
1762 int(build.estimated_time * 1000) - elapsed,
1763 0)
1764 worker = {
1765 'name': build.worker.name,
1766 'hostname': build.worker.hostname,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001767 }
1768 if remaining and remaining > max_remaining:
1769 max_remaining = remaining
1770
1771 ret['jobs'].append({
1772 'name': job.name,
Tobias Henkel65639f82017-07-10 10:25:42 +02001773 'dependencies': list(job.dependencies),
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001774 'elapsed_time': elapsed,
1775 'remaining_time': remaining,
James E. Blairb7273ef2016-04-19 08:58:51 -07001776 'url': build_url,
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001777 'finger_url': finger_url,
James E. Blairb7273ef2016-04-19 08:58:51 -07001778 'report_url': report_url,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001779 'result': result,
1780 'voting': job.voting,
1781 'uuid': build.uuid if build else None,
Paul Belanger174a8272017-03-14 13:20:10 -04001782 'execute_time': build.execute_time if build else None,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001783 'start_time': build.start_time if build else None,
1784 'end_time': build.end_time if build else None,
1785 'estimated_time': build.estimated_time if build else None,
1786 'pipeline': build.pipeline.name if build else None,
1787 'canceled': build.canceled if build else None,
1788 'retry': build.retry if build else None,
Timothy Chavezb2332082015-08-07 20:08:04 -05001789 'node_labels': build.node_labels if build else [],
1790 'node_name': build.node_name if build else None,
1791 'worker': worker,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001792 })
1793
James E. Blairdbfd3282016-07-21 10:46:19 -07001794 if self.haveAllJobsStarted():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001795 ret['remaining_time'] = max_remaining
1796 else:
1797 ret['remaining_time'] = None
1798 return ret
1799
1800 def formatStatus(self, indent=0, html=False):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001801 indent_str = ' ' * indent
1802 ret = ''
Monty Taylor55d0f562017-05-17 14:30:21 -05001803 if html and getattr(self.change, 'url', None) is not None:
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001804 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
1805 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001806 self.change.project.name,
1807 self.change.url,
1808 self.change._id())
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001809 else:
1810 ret += '%sProject %s change %s based on %s\n' % (
1811 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001812 self.change.project.name,
1813 self.change._id(),
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001814 self.item_ahead)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001815 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001816 build = self.current_build_set.getBuild(job.name)
1817 if build:
1818 result = build.result
1819 else:
1820 result = None
1821 job_name = job.name
1822 if not job.voting:
1823 voting = ' (non-voting)'
1824 else:
1825 voting = ''
1826 if html:
1827 if build:
1828 url = build.url
1829 else:
1830 url = None
1831 if url is not None:
1832 job_name = '<a href="%s">%s</a>' % (url, job_name)
1833 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
1834 ret += '\n'
1835 return ret
1836
James E. Blaira04b0792017-04-27 09:59:06 -07001837 def makeMergerItem(self):
1838 # Create a dictionary with all info about the item needed by
1839 # the merger.
1840 number = None
1841 patchset = None
1842 oldrev = None
1843 newrev = None
1844 refspec = None
James E. Blair21037782017-07-19 11:56:55 -07001845 branch = None
James E. Blaira04b0792017-04-27 09:59:06 -07001846 if hasattr(self.change, 'number'):
1847 number = self.change.number
1848 patchset = self.change.patchset
1849 refspec = self.change.refspec
James E. Blair21037782017-07-19 11:56:55 -07001850 if hasattr(self.change, 'newrev'):
James E. Blaira04b0792017-04-27 09:59:06 -07001851 oldrev = self.change.oldrev
1852 newrev = self.change.newrev
James E. Blair21037782017-07-19 11:56:55 -07001853 if hasattr(self.change, 'branch'):
1854 branch = self.change.branch
1855
James E. Blaira04b0792017-04-27 09:59:06 -07001856 source = self.change.project.source
1857 connection_name = source.connection.connection_name
James E. Blair2a535672017-04-27 12:03:15 -07001858 project = self.change.project
James E. Blaira04b0792017-04-27 09:59:06 -07001859
James E. Blair2a535672017-04-27 12:03:15 -07001860 return dict(project=project.name,
1861 connection=connection_name,
James E. Blaira04b0792017-04-27 09:59:06 -07001862 merge_mode=self.current_build_set.getMergeMode(),
1863 refspec=refspec,
1864 branch=branch,
1865 ref=self.current_build_set.ref,
1866 number=number,
1867 patchset=patchset,
1868 oldrev=oldrev,
1869 newrev=newrev,
1870 )
1871
James E. Blairfee8d652013-06-07 08:57:52 -07001872
Clint Byrumf8cc9902017-03-22 22:38:25 -07001873class Ref(object):
1874 """An existing state of a Project."""
James E. Blairfee8d652013-06-07 08:57:52 -07001875
1876 def __init__(self, project):
1877 self.project = project
Clint Byrumf8cc9902017-03-22 22:38:25 -07001878 self.ref = None
1879 self.oldrev = None
1880 self.newrev = None
Jesse Keating71a47ff2017-06-06 11:36:43 -07001881 self.files = []
1882
Clint Byrumf8cc9902017-03-22 22:38:25 -07001883 def _id(self):
1884 return self.newrev
1885
1886 def __repr__(self):
1887 rep = None
1888 if self.newrev == '0000000000000000000000000000000000000000':
James E. Blair21037782017-07-19 11:56:55 -07001889 rep = '<%s 0x%x deletes %s from %s' % (
1890 type(self).__name__,
1891 id(self), self.ref, self.oldrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001892 elif self.oldrev == '0000000000000000000000000000000000000000':
James E. Blair21037782017-07-19 11:56:55 -07001893 rep = '<%s 0x%x creates %s on %s>' % (
1894 type(self).__name__,
1895 id(self), self.ref, self.newrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001896 else:
1897 # Catch all
James E. Blair21037782017-07-19 11:56:55 -07001898 rep = '<%s 0x%x %s updated %s..%s>' % (
1899 type(self).__name__,
1900 id(self), self.ref, self.oldrev, self.newrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001901 return rep
1902
James E. Blairfee8d652013-06-07 08:57:52 -07001903 def equals(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001904 if (self.project == other.project
1905 and self.ref == other.ref
1906 and self.newrev == other.newrev):
1907 return True
1908 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001909
1910 def isUpdateOf(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001911 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001912
1913 def filterJobs(self, jobs):
1914 return filter(lambda job: job.changeMatches(self), jobs)
1915
1916 def getRelatedChanges(self):
1917 return set()
1918
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001919 def updatesConfig(self):
Tristan Cacqueray829e6172017-06-13 06:49:36 +00001920 if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files or \
1921 [True for fn in self.files if fn.startswith("zuul.d/") or
1922 fn.startswith(".zuul.d/")]:
Jesse Keating71a47ff2017-06-06 11:36:43 -07001923 return True
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001924 return False
1925
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001926 def getSafeAttributes(self):
1927 return Attributes(project=self.project,
1928 ref=self.ref,
1929 oldrev=self.oldrev,
1930 newrev=self.newrev)
1931
James E. Blair1e8dd892012-05-30 09:15:05 -07001932
James E. Blair21037782017-07-19 11:56:55 -07001933class Branch(Ref):
1934 """An existing branch state for a Project."""
1935 def __init__(self, project):
1936 super(Branch, self).__init__(project)
1937 self.branch = None
1938
1939
1940class Tag(Ref):
1941 """An existing tag state for a Project."""
1942 def __init__(self, project):
1943 super(Tag, self).__init__(project)
1944 self.tag = None
1945
1946
1947class Change(Branch):
Monty Taylora42a55b2016-07-29 07:53:33 -07001948 """A proposed new state for a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001949 def __init__(self, project):
1950 super(Change, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -07001951 self.number = None
1952 self.url = None
1953 self.patchset = None
1954 self.refspec = None
1955
James E. Blair6965a4b2014-12-16 17:19:04 -08001956 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -07001957 self.needed_by_changes = []
1958 self.is_current_patchset = True
1959 self.can_merge = False
1960 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -07001961 self.failed_to_merge = False
James E. Blair11041d22014-05-02 14:49:53 -07001962 self.open = None
1963 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001964 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001965
Jan Hruban3b415922016-02-03 13:10:22 +01001966 self.source_event = None
1967
James E. Blair4aea70c2012-07-26 14:23:24 -07001968 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -07001969 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -07001970
1971 def __repr__(self):
1972 return '<Change 0x%x %s>' % (id(self), self._id())
1973
1974 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +08001975 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -07001976 return True
1977 return False
1978
James E. Blair2fa50962013-01-30 21:50:41 -08001979 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -08001980 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -07001981 (hasattr(other, 'patchset') and
1982 self.patchset is not None and
1983 other.patchset is not None and
1984 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -08001985 return True
1986 return False
1987
James E. Blairfee8d652013-06-07 08:57:52 -07001988 def getRelatedChanges(self):
1989 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -08001990 for c in self.needs_changes:
1991 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -07001992 for c in self.needed_by_changes:
1993 related.add(c)
1994 related.update(c.getRelatedChanges())
1995 return related
James E. Blair4aea70c2012-07-26 14:23:24 -07001996
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001997 def getSafeAttributes(self):
1998 return Attributes(project=self.project,
1999 number=self.number,
2000 patchset=self.patchset)
2001
James E. Blair4aea70c2012-07-26 14:23:24 -07002002
James E. Blairee743612012-05-29 14:49:32 -07002003class TriggerEvent(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002004 """Incoming event from an external system."""
James E. Blairee743612012-05-29 14:49:32 -07002005 def __init__(self):
James E. Blairaad3ae22017-05-18 14:11:29 -07002006 # TODO(jeblair): further reduce this list
James E. Blairee743612012-05-29 14:49:32 -07002007 self.data = None
James E. Blair32663402012-06-01 10:04:18 -07002008 # common
James E. Blairee743612012-05-29 14:49:32 -07002009 self.type = None
Jesse Keating71a47ff2017-06-06 11:36:43 -07002010 self.branch_updated = False
Paul Belangerbaca3132016-11-04 12:49:54 -04002011 # For management events (eg: enqueue / promote)
2012 self.tenant_name = None
James E. Blair6f284b42017-03-31 14:14:41 -07002013 self.project_hostname = None
James E. Blairee743612012-05-29 14:49:32 -07002014 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -07002015 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +01002016 # Representation of the user account that performed the event.
2017 self.account = None
James E. Blair32663402012-06-01 10:04:18 -07002018 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -07002019 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -07002020 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -07002021 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -07002022 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -07002023 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07002024 self.comment = None
Jesse Keating5c05a9f2017-01-12 14:44:58 -08002025 self.state = None
James E. Blair32663402012-06-01 10:04:18 -07002026 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -07002027 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -07002028 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07002029 self.newrev = None
James E. Blairad28e912013-11-27 10:43:22 -08002030 # For events that arrive with a destination pipeline (eg, from
2031 # an admin command, etc):
2032 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07002033
James E. Blair6f284b42017-03-31 14:14:41 -07002034 @property
2035 def canonical_project_name(self):
2036 return self.project_hostname + '/' + self.project_name
2037
Jan Hruban324ca5b2015-11-05 19:28:54 +01002038 def isPatchsetCreated(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01002039 return False
2040
2041 def isChangeAbandoned(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01002042 return False
2043
James E. Blair1e8dd892012-05-30 09:15:05 -07002044
James E. Blair9c17dbf2014-06-23 14:21:58 -07002045class BaseFilter(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002046 """Base Class for filtering which Changes and Events to process."""
James E. Blairaad3ae22017-05-18 14:11:29 -07002047 pass
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002048
James E. Blair9c17dbf2014-06-23 14:21:58 -07002049
2050class EventFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002051 """Allows a Pipeline to only respond to certain events."""
James E. Blairaad3ae22017-05-18 14:11:29 -07002052 def __init__(self, trigger):
2053 super(EventFilter, self).__init__()
James E. Blairc0dedf82014-08-06 09:37:52 -07002054 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07002055
James E. Blairaad3ae22017-05-18 14:11:29 -07002056 def matches(self, event, ref):
2057 # TODO(jeblair): consider removing ref argument
James E. Blairee743612012-05-29 14:49:32 -07002058 return True
James E. Blaireff88162013-07-01 12:44:14 -04002059
2060
James E. Blairaad3ae22017-05-18 14:11:29 -07002061class RefFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002062 """Allows a Manager to only enqueue Changes that meet certain criteria."""
Jesse Keating8c2eb572017-05-30 17:31:45 -07002063 def __init__(self, connection_name):
James E. Blairaad3ae22017-05-18 14:11:29 -07002064 super(RefFilter, self).__init__()
Jesse Keating8c2eb572017-05-30 17:31:45 -07002065 self.connection_name = connection_name
James E. Blair11041d22014-05-02 14:49:53 -07002066
2067 def matches(self, change):
James E. Blair11041d22014-05-02 14:49:53 -07002068 return True
2069
2070
James E. Blairb97ed802015-12-21 15:55:35 -08002071class ProjectPipelineConfig(object):
2072 # Represents a project cofiguration in the context of a pipeline
2073 def __init__(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002074 self.job_list = JobList()
James E. Blairb97ed802015-12-21 15:55:35 -08002075 self.queue_name = None
Adam Gandelman8bd57102016-12-02 12:58:42 -08002076 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08002077
2078
James E. Blair08d9b782017-06-29 14:22:48 -07002079class TenantProjectConfig(object):
2080 """A project in the context of a tenant.
2081
2082 A Project is globally unique in the system, however, when used in
2083 a tenant, some metadata about the project local to the tenant is
2084 stored in a TenantProjectConfig.
2085 """
2086
2087 def __init__(self, project):
2088 self.project = project
2089 self.load_classes = set()
James E. Blair6459db12017-06-29 14:57:20 -07002090 self.shadow_projects = set()
James E. Blair08d9b782017-06-29 14:22:48 -07002091
2092
James E. Blairb97ed802015-12-21 15:55:35 -08002093class ProjectConfig(object):
2094 # Represents a project cofiguration
2095 def __init__(self, name):
2096 self.name = name
Adam Gandelman8bd57102016-12-02 12:58:42 -08002097 self.merge_mode = None
James E. Blair040b6502017-05-23 10:18:21 -07002098 self.default_branch = None
James E. Blairb97ed802015-12-21 15:55:35 -08002099 self.pipelines = {}
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002100 self.private_key_file = None
James E. Blairb97ed802015-12-21 15:55:35 -08002101
2102
James E. Blaird8e778f2015-12-22 14:09:20 -08002103class UnparsedAbideConfig(object):
James E. Blair08d9b782017-06-29 14:22:48 -07002104
Monty Taylora42a55b2016-07-29 07:53:33 -07002105 """A collection of yaml lists that has not yet been parsed into objects.
2106
2107 An Abide is a collection of tenants.
2108 """
2109
James E. Blaird8e778f2015-12-22 14:09:20 -08002110 def __init__(self):
2111 self.tenants = []
2112
2113 def extend(self, conf):
2114 if isinstance(conf, UnparsedAbideConfig):
2115 self.tenants.extend(conf.tenants)
2116 return
2117
2118 if not isinstance(conf, list):
2119 raise Exception("Configuration items must be in the form of "
2120 "a list of dictionaries (when parsing %s)" %
2121 (conf,))
2122 for item in conf:
2123 if not isinstance(item, dict):
2124 raise Exception("Configuration items must be in the form of "
2125 "a list of dictionaries (when parsing %s)" %
2126 (conf,))
2127 if len(item.keys()) > 1:
2128 raise Exception("Configuration item dictionaries must have "
2129 "a single key (when parsing %s)" %
2130 (conf,))
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002131 key, value = list(item.items())[0]
James E. Blaird8e778f2015-12-22 14:09:20 -08002132 if key == 'tenant':
2133 self.tenants.append(value)
2134 else:
2135 raise Exception("Configuration item not recognized "
2136 "(when parsing %s)" %
2137 (conf,))
2138
2139
2140class UnparsedTenantConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002141 """A collection of yaml lists that has not yet been parsed into objects."""
2142
James E. Blaird8e778f2015-12-22 14:09:20 -08002143 def __init__(self):
2144 self.pipelines = []
2145 self.jobs = []
2146 self.project_templates = []
James E. Blairff555742017-02-19 11:34:27 -08002147 self.projects = {}
James E. Blaira98340f2016-09-02 11:33:49 -07002148 self.nodesets = []
James E. Blair01f83b72017-03-15 13:03:40 -07002149 self.secrets = []
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002150 self.semaphores = []
James E. Blaird8e778f2015-12-22 14:09:20 -08002151
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002152 def copy(self):
2153 r = UnparsedTenantConfig()
2154 r.pipelines = copy.deepcopy(self.pipelines)
2155 r.jobs = copy.deepcopy(self.jobs)
2156 r.project_templates = copy.deepcopy(self.project_templates)
2157 r.projects = copy.deepcopy(self.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07002158 r.nodesets = copy.deepcopy(self.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002159 r.secrets = copy.deepcopy(self.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002160 r.semaphores = copy.deepcopy(self.semaphores)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002161 return r
2162
James E. Blairec7ff302017-03-04 07:31:32 -08002163 def extend(self, conf):
James E. Blaird8e778f2015-12-22 14:09:20 -08002164 if isinstance(conf, UnparsedTenantConfig):
2165 self.pipelines.extend(conf.pipelines)
2166 self.jobs.extend(conf.jobs)
2167 self.project_templates.extend(conf.project_templates)
James E. Blairff555742017-02-19 11:34:27 -08002168 for k, v in conf.projects.items():
2169 self.projects.setdefault(k, []).extend(v)
James E. Blaira98340f2016-09-02 11:33:49 -07002170 self.nodesets.extend(conf.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002171 self.secrets.extend(conf.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002172 self.semaphores.extend(conf.semaphores)
James E. Blaird8e778f2015-12-22 14:09:20 -08002173 return
2174
2175 if not isinstance(conf, list):
2176 raise Exception("Configuration items must be in the form of "
2177 "a list of dictionaries (when parsing %s)" %
2178 (conf,))
James E. Blaircdab2032017-02-01 09:09:29 -08002179
James E. Blaird8e778f2015-12-22 14:09:20 -08002180 for item in conf:
2181 if not isinstance(item, dict):
2182 raise Exception("Configuration items must be in the form of "
2183 "a list of dictionaries (when parsing %s)" %
2184 (conf,))
2185 if len(item.keys()) > 1:
2186 raise Exception("Configuration item dictionaries must have "
2187 "a single key (when parsing %s)" %
2188 (conf,))
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002189 key, value = list(item.items())[0]
James E. Blair66b274e2017-01-31 14:47:52 -08002190 if key == 'project':
James E. Blairff555742017-02-19 11:34:27 -08002191 name = value['name']
2192 self.projects.setdefault(name, []).append(value)
James E. Blair66b274e2017-01-31 14:47:52 -08002193 elif key == 'job':
James E. Blaird8e778f2015-12-22 14:09:20 -08002194 self.jobs.append(value)
2195 elif key == 'project-template':
2196 self.project_templates.append(value)
2197 elif key == 'pipeline':
2198 self.pipelines.append(value)
James E. Blaira98340f2016-09-02 11:33:49 -07002199 elif key == 'nodeset':
2200 self.nodesets.append(value)
James E. Blair01f83b72017-03-15 13:03:40 -07002201 elif key == 'secret':
2202 self.secrets.append(value)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002203 elif key == 'semaphore':
2204 self.semaphores.append(value)
James E. Blaird8e778f2015-12-22 14:09:20 -08002205 else:
Morgan Fainberg4245a422016-08-05 16:20:12 -07002206 raise Exception("Configuration item `%s` not recognized "
James E. Blaird8e778f2015-12-22 14:09:20 -08002207 "(when parsing %s)" %
Morgan Fainberg4245a422016-08-05 16:20:12 -07002208 (item, conf,))
James E. Blaird8e778f2015-12-22 14:09:20 -08002209
2210
James E. Blaireff88162013-07-01 12:44:14 -04002211class Layout(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002212 """Holds all of the Pipelines."""
2213
James E. Blair6459db12017-06-29 14:57:20 -07002214 def __init__(self, tenant):
2215 self.tenant = tenant
James E. Blairb97ed802015-12-21 15:55:35 -08002216 self.project_configs = {}
2217 self.project_templates = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07002218 self.pipelines = OrderedDict()
James E. Blair83005782015-12-11 14:46:03 -08002219 # This is a dictionary of name -> [jobs]. The first element
2220 # of the list is the first job added with that name. It is
2221 # the reference definition for a given job. Subsequent
2222 # elements are aspects of that job with different matchers
2223 # that override some attribute of the job. These aspects all
2224 # inherit from the reference definition.
James E. Blairfef88ec2016-11-30 08:38:27 -08002225 self.jobs = {'noop': [Job('noop')]}
James E. Blaira98340f2016-09-02 11:33:49 -07002226 self.nodesets = {}
James E. Blair01f83b72017-03-15 13:03:40 -07002227 self.secrets = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002228 self.semaphores = {}
James E. Blaireff88162013-07-01 12:44:14 -04002229
2230 def getJob(self, name):
2231 if name in self.jobs:
James E. Blair83005782015-12-11 14:46:03 -08002232 return self.jobs[name][0]
James E. Blairb97ed802015-12-21 15:55:35 -08002233 raise Exception("Job %s not defined" % (name,))
James E. Blair83005782015-12-11 14:46:03 -08002234
2235 def getJobs(self, name):
2236 return self.jobs.get(name, [])
2237
2238 def addJob(self, job):
James E. Blair4317e9f2016-07-15 10:05:47 -07002239 # We can have multiple variants of a job all with the same
2240 # name, but these variants must all be defined in the same repo.
James E. Blaircdab2032017-02-01 09:09:29 -08002241 prior_jobs = [j for j in self.getJobs(job.name) if
2242 j.source_context.project !=
2243 job.source_context.project]
James E. Blair6459db12017-06-29 14:57:20 -07002244 # Unless the repo is permitted to shadow another. If so, and
2245 # the job we are adding is from a repo that is permitted to
2246 # shadow the one with the older jobs, skip adding this job.
2247 job_project = job.source_context.project
2248 job_tpc = self.tenant.project_configs[job_project.canonical_name]
2249 skip_add = False
2250 for prior_job in prior_jobs[:]:
2251 prior_project = prior_job.source_context.project
2252 if prior_project in job_tpc.shadow_projects:
2253 prior_jobs.remove(prior_job)
2254 skip_add = True
2255
James E. Blair4317e9f2016-07-15 10:05:47 -07002256 if prior_jobs:
2257 raise Exception("Job %s in %s is not permitted to shadow "
James E. Blaircdab2032017-02-01 09:09:29 -08002258 "job %s in %s" % (
2259 job,
2260 job.source_context.project,
2261 prior_jobs[0],
2262 prior_jobs[0].source_context.project))
James E. Blair6459db12017-06-29 14:57:20 -07002263 if skip_add:
2264 return False
James E. Blair83005782015-12-11 14:46:03 -08002265 if job.name in self.jobs:
2266 self.jobs[job.name].append(job)
2267 else:
2268 self.jobs[job.name] = [job]
James E. Blair6459db12017-06-29 14:57:20 -07002269 return True
James E. Blair83005782015-12-11 14:46:03 -08002270
James E. Blaira98340f2016-09-02 11:33:49 -07002271 def addNodeSet(self, nodeset):
2272 if nodeset.name in self.nodesets:
2273 raise Exception("NodeSet %s already defined" % (nodeset.name,))
2274 self.nodesets[nodeset.name] = nodeset
2275
James E. Blair01f83b72017-03-15 13:03:40 -07002276 def addSecret(self, secret):
2277 if secret.name in self.secrets:
2278 raise Exception("Secret %s already defined" % (secret.name,))
2279 self.secrets[secret.name] = secret
2280
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002281 def addSemaphore(self, semaphore):
2282 if semaphore.name in self.semaphores:
2283 raise Exception("Semaphore %s already defined" % (semaphore.name,))
2284 self.semaphores[semaphore.name] = semaphore
2285
James E. Blair83005782015-12-11 14:46:03 -08002286 def addPipeline(self, pipeline):
2287 self.pipelines[pipeline.name] = pipeline
James E. Blair59fdbac2015-12-07 17:08:06 -08002288
James E. Blairb97ed802015-12-21 15:55:35 -08002289 def addProjectTemplate(self, project_template):
2290 self.project_templates[project_template.name] = project_template
2291
James E. Blairf59f3cf2017-02-19 14:50:26 -08002292 def addProjectConfig(self, project_config):
James E. Blairb97ed802015-12-21 15:55:35 -08002293 self.project_configs[project_config.name] = project_config
James E. Blairb97ed802015-12-21 15:55:35 -08002294
James E. Blaird2348362017-03-17 13:59:35 -07002295 def _createJobGraph(self, item, job_list, job_graph):
2296 change = item.change
2297 pipeline = item.pipeline
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002298 for jobname in job_list.jobs:
2299 # This is the final job we are constructing
James E. Blaira7f51ca2017-02-07 16:01:26 -08002300 frozen_job = None
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002301 # Whether the change matches any globally defined variant
James E. Blaira7f51ca2017-02-07 16:01:26 -08002302 matched = False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002303 for variant in self.getJobs(jobname):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002304 if variant.changeMatches(change):
James E. Blaira7f51ca2017-02-07 16:01:26 -08002305 if frozen_job is None:
2306 frozen_job = variant.copy()
2307 frozen_job.setRun()
2308 else:
2309 frozen_job.applyVariant(variant)
2310 matched = True
2311 if not matched:
James E. Blair6e85c2b2016-11-21 16:47:01 -08002312 # A change must match at least one defined job variant
2313 # (that is to say that it must match more than just
2314 # the job that is defined in the tree).
2315 continue
James E. Blaira7f51ca2017-02-07 16:01:26 -08002316 # If the job does not allow auth inheritance, do not allow
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002317 # the project-pipeline variants to update its execution
James E. Blaira7f51ca2017-02-07 16:01:26 -08002318 # attributes.
James E. Blair8525e2b2017-03-15 14:05:47 -07002319 if frozen_job.auth and not frozen_job.auth.inherit:
James E. Blaira7f51ca2017-02-07 16:01:26 -08002320 frozen_job.final = True
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002321 # Whether the change matches any of the project pipeline
2322 # variants
2323 matched = False
2324 for variant in job_list.jobs[jobname]:
2325 if variant.changeMatches(change):
2326 frozen_job.applyVariant(variant)
2327 matched = True
2328 if not matched:
2329 # A change must match at least one project pipeline
2330 # job variant.
2331 continue
James E. Blairb3f5db12017-03-17 12:57:39 -07002332 if (frozen_job.allowed_projects and
2333 change.project.name not in frozen_job.allowed_projects):
2334 raise Exception("Project %s is not allowed to run job %s" %
2335 (change.project.name, frozen_job.name))
James E. Blaird2348362017-03-17 13:59:35 -07002336 if ((not pipeline.allow_secrets) and frozen_job.auth and
2337 frozen_job.auth.secrets):
2338 raise Exception("Pipeline %s does not allow jobs with "
2339 "secrets (job %s)" % (
2340 pipeline.name, frozen_job.name))
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002341 job_graph.addJob(frozen_job)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002342
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002343 def createJobGraph(self, item):
Paul Belanger160cb8e2016-11-11 19:04:24 -05002344 project_config = self.project_configs.get(
James E. Blair0ffa0102017-03-30 13:11:33 -07002345 item.change.project.canonical_name, None)
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002346 ret = JobGraph()
Paul Belanger15e3e202016-10-14 16:27:34 -04002347 # NOTE(pabelanger): It is possible for a foreign project not to have a
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002348 # configured pipeline, if so return an empty JobGraph.
Paul Belanger160cb8e2016-11-11 19:04:24 -05002349 if project_config and item.pipeline.name in project_config.pipelines:
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002350 project_job_list = \
2351 project_config.pipelines[item.pipeline.name].job_list
James E. Blaird2348362017-03-17 13:59:35 -07002352 self._createJobGraph(item, project_job_list, ret)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002353 return ret
2354
James E. Blair0d3e83b2017-06-05 13:51:57 -07002355 def hasProject(self, project):
2356 return project.canonical_name in self.project_configs
2357
James E. Blair59fdbac2015-12-07 17:08:06 -08002358
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002359class Semaphore(object):
2360 def __init__(self, name, max=1):
2361 self.name = name
2362 self.max = int(max)
2363
2364
2365class SemaphoreHandler(object):
2366 log = logging.getLogger("zuul.SemaphoreHandler")
2367
2368 def __init__(self):
2369 self.semaphores = {}
2370
2371 def acquire(self, item, job):
2372 if not job.semaphore:
2373 return True
2374
2375 semaphore_key = job.semaphore
2376
2377 m = self.semaphores.get(semaphore_key)
2378 if not m:
2379 # The semaphore is not held, acquire it
2380 self._acquire(semaphore_key, item, job.name)
2381 return True
2382 if (item, job.name) in m:
2383 # This item already holds the semaphore
2384 return True
2385
2386 # semaphore is there, check max
2387 if len(m) < self._max_count(item, job.semaphore):
2388 self._acquire(semaphore_key, item, job.name)
2389 return True
2390
2391 return False
2392
2393 def release(self, item, job):
2394 if not job.semaphore:
2395 return
2396
2397 semaphore_key = job.semaphore
2398
2399 m = self.semaphores.get(semaphore_key)
2400 if not m:
2401 # The semaphore is not held, nothing to do
2402 self.log.error("Semaphore can not be released for %s "
2403 "because the semaphore is not held" %
2404 item)
2405 return
2406 if (item, job.name) in m:
2407 # This item is a holder of the semaphore
2408 self._release(semaphore_key, item, job.name)
2409 return
2410 self.log.error("Semaphore can not be released for %s "
2411 "which does not hold it" % item)
2412
2413 def _acquire(self, semaphore_key, item, job_name):
2414 self.log.debug("Semaphore acquire {semaphore}: job {job}, item {item}"
2415 .format(semaphore=semaphore_key,
2416 job=job_name,
2417 item=item))
2418 if semaphore_key not in self.semaphores:
2419 self.semaphores[semaphore_key] = []
2420 self.semaphores[semaphore_key].append((item, job_name))
2421
2422 def _release(self, semaphore_key, item, job_name):
2423 self.log.debug("Semaphore release {semaphore}: job {job}, item {item}"
2424 .format(semaphore=semaphore_key,
2425 job=job_name,
2426 item=item))
2427 sem_item = (item, job_name)
2428 if sem_item in self.semaphores[semaphore_key]:
2429 self.semaphores[semaphore_key].remove(sem_item)
2430
2431 # cleanup if there is no user of the semaphore anymore
2432 if len(self.semaphores[semaphore_key]) == 0:
2433 del self.semaphores[semaphore_key]
2434
2435 @staticmethod
2436 def _max_count(item, semaphore_name):
2437 if not item.current_build_set.layout:
2438 # This should not occur as the layout of the item must already be
2439 # built when acquiring or releasing a semaphore for a job.
2440 raise Exception("Item {} has no layout".format(item))
2441
2442 # find the right semaphore
2443 default_semaphore = Semaphore(semaphore_name, 1)
2444 semaphores = item.current_build_set.layout.semaphores
2445 return semaphores.get(semaphore_name, default_semaphore).max
2446
2447
James E. Blair59fdbac2015-12-07 17:08:06 -08002448class Tenant(object):
2449 def __init__(self, name):
2450 self.name = name
2451 self.layout = None
James E. Blair646322f2017-01-27 15:50:34 -08002452 # The unparsed configuration from the main zuul config for
2453 # this tenant.
2454 self.unparsed_config = None
James E. Blair109da3f2017-04-04 14:39:43 -07002455 # The list of projects from which we will read full
James E. Blair8a395f92017-03-30 11:15:33 -07002456 # configuration.
James E. Blair109da3f2017-04-04 14:39:43 -07002457 self.config_projects = []
2458 # The unparsed config from those projects.
2459 self.config_projects_config = None
2460 # The list of projects from which we will read untrusted
2461 # in-repo configuration.
2462 self.untrusted_projects = []
2463 # The unparsed config from those projects.
2464 self.untrusted_projects_config = None
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002465 self.semaphore_handler = SemaphoreHandler()
James E. Blair08d9b782017-06-29 14:22:48 -07002466 # Metadata about projects for this tenant
2467 # canonical project name -> TenantProjectConfig
2468 self.project_configs = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002469
James E. Blairc2a54fd2017-03-29 15:19:26 -07002470 # A mapping of project names to projects. project_name ->
2471 # VALUE where VALUE is a further dictionary of
2472 # canonical_hostname -> Project.
2473 self.projects = {}
2474 self.canonical_hostnames = set()
2475
James E. Blair08d9b782017-06-29 14:22:48 -07002476 def _addProject(self, tpc):
James E. Blairc2a54fd2017-03-29 15:19:26 -07002477 """Add a project to the project index
2478
James E. Blair08d9b782017-06-29 14:22:48 -07002479 :arg TenantProjectConfig tpc: The TenantProjectConfig (with
2480 associated project) to add.
2481
James E. Blairc2a54fd2017-03-29 15:19:26 -07002482 """
James E. Blair08d9b782017-06-29 14:22:48 -07002483 project = tpc.project
James E. Blairc2a54fd2017-03-29 15:19:26 -07002484 self.canonical_hostnames.add(project.canonical_hostname)
2485 hostname_dict = self.projects.setdefault(project.name, {})
2486 if project.canonical_hostname in hostname_dict:
2487 raise Exception("Project %s is already in project index" %
2488 (project,))
2489 hostname_dict[project.canonical_hostname] = project
James E. Blair08d9b782017-06-29 14:22:48 -07002490 self.project_configs[project.canonical_name] = tpc
James E. Blairc2a54fd2017-03-29 15:19:26 -07002491
2492 def getProject(self, name):
2493 """Return a project given its name.
2494
2495 :arg str name: The name of the project. It may be fully
2496 qualified (E.g., "git.example.com/subpath/project") or may
2497 contain only the project name name may be supplied (E.g.,
2498 "subpath/project").
2499
2500 :returns: A tuple (trusted, project) or (None, None) if the
2501 project is not found or ambiguous. The "trusted" boolean
2502 indicates whether or not the project is trusted by this
2503 tenant.
2504 :rtype: (bool, Project)
2505
2506 """
2507 path = name.split('/', 1)
2508 if path[0] in self.canonical_hostnames:
2509 hostname = path[0]
2510 project_name = path[1]
2511 else:
2512 hostname = None
2513 project_name = name
2514 hostname_dict = self.projects.get(project_name)
2515 project = None
2516 if hostname_dict:
2517 if hostname:
2518 project = hostname_dict.get(hostname)
2519 else:
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002520 values = list(hostname_dict.values())
James E. Blairc2a54fd2017-03-29 15:19:26 -07002521 if len(values) == 1:
2522 project = values[0]
2523 else:
2524 raise Exception("Project name '%s' is ambiguous, "
2525 "please fully qualify the project "
2526 "with a hostname" % (name,))
2527 if project is None:
2528 return (None, None)
James E. Blair109da3f2017-04-04 14:39:43 -07002529 if project in self.config_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002530 return (True, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002531 if project in self.untrusted_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002532 return (False, project)
2533 # This should never happen:
2534 raise Exception("Project %s is neither trusted nor untrusted" %
2535 (project,))
2536
James E. Blair08d9b782017-06-29 14:22:48 -07002537 def addConfigProject(self, tpc):
2538 self.config_projects.append(tpc.project)
2539 self._addProject(tpc)
James E. Blair5ac93842017-01-20 06:47:34 -08002540
James E. Blair08d9b782017-06-29 14:22:48 -07002541 def addUntrustedProject(self, tpc):
2542 self.untrusted_projects.append(tpc.project)
2543 self._addProject(tpc)
James E. Blair5ac93842017-01-20 06:47:34 -08002544
Jamie Lennoxbf997c82017-05-10 09:58:40 +10002545 def getSafeAttributes(self):
2546 return Attributes(name=self.name)
2547
James E. Blair59fdbac2015-12-07 17:08:06 -08002548
2549class Abide(object):
2550 def __init__(self):
2551 self.tenants = OrderedDict()
James E. Blairce8a2132016-05-19 15:21:52 -07002552
2553
2554class JobTimeData(object):
2555 format = 'B10H10H10B'
2556 version = 0
2557
2558 def __init__(self, path):
2559 self.path = path
2560 self.success_times = [0 for x in range(10)]
2561 self.failure_times = [0 for x in range(10)]
2562 self.results = [0 for x in range(10)]
2563
2564 def load(self):
2565 if not os.path.exists(self.path):
2566 return
Clint Byruma4471d12017-05-10 20:57:40 -04002567 with open(self.path, 'rb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002568 data = struct.unpack(self.format, f.read())
2569 version = data[0]
2570 if version != self.version:
2571 raise Exception("Unkown data version")
2572 self.success_times = list(data[1:11])
2573 self.failure_times = list(data[11:21])
2574 self.results = list(data[21:32])
2575
2576 def save(self):
2577 tmpfile = self.path + '.tmp'
2578 data = [self.version]
2579 data.extend(self.success_times)
2580 data.extend(self.failure_times)
2581 data.extend(self.results)
2582 data = struct.pack(self.format, *data)
Clint Byruma4471d12017-05-10 20:57:40 -04002583 with open(tmpfile, 'wb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002584 f.write(data)
2585 os.rename(tmpfile, self.path)
2586
2587 def add(self, elapsed, result):
2588 elapsed = int(elapsed)
2589 if result == 'SUCCESS':
2590 self.success_times.append(elapsed)
2591 self.success_times.pop(0)
2592 result = 0
2593 else:
2594 self.failure_times.append(elapsed)
2595 self.failure_times.pop(0)
2596 result = 1
2597 self.results.append(result)
2598 self.results.pop(0)
2599
2600 def getEstimatedTime(self):
2601 times = [x for x in self.success_times if x]
2602 if times:
2603 return float(sum(times)) / len(times)
2604 return 0.0
2605
2606
2607class TimeDataBase(object):
2608 def __init__(self, root):
2609 self.root = root
2610 self.jobs = {}
2611
2612 def _getTD(self, name):
2613 td = self.jobs.get(name)
2614 if not td:
2615 td = JobTimeData(os.path.join(self.root, name))
2616 self.jobs[name] = td
2617 td.load()
2618 return td
2619
2620 def getEstimatedTime(self, name):
2621 return self._getTD(name).getEstimatedTime()
2622
2623 def update(self, name, elapsed, result):
2624 td = self._getTD(name)
2625 td.add(elapsed, result)
2626 td.save()