blob: ef67828421654e5627f93a566d4c58a84db27851 [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. Blair6563e4b2017-04-28 08:14:48 -0700705 def __init__(self, target_name, connection_name, project_name):
James E. Blair5ac93842017-01-20 06:47:34 -0800706 super(ZuulRole, self).__init__(target_name)
707 self.connection_name = connection_name
708 self.project_name = project_name
James E. Blair5ac93842017-01-20 06:47:34 -0800709
710 def __repr__(self):
711 return '<ZuulRole %s %s>' % (self.project_name, self.target_name)
712
Clint Byrumaf7438f2017-05-10 17:26:57 -0400713 __hash__ = object.__hash__
714
James E. Blair5ac93842017-01-20 06:47:34 -0800715 def __eq__(self, other):
716 if not isinstance(other, ZuulRole):
717 return False
718 return (super(ZuulRole, self).__eq__(other) and
James E. Blair1b27f6a2017-07-14 14:09:07 -0700719 self.connection_name == other.connection_name and
James E. Blair6563e4b2017-04-28 08:14:48 -0700720 self.project_name == other.project_name)
James E. Blair5ac93842017-01-20 06:47:34 -0800721
722 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400723 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800724 d = super(ZuulRole, self).toDict()
725 d['type'] = 'zuul'
726 d['connection'] = self.connection_name
727 d['project'] = self.project_name
James E. Blair5ac93842017-01-20 06:47:34 -0800728 return d
729
730
James E. Blair8525e2b2017-03-15 14:05:47 -0700731class AuthContext(object):
732 """The authentication information for a job.
733
734 Authentication information (both the actual data and metadata such
735 as whether it should be inherited) for a job is grouped together
736 in this object.
737 """
738
739 def __init__(self, inherit=False):
740 self.inherit = inherit
741 self.secrets = []
742
743 def __ne__(self, other):
744 return not self.__eq__(other)
745
746 def __eq__(self, other):
747 if not isinstance(other, AuthContext):
748 return False
749 return (self.inherit == other.inherit and
750 self.secrets == other.secrets)
751
752
James E. Blairee743612012-05-29 14:49:32 -0700753class Job(object):
James E. Blair66b274e2017-01-31 14:47:52 -0800754
James E. Blaira7f51ca2017-02-07 16:01:26 -0800755 """A Job represents the defintion of actions to perform.
756
James E. Blaird4ade8c2017-02-19 15:25:46 -0800757 A Job is an abstract configuration concept. It describes what,
758 where, and under what circumstances something should be run
759 (contrast this with Build which is a concrete single execution of
760 a Job).
761
James E. Blaira7f51ca2017-02-07 16:01:26 -0800762 NB: Do not modify attributes of this class, set them directly
763 (e.g., "job.run = ..." rather than "job.run.append(...)").
764 """
Monty Taylora42a55b2016-07-29 07:53:33 -0700765
James E. Blairee743612012-05-29 14:49:32 -0700766 def __init__(self, name):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800767 # These attributes may override even the final form of a job
768 # in the context of a project-pipeline. They can not affect
769 # the execution of the job, but only whether the job is run
770 # and how it is reported.
771 self.context_attributes = dict(
772 voting=True,
773 hold_following_changes=False,
James E. Blair1774dd52017-02-03 10:52:32 -0800774 failure_message=None,
775 success_message=None,
776 failure_url=None,
777 success_url=None,
778 # Matchers. These are separate so they can be individually
779 # overidden.
780 branch_matcher=None,
781 file_matcher=None,
782 irrelevant_file_matcher=None, # skip-if
James E. Blaira7f51ca2017-02-07 16:01:26 -0800783 tags=frozenset(),
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200784 dependencies=frozenset(),
James E. Blair1774dd52017-02-03 10:52:32 -0800785 )
786
James E. Blaira7f51ca2017-02-07 16:01:26 -0800787 # These attributes affect how the job is actually run and more
788 # care must be taken when overriding them. If a job is
789 # declared "final", these may not be overriden in a
790 # project-pipeline.
791 self.execution_attributes = dict(
792 timeout=None,
James E. Blair490cf042017-02-24 23:07:21 -0500793 variables={},
James E. Blaira7f51ca2017-02-07 16:01:26 -0800794 nodeset=NodeSet(),
James E. Blair8525e2b2017-03-15 14:05:47 -0700795 auth=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800796 workspace=None,
797 pre_run=(),
798 post_run=(),
799 run=(),
800 implied_run=(),
Tobias Henkel9a0e1942017-03-20 16:16:02 +0100801 semaphore=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800802 attempts=3,
803 final=False,
James E. Blair5fc81922017-07-12 13:19:37 -0700804 roles=(),
James E. Blair912322f2017-05-23 13:11:25 -0700805 required_projects={},
James E. Blairb3f5db12017-03-17 12:57:39 -0700806 allowed_projects=None,
James E. Blair7391ecb2017-05-23 13:32:40 -0700807 override_branch=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800808 )
809
810 # These are generally internal attributes which are not
811 # accessible via configuration.
812 self.other_attributes = dict(
813 name=None,
814 source_context=None,
815 inheritance_path=(),
816 )
817
818 self.inheritable_attributes = {}
819 self.inheritable_attributes.update(self.context_attributes)
820 self.inheritable_attributes.update(self.execution_attributes)
821 self.attributes = {}
822 self.attributes.update(self.inheritable_attributes)
823 self.attributes.update(self.other_attributes)
824
James E. Blairee743612012-05-29 14:49:32 -0700825 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800826
James E. Blair66b274e2017-01-31 14:47:52 -0800827 def __ne__(self, other):
828 return not self.__eq__(other)
829
Paul Belangere22baea2016-11-03 16:59:27 -0400830 def __eq__(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800831 # Compare the name and all inheritable attributes to determine
832 # whether two jobs with the same name are identically
833 # configured. Useful upon reconfiguration.
834 if not isinstance(other, Job):
835 return False
836 if self.name != other.name:
837 return False
838 for k, v in self.attributes.items():
839 if getattr(self, k) != getattr(other, k):
840 return False
841 return True
James E. Blairee743612012-05-29 14:49:32 -0700842
Clint Byrumaf7438f2017-05-10 17:26:57 -0400843 __hash__ = object.__hash__
844
James E. Blairee743612012-05-29 14:49:32 -0700845 def __str__(self):
846 return self.name
847
848 def __repr__(self):
James E. Blair72ae9da2017-02-03 14:21:30 -0800849 return '<Job %s branches: %s source: %s>' % (self.name,
850 self.branch_matcher,
851 self.source_context)
James E. Blair83005782015-12-11 14:46:03 -0800852
James E. Blaira7f51ca2017-02-07 16:01:26 -0800853 def __getattr__(self, name):
854 v = self.__dict__.get(name)
855 if v is None:
856 return copy.deepcopy(self.attributes[name])
857 return v
858
859 def _get(self, name):
860 return self.__dict__.get(name)
861
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100862 def getSafeAttributes(self):
863 return Attributes(name=self.name)
864
James E. Blaira7f51ca2017-02-07 16:01:26 -0800865 def setRun(self):
866 if not self.run:
867 self.run = self.implied_run
868
James E. Blair5fc81922017-07-12 13:19:37 -0700869 def addRoles(self, roles):
870 newroles = list(self.roles)
James E. Blair4eec8282017-07-12 17:33:26 -0700871 for role in reversed(roles):
James E. Blair5fc81922017-07-12 13:19:37 -0700872 if role not in newroles:
James E. Blair4eec8282017-07-12 17:33:26 -0700873 newroles.insert(0, role)
James E. Blair5fc81922017-07-12 13:19:37 -0700874 self.roles = tuple(newroles)
875
James E. Blair490cf042017-02-24 23:07:21 -0500876 def updateVariables(self, other_vars):
877 v = self.variables
878 Job._deepUpdate(v, other_vars)
879 self.variables = v
880
James E. Blair912322f2017-05-23 13:11:25 -0700881 def updateProjects(self, other_projects):
882 required_projects = self.required_projects
883 Job._deepUpdate(required_projects, other_projects)
884 self.required_projects = required_projects
James E. Blair27f3dfc2017-05-23 13:07:28 -0700885
James E. Blair490cf042017-02-24 23:07:21 -0500886 @staticmethod
887 def _deepUpdate(a, b):
888 # Merge nested dictionaries if possible, otherwise, overwrite
889 # the value in 'a' with the value in 'b'.
890 for k, bv in b.items():
891 av = a.get(k)
892 if isinstance(av, dict) and isinstance(bv, dict):
893 Job._deepUpdate(av, bv)
894 else:
895 a[k] = bv
896
James E. Blaira7f51ca2017-02-07 16:01:26 -0800897 def inheritFrom(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800898 """Copy the inheritable attributes which have been set on the other
899 job to this job."""
James E. Blaira7f51ca2017-02-07 16:01:26 -0800900 if not isinstance(other, Job):
901 raise Exception("Job unable to inherit from %s" % (other,))
902
903 do_not_inherit = set()
James E. Blair8525e2b2017-03-15 14:05:47 -0700904 if other.auth and not other.auth.inherit:
James E. Blaira7f51ca2017-02-07 16:01:26 -0800905 do_not_inherit.add('auth')
906
907 # copy all attributes
908 for k in self.inheritable_attributes:
909 if (other._get(k) is not None and k not in do_not_inherit):
910 setattr(self, k, copy.deepcopy(getattr(other, k)))
911
912 msg = 'inherit from %s' % (repr(other),)
913 self.inheritance_path = other.inheritance_path + (msg,)
914
915 def copy(self):
916 job = Job(self.name)
917 for k in self.attributes:
918 if self._get(k) is not None:
919 setattr(job, k, copy.deepcopy(self._get(k)))
920 return job
921
922 def applyVariant(self, other):
923 """Copy the attributes which have been set on the other job to this
924 job."""
James E. Blair83005782015-12-11 14:46:03 -0800925
926 if not isinstance(other, Job):
927 raise Exception("Job unable to inherit from %s" % (other,))
James E. Blaira7f51ca2017-02-07 16:01:26 -0800928
929 for k in self.execution_attributes:
930 if (other._get(k) is not None and
931 k not in set(['final'])):
932 if self.final:
933 raise Exception("Unable to modify final job %s attribute "
934 "%s=%s with variant %s" % (
935 repr(self), k, other._get(k),
936 repr(other)))
James E. Blair27f3dfc2017-05-23 13:07:28 -0700937 if k not in set(['pre_run', 'post_run', 'roles', 'variables',
James E. Blair912322f2017-05-23 13:11:25 -0700938 'required_projects']):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800939 setattr(self, k, copy.deepcopy(other._get(k)))
940
941 # Don't set final above so that we don't trip an error halfway
942 # through assignment.
943 if other.final != self.attributes['final']:
944 self.final = other.final
945
946 if other._get('pre_run') is not None:
947 self.pre_run = self.pre_run + other.pre_run
948 if other._get('post_run') is not None:
949 self.post_run = other.post_run + self.post_run
James E. Blair5ac93842017-01-20 06:47:34 -0800950 if other._get('roles') is not None:
James E. Blair5fc81922017-07-12 13:19:37 -0700951 self.addRoles(other.roles)
James E. Blair490cf042017-02-24 23:07:21 -0500952 if other._get('variables') is not None:
953 self.updateVariables(other.variables)
James E. Blair912322f2017-05-23 13:11:25 -0700954 if other._get('required_projects') is not None:
955 self.updateProjects(other.required_projects)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800956
957 for k in self.context_attributes:
958 if (other._get(k) is not None and
959 k not in set(['tags'])):
960 setattr(self, k, copy.deepcopy(other._get(k)))
961
962 if other._get('tags') is not None:
963 self.tags = self.tags.union(other.tags)
964
965 msg = 'apply variant %s' % (repr(other),)
966 self.inheritance_path = self.inheritance_path + (msg,)
James E. Blairee743612012-05-29 14:49:32 -0700967
James E. Blaire421a232012-07-25 16:59:21 -0700968 def changeMatches(self, change):
James E. Blair83005782015-12-11 14:46:03 -0800969 if self.branch_matcher and not self.branch_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800970 return False
971
James E. Blair83005782015-12-11 14:46:03 -0800972 if self.file_matcher and not self.file_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800973 return False
974
James E. Blair83005782015-12-11 14:46:03 -0800975 # NB: This is a negative match.
976 if (self.irrelevant_file_matcher and
977 self.irrelevant_file_matcher.matches(change)):
Maru Newby3fe5f852015-01-13 04:22:14 +0000978 return False
979
James E. Blair70c71582013-03-06 08:50:50 -0800980 return True
James E. Blaire5a847f2012-07-10 15:29:14 -0700981
James E. Blair1e8dd892012-05-30 09:15:05 -0700982
James E. Blair912322f2017-05-23 13:11:25 -0700983class JobProject(object):
James E. Blair27f3dfc2017-05-23 13:07:28 -0700984 """ A reference to a project from a job. """
985
986 def __init__(self, project_name, override_branch=None):
987 self.project_name = project_name
988 self.override_branch = override_branch
989
990
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200991class JobList(object):
992 """ A list of jobs in a project's pipeline. """
Monty Taylora42a55b2016-07-29 07:53:33 -0700993
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200994 def __init__(self):
995 self.jobs = OrderedDict() # job.name -> [job, ...]
James E. Blair12748b52017-02-07 17:17:53 -0800996
James E. Blairee743612012-05-29 14:49:32 -0700997 def addJob(self, job):
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200998 if job.name in self.jobs:
999 self.jobs[job.name].append(job)
1000 else:
1001 self.jobs[job.name] = [job]
James E. Blairee743612012-05-29 14:49:32 -07001002
James E. Blaira7f51ca2017-02-07 16:01:26 -08001003 def inheritFrom(self, other):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001004 for jobname, jobs in other.jobs.items():
1005 if jobname in self.jobs:
Jesse Keatingd1f434a2017-05-16 20:28:35 -07001006 self.jobs[jobname].extend(jobs)
James E. Blaira7f51ca2017-02-07 16:01:26 -08001007 else:
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001008 self.jobs[jobname] = jobs
1009
1010
1011class JobGraph(object):
1012 """ A JobGraph represents the dependency graph between Job."""
1013
1014 def __init__(self):
1015 self.jobs = OrderedDict() # job_name -> Job
1016 self._dependencies = {} # dependent_job_name -> set(parent_job_names)
1017
1018 def __repr__(self):
1019 return '<JobGraph %s>' % (self.jobs)
1020
1021 def addJob(self, job):
1022 # A graph must be created after the job list is frozen,
1023 # therefore we should only get one job with the same name.
1024 if job.name in self.jobs:
1025 raise Exception("Job %s already added" % (job.name,))
1026 self.jobs[job.name] = job
1027 # Append the dependency information
1028 self._dependencies.setdefault(job.name, set())
1029 try:
1030 for dependency in job.dependencies:
1031 # Make sure a circular dependency is never created
1032 ancestor_jobs = self._getParentJobNamesRecursively(
1033 dependency, soft=True)
1034 ancestor_jobs.add(dependency)
1035 if any((job.name == anc_job) for anc_job in ancestor_jobs):
1036 raise Exception("Dependency cycle detected in job %s" %
1037 (job.name,))
1038 self._dependencies[job.name].add(dependency)
1039 except Exception:
1040 del self.jobs[job.name]
1041 del self._dependencies[job.name]
1042 raise
1043
1044 def getJobs(self):
Clint Byruma4471d12017-05-10 20:57:40 -04001045 return list(self.jobs.values()) # Report in the order of layout cfg
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001046
1047 def _getDirectDependentJobs(self, parent_job):
1048 ret = set()
1049 for dependent_name, parent_names in self._dependencies.items():
1050 if parent_job in parent_names:
1051 ret.add(dependent_name)
1052 return ret
1053
1054 def getDependentJobsRecursively(self, parent_job):
1055 all_dependent_jobs = set()
1056 jobs_to_iterate = set([parent_job])
1057 while len(jobs_to_iterate) > 0:
1058 current_job = jobs_to_iterate.pop()
1059 current_dependent_jobs = self._getDirectDependentJobs(current_job)
1060 new_dependent_jobs = current_dependent_jobs - all_dependent_jobs
1061 jobs_to_iterate |= new_dependent_jobs
1062 all_dependent_jobs |= new_dependent_jobs
1063 return [self.jobs[name] for name in all_dependent_jobs]
1064
1065 def getParentJobsRecursively(self, dependent_job):
1066 return [self.jobs[name] for name in
1067 self._getParentJobNamesRecursively(dependent_job)]
1068
1069 def _getParentJobNamesRecursively(self, dependent_job, soft=False):
1070 all_parent_jobs = set()
1071 jobs_to_iterate = set([dependent_job])
1072 while len(jobs_to_iterate) > 0:
1073 current_job = jobs_to_iterate.pop()
1074 current_parent_jobs = self._dependencies.get(current_job)
1075 if current_parent_jobs is None:
1076 if soft:
1077 current_parent_jobs = set()
1078 else:
1079 raise Exception("Dependent job %s not found: " %
1080 (dependent_job,))
1081 new_parent_jobs = current_parent_jobs - all_parent_jobs
1082 jobs_to_iterate |= new_parent_jobs
1083 all_parent_jobs |= new_parent_jobs
1084 return all_parent_jobs
James E. Blairb97ed802015-12-21 15:55:35 -08001085
James E. Blair1e8dd892012-05-30 09:15:05 -07001086
James E. Blair4aea70c2012-07-26 14:23:24 -07001087class Build(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001088 """A Build is an instance of a single execution of a Job.
1089
1090 While a Job describes what to run, a Build describes an actual
1091 execution of that Job. Each build is associated with exactly one
1092 Job (related builds are grouped together in a BuildSet).
1093 """
Monty Taylora42a55b2016-07-29 07:53:33 -07001094
James E. Blair4aea70c2012-07-26 14:23:24 -07001095 def __init__(self, job, uuid):
1096 self.job = job
1097 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -07001098 self.url = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001099 self.result = None
James E. Blair196f61a2017-06-30 15:42:29 -07001100 self.result_data = {}
James E. Blair6f699732017-07-18 14:19:11 -07001101 self.error_detail = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001102 self.build_set = None
Paul Belanger174a8272017-03-14 13:20:10 -04001103 self.execute_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -08001104 self.start_time = None
1105 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -07001106 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -07001107 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -07001108 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -07001109 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -07001110 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +08001111 self.worker = Worker()
Timothy Chavezb2332082015-08-07 20:08:04 -05001112 self.node_labels = []
1113 self.node_name = None
James E. Blairee743612012-05-29 14:49:32 -07001114
1115 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +08001116 return ('<Build %s of %s on %s>' %
1117 (self.uuid, self.job.name, self.worker))
1118
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001119 def getSafeAttributes(self):
James E. Blair196f61a2017-06-30 15:42:29 -07001120 return Attributes(uuid=self.uuid,
1121 result=self.result,
James E. Blair6f699732017-07-18 14:19:11 -07001122 error_detail=self.error_detail,
James E. Blair196f61a2017-06-30 15:42:29 -07001123 result_data=self.result_data)
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001124
Joshua Heskethba8776a2014-01-12 14:35:40 +08001125
1126class Worker(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001127 """Information about the specific worker executing a Build."""
Joshua Heskethba8776a2014-01-12 14:35:40 +08001128 def __init__(self):
1129 self.name = "Unknown"
1130 self.hostname = None
Monty Taylor0dbe1592017-06-11 10:57:27 -05001131 self.log_port = None
Joshua Heskethba8776a2014-01-12 14:35:40 +08001132
1133 def updateFromData(self, data):
1134 """Update worker information if contained in the WORK_DATA response."""
1135 self.name = data.get('worker_name', self.name)
1136 self.hostname = data.get('worker_hostname', self.hostname)
Monty Taylor0dbe1592017-06-11 10:57:27 -05001137 self.log_port = data.get('worker_log_port', self.log_port)
Joshua Heskethba8776a2014-01-12 14:35:40 +08001138
1139 def __repr__(self):
1140 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -07001141
James E. Blair1e8dd892012-05-30 09:15:05 -07001142
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001143class RepoFiles(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001144 """RepoFiles holds config-file content for per-project job config.
1145
1146 When Zuul asks a merger to prepare a future multiple-repo state
1147 and collect Zuul configuration files so that we can dynamically
1148 load our configuration, this class provides cached access to that
1149 data for use by the Change which updated the config files and any
1150 changes that follow it in a ChangeQueue.
1151
1152 It is attached to a BuildSet since the content of Zuul
1153 configuration files can change with each new BuildSet.
1154 """
1155
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001156 def __init__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001157 self.connections = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001158
1159 def __repr__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001160 return '<RepoFiles %s>' % self.connections
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001161
1162 def setFiles(self, items):
James E. Blair2a535672017-04-27 12:03:15 -07001163 self.hostnames = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001164 for item in items:
James E. Blair2a535672017-04-27 12:03:15 -07001165 connection = self.connections.setdefault(
1166 item['connection'], {})
1167 project = connection.setdefault(item['project'], {})
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001168 branch = project.setdefault(item['branch'], {})
1169 branch.update(item['files'])
1170
James E. Blair2a535672017-04-27 12:03:15 -07001171 def getFile(self, connection_name, project_name, branch, fn):
1172 host = self.connections.get(connection_name, {})
1173 return host.get(project_name, {}).get(branch, {}).get(fn)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001174
1175
James E. Blair7e530ad2012-07-03 16:12:28 -07001176class BuildSet(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001177 """A collection of Builds for one specific potential future repository
1178 state.
Monty Taylora42a55b2016-07-29 07:53:33 -07001179
Paul Belanger174a8272017-03-14 13:20:10 -04001180 When Zuul executes Builds for a change, it creates a Build to
James E. Blaird4ade8c2017-02-19 15:25:46 -08001181 represent each execution of each job and a BuildSet to keep track
Paul Belanger174a8272017-03-14 13:20:10 -04001182 of all the Builds running for that Change. When Zuul re-executes
James E. Blaird4ade8c2017-02-19 15:25:46 -08001183 Builds for a Change with a different configuration, all of the
1184 running Builds in the BuildSet for that change are aborted, and a
1185 new BuildSet is created to hold the Builds for the Jobs being
1186 run with the new configuration.
1187
1188 A BuildSet also holds the UUID used to produce the Zuul Ref that
1189 builders check out.
1190
Monty Taylora42a55b2016-07-29 07:53:33 -07001191 """
James E. Blair4076e2b2014-01-28 12:42:20 -08001192 # Merge states:
1193 NEW = 1
1194 PENDING = 2
1195 COMPLETE = 3
1196
Antoine Musso9b229282014-08-18 23:45:43 +02001197 states_map = {
1198 1: 'NEW',
1199 2: 'PENDING',
1200 3: 'COMPLETE',
1201 }
1202
James E. Blairfee8d652013-06-07 08:57:52 -07001203 def __init__(self, item):
1204 self.item = item
James E. Blair7e530ad2012-07-03 16:12:28 -07001205 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -07001206 self.result = None
1207 self.next_build_set = None
1208 self.previous_build_set = None
Jamie Lennox3f16de52017-05-09 14:24:11 +10001209 self.uuid = None
James E. Blair81515ad2012-10-01 18:29:08 -07001210 self.commit = None
James E. Blair4076e2b2014-01-28 12:42:20 -08001211 self.zuul_url = None
James E. Blair1960d682017-04-28 15:44:14 -07001212 self.dependent_items = None
1213 self.merger_items = None
James E. Blair973721f2012-08-15 10:19:43 -07001214 self.unable_to_merge = False
James E. Blaire53250c2017-03-01 14:34:36 -08001215 self.config_error = None # None or an error message string.
James E. Blair972e3c72013-08-29 12:04:55 -07001216 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -08001217 self.merge_state = self.NEW
James E. Blair0eaad552016-09-02 12:09:54 -07001218 self.nodesets = {} # job -> nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001219 self.node_requests = {} # job -> reqs
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001220 self.files = RepoFiles()
James E. Blair1960d682017-04-28 15:44:14 -07001221 self.repo_state = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001222 self.layout = None
Paul Belanger71d98172016-11-08 10:56:31 -05001223 self.tries = {}
James E. Blair7e530ad2012-07-03 16:12:28 -07001224
Jamie Lennox3f16de52017-05-09 14:24:11 +10001225 @property
1226 def ref(self):
1227 # NOTE(jamielennox): The concept of buildset ref is to be removed and a
1228 # buildset UUID identifier available instead. Currently the ref is
1229 # checked to see if the BuildSet has been configured.
1230 return 'Z' + self.uuid if self.uuid else None
1231
Antoine Musso9b229282014-08-18 23:45:43 +02001232 def __repr__(self):
1233 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
1234 self.item,
1235 len(self.builds),
1236 self.getStateName(self.merge_state))
1237
James E. Blair4886cc12012-07-18 15:39:41 -07001238 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -07001239 # The change isn't enqueued until after it's created
1240 # so we don't know what the other changes ahead will be
1241 # until jobs start.
James E. Blair1960d682017-04-28 15:44:14 -07001242 if self.dependent_items is None:
1243 items = []
James E. Blairfee8d652013-06-07 08:57:52 -07001244 next_item = self.item.item_ahead
1245 while next_item:
James E. Blair1960d682017-04-28 15:44:14 -07001246 items.append(next_item)
James E. Blairfee8d652013-06-07 08:57:52 -07001247 next_item = next_item.item_ahead
James E. Blair1960d682017-04-28 15:44:14 -07001248 self.dependent_items = items
Jamie Lennox3f16de52017-05-09 14:24:11 +10001249 if not self.uuid:
1250 self.uuid = uuid4().hex
James E. Blair1960d682017-04-28 15:44:14 -07001251 if self.merger_items is None:
1252 items = [self.item] + self.dependent_items
1253 items.reverse()
1254 self.merger_items = [i.makeMergerItem() for i in items]
James E. Blair4886cc12012-07-18 15:39:41 -07001255
Antoine Musso9b229282014-08-18 23:45:43 +02001256 def getStateName(self, state_num):
1257 return self.states_map.get(
1258 state_num, 'UNKNOWN (%s)' % state_num)
1259
James E. Blair4886cc12012-07-18 15:39:41 -07001260 def addBuild(self, build):
1261 self.builds[build.job.name] = build
Paul Belanger71d98172016-11-08 10:56:31 -05001262 if build.job.name not in self.tries:
James E. Blair5aed1112016-12-15 09:18:05 -08001263 self.tries[build.job.name] = 1
James E. Blair4886cc12012-07-18 15:39:41 -07001264 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -07001265
James E. Blair4a28a882013-08-23 15:17:33 -07001266 def removeBuild(self, build):
Paul Belanger71d98172016-11-08 10:56:31 -05001267 self.tries[build.job.name] += 1
James E. Blair4a28a882013-08-23 15:17:33 -07001268 del self.builds[build.job.name]
1269
James E. Blair7e530ad2012-07-03 16:12:28 -07001270 def getBuild(self, job_name):
1271 return self.builds.get(job_name)
1272
James E. Blair11700c32012-07-05 17:50:05 -07001273 def getBuilds(self):
Clint Byrum1d0c7d12017-05-10 19:40:53 -07001274 keys = list(self.builds.keys())
James E. Blair11700c32012-07-05 17:50:05 -07001275 keys.sort()
1276 return [self.builds.get(x) for x in keys]
1277
James E. Blair0eaad552016-09-02 12:09:54 -07001278 def getJobNodeSet(self, job_name):
1279 # Return None if not provisioned; empty NodeSet if no nodes
1280 # required
1281 return self.nodesets.get(job_name)
James E. Blair8d692392016-04-08 17:47:58 -07001282
James E. Blaire18d4602017-01-05 11:17:28 -08001283 def removeJobNodeSet(self, job_name):
1284 if job_name not in self.nodesets:
1285 raise Exception("No job set for %s" % (job_name))
1286 del self.nodesets[job_name]
1287
James E. Blair8d692392016-04-08 17:47:58 -07001288 def setJobNodeRequest(self, job_name, req):
1289 if job_name in self.node_requests:
1290 raise Exception("Prior node request for %s" % (job_name))
1291 self.node_requests[job_name] = req
1292
1293 def getJobNodeRequest(self, job_name):
1294 return self.node_requests.get(job_name)
1295
James E. Blair0eaad552016-09-02 12:09:54 -07001296 def jobNodeRequestComplete(self, job_name, req, nodeset):
1297 if job_name in self.nodesets:
James E. Blair8d692392016-04-08 17:47:58 -07001298 raise Exception("Prior node request for %s" % (job_name))
James E. Blair0eaad552016-09-02 12:09:54 -07001299 self.nodesets[job_name] = nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001300 del self.node_requests[job_name]
1301
Paul Belanger71d98172016-11-08 10:56:31 -05001302 def getTries(self, job_name):
Clint Byrum804073b2017-05-10 17:47:45 -04001303 return self.tries.get(job_name, 0)
Paul Belanger71d98172016-11-08 10:56:31 -05001304
James E. Blair0ffa0102017-03-30 13:11:33 -07001305 def getMergeMode(self):
James E. Blair1960d682017-04-28 15:44:14 -07001306 # We may be called before this build set has a shadow layout
1307 # (ie, we are called to perform the merge to create that
1308 # layout). It's possible that the change we are merging will
1309 # update the merge-mode for the project, but there's not much
1310 # we can do about that here. Instead, do the best we can by
1311 # using the nearest shadow layout to determine the merge mode,
1312 # or if that fails, the current live layout, or if that fails,
1313 # use the default: merge-resolve.
1314 item = self.item
1315 layout = None
1316 while item:
1317 layout = item.current_build_set.layout
1318 if layout:
1319 break
1320 item = item.item_ahead
1321 if not layout:
1322 layout = self.item.pipeline.layout
1323 if layout:
James E. Blair0ffa0102017-03-30 13:11:33 -07001324 project = self.item.change.project
James E. Blair1960d682017-04-28 15:44:14 -07001325 project_config = layout.project_configs.get(
James E. Blair0ffa0102017-03-30 13:11:33 -07001326 project.canonical_name)
1327 if project_config:
1328 return project_config.merge_mode
1329 return MERGER_MERGE_RESOLVE
Adam Gandelman8bd57102016-12-02 12:58:42 -08001330
Jamie Lennox3f16de52017-05-09 14:24:11 +10001331 def getSafeAttributes(self):
1332 return Attributes(uuid=self.uuid)
1333
James E. Blair7e530ad2012-07-03 16:12:28 -07001334
James E. Blairfee8d652013-06-07 08:57:52 -07001335class QueueItem(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001336 """Represents the position of a Change in a ChangeQueue.
1337
1338 All Changes are enqueued into ChangeQueue in a QueueItem. The QueueItem
1339 holds the current `BuildSet` as well as all previous `BuildSets` that were
1340 produced for this `QueueItem`.
1341 """
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001342 log = logging.getLogger("zuul.QueueItem")
James E. Blair32663402012-06-01 10:04:18 -07001343
James E. Blairbfb8e042014-12-30 17:01:44 -08001344 def __init__(self, queue, change):
1345 self.pipeline = queue.pipeline
1346 self.queue = queue
Monty Taylor55d0f562017-05-17 14:30:21 -05001347 self.change = change # a ref
James E. Blair7e530ad2012-07-03 16:12:28 -07001348 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -07001349 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -07001350 self.current_build_set = BuildSet(self)
1351 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -07001352 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -07001353 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -08001354 self.enqueue_time = None
1355 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -07001356 self.reported = False
Tobias Henkel9842bd72017-05-16 13:40:03 +02001357 self.reported_start = False
1358 self.quiet = False
James E. Blairbfb8e042014-12-30 17:01:44 -08001359 self.active = False # Whether an item is within an active window
1360 self.live = True # Whether an item is intended to be processed at all
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001361 self.job_graph = None
James E. Blaire5a847f2012-07-10 15:29:14 -07001362
James E. Blair972e3c72013-08-29 12:04:55 -07001363 def __repr__(self):
1364 if self.pipeline:
1365 pipeline = self.pipeline.name
1366 else:
1367 pipeline = None
1368 return '<QueueItem 0x%x for %s in %s>' % (
1369 id(self), self.change, pipeline)
1370
James E. Blairee743612012-05-29 14:49:32 -07001371 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -07001372 old = self.current_build_set
1373 self.current_build_set.result = 'CANCELED'
1374 self.current_build_set = BuildSet(self)
1375 old.next_build_set = self.current_build_set
1376 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -07001377 self.build_sets.append(self.current_build_set)
James E. Blairee743612012-05-29 14:49:32 -07001378
1379 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -07001380 self.current_build_set.addBuild(build)
James E. Blair66eeebf2013-07-27 17:44:32 -07001381 build.pipeline = self.pipeline
James E. Blairee743612012-05-29 14:49:32 -07001382
James E. Blair4a28a882013-08-23 15:17:33 -07001383 def removeBuild(self, build):
1384 self.current_build_set.removeBuild(build)
1385
James E. Blairfee8d652013-06-07 08:57:52 -07001386 def setReportedResult(self, result):
1387 self.current_build_set.result = result
1388
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001389 def freezeJobGraph(self):
James E. Blair83005782015-12-11 14:46:03 -08001390 """Find or create actual matching jobs for this item's change and
1391 store the resulting job tree."""
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001392 layout = self.current_build_set.layout
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001393 job_graph = layout.createJobGraph(self)
1394 for job in job_graph.getJobs():
1395 # Ensure that each jobs's dependencies are fully
1396 # accessible. This will raise an exception if not.
1397 job_graph.getParentJobsRecursively(job.name)
1398 self.job_graph = job_graph
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001399
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001400 def hasJobGraph(self):
1401 """Returns True if the item has a job graph."""
1402 return self.job_graph is not None
James E. Blair83005782015-12-11 14:46:03 -08001403
1404 def getJobs(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001405 if not self.live or not self.job_graph:
James E. Blair83005782015-12-11 14:46:03 -08001406 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001407 return self.job_graph.getJobs()
1408
1409 def getJob(self, name):
1410 if not self.job_graph:
1411 return None
1412 return self.job_graph.jobs.get(name)
James E. Blair83005782015-12-11 14:46:03 -08001413
James E. Blairdbfd3282016-07-21 10:46:19 -07001414 def haveAllJobsStarted(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001415 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001416 return False
1417 for job in self.getJobs():
1418 build = self.current_build_set.getBuild(job.name)
1419 if not build or not build.start_time:
1420 return False
1421 return True
1422
1423 def areAllJobsComplete(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001424 if (self.current_build_set.config_error or
1425 self.current_build_set.unable_to_merge):
1426 return True
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001427 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001428 return False
1429 for job in self.getJobs():
1430 build = self.current_build_set.getBuild(job.name)
1431 if not build or not build.result:
1432 return False
1433 return True
1434
1435 def didAllJobsSucceed(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001436 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001437 return False
1438 for job in self.getJobs():
1439 if not job.voting:
1440 continue
1441 build = self.current_build_set.getBuild(job.name)
1442 if not build:
1443 return False
1444 if build.result != 'SUCCESS':
1445 return False
1446 return True
1447
1448 def didAnyJobFail(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001449 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001450 return False
1451 for job in self.getJobs():
1452 if not job.voting:
1453 continue
1454 build = self.current_build_set.getBuild(job.name)
1455 if build and build.result and (build.result != 'SUCCESS'):
1456 return True
1457 return False
1458
1459 def didMergerFail(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001460 return self.current_build_set.unable_to_merge
1461
1462 def getConfigError(self):
1463 return self.current_build_set.config_error
James E. Blairdbfd3282016-07-21 10:46:19 -07001464
James E. Blair0d3e83b2017-06-05 13:51:57 -07001465 def wasDequeuedNeedingChange(self):
1466 return self.dequeued_needing_change
1467
James E. Blairdbfd3282016-07-21 10:46:19 -07001468 def isHoldingFollowingChanges(self):
1469 if not self.live:
1470 return False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001471 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001472 return False
1473 for job in self.getJobs():
1474 if not job.hold_following_changes:
1475 continue
1476 build = self.current_build_set.getBuild(job.name)
1477 if not build:
1478 return True
1479 if build.result != 'SUCCESS':
1480 return True
1481
1482 if not self.item_ahead:
1483 return False
1484 return self.item_ahead.isHoldingFollowingChanges()
1485
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001486 def findJobsToRun(self, semaphore_handler):
James E. Blairdbfd3282016-07-21 10:46:19 -07001487 torun = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001488 if not self.live:
1489 return []
1490 if not self.job_graph:
1491 return []
James E. Blair791b5392016-08-03 11:25:56 -07001492 if self.item_ahead:
1493 # Only run jobs if any 'hold' jobs on the change ahead
1494 # have completed successfully.
1495 if self.item_ahead.isHoldingFollowingChanges():
1496 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001497
1498 successful_job_names = set()
1499 jobs_not_started = set()
1500 for job in self.job_graph.getJobs():
1501 build = self.current_build_set.getBuild(job.name)
1502 if build:
1503 if build.result == 'SUCCESS':
1504 successful_job_names.add(job.name)
1505 else:
1506 jobs_not_started.add(job)
1507
1508 # Attempt to request nodes for jobs in the order jobs appear
1509 # in configuration.
1510 for job in self.job_graph.getJobs():
1511 if job not in jobs_not_started:
1512 continue
1513 all_parent_jobs_successful = True
1514 for parent_job in self.job_graph.getParentJobsRecursively(
1515 job.name):
1516 if parent_job.name not in successful_job_names:
1517 all_parent_jobs_successful = False
1518 break
1519 if all_parent_jobs_successful:
1520 nodeset = self.current_build_set.getJobNodeSet(job.name)
1521 if nodeset is None:
1522 # The nodes for this job are not ready, skip
1523 # it for now.
James E. Blairdbfd3282016-07-21 10:46:19 -07001524 continue
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001525 if semaphore_handler.acquire(self, job):
1526 # If this job needs a semaphore, either acquire it or
1527 # make sure that we have it before running the job.
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001528 torun.append(job)
James E. Blairdbfd3282016-07-21 10:46:19 -07001529 return torun
1530
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001531 def findJobsToRequest(self):
James E. Blair6ab79e02017-01-06 10:10:17 -08001532 build_set = self.current_build_set
James E. Blairdbfd3282016-07-21 10:46:19 -07001533 toreq = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001534 if not self.live:
1535 return []
1536 if not self.job_graph:
1537 return []
James E. Blair6ab79e02017-01-06 10:10:17 -08001538 if self.item_ahead:
1539 if self.item_ahead.isHoldingFollowingChanges():
1540 return []
James E. Blairdbfd3282016-07-21 10:46:19 -07001541
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001542 successful_job_names = set()
1543 jobs_not_requested = set()
1544 for job in self.job_graph.getJobs():
1545 build = build_set.getBuild(job.name)
1546 if build and build.result == 'SUCCESS':
1547 successful_job_names.add(job.name)
1548 else:
1549 nodeset = build_set.getJobNodeSet(job.name)
1550 if nodeset is None:
1551 req = build_set.getJobNodeRequest(job.name)
1552 if req is None:
1553 jobs_not_requested.add(job)
1554
1555 # Attempt to request nodes for jobs in the order jobs appear
1556 # in configuration.
1557 for job in self.job_graph.getJobs():
1558 if job not in jobs_not_requested:
1559 continue
1560 all_parent_jobs_successful = True
1561 for parent_job in self.job_graph.getParentJobsRecursively(
1562 job.name):
1563 if parent_job.name not in successful_job_names:
1564 all_parent_jobs_successful = False
1565 break
1566 if all_parent_jobs_successful:
1567 toreq.append(job)
1568 return toreq
James E. Blairdbfd3282016-07-21 10:46:19 -07001569
1570 def setResult(self, build):
1571 if build.retry:
1572 self.removeBuild(build)
1573 elif build.result != 'SUCCESS':
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001574 for job in self.job_graph.getDependentJobsRecursively(
1575 build.job.name):
James E. Blairdbfd3282016-07-21 10:46:19 -07001576 fakebuild = Build(job, None)
1577 fakebuild.result = 'SKIPPED'
1578 self.addBuild(fakebuild)
1579
James E. Blair6ab79e02017-01-06 10:10:17 -08001580 def setNodeRequestFailure(self, job):
1581 fakebuild = Build(job, None)
1582 self.addBuild(fakebuild)
1583 fakebuild.result = 'NODE_FAILURE'
1584 self.setResult(fakebuild)
1585
James E. Blairdbfd3282016-07-21 10:46:19 -07001586 def setDequeuedNeedingChange(self):
1587 self.dequeued_needing_change = True
1588 self._setAllJobsSkipped()
1589
1590 def setUnableToMerge(self):
1591 self.current_build_set.unable_to_merge = True
1592 self._setAllJobsSkipped()
1593
James E. Blaire53250c2017-03-01 14:34:36 -08001594 def setConfigError(self, error):
1595 self.current_build_set.config_error = error
1596 self._setAllJobsSkipped()
1597
James E. Blairdbfd3282016-07-21 10:46:19 -07001598 def _setAllJobsSkipped(self):
1599 for job in self.getJobs():
1600 fakebuild = Build(job, None)
1601 fakebuild.result = 'SKIPPED'
1602 self.addBuild(fakebuild)
1603
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001604 def formatUrlPattern(self, url_pattern, job=None, build=None):
1605 url = None
1606 # Produce safe versions of objects which may be useful in
1607 # result formatting, but don't allow users to crawl through
1608 # the entire data structure where they might be able to access
1609 # secrets, etc.
1610 safe_change = self.change.getSafeAttributes()
1611 safe_pipeline = self.pipeline.getSafeAttributes()
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001612 safe_tenant = self.pipeline.layout.tenant.getSafeAttributes()
Jamie Lennox3f16de52017-05-09 14:24:11 +10001613 safe_buildset = self.current_build_set.getSafeAttributes()
K Jonathan Harkera7f390c2017-06-02 12:31:22 -07001614 safe_job = job.getSafeAttributes() if job else {}
1615 safe_build = build.getSafeAttributes() if build else {}
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001616 try:
1617 url = url_pattern.format(change=safe_change,
1618 pipeline=safe_pipeline,
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001619 tenant=safe_tenant,
Jamie Lennox3f16de52017-05-09 14:24:11 +10001620 buildset=safe_buildset,
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001621 job=safe_job,
1622 build=safe_build)
1623 except KeyError as e:
1624 self.log.error("Error while formatting url for job %s: unknown "
1625 "key %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001626 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001627 except AttributeError as e:
1628 self.log.error("Error while formatting url for job %s: unknown "
1629 "attribute %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001630 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001631 except Exception:
1632 self.log.exception("Error while formatting url for job %s with "
1633 "pattern %s:" % (job, url_pattern))
1634
1635 return url
1636
James E. Blair800e7ff2017-03-17 16:06:52 -07001637 def formatJobResult(self, job):
James E. Blairb7273ef2016-04-19 08:58:51 -07001638 build = self.current_build_set.getBuild(job.name)
1639 result = build.result
James E. Blair800e7ff2017-03-17 16:06:52 -07001640 pattern = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001641 if result == 'SUCCESS':
1642 if job.success_message:
1643 result = job.success_message
James E. Blaira5dba232016-08-08 15:53:24 -07001644 if job.success_url:
1645 pattern = job.success_url
Tobias Henkel077f2f32017-05-30 20:16:46 +02001646 else:
James E. Blairb7273ef2016-04-19 08:58:51 -07001647 if job.failure_message:
1648 result = job.failure_message
James E. Blaira5dba232016-08-08 15:53:24 -07001649 if job.failure_url:
1650 pattern = job.failure_url
James E. Blair88e79c02017-07-07 13:36:54 -07001651 url = None # The final URL
1652 default_url = build.result_data.get('zuul', {}).get('log_url')
James E. Blairb7273ef2016-04-19 08:58:51 -07001653 if pattern:
James E. Blair88e79c02017-07-07 13:36:54 -07001654 job_url = self.formatUrlPattern(pattern, job, build)
1655 else:
1656 job_url = None
1657 try:
1658 if job_url:
1659 u = urllib.parse.urlparse(job_url)
1660 if u.scheme:
1661 # The job success or failure url is absolute, so it's
1662 # our final url.
1663 url = job_url
1664 else:
1665 # We have a relative job url. Combine it with our
1666 # default url.
1667 if default_url:
1668 url = urllib.parse.urljoin(default_url, job_url)
1669 except Exception:
1670 self.log.exception("Error while parsing url for job %s:"
1671 % (job,))
James E. Blairb7273ef2016-04-19 08:58:51 -07001672 if not url:
James E. Blair88e79c02017-07-07 13:36:54 -07001673 url = default_url or build.url or job.name
James E. Blairb7273ef2016-04-19 08:58:51 -07001674 return (result, url)
1675
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001676 def formatJSON(self, websocket_url=None):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001677 ret = {}
1678 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -08001679 ret['live'] = self.live
Monty Taylor55d0f562017-05-17 14:30:21 -05001680 if hasattr(self.change, 'url') and self.change.url is not None:
1681 ret['url'] = self.change.url
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001682 else:
1683 ret['url'] = None
Monty Taylor55d0f562017-05-17 14:30:21 -05001684 ret['id'] = self.change._id()
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001685 if self.item_ahead:
1686 ret['item_ahead'] = self.item_ahead.change._id()
1687 else:
1688 ret['item_ahead'] = None
1689 ret['items_behind'] = [i.change._id() for i in self.items_behind]
1690 ret['failing_reasons'] = self.current_build_set.failing_reasons
1691 ret['zuul_ref'] = self.current_build_set.ref
Monty Taylor55d0f562017-05-17 14:30:21 -05001692 if self.change.project:
1693 ret['project'] = self.change.project.name
Ramy Asselin07cc33c2015-06-12 14:06:34 -07001694 else:
1695 # For cross-project dependencies with the depends-on
1696 # project not known to zuul, the project is None
1697 # Set it to a static value
1698 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001699 ret['enqueue_time'] = int(self.enqueue_time * 1000)
1700 ret['jobs'] = []
Monty Taylor55d0f562017-05-17 14:30:21 -05001701 if hasattr(self.change, 'owner'):
1702 ret['owner'] = self.change.owner
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001703 else:
1704 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001705 max_remaining = 0
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001706 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001707 now = time.time()
1708 build = self.current_build_set.getBuild(job.name)
1709 elapsed = None
1710 remaining = None
1711 result = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001712 build_url = None
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001713 finger_url = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001714 report_url = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001715 worker = None
1716 if build:
1717 result = build.result
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001718 finger_url = build.url
1719 # TODO(tobiash): add support for custom web root
1720 urlformat = 'static/stream.html?' \
1721 'uuid={build.uuid}&' \
1722 'logfile=console.log'
1723 if websocket_url:
1724 urlformat += '&websocket_url={websocket_url}'
1725 build_url = urlformat.format(
1726 build=build, websocket_url=websocket_url)
James E. Blair800e7ff2017-03-17 16:06:52 -07001727 (unused, report_url) = self.formatJobResult(job)
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001728 if build.start_time:
1729 if build.end_time:
1730 elapsed = int((build.end_time -
1731 build.start_time) * 1000)
1732 remaining = 0
1733 else:
1734 elapsed = int((now - build.start_time) * 1000)
1735 if build.estimated_time:
1736 remaining = max(
1737 int(build.estimated_time * 1000) - elapsed,
1738 0)
1739 worker = {
1740 'name': build.worker.name,
1741 'hostname': build.worker.hostname,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001742 }
1743 if remaining and remaining > max_remaining:
1744 max_remaining = remaining
1745
1746 ret['jobs'].append({
1747 'name': job.name,
Tobias Henkel65639f82017-07-10 10:25:42 +02001748 'dependencies': list(job.dependencies),
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001749 'elapsed_time': elapsed,
1750 'remaining_time': remaining,
James E. Blairb7273ef2016-04-19 08:58:51 -07001751 'url': build_url,
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001752 'finger_url': finger_url,
James E. Blairb7273ef2016-04-19 08:58:51 -07001753 'report_url': report_url,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001754 'result': result,
1755 'voting': job.voting,
1756 'uuid': build.uuid if build else None,
Paul Belanger174a8272017-03-14 13:20:10 -04001757 'execute_time': build.execute_time if build else None,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001758 'start_time': build.start_time if build else None,
1759 'end_time': build.end_time if build else None,
1760 'estimated_time': build.estimated_time if build else None,
1761 'pipeline': build.pipeline.name if build else None,
1762 'canceled': build.canceled if build else None,
1763 'retry': build.retry if build else None,
Timothy Chavezb2332082015-08-07 20:08:04 -05001764 'node_labels': build.node_labels if build else [],
1765 'node_name': build.node_name if build else None,
1766 'worker': worker,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001767 })
1768
James E. Blairdbfd3282016-07-21 10:46:19 -07001769 if self.haveAllJobsStarted():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001770 ret['remaining_time'] = max_remaining
1771 else:
1772 ret['remaining_time'] = None
1773 return ret
1774
1775 def formatStatus(self, indent=0, html=False):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001776 indent_str = ' ' * indent
1777 ret = ''
Monty Taylor55d0f562017-05-17 14:30:21 -05001778 if html and getattr(self.change, 'url', None) is not None:
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001779 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
1780 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001781 self.change.project.name,
1782 self.change.url,
1783 self.change._id())
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001784 else:
1785 ret += '%sProject %s change %s based on %s\n' % (
1786 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001787 self.change.project.name,
1788 self.change._id(),
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001789 self.item_ahead)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001790 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001791 build = self.current_build_set.getBuild(job.name)
1792 if build:
1793 result = build.result
1794 else:
1795 result = None
1796 job_name = job.name
1797 if not job.voting:
1798 voting = ' (non-voting)'
1799 else:
1800 voting = ''
1801 if html:
1802 if build:
1803 url = build.url
1804 else:
1805 url = None
1806 if url is not None:
1807 job_name = '<a href="%s">%s</a>' % (url, job_name)
1808 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
1809 ret += '\n'
1810 return ret
1811
James E. Blaira04b0792017-04-27 09:59:06 -07001812 def makeMergerItem(self):
1813 # Create a dictionary with all info about the item needed by
1814 # the merger.
1815 number = None
1816 patchset = None
1817 oldrev = None
1818 newrev = None
1819 refspec = None
1820 if hasattr(self.change, 'number'):
1821 number = self.change.number
1822 patchset = self.change.patchset
1823 refspec = self.change.refspec
1824 branch = self.change.branch
1825 elif hasattr(self.change, 'newrev'):
1826 oldrev = self.change.oldrev
1827 newrev = self.change.newrev
1828 branch = self.change.ref
1829 else:
1830 oldrev = None
1831 newrev = None
1832 branch = None
1833 source = self.change.project.source
1834 connection_name = source.connection.connection_name
James E. Blair2a535672017-04-27 12:03:15 -07001835 project = self.change.project
James E. Blaira04b0792017-04-27 09:59:06 -07001836
James E. Blair2a535672017-04-27 12:03:15 -07001837 return dict(project=project.name,
1838 connection=connection_name,
James E. Blaira04b0792017-04-27 09:59:06 -07001839 merge_mode=self.current_build_set.getMergeMode(),
1840 refspec=refspec,
1841 branch=branch,
1842 ref=self.current_build_set.ref,
1843 number=number,
1844 patchset=patchset,
1845 oldrev=oldrev,
1846 newrev=newrev,
1847 )
1848
James E. Blairfee8d652013-06-07 08:57:52 -07001849
Clint Byrumf8cc9902017-03-22 22:38:25 -07001850class Ref(object):
1851 """An existing state of a Project."""
James E. Blairfee8d652013-06-07 08:57:52 -07001852
1853 def __init__(self, project):
1854 self.project = project
Clint Byrumf8cc9902017-03-22 22:38:25 -07001855 self.ref = None
1856 self.oldrev = None
1857 self.newrev = None
James E. Blairfee8d652013-06-07 08:57:52 -07001858
Jesse Keating71a47ff2017-06-06 11:36:43 -07001859 self.files = []
1860
Joshua Hesketh36c3fa52014-01-22 11:40:52 +11001861 def getBasePath(self):
1862 base_path = ''
Clint Byrumf8cc9902017-03-22 22:38:25 -07001863 if hasattr(self, 'ref'):
Joshua Hesketh36c3fa52014-01-22 11:40:52 +11001864 base_path = "%s/%s" % (self.newrev[:2], self.newrev)
1865
1866 return base_path
1867
Clint Byrumf8cc9902017-03-22 22:38:25 -07001868 def _id(self):
1869 return self.newrev
1870
1871 def __repr__(self):
1872 rep = None
1873 if self.newrev == '0000000000000000000000000000000000000000':
1874 rep = '<Ref 0x%x deletes %s from %s' % (
1875 id(self), self.ref, self.oldrev)
1876 elif self.oldrev == '0000000000000000000000000000000000000000':
1877 rep = '<Ref 0x%x creates %s on %s>' % (
1878 id(self), self.ref, self.newrev)
1879 else:
1880 # Catch all
1881 rep = '<Ref 0x%x %s updated %s..%s>' % (
1882 id(self), self.ref, self.oldrev, self.newrev)
1883
1884 return rep
1885
James E. Blairfee8d652013-06-07 08:57:52 -07001886 def equals(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001887 if (self.project == other.project
1888 and self.ref == other.ref
1889 and self.newrev == other.newrev):
1890 return True
1891 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001892
1893 def isUpdateOf(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001894 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001895
1896 def filterJobs(self, jobs):
1897 return filter(lambda job: job.changeMatches(self), jobs)
1898
1899 def getRelatedChanges(self):
1900 return set()
1901
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001902 def updatesConfig(self):
Tristan Cacqueray829e6172017-06-13 06:49:36 +00001903 if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files or \
1904 [True for fn in self.files if fn.startswith("zuul.d/") or
1905 fn.startswith(".zuul.d/")]:
Jesse Keating71a47ff2017-06-06 11:36:43 -07001906 return True
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001907 return False
1908
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001909 def getSafeAttributes(self):
1910 return Attributes(project=self.project,
1911 ref=self.ref,
1912 oldrev=self.oldrev,
1913 newrev=self.newrev)
1914
James E. Blair1e8dd892012-05-30 09:15:05 -07001915
Clint Byrumf8cc9902017-03-22 22:38:25 -07001916class Change(Ref):
Monty Taylora42a55b2016-07-29 07:53:33 -07001917 """A proposed new state for a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001918 def __init__(self, project):
1919 super(Change, self).__init__(project)
1920 self.branch = None
1921 self.number = None
1922 self.url = None
1923 self.patchset = None
1924 self.refspec = None
1925
James E. Blair6965a4b2014-12-16 17:19:04 -08001926 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -07001927 self.needed_by_changes = []
1928 self.is_current_patchset = True
1929 self.can_merge = False
1930 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -07001931 self.failed_to_merge = False
James E. Blair11041d22014-05-02 14:49:53 -07001932 self.open = None
1933 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001934 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001935
Jan Hruban3b415922016-02-03 13:10:22 +01001936 self.source_event = None
1937
James E. Blair4aea70c2012-07-26 14:23:24 -07001938 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -07001939 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -07001940
1941 def __repr__(self):
1942 return '<Change 0x%x %s>' % (id(self), self._id())
1943
Clint Byrumf8cc9902017-03-22 22:38:25 -07001944 def getBasePath(self):
1945 if hasattr(self, 'refspec'):
1946 return "%s/%s/%s" % (
Gregory Haynes4fc12542015-04-22 20:38:06 -07001947 str(self.number)[-2:], self.number, self.patchset)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001948 return super(Change, self).getBasePath()
1949
James E. Blair4aea70c2012-07-26 14:23:24 -07001950 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +08001951 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -07001952 return True
1953 return False
1954
James E. Blair2fa50962013-01-30 21:50:41 -08001955 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -08001956 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -07001957 (hasattr(other, 'patchset') and
1958 self.patchset is not None and
1959 other.patchset is not None and
1960 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -08001961 return True
1962 return False
1963
James E. Blairfee8d652013-06-07 08:57:52 -07001964 def getRelatedChanges(self):
1965 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -08001966 for c in self.needs_changes:
1967 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -07001968 for c in self.needed_by_changes:
1969 related.add(c)
1970 related.update(c.getRelatedChanges())
1971 return related
James E. Blair4aea70c2012-07-26 14:23:24 -07001972
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001973 def getSafeAttributes(self):
1974 return Attributes(project=self.project,
1975 number=self.number,
1976 patchset=self.patchset)
1977
James E. Blair4aea70c2012-07-26 14:23:24 -07001978
James E. Blairee743612012-05-29 14:49:32 -07001979class TriggerEvent(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001980 """Incoming event from an external system."""
James E. Blairee743612012-05-29 14:49:32 -07001981 def __init__(self):
James E. Blairaad3ae22017-05-18 14:11:29 -07001982 # TODO(jeblair): further reduce this list
James E. Blairee743612012-05-29 14:49:32 -07001983 self.data = None
James E. Blair32663402012-06-01 10:04:18 -07001984 # common
James E. Blairee743612012-05-29 14:49:32 -07001985 self.type = None
Jesse Keating71a47ff2017-06-06 11:36:43 -07001986 self.branch_updated = False
Paul Belangerbaca3132016-11-04 12:49:54 -04001987 # For management events (eg: enqueue / promote)
1988 self.tenant_name = None
James E. Blair6f284b42017-03-31 14:14:41 -07001989 self.project_hostname = None
James E. Blairee743612012-05-29 14:49:32 -07001990 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -07001991 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +01001992 # Representation of the user account that performed the event.
1993 self.account = None
James E. Blair32663402012-06-01 10:04:18 -07001994 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -07001995 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -07001996 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -07001997 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -07001998 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -07001999 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07002000 self.comment = None
Jesse Keating5c05a9f2017-01-12 14:44:58 -08002001 self.state = None
James E. Blair32663402012-06-01 10:04:18 -07002002 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -07002003 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -07002004 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07002005 self.newrev = None
James E. Blairad28e912013-11-27 10:43:22 -08002006 # For events that arrive with a destination pipeline (eg, from
2007 # an admin command, etc):
2008 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07002009
James E. Blair6f284b42017-03-31 14:14:41 -07002010 @property
2011 def canonical_project_name(self):
2012 return self.project_hostname + '/' + self.project_name
2013
Jan Hruban324ca5b2015-11-05 19:28:54 +01002014 def isPatchsetCreated(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01002015 return False
2016
2017 def isChangeAbandoned(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01002018 return False
2019
James E. Blair1e8dd892012-05-30 09:15:05 -07002020
James E. Blair9c17dbf2014-06-23 14:21:58 -07002021class BaseFilter(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002022 """Base Class for filtering which Changes and Events to process."""
James E. Blairaad3ae22017-05-18 14:11:29 -07002023 pass
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002024
James E. Blair9c17dbf2014-06-23 14:21:58 -07002025
2026class EventFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002027 """Allows a Pipeline to only respond to certain events."""
James E. Blairaad3ae22017-05-18 14:11:29 -07002028 def __init__(self, trigger):
2029 super(EventFilter, self).__init__()
James E. Blairc0dedf82014-08-06 09:37:52 -07002030 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07002031
James E. Blairaad3ae22017-05-18 14:11:29 -07002032 def matches(self, event, ref):
2033 # TODO(jeblair): consider removing ref argument
James E. Blairee743612012-05-29 14:49:32 -07002034 return True
James E. Blaireff88162013-07-01 12:44:14 -04002035
2036
James E. Blairaad3ae22017-05-18 14:11:29 -07002037class RefFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002038 """Allows a Manager to only enqueue Changes that meet certain criteria."""
Jesse Keating8c2eb572017-05-30 17:31:45 -07002039 def __init__(self, connection_name):
James E. Blairaad3ae22017-05-18 14:11:29 -07002040 super(RefFilter, self).__init__()
Jesse Keating8c2eb572017-05-30 17:31:45 -07002041 self.connection_name = connection_name
James E. Blair11041d22014-05-02 14:49:53 -07002042
2043 def matches(self, change):
James E. Blair11041d22014-05-02 14:49:53 -07002044 return True
2045
2046
James E. Blairb97ed802015-12-21 15:55:35 -08002047class ProjectPipelineConfig(object):
2048 # Represents a project cofiguration in the context of a pipeline
2049 def __init__(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002050 self.job_list = JobList()
James E. Blairb97ed802015-12-21 15:55:35 -08002051 self.queue_name = None
Adam Gandelman8bd57102016-12-02 12:58:42 -08002052 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08002053
2054
James E. Blair08d9b782017-06-29 14:22:48 -07002055class TenantProjectConfig(object):
2056 """A project in the context of a tenant.
2057
2058 A Project is globally unique in the system, however, when used in
2059 a tenant, some metadata about the project local to the tenant is
2060 stored in a TenantProjectConfig.
2061 """
2062
2063 def __init__(self, project):
2064 self.project = project
2065 self.load_classes = set()
James E. Blair6459db12017-06-29 14:57:20 -07002066 self.shadow_projects = set()
James E. Blair08d9b782017-06-29 14:22:48 -07002067
2068
James E. Blairb97ed802015-12-21 15:55:35 -08002069class ProjectConfig(object):
2070 # Represents a project cofiguration
2071 def __init__(self, name):
2072 self.name = name
Adam Gandelman8bd57102016-12-02 12:58:42 -08002073 self.merge_mode = None
James E. Blair040b6502017-05-23 10:18:21 -07002074 self.default_branch = None
James E. Blairb97ed802015-12-21 15:55:35 -08002075 self.pipelines = {}
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002076 self.private_key_file = None
James E. Blairb97ed802015-12-21 15:55:35 -08002077
2078
James E. Blaird8e778f2015-12-22 14:09:20 -08002079class UnparsedAbideConfig(object):
James E. Blair08d9b782017-06-29 14:22:48 -07002080
Monty Taylora42a55b2016-07-29 07:53:33 -07002081 """A collection of yaml lists that has not yet been parsed into objects.
2082
2083 An Abide is a collection of tenants.
2084 """
2085
James E. Blaird8e778f2015-12-22 14:09:20 -08002086 def __init__(self):
2087 self.tenants = []
2088
2089 def extend(self, conf):
2090 if isinstance(conf, UnparsedAbideConfig):
2091 self.tenants.extend(conf.tenants)
2092 return
2093
2094 if not isinstance(conf, list):
2095 raise Exception("Configuration items must be in the form of "
2096 "a list of dictionaries (when parsing %s)" %
2097 (conf,))
2098 for item in conf:
2099 if not isinstance(item, dict):
2100 raise Exception("Configuration items must be in the form of "
2101 "a list of dictionaries (when parsing %s)" %
2102 (conf,))
2103 if len(item.keys()) > 1:
2104 raise Exception("Configuration item dictionaries must have "
2105 "a single key (when parsing %s)" %
2106 (conf,))
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002107 key, value = list(item.items())[0]
James E. Blaird8e778f2015-12-22 14:09:20 -08002108 if key == 'tenant':
2109 self.tenants.append(value)
2110 else:
2111 raise Exception("Configuration item not recognized "
2112 "(when parsing %s)" %
2113 (conf,))
2114
2115
2116class UnparsedTenantConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002117 """A collection of yaml lists that has not yet been parsed into objects."""
2118
James E. Blaird8e778f2015-12-22 14:09:20 -08002119 def __init__(self):
2120 self.pipelines = []
2121 self.jobs = []
2122 self.project_templates = []
James E. Blairff555742017-02-19 11:34:27 -08002123 self.projects = {}
James E. Blaira98340f2016-09-02 11:33:49 -07002124 self.nodesets = []
James E. Blair01f83b72017-03-15 13:03:40 -07002125 self.secrets = []
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002126 self.semaphores = []
James E. Blaird8e778f2015-12-22 14:09:20 -08002127
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002128 def copy(self):
2129 r = UnparsedTenantConfig()
2130 r.pipelines = copy.deepcopy(self.pipelines)
2131 r.jobs = copy.deepcopy(self.jobs)
2132 r.project_templates = copy.deepcopy(self.project_templates)
2133 r.projects = copy.deepcopy(self.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07002134 r.nodesets = copy.deepcopy(self.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002135 r.secrets = copy.deepcopy(self.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002136 r.semaphores = copy.deepcopy(self.semaphores)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002137 return r
2138
James E. Blairec7ff302017-03-04 07:31:32 -08002139 def extend(self, conf):
James E. Blaird8e778f2015-12-22 14:09:20 -08002140 if isinstance(conf, UnparsedTenantConfig):
2141 self.pipelines.extend(conf.pipelines)
2142 self.jobs.extend(conf.jobs)
2143 self.project_templates.extend(conf.project_templates)
James E. Blairff555742017-02-19 11:34:27 -08002144 for k, v in conf.projects.items():
2145 self.projects.setdefault(k, []).extend(v)
James E. Blaira98340f2016-09-02 11:33:49 -07002146 self.nodesets.extend(conf.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002147 self.secrets.extend(conf.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002148 self.semaphores.extend(conf.semaphores)
James E. Blaird8e778f2015-12-22 14:09:20 -08002149 return
2150
2151 if not isinstance(conf, list):
2152 raise Exception("Configuration items must be in the form of "
2153 "a list of dictionaries (when parsing %s)" %
2154 (conf,))
James E. Blaircdab2032017-02-01 09:09:29 -08002155
James E. Blaird8e778f2015-12-22 14:09:20 -08002156 for item in conf:
2157 if not isinstance(item, dict):
2158 raise Exception("Configuration items must be in the form of "
2159 "a list of dictionaries (when parsing %s)" %
2160 (conf,))
2161 if len(item.keys()) > 1:
2162 raise Exception("Configuration item dictionaries must have "
2163 "a single key (when parsing %s)" %
2164 (conf,))
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002165 key, value = list(item.items())[0]
James E. Blair66b274e2017-01-31 14:47:52 -08002166 if key == 'project':
James E. Blairff555742017-02-19 11:34:27 -08002167 name = value['name']
2168 self.projects.setdefault(name, []).append(value)
James E. Blair66b274e2017-01-31 14:47:52 -08002169 elif key == 'job':
James E. Blaird8e778f2015-12-22 14:09:20 -08002170 self.jobs.append(value)
2171 elif key == 'project-template':
2172 self.project_templates.append(value)
2173 elif key == 'pipeline':
2174 self.pipelines.append(value)
James E. Blaira98340f2016-09-02 11:33:49 -07002175 elif key == 'nodeset':
2176 self.nodesets.append(value)
James E. Blair01f83b72017-03-15 13:03:40 -07002177 elif key == 'secret':
2178 self.secrets.append(value)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002179 elif key == 'semaphore':
2180 self.semaphores.append(value)
James E. Blaird8e778f2015-12-22 14:09:20 -08002181 else:
Morgan Fainberg4245a422016-08-05 16:20:12 -07002182 raise Exception("Configuration item `%s` not recognized "
James E. Blaird8e778f2015-12-22 14:09:20 -08002183 "(when parsing %s)" %
Morgan Fainberg4245a422016-08-05 16:20:12 -07002184 (item, conf,))
James E. Blaird8e778f2015-12-22 14:09:20 -08002185
2186
James E. Blaireff88162013-07-01 12:44:14 -04002187class Layout(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002188 """Holds all of the Pipelines."""
2189
James E. Blair6459db12017-06-29 14:57:20 -07002190 def __init__(self, tenant):
2191 self.tenant = tenant
James E. Blairb97ed802015-12-21 15:55:35 -08002192 self.project_configs = {}
2193 self.project_templates = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07002194 self.pipelines = OrderedDict()
James E. Blair83005782015-12-11 14:46:03 -08002195 # This is a dictionary of name -> [jobs]. The first element
2196 # of the list is the first job added with that name. It is
2197 # the reference definition for a given job. Subsequent
2198 # elements are aspects of that job with different matchers
2199 # that override some attribute of the job. These aspects all
2200 # inherit from the reference definition.
James E. Blairfef88ec2016-11-30 08:38:27 -08002201 self.jobs = {'noop': [Job('noop')]}
James E. Blaira98340f2016-09-02 11:33:49 -07002202 self.nodesets = {}
James E. Blair01f83b72017-03-15 13:03:40 -07002203 self.secrets = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002204 self.semaphores = {}
James E. Blaireff88162013-07-01 12:44:14 -04002205
2206 def getJob(self, name):
2207 if name in self.jobs:
James E. Blair83005782015-12-11 14:46:03 -08002208 return self.jobs[name][0]
James E. Blairb97ed802015-12-21 15:55:35 -08002209 raise Exception("Job %s not defined" % (name,))
James E. Blair83005782015-12-11 14:46:03 -08002210
2211 def getJobs(self, name):
2212 return self.jobs.get(name, [])
2213
2214 def addJob(self, job):
James E. Blair4317e9f2016-07-15 10:05:47 -07002215 # We can have multiple variants of a job all with the same
2216 # name, but these variants must all be defined in the same repo.
James E. Blaircdab2032017-02-01 09:09:29 -08002217 prior_jobs = [j for j in self.getJobs(job.name) if
2218 j.source_context.project !=
2219 job.source_context.project]
James E. Blair6459db12017-06-29 14:57:20 -07002220 # Unless the repo is permitted to shadow another. If so, and
2221 # the job we are adding is from a repo that is permitted to
2222 # shadow the one with the older jobs, skip adding this job.
2223 job_project = job.source_context.project
2224 job_tpc = self.tenant.project_configs[job_project.canonical_name]
2225 skip_add = False
2226 for prior_job in prior_jobs[:]:
2227 prior_project = prior_job.source_context.project
2228 if prior_project in job_tpc.shadow_projects:
2229 prior_jobs.remove(prior_job)
2230 skip_add = True
2231
James E. Blair4317e9f2016-07-15 10:05:47 -07002232 if prior_jobs:
2233 raise Exception("Job %s in %s is not permitted to shadow "
James E. Blaircdab2032017-02-01 09:09:29 -08002234 "job %s in %s" % (
2235 job,
2236 job.source_context.project,
2237 prior_jobs[0],
2238 prior_jobs[0].source_context.project))
James E. Blair6459db12017-06-29 14:57:20 -07002239 if skip_add:
2240 return False
James E. Blair83005782015-12-11 14:46:03 -08002241 if job.name in self.jobs:
2242 self.jobs[job.name].append(job)
2243 else:
2244 self.jobs[job.name] = [job]
James E. Blair6459db12017-06-29 14:57:20 -07002245 return True
James E. Blair83005782015-12-11 14:46:03 -08002246
James E. Blaira98340f2016-09-02 11:33:49 -07002247 def addNodeSet(self, nodeset):
2248 if nodeset.name in self.nodesets:
2249 raise Exception("NodeSet %s already defined" % (nodeset.name,))
2250 self.nodesets[nodeset.name] = nodeset
2251
James E. Blair01f83b72017-03-15 13:03:40 -07002252 def addSecret(self, secret):
2253 if secret.name in self.secrets:
2254 raise Exception("Secret %s already defined" % (secret.name,))
2255 self.secrets[secret.name] = secret
2256
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002257 def addSemaphore(self, semaphore):
2258 if semaphore.name in self.semaphores:
2259 raise Exception("Semaphore %s already defined" % (semaphore.name,))
2260 self.semaphores[semaphore.name] = semaphore
2261
James E. Blair83005782015-12-11 14:46:03 -08002262 def addPipeline(self, pipeline):
2263 self.pipelines[pipeline.name] = pipeline
James E. Blair59fdbac2015-12-07 17:08:06 -08002264
James E. Blairb97ed802015-12-21 15:55:35 -08002265 def addProjectTemplate(self, project_template):
2266 self.project_templates[project_template.name] = project_template
2267
James E. Blairf59f3cf2017-02-19 14:50:26 -08002268 def addProjectConfig(self, project_config):
James E. Blairb97ed802015-12-21 15:55:35 -08002269 self.project_configs[project_config.name] = project_config
James E. Blairb97ed802015-12-21 15:55:35 -08002270
James E. Blaird2348362017-03-17 13:59:35 -07002271 def _createJobGraph(self, item, job_list, job_graph):
2272 change = item.change
2273 pipeline = item.pipeline
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002274 for jobname in job_list.jobs:
2275 # This is the final job we are constructing
James E. Blaira7f51ca2017-02-07 16:01:26 -08002276 frozen_job = None
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002277 # Whether the change matches any globally defined variant
James E. Blaira7f51ca2017-02-07 16:01:26 -08002278 matched = False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002279 for variant in self.getJobs(jobname):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002280 if variant.changeMatches(change):
James E. Blaira7f51ca2017-02-07 16:01:26 -08002281 if frozen_job is None:
2282 frozen_job = variant.copy()
2283 frozen_job.setRun()
2284 else:
2285 frozen_job.applyVariant(variant)
2286 matched = True
2287 if not matched:
James E. Blair6e85c2b2016-11-21 16:47:01 -08002288 # A change must match at least one defined job variant
2289 # (that is to say that it must match more than just
2290 # the job that is defined in the tree).
2291 continue
James E. Blaira7f51ca2017-02-07 16:01:26 -08002292 # If the job does not allow auth inheritance, do not allow
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002293 # the project-pipeline variants to update its execution
James E. Blaira7f51ca2017-02-07 16:01:26 -08002294 # attributes.
James E. Blair8525e2b2017-03-15 14:05:47 -07002295 if frozen_job.auth and not frozen_job.auth.inherit:
James E. Blaira7f51ca2017-02-07 16:01:26 -08002296 frozen_job.final = True
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002297 # Whether the change matches any of the project pipeline
2298 # variants
2299 matched = False
2300 for variant in job_list.jobs[jobname]:
2301 if variant.changeMatches(change):
2302 frozen_job.applyVariant(variant)
2303 matched = True
2304 if not matched:
2305 # A change must match at least one project pipeline
2306 # job variant.
2307 continue
James E. Blairb3f5db12017-03-17 12:57:39 -07002308 if (frozen_job.allowed_projects and
2309 change.project.name not in frozen_job.allowed_projects):
2310 raise Exception("Project %s is not allowed to run job %s" %
2311 (change.project.name, frozen_job.name))
James E. Blaird2348362017-03-17 13:59:35 -07002312 if ((not pipeline.allow_secrets) and frozen_job.auth and
2313 frozen_job.auth.secrets):
2314 raise Exception("Pipeline %s does not allow jobs with "
2315 "secrets (job %s)" % (
2316 pipeline.name, frozen_job.name))
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002317 job_graph.addJob(frozen_job)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002318
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002319 def createJobGraph(self, item):
Paul Belanger160cb8e2016-11-11 19:04:24 -05002320 project_config = self.project_configs.get(
James E. Blair0ffa0102017-03-30 13:11:33 -07002321 item.change.project.canonical_name, None)
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002322 ret = JobGraph()
Paul Belanger15e3e202016-10-14 16:27:34 -04002323 # NOTE(pabelanger): It is possible for a foreign project not to have a
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002324 # configured pipeline, if so return an empty JobGraph.
Paul Belanger160cb8e2016-11-11 19:04:24 -05002325 if project_config and item.pipeline.name in project_config.pipelines:
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002326 project_job_list = \
2327 project_config.pipelines[item.pipeline.name].job_list
James E. Blaird2348362017-03-17 13:59:35 -07002328 self._createJobGraph(item, project_job_list, ret)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002329 return ret
2330
James E. Blair0d3e83b2017-06-05 13:51:57 -07002331 def hasProject(self, project):
2332 return project.canonical_name in self.project_configs
2333
James E. Blair59fdbac2015-12-07 17:08:06 -08002334
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002335class Semaphore(object):
2336 def __init__(self, name, max=1):
2337 self.name = name
2338 self.max = int(max)
2339
2340
2341class SemaphoreHandler(object):
2342 log = logging.getLogger("zuul.SemaphoreHandler")
2343
2344 def __init__(self):
2345 self.semaphores = {}
2346
2347 def acquire(self, item, job):
2348 if not job.semaphore:
2349 return True
2350
2351 semaphore_key = job.semaphore
2352
2353 m = self.semaphores.get(semaphore_key)
2354 if not m:
2355 # The semaphore is not held, acquire it
2356 self._acquire(semaphore_key, item, job.name)
2357 return True
2358 if (item, job.name) in m:
2359 # This item already holds the semaphore
2360 return True
2361
2362 # semaphore is there, check max
2363 if len(m) < self._max_count(item, job.semaphore):
2364 self._acquire(semaphore_key, item, job.name)
2365 return True
2366
2367 return False
2368
2369 def release(self, item, job):
2370 if not job.semaphore:
2371 return
2372
2373 semaphore_key = job.semaphore
2374
2375 m = self.semaphores.get(semaphore_key)
2376 if not m:
2377 # The semaphore is not held, nothing to do
2378 self.log.error("Semaphore can not be released for %s "
2379 "because the semaphore is not held" %
2380 item)
2381 return
2382 if (item, job.name) in m:
2383 # This item is a holder of the semaphore
2384 self._release(semaphore_key, item, job.name)
2385 return
2386 self.log.error("Semaphore can not be released for %s "
2387 "which does not hold it" % item)
2388
2389 def _acquire(self, semaphore_key, item, job_name):
2390 self.log.debug("Semaphore acquire {semaphore}: job {job}, item {item}"
2391 .format(semaphore=semaphore_key,
2392 job=job_name,
2393 item=item))
2394 if semaphore_key not in self.semaphores:
2395 self.semaphores[semaphore_key] = []
2396 self.semaphores[semaphore_key].append((item, job_name))
2397
2398 def _release(self, semaphore_key, item, job_name):
2399 self.log.debug("Semaphore release {semaphore}: job {job}, item {item}"
2400 .format(semaphore=semaphore_key,
2401 job=job_name,
2402 item=item))
2403 sem_item = (item, job_name)
2404 if sem_item in self.semaphores[semaphore_key]:
2405 self.semaphores[semaphore_key].remove(sem_item)
2406
2407 # cleanup if there is no user of the semaphore anymore
2408 if len(self.semaphores[semaphore_key]) == 0:
2409 del self.semaphores[semaphore_key]
2410
2411 @staticmethod
2412 def _max_count(item, semaphore_name):
2413 if not item.current_build_set.layout:
2414 # This should not occur as the layout of the item must already be
2415 # built when acquiring or releasing a semaphore for a job.
2416 raise Exception("Item {} has no layout".format(item))
2417
2418 # find the right semaphore
2419 default_semaphore = Semaphore(semaphore_name, 1)
2420 semaphores = item.current_build_set.layout.semaphores
2421 return semaphores.get(semaphore_name, default_semaphore).max
2422
2423
James E. Blair59fdbac2015-12-07 17:08:06 -08002424class Tenant(object):
2425 def __init__(self, name):
2426 self.name = name
2427 self.layout = None
James E. Blair646322f2017-01-27 15:50:34 -08002428 # The unparsed configuration from the main zuul config for
2429 # this tenant.
2430 self.unparsed_config = None
James E. Blair109da3f2017-04-04 14:39:43 -07002431 # The list of projects from which we will read full
James E. Blair8a395f92017-03-30 11:15:33 -07002432 # configuration.
James E. Blair109da3f2017-04-04 14:39:43 -07002433 self.config_projects = []
2434 # The unparsed config from those projects.
2435 self.config_projects_config = None
2436 # The list of projects from which we will read untrusted
2437 # in-repo configuration.
2438 self.untrusted_projects = []
2439 # The unparsed config from those projects.
2440 self.untrusted_projects_config = None
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002441 self.semaphore_handler = SemaphoreHandler()
James E. Blair08d9b782017-06-29 14:22:48 -07002442 # Metadata about projects for this tenant
2443 # canonical project name -> TenantProjectConfig
2444 self.project_configs = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002445
James E. Blairc2a54fd2017-03-29 15:19:26 -07002446 # A mapping of project names to projects. project_name ->
2447 # VALUE where VALUE is a further dictionary of
2448 # canonical_hostname -> Project.
2449 self.projects = {}
2450 self.canonical_hostnames = set()
2451
James E. Blair08d9b782017-06-29 14:22:48 -07002452 def _addProject(self, tpc):
James E. Blairc2a54fd2017-03-29 15:19:26 -07002453 """Add a project to the project index
2454
James E. Blair08d9b782017-06-29 14:22:48 -07002455 :arg TenantProjectConfig tpc: The TenantProjectConfig (with
2456 associated project) to add.
2457
James E. Blairc2a54fd2017-03-29 15:19:26 -07002458 """
James E. Blair08d9b782017-06-29 14:22:48 -07002459 project = tpc.project
James E. Blairc2a54fd2017-03-29 15:19:26 -07002460 self.canonical_hostnames.add(project.canonical_hostname)
2461 hostname_dict = self.projects.setdefault(project.name, {})
2462 if project.canonical_hostname in hostname_dict:
2463 raise Exception("Project %s is already in project index" %
2464 (project,))
2465 hostname_dict[project.canonical_hostname] = project
James E. Blair08d9b782017-06-29 14:22:48 -07002466 self.project_configs[project.canonical_name] = tpc
James E. Blairc2a54fd2017-03-29 15:19:26 -07002467
2468 def getProject(self, name):
2469 """Return a project given its name.
2470
2471 :arg str name: The name of the project. It may be fully
2472 qualified (E.g., "git.example.com/subpath/project") or may
2473 contain only the project name name may be supplied (E.g.,
2474 "subpath/project").
2475
2476 :returns: A tuple (trusted, project) or (None, None) if the
2477 project is not found or ambiguous. The "trusted" boolean
2478 indicates whether or not the project is trusted by this
2479 tenant.
2480 :rtype: (bool, Project)
2481
2482 """
2483 path = name.split('/', 1)
2484 if path[0] in self.canonical_hostnames:
2485 hostname = path[0]
2486 project_name = path[1]
2487 else:
2488 hostname = None
2489 project_name = name
2490 hostname_dict = self.projects.get(project_name)
2491 project = None
2492 if hostname_dict:
2493 if hostname:
2494 project = hostname_dict.get(hostname)
2495 else:
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002496 values = list(hostname_dict.values())
James E. Blairc2a54fd2017-03-29 15:19:26 -07002497 if len(values) == 1:
2498 project = values[0]
2499 else:
2500 raise Exception("Project name '%s' is ambiguous, "
2501 "please fully qualify the project "
2502 "with a hostname" % (name,))
2503 if project is None:
2504 return (None, None)
James E. Blair109da3f2017-04-04 14:39:43 -07002505 if project in self.config_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002506 return (True, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002507 if project in self.untrusted_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002508 return (False, project)
2509 # This should never happen:
2510 raise Exception("Project %s is neither trusted nor untrusted" %
2511 (project,))
2512
James E. Blair08d9b782017-06-29 14:22:48 -07002513 def addConfigProject(self, tpc):
2514 self.config_projects.append(tpc.project)
2515 self._addProject(tpc)
James E. Blair5ac93842017-01-20 06:47:34 -08002516
James E. Blair08d9b782017-06-29 14:22:48 -07002517 def addUntrustedProject(self, tpc):
2518 self.untrusted_projects.append(tpc.project)
2519 self._addProject(tpc)
James E. Blair5ac93842017-01-20 06:47:34 -08002520
Jamie Lennoxbf997c82017-05-10 09:58:40 +10002521 def getSafeAttributes(self):
2522 return Attributes(name=self.name)
2523
James E. Blair59fdbac2015-12-07 17:08:06 -08002524
2525class Abide(object):
2526 def __init__(self):
2527 self.tenants = OrderedDict()
James E. Blairce8a2132016-05-19 15:21:52 -07002528
2529
2530class JobTimeData(object):
2531 format = 'B10H10H10B'
2532 version = 0
2533
2534 def __init__(self, path):
2535 self.path = path
2536 self.success_times = [0 for x in range(10)]
2537 self.failure_times = [0 for x in range(10)]
2538 self.results = [0 for x in range(10)]
2539
2540 def load(self):
2541 if not os.path.exists(self.path):
2542 return
Clint Byruma4471d12017-05-10 20:57:40 -04002543 with open(self.path, 'rb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002544 data = struct.unpack(self.format, f.read())
2545 version = data[0]
2546 if version != self.version:
2547 raise Exception("Unkown data version")
2548 self.success_times = list(data[1:11])
2549 self.failure_times = list(data[11:21])
2550 self.results = list(data[21:32])
2551
2552 def save(self):
2553 tmpfile = self.path + '.tmp'
2554 data = [self.version]
2555 data.extend(self.success_times)
2556 data.extend(self.failure_times)
2557 data.extend(self.results)
2558 data = struct.pack(self.format, *data)
Clint Byruma4471d12017-05-10 20:57:40 -04002559 with open(tmpfile, 'wb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002560 f.write(data)
2561 os.rename(tmpfile, self.path)
2562
2563 def add(self, elapsed, result):
2564 elapsed = int(elapsed)
2565 if result == 'SUCCESS':
2566 self.success_times.append(elapsed)
2567 self.success_times.pop(0)
2568 result = 0
2569 else:
2570 self.failure_times.append(elapsed)
2571 self.failure_times.pop(0)
2572 result = 1
2573 self.results.append(result)
2574 self.results.pop(0)
2575
2576 def getEstimatedTime(self):
2577 times = [x for x in self.success_times if x]
2578 if times:
2579 return float(sum(times)) / len(times)
2580 return 0.0
2581
2582
2583class TimeDataBase(object):
2584 def __init__(self, root):
2585 self.root = root
2586 self.jobs = {}
2587
2588 def _getTD(self, name):
2589 td = self.jobs.get(name)
2590 if not td:
2591 td = JobTimeData(os.path.join(self.root, name))
2592 self.jobs[name] = td
2593 td.load()
2594 return td
2595
2596 def getEstimatedTime(self, name):
2597 return self._getTD(name).getEstimatedTime()
2598
2599 def update(self, name, elapsed, result):
2600 td = self._getTD(name)
2601 td.add(elapsed, result)
2602 td.save()