blob: 0e42368f650fbd70e76ed3b6758be0dd24e609b9 [file] [log] [blame]
James E. Blairee743612012-05-29 14:49:32 -07001# Copyright 2012 Hewlett-Packard Development Company, L.P.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
James E. Blair5ac93842017-01-20 06:47:34 -080015import abc
Tristan Cacqueraye7410af2017-06-19 04:32:08 +000016from collections import OrderedDict
James E. Blair1b265312014-06-24 09:35:21 -070017import copy
Tobias Henkel9a0e1942017-03-20 16:16:02 +010018import logging
James E. Blairce8a2132016-05-19 15:21:52 -070019import os
James E. Blairce8a2132016-05-19 15:21:52 -070020import struct
James E. Blairff986a12012-05-30 14:56:51 -070021import time
James E. Blair4886cc12012-07-18 15:39:41 -070022from uuid import uuid4
James E. Blair88e79c02017-07-07 13:36:54 -070023import urllib.parse
James E. Blair97043882017-09-06 15:51:17 -070024import textwrap
James E. Blair5a9918a2013-08-27 10:06:27 -070025
James E. Blair19deff22013-08-25 13:17:35 -070026MERGER_MERGE = 1 # "git merge"
27MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
28MERGER_CHERRY_PICK = 3 # "git cherry-pick"
29
30MERGER_MAP = {
31 'merge': MERGER_MERGE,
32 'merge-resolve': MERGER_MERGE_RESOLVE,
33 'cherry-pick': MERGER_CHERRY_PICK,
34}
James E. Blairee743612012-05-29 14:49:32 -070035
James E. Blair64ed6f22013-07-10 14:07:23 -070036PRECEDENCE_NORMAL = 0
37PRECEDENCE_LOW = 1
38PRECEDENCE_HIGH = 2
39
40PRECEDENCE_MAP = {
41 None: PRECEDENCE_NORMAL,
42 'low': PRECEDENCE_LOW,
43 'normal': PRECEDENCE_NORMAL,
44 'high': PRECEDENCE_HIGH,
45}
46
James E. Blair803e94f2017-01-06 09:18:59 -080047# Request states
48STATE_REQUESTED = 'requested'
49STATE_PENDING = 'pending'
50STATE_FULFILLED = 'fulfilled'
51STATE_FAILED = 'failed'
52REQUEST_STATES = set([STATE_REQUESTED,
53 STATE_PENDING,
54 STATE_FULFILLED,
55 STATE_FAILED])
56
57# Node states
58STATE_BUILDING = 'building'
59STATE_TESTING = 'testing'
60STATE_READY = 'ready'
61STATE_IN_USE = 'in-use'
62STATE_USED = 'used'
63STATE_HOLD = 'hold'
64STATE_DELETING = 'deleting'
65NODE_STATES = set([STATE_BUILDING,
66 STATE_TESTING,
67 STATE_READY,
68 STATE_IN_USE,
69 STATE_USED,
70 STATE_HOLD,
71 STATE_DELETING])
72
James E. Blair1e8dd892012-05-30 09:15:05 -070073
Joshua Hesketh58419cb2017-02-24 13:09:22 -050074class Attributes(object):
75 """A class to hold attributes for string formatting."""
76
77 def __init__(self, **kw):
78 setattr(self, '__dict__', kw)
79
80
James E. Blair4aea70c2012-07-26 14:23:24 -070081class Pipeline(object):
James E. Blair6053de42017-04-05 11:27:11 -070082 """A configuration that ties together triggers, reporters and managers
Monty Taylor82dfd412016-07-29 12:01:28 -070083
84 Trigger
85 A description of which events should be processed
86
87 Manager
88 Responsible for enqueing and dequeing Changes
89
90 Reporter
91 Communicates success and failure results somewhere
Monty Taylora42a55b2016-07-29 07:53:33 -070092 """
James E. Blair83005782015-12-11 14:46:03 -080093 def __init__(self, name, layout):
James E. Blair4aea70c2012-07-26 14:23:24 -070094 self.name = name
James E. Blair83005782015-12-11 14:46:03 -080095 self.layout = layout
James E. Blair8dbd56a2012-12-22 10:55:10 -080096 self.description = None
James E. Blair56370192013-01-14 15:47:28 -080097 self.failure_message = None
Joshua Heskethb7179772014-01-30 23:30:46 +110098 self.merge_failure_message = None
James E. Blair56370192013-01-14 15:47:28 -080099 self.success_message = None
Joshua Hesketh3979e3e2014-03-04 11:21:10 +1100100 self.footer_message = None
James E. Blair60af7f42016-03-11 16:11:06 -0800101 self.start_message = None
James E. Blair8eb564a2017-08-10 09:21:41 -0700102 self.post_review = False
James E. Blair2fa50962013-01-30 21:50:41 -0800103 self.dequeue_on_new_patchset = True
James E. Blair17dd6772015-02-09 14:45:18 -0800104 self.ignore_dependencies = False
James E. Blair4aea70c2012-07-26 14:23:24 -0700105 self.manager = None
James E. Blaire0487072012-08-29 17:38:31 -0700106 self.queues = []
James E. Blair64ed6f22013-07-10 14:07:23 -0700107 self.precedence = PRECEDENCE_NORMAL
James E. Blair83005782015-12-11 14:46:03 -0800108 self.triggers = []
Joshua Hesketh352264b2015-08-11 23:42:08 +1000109 self.start_actions = []
110 self.success_actions = []
111 self.failure_actions = []
112 self.merge_failure_actions = []
113 self.disabled_actions = []
Joshua Hesketh89e829d2015-02-10 16:29:45 +1100114 self.disable_at = None
115 self._consecutive_failures = 0
116 self._disabled = False
Clark Boylan7603a372014-01-21 11:43:20 -0800117 self.window = None
118 self.window_floor = None
119 self.window_increase_type = None
120 self.window_increase_factor = None
121 self.window_decrease_type = None
122 self.window_decrease_factor = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700123
James E. Blair83005782015-12-11 14:46:03 -0800124 @property
125 def actions(self):
126 return (
127 self.start_actions +
128 self.success_actions +
129 self.failure_actions +
130 self.merge_failure_actions +
131 self.disabled_actions
132 )
133
James E. Blaird09c17a2012-08-07 09:23:14 -0700134 def __repr__(self):
135 return '<Pipeline %s>' % self.name
136
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100137 def getSafeAttributes(self):
138 return Attributes(name=self.name)
139
James E. Blair4aea70c2012-07-26 14:23:24 -0700140 def setManager(self, manager):
141 self.manager = manager
142
James E. Blaire0487072012-08-29 17:38:31 -0700143 def addQueue(self, queue):
144 self.queues.append(queue)
145
146 def getQueue(self, project):
147 for queue in self.queues:
148 if project in queue.projects:
149 return queue
150 return None
151
James E. Blairbfb8e042014-12-30 17:01:44 -0800152 def removeQueue(self, queue):
Tobias Henkel6b9390f2017-03-28 11:23:21 +0200153 if queue in self.queues:
154 self.queues.remove(queue)
James E. Blairbfb8e042014-12-30 17:01:44 -0800155
James E. Blaire0487072012-08-29 17:38:31 -0700156 def getChangesInQueue(self):
157 changes = []
158 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700159 changes.extend([x.change for x in shared_queue.queue])
James E. Blaire0487072012-08-29 17:38:31 -0700160 return changes
161
James E. Blairfee8d652013-06-07 08:57:52 -0700162 def getAllItems(self):
163 items = []
James E. Blaire0487072012-08-29 17:38:31 -0700164 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700165 items.extend(shared_queue.queue)
James E. Blairfee8d652013-06-07 08:57:52 -0700166 return items
James E. Blaire0487072012-08-29 17:38:31 -0700167
Tobias Henkelb4407fc2017-07-07 13:52:56 +0200168 def formatStatusJSON(self, websocket_url=None):
James E. Blair8dbd56a2012-12-22 10:55:10 -0800169 j_pipeline = dict(name=self.name,
170 description=self.description)
171 j_queues = []
172 j_pipeline['change_queues'] = j_queues
173 for queue in self.queues:
174 j_queue = dict(name=queue.name)
175 j_queues.append(j_queue)
176 j_queue['heads'] = []
Clark Boylanaf2476f2014-01-23 14:47:36 -0800177 j_queue['window'] = queue.window
James E. Blair972e3c72013-08-29 12:04:55 -0700178
179 j_changes = []
180 for e in queue.queue:
181 if not e.item_ahead:
182 if j_changes:
183 j_queue['heads'].append(j_changes)
184 j_changes = []
Tobias Henkelb4407fc2017-07-07 13:52:56 +0200185 j_changes.append(e.formatJSON(websocket_url))
James E. Blair972e3c72013-08-29 12:04:55 -0700186 if (len(j_changes) > 1 and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000187 (j_changes[-2]['remaining_time'] is not None) and
188 (j_changes[-1]['remaining_time'] is not None)):
James E. Blair972e3c72013-08-29 12:04:55 -0700189 j_changes[-1]['remaining_time'] = max(
190 j_changes[-2]['remaining_time'],
191 j_changes[-1]['remaining_time'])
192 if j_changes:
James E. Blair8dbd56a2012-12-22 10:55:10 -0800193 j_queue['heads'].append(j_changes)
194 return j_pipeline
195
James E. Blair4aea70c2012-07-26 14:23:24 -0700196
James E. Blairee743612012-05-29 14:49:32 -0700197class ChangeQueue(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700198 """A ChangeQueue contains Changes to be processed related projects.
199
Monty Taylor82dfd412016-07-29 12:01:28 -0700200 A Pipeline with a DependentPipelineManager has multiple parallel
201 ChangeQueues shared by different projects. For instance, there may a
202 ChangeQueue shared by interrelated projects foo and bar, and a second queue
203 for independent project baz.
Monty Taylora42a55b2016-07-29 07:53:33 -0700204
Monty Taylor82dfd412016-07-29 12:01:28 -0700205 A Pipeline with an IndependentPipelineManager puts every Change into its
206 own ChangeQueue
Monty Taylora42a55b2016-07-29 07:53:33 -0700207
208 The ChangeQueue Window is inspired by TCP windows and controlls how many
209 Changes in a given ChangeQueue will be considered active and ready to
210 be processed. If a Change succeeds, the Window is increased by
211 `window_increase_factor`. If a Change fails, the Window is decreased by
212 `window_decrease_factor`.
Jesse Keating78f544a2017-07-13 14:27:40 -0700213
214 A ChangeQueue may be a dynamically created queue, which may be removed
215 from a DependentPipelineManager once empty.
Monty Taylora42a55b2016-07-29 07:53:33 -0700216 """
James E. Blairbfb8e042014-12-30 17:01:44 -0800217 def __init__(self, pipeline, window=0, window_floor=1,
Clark Boylan7603a372014-01-21 11:43:20 -0800218 window_increase_type='linear', window_increase_factor=1,
James E. Blair0dcef7a2016-08-19 09:35:17 -0700219 window_decrease_type='exponential', window_decrease_factor=2,
Jesse Keating78f544a2017-07-13 14:27:40 -0700220 name=None, dynamic=False):
James E. Blair4aea70c2012-07-26 14:23:24 -0700221 self.pipeline = pipeline
James E. Blair0dcef7a2016-08-19 09:35:17 -0700222 if name:
223 self.name = name
224 else:
225 self.name = ''
James E. Blairee743612012-05-29 14:49:32 -0700226 self.projects = []
227 self._jobs = set()
228 self.queue = []
Clark Boylan7603a372014-01-21 11:43:20 -0800229 self.window = window
230 self.window_floor = window_floor
231 self.window_increase_type = window_increase_type
232 self.window_increase_factor = window_increase_factor
233 self.window_decrease_type = window_decrease_type
234 self.window_decrease_factor = window_decrease_factor
Jesse Keating78f544a2017-07-13 14:27:40 -0700235 self.dynamic = dynamic
James E. Blairee743612012-05-29 14:49:32 -0700236
James E. Blair9f9667e2012-06-12 17:51:08 -0700237 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700238 return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
James E. Blairee743612012-05-29 14:49:32 -0700239
240 def getJobs(self):
241 return self._jobs
242
243 def addProject(self, project):
244 if project not in self.projects:
245 self.projects.append(project)
James E. Blairc8a1e052014-02-25 09:29:26 -0800246
James E. Blair0dcef7a2016-08-19 09:35:17 -0700247 if not self.name:
248 self.name = project.name
James E. Blairee743612012-05-29 14:49:32 -0700249
250 def enqueueChange(self, change):
James E. Blairbfb8e042014-12-30 17:01:44 -0800251 item = QueueItem(self, change)
James E. Blaircdccd972013-07-01 12:10:22 -0700252 self.enqueueItem(item)
253 item.enqueue_time = time.time()
254 return item
255
256 def enqueueItem(self, item):
James E. Blair4a035d92014-01-23 13:10:48 -0800257 item.pipeline = self.pipeline
James E. Blairbfb8e042014-12-30 17:01:44 -0800258 item.queue = self
259 if self.queue:
James E. Blairfee8d652013-06-07 08:57:52 -0700260 item.item_ahead = self.queue[-1]
James E. Blair972e3c72013-08-29 12:04:55 -0700261 item.item_ahead.items_behind.append(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700262 self.queue.append(item)
James E. Blairee743612012-05-29 14:49:32 -0700263
James E. Blairfee8d652013-06-07 08:57:52 -0700264 def dequeueItem(self, item):
265 if item in self.queue:
266 self.queue.remove(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700267 if item.item_ahead:
James E. Blair972e3c72013-08-29 12:04:55 -0700268 item.item_ahead.items_behind.remove(item)
269 for item_behind in item.items_behind:
270 if item.item_ahead:
271 item.item_ahead.items_behind.append(item_behind)
272 item_behind.item_ahead = item.item_ahead
James E. Blairfee8d652013-06-07 08:57:52 -0700273 item.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700274 item.items_behind = []
James E. Blairfee8d652013-06-07 08:57:52 -0700275 item.dequeue_time = time.time()
James E. Blaire0487072012-08-29 17:38:31 -0700276
James E. Blair972e3c72013-08-29 12:04:55 -0700277 def moveItem(self, item, item_ahead):
James E. Blair972e3c72013-08-29 12:04:55 -0700278 if item.item_ahead == item_ahead:
279 return False
280 # Remove from current location
281 if item.item_ahead:
282 item.item_ahead.items_behind.remove(item)
283 for item_behind in item.items_behind:
284 if item.item_ahead:
285 item.item_ahead.items_behind.append(item_behind)
286 item_behind.item_ahead = item.item_ahead
287 # Add to new location
288 item.item_ahead = item_ahead
James E. Blair00451262013-09-20 11:40:17 -0700289 item.items_behind = []
James E. Blair972e3c72013-08-29 12:04:55 -0700290 if item.item_ahead:
291 item.item_ahead.items_behind.append(item)
292 return True
James E. Blairee743612012-05-29 14:49:32 -0700293
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800294 def isActionable(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800295 if self.window:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800296 return item in self.queue[:self.window]
Clark Boylan7603a372014-01-21 11:43:20 -0800297 else:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800298 return True
Clark Boylan7603a372014-01-21 11:43:20 -0800299
300 def increaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800301 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800302 if self.window_increase_type == 'linear':
303 self.window += self.window_increase_factor
304 elif self.window_increase_type == 'exponential':
305 self.window *= self.window_increase_factor
306
307 def decreaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800308 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800309 if self.window_decrease_type == 'linear':
310 self.window = max(
311 self.window_floor,
312 self.window - self.window_decrease_factor)
313 elif self.window_decrease_type == 'exponential':
314 self.window = max(
315 self.window_floor,
Morgan Fainbergfdae2252016-06-07 20:13:20 -0700316 int(self.window / self.window_decrease_factor))
James E. Blairee743612012-05-29 14:49:32 -0700317
James E. Blair1e8dd892012-05-30 09:15:05 -0700318
James E. Blair4aea70c2012-07-26 14:23:24 -0700319class Project(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700320 """A Project represents a git repository such as openstack/nova."""
321
James E. Blaircf440a22016-07-15 09:11:58 -0700322 # NOTE: Projects should only be instantiated via a Source object
323 # so that they are associated with and cached by their Connection.
324 # This makes a Project instance a unique identifier for a given
325 # project from a given source.
326
James E. Blair0a899752017-03-29 13:22:16 -0700327 def __init__(self, name, source, foreign=False):
James E. Blair4aea70c2012-07-26 14:23:24 -0700328 self.name = name
James E. Blair8a395f92017-03-30 11:15:33 -0700329 self.source = source
James E. Blair0a899752017-03-29 13:22:16 -0700330 self.connection_name = source.connection.connection_name
331 self.canonical_hostname = source.canonical_hostname
James E. Blairc2a54fd2017-03-29 15:19:26 -0700332 self.canonical_name = source.canonical_hostname + '/' + name
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000333 # foreign projects are those referenced in dependencies
334 # of layout projects, this should matter
335 # when deciding whether to enqueue their changes
James E. Blaircf440a22016-07-15 09:11:58 -0700336 # TODOv3 (jeblair): re-add support for foreign projects if needed
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000337 self.foreign = foreign
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700338 self.unparsed_config = None
James E. Blaire3162022017-02-20 16:47:27 -0500339 self.unparsed_branch_config = {} # branch -> UnparsedTenantConfig
James E. Blair4aea70c2012-07-26 14:23:24 -0700340
341 def __str__(self):
342 return self.name
343
344 def __repr__(self):
345 return '<Project %s>' % (self.name)
346
347
James E. Blair34776ee2016-08-25 13:53:54 -0700348class Node(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700349 """A single node for use by a job.
350
351 This may represent a request for a node, or an actual node
352 provided by Nodepool.
353 """
354
James E. Blair16d96a02017-06-08 11:32:56 -0700355 def __init__(self, name, label):
James E. Blair34776ee2016-08-25 13:53:54 -0700356 self.name = name
James E. Blair16d96a02017-06-08 11:32:56 -0700357 self.label = label
James E. Blaircbf43672017-01-04 14:33:41 -0800358 self.id = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800359 self.lock = None
David Shrewsburyffab07a2017-07-24 12:45:07 -0400360 self.hold_job = None
David Shrewsburyf9af9df2017-08-01 15:19:26 -0400361 self.comment = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800362 # Attributes from Nodepool
363 self._state = 'unknown'
364 self.state_time = time.time()
Monty Taylor56f61332017-04-11 05:38:12 -0500365 self.interface_ip = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800366 self.public_ipv4 = None
367 self.private_ipv4 = None
368 self.public_ipv6 = None
Tristan Cacqueray80954402017-05-28 00:33:55 +0000369 self.ssh_port = 22
James E. Blaircacdf2b2017-01-04 13:14:37 -0800370 self._keys = []
Paul Belanger30ba93a2017-03-16 16:28:10 -0400371 self.az = None
372 self.provider = None
373 self.region = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800374
375 @property
376 def state(self):
377 return self._state
378
379 @state.setter
380 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800381 if value not in NODE_STATES:
382 raise TypeError("'%s' is not a valid state" % value)
James E. Blaira38c28e2017-01-04 10:33:20 -0800383 self._state = value
384 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700385
386 def __repr__(self):
James E. Blair16d96a02017-06-08 11:32:56 -0700387 return '<Node %s %s:%s>' % (self.id, self.name, self.label)
James E. Blair34776ee2016-08-25 13:53:54 -0700388
James E. Blair0d952152017-02-07 17:14:44 -0800389 def __ne__(self, other):
390 return not self.__eq__(other)
391
392 def __eq__(self, other):
393 if not isinstance(other, Node):
394 return False
395 return (self.name == other.name and
James E. Blair16d96a02017-06-08 11:32:56 -0700396 self.label == other.label and
James E. Blair0d952152017-02-07 17:14:44 -0800397 self.id == other.id)
398
James E. Blaircacdf2b2017-01-04 13:14:37 -0800399 def toDict(self):
400 d = {}
401 d['state'] = self.state
David Shrewsburyffab07a2017-07-24 12:45:07 -0400402 d['hold_job'] = self.hold_job
David Shrewsburyf9af9df2017-08-01 15:19:26 -0400403 d['comment'] = self.comment
James E. Blaircacdf2b2017-01-04 13:14:37 -0800404 for k in self._keys:
405 d[k] = getattr(self, k)
406 return d
407
James E. Blaira38c28e2017-01-04 10:33:20 -0800408 def updateFromDict(self, data):
409 self._state = data['state']
James E. Blaircacdf2b2017-01-04 13:14:37 -0800410 keys = []
411 for k, v in data.items():
412 if k == 'state':
413 continue
414 keys.append(k)
415 setattr(self, k, v)
416 self._keys = keys
James E. Blaira38c28e2017-01-04 10:33:20 -0800417
James E. Blair34776ee2016-08-25 13:53:54 -0700418
Monty Taylor7b19ba72017-05-24 07:42:54 -0500419class Group(object):
420 """A logical group of nodes for use by a job.
421
422 A Group is a named set of node names that will be provided to
423 jobs in the inventory to describe logical units where some subset of tasks
424 run.
425 """
426
427 def __init__(self, name, nodes):
428 self.name = name
429 self.nodes = nodes
430
431 def __repr__(self):
432 return '<Group %s %s>' % (self.name, str(self.nodes))
433
434 def __ne__(self, other):
435 return not self.__eq__(other)
436
437 def __eq__(self, other):
438 if not isinstance(other, Group):
439 return False
440 return (self.name == other.name and
441 self.nodes == other.nodes)
442
443 def toDict(self):
444 return {
445 'name': self.name,
446 'nodes': self.nodes
447 }
448
449
James E. Blaira98340f2016-09-02 11:33:49 -0700450class NodeSet(object):
451 """A set of nodes.
452
453 In configuration, NodeSets are attributes of Jobs indicating that
454 a Job requires nodes matching this description.
455
456 They may appear as top-level configuration objects and be named,
457 or they may appears anonymously in in-line job definitions.
458 """
459
460 def __init__(self, name=None):
461 self.name = name or ''
462 self.nodes = OrderedDict()
Monty Taylor7b19ba72017-05-24 07:42:54 -0500463 self.groups = OrderedDict()
James E. Blaira98340f2016-09-02 11:33:49 -0700464
James E. Blair1774dd52017-02-03 10:52:32 -0800465 def __ne__(self, other):
466 return not self.__eq__(other)
467
468 def __eq__(self, other):
469 if not isinstance(other, NodeSet):
470 return False
471 return (self.name == other.name and
472 self.nodes == other.nodes)
473
James E. Blaircbf43672017-01-04 14:33:41 -0800474 def copy(self):
475 n = NodeSet(self.name)
476 for name, node in self.nodes.items():
James E. Blair16d96a02017-06-08 11:32:56 -0700477 n.addNode(Node(node.name, node.label))
Monty Taylor7b19ba72017-05-24 07:42:54 -0500478 for name, group in self.groups.items():
479 n.addGroup(Group(group.name, group.nodes[:]))
James E. Blaircbf43672017-01-04 14:33:41 -0800480 return n
481
James E. Blaira98340f2016-09-02 11:33:49 -0700482 def addNode(self, node):
483 if node.name in self.nodes:
484 raise Exception("Duplicate node in %s" % (self,))
485 self.nodes[node.name] = node
486
James E. Blair0eaad552016-09-02 12:09:54 -0700487 def getNodes(self):
Clint Byruma4471d12017-05-10 20:57:40 -0400488 return list(self.nodes.values())
James E. Blair0eaad552016-09-02 12:09:54 -0700489
Monty Taylor7b19ba72017-05-24 07:42:54 -0500490 def addGroup(self, group):
491 if group.name in self.groups:
492 raise Exception("Duplicate group in %s" % (self,))
493 self.groups[group.name] = group
494
495 def getGroups(self):
496 return list(self.groups.values())
497
James E. Blaira98340f2016-09-02 11:33:49 -0700498 def __repr__(self):
499 if self.name:
500 name = self.name + ' '
501 else:
502 name = ''
Monty Taylor7b19ba72017-05-24 07:42:54 -0500503 return '<NodeSet %s%s%s>' % (name, self.nodes, self.groups)
James E. Blaira98340f2016-09-02 11:33:49 -0700504
Tristan Cacqueray82f864b2017-08-01 05:54:42 +0000505 def __len__(self):
506 return len(self.nodes)
507
James E. Blaira98340f2016-09-02 11:33:49 -0700508
James E. Blair34776ee2016-08-25 13:53:54 -0700509class NodeRequest(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700510 """A request for a set of nodes."""
511
James E. Blair8b2a1472017-02-19 15:33:55 -0800512 def __init__(self, requestor, build_set, job, nodeset):
513 self.requestor = requestor
James E. Blair34776ee2016-08-25 13:53:54 -0700514 self.build_set = build_set
515 self.job = job
James E. Blair0eaad552016-09-02 12:09:54 -0700516 self.nodeset = nodeset
James E. Blair803e94f2017-01-06 09:18:59 -0800517 self._state = STATE_REQUESTED
James E. Blairdce6cea2016-12-20 16:45:32 -0800518 self.state_time = time.time()
519 self.stat = None
520 self.uid = uuid4().hex
James E. Blaircbf43672017-01-04 14:33:41 -0800521 self.id = None
James E. Blaircbbce0d2017-05-19 07:28:29 -0700522 # Zuul internal flags (not stored in ZK so they are not
James E. Blaira38c28e2017-01-04 10:33:20 -0800523 # overwritten).
524 self.failed = False
James E. Blaircbbce0d2017-05-19 07:28:29 -0700525 self.canceled = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800526
527 @property
James E. Blair6ab79e02017-01-06 10:10:17 -0800528 def fulfilled(self):
529 return (self._state == STATE_FULFILLED) and not self.failed
530
531 @property
James E. Blairdce6cea2016-12-20 16:45:32 -0800532 def state(self):
533 return self._state
534
535 @state.setter
536 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800537 if value not in REQUEST_STATES:
538 raise TypeError("'%s' is not a valid state" % value)
James E. Blairdce6cea2016-12-20 16:45:32 -0800539 self._state = value
540 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700541
542 def __repr__(self):
James E. Blaircbf43672017-01-04 14:33:41 -0800543 return '<NodeRequest %s %s>' % (self.id, self.nodeset)
James E. Blair34776ee2016-08-25 13:53:54 -0700544
James E. Blairdce6cea2016-12-20 16:45:32 -0800545 def toDict(self):
546 d = {}
James E. Blair16d96a02017-06-08 11:32:56 -0700547 nodes = [n.label for n in self.nodeset.getNodes()]
James E. Blairdce6cea2016-12-20 16:45:32 -0800548 d['node_types'] = nodes
James E. Blair8b2a1472017-02-19 15:33:55 -0800549 d['requestor'] = self.requestor
James E. Blairdce6cea2016-12-20 16:45:32 -0800550 d['state'] = self.state
551 d['state_time'] = self.state_time
552 return d
553
554 def updateFromDict(self, data):
555 self._state = data['state']
556 self.state_time = data['state_time']
557
James E. Blair34776ee2016-08-25 13:53:54 -0700558
James E. Blair01f83b72017-03-15 13:03:40 -0700559class Secret(object):
560 """A collection of private data.
561
562 In configuration, Secrets are collections of private data in
563 key-value pair format. They are defined as top-level
564 configuration objects and then referenced by Jobs.
565
566 """
567
James E. Blair8525e2b2017-03-15 14:05:47 -0700568 def __init__(self, name, source_context):
James E. Blair01f83b72017-03-15 13:03:40 -0700569 self.name = name
James E. Blair8525e2b2017-03-15 14:05:47 -0700570 self.source_context = source_context
James E. Blair01f83b72017-03-15 13:03:40 -0700571 # The secret data may or may not be encrypted. This attribute
572 # is named 'secret_data' to make it easy to search for and
573 # spot where it is directly used.
574 self.secret_data = {}
575
576 def __ne__(self, other):
577 return not self.__eq__(other)
578
579 def __eq__(self, other):
580 if not isinstance(other, Secret):
581 return False
582 return (self.name == other.name and
James E. Blair8525e2b2017-03-15 14:05:47 -0700583 self.source_context == other.source_context and
James E. Blair01f83b72017-03-15 13:03:40 -0700584 self.secret_data == other.secret_data)
585
586 def __repr__(self):
587 return '<Secret %s>' % (self.name,)
588
James E. Blair18f86a32017-03-15 14:43:26 -0700589 def decrypt(self, private_key):
590 """Return a copy of this secret with any encrypted data decrypted.
591 Note that the original remains encrypted."""
592
593 r = copy.deepcopy(self)
594 decrypted_secret_data = {}
595 for k, v in r.secret_data.items():
596 if hasattr(v, 'decrypt'):
597 decrypted_secret_data[k] = v.decrypt(private_key)
598 else:
599 decrypted_secret_data[k] = v
600 r.secret_data = decrypted_secret_data
601 return r
602
James E. Blair01f83b72017-03-15 13:03:40 -0700603
James E. Blaircdab2032017-02-01 09:09:29 -0800604class SourceContext(object):
605 """A reference to the branch of a project in configuration.
606
607 Jobs and playbooks reference this to keep track of where they
608 originate."""
609
James E. Blair6f140c72017-03-03 10:32:07 -0800610 def __init__(self, project, branch, path, trusted):
James E. Blaircdab2032017-02-01 09:09:29 -0800611 self.project = project
612 self.branch = branch
James E. Blair6f140c72017-03-03 10:32:07 -0800613 self.path = path
Monty Taylore6562aa2017-02-20 07:37:39 -0500614 self.trusted = trusted
James E. Blaircdab2032017-02-01 09:09:29 -0800615
James E. Blair6f140c72017-03-03 10:32:07 -0800616 def __str__(self):
617 return '%s/%s@%s' % (self.project, self.path, self.branch)
618
James E. Blaircdab2032017-02-01 09:09:29 -0800619 def __repr__(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800620 return '<SourceContext %s trusted:%s>' % (str(self),
621 self.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800622
James E. Blaira7f51ca2017-02-07 16:01:26 -0800623 def __deepcopy__(self, memo):
624 return self.copy()
625
626 def copy(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800627 return self.__class__(self.project, self.branch, self.path,
628 self.trusted)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800629
James E. Blaircdab2032017-02-01 09:09:29 -0800630 def __ne__(self, other):
631 return not self.__eq__(other)
632
633 def __eq__(self, other):
634 if not isinstance(other, SourceContext):
635 return False
636 return (self.project == other.project and
637 self.branch == other.branch and
James E. Blair6f140c72017-03-03 10:32:07 -0800638 self.path == other.path and
Monty Taylore6562aa2017-02-20 07:37:39 -0500639 self.trusted == other.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800640
641
James E. Blair66b274e2017-01-31 14:47:52 -0800642class PlaybookContext(object):
James E. Blaircdab2032017-02-01 09:09:29 -0800643
James E. Blair66b274e2017-01-31 14:47:52 -0800644 """A reference to a playbook in the context of a project.
645
646 Jobs refer to objects of this class for their main, pre, and post
647 playbooks so that we can keep track of which repos and security
James E. Blair74a82cf2017-07-12 17:23:08 -0700648 contexts are needed in order to run them.
James E. Blair66b274e2017-01-31 14:47:52 -0800649
James E. Blair74a82cf2017-07-12 17:23:08 -0700650 We also keep a list of roles so that playbooks only run with the
651 roles which were defined at the point the playbook was defined.
652
653 """
654
James E. Blair892cca62017-08-09 11:36:58 -0700655 def __init__(self, source_context, path, roles, secrets):
James E. Blaircdab2032017-02-01 09:09:29 -0800656 self.source_context = source_context
James E. Blair66b274e2017-01-31 14:47:52 -0800657 self.path = path
James E. Blair74a82cf2017-07-12 17:23:08 -0700658 self.roles = roles
James E. Blair892cca62017-08-09 11:36:58 -0700659 self.secrets = secrets
James E. Blair66b274e2017-01-31 14:47:52 -0800660
661 def __repr__(self):
James E. Blaircdab2032017-02-01 09:09:29 -0800662 return '<PlaybookContext %s %s>' % (self.source_context,
663 self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800664
665 def __ne__(self, other):
666 return not self.__eq__(other)
667
668 def __eq__(self, other):
669 if not isinstance(other, PlaybookContext):
670 return False
James E. Blaircdab2032017-02-01 09:09:29 -0800671 return (self.source_context == other.source_context and
James E. Blair74a82cf2017-07-12 17:23:08 -0700672 self.path == other.path and
James E. Blair892cca62017-08-09 11:36:58 -0700673 self.roles == other.roles and
674 self.secrets == other.secrets)
James E. Blair66b274e2017-01-31 14:47:52 -0800675
676 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400677 # Render to a dict to use in passing json to the executor
James E. Blair892cca62017-08-09 11:36:58 -0700678 secrets = {}
679 for secret in self.secrets:
680 secret_data = copy.deepcopy(secret.secret_data)
681 secrets[secret.name] = secret_data
James E. Blair66b274e2017-01-31 14:47:52 -0800682 return dict(
James E. Blaircdab2032017-02-01 09:09:29 -0800683 connection=self.source_context.project.connection_name,
684 project=self.source_context.project.name,
685 branch=self.source_context.branch,
Monty Taylore6562aa2017-02-20 07:37:39 -0500686 trusted=self.source_context.trusted,
James E. Blair74a82cf2017-07-12 17:23:08 -0700687 roles=[r.toDict() for r in self.roles],
James E. Blair892cca62017-08-09 11:36:58 -0700688 secrets=secrets,
James E. Blaircdab2032017-02-01 09:09:29 -0800689 path=self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800690
691
Monty Taylorb934c1a2017-06-16 19:31:47 -0500692class Role(object, metaclass=abc.ABCMeta):
James E. Blair5ac93842017-01-20 06:47:34 -0800693 """A reference to an ansible role."""
694
695 def __init__(self, target_name):
696 self.target_name = target_name
697
698 @abc.abstractmethod
699 def __repr__(self):
700 pass
701
702 def __ne__(self, other):
703 return not self.__eq__(other)
704
705 @abc.abstractmethod
706 def __eq__(self, other):
707 if not isinstance(other, Role):
708 return False
709 return (self.target_name == other.target_name)
710
711 @abc.abstractmethod
712 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400713 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800714 return dict(target_name=self.target_name)
715
716
717class ZuulRole(Role):
718 """A reference to an ansible role in a Zuul project."""
719
James E. Blairbb94dfa2017-07-11 07:45:19 -0700720 def __init__(self, target_name, connection_name, project_name,
721 implicit=False):
James E. Blair5ac93842017-01-20 06:47:34 -0800722 super(ZuulRole, self).__init__(target_name)
723 self.connection_name = connection_name
724 self.project_name = project_name
James E. Blairbb94dfa2017-07-11 07:45:19 -0700725 self.implicit = implicit
James E. Blair5ac93842017-01-20 06:47:34 -0800726
727 def __repr__(self):
728 return '<ZuulRole %s %s>' % (self.project_name, self.target_name)
729
Clint Byrumaf7438f2017-05-10 17:26:57 -0400730 __hash__ = object.__hash__
731
James E. Blair5ac93842017-01-20 06:47:34 -0800732 def __eq__(self, other):
733 if not isinstance(other, ZuulRole):
734 return False
James E. Blairbb94dfa2017-07-11 07:45:19 -0700735 # Implicit is not consulted for equality so that we can handle
736 # implicit to explicit conversions.
James E. Blair5ac93842017-01-20 06:47:34 -0800737 return (super(ZuulRole, self).__eq__(other) and
James E. Blair1b27f6a2017-07-14 14:09:07 -0700738 self.connection_name == other.connection_name and
James E. Blair6563e4b2017-04-28 08:14:48 -0700739 self.project_name == other.project_name)
James E. Blair5ac93842017-01-20 06:47:34 -0800740
741 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400742 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800743 d = super(ZuulRole, self).toDict()
744 d['type'] = 'zuul'
745 d['connection'] = self.connection_name
746 d['project'] = self.project_name
James E. Blairbb94dfa2017-07-11 07:45:19 -0700747 d['implicit'] = self.implicit
James E. Blair5ac93842017-01-20 06:47:34 -0800748 return d
749
750
James E. Blairee743612012-05-29 14:49:32 -0700751class Job(object):
James E. Blair66b274e2017-01-31 14:47:52 -0800752
James E. Blaira7f51ca2017-02-07 16:01:26 -0800753 """A Job represents the defintion of actions to perform.
754
James E. Blaird4ade8c2017-02-19 15:25:46 -0800755 A Job is an abstract configuration concept. It describes what,
756 where, and under what circumstances something should be run
757 (contrast this with Build which is a concrete single execution of
758 a Job).
759
James E. Blaira7f51ca2017-02-07 16:01:26 -0800760 NB: Do not modify attributes of this class, set them directly
761 (e.g., "job.run = ..." rather than "job.run.append(...)").
762 """
Monty Taylora42a55b2016-07-29 07:53:33 -0700763
James E. Blairee743612012-05-29 14:49:32 -0700764 def __init__(self, name):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800765 # These attributes may override even the final form of a job
766 # in the context of a project-pipeline. They can not affect
767 # the execution of the job, but only whether the job is run
768 # and how it is reported.
769 self.context_attributes = dict(
770 voting=True,
771 hold_following_changes=False,
James E. Blair1774dd52017-02-03 10:52:32 -0800772 failure_message=None,
773 success_message=None,
774 failure_url=None,
775 success_url=None,
776 # Matchers. These are separate so they can be individually
777 # overidden.
778 branch_matcher=None,
779 file_matcher=None,
780 irrelevant_file_matcher=None, # skip-if
James E. Blaira7f51ca2017-02-07 16:01:26 -0800781 tags=frozenset(),
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200782 dependencies=frozenset(),
James E. Blair1774dd52017-02-03 10:52:32 -0800783 )
784
James E. Blaira7f51ca2017-02-07 16:01:26 -0800785 # These attributes affect how the job is actually run and more
786 # care must be taken when overriding them. If a job is
787 # declared "final", these may not be overriden in a
788 # project-pipeline.
789 self.execution_attributes = dict(
790 timeout=None,
James E. Blair490cf042017-02-24 23:07:21 -0500791 variables={},
James E. Blaira7f51ca2017-02-07 16:01:26 -0800792 nodeset=NodeSet(),
James E. Blaira7f51ca2017-02-07 16:01:26 -0800793 workspace=None,
794 pre_run=(),
795 post_run=(),
796 run=(),
797 implied_run=(),
Tobias Henkel9a0e1942017-03-20 16:16:02 +0100798 semaphore=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800799 attempts=3,
800 final=False,
James E. Blair5fc81922017-07-12 13:19:37 -0700801 roles=(),
James E. Blair912322f2017-05-23 13:11:25 -0700802 required_projects={},
James E. Blairb3f5db12017-03-17 12:57:39 -0700803 allowed_projects=None,
James E. Blair7391ecb2017-05-23 13:32:40 -0700804 override_branch=None,
James E. Blair8eb564a2017-08-10 09:21:41 -0700805 post_review=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800806 )
807
808 # These are generally internal attributes which are not
809 # accessible via configuration.
810 self.other_attributes = dict(
811 name=None,
812 source_context=None,
813 inheritance_path=(),
814 )
815
816 self.inheritable_attributes = {}
817 self.inheritable_attributes.update(self.context_attributes)
818 self.inheritable_attributes.update(self.execution_attributes)
819 self.attributes = {}
820 self.attributes.update(self.inheritable_attributes)
821 self.attributes.update(self.other_attributes)
822
James E. Blairee743612012-05-29 14:49:32 -0700823 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800824
James E. Blair66b274e2017-01-31 14:47:52 -0800825 def __ne__(self, other):
826 return not self.__eq__(other)
827
Paul Belangere22baea2016-11-03 16:59:27 -0400828 def __eq__(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800829 # Compare the name and all inheritable attributes to determine
830 # whether two jobs with the same name are identically
831 # configured. Useful upon reconfiguration.
832 if not isinstance(other, Job):
833 return False
834 if self.name != other.name:
835 return False
836 for k, v in self.attributes.items():
837 if getattr(self, k) != getattr(other, k):
838 return False
839 return True
James E. Blairee743612012-05-29 14:49:32 -0700840
Clint Byrumaf7438f2017-05-10 17:26:57 -0400841 __hash__ = object.__hash__
842
James E. Blairee743612012-05-29 14:49:32 -0700843 def __str__(self):
844 return self.name
845
846 def __repr__(self):
James E. Blair72ae9da2017-02-03 14:21:30 -0800847 return '<Job %s branches: %s source: %s>' % (self.name,
848 self.branch_matcher,
849 self.source_context)
James E. Blair83005782015-12-11 14:46:03 -0800850
James E. Blaira7f51ca2017-02-07 16:01:26 -0800851 def __getattr__(self, name):
852 v = self.__dict__.get(name)
853 if v is None:
854 return copy.deepcopy(self.attributes[name])
855 return v
856
857 def _get(self, name):
858 return self.__dict__.get(name)
859
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100860 def getSafeAttributes(self):
861 return Attributes(name=self.name)
862
James E. Blaira7f51ca2017-02-07 16:01:26 -0800863 def setRun(self):
864 if not self.run:
865 self.run = self.implied_run
866
James E. Blair5fc81922017-07-12 13:19:37 -0700867 def addRoles(self, roles):
James E. Blairbb94dfa2017-07-11 07:45:19 -0700868 newroles = []
869 # Start with a copy of the existing roles, but if any of them
870 # are implicit roles which are identified as explicit in the
871 # new roles list, replace them with the explicit version.
872 changed = False
873 for existing_role in self.roles:
874 if existing_role in roles:
875 new_role = roles[roles.index(existing_role)]
876 else:
877 new_role = None
878 if (new_role and
879 isinstance(new_role, ZuulRole) and
880 isinstance(existing_role, ZuulRole) and
881 existing_role.implicit and not new_role.implicit):
882 newroles.append(new_role)
883 changed = True
884 else:
885 newroles.append(existing_role)
886 # Now add the new roles.
James E. Blair4eec8282017-07-12 17:33:26 -0700887 for role in reversed(roles):
James E. Blair5fc81922017-07-12 13:19:37 -0700888 if role not in newroles:
James E. Blair4eec8282017-07-12 17:33:26 -0700889 newroles.insert(0, role)
James E. Blairbb94dfa2017-07-11 07:45:19 -0700890 changed = True
891 if changed:
892 self.roles = tuple(newroles)
James E. Blair5fc81922017-07-12 13:19:37 -0700893
James E. Blair490cf042017-02-24 23:07:21 -0500894 def updateVariables(self, other_vars):
895 v = self.variables
896 Job._deepUpdate(v, other_vars)
897 self.variables = v
898
James E. Blair912322f2017-05-23 13:11:25 -0700899 def updateProjects(self, other_projects):
900 required_projects = self.required_projects
901 Job._deepUpdate(required_projects, other_projects)
902 self.required_projects = required_projects
James E. Blair27f3dfc2017-05-23 13:07:28 -0700903
James E. Blair490cf042017-02-24 23:07:21 -0500904 @staticmethod
905 def _deepUpdate(a, b):
906 # Merge nested dictionaries if possible, otherwise, overwrite
907 # the value in 'a' with the value in 'b'.
908 for k, bv in b.items():
909 av = a.get(k)
910 if isinstance(av, dict) and isinstance(bv, dict):
911 Job._deepUpdate(av, bv)
912 else:
913 a[k] = bv
914
James E. Blaira7f51ca2017-02-07 16:01:26 -0800915 def inheritFrom(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800916 """Copy the inheritable attributes which have been set on the other
917 job to this job."""
James E. Blaira7f51ca2017-02-07 16:01:26 -0800918 if not isinstance(other, Job):
919 raise Exception("Job unable to inherit from %s" % (other,))
920
Tobias Henkel83167622017-06-30 19:45:03 +0200921 if other.final:
922 raise Exception("Unable to inherit from final job %s" %
923 (repr(other),))
924
James E. Blaira7f51ca2017-02-07 16:01:26 -0800925 # copy all attributes
926 for k in self.inheritable_attributes:
James E. Blair892cca62017-08-09 11:36:58 -0700927 if (other._get(k) is not None):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800928 setattr(self, k, copy.deepcopy(getattr(other, k)))
929
930 msg = 'inherit from %s' % (repr(other),)
931 self.inheritance_path = other.inheritance_path + (msg,)
932
933 def copy(self):
934 job = Job(self.name)
935 for k in self.attributes:
936 if self._get(k) is not None:
937 setattr(job, k, copy.deepcopy(self._get(k)))
938 return job
939
940 def applyVariant(self, other):
941 """Copy the attributes which have been set on the other job to this
942 job."""
James E. Blair83005782015-12-11 14:46:03 -0800943
944 if not isinstance(other, Job):
945 raise Exception("Job unable to inherit from %s" % (other,))
James E. Blaira7f51ca2017-02-07 16:01:26 -0800946
947 for k in self.execution_attributes:
948 if (other._get(k) is not None and
949 k not in set(['final'])):
950 if self.final:
951 raise Exception("Unable to modify final job %s attribute "
952 "%s=%s with variant %s" % (
953 repr(self), k, other._get(k),
954 repr(other)))
James E. Blair27f3dfc2017-05-23 13:07:28 -0700955 if k not in set(['pre_run', 'post_run', 'roles', 'variables',
James E. Blair912322f2017-05-23 13:11:25 -0700956 'required_projects']):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800957 setattr(self, k, copy.deepcopy(other._get(k)))
958
959 # Don't set final above so that we don't trip an error halfway
960 # through assignment.
961 if other.final != self.attributes['final']:
962 self.final = other.final
963
964 if other._get('pre_run') is not None:
965 self.pre_run = self.pre_run + other.pre_run
966 if other._get('post_run') is not None:
967 self.post_run = other.post_run + self.post_run
James E. Blair5ac93842017-01-20 06:47:34 -0800968 if other._get('roles') is not None:
James E. Blair5fc81922017-07-12 13:19:37 -0700969 self.addRoles(other.roles)
James E. Blair490cf042017-02-24 23:07:21 -0500970 if other._get('variables') is not None:
971 self.updateVariables(other.variables)
James E. Blair912322f2017-05-23 13:11:25 -0700972 if other._get('required_projects') is not None:
973 self.updateProjects(other.required_projects)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800974
975 for k in self.context_attributes:
976 if (other._get(k) is not None and
977 k not in set(['tags'])):
978 setattr(self, k, copy.deepcopy(other._get(k)))
979
980 if other._get('tags') is not None:
981 self.tags = self.tags.union(other.tags)
982
983 msg = 'apply variant %s' % (repr(other),)
984 self.inheritance_path = self.inheritance_path + (msg,)
James E. Blairee743612012-05-29 14:49:32 -0700985
James E. Blaire421a232012-07-25 16:59:21 -0700986 def changeMatches(self, change):
James E. Blair83005782015-12-11 14:46:03 -0800987 if self.branch_matcher and not self.branch_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800988 return False
989
James E. Blair83005782015-12-11 14:46:03 -0800990 if self.file_matcher and not self.file_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800991 return False
992
James E. Blair83005782015-12-11 14:46:03 -0800993 # NB: This is a negative match.
994 if (self.irrelevant_file_matcher and
995 self.irrelevant_file_matcher.matches(change)):
Maru Newby3fe5f852015-01-13 04:22:14 +0000996 return False
997
James E. Blair70c71582013-03-06 08:50:50 -0800998 return True
James E. Blaire5a847f2012-07-10 15:29:14 -0700999
James E. Blair1e8dd892012-05-30 09:15:05 -07001000
James E. Blair912322f2017-05-23 13:11:25 -07001001class JobProject(object):
James E. Blair27f3dfc2017-05-23 13:07:28 -07001002 """ A reference to a project from a job. """
1003
1004 def __init__(self, project_name, override_branch=None):
1005 self.project_name = project_name
1006 self.override_branch = override_branch
1007
1008
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001009class JobList(object):
1010 """ A list of jobs in a project's pipeline. """
Monty Taylora42a55b2016-07-29 07:53:33 -07001011
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001012 def __init__(self):
1013 self.jobs = OrderedDict() # job.name -> [job, ...]
James E. Blair12748b52017-02-07 17:17:53 -08001014
James E. Blairee743612012-05-29 14:49:32 -07001015 def addJob(self, job):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001016 if job.name in self.jobs:
1017 self.jobs[job.name].append(job)
1018 else:
1019 self.jobs[job.name] = [job]
James E. Blairee743612012-05-29 14:49:32 -07001020
James E. Blaira7f51ca2017-02-07 16:01:26 -08001021 def inheritFrom(self, other):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001022 for jobname, jobs in other.jobs.items():
1023 if jobname in self.jobs:
Jesse Keatingd1f434a2017-05-16 20:28:35 -07001024 self.jobs[jobname].extend(jobs)
James E. Blaira7f51ca2017-02-07 16:01:26 -08001025 else:
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001026 self.jobs[jobname] = jobs
1027
1028
1029class JobGraph(object):
1030 """ A JobGraph represents the dependency graph between Job."""
1031
1032 def __init__(self):
1033 self.jobs = OrderedDict() # job_name -> Job
1034 self._dependencies = {} # dependent_job_name -> set(parent_job_names)
1035
1036 def __repr__(self):
1037 return '<JobGraph %s>' % (self.jobs)
1038
1039 def addJob(self, job):
1040 # A graph must be created after the job list is frozen,
1041 # therefore we should only get one job with the same name.
1042 if job.name in self.jobs:
1043 raise Exception("Job %s already added" % (job.name,))
1044 self.jobs[job.name] = job
1045 # Append the dependency information
1046 self._dependencies.setdefault(job.name, set())
1047 try:
1048 for dependency in job.dependencies:
1049 # Make sure a circular dependency is never created
1050 ancestor_jobs = self._getParentJobNamesRecursively(
1051 dependency, soft=True)
1052 ancestor_jobs.add(dependency)
1053 if any((job.name == anc_job) for anc_job in ancestor_jobs):
1054 raise Exception("Dependency cycle detected in job %s" %
1055 (job.name,))
1056 self._dependencies[job.name].add(dependency)
1057 except Exception:
1058 del self.jobs[job.name]
1059 del self._dependencies[job.name]
1060 raise
1061
1062 def getJobs(self):
Clint Byruma4471d12017-05-10 20:57:40 -04001063 return list(self.jobs.values()) # Report in the order of layout cfg
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001064
1065 def _getDirectDependentJobs(self, parent_job):
1066 ret = set()
1067 for dependent_name, parent_names in self._dependencies.items():
1068 if parent_job in parent_names:
1069 ret.add(dependent_name)
1070 return ret
1071
1072 def getDependentJobsRecursively(self, parent_job):
1073 all_dependent_jobs = set()
1074 jobs_to_iterate = set([parent_job])
1075 while len(jobs_to_iterate) > 0:
1076 current_job = jobs_to_iterate.pop()
1077 current_dependent_jobs = self._getDirectDependentJobs(current_job)
1078 new_dependent_jobs = current_dependent_jobs - all_dependent_jobs
1079 jobs_to_iterate |= new_dependent_jobs
1080 all_dependent_jobs |= new_dependent_jobs
1081 return [self.jobs[name] for name in all_dependent_jobs]
1082
1083 def getParentJobsRecursively(self, dependent_job):
1084 return [self.jobs[name] for name in
1085 self._getParentJobNamesRecursively(dependent_job)]
1086
1087 def _getParentJobNamesRecursively(self, dependent_job, soft=False):
1088 all_parent_jobs = set()
1089 jobs_to_iterate = set([dependent_job])
1090 while len(jobs_to_iterate) > 0:
1091 current_job = jobs_to_iterate.pop()
1092 current_parent_jobs = self._dependencies.get(current_job)
1093 if current_parent_jobs is None:
1094 if soft:
1095 current_parent_jobs = set()
1096 else:
1097 raise Exception("Dependent job %s not found: " %
1098 (dependent_job,))
1099 new_parent_jobs = current_parent_jobs - all_parent_jobs
1100 jobs_to_iterate |= new_parent_jobs
1101 all_parent_jobs |= new_parent_jobs
1102 return all_parent_jobs
James E. Blairb97ed802015-12-21 15:55:35 -08001103
James E. Blair1e8dd892012-05-30 09:15:05 -07001104
James E. Blair4aea70c2012-07-26 14:23:24 -07001105class Build(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001106 """A Build is an instance of a single execution of a Job.
1107
1108 While a Job describes what to run, a Build describes an actual
1109 execution of that Job. Each build is associated with exactly one
1110 Job (related builds are grouped together in a BuildSet).
1111 """
Monty Taylora42a55b2016-07-29 07:53:33 -07001112
James E. Blair4aea70c2012-07-26 14:23:24 -07001113 def __init__(self, job, uuid):
1114 self.job = job
1115 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -07001116 self.url = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001117 self.result = None
James E. Blair196f61a2017-06-30 15:42:29 -07001118 self.result_data = {}
James E. Blair6f699732017-07-18 14:19:11 -07001119 self.error_detail = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001120 self.build_set = None
Paul Belanger174a8272017-03-14 13:20:10 -04001121 self.execute_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -08001122 self.start_time = None
1123 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -07001124 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -07001125 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -07001126 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -07001127 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -07001128 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +08001129 self.worker = Worker()
Timothy Chavezb2332082015-08-07 20:08:04 -05001130 self.node_labels = []
1131 self.node_name = None
James E. Blairee743612012-05-29 14:49:32 -07001132
1133 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +08001134 return ('<Build %s of %s on %s>' %
1135 (self.uuid, self.job.name, self.worker))
1136
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001137 def getSafeAttributes(self):
James E. Blair196f61a2017-06-30 15:42:29 -07001138 return Attributes(uuid=self.uuid,
1139 result=self.result,
James E. Blair6f699732017-07-18 14:19:11 -07001140 error_detail=self.error_detail,
James E. Blair196f61a2017-06-30 15:42:29 -07001141 result_data=self.result_data)
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001142
Joshua Heskethba8776a2014-01-12 14:35:40 +08001143
1144class Worker(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001145 """Information about the specific worker executing a Build."""
Joshua Heskethba8776a2014-01-12 14:35:40 +08001146 def __init__(self):
1147 self.name = "Unknown"
1148 self.hostname = None
Monty Taylor0dbe1592017-06-11 10:57:27 -05001149 self.log_port = None
Joshua Heskethba8776a2014-01-12 14:35:40 +08001150
1151 def updateFromData(self, data):
1152 """Update worker information if contained in the WORK_DATA response."""
1153 self.name = data.get('worker_name', self.name)
1154 self.hostname = data.get('worker_hostname', self.hostname)
Monty Taylor0dbe1592017-06-11 10:57:27 -05001155 self.log_port = data.get('worker_log_port', self.log_port)
Joshua Heskethba8776a2014-01-12 14:35:40 +08001156
1157 def __repr__(self):
1158 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -07001159
James E. Blair1e8dd892012-05-30 09:15:05 -07001160
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001161class RepoFiles(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001162 """RepoFiles holds config-file content for per-project job config.
1163
1164 When Zuul asks a merger to prepare a future multiple-repo state
1165 and collect Zuul configuration files so that we can dynamically
1166 load our configuration, this class provides cached access to that
1167 data for use by the Change which updated the config files and any
1168 changes that follow it in a ChangeQueue.
1169
1170 It is attached to a BuildSet since the content of Zuul
1171 configuration files can change with each new BuildSet.
1172 """
1173
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001174 def __init__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001175 self.connections = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001176
1177 def __repr__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001178 return '<RepoFiles %s>' % self.connections
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001179
1180 def setFiles(self, items):
James E. Blair2a535672017-04-27 12:03:15 -07001181 self.hostnames = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001182 for item in items:
James E. Blair2a535672017-04-27 12:03:15 -07001183 connection = self.connections.setdefault(
1184 item['connection'], {})
1185 project = connection.setdefault(item['project'], {})
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001186 branch = project.setdefault(item['branch'], {})
1187 branch.update(item['files'])
1188
James E. Blair2a535672017-04-27 12:03:15 -07001189 def getFile(self, connection_name, project_name, branch, fn):
1190 host = self.connections.get(connection_name, {})
1191 return host.get(project_name, {}).get(branch, {}).get(fn)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001192
1193
James E. Blair7e530ad2012-07-03 16:12:28 -07001194class BuildSet(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001195 """A collection of Builds for one specific potential future repository
1196 state.
Monty Taylora42a55b2016-07-29 07:53:33 -07001197
Paul Belanger174a8272017-03-14 13:20:10 -04001198 When Zuul executes Builds for a change, it creates a Build to
James E. Blaird4ade8c2017-02-19 15:25:46 -08001199 represent each execution of each job and a BuildSet to keep track
Paul Belanger174a8272017-03-14 13:20:10 -04001200 of all the Builds running for that Change. When Zuul re-executes
James E. Blaird4ade8c2017-02-19 15:25:46 -08001201 Builds for a Change with a different configuration, all of the
1202 running Builds in the BuildSet for that change are aborted, and a
1203 new BuildSet is created to hold the Builds for the Jobs being
1204 run with the new configuration.
1205
1206 A BuildSet also holds the UUID used to produce the Zuul Ref that
1207 builders check out.
1208
Monty Taylora42a55b2016-07-29 07:53:33 -07001209 """
James E. Blair4076e2b2014-01-28 12:42:20 -08001210 # Merge states:
1211 NEW = 1
1212 PENDING = 2
1213 COMPLETE = 3
1214
Antoine Musso9b229282014-08-18 23:45:43 +02001215 states_map = {
1216 1: 'NEW',
1217 2: 'PENDING',
1218 3: 'COMPLETE',
1219 }
1220
James E. Blairfee8d652013-06-07 08:57:52 -07001221 def __init__(self, item):
1222 self.item = item
James E. Blair7e530ad2012-07-03 16:12:28 -07001223 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -07001224 self.result = None
1225 self.next_build_set = None
1226 self.previous_build_set = None
Jamie Lennox3f16de52017-05-09 14:24:11 +10001227 self.uuid = None
James E. Blair81515ad2012-10-01 18:29:08 -07001228 self.commit = None
James E. Blair1960d682017-04-28 15:44:14 -07001229 self.dependent_items = None
1230 self.merger_items = None
James E. Blair973721f2012-08-15 10:19:43 -07001231 self.unable_to_merge = False
James E. Blaire53250c2017-03-01 14:34:36 -08001232 self.config_error = None # None or an error message string.
James E. Blair972e3c72013-08-29 12:04:55 -07001233 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -08001234 self.merge_state = self.NEW
James E. Blair0eaad552016-09-02 12:09:54 -07001235 self.nodesets = {} # job -> nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001236 self.node_requests = {} # job -> reqs
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001237 self.files = RepoFiles()
James E. Blair1960d682017-04-28 15:44:14 -07001238 self.repo_state = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001239 self.layout = None
Paul Belanger71d98172016-11-08 10:56:31 -05001240 self.tries = {}
James E. Blair7e530ad2012-07-03 16:12:28 -07001241
Jamie Lennox3f16de52017-05-09 14:24:11 +10001242 @property
1243 def ref(self):
1244 # NOTE(jamielennox): The concept of buildset ref is to be removed and a
1245 # buildset UUID identifier available instead. Currently the ref is
1246 # checked to see if the BuildSet has been configured.
1247 return 'Z' + self.uuid if self.uuid else None
1248
Antoine Musso9b229282014-08-18 23:45:43 +02001249 def __repr__(self):
1250 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
1251 self.item,
1252 len(self.builds),
1253 self.getStateName(self.merge_state))
1254
James E. Blair4886cc12012-07-18 15:39:41 -07001255 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -07001256 # The change isn't enqueued until after it's created
1257 # so we don't know what the other changes ahead will be
1258 # until jobs start.
James E. Blair1960d682017-04-28 15:44:14 -07001259 if self.dependent_items is None:
1260 items = []
James E. Blairfee8d652013-06-07 08:57:52 -07001261 next_item = self.item.item_ahead
1262 while next_item:
James E. Blair1960d682017-04-28 15:44:14 -07001263 items.append(next_item)
James E. Blairfee8d652013-06-07 08:57:52 -07001264 next_item = next_item.item_ahead
James E. Blair1960d682017-04-28 15:44:14 -07001265 self.dependent_items = items
Jamie Lennox3f16de52017-05-09 14:24:11 +10001266 if not self.uuid:
1267 self.uuid = uuid4().hex
James E. Blair1960d682017-04-28 15:44:14 -07001268 if self.merger_items is None:
1269 items = [self.item] + self.dependent_items
1270 items.reverse()
1271 self.merger_items = [i.makeMergerItem() for i in items]
James E. Blair4886cc12012-07-18 15:39:41 -07001272
Antoine Musso9b229282014-08-18 23:45:43 +02001273 def getStateName(self, state_num):
1274 return self.states_map.get(
1275 state_num, 'UNKNOWN (%s)' % state_num)
1276
James E. Blair4886cc12012-07-18 15:39:41 -07001277 def addBuild(self, build):
1278 self.builds[build.job.name] = build
Paul Belanger71d98172016-11-08 10:56:31 -05001279 if build.job.name not in self.tries:
James E. Blair5aed1112016-12-15 09:18:05 -08001280 self.tries[build.job.name] = 1
James E. Blair4886cc12012-07-18 15:39:41 -07001281 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -07001282
James E. Blair4a28a882013-08-23 15:17:33 -07001283 def removeBuild(self, build):
Paul Belanger71d98172016-11-08 10:56:31 -05001284 self.tries[build.job.name] += 1
James E. Blair4a28a882013-08-23 15:17:33 -07001285 del self.builds[build.job.name]
1286
James E. Blair7e530ad2012-07-03 16:12:28 -07001287 def getBuild(self, job_name):
1288 return self.builds.get(job_name)
1289
James E. Blair11700c32012-07-05 17:50:05 -07001290 def getBuilds(self):
Clint Byrum1d0c7d12017-05-10 19:40:53 -07001291 keys = list(self.builds.keys())
James E. Blair11700c32012-07-05 17:50:05 -07001292 keys.sort()
1293 return [self.builds.get(x) for x in keys]
1294
James E. Blair0eaad552016-09-02 12:09:54 -07001295 def getJobNodeSet(self, job_name):
1296 # Return None if not provisioned; empty NodeSet if no nodes
1297 # required
1298 return self.nodesets.get(job_name)
James E. Blair8d692392016-04-08 17:47:58 -07001299
James E. Blaire18d4602017-01-05 11:17:28 -08001300 def removeJobNodeSet(self, job_name):
1301 if job_name not in self.nodesets:
1302 raise Exception("No job set for %s" % (job_name))
1303 del self.nodesets[job_name]
1304
James E. Blair8d692392016-04-08 17:47:58 -07001305 def setJobNodeRequest(self, job_name, req):
1306 if job_name in self.node_requests:
1307 raise Exception("Prior node request for %s" % (job_name))
1308 self.node_requests[job_name] = req
1309
1310 def getJobNodeRequest(self, job_name):
1311 return self.node_requests.get(job_name)
1312
James E. Blair0eaad552016-09-02 12:09:54 -07001313 def jobNodeRequestComplete(self, job_name, req, nodeset):
1314 if job_name in self.nodesets:
James E. Blair8d692392016-04-08 17:47:58 -07001315 raise Exception("Prior node request for %s" % (job_name))
James E. Blair0eaad552016-09-02 12:09:54 -07001316 self.nodesets[job_name] = nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001317 del self.node_requests[job_name]
1318
Paul Belanger71d98172016-11-08 10:56:31 -05001319 def getTries(self, job_name):
Clint Byrum804073b2017-05-10 17:47:45 -04001320 return self.tries.get(job_name, 0)
Paul Belanger71d98172016-11-08 10:56:31 -05001321
James E. Blair0ffa0102017-03-30 13:11:33 -07001322 def getMergeMode(self):
James E. Blair1960d682017-04-28 15:44:14 -07001323 # We may be called before this build set has a shadow layout
1324 # (ie, we are called to perform the merge to create that
1325 # layout). It's possible that the change we are merging will
1326 # update the merge-mode for the project, but there's not much
1327 # we can do about that here. Instead, do the best we can by
1328 # using the nearest shadow layout to determine the merge mode,
1329 # or if that fails, the current live layout, or if that fails,
1330 # use the default: merge-resolve.
1331 item = self.item
1332 layout = None
1333 while item:
1334 layout = item.current_build_set.layout
1335 if layout:
1336 break
1337 item = item.item_ahead
1338 if not layout:
1339 layout = self.item.pipeline.layout
1340 if layout:
James E. Blair0ffa0102017-03-30 13:11:33 -07001341 project = self.item.change.project
James E. Blair1960d682017-04-28 15:44:14 -07001342 project_config = layout.project_configs.get(
James E. Blair0ffa0102017-03-30 13:11:33 -07001343 project.canonical_name)
1344 if project_config:
1345 return project_config.merge_mode
1346 return MERGER_MERGE_RESOLVE
Adam Gandelman8bd57102016-12-02 12:58:42 -08001347
Jamie Lennox3f16de52017-05-09 14:24:11 +10001348 def getSafeAttributes(self):
1349 return Attributes(uuid=self.uuid)
1350
James E. Blair7e530ad2012-07-03 16:12:28 -07001351
James E. Blairfee8d652013-06-07 08:57:52 -07001352class QueueItem(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001353 """Represents the position of a Change in a ChangeQueue.
1354
1355 All Changes are enqueued into ChangeQueue in a QueueItem. The QueueItem
1356 holds the current `BuildSet` as well as all previous `BuildSets` that were
1357 produced for this `QueueItem`.
1358 """
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001359 log = logging.getLogger("zuul.QueueItem")
James E. Blair32663402012-06-01 10:04:18 -07001360
James E. Blairbfb8e042014-12-30 17:01:44 -08001361 def __init__(self, queue, change):
1362 self.pipeline = queue.pipeline
1363 self.queue = queue
Monty Taylor55d0f562017-05-17 14:30:21 -05001364 self.change = change # a ref
James E. Blair7e530ad2012-07-03 16:12:28 -07001365 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -07001366 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -07001367 self.current_build_set = BuildSet(self)
1368 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -07001369 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -07001370 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -08001371 self.enqueue_time = None
1372 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -07001373 self.reported = False
Tobias Henkel9842bd72017-05-16 13:40:03 +02001374 self.reported_start = False
1375 self.quiet = False
James E. Blairbfb8e042014-12-30 17:01:44 -08001376 self.active = False # Whether an item is within an active window
1377 self.live = True # Whether an item is intended to be processed at all
James E. Blairc9455002017-09-06 09:22:19 -07001378 # TODO(jeblair): move job_graph to buildset
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001379 self.job_graph = None
James E. Blaire5a847f2012-07-10 15:29:14 -07001380
James E. Blair972e3c72013-08-29 12:04:55 -07001381 def __repr__(self):
1382 if self.pipeline:
1383 pipeline = self.pipeline.name
1384 else:
1385 pipeline = None
1386 return '<QueueItem 0x%x for %s in %s>' % (
1387 id(self), self.change, pipeline)
1388
James E. Blairee743612012-05-29 14:49:32 -07001389 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -07001390 old = self.current_build_set
1391 self.current_build_set.result = 'CANCELED'
1392 self.current_build_set = BuildSet(self)
1393 old.next_build_set = self.current_build_set
1394 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -07001395 self.build_sets.append(self.current_build_set)
James E. Blairc9455002017-09-06 09:22:19 -07001396 self.job_graph = None
James E. Blairee743612012-05-29 14:49:32 -07001397
1398 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -07001399 self.current_build_set.addBuild(build)
James E. Blair66eeebf2013-07-27 17:44:32 -07001400 build.pipeline = self.pipeline
James E. Blairee743612012-05-29 14:49:32 -07001401
James E. Blair4a28a882013-08-23 15:17:33 -07001402 def removeBuild(self, build):
1403 self.current_build_set.removeBuild(build)
1404
James E. Blairfee8d652013-06-07 08:57:52 -07001405 def setReportedResult(self, result):
1406 self.current_build_set.result = result
1407
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001408 def freezeJobGraph(self):
James E. Blair83005782015-12-11 14:46:03 -08001409 """Find or create actual matching jobs for this item's change and
1410 store the resulting job tree."""
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001411 layout = self.current_build_set.layout
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001412 job_graph = layout.createJobGraph(self)
1413 for job in job_graph.getJobs():
1414 # Ensure that each jobs's dependencies are fully
1415 # accessible. This will raise an exception if not.
1416 job_graph.getParentJobsRecursively(job.name)
1417 self.job_graph = job_graph
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001418
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001419 def hasJobGraph(self):
1420 """Returns True if the item has a job graph."""
1421 return self.job_graph is not None
James E. Blair83005782015-12-11 14:46:03 -08001422
1423 def getJobs(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001424 if not self.live or not self.job_graph:
James E. Blair83005782015-12-11 14:46:03 -08001425 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001426 return self.job_graph.getJobs()
1427
1428 def getJob(self, name):
1429 if not self.job_graph:
1430 return None
1431 return self.job_graph.jobs.get(name)
James E. Blair83005782015-12-11 14:46:03 -08001432
James E. Blairdbfd3282016-07-21 10:46:19 -07001433 def haveAllJobsStarted(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001434 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001435 return False
1436 for job in self.getJobs():
1437 build = self.current_build_set.getBuild(job.name)
1438 if not build or not build.start_time:
1439 return False
1440 return True
1441
1442 def areAllJobsComplete(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001443 if (self.current_build_set.config_error or
1444 self.current_build_set.unable_to_merge):
1445 return True
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001446 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001447 return False
1448 for job in self.getJobs():
1449 build = self.current_build_set.getBuild(job.name)
1450 if not build or not build.result:
1451 return False
1452 return True
1453
1454 def didAllJobsSucceed(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001455 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001456 return False
1457 for job in self.getJobs():
1458 if not job.voting:
1459 continue
1460 build = self.current_build_set.getBuild(job.name)
1461 if not build:
1462 return False
1463 if build.result != 'SUCCESS':
1464 return False
1465 return True
1466
1467 def didAnyJobFail(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001468 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001469 return False
1470 for job in self.getJobs():
1471 if not job.voting:
1472 continue
1473 build = self.current_build_set.getBuild(job.name)
1474 if build and build.result and (build.result != 'SUCCESS'):
1475 return True
1476 return False
1477
1478 def didMergerFail(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001479 return self.current_build_set.unable_to_merge
1480
1481 def getConfigError(self):
1482 return self.current_build_set.config_error
James E. Blairdbfd3282016-07-21 10:46:19 -07001483
James E. Blair0d3e83b2017-06-05 13:51:57 -07001484 def wasDequeuedNeedingChange(self):
1485 return self.dequeued_needing_change
1486
James E. Blairdbfd3282016-07-21 10:46:19 -07001487 def isHoldingFollowingChanges(self):
1488 if not self.live:
1489 return False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001490 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001491 return False
1492 for job in self.getJobs():
1493 if not job.hold_following_changes:
1494 continue
1495 build = self.current_build_set.getBuild(job.name)
1496 if not build:
1497 return True
1498 if build.result != 'SUCCESS':
1499 return True
1500
1501 if not self.item_ahead:
1502 return False
1503 return self.item_ahead.isHoldingFollowingChanges()
1504
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001505 def findJobsToRun(self, semaphore_handler):
James E. Blairdbfd3282016-07-21 10:46:19 -07001506 torun = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001507 if not self.live:
1508 return []
1509 if not self.job_graph:
1510 return []
James E. Blair791b5392016-08-03 11:25:56 -07001511 if self.item_ahead:
1512 # Only run jobs if any 'hold' jobs on the change ahead
1513 # have completed successfully.
1514 if self.item_ahead.isHoldingFollowingChanges():
1515 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001516
1517 successful_job_names = set()
1518 jobs_not_started = set()
1519 for job in self.job_graph.getJobs():
1520 build = self.current_build_set.getBuild(job.name)
1521 if build:
1522 if build.result == 'SUCCESS':
1523 successful_job_names.add(job.name)
1524 else:
1525 jobs_not_started.add(job)
1526
1527 # Attempt to request nodes for jobs in the order jobs appear
1528 # in configuration.
1529 for job in self.job_graph.getJobs():
1530 if job not in jobs_not_started:
1531 continue
1532 all_parent_jobs_successful = True
1533 for parent_job in self.job_graph.getParentJobsRecursively(
1534 job.name):
1535 if parent_job.name not in successful_job_names:
1536 all_parent_jobs_successful = False
1537 break
1538 if all_parent_jobs_successful:
1539 nodeset = self.current_build_set.getJobNodeSet(job.name)
1540 if nodeset is None:
1541 # The nodes for this job are not ready, skip
1542 # it for now.
James E. Blairdbfd3282016-07-21 10:46:19 -07001543 continue
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001544 if semaphore_handler.acquire(self, job):
1545 # If this job needs a semaphore, either acquire it or
1546 # make sure that we have it before running the job.
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001547 torun.append(job)
James E. Blairdbfd3282016-07-21 10:46:19 -07001548 return torun
1549
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001550 def findJobsToRequest(self):
James E. Blair6ab79e02017-01-06 10:10:17 -08001551 build_set = self.current_build_set
James E. Blairdbfd3282016-07-21 10:46:19 -07001552 toreq = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001553 if not self.live:
1554 return []
1555 if not self.job_graph:
1556 return []
James E. Blair6ab79e02017-01-06 10:10:17 -08001557 if self.item_ahead:
1558 if self.item_ahead.isHoldingFollowingChanges():
1559 return []
James E. Blairdbfd3282016-07-21 10:46:19 -07001560
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001561 successful_job_names = set()
1562 jobs_not_requested = set()
1563 for job in self.job_graph.getJobs():
1564 build = build_set.getBuild(job.name)
1565 if build and build.result == 'SUCCESS':
1566 successful_job_names.add(job.name)
1567 else:
1568 nodeset = build_set.getJobNodeSet(job.name)
1569 if nodeset is None:
1570 req = build_set.getJobNodeRequest(job.name)
1571 if req is None:
1572 jobs_not_requested.add(job)
1573
1574 # Attempt to request nodes for jobs in the order jobs appear
1575 # in configuration.
1576 for job in self.job_graph.getJobs():
1577 if job not in jobs_not_requested:
1578 continue
1579 all_parent_jobs_successful = True
1580 for parent_job in self.job_graph.getParentJobsRecursively(
1581 job.name):
1582 if parent_job.name not in successful_job_names:
1583 all_parent_jobs_successful = False
1584 break
1585 if all_parent_jobs_successful:
1586 toreq.append(job)
1587 return toreq
James E. Blairdbfd3282016-07-21 10:46:19 -07001588
1589 def setResult(self, build):
1590 if build.retry:
1591 self.removeBuild(build)
1592 elif build.result != 'SUCCESS':
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001593 for job in self.job_graph.getDependentJobsRecursively(
1594 build.job.name):
James E. Blairdbfd3282016-07-21 10:46:19 -07001595 fakebuild = Build(job, None)
1596 fakebuild.result = 'SKIPPED'
1597 self.addBuild(fakebuild)
1598
James E. Blair6ab79e02017-01-06 10:10:17 -08001599 def setNodeRequestFailure(self, job):
1600 fakebuild = Build(job, None)
1601 self.addBuild(fakebuild)
1602 fakebuild.result = 'NODE_FAILURE'
1603 self.setResult(fakebuild)
1604
James E. Blairdbfd3282016-07-21 10:46:19 -07001605 def setDequeuedNeedingChange(self):
1606 self.dequeued_needing_change = True
1607 self._setAllJobsSkipped()
1608
1609 def setUnableToMerge(self):
1610 self.current_build_set.unable_to_merge = True
1611 self._setAllJobsSkipped()
1612
James E. Blaire53250c2017-03-01 14:34:36 -08001613 def setConfigError(self, error):
1614 self.current_build_set.config_error = error
1615 self._setAllJobsSkipped()
1616
James E. Blairdbfd3282016-07-21 10:46:19 -07001617 def _setAllJobsSkipped(self):
1618 for job in self.getJobs():
1619 fakebuild = Build(job, None)
1620 fakebuild.result = 'SKIPPED'
1621 self.addBuild(fakebuild)
1622
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001623 def formatUrlPattern(self, url_pattern, job=None, build=None):
1624 url = None
1625 # Produce safe versions of objects which may be useful in
1626 # result formatting, but don't allow users to crawl through
1627 # the entire data structure where they might be able to access
1628 # secrets, etc.
1629 safe_change = self.change.getSafeAttributes()
1630 safe_pipeline = self.pipeline.getSafeAttributes()
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001631 safe_tenant = self.pipeline.layout.tenant.getSafeAttributes()
Jamie Lennox3f16de52017-05-09 14:24:11 +10001632 safe_buildset = self.current_build_set.getSafeAttributes()
K Jonathan Harkera7f390c2017-06-02 12:31:22 -07001633 safe_job = job.getSafeAttributes() if job else {}
1634 safe_build = build.getSafeAttributes() if build else {}
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001635 try:
1636 url = url_pattern.format(change=safe_change,
1637 pipeline=safe_pipeline,
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001638 tenant=safe_tenant,
Jamie Lennox3f16de52017-05-09 14:24:11 +10001639 buildset=safe_buildset,
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001640 job=safe_job,
1641 build=safe_build)
1642 except KeyError as e:
1643 self.log.error("Error while formatting url for job %s: unknown "
1644 "key %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001645 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001646 except AttributeError as e:
1647 self.log.error("Error while formatting url for job %s: unknown "
1648 "attribute %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001649 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001650 except Exception:
1651 self.log.exception("Error while formatting url for job %s with "
1652 "pattern %s:" % (job, url_pattern))
1653
1654 return url
1655
James E. Blair800e7ff2017-03-17 16:06:52 -07001656 def formatJobResult(self, job):
James E. Blairb7273ef2016-04-19 08:58:51 -07001657 build = self.current_build_set.getBuild(job.name)
1658 result = build.result
James E. Blair800e7ff2017-03-17 16:06:52 -07001659 pattern = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001660 if result == 'SUCCESS':
1661 if job.success_message:
1662 result = job.success_message
James E. Blaira5dba232016-08-08 15:53:24 -07001663 if job.success_url:
1664 pattern = job.success_url
Tobias Henkel077f2f32017-05-30 20:16:46 +02001665 else:
James E. Blairb7273ef2016-04-19 08:58:51 -07001666 if job.failure_message:
1667 result = job.failure_message
James E. Blaira5dba232016-08-08 15:53:24 -07001668 if job.failure_url:
1669 pattern = job.failure_url
James E. Blair88e79c02017-07-07 13:36:54 -07001670 url = None # The final URL
1671 default_url = build.result_data.get('zuul', {}).get('log_url')
James E. Blairb7273ef2016-04-19 08:58:51 -07001672 if pattern:
James E. Blair88e79c02017-07-07 13:36:54 -07001673 job_url = self.formatUrlPattern(pattern, job, build)
1674 else:
1675 job_url = None
1676 try:
1677 if job_url:
1678 u = urllib.parse.urlparse(job_url)
1679 if u.scheme:
1680 # The job success or failure url is absolute, so it's
1681 # our final url.
1682 url = job_url
1683 else:
1684 # We have a relative job url. Combine it with our
1685 # default url.
1686 if default_url:
1687 url = urllib.parse.urljoin(default_url, job_url)
1688 except Exception:
1689 self.log.exception("Error while parsing url for job %s:"
1690 % (job,))
James E. Blairb7273ef2016-04-19 08:58:51 -07001691 if not url:
James E. Blair88e79c02017-07-07 13:36:54 -07001692 url = default_url or build.url or job.name
James E. Blairb7273ef2016-04-19 08:58:51 -07001693 return (result, url)
1694
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001695 def formatJSON(self, websocket_url=None):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001696 ret = {}
1697 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -08001698 ret['live'] = self.live
Monty Taylor55d0f562017-05-17 14:30:21 -05001699 if hasattr(self.change, 'url') and self.change.url is not None:
1700 ret['url'] = self.change.url
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001701 else:
1702 ret['url'] = None
Monty Taylor55d0f562017-05-17 14:30:21 -05001703 ret['id'] = self.change._id()
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001704 if self.item_ahead:
1705 ret['item_ahead'] = self.item_ahead.change._id()
1706 else:
1707 ret['item_ahead'] = None
1708 ret['items_behind'] = [i.change._id() for i in self.items_behind]
1709 ret['failing_reasons'] = self.current_build_set.failing_reasons
1710 ret['zuul_ref'] = self.current_build_set.ref
Monty Taylor55d0f562017-05-17 14:30:21 -05001711 if self.change.project:
1712 ret['project'] = self.change.project.name
Ramy Asselin07cc33c2015-06-12 14:06:34 -07001713 else:
1714 # For cross-project dependencies with the depends-on
1715 # project not known to zuul, the project is None
1716 # Set it to a static value
1717 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001718 ret['enqueue_time'] = int(self.enqueue_time * 1000)
1719 ret['jobs'] = []
Monty Taylor55d0f562017-05-17 14:30:21 -05001720 if hasattr(self.change, 'owner'):
1721 ret['owner'] = self.change.owner
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001722 else:
1723 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001724 max_remaining = 0
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001725 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001726 now = time.time()
1727 build = self.current_build_set.getBuild(job.name)
1728 elapsed = None
1729 remaining = None
1730 result = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001731 build_url = None
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001732 finger_url = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001733 report_url = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001734 worker = None
1735 if build:
1736 result = build.result
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001737 finger_url = build.url
1738 # TODO(tobiash): add support for custom web root
1739 urlformat = 'static/stream.html?' \
1740 'uuid={build.uuid}&' \
1741 'logfile=console.log'
1742 if websocket_url:
1743 urlformat += '&websocket_url={websocket_url}'
1744 build_url = urlformat.format(
1745 build=build, websocket_url=websocket_url)
James E. Blair800e7ff2017-03-17 16:06:52 -07001746 (unused, report_url) = self.formatJobResult(job)
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001747 if build.start_time:
1748 if build.end_time:
1749 elapsed = int((build.end_time -
1750 build.start_time) * 1000)
1751 remaining = 0
1752 else:
1753 elapsed = int((now - build.start_time) * 1000)
1754 if build.estimated_time:
1755 remaining = max(
1756 int(build.estimated_time * 1000) - elapsed,
1757 0)
1758 worker = {
1759 'name': build.worker.name,
1760 'hostname': build.worker.hostname,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001761 }
1762 if remaining and remaining > max_remaining:
1763 max_remaining = remaining
1764
1765 ret['jobs'].append({
1766 'name': job.name,
Tobias Henkel65639f82017-07-10 10:25:42 +02001767 'dependencies': list(job.dependencies),
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001768 'elapsed_time': elapsed,
1769 'remaining_time': remaining,
James E. Blairb7273ef2016-04-19 08:58:51 -07001770 'url': build_url,
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001771 'finger_url': finger_url,
James E. Blairb7273ef2016-04-19 08:58:51 -07001772 'report_url': report_url,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001773 'result': result,
1774 'voting': job.voting,
1775 'uuid': build.uuid if build else None,
Paul Belanger174a8272017-03-14 13:20:10 -04001776 'execute_time': build.execute_time if build else None,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001777 'start_time': build.start_time if build else None,
1778 'end_time': build.end_time if build else None,
1779 'estimated_time': build.estimated_time if build else None,
1780 'pipeline': build.pipeline.name if build else None,
1781 'canceled': build.canceled if build else None,
1782 'retry': build.retry if build else None,
Timothy Chavezb2332082015-08-07 20:08:04 -05001783 'node_labels': build.node_labels if build else [],
1784 'node_name': build.node_name if build else None,
1785 'worker': worker,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001786 })
1787
James E. Blairdbfd3282016-07-21 10:46:19 -07001788 if self.haveAllJobsStarted():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001789 ret['remaining_time'] = max_remaining
1790 else:
1791 ret['remaining_time'] = None
1792 return ret
1793
1794 def formatStatus(self, indent=0, html=False):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001795 indent_str = ' ' * indent
1796 ret = ''
Monty Taylor55d0f562017-05-17 14:30:21 -05001797 if html and getattr(self.change, 'url', None) is not None:
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001798 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
1799 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001800 self.change.project.name,
1801 self.change.url,
1802 self.change._id())
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001803 else:
1804 ret += '%sProject %s change %s based on %s\n' % (
1805 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001806 self.change.project.name,
1807 self.change._id(),
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001808 self.item_ahead)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001809 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001810 build = self.current_build_set.getBuild(job.name)
1811 if build:
1812 result = build.result
1813 else:
1814 result = None
1815 job_name = job.name
1816 if not job.voting:
1817 voting = ' (non-voting)'
1818 else:
1819 voting = ''
1820 if html:
1821 if build:
1822 url = build.url
1823 else:
1824 url = None
1825 if url is not None:
1826 job_name = '<a href="%s">%s</a>' % (url, job_name)
1827 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
1828 ret += '\n'
1829 return ret
1830
James E. Blaira04b0792017-04-27 09:59:06 -07001831 def makeMergerItem(self):
1832 # Create a dictionary with all info about the item needed by
1833 # the merger.
1834 number = None
1835 patchset = None
1836 oldrev = None
1837 newrev = None
James E. Blair21037782017-07-19 11:56:55 -07001838 branch = None
James E. Blaira04b0792017-04-27 09:59:06 -07001839 if hasattr(self.change, 'number'):
1840 number = self.change.number
1841 patchset = self.change.patchset
James E. Blair21037782017-07-19 11:56:55 -07001842 if hasattr(self.change, 'newrev'):
James E. Blaira04b0792017-04-27 09:59:06 -07001843 oldrev = self.change.oldrev
1844 newrev = self.change.newrev
James E. Blair21037782017-07-19 11:56:55 -07001845 if hasattr(self.change, 'branch'):
1846 branch = self.change.branch
1847
James E. Blaira04b0792017-04-27 09:59:06 -07001848 source = self.change.project.source
1849 connection_name = source.connection.connection_name
James E. Blair2a535672017-04-27 12:03:15 -07001850 project = self.change.project
James E. Blaira04b0792017-04-27 09:59:06 -07001851
James E. Blair2a535672017-04-27 12:03:15 -07001852 return dict(project=project.name,
1853 connection=connection_name,
James E. Blaira04b0792017-04-27 09:59:06 -07001854 merge_mode=self.current_build_set.getMergeMode(),
James E. Blair247cab72017-07-20 16:52:36 -07001855 ref=self.change.ref,
James E. Blaira04b0792017-04-27 09:59:06 -07001856 branch=branch,
James E. Blair247cab72017-07-20 16:52:36 -07001857 buildset_uuid=self.current_build_set.uuid,
James E. Blaira04b0792017-04-27 09:59:06 -07001858 number=number,
1859 patchset=patchset,
1860 oldrev=oldrev,
1861 newrev=newrev,
1862 )
1863
James E. Blairfee8d652013-06-07 08:57:52 -07001864
Clint Byrumf8cc9902017-03-22 22:38:25 -07001865class Ref(object):
1866 """An existing state of a Project."""
James E. Blairfee8d652013-06-07 08:57:52 -07001867
1868 def __init__(self, project):
1869 self.project = project
Clint Byrumf8cc9902017-03-22 22:38:25 -07001870 self.ref = None
1871 self.oldrev = None
1872 self.newrev = None
Jesse Keating71a47ff2017-06-06 11:36:43 -07001873 self.files = []
1874
Clint Byrumf8cc9902017-03-22 22:38:25 -07001875 def _id(self):
1876 return self.newrev
1877
1878 def __repr__(self):
1879 rep = None
1880 if self.newrev == '0000000000000000000000000000000000000000':
James E. Blair21037782017-07-19 11:56:55 -07001881 rep = '<%s 0x%x deletes %s from %s' % (
1882 type(self).__name__,
1883 id(self), self.ref, self.oldrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001884 elif self.oldrev == '0000000000000000000000000000000000000000':
James E. Blair21037782017-07-19 11:56:55 -07001885 rep = '<%s 0x%x creates %s on %s>' % (
1886 type(self).__name__,
1887 id(self), self.ref, self.newrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001888 else:
1889 # Catch all
James E. Blair21037782017-07-19 11:56:55 -07001890 rep = '<%s 0x%x %s updated %s..%s>' % (
1891 type(self).__name__,
1892 id(self), self.ref, self.oldrev, self.newrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001893 return rep
1894
James E. Blairfee8d652013-06-07 08:57:52 -07001895 def equals(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001896 if (self.project == other.project
1897 and self.ref == other.ref
1898 and self.newrev == other.newrev):
1899 return True
1900 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001901
1902 def isUpdateOf(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001903 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001904
1905 def filterJobs(self, jobs):
1906 return filter(lambda job: job.changeMatches(self), jobs)
1907
1908 def getRelatedChanges(self):
1909 return set()
1910
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001911 def updatesConfig(self):
Tristan Cacqueray829e6172017-06-13 06:49:36 +00001912 if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files or \
1913 [True for fn in self.files if fn.startswith("zuul.d/") or
1914 fn.startswith(".zuul.d/")]:
Jesse Keating71a47ff2017-06-06 11:36:43 -07001915 return True
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001916 return False
1917
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001918 def getSafeAttributes(self):
1919 return Attributes(project=self.project,
1920 ref=self.ref,
1921 oldrev=self.oldrev,
1922 newrev=self.newrev)
1923
James E. Blair1e8dd892012-05-30 09:15:05 -07001924
James E. Blair21037782017-07-19 11:56:55 -07001925class Branch(Ref):
1926 """An existing branch state for a Project."""
1927 def __init__(self, project):
1928 super(Branch, self).__init__(project)
1929 self.branch = None
1930
1931
1932class Tag(Ref):
1933 """An existing tag state for a Project."""
1934 def __init__(self, project):
1935 super(Tag, self).__init__(project)
1936 self.tag = None
1937
1938
1939class Change(Branch):
Monty Taylora42a55b2016-07-29 07:53:33 -07001940 """A proposed new state for a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001941 def __init__(self, project):
1942 super(Change, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -07001943 self.number = None
1944 self.url = None
1945 self.patchset = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001946
James E. Blair6965a4b2014-12-16 17:19:04 -08001947 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -07001948 self.needed_by_changes = []
1949 self.is_current_patchset = True
1950 self.can_merge = False
1951 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -07001952 self.failed_to_merge = False
James E. Blair11041d22014-05-02 14:49:53 -07001953 self.open = None
1954 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001955 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001956
Jan Hruban3b415922016-02-03 13:10:22 +01001957 self.source_event = None
1958
James E. Blair4aea70c2012-07-26 14:23:24 -07001959 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -07001960 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -07001961
1962 def __repr__(self):
1963 return '<Change 0x%x %s>' % (id(self), self._id())
1964
1965 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +08001966 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -07001967 return True
1968 return False
1969
James E. Blair2fa50962013-01-30 21:50:41 -08001970 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -08001971 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -07001972 (hasattr(other, 'patchset') and
1973 self.patchset is not None and
1974 other.patchset is not None and
1975 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -08001976 return True
1977 return False
1978
James E. Blairfee8d652013-06-07 08:57:52 -07001979 def getRelatedChanges(self):
1980 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -08001981 for c in self.needs_changes:
1982 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -07001983 for c in self.needed_by_changes:
1984 related.add(c)
1985 related.update(c.getRelatedChanges())
1986 return related
James E. Blair4aea70c2012-07-26 14:23:24 -07001987
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001988 def getSafeAttributes(self):
1989 return Attributes(project=self.project,
1990 number=self.number,
1991 patchset=self.patchset)
1992
James E. Blair4aea70c2012-07-26 14:23:24 -07001993
James E. Blairee743612012-05-29 14:49:32 -07001994class TriggerEvent(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001995 """Incoming event from an external system."""
James E. Blairee743612012-05-29 14:49:32 -07001996 def __init__(self):
James E. Blairaad3ae22017-05-18 14:11:29 -07001997 # TODO(jeblair): further reduce this list
James E. Blairee743612012-05-29 14:49:32 -07001998 self.data = None
James E. Blair32663402012-06-01 10:04:18 -07001999 # common
James E. Blairee743612012-05-29 14:49:32 -07002000 self.type = None
Jesse Keating71a47ff2017-06-06 11:36:43 -07002001 self.branch_updated = False
James E. Blair72facdc2017-08-17 10:29:12 -07002002 self.branch_created = False
2003 self.branch_deleted = False
James E. Blair247cab72017-07-20 16:52:36 -07002004 self.ref = None
Paul Belangerbaca3132016-11-04 12:49:54 -04002005 # For management events (eg: enqueue / promote)
2006 self.tenant_name = None
James E. Blair6f284b42017-03-31 14:14:41 -07002007 self.project_hostname = None
James E. Blairee743612012-05-29 14:49:32 -07002008 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -07002009 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +01002010 # Representation of the user account that performed the event.
2011 self.account = None
James E. Blair32663402012-06-01 10:04:18 -07002012 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -07002013 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -07002014 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -07002015 self.patch_number = None
James E. Blairee743612012-05-29 14:49:32 -07002016 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07002017 self.comment = None
Jesse Keating5c05a9f2017-01-12 14:44:58 -08002018 self.state = None
James E. Blair32663402012-06-01 10:04:18 -07002019 # ref-updated
James E. Blair32663402012-06-01 10:04:18 -07002020 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07002021 self.newrev = None
James E. Blairad28e912013-11-27 10:43:22 -08002022 # For events that arrive with a destination pipeline (eg, from
2023 # an admin command, etc):
2024 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07002025
James E. Blair6f284b42017-03-31 14:14:41 -07002026 @property
2027 def canonical_project_name(self):
2028 return self.project_hostname + '/' + self.project_name
2029
Jan Hruban324ca5b2015-11-05 19:28:54 +01002030 def isPatchsetCreated(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01002031 return False
2032
2033 def isChangeAbandoned(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01002034 return False
2035
James E. Blair1e8dd892012-05-30 09:15:05 -07002036
James E. Blair9c17dbf2014-06-23 14:21:58 -07002037class BaseFilter(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002038 """Base Class for filtering which Changes and Events to process."""
James E. Blairaad3ae22017-05-18 14:11:29 -07002039 pass
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002040
James E. Blair9c17dbf2014-06-23 14:21:58 -07002041
2042class EventFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002043 """Allows a Pipeline to only respond to certain events."""
James E. Blairaad3ae22017-05-18 14:11:29 -07002044 def __init__(self, trigger):
2045 super(EventFilter, self).__init__()
James E. Blairc0dedf82014-08-06 09:37:52 -07002046 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07002047
James E. Blairaad3ae22017-05-18 14:11:29 -07002048 def matches(self, event, ref):
2049 # TODO(jeblair): consider removing ref argument
James E. Blairee743612012-05-29 14:49:32 -07002050 return True
James E. Blaireff88162013-07-01 12:44:14 -04002051
2052
James E. Blairaad3ae22017-05-18 14:11:29 -07002053class RefFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002054 """Allows a Manager to only enqueue Changes that meet certain criteria."""
Jesse Keating8c2eb572017-05-30 17:31:45 -07002055 def __init__(self, connection_name):
James E. Blairaad3ae22017-05-18 14:11:29 -07002056 super(RefFilter, self).__init__()
Jesse Keating8c2eb572017-05-30 17:31:45 -07002057 self.connection_name = connection_name
James E. Blair11041d22014-05-02 14:49:53 -07002058
2059 def matches(self, change):
James E. Blair11041d22014-05-02 14:49:53 -07002060 return True
2061
2062
James E. Blairb97ed802015-12-21 15:55:35 -08002063class ProjectPipelineConfig(object):
2064 # Represents a project cofiguration in the context of a pipeline
2065 def __init__(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002066 self.job_list = JobList()
James E. Blairb97ed802015-12-21 15:55:35 -08002067 self.queue_name = None
Adam Gandelman8bd57102016-12-02 12:58:42 -08002068 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08002069
2070
James E. Blair08d9b782017-06-29 14:22:48 -07002071class TenantProjectConfig(object):
2072 """A project in the context of a tenant.
2073
2074 A Project is globally unique in the system, however, when used in
2075 a tenant, some metadata about the project local to the tenant is
2076 stored in a TenantProjectConfig.
2077 """
2078
2079 def __init__(self, project):
2080 self.project = project
2081 self.load_classes = set()
James E. Blair6459db12017-06-29 14:57:20 -07002082 self.shadow_projects = set()
James E. Blair08d9b782017-06-29 14:22:48 -07002083
Tobias Henkeleca46202017-08-02 20:27:10 +02002084 # The tenant's default setting of exclude_unprotected_branches will
2085 # be overridden by this one if not None.
2086 self.exclude_unprotected_branches = None
2087
James E. Blair08d9b782017-06-29 14:22:48 -07002088
James E. Blairb97ed802015-12-21 15:55:35 -08002089class ProjectConfig(object):
2090 # Represents a project cofiguration
2091 def __init__(self, name):
2092 self.name = name
Adam Gandelman8bd57102016-12-02 12:58:42 -08002093 self.merge_mode = None
James E. Blair040b6502017-05-23 10:18:21 -07002094 self.default_branch = None
James E. Blairb97ed802015-12-21 15:55:35 -08002095 self.pipelines = {}
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002096 self.private_key_file = None
James E. Blairb97ed802015-12-21 15:55:35 -08002097
2098
James E. Blair97043882017-09-06 15:51:17 -07002099class ConfigItemNotListError(Exception):
2100 def __init__(self):
2101 message = textwrap.dedent("""\
2102 Configuration file is not a list. Each zuul.yaml configuration
2103 file must be a list of items, for example:
2104
2105 - job:
2106 name: foo
2107
2108 - project:
2109 name: bar
2110
2111 Ensure that every item starts with "- " so that it is parsed as a
2112 YAML list.
2113 """)
2114 super(ConfigItemNotListError, self).__init__(message)
2115
2116
2117class ConfigItemNotDictError(Exception):
2118 def __init__(self):
2119 message = textwrap.dedent("""\
2120 Configuration item is not a dictionary. Each zuul.yaml
2121 configuration file must be a list of dictionaries, for
2122 example:
2123
2124 - job:
2125 name: foo
2126
2127 - project:
2128 name: bar
2129
2130 Ensure that every item in the list is a dictionary with one
2131 key (in this example, 'job' and 'project').
2132 """)
2133 super(ConfigItemNotDictError, self).__init__(message)
2134
2135
2136class ConfigItemMultipleKeysError(Exception):
2137 def __init__(self):
2138 message = textwrap.dedent("""\
2139 Configuration item has more than one key. Each zuul.yaml
2140 configuration file must be a list of dictionaries with a
2141 single key, for example:
2142
2143 - job:
2144 name: foo
2145
2146 - project:
2147 name: bar
2148
2149 Ensure that every item in the list is a dictionary with only
2150 one key (in this example, 'job' and 'project'). This error
2151 may be caused by insufficient indentation of the keys under
2152 the configuration item ('name' in this example).
2153 """)
2154 super(ConfigItemMultipleKeysError, self).__init__(message)
2155
2156
2157class ConfigItemUnknownError(Exception):
2158 def __init__(self):
2159 message = textwrap.dedent("""\
2160 Configuration item not recognized. Each zuul.yaml
2161 configuration file must be a list of dictionaries, for
2162 example:
2163
2164 - job:
2165 name: foo
2166
2167 - project:
2168 name: bar
2169
2170 The dictionary keys must match one of the configuration item
2171 types recognized by zuul (for example, 'job' or 'project').
2172 """)
2173 super(ConfigItemUnknownError, self).__init__(message)
2174
2175
James E. Blaird8e778f2015-12-22 14:09:20 -08002176class UnparsedAbideConfig(object):
James E. Blair08d9b782017-06-29 14:22:48 -07002177
Monty Taylora42a55b2016-07-29 07:53:33 -07002178 """A collection of yaml lists that has not yet been parsed into objects.
2179
2180 An Abide is a collection of tenants.
2181 """
2182
James E. Blaird8e778f2015-12-22 14:09:20 -08002183 def __init__(self):
2184 self.tenants = []
2185
2186 def extend(self, conf):
2187 if isinstance(conf, UnparsedAbideConfig):
2188 self.tenants.extend(conf.tenants)
2189 return
2190
2191 if not isinstance(conf, list):
James E. Blair97043882017-09-06 15:51:17 -07002192 raise ConfigItemNotListError()
2193
James E. Blaird8e778f2015-12-22 14:09:20 -08002194 for item in conf:
2195 if not isinstance(item, dict):
James E. Blair97043882017-09-06 15:51:17 -07002196 raise ConfigItemNotDictError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002197 if len(item.keys()) > 1:
James E. Blair97043882017-09-06 15:51:17 -07002198 raise ConfigItemMultipleKeysError()
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002199 key, value = list(item.items())[0]
James E. Blaird8e778f2015-12-22 14:09:20 -08002200 if key == 'tenant':
2201 self.tenants.append(value)
2202 else:
James E. Blair97043882017-09-06 15:51:17 -07002203 raise ConfigItemUnknownError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002204
2205
2206class UnparsedTenantConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002207 """A collection of yaml lists that has not yet been parsed into objects."""
2208
James E. Blaird8e778f2015-12-22 14:09:20 -08002209 def __init__(self):
2210 self.pipelines = []
2211 self.jobs = []
2212 self.project_templates = []
James E. Blairff555742017-02-19 11:34:27 -08002213 self.projects = {}
James E. Blaira98340f2016-09-02 11:33:49 -07002214 self.nodesets = []
James E. Blair01f83b72017-03-15 13:03:40 -07002215 self.secrets = []
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002216 self.semaphores = []
James E. Blaird8e778f2015-12-22 14:09:20 -08002217
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002218 def copy(self):
2219 r = UnparsedTenantConfig()
2220 r.pipelines = copy.deepcopy(self.pipelines)
2221 r.jobs = copy.deepcopy(self.jobs)
2222 r.project_templates = copy.deepcopy(self.project_templates)
2223 r.projects = copy.deepcopy(self.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07002224 r.nodesets = copy.deepcopy(self.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002225 r.secrets = copy.deepcopy(self.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002226 r.semaphores = copy.deepcopy(self.semaphores)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002227 return r
2228
James E. Blairec7ff302017-03-04 07:31:32 -08002229 def extend(self, conf):
James E. Blaird8e778f2015-12-22 14:09:20 -08002230 if isinstance(conf, UnparsedTenantConfig):
2231 self.pipelines.extend(conf.pipelines)
2232 self.jobs.extend(conf.jobs)
2233 self.project_templates.extend(conf.project_templates)
James E. Blairff555742017-02-19 11:34:27 -08002234 for k, v in conf.projects.items():
2235 self.projects.setdefault(k, []).extend(v)
James E. Blaira98340f2016-09-02 11:33:49 -07002236 self.nodesets.extend(conf.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002237 self.secrets.extend(conf.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002238 self.semaphores.extend(conf.semaphores)
James E. Blaird8e778f2015-12-22 14:09:20 -08002239 return
2240
2241 if not isinstance(conf, list):
James E. Blair97043882017-09-06 15:51:17 -07002242 raise ConfigItemNotListError()
James E. Blaircdab2032017-02-01 09:09:29 -08002243
James E. Blaird8e778f2015-12-22 14:09:20 -08002244 for item in conf:
2245 if not isinstance(item, dict):
James E. Blair97043882017-09-06 15:51:17 -07002246 raise ConfigItemNotDictError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002247 if len(item.keys()) > 1:
James E. Blair97043882017-09-06 15:51:17 -07002248 raise ConfigItemMultipleKeysError()
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002249 key, value = list(item.items())[0]
James E. Blair66b274e2017-01-31 14:47:52 -08002250 if key == 'project':
James E. Blairff555742017-02-19 11:34:27 -08002251 name = value['name']
2252 self.projects.setdefault(name, []).append(value)
James E. Blair66b274e2017-01-31 14:47:52 -08002253 elif key == 'job':
James E. Blaird8e778f2015-12-22 14:09:20 -08002254 self.jobs.append(value)
2255 elif key == 'project-template':
2256 self.project_templates.append(value)
2257 elif key == 'pipeline':
2258 self.pipelines.append(value)
James E. Blaira98340f2016-09-02 11:33:49 -07002259 elif key == 'nodeset':
2260 self.nodesets.append(value)
James E. Blair01f83b72017-03-15 13:03:40 -07002261 elif key == 'secret':
2262 self.secrets.append(value)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002263 elif key == 'semaphore':
2264 self.semaphores.append(value)
James E. Blaird8e778f2015-12-22 14:09:20 -08002265 else:
James E. Blair97043882017-09-06 15:51:17 -07002266 raise ConfigItemUnknownError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002267
2268
James E. Blaireff88162013-07-01 12:44:14 -04002269class Layout(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002270 """Holds all of the Pipelines."""
2271
James E. Blair6459db12017-06-29 14:57:20 -07002272 def __init__(self, tenant):
2273 self.tenant = tenant
James E. Blairb97ed802015-12-21 15:55:35 -08002274 self.project_configs = {}
2275 self.project_templates = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07002276 self.pipelines = OrderedDict()
James E. Blair83005782015-12-11 14:46:03 -08002277 # This is a dictionary of name -> [jobs]. The first element
2278 # of the list is the first job added with that name. It is
2279 # the reference definition for a given job. Subsequent
2280 # elements are aspects of that job with different matchers
2281 # that override some attribute of the job. These aspects all
2282 # inherit from the reference definition.
James E. Blairfef88ec2016-11-30 08:38:27 -08002283 self.jobs = {'noop': [Job('noop')]}
James E. Blaira98340f2016-09-02 11:33:49 -07002284 self.nodesets = {}
James E. Blair01f83b72017-03-15 13:03:40 -07002285 self.secrets = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002286 self.semaphores = {}
James E. Blaireff88162013-07-01 12:44:14 -04002287
2288 def getJob(self, name):
2289 if name in self.jobs:
James E. Blair83005782015-12-11 14:46:03 -08002290 return self.jobs[name][0]
James E. Blairb97ed802015-12-21 15:55:35 -08002291 raise Exception("Job %s not defined" % (name,))
James E. Blair83005782015-12-11 14:46:03 -08002292
James E. Blair2bab6e72017-08-07 09:52:45 -07002293 def hasJob(self, name):
2294 return name in self.jobs
2295
James E. Blair83005782015-12-11 14:46:03 -08002296 def getJobs(self, name):
2297 return self.jobs.get(name, [])
2298
2299 def addJob(self, job):
James E. Blair4317e9f2016-07-15 10:05:47 -07002300 # We can have multiple variants of a job all with the same
2301 # name, but these variants must all be defined in the same repo.
James E. Blaircdab2032017-02-01 09:09:29 -08002302 prior_jobs = [j for j in self.getJobs(job.name) if
2303 j.source_context.project !=
2304 job.source_context.project]
James E. Blair6459db12017-06-29 14:57:20 -07002305 # Unless the repo is permitted to shadow another. If so, and
2306 # the job we are adding is from a repo that is permitted to
2307 # shadow the one with the older jobs, skip adding this job.
2308 job_project = job.source_context.project
2309 job_tpc = self.tenant.project_configs[job_project.canonical_name]
2310 skip_add = False
2311 for prior_job in prior_jobs[:]:
2312 prior_project = prior_job.source_context.project
2313 if prior_project in job_tpc.shadow_projects:
2314 prior_jobs.remove(prior_job)
2315 skip_add = True
2316
James E. Blair4317e9f2016-07-15 10:05:47 -07002317 if prior_jobs:
2318 raise Exception("Job %s in %s is not permitted to shadow "
James E. Blaircdab2032017-02-01 09:09:29 -08002319 "job %s in %s" % (
2320 job,
2321 job.source_context.project,
2322 prior_jobs[0],
2323 prior_jobs[0].source_context.project))
James E. Blair6459db12017-06-29 14:57:20 -07002324 if skip_add:
2325 return False
James E. Blair83005782015-12-11 14:46:03 -08002326 if job.name in self.jobs:
2327 self.jobs[job.name].append(job)
2328 else:
2329 self.jobs[job.name] = [job]
James E. Blair6459db12017-06-29 14:57:20 -07002330 return True
James E. Blair83005782015-12-11 14:46:03 -08002331
James E. Blaira98340f2016-09-02 11:33:49 -07002332 def addNodeSet(self, nodeset):
2333 if nodeset.name in self.nodesets:
2334 raise Exception("NodeSet %s already defined" % (nodeset.name,))
2335 self.nodesets[nodeset.name] = nodeset
2336
James E. Blair01f83b72017-03-15 13:03:40 -07002337 def addSecret(self, secret):
2338 if secret.name in self.secrets:
2339 raise Exception("Secret %s already defined" % (secret.name,))
2340 self.secrets[secret.name] = secret
2341
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002342 def addSemaphore(self, semaphore):
2343 if semaphore.name in self.semaphores:
2344 raise Exception("Semaphore %s already defined" % (semaphore.name,))
2345 self.semaphores[semaphore.name] = semaphore
2346
James E. Blair83005782015-12-11 14:46:03 -08002347 def addPipeline(self, pipeline):
2348 self.pipelines[pipeline.name] = pipeline
James E. Blair59fdbac2015-12-07 17:08:06 -08002349
James E. Blairb97ed802015-12-21 15:55:35 -08002350 def addProjectTemplate(self, project_template):
2351 self.project_templates[project_template.name] = project_template
2352
James E. Blairf59f3cf2017-02-19 14:50:26 -08002353 def addProjectConfig(self, project_config):
James E. Blairb97ed802015-12-21 15:55:35 -08002354 self.project_configs[project_config.name] = project_config
James E. Blairb97ed802015-12-21 15:55:35 -08002355
James E. Blaird2348362017-03-17 13:59:35 -07002356 def _createJobGraph(self, item, job_list, job_graph):
2357 change = item.change
2358 pipeline = item.pipeline
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002359 for jobname in job_list.jobs:
2360 # This is the final job we are constructing
James E. Blaira7f51ca2017-02-07 16:01:26 -08002361 frozen_job = None
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002362 # Whether the change matches any globally defined variant
James E. Blaira7f51ca2017-02-07 16:01:26 -08002363 matched = False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002364 for variant in self.getJobs(jobname):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002365 if variant.changeMatches(change):
James E. Blaira7f51ca2017-02-07 16:01:26 -08002366 if frozen_job is None:
2367 frozen_job = variant.copy()
2368 frozen_job.setRun()
2369 else:
2370 frozen_job.applyVariant(variant)
2371 matched = True
2372 if not matched:
James E. Blair6e85c2b2016-11-21 16:47:01 -08002373 # A change must match at least one defined job variant
2374 # (that is to say that it must match more than just
2375 # the job that is defined in the tree).
2376 continue
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002377 # Whether the change matches any of the project pipeline
2378 # variants
2379 matched = False
2380 for variant in job_list.jobs[jobname]:
2381 if variant.changeMatches(change):
2382 frozen_job.applyVariant(variant)
2383 matched = True
2384 if not matched:
2385 # A change must match at least one project pipeline
2386 # job variant.
2387 continue
James E. Blairb3f5db12017-03-17 12:57:39 -07002388 if (frozen_job.allowed_projects and
2389 change.project.name not in frozen_job.allowed_projects):
2390 raise Exception("Project %s is not allowed to run job %s" %
2391 (change.project.name, frozen_job.name))
James E. Blair8eb564a2017-08-10 09:21:41 -07002392 if ((not pipeline.post_review) and frozen_job.post_review):
2393 raise Exception("Pre-review pipeline %s does not allow "
2394 "post-review job %s" % (
James E. Blaird2348362017-03-17 13:59:35 -07002395 pipeline.name, frozen_job.name))
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002396 job_graph.addJob(frozen_job)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002397
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002398 def createJobGraph(self, item):
Paul Belanger15e3e202016-10-14 16:27:34 -04002399 # NOTE(pabelanger): It is possible for a foreign project not to have a
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002400 # configured pipeline, if so return an empty JobGraph.
James E. Blairc9455002017-09-06 09:22:19 -07002401 ret = JobGraph()
2402 ppc = self.getProjectPipelineConfig(item.change.project,
2403 item.pipeline)
2404 if ppc:
2405 self._createJobGraph(item, ppc.job_list, ret)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002406 return ret
2407
James E. Blairc9455002017-09-06 09:22:19 -07002408 def getProjectPipelineConfig(self, project, pipeline):
2409 project_config = self.project_configs.get(
2410 project.canonical_name, None)
2411 if not project_config:
2412 return None
2413 return project_config.pipelines.get(pipeline.name, None)
James E. Blair0d3e83b2017-06-05 13:51:57 -07002414
James E. Blair59fdbac2015-12-07 17:08:06 -08002415
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002416class Semaphore(object):
2417 def __init__(self, name, max=1):
2418 self.name = name
2419 self.max = int(max)
2420
2421
2422class SemaphoreHandler(object):
2423 log = logging.getLogger("zuul.SemaphoreHandler")
2424
2425 def __init__(self):
2426 self.semaphores = {}
2427
2428 def acquire(self, item, job):
2429 if not job.semaphore:
2430 return True
2431
2432 semaphore_key = job.semaphore
2433
2434 m = self.semaphores.get(semaphore_key)
2435 if not m:
2436 # The semaphore is not held, acquire it
2437 self._acquire(semaphore_key, item, job.name)
2438 return True
2439 if (item, job.name) in m:
2440 # This item already holds the semaphore
2441 return True
2442
2443 # semaphore is there, check max
2444 if len(m) < self._max_count(item, job.semaphore):
2445 self._acquire(semaphore_key, item, job.name)
2446 return True
2447
2448 return False
2449
2450 def release(self, item, job):
2451 if not job.semaphore:
2452 return
2453
2454 semaphore_key = job.semaphore
2455
2456 m = self.semaphores.get(semaphore_key)
2457 if not m:
2458 # The semaphore is not held, nothing to do
2459 self.log.error("Semaphore can not be released for %s "
2460 "because the semaphore is not held" %
2461 item)
2462 return
2463 if (item, job.name) in m:
2464 # This item is a holder of the semaphore
2465 self._release(semaphore_key, item, job.name)
2466 return
2467 self.log.error("Semaphore can not be released for %s "
2468 "which does not hold it" % item)
2469
2470 def _acquire(self, semaphore_key, item, job_name):
2471 self.log.debug("Semaphore acquire {semaphore}: job {job}, item {item}"
2472 .format(semaphore=semaphore_key,
2473 job=job_name,
2474 item=item))
2475 if semaphore_key not in self.semaphores:
2476 self.semaphores[semaphore_key] = []
2477 self.semaphores[semaphore_key].append((item, job_name))
2478
2479 def _release(self, semaphore_key, item, job_name):
2480 self.log.debug("Semaphore release {semaphore}: job {job}, item {item}"
2481 .format(semaphore=semaphore_key,
2482 job=job_name,
2483 item=item))
2484 sem_item = (item, job_name)
2485 if sem_item in self.semaphores[semaphore_key]:
2486 self.semaphores[semaphore_key].remove(sem_item)
2487
2488 # cleanup if there is no user of the semaphore anymore
2489 if len(self.semaphores[semaphore_key]) == 0:
2490 del self.semaphores[semaphore_key]
2491
2492 @staticmethod
2493 def _max_count(item, semaphore_name):
2494 if not item.current_build_set.layout:
2495 # This should not occur as the layout of the item must already be
2496 # built when acquiring or releasing a semaphore for a job.
2497 raise Exception("Item {} has no layout".format(item))
2498
2499 # find the right semaphore
2500 default_semaphore = Semaphore(semaphore_name, 1)
2501 semaphores = item.current_build_set.layout.semaphores
2502 return semaphores.get(semaphore_name, default_semaphore).max
2503
2504
James E. Blair59fdbac2015-12-07 17:08:06 -08002505class Tenant(object):
2506 def __init__(self, name):
2507 self.name = name
Tristan Cacqueray82f864b2017-08-01 05:54:42 +00002508 self.max_nodes_per_job = 5
Tristan Cacquerayc98bff72017-09-10 15:25:26 +00002509 self.max_job_timeout = 10800
Tobias Henkeleca46202017-08-02 20:27:10 +02002510 self.exclude_unprotected_branches = False
James E. Blair2bab6e72017-08-07 09:52:45 -07002511 self.default_base_job = None
James E. Blair59fdbac2015-12-07 17:08:06 -08002512 self.layout = None
James E. Blair646322f2017-01-27 15:50:34 -08002513 # The unparsed configuration from the main zuul config for
2514 # this tenant.
2515 self.unparsed_config = None
James E. Blair109da3f2017-04-04 14:39:43 -07002516 # The list of projects from which we will read full
James E. Blair8a395f92017-03-30 11:15:33 -07002517 # configuration.
James E. Blair109da3f2017-04-04 14:39:43 -07002518 self.config_projects = []
2519 # The unparsed config from those projects.
2520 self.config_projects_config = None
2521 # The list of projects from which we will read untrusted
2522 # in-repo configuration.
2523 self.untrusted_projects = []
2524 # The unparsed config from those projects.
2525 self.untrusted_projects_config = None
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002526 self.semaphore_handler = SemaphoreHandler()
James E. Blair08d9b782017-06-29 14:22:48 -07002527 # Metadata about projects for this tenant
2528 # canonical project name -> TenantProjectConfig
2529 self.project_configs = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002530
James E. Blairc2a54fd2017-03-29 15:19:26 -07002531 # A mapping of project names to projects. project_name ->
2532 # VALUE where VALUE is a further dictionary of
2533 # canonical_hostname -> Project.
2534 self.projects = {}
2535 self.canonical_hostnames = set()
2536
James E. Blair08d9b782017-06-29 14:22:48 -07002537 def _addProject(self, tpc):
James E. Blairc2a54fd2017-03-29 15:19:26 -07002538 """Add a project to the project index
2539
James E. Blair08d9b782017-06-29 14:22:48 -07002540 :arg TenantProjectConfig tpc: The TenantProjectConfig (with
2541 associated project) to add.
2542
James E. Blairc2a54fd2017-03-29 15:19:26 -07002543 """
James E. Blair08d9b782017-06-29 14:22:48 -07002544 project = tpc.project
James E. Blairc2a54fd2017-03-29 15:19:26 -07002545 self.canonical_hostnames.add(project.canonical_hostname)
2546 hostname_dict = self.projects.setdefault(project.name, {})
2547 if project.canonical_hostname in hostname_dict:
2548 raise Exception("Project %s is already in project index" %
2549 (project,))
2550 hostname_dict[project.canonical_hostname] = project
James E. Blair08d9b782017-06-29 14:22:48 -07002551 self.project_configs[project.canonical_name] = tpc
James E. Blairc2a54fd2017-03-29 15:19:26 -07002552
2553 def getProject(self, name):
2554 """Return a project given its name.
2555
2556 :arg str name: The name of the project. It may be fully
2557 qualified (E.g., "git.example.com/subpath/project") or may
2558 contain only the project name name may be supplied (E.g.,
2559 "subpath/project").
2560
2561 :returns: A tuple (trusted, project) or (None, None) if the
2562 project is not found or ambiguous. The "trusted" boolean
2563 indicates whether or not the project is trusted by this
2564 tenant.
2565 :rtype: (bool, Project)
2566
2567 """
2568 path = name.split('/', 1)
2569 if path[0] in self.canonical_hostnames:
2570 hostname = path[0]
2571 project_name = path[1]
2572 else:
2573 hostname = None
2574 project_name = name
2575 hostname_dict = self.projects.get(project_name)
2576 project = None
2577 if hostname_dict:
2578 if hostname:
2579 project = hostname_dict.get(hostname)
2580 else:
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002581 values = list(hostname_dict.values())
James E. Blairc2a54fd2017-03-29 15:19:26 -07002582 if len(values) == 1:
2583 project = values[0]
2584 else:
2585 raise Exception("Project name '%s' is ambiguous, "
2586 "please fully qualify the project "
2587 "with a hostname" % (name,))
2588 if project is None:
2589 return (None, None)
James E. Blair109da3f2017-04-04 14:39:43 -07002590 if project in self.config_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002591 return (True, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002592 if project in self.untrusted_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002593 return (False, project)
2594 # This should never happen:
2595 raise Exception("Project %s is neither trusted nor untrusted" %
2596 (project,))
2597
James E. Blair08d9b782017-06-29 14:22:48 -07002598 def addConfigProject(self, tpc):
2599 self.config_projects.append(tpc.project)
2600 self._addProject(tpc)
James E. Blair5ac93842017-01-20 06:47:34 -08002601
James E. Blair08d9b782017-06-29 14:22:48 -07002602 def addUntrustedProject(self, tpc):
2603 self.untrusted_projects.append(tpc.project)
2604 self._addProject(tpc)
James E. Blair5ac93842017-01-20 06:47:34 -08002605
Jamie Lennoxbf997c82017-05-10 09:58:40 +10002606 def getSafeAttributes(self):
2607 return Attributes(name=self.name)
2608
James E. Blair59fdbac2015-12-07 17:08:06 -08002609
2610class Abide(object):
2611 def __init__(self):
2612 self.tenants = OrderedDict()
James E. Blairce8a2132016-05-19 15:21:52 -07002613
2614
2615class JobTimeData(object):
2616 format = 'B10H10H10B'
2617 version = 0
2618
2619 def __init__(self, path):
2620 self.path = path
2621 self.success_times = [0 for x in range(10)]
2622 self.failure_times = [0 for x in range(10)]
2623 self.results = [0 for x in range(10)]
2624
2625 def load(self):
2626 if not os.path.exists(self.path):
2627 return
Clint Byruma4471d12017-05-10 20:57:40 -04002628 with open(self.path, 'rb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002629 data = struct.unpack(self.format, f.read())
2630 version = data[0]
2631 if version != self.version:
2632 raise Exception("Unkown data version")
2633 self.success_times = list(data[1:11])
2634 self.failure_times = list(data[11:21])
2635 self.results = list(data[21:32])
2636
2637 def save(self):
2638 tmpfile = self.path + '.tmp'
2639 data = [self.version]
2640 data.extend(self.success_times)
2641 data.extend(self.failure_times)
2642 data.extend(self.results)
2643 data = struct.pack(self.format, *data)
Clint Byruma4471d12017-05-10 20:57:40 -04002644 with open(tmpfile, 'wb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002645 f.write(data)
2646 os.rename(tmpfile, self.path)
2647
2648 def add(self, elapsed, result):
2649 elapsed = int(elapsed)
2650 if result == 'SUCCESS':
2651 self.success_times.append(elapsed)
2652 self.success_times.pop(0)
2653 result = 0
2654 else:
2655 self.failure_times.append(elapsed)
2656 self.failure_times.pop(0)
2657 result = 1
2658 self.results.append(result)
2659 self.results.pop(0)
2660
2661 def getEstimatedTime(self):
2662 times = [x for x in self.success_times if x]
2663 if times:
2664 return float(sum(times)) / len(times)
2665 return 0.0
2666
2667
2668class TimeDataBase(object):
2669 def __init__(self, root):
2670 self.root = root
James E. Blairce8a2132016-05-19 15:21:52 -07002671
James E. Blairae0f23c2017-09-13 10:55:15 -06002672 def _getTD(self, build):
2673 if hasattr(build.build_set.item.change, 'branch'):
2674 branch = build.build_set.item.change.branch
2675 else:
2676 branch = ''
2677
2678 dir_path = os.path.join(
2679 self.root,
2680 build.build_set.item.pipeline.layout.tenant.name,
2681 build.build_set.item.change.project.canonical_name,
2682 branch)
2683 if not os.path.exists(dir_path):
2684 os.makedirs(dir_path)
2685 path = os.path.join(dir_path, build.job.name)
2686
2687 td = JobTimeData(path)
2688 td.load()
James E. Blairce8a2132016-05-19 15:21:52 -07002689 return td
2690
2691 def getEstimatedTime(self, name):
2692 return self._getTD(name).getEstimatedTime()
2693
James E. Blairae0f23c2017-09-13 10:55:15 -06002694 def update(self, build, elapsed, result):
2695 td = self._getTD(build)
James E. Blairce8a2132016-05-19 15:21:52 -07002696 td.add(elapsed, result)
2697 td.save()