blob: 5f64c473f1f18f7ffe2325c28861cd7f2d7f3f98 [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
Monty Taylor6dc5bc12017-09-29 15:47:31 -050047PRIORITY_MAP = {
48 PRECEDENCE_NORMAL: 200,
49 PRECEDENCE_LOW: 300,
50 PRECEDENCE_HIGH: 100,
51}
52
James E. Blair803e94f2017-01-06 09:18:59 -080053# Request states
54STATE_REQUESTED = 'requested'
55STATE_PENDING = 'pending'
56STATE_FULFILLED = 'fulfilled'
57STATE_FAILED = 'failed'
58REQUEST_STATES = set([STATE_REQUESTED,
59 STATE_PENDING,
60 STATE_FULFILLED,
61 STATE_FAILED])
62
63# Node states
64STATE_BUILDING = 'building'
65STATE_TESTING = 'testing'
66STATE_READY = 'ready'
67STATE_IN_USE = 'in-use'
68STATE_USED = 'used'
69STATE_HOLD = 'hold'
70STATE_DELETING = 'deleting'
71NODE_STATES = set([STATE_BUILDING,
72 STATE_TESTING,
73 STATE_READY,
74 STATE_IN_USE,
75 STATE_USED,
76 STATE_HOLD,
77 STATE_DELETING])
78
James E. Blair1e8dd892012-05-30 09:15:05 -070079
Joshua Hesketh58419cb2017-02-24 13:09:22 -050080class Attributes(object):
81 """A class to hold attributes for string formatting."""
82
83 def __init__(self, **kw):
84 setattr(self, '__dict__', kw)
85
86
James E. Blair4aea70c2012-07-26 14:23:24 -070087class Pipeline(object):
James E. Blair6053de42017-04-05 11:27:11 -070088 """A configuration that ties together triggers, reporters and managers
Monty Taylor82dfd412016-07-29 12:01:28 -070089
90 Trigger
91 A description of which events should be processed
92
93 Manager
94 Responsible for enqueing and dequeing Changes
95
96 Reporter
97 Communicates success and failure results somewhere
Monty Taylora42a55b2016-07-29 07:53:33 -070098 """
James E. Blair83005782015-12-11 14:46:03 -080099 def __init__(self, name, layout):
James E. Blair4aea70c2012-07-26 14:23:24 -0700100 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800101 self.layout = layout
James E. Blair8dbd56a2012-12-22 10:55:10 -0800102 self.description = None
James E. Blair56370192013-01-14 15:47:28 -0800103 self.failure_message = None
Joshua Heskethb7179772014-01-30 23:30:46 +1100104 self.merge_failure_message = None
James E. Blair56370192013-01-14 15:47:28 -0800105 self.success_message = None
Joshua Hesketh3979e3e2014-03-04 11:21:10 +1100106 self.footer_message = None
James E. Blair60af7f42016-03-11 16:11:06 -0800107 self.start_message = None
James E. Blair8eb564a2017-08-10 09:21:41 -0700108 self.post_review = False
James E. Blair2fa50962013-01-30 21:50:41 -0800109 self.dequeue_on_new_patchset = True
James E. Blair17dd6772015-02-09 14:45:18 -0800110 self.ignore_dependencies = False
James E. Blair4aea70c2012-07-26 14:23:24 -0700111 self.manager = None
James E. Blaire0487072012-08-29 17:38:31 -0700112 self.queues = []
James E. Blair64ed6f22013-07-10 14:07:23 -0700113 self.precedence = PRECEDENCE_NORMAL
James E. Blair83005782015-12-11 14:46:03 -0800114 self.triggers = []
Joshua Hesketh352264b2015-08-11 23:42:08 +1000115 self.start_actions = []
116 self.success_actions = []
117 self.failure_actions = []
118 self.merge_failure_actions = []
119 self.disabled_actions = []
Joshua Hesketh89e829d2015-02-10 16:29:45 +1100120 self.disable_at = None
121 self._consecutive_failures = 0
122 self._disabled = False
Clark Boylan7603a372014-01-21 11:43:20 -0800123 self.window = None
124 self.window_floor = None
125 self.window_increase_type = None
126 self.window_increase_factor = None
127 self.window_decrease_type = None
128 self.window_decrease_factor = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700129
James E. Blair83005782015-12-11 14:46:03 -0800130 @property
131 def actions(self):
132 return (
133 self.start_actions +
134 self.success_actions +
135 self.failure_actions +
136 self.merge_failure_actions +
137 self.disabled_actions
138 )
139
James E. Blaird09c17a2012-08-07 09:23:14 -0700140 def __repr__(self):
141 return '<Pipeline %s>' % self.name
142
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100143 def getSafeAttributes(self):
144 return Attributes(name=self.name)
145
James E. Blair4aea70c2012-07-26 14:23:24 -0700146 def setManager(self, manager):
147 self.manager = manager
148
James E. Blaire0487072012-08-29 17:38:31 -0700149 def addQueue(self, queue):
150 self.queues.append(queue)
151
152 def getQueue(self, project):
153 for queue in self.queues:
154 if project in queue.projects:
155 return queue
156 return None
157
James E. Blairbfb8e042014-12-30 17:01:44 -0800158 def removeQueue(self, queue):
Tobias Henkel6b9390f2017-03-28 11:23:21 +0200159 if queue in self.queues:
160 self.queues.remove(queue)
James E. Blairbfb8e042014-12-30 17:01:44 -0800161
James E. Blaire0487072012-08-29 17:38:31 -0700162 def getChangesInQueue(self):
163 changes = []
164 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700165 changes.extend([x.change for x in shared_queue.queue])
James E. Blaire0487072012-08-29 17:38:31 -0700166 return changes
167
James E. Blairfee8d652013-06-07 08:57:52 -0700168 def getAllItems(self):
169 items = []
James E. Blaire0487072012-08-29 17:38:31 -0700170 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700171 items.extend(shared_queue.queue)
James E. Blairfee8d652013-06-07 08:57:52 -0700172 return items
James E. Blaire0487072012-08-29 17:38:31 -0700173
Tobias Henkelb4407fc2017-07-07 13:52:56 +0200174 def formatStatusJSON(self, websocket_url=None):
James E. Blair8dbd56a2012-12-22 10:55:10 -0800175 j_pipeline = dict(name=self.name,
176 description=self.description)
177 j_queues = []
178 j_pipeline['change_queues'] = j_queues
179 for queue in self.queues:
180 j_queue = dict(name=queue.name)
181 j_queues.append(j_queue)
182 j_queue['heads'] = []
Clark Boylanaf2476f2014-01-23 14:47:36 -0800183 j_queue['window'] = queue.window
James E. Blair972e3c72013-08-29 12:04:55 -0700184
185 j_changes = []
186 for e in queue.queue:
187 if not e.item_ahead:
188 if j_changes:
189 j_queue['heads'].append(j_changes)
190 j_changes = []
Tobias Henkelb4407fc2017-07-07 13:52:56 +0200191 j_changes.append(e.formatJSON(websocket_url))
James E. Blair972e3c72013-08-29 12:04:55 -0700192 if (len(j_changes) > 1 and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000193 (j_changes[-2]['remaining_time'] is not None) and
194 (j_changes[-1]['remaining_time'] is not None)):
James E. Blair972e3c72013-08-29 12:04:55 -0700195 j_changes[-1]['remaining_time'] = max(
196 j_changes[-2]['remaining_time'],
197 j_changes[-1]['remaining_time'])
198 if j_changes:
James E. Blair8dbd56a2012-12-22 10:55:10 -0800199 j_queue['heads'].append(j_changes)
200 return j_pipeline
201
James E. Blair4aea70c2012-07-26 14:23:24 -0700202
James E. Blairee743612012-05-29 14:49:32 -0700203class ChangeQueue(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700204 """A ChangeQueue contains Changes to be processed related projects.
205
Monty Taylor82dfd412016-07-29 12:01:28 -0700206 A Pipeline with a DependentPipelineManager has multiple parallel
207 ChangeQueues shared by different projects. For instance, there may a
208 ChangeQueue shared by interrelated projects foo and bar, and a second queue
209 for independent project baz.
Monty Taylora42a55b2016-07-29 07:53:33 -0700210
Monty Taylor82dfd412016-07-29 12:01:28 -0700211 A Pipeline with an IndependentPipelineManager puts every Change into its
212 own ChangeQueue
Monty Taylora42a55b2016-07-29 07:53:33 -0700213
214 The ChangeQueue Window is inspired by TCP windows and controlls how many
215 Changes in a given ChangeQueue will be considered active and ready to
216 be processed. If a Change succeeds, the Window is increased by
217 `window_increase_factor`. If a Change fails, the Window is decreased by
218 `window_decrease_factor`.
Jesse Keating78f544a2017-07-13 14:27:40 -0700219
220 A ChangeQueue may be a dynamically created queue, which may be removed
221 from a DependentPipelineManager once empty.
Monty Taylora42a55b2016-07-29 07:53:33 -0700222 """
James E. Blairbfb8e042014-12-30 17:01:44 -0800223 def __init__(self, pipeline, window=0, window_floor=1,
Clark Boylan7603a372014-01-21 11:43:20 -0800224 window_increase_type='linear', window_increase_factor=1,
James E. Blair0dcef7a2016-08-19 09:35:17 -0700225 window_decrease_type='exponential', window_decrease_factor=2,
Jesse Keating78f544a2017-07-13 14:27:40 -0700226 name=None, dynamic=False):
James E. Blair4aea70c2012-07-26 14:23:24 -0700227 self.pipeline = pipeline
James E. Blair0dcef7a2016-08-19 09:35:17 -0700228 if name:
229 self.name = name
230 else:
231 self.name = ''
James E. Blairee743612012-05-29 14:49:32 -0700232 self.projects = []
233 self._jobs = set()
234 self.queue = []
Clark Boylan7603a372014-01-21 11:43:20 -0800235 self.window = window
236 self.window_floor = window_floor
237 self.window_increase_type = window_increase_type
238 self.window_increase_factor = window_increase_factor
239 self.window_decrease_type = window_decrease_type
240 self.window_decrease_factor = window_decrease_factor
Jesse Keating78f544a2017-07-13 14:27:40 -0700241 self.dynamic = dynamic
James E. Blairee743612012-05-29 14:49:32 -0700242
James E. Blair9f9667e2012-06-12 17:51:08 -0700243 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700244 return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
James E. Blairee743612012-05-29 14:49:32 -0700245
246 def getJobs(self):
247 return self._jobs
248
249 def addProject(self, project):
250 if project not in self.projects:
251 self.projects.append(project)
James E. Blairc8a1e052014-02-25 09:29:26 -0800252
James E. Blair0dcef7a2016-08-19 09:35:17 -0700253 if not self.name:
254 self.name = project.name
James E. Blairee743612012-05-29 14:49:32 -0700255
256 def enqueueChange(self, change):
James E. Blairbfb8e042014-12-30 17:01:44 -0800257 item = QueueItem(self, change)
James E. Blaircdccd972013-07-01 12:10:22 -0700258 self.enqueueItem(item)
259 item.enqueue_time = time.time()
260 return item
261
262 def enqueueItem(self, item):
James E. Blair4a035d92014-01-23 13:10:48 -0800263 item.pipeline = self.pipeline
James E. Blairbfb8e042014-12-30 17:01:44 -0800264 item.queue = self
265 if self.queue:
James E. Blairfee8d652013-06-07 08:57:52 -0700266 item.item_ahead = self.queue[-1]
James E. Blair972e3c72013-08-29 12:04:55 -0700267 item.item_ahead.items_behind.append(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700268 self.queue.append(item)
James E. Blairee743612012-05-29 14:49:32 -0700269
James E. Blairfee8d652013-06-07 08:57:52 -0700270 def dequeueItem(self, item):
271 if item in self.queue:
272 self.queue.remove(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700273 if item.item_ahead:
James E. Blair972e3c72013-08-29 12:04:55 -0700274 item.item_ahead.items_behind.remove(item)
275 for item_behind in item.items_behind:
276 if item.item_ahead:
277 item.item_ahead.items_behind.append(item_behind)
278 item_behind.item_ahead = item.item_ahead
James E. Blairfee8d652013-06-07 08:57:52 -0700279 item.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700280 item.items_behind = []
James E. Blairfee8d652013-06-07 08:57:52 -0700281 item.dequeue_time = time.time()
James E. Blaire0487072012-08-29 17:38:31 -0700282
James E. Blair972e3c72013-08-29 12:04:55 -0700283 def moveItem(self, item, item_ahead):
James E. Blair972e3c72013-08-29 12:04:55 -0700284 if item.item_ahead == item_ahead:
285 return False
286 # Remove from current location
287 if item.item_ahead:
288 item.item_ahead.items_behind.remove(item)
289 for item_behind in item.items_behind:
290 if item.item_ahead:
291 item.item_ahead.items_behind.append(item_behind)
292 item_behind.item_ahead = item.item_ahead
293 # Add to new location
294 item.item_ahead = item_ahead
James E. Blair00451262013-09-20 11:40:17 -0700295 item.items_behind = []
James E. Blair972e3c72013-08-29 12:04:55 -0700296 if item.item_ahead:
297 item.item_ahead.items_behind.append(item)
298 return True
James E. Blairee743612012-05-29 14:49:32 -0700299
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800300 def isActionable(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800301 if self.window:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800302 return item in self.queue[:self.window]
Clark Boylan7603a372014-01-21 11:43:20 -0800303 else:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800304 return True
Clark Boylan7603a372014-01-21 11:43:20 -0800305
306 def increaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800307 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800308 if self.window_increase_type == 'linear':
309 self.window += self.window_increase_factor
310 elif self.window_increase_type == 'exponential':
311 self.window *= self.window_increase_factor
312
313 def decreaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800314 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800315 if self.window_decrease_type == 'linear':
316 self.window = max(
317 self.window_floor,
318 self.window - self.window_decrease_factor)
319 elif self.window_decrease_type == 'exponential':
320 self.window = max(
321 self.window_floor,
Morgan Fainbergfdae2252016-06-07 20:13:20 -0700322 int(self.window / self.window_decrease_factor))
James E. Blairee743612012-05-29 14:49:32 -0700323
James E. Blair1e8dd892012-05-30 09:15:05 -0700324
James E. Blair4aea70c2012-07-26 14:23:24 -0700325class Project(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700326 """A Project represents a git repository such as openstack/nova."""
327
James E. Blaircf440a22016-07-15 09:11:58 -0700328 # NOTE: Projects should only be instantiated via a Source object
329 # so that they are associated with and cached by their Connection.
330 # This makes a Project instance a unique identifier for a given
331 # project from a given source.
332
James E. Blair0a899752017-03-29 13:22:16 -0700333 def __init__(self, name, source, foreign=False):
James E. Blair4aea70c2012-07-26 14:23:24 -0700334 self.name = name
James E. Blair8a395f92017-03-30 11:15:33 -0700335 self.source = source
James E. Blair0a899752017-03-29 13:22:16 -0700336 self.connection_name = source.connection.connection_name
337 self.canonical_hostname = source.canonical_hostname
James E. Blairc2a54fd2017-03-29 15:19:26 -0700338 self.canonical_name = source.canonical_hostname + '/' + name
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000339 # foreign projects are those referenced in dependencies
340 # of layout projects, this should matter
341 # when deciding whether to enqueue their changes
James E. Blaircf440a22016-07-15 09:11:58 -0700342 # TODOv3 (jeblair): re-add support for foreign projects if needed
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000343 self.foreign = foreign
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700344 self.unparsed_config = None
James E. Blaire3162022017-02-20 16:47:27 -0500345 self.unparsed_branch_config = {} # branch -> UnparsedTenantConfig
James E. Blair4aea70c2012-07-26 14:23:24 -0700346
347 def __str__(self):
348 return self.name
349
350 def __repr__(self):
351 return '<Project %s>' % (self.name)
352
353
James E. Blair34776ee2016-08-25 13:53:54 -0700354class Node(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700355 """A single node for use by a job.
356
357 This may represent a request for a node, or an actual node
358 provided by Nodepool.
359 """
360
James E. Blair16d96a02017-06-08 11:32:56 -0700361 def __init__(self, name, label):
James E. Blair34776ee2016-08-25 13:53:54 -0700362 self.name = name
James E. Blair16d96a02017-06-08 11:32:56 -0700363 self.label = label
James E. Blaircbf43672017-01-04 14:33:41 -0800364 self.id = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800365 self.lock = None
David Shrewsburyffab07a2017-07-24 12:45:07 -0400366 self.hold_job = None
David Shrewsburyf9af9df2017-08-01 15:19:26 -0400367 self.comment = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800368 # Attributes from Nodepool
369 self._state = 'unknown'
370 self.state_time = time.time()
Monty Taylor56f61332017-04-11 05:38:12 -0500371 self.interface_ip = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800372 self.public_ipv4 = None
373 self.private_ipv4 = None
374 self.public_ipv6 = None
Tristan Cacqueray80954402017-05-28 00:33:55 +0000375 self.ssh_port = 22
James E. Blaircacdf2b2017-01-04 13:14:37 -0800376 self._keys = []
Paul Belanger30ba93a2017-03-16 16:28:10 -0400377 self.az = None
378 self.provider = None
379 self.region = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800380
381 @property
382 def state(self):
383 return self._state
384
385 @state.setter
386 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800387 if value not in NODE_STATES:
388 raise TypeError("'%s' is not a valid state" % value)
James E. Blaira38c28e2017-01-04 10:33:20 -0800389 self._state = value
390 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700391
392 def __repr__(self):
James E. Blair16d96a02017-06-08 11:32:56 -0700393 return '<Node %s %s:%s>' % (self.id, self.name, self.label)
James E. Blair34776ee2016-08-25 13:53:54 -0700394
James E. Blair0d952152017-02-07 17:14:44 -0800395 def __ne__(self, other):
396 return not self.__eq__(other)
397
398 def __eq__(self, other):
399 if not isinstance(other, Node):
400 return False
401 return (self.name == other.name and
James E. Blair16d96a02017-06-08 11:32:56 -0700402 self.label == other.label and
James E. Blair0d952152017-02-07 17:14:44 -0800403 self.id == other.id)
404
James E. Blaircacdf2b2017-01-04 13:14:37 -0800405 def toDict(self):
406 d = {}
407 d['state'] = self.state
David Shrewsburyffab07a2017-07-24 12:45:07 -0400408 d['hold_job'] = self.hold_job
David Shrewsburyf9af9df2017-08-01 15:19:26 -0400409 d['comment'] = self.comment
James E. Blaircacdf2b2017-01-04 13:14:37 -0800410 for k in self._keys:
411 d[k] = getattr(self, k)
412 return d
413
James E. Blaira38c28e2017-01-04 10:33:20 -0800414 def updateFromDict(self, data):
415 self._state = data['state']
James E. Blaircacdf2b2017-01-04 13:14:37 -0800416 keys = []
417 for k, v in data.items():
418 if k == 'state':
419 continue
420 keys.append(k)
421 setattr(self, k, v)
422 self._keys = keys
James E. Blaira38c28e2017-01-04 10:33:20 -0800423
James E. Blair34776ee2016-08-25 13:53:54 -0700424
Monty Taylor7b19ba72017-05-24 07:42:54 -0500425class Group(object):
426 """A logical group of nodes for use by a job.
427
428 A Group is a named set of node names that will be provided to
429 jobs in the inventory to describe logical units where some subset of tasks
430 run.
431 """
432
433 def __init__(self, name, nodes):
434 self.name = name
435 self.nodes = nodes
436
437 def __repr__(self):
438 return '<Group %s %s>' % (self.name, str(self.nodes))
439
440 def __ne__(self, other):
441 return not self.__eq__(other)
442
443 def __eq__(self, other):
444 if not isinstance(other, Group):
445 return False
446 return (self.name == other.name and
447 self.nodes == other.nodes)
448
449 def toDict(self):
450 return {
451 'name': self.name,
452 'nodes': self.nodes
453 }
454
455
James E. Blaira98340f2016-09-02 11:33:49 -0700456class NodeSet(object):
457 """A set of nodes.
458
459 In configuration, NodeSets are attributes of Jobs indicating that
460 a Job requires nodes matching this description.
461
462 They may appear as top-level configuration objects and be named,
463 or they may appears anonymously in in-line job definitions.
464 """
465
466 def __init__(self, name=None):
467 self.name = name or ''
468 self.nodes = OrderedDict()
Monty Taylor7b19ba72017-05-24 07:42:54 -0500469 self.groups = OrderedDict()
James E. Blaira98340f2016-09-02 11:33:49 -0700470
James E. Blair1774dd52017-02-03 10:52:32 -0800471 def __ne__(self, other):
472 return not self.__eq__(other)
473
474 def __eq__(self, other):
475 if not isinstance(other, NodeSet):
476 return False
477 return (self.name == other.name and
478 self.nodes == other.nodes)
479
James E. Blaircbf43672017-01-04 14:33:41 -0800480 def copy(self):
481 n = NodeSet(self.name)
482 for name, node in self.nodes.items():
James E. Blair16d96a02017-06-08 11:32:56 -0700483 n.addNode(Node(node.name, node.label))
Monty Taylor7b19ba72017-05-24 07:42:54 -0500484 for name, group in self.groups.items():
485 n.addGroup(Group(group.name, group.nodes[:]))
James E. Blaircbf43672017-01-04 14:33:41 -0800486 return n
487
James E. Blaira98340f2016-09-02 11:33:49 -0700488 def addNode(self, node):
489 if node.name in self.nodes:
490 raise Exception("Duplicate node in %s" % (self,))
491 self.nodes[node.name] = node
492
James E. Blair0eaad552016-09-02 12:09:54 -0700493 def getNodes(self):
Clint Byruma4471d12017-05-10 20:57:40 -0400494 return list(self.nodes.values())
James E. Blair0eaad552016-09-02 12:09:54 -0700495
Monty Taylor7b19ba72017-05-24 07:42:54 -0500496 def addGroup(self, group):
497 if group.name in self.groups:
498 raise Exception("Duplicate group in %s" % (self,))
499 self.groups[group.name] = group
500
501 def getGroups(self):
502 return list(self.groups.values())
503
James E. Blaira98340f2016-09-02 11:33:49 -0700504 def __repr__(self):
505 if self.name:
506 name = self.name + ' '
507 else:
508 name = ''
Monty Taylor7b19ba72017-05-24 07:42:54 -0500509 return '<NodeSet %s%s%s>' % (name, self.nodes, self.groups)
James E. Blaira98340f2016-09-02 11:33:49 -0700510
Tristan Cacqueray82f864b2017-08-01 05:54:42 +0000511 def __len__(self):
512 return len(self.nodes)
513
James E. Blaira98340f2016-09-02 11:33:49 -0700514
James E. Blair34776ee2016-08-25 13:53:54 -0700515class NodeRequest(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700516 """A request for a set of nodes."""
517
James E. Blair8b2a1472017-02-19 15:33:55 -0800518 def __init__(self, requestor, build_set, job, nodeset):
519 self.requestor = requestor
James E. Blair34776ee2016-08-25 13:53:54 -0700520 self.build_set = build_set
521 self.job = job
James E. Blair0eaad552016-09-02 12:09:54 -0700522 self.nodeset = nodeset
James E. Blair803e94f2017-01-06 09:18:59 -0800523 self._state = STATE_REQUESTED
James E. Blairdce6cea2016-12-20 16:45:32 -0800524 self.state_time = time.time()
525 self.stat = None
526 self.uid = uuid4().hex
James E. Blaircbf43672017-01-04 14:33:41 -0800527 self.id = None
James E. Blaircbbce0d2017-05-19 07:28:29 -0700528 # Zuul internal flags (not stored in ZK so they are not
James E. Blaira38c28e2017-01-04 10:33:20 -0800529 # overwritten).
530 self.failed = False
James E. Blaircbbce0d2017-05-19 07:28:29 -0700531 self.canceled = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800532
533 @property
Monty Taylor6dc5bc12017-09-29 15:47:31 -0500534 def priority(self):
535 return PRIORITY_MAP[self.build_set.item.pipeline.precedence]
536
537 @property
James E. Blair6ab79e02017-01-06 10:10:17 -0800538 def fulfilled(self):
539 return (self._state == STATE_FULFILLED) and not self.failed
540
541 @property
James E. Blairdce6cea2016-12-20 16:45:32 -0800542 def state(self):
543 return self._state
544
545 @state.setter
546 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800547 if value not in REQUEST_STATES:
548 raise TypeError("'%s' is not a valid state" % value)
James E. Blairdce6cea2016-12-20 16:45:32 -0800549 self._state = value
550 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700551
552 def __repr__(self):
James E. Blaircbf43672017-01-04 14:33:41 -0800553 return '<NodeRequest %s %s>' % (self.id, self.nodeset)
James E. Blair34776ee2016-08-25 13:53:54 -0700554
James E. Blairdce6cea2016-12-20 16:45:32 -0800555 def toDict(self):
556 d = {}
James E. Blair16d96a02017-06-08 11:32:56 -0700557 nodes = [n.label for n in self.nodeset.getNodes()]
James E. Blairdce6cea2016-12-20 16:45:32 -0800558 d['node_types'] = nodes
James E. Blair8b2a1472017-02-19 15:33:55 -0800559 d['requestor'] = self.requestor
James E. Blairdce6cea2016-12-20 16:45:32 -0800560 d['state'] = self.state
561 d['state_time'] = self.state_time
562 return d
563
564 def updateFromDict(self, data):
565 self._state = data['state']
566 self.state_time = data['state_time']
567
James E. Blair34776ee2016-08-25 13:53:54 -0700568
James E. Blair01f83b72017-03-15 13:03:40 -0700569class Secret(object):
570 """A collection of private data.
571
572 In configuration, Secrets are collections of private data in
573 key-value pair format. They are defined as top-level
574 configuration objects and then referenced by Jobs.
575
576 """
577
James E. Blair8525e2b2017-03-15 14:05:47 -0700578 def __init__(self, name, source_context):
James E. Blair01f83b72017-03-15 13:03:40 -0700579 self.name = name
James E. Blair8525e2b2017-03-15 14:05:47 -0700580 self.source_context = source_context
James E. Blair01f83b72017-03-15 13:03:40 -0700581 # The secret data may or may not be encrypted. This attribute
582 # is named 'secret_data' to make it easy to search for and
583 # spot where it is directly used.
584 self.secret_data = {}
585
586 def __ne__(self, other):
587 return not self.__eq__(other)
588
589 def __eq__(self, other):
590 if not isinstance(other, Secret):
591 return False
592 return (self.name == other.name and
James E. Blair8525e2b2017-03-15 14:05:47 -0700593 self.source_context == other.source_context and
James E. Blair01f83b72017-03-15 13:03:40 -0700594 self.secret_data == other.secret_data)
595
596 def __repr__(self):
597 return '<Secret %s>' % (self.name,)
598
James E. Blair18f86a32017-03-15 14:43:26 -0700599 def decrypt(self, private_key):
600 """Return a copy of this secret with any encrypted data decrypted.
601 Note that the original remains encrypted."""
602
603 r = copy.deepcopy(self)
604 decrypted_secret_data = {}
605 for k, v in r.secret_data.items():
606 if hasattr(v, 'decrypt'):
607 decrypted_secret_data[k] = v.decrypt(private_key)
608 else:
609 decrypted_secret_data[k] = v
610 r.secret_data = decrypted_secret_data
611 return r
612
James E. Blair01f83b72017-03-15 13:03:40 -0700613
James E. Blaircdab2032017-02-01 09:09:29 -0800614class SourceContext(object):
615 """A reference to the branch of a project in configuration.
616
617 Jobs and playbooks reference this to keep track of where they
618 originate."""
619
James E. Blair6f140c72017-03-03 10:32:07 -0800620 def __init__(self, project, branch, path, trusted):
James E. Blaircdab2032017-02-01 09:09:29 -0800621 self.project = project
622 self.branch = branch
James E. Blair6f140c72017-03-03 10:32:07 -0800623 self.path = path
Monty Taylore6562aa2017-02-20 07:37:39 -0500624 self.trusted = trusted
James E. Blaircdab2032017-02-01 09:09:29 -0800625
James E. Blair6f140c72017-03-03 10:32:07 -0800626 def __str__(self):
627 return '%s/%s@%s' % (self.project, self.path, self.branch)
628
James E. Blaircdab2032017-02-01 09:09:29 -0800629 def __repr__(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800630 return '<SourceContext %s trusted:%s>' % (str(self),
631 self.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800632
James E. Blaira7f51ca2017-02-07 16:01:26 -0800633 def __deepcopy__(self, memo):
634 return self.copy()
635
636 def copy(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800637 return self.__class__(self.project, self.branch, self.path,
638 self.trusted)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800639
Tristan Cacqueraye50af2e2017-09-19 14:18:28 +0000640 def isSameProject(self, other):
641 if not isinstance(other, SourceContext):
642 return False
643 return (self.project == other.project and
644 self.branch == other.branch and
645 self.trusted == other.trusted)
646
James E. Blaircdab2032017-02-01 09:09:29 -0800647 def __ne__(self, other):
648 return not self.__eq__(other)
649
650 def __eq__(self, other):
651 if not isinstance(other, SourceContext):
652 return False
653 return (self.project == other.project and
654 self.branch == other.branch and
James E. Blair6f140c72017-03-03 10:32:07 -0800655 self.path == other.path and
Monty Taylore6562aa2017-02-20 07:37:39 -0500656 self.trusted == other.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800657
658
James E. Blair66b274e2017-01-31 14:47:52 -0800659class PlaybookContext(object):
James E. Blaircdab2032017-02-01 09:09:29 -0800660
James E. Blair66b274e2017-01-31 14:47:52 -0800661 """A reference to a playbook in the context of a project.
662
663 Jobs refer to objects of this class for their main, pre, and post
664 playbooks so that we can keep track of which repos and security
James E. Blair74a82cf2017-07-12 17:23:08 -0700665 contexts are needed in order to run them.
James E. Blair66b274e2017-01-31 14:47:52 -0800666
James E. Blair74a82cf2017-07-12 17:23:08 -0700667 We also keep a list of roles so that playbooks only run with the
668 roles which were defined at the point the playbook was defined.
669
670 """
671
James E. Blair892cca62017-08-09 11:36:58 -0700672 def __init__(self, source_context, path, roles, secrets):
James E. Blaircdab2032017-02-01 09:09:29 -0800673 self.source_context = source_context
James E. Blair66b274e2017-01-31 14:47:52 -0800674 self.path = path
James E. Blair74a82cf2017-07-12 17:23:08 -0700675 self.roles = roles
James E. Blair892cca62017-08-09 11:36:58 -0700676 self.secrets = secrets
James E. Blair66b274e2017-01-31 14:47:52 -0800677
678 def __repr__(self):
James E. Blaircdab2032017-02-01 09:09:29 -0800679 return '<PlaybookContext %s %s>' % (self.source_context,
680 self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800681
682 def __ne__(self, other):
683 return not self.__eq__(other)
684
685 def __eq__(self, other):
686 if not isinstance(other, PlaybookContext):
687 return False
James E. Blaircdab2032017-02-01 09:09:29 -0800688 return (self.source_context == other.source_context and
James E. Blair74a82cf2017-07-12 17:23:08 -0700689 self.path == other.path and
James E. Blair892cca62017-08-09 11:36:58 -0700690 self.roles == other.roles and
691 self.secrets == other.secrets)
James E. Blair66b274e2017-01-31 14:47:52 -0800692
693 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400694 # Render to a dict to use in passing json to the executor
James E. Blair892cca62017-08-09 11:36:58 -0700695 secrets = {}
696 for secret in self.secrets:
697 secret_data = copy.deepcopy(secret.secret_data)
698 secrets[secret.name] = secret_data
James E. Blair66b274e2017-01-31 14:47:52 -0800699 return dict(
James E. Blaircdab2032017-02-01 09:09:29 -0800700 connection=self.source_context.project.connection_name,
701 project=self.source_context.project.name,
702 branch=self.source_context.branch,
Monty Taylore6562aa2017-02-20 07:37:39 -0500703 trusted=self.source_context.trusted,
James E. Blair74a82cf2017-07-12 17:23:08 -0700704 roles=[r.toDict() for r in self.roles],
James E. Blair892cca62017-08-09 11:36:58 -0700705 secrets=secrets,
James E. Blaircdab2032017-02-01 09:09:29 -0800706 path=self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800707
708
Monty Taylorb934c1a2017-06-16 19:31:47 -0500709class Role(object, metaclass=abc.ABCMeta):
James E. Blair5ac93842017-01-20 06:47:34 -0800710 """A reference to an ansible role."""
711
712 def __init__(self, target_name):
713 self.target_name = target_name
714
715 @abc.abstractmethod
716 def __repr__(self):
717 pass
718
719 def __ne__(self, other):
720 return not self.__eq__(other)
721
722 @abc.abstractmethod
723 def __eq__(self, other):
724 if not isinstance(other, Role):
725 return False
726 return (self.target_name == other.target_name)
727
728 @abc.abstractmethod
729 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400730 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800731 return dict(target_name=self.target_name)
732
733
734class ZuulRole(Role):
735 """A reference to an ansible role in a Zuul project."""
736
James E. Blairbb94dfa2017-07-11 07:45:19 -0700737 def __init__(self, target_name, connection_name, project_name,
738 implicit=False):
James E. Blair5ac93842017-01-20 06:47:34 -0800739 super(ZuulRole, self).__init__(target_name)
740 self.connection_name = connection_name
741 self.project_name = project_name
James E. Blairbb94dfa2017-07-11 07:45:19 -0700742 self.implicit = implicit
James E. Blair5ac93842017-01-20 06:47:34 -0800743
744 def __repr__(self):
745 return '<ZuulRole %s %s>' % (self.project_name, self.target_name)
746
Clint Byrumaf7438f2017-05-10 17:26:57 -0400747 __hash__ = object.__hash__
748
James E. Blair5ac93842017-01-20 06:47:34 -0800749 def __eq__(self, other):
750 if not isinstance(other, ZuulRole):
751 return False
James E. Blairbb94dfa2017-07-11 07:45:19 -0700752 # Implicit is not consulted for equality so that we can handle
753 # implicit to explicit conversions.
James E. Blair5ac93842017-01-20 06:47:34 -0800754 return (super(ZuulRole, self).__eq__(other) and
James E. Blair1b27f6a2017-07-14 14:09:07 -0700755 self.connection_name == other.connection_name and
James E. Blair6563e4b2017-04-28 08:14:48 -0700756 self.project_name == other.project_name)
James E. Blair5ac93842017-01-20 06:47:34 -0800757
758 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400759 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800760 d = super(ZuulRole, self).toDict()
761 d['type'] = 'zuul'
762 d['connection'] = self.connection_name
763 d['project'] = self.project_name
James E. Blairbb94dfa2017-07-11 07:45:19 -0700764 d['implicit'] = self.implicit
James E. Blair5ac93842017-01-20 06:47:34 -0800765 return d
766
767
James E. Blairee743612012-05-29 14:49:32 -0700768class Job(object):
James E. Blair66b274e2017-01-31 14:47:52 -0800769
James E. Blaira7f51ca2017-02-07 16:01:26 -0800770 """A Job represents the defintion of actions to perform.
771
James E. Blaird4ade8c2017-02-19 15:25:46 -0800772 A Job is an abstract configuration concept. It describes what,
773 where, and under what circumstances something should be run
774 (contrast this with Build which is a concrete single execution of
775 a Job).
776
James E. Blaira7f51ca2017-02-07 16:01:26 -0800777 NB: Do not modify attributes of this class, set them directly
778 (e.g., "job.run = ..." rather than "job.run.append(...)").
779 """
Monty Taylora42a55b2016-07-29 07:53:33 -0700780
James E. Blairee743612012-05-29 14:49:32 -0700781 def __init__(self, name):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800782 # These attributes may override even the final form of a job
783 # in the context of a project-pipeline. They can not affect
784 # the execution of the job, but only whether the job is run
785 # and how it is reported.
786 self.context_attributes = dict(
787 voting=True,
788 hold_following_changes=False,
James E. Blair1774dd52017-02-03 10:52:32 -0800789 failure_message=None,
790 success_message=None,
791 failure_url=None,
792 success_url=None,
793 # Matchers. These are separate so they can be individually
794 # overidden.
795 branch_matcher=None,
796 file_matcher=None,
797 irrelevant_file_matcher=None, # skip-if
James E. Blaira7f51ca2017-02-07 16:01:26 -0800798 tags=frozenset(),
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200799 dependencies=frozenset(),
James E. Blair1774dd52017-02-03 10:52:32 -0800800 )
801
James E. Blaira7f51ca2017-02-07 16:01:26 -0800802 # These attributes affect how the job is actually run and more
803 # care must be taken when overriding them. If a job is
804 # declared "final", these may not be overriden in a
805 # project-pipeline.
806 self.execution_attributes = dict(
807 timeout=None,
James E. Blair490cf042017-02-24 23:07:21 -0500808 variables={},
James E. Blaira7f51ca2017-02-07 16:01:26 -0800809 nodeset=NodeSet(),
James E. Blaira7f51ca2017-02-07 16:01:26 -0800810 workspace=None,
811 pre_run=(),
812 post_run=(),
813 run=(),
814 implied_run=(),
Tobias Henkel9a0e1942017-03-20 16:16:02 +0100815 semaphore=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800816 attempts=3,
817 final=False,
James E. Blair5fc81922017-07-12 13:19:37 -0700818 roles=(),
James E. Blair912322f2017-05-23 13:11:25 -0700819 required_projects={},
James E. Blairb3f5db12017-03-17 12:57:39 -0700820 allowed_projects=None,
James E. Blair7391ecb2017-05-23 13:32:40 -0700821 override_branch=None,
James E. Blair8eb564a2017-08-10 09:21:41 -0700822 post_review=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800823 )
824
825 # These are generally internal attributes which are not
826 # accessible via configuration.
827 self.other_attributes = dict(
828 name=None,
829 source_context=None,
830 inheritance_path=(),
831 )
832
833 self.inheritable_attributes = {}
834 self.inheritable_attributes.update(self.context_attributes)
835 self.inheritable_attributes.update(self.execution_attributes)
836 self.attributes = {}
837 self.attributes.update(self.inheritable_attributes)
838 self.attributes.update(self.other_attributes)
839
James E. Blairee743612012-05-29 14:49:32 -0700840 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800841
James E. Blair66b274e2017-01-31 14:47:52 -0800842 def __ne__(self, other):
843 return not self.__eq__(other)
844
Paul Belangere22baea2016-11-03 16:59:27 -0400845 def __eq__(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800846 # Compare the name and all inheritable attributes to determine
847 # whether two jobs with the same name are identically
848 # configured. Useful upon reconfiguration.
849 if not isinstance(other, Job):
850 return False
851 if self.name != other.name:
852 return False
853 for k, v in self.attributes.items():
854 if getattr(self, k) != getattr(other, k):
855 return False
856 return True
James E. Blairee743612012-05-29 14:49:32 -0700857
Clint Byrumaf7438f2017-05-10 17:26:57 -0400858 __hash__ = object.__hash__
859
James E. Blairee743612012-05-29 14:49:32 -0700860 def __str__(self):
861 return self.name
862
863 def __repr__(self):
James E. Blair72ae9da2017-02-03 14:21:30 -0800864 return '<Job %s branches: %s source: %s>' % (self.name,
865 self.branch_matcher,
866 self.source_context)
James E. Blair83005782015-12-11 14:46:03 -0800867
James E. Blaira7f51ca2017-02-07 16:01:26 -0800868 def __getattr__(self, name):
869 v = self.__dict__.get(name)
870 if v is None:
871 return copy.deepcopy(self.attributes[name])
872 return v
873
874 def _get(self, name):
875 return self.__dict__.get(name)
876
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100877 def getSafeAttributes(self):
878 return Attributes(name=self.name)
879
James E. Blaira7f51ca2017-02-07 16:01:26 -0800880 def setRun(self):
881 if not self.run:
882 self.run = self.implied_run
883
James E. Blair5fc81922017-07-12 13:19:37 -0700884 def addRoles(self, roles):
James E. Blairbb94dfa2017-07-11 07:45:19 -0700885 newroles = []
886 # Start with a copy of the existing roles, but if any of them
887 # are implicit roles which are identified as explicit in the
888 # new roles list, replace them with the explicit version.
889 changed = False
890 for existing_role in self.roles:
891 if existing_role in roles:
892 new_role = roles[roles.index(existing_role)]
893 else:
894 new_role = None
895 if (new_role and
896 isinstance(new_role, ZuulRole) and
897 isinstance(existing_role, ZuulRole) and
898 existing_role.implicit and not new_role.implicit):
899 newroles.append(new_role)
900 changed = True
901 else:
902 newroles.append(existing_role)
903 # Now add the new roles.
James E. Blair4eec8282017-07-12 17:33:26 -0700904 for role in reversed(roles):
James E. Blair5fc81922017-07-12 13:19:37 -0700905 if role not in newroles:
James E. Blair4eec8282017-07-12 17:33:26 -0700906 newroles.insert(0, role)
James E. Blairbb94dfa2017-07-11 07:45:19 -0700907 changed = True
908 if changed:
909 self.roles = tuple(newroles)
James E. Blair5fc81922017-07-12 13:19:37 -0700910
James E. Blair490cf042017-02-24 23:07:21 -0500911 def updateVariables(self, other_vars):
912 v = self.variables
913 Job._deepUpdate(v, other_vars)
914 self.variables = v
915
James E. Blair912322f2017-05-23 13:11:25 -0700916 def updateProjects(self, other_projects):
917 required_projects = self.required_projects
918 Job._deepUpdate(required_projects, other_projects)
919 self.required_projects = required_projects
James E. Blair27f3dfc2017-05-23 13:07:28 -0700920
James E. Blair490cf042017-02-24 23:07:21 -0500921 @staticmethod
922 def _deepUpdate(a, b):
923 # Merge nested dictionaries if possible, otherwise, overwrite
924 # the value in 'a' with the value in 'b'.
925 for k, bv in b.items():
926 av = a.get(k)
927 if isinstance(av, dict) and isinstance(bv, dict):
928 Job._deepUpdate(av, bv)
929 else:
930 a[k] = bv
931
James E. Blaira7f51ca2017-02-07 16:01:26 -0800932 def inheritFrom(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800933 """Copy the inheritable attributes which have been set on the other
934 job to this job."""
James E. Blaira7f51ca2017-02-07 16:01:26 -0800935 if not isinstance(other, Job):
936 raise Exception("Job unable to inherit from %s" % (other,))
937
Tobias Henkel83167622017-06-30 19:45:03 +0200938 if other.final:
939 raise Exception("Unable to inherit from final job %s" %
940 (repr(other),))
941
James E. Blaira7f51ca2017-02-07 16:01:26 -0800942 # copy all attributes
943 for k in self.inheritable_attributes:
James E. Blair892cca62017-08-09 11:36:58 -0700944 if (other._get(k) is not None):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800945 setattr(self, k, copy.deepcopy(getattr(other, k)))
946
947 msg = 'inherit from %s' % (repr(other),)
948 self.inheritance_path = other.inheritance_path + (msg,)
949
950 def copy(self):
951 job = Job(self.name)
952 for k in self.attributes:
953 if self._get(k) is not None:
954 setattr(job, k, copy.deepcopy(self._get(k)))
955 return job
956
957 def applyVariant(self, other):
958 """Copy the attributes which have been set on the other job to this
959 job."""
James E. Blair83005782015-12-11 14:46:03 -0800960
961 if not isinstance(other, Job):
962 raise Exception("Job unable to inherit from %s" % (other,))
James E. Blaira7f51ca2017-02-07 16:01:26 -0800963
964 for k in self.execution_attributes:
965 if (other._get(k) is not None and
966 k not in set(['final'])):
967 if self.final:
968 raise Exception("Unable to modify final job %s attribute "
969 "%s=%s with variant %s" % (
970 repr(self), k, other._get(k),
971 repr(other)))
James E. Blair27f3dfc2017-05-23 13:07:28 -0700972 if k not in set(['pre_run', 'post_run', 'roles', 'variables',
James E. Blair912322f2017-05-23 13:11:25 -0700973 'required_projects']):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800974 setattr(self, k, copy.deepcopy(other._get(k)))
975
976 # Don't set final above so that we don't trip an error halfway
977 # through assignment.
978 if other.final != self.attributes['final']:
979 self.final = other.final
980
981 if other._get('pre_run') is not None:
982 self.pre_run = self.pre_run + other.pre_run
983 if other._get('post_run') is not None:
984 self.post_run = other.post_run + self.post_run
James E. Blair5ac93842017-01-20 06:47:34 -0800985 if other._get('roles') is not None:
James E. Blair5fc81922017-07-12 13:19:37 -0700986 self.addRoles(other.roles)
James E. Blair490cf042017-02-24 23:07:21 -0500987 if other._get('variables') is not None:
988 self.updateVariables(other.variables)
James E. Blair912322f2017-05-23 13:11:25 -0700989 if other._get('required_projects') is not None:
990 self.updateProjects(other.required_projects)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800991
992 for k in self.context_attributes:
993 if (other._get(k) is not None and
994 k not in set(['tags'])):
995 setattr(self, k, copy.deepcopy(other._get(k)))
996
997 if other._get('tags') is not None:
998 self.tags = self.tags.union(other.tags)
999
1000 msg = 'apply variant %s' % (repr(other),)
1001 self.inheritance_path = self.inheritance_path + (msg,)
James E. Blairee743612012-05-29 14:49:32 -07001002
James E. Blaire421a232012-07-25 16:59:21 -07001003 def changeMatches(self, change):
James E. Blair83005782015-12-11 14:46:03 -08001004 if self.branch_matcher and not self.branch_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -08001005 return False
1006
James E. Blair83005782015-12-11 14:46:03 -08001007 if self.file_matcher and not self.file_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -08001008 return False
1009
James E. Blair83005782015-12-11 14:46:03 -08001010 # NB: This is a negative match.
1011 if (self.irrelevant_file_matcher and
1012 self.irrelevant_file_matcher.matches(change)):
Maru Newby3fe5f852015-01-13 04:22:14 +00001013 return False
1014
James E. Blair70c71582013-03-06 08:50:50 -08001015 return True
James E. Blaire5a847f2012-07-10 15:29:14 -07001016
James E. Blair1e8dd892012-05-30 09:15:05 -07001017
James E. Blair912322f2017-05-23 13:11:25 -07001018class JobProject(object):
James E. Blair27f3dfc2017-05-23 13:07:28 -07001019 """ A reference to a project from a job. """
1020
1021 def __init__(self, project_name, override_branch=None):
1022 self.project_name = project_name
1023 self.override_branch = override_branch
1024
1025
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001026class JobList(object):
1027 """ A list of jobs in a project's pipeline. """
Monty Taylora42a55b2016-07-29 07:53:33 -07001028
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001029 def __init__(self):
1030 self.jobs = OrderedDict() # job.name -> [job, ...]
James E. Blair12748b52017-02-07 17:17:53 -08001031
James E. Blairee743612012-05-29 14:49:32 -07001032 def addJob(self, job):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001033 if job.name in self.jobs:
1034 self.jobs[job.name].append(job)
1035 else:
1036 self.jobs[job.name] = [job]
James E. Blairee743612012-05-29 14:49:32 -07001037
James E. Blaira7f51ca2017-02-07 16:01:26 -08001038 def inheritFrom(self, other):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001039 for jobname, jobs in other.jobs.items():
1040 if jobname in self.jobs:
Jesse Keatingd1f434a2017-05-16 20:28:35 -07001041 self.jobs[jobname].extend(jobs)
James E. Blaira7f51ca2017-02-07 16:01:26 -08001042 else:
James E. Blairfceaf412017-09-29 13:44:57 -07001043 # Be sure to make a copy here since this list may be
1044 # modified.
1045 self.jobs[jobname] = jobs[:]
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001046
1047
1048class JobGraph(object):
1049 """ A JobGraph represents the dependency graph between Job."""
1050
1051 def __init__(self):
1052 self.jobs = OrderedDict() # job_name -> Job
1053 self._dependencies = {} # dependent_job_name -> set(parent_job_names)
1054
1055 def __repr__(self):
1056 return '<JobGraph %s>' % (self.jobs)
1057
1058 def addJob(self, job):
1059 # A graph must be created after the job list is frozen,
1060 # therefore we should only get one job with the same name.
1061 if job.name in self.jobs:
1062 raise Exception("Job %s already added" % (job.name,))
1063 self.jobs[job.name] = job
1064 # Append the dependency information
1065 self._dependencies.setdefault(job.name, set())
1066 try:
1067 for dependency in job.dependencies:
1068 # Make sure a circular dependency is never created
1069 ancestor_jobs = self._getParentJobNamesRecursively(
1070 dependency, soft=True)
1071 ancestor_jobs.add(dependency)
1072 if any((job.name == anc_job) for anc_job in ancestor_jobs):
1073 raise Exception("Dependency cycle detected in job %s" %
1074 (job.name,))
1075 self._dependencies[job.name].add(dependency)
1076 except Exception:
1077 del self.jobs[job.name]
1078 del self._dependencies[job.name]
1079 raise
1080
1081 def getJobs(self):
Clint Byruma4471d12017-05-10 20:57:40 -04001082 return list(self.jobs.values()) # Report in the order of layout cfg
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001083
1084 def _getDirectDependentJobs(self, parent_job):
1085 ret = set()
1086 for dependent_name, parent_names in self._dependencies.items():
1087 if parent_job in parent_names:
1088 ret.add(dependent_name)
1089 return ret
1090
1091 def getDependentJobsRecursively(self, parent_job):
1092 all_dependent_jobs = set()
1093 jobs_to_iterate = set([parent_job])
1094 while len(jobs_to_iterate) > 0:
1095 current_job = jobs_to_iterate.pop()
1096 current_dependent_jobs = self._getDirectDependentJobs(current_job)
1097 new_dependent_jobs = current_dependent_jobs - all_dependent_jobs
1098 jobs_to_iterate |= new_dependent_jobs
1099 all_dependent_jobs |= new_dependent_jobs
1100 return [self.jobs[name] for name in all_dependent_jobs]
1101
1102 def getParentJobsRecursively(self, dependent_job):
1103 return [self.jobs[name] for name in
1104 self._getParentJobNamesRecursively(dependent_job)]
1105
1106 def _getParentJobNamesRecursively(self, dependent_job, soft=False):
1107 all_parent_jobs = set()
1108 jobs_to_iterate = set([dependent_job])
1109 while len(jobs_to_iterate) > 0:
1110 current_job = jobs_to_iterate.pop()
1111 current_parent_jobs = self._dependencies.get(current_job)
1112 if current_parent_jobs is None:
1113 if soft:
1114 current_parent_jobs = set()
1115 else:
1116 raise Exception("Dependent job %s not found: " %
1117 (dependent_job,))
1118 new_parent_jobs = current_parent_jobs - all_parent_jobs
1119 jobs_to_iterate |= new_parent_jobs
1120 all_parent_jobs |= new_parent_jobs
1121 return all_parent_jobs
James E. Blairb97ed802015-12-21 15:55:35 -08001122
James E. Blair1e8dd892012-05-30 09:15:05 -07001123
James E. Blair4aea70c2012-07-26 14:23:24 -07001124class Build(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001125 """A Build is an instance of a single execution of a Job.
1126
1127 While a Job describes what to run, a Build describes an actual
1128 execution of that Job. Each build is associated with exactly one
1129 Job (related builds are grouped together in a BuildSet).
1130 """
Monty Taylora42a55b2016-07-29 07:53:33 -07001131
James E. Blair4aea70c2012-07-26 14:23:24 -07001132 def __init__(self, job, uuid):
1133 self.job = job
1134 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -07001135 self.url = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001136 self.result = None
James E. Blair196f61a2017-06-30 15:42:29 -07001137 self.result_data = {}
James E. Blair6f699732017-07-18 14:19:11 -07001138 self.error_detail = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001139 self.build_set = None
Paul Belanger174a8272017-03-14 13:20:10 -04001140 self.execute_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -08001141 self.start_time = None
1142 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -07001143 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -07001144 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -07001145 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -07001146 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -07001147 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +08001148 self.worker = Worker()
Timothy Chavezb2332082015-08-07 20:08:04 -05001149 self.node_labels = []
1150 self.node_name = None
James E. Blairee743612012-05-29 14:49:32 -07001151
1152 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +08001153 return ('<Build %s of %s on %s>' %
1154 (self.uuid, self.job.name, self.worker))
1155
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001156 def getSafeAttributes(self):
James E. Blair196f61a2017-06-30 15:42:29 -07001157 return Attributes(uuid=self.uuid,
1158 result=self.result,
James E. Blair6f699732017-07-18 14:19:11 -07001159 error_detail=self.error_detail,
James E. Blair196f61a2017-06-30 15:42:29 -07001160 result_data=self.result_data)
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001161
Joshua Heskethba8776a2014-01-12 14:35:40 +08001162
1163class Worker(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001164 """Information about the specific worker executing a Build."""
Joshua Heskethba8776a2014-01-12 14:35:40 +08001165 def __init__(self):
1166 self.name = "Unknown"
1167 self.hostname = None
Monty Taylor0dbe1592017-06-11 10:57:27 -05001168 self.log_port = None
Joshua Heskethba8776a2014-01-12 14:35:40 +08001169
1170 def updateFromData(self, data):
1171 """Update worker information if contained in the WORK_DATA response."""
1172 self.name = data.get('worker_name', self.name)
1173 self.hostname = data.get('worker_hostname', self.hostname)
Monty Taylor0dbe1592017-06-11 10:57:27 -05001174 self.log_port = data.get('worker_log_port', self.log_port)
Joshua Heskethba8776a2014-01-12 14:35:40 +08001175
1176 def __repr__(self):
1177 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -07001178
James E. Blair1e8dd892012-05-30 09:15:05 -07001179
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001180class RepoFiles(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001181 """RepoFiles holds config-file content for per-project job config.
1182
1183 When Zuul asks a merger to prepare a future multiple-repo state
1184 and collect Zuul configuration files so that we can dynamically
1185 load our configuration, this class provides cached access to that
1186 data for use by the Change which updated the config files and any
1187 changes that follow it in a ChangeQueue.
1188
1189 It is attached to a BuildSet since the content of Zuul
1190 configuration files can change with each new BuildSet.
1191 """
1192
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001193 def __init__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001194 self.connections = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001195
1196 def __repr__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001197 return '<RepoFiles %s>' % self.connections
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001198
1199 def setFiles(self, items):
James E. Blair2a535672017-04-27 12:03:15 -07001200 self.hostnames = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001201 for item in items:
James E. Blair2a535672017-04-27 12:03:15 -07001202 connection = self.connections.setdefault(
1203 item['connection'], {})
1204 project = connection.setdefault(item['project'], {})
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001205 branch = project.setdefault(item['branch'], {})
1206 branch.update(item['files'])
1207
James E. Blair2a535672017-04-27 12:03:15 -07001208 def getFile(self, connection_name, project_name, branch, fn):
1209 host = self.connections.get(connection_name, {})
1210 return host.get(project_name, {}).get(branch, {}).get(fn)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001211
1212
James E. Blair7e530ad2012-07-03 16:12:28 -07001213class BuildSet(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001214 """A collection of Builds for one specific potential future repository
1215 state.
Monty Taylora42a55b2016-07-29 07:53:33 -07001216
Paul Belanger174a8272017-03-14 13:20:10 -04001217 When Zuul executes Builds for a change, it creates a Build to
James E. Blaird4ade8c2017-02-19 15:25:46 -08001218 represent each execution of each job and a BuildSet to keep track
Paul Belanger174a8272017-03-14 13:20:10 -04001219 of all the Builds running for that Change. When Zuul re-executes
James E. Blaird4ade8c2017-02-19 15:25:46 -08001220 Builds for a Change with a different configuration, all of the
1221 running Builds in the BuildSet for that change are aborted, and a
1222 new BuildSet is created to hold the Builds for the Jobs being
1223 run with the new configuration.
1224
1225 A BuildSet also holds the UUID used to produce the Zuul Ref that
1226 builders check out.
1227
Monty Taylora42a55b2016-07-29 07:53:33 -07001228 """
James E. Blair4076e2b2014-01-28 12:42:20 -08001229 # Merge states:
1230 NEW = 1
1231 PENDING = 2
1232 COMPLETE = 3
1233
Antoine Musso9b229282014-08-18 23:45:43 +02001234 states_map = {
1235 1: 'NEW',
1236 2: 'PENDING',
1237 3: 'COMPLETE',
1238 }
1239
James E. Blairfee8d652013-06-07 08:57:52 -07001240 def __init__(self, item):
1241 self.item = item
James E. Blair7e530ad2012-07-03 16:12:28 -07001242 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -07001243 self.result = None
1244 self.next_build_set = None
1245 self.previous_build_set = None
Jamie Lennox3f16de52017-05-09 14:24:11 +10001246 self.uuid = None
James E. Blair81515ad2012-10-01 18:29:08 -07001247 self.commit = None
James E. Blair1960d682017-04-28 15:44:14 -07001248 self.dependent_items = None
1249 self.merger_items = None
James E. Blair973721f2012-08-15 10:19:43 -07001250 self.unable_to_merge = False
James E. Blaire53250c2017-03-01 14:34:36 -08001251 self.config_error = None # None or an error message string.
James E. Blair972e3c72013-08-29 12:04:55 -07001252 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -08001253 self.merge_state = self.NEW
James E. Blair0eaad552016-09-02 12:09:54 -07001254 self.nodesets = {} # job -> nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001255 self.node_requests = {} # job -> reqs
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001256 self.files = RepoFiles()
James E. Blair1960d682017-04-28 15:44:14 -07001257 self.repo_state = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001258 self.layout = None
Paul Belanger71d98172016-11-08 10:56:31 -05001259 self.tries = {}
James E. Blair7e530ad2012-07-03 16:12:28 -07001260
Jamie Lennox3f16de52017-05-09 14:24:11 +10001261 @property
1262 def ref(self):
1263 # NOTE(jamielennox): The concept of buildset ref is to be removed and a
1264 # buildset UUID identifier available instead. Currently the ref is
1265 # checked to see if the BuildSet has been configured.
1266 return 'Z' + self.uuid if self.uuid else None
1267
Antoine Musso9b229282014-08-18 23:45:43 +02001268 def __repr__(self):
1269 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
1270 self.item,
1271 len(self.builds),
1272 self.getStateName(self.merge_state))
1273
James E. Blair4886cc12012-07-18 15:39:41 -07001274 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -07001275 # The change isn't enqueued until after it's created
1276 # so we don't know what the other changes ahead will be
1277 # until jobs start.
James E. Blair1960d682017-04-28 15:44:14 -07001278 if self.dependent_items is None:
1279 items = []
James E. Blairfee8d652013-06-07 08:57:52 -07001280 next_item = self.item.item_ahead
1281 while next_item:
James E. Blair1960d682017-04-28 15:44:14 -07001282 items.append(next_item)
James E. Blairfee8d652013-06-07 08:57:52 -07001283 next_item = next_item.item_ahead
James E. Blair1960d682017-04-28 15:44:14 -07001284 self.dependent_items = items
Jamie Lennox3f16de52017-05-09 14:24:11 +10001285 if not self.uuid:
1286 self.uuid = uuid4().hex
James E. Blair1960d682017-04-28 15:44:14 -07001287 if self.merger_items is None:
1288 items = [self.item] + self.dependent_items
1289 items.reverse()
1290 self.merger_items = [i.makeMergerItem() for i in items]
James E. Blair4886cc12012-07-18 15:39:41 -07001291
Antoine Musso9b229282014-08-18 23:45:43 +02001292 def getStateName(self, state_num):
1293 return self.states_map.get(
1294 state_num, 'UNKNOWN (%s)' % state_num)
1295
James E. Blair4886cc12012-07-18 15:39:41 -07001296 def addBuild(self, build):
1297 self.builds[build.job.name] = build
Paul Belanger71d98172016-11-08 10:56:31 -05001298 if build.job.name not in self.tries:
James E. Blair5aed1112016-12-15 09:18:05 -08001299 self.tries[build.job.name] = 1
James E. Blair4886cc12012-07-18 15:39:41 -07001300 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -07001301
James E. Blair4a28a882013-08-23 15:17:33 -07001302 def removeBuild(self, build):
Paul Belanger71d98172016-11-08 10:56:31 -05001303 self.tries[build.job.name] += 1
James E. Blair4a28a882013-08-23 15:17:33 -07001304 del self.builds[build.job.name]
1305
James E. Blair7e530ad2012-07-03 16:12:28 -07001306 def getBuild(self, job_name):
1307 return self.builds.get(job_name)
1308
James E. Blair11700c32012-07-05 17:50:05 -07001309 def getBuilds(self):
Clint Byrum1d0c7d12017-05-10 19:40:53 -07001310 keys = list(self.builds.keys())
James E. Blair11700c32012-07-05 17:50:05 -07001311 keys.sort()
1312 return [self.builds.get(x) for x in keys]
1313
James E. Blair0eaad552016-09-02 12:09:54 -07001314 def getJobNodeSet(self, job_name):
1315 # Return None if not provisioned; empty NodeSet if no nodes
1316 # required
1317 return self.nodesets.get(job_name)
James E. Blair8d692392016-04-08 17:47:58 -07001318
James E. Blaire18d4602017-01-05 11:17:28 -08001319 def removeJobNodeSet(self, job_name):
1320 if job_name not in self.nodesets:
1321 raise Exception("No job set for %s" % (job_name))
1322 del self.nodesets[job_name]
1323
James E. Blair8d692392016-04-08 17:47:58 -07001324 def setJobNodeRequest(self, job_name, req):
1325 if job_name in self.node_requests:
1326 raise Exception("Prior node request for %s" % (job_name))
1327 self.node_requests[job_name] = req
1328
1329 def getJobNodeRequest(self, job_name):
1330 return self.node_requests.get(job_name)
1331
James E. Blair0eaad552016-09-02 12:09:54 -07001332 def jobNodeRequestComplete(self, job_name, req, nodeset):
1333 if job_name in self.nodesets:
James E. Blair8d692392016-04-08 17:47:58 -07001334 raise Exception("Prior node request for %s" % (job_name))
James E. Blair0eaad552016-09-02 12:09:54 -07001335 self.nodesets[job_name] = nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001336 del self.node_requests[job_name]
1337
Paul Belanger71d98172016-11-08 10:56:31 -05001338 def getTries(self, job_name):
Clint Byrum804073b2017-05-10 17:47:45 -04001339 return self.tries.get(job_name, 0)
Paul Belanger71d98172016-11-08 10:56:31 -05001340
James E. Blair0ffa0102017-03-30 13:11:33 -07001341 def getMergeMode(self):
James E. Blair1960d682017-04-28 15:44:14 -07001342 # We may be called before this build set has a shadow layout
1343 # (ie, we are called to perform the merge to create that
1344 # layout). It's possible that the change we are merging will
1345 # update the merge-mode for the project, but there's not much
1346 # we can do about that here. Instead, do the best we can by
1347 # using the nearest shadow layout to determine the merge mode,
1348 # or if that fails, the current live layout, or if that fails,
1349 # use the default: merge-resolve.
1350 item = self.item
1351 layout = None
1352 while item:
1353 layout = item.current_build_set.layout
1354 if layout:
1355 break
1356 item = item.item_ahead
1357 if not layout:
1358 layout = self.item.pipeline.layout
1359 if layout:
James E. Blair0ffa0102017-03-30 13:11:33 -07001360 project = self.item.change.project
James E. Blair1960d682017-04-28 15:44:14 -07001361 project_config = layout.project_configs.get(
James E. Blair0ffa0102017-03-30 13:11:33 -07001362 project.canonical_name)
1363 if project_config:
1364 return project_config.merge_mode
1365 return MERGER_MERGE_RESOLVE
Adam Gandelman8bd57102016-12-02 12:58:42 -08001366
Jamie Lennox3f16de52017-05-09 14:24:11 +10001367 def getSafeAttributes(self):
1368 return Attributes(uuid=self.uuid)
1369
James E. Blair7e530ad2012-07-03 16:12:28 -07001370
James E. Blairfee8d652013-06-07 08:57:52 -07001371class QueueItem(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001372 """Represents the position of a Change in a ChangeQueue.
1373
1374 All Changes are enqueued into ChangeQueue in a QueueItem. The QueueItem
1375 holds the current `BuildSet` as well as all previous `BuildSets` that were
1376 produced for this `QueueItem`.
1377 """
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001378 log = logging.getLogger("zuul.QueueItem")
James E. Blair32663402012-06-01 10:04:18 -07001379
James E. Blairbfb8e042014-12-30 17:01:44 -08001380 def __init__(self, queue, change):
1381 self.pipeline = queue.pipeline
1382 self.queue = queue
Monty Taylor55d0f562017-05-17 14:30:21 -05001383 self.change = change # a ref
James E. Blair7e530ad2012-07-03 16:12:28 -07001384 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -07001385 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -07001386 self.current_build_set = BuildSet(self)
1387 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -07001388 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -07001389 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -08001390 self.enqueue_time = None
1391 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -07001392 self.reported = False
Tobias Henkel9842bd72017-05-16 13:40:03 +02001393 self.reported_start = False
1394 self.quiet = False
James E. Blairbfb8e042014-12-30 17:01:44 -08001395 self.active = False # Whether an item is within an active window
1396 self.live = True # Whether an item is intended to be processed at all
James E. Blairc9455002017-09-06 09:22:19 -07001397 # TODO(jeblair): move job_graph to buildset
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001398 self.job_graph = None
James E. Blaire5a847f2012-07-10 15:29:14 -07001399
James E. Blair972e3c72013-08-29 12:04:55 -07001400 def __repr__(self):
1401 if self.pipeline:
1402 pipeline = self.pipeline.name
1403 else:
1404 pipeline = None
1405 return '<QueueItem 0x%x for %s in %s>' % (
1406 id(self), self.change, pipeline)
1407
James E. Blairee743612012-05-29 14:49:32 -07001408 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -07001409 old = self.current_build_set
1410 self.current_build_set.result = 'CANCELED'
1411 self.current_build_set = BuildSet(self)
1412 old.next_build_set = self.current_build_set
1413 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -07001414 self.build_sets.append(self.current_build_set)
James E. Blairc9455002017-09-06 09:22:19 -07001415 self.job_graph = None
James E. Blairee743612012-05-29 14:49:32 -07001416
1417 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -07001418 self.current_build_set.addBuild(build)
James E. Blair66eeebf2013-07-27 17:44:32 -07001419 build.pipeline = self.pipeline
James E. Blairee743612012-05-29 14:49:32 -07001420
James E. Blair4a28a882013-08-23 15:17:33 -07001421 def removeBuild(self, build):
1422 self.current_build_set.removeBuild(build)
1423
James E. Blairfee8d652013-06-07 08:57:52 -07001424 def setReportedResult(self, result):
1425 self.current_build_set.result = result
1426
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001427 def freezeJobGraph(self):
James E. Blair83005782015-12-11 14:46:03 -08001428 """Find or create actual matching jobs for this item's change and
1429 store the resulting job tree."""
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001430 layout = self.current_build_set.layout
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001431 job_graph = layout.createJobGraph(self)
1432 for job in job_graph.getJobs():
1433 # Ensure that each jobs's dependencies are fully
1434 # accessible. This will raise an exception if not.
1435 job_graph.getParentJobsRecursively(job.name)
1436 self.job_graph = job_graph
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001437
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001438 def hasJobGraph(self):
1439 """Returns True if the item has a job graph."""
1440 return self.job_graph is not None
James E. Blair83005782015-12-11 14:46:03 -08001441
1442 def getJobs(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001443 if not self.live or not self.job_graph:
James E. Blair83005782015-12-11 14:46:03 -08001444 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001445 return self.job_graph.getJobs()
1446
1447 def getJob(self, name):
1448 if not self.job_graph:
1449 return None
1450 return self.job_graph.jobs.get(name)
James E. Blair83005782015-12-11 14:46:03 -08001451
James E. Blairdbfd3282016-07-21 10:46:19 -07001452 def haveAllJobsStarted(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001453 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001454 return False
1455 for job in self.getJobs():
1456 build = self.current_build_set.getBuild(job.name)
1457 if not build or not build.start_time:
1458 return False
1459 return True
1460
1461 def areAllJobsComplete(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001462 if (self.current_build_set.config_error or
1463 self.current_build_set.unable_to_merge):
1464 return True
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001465 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001466 return False
1467 for job in self.getJobs():
1468 build = self.current_build_set.getBuild(job.name)
1469 if not build or not build.result:
1470 return False
1471 return True
1472
1473 def didAllJobsSucceed(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001474 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001475 return False
1476 for job in self.getJobs():
1477 if not job.voting:
1478 continue
1479 build = self.current_build_set.getBuild(job.name)
1480 if not build:
1481 return False
1482 if build.result != 'SUCCESS':
1483 return False
1484 return True
1485
1486 def didAnyJobFail(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001487 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001488 return False
1489 for job in self.getJobs():
1490 if not job.voting:
1491 continue
1492 build = self.current_build_set.getBuild(job.name)
1493 if build and build.result and (build.result != 'SUCCESS'):
1494 return True
1495 return False
1496
1497 def didMergerFail(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001498 return self.current_build_set.unable_to_merge
1499
1500 def getConfigError(self):
1501 return self.current_build_set.config_error
James E. Blairdbfd3282016-07-21 10:46:19 -07001502
James E. Blair0d3e83b2017-06-05 13:51:57 -07001503 def wasDequeuedNeedingChange(self):
1504 return self.dequeued_needing_change
1505
James E. Blairdbfd3282016-07-21 10:46:19 -07001506 def isHoldingFollowingChanges(self):
1507 if not self.live:
1508 return False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001509 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001510 return False
1511 for job in self.getJobs():
1512 if not job.hold_following_changes:
1513 continue
1514 build = self.current_build_set.getBuild(job.name)
1515 if not build:
1516 return True
1517 if build.result != 'SUCCESS':
1518 return True
1519
1520 if not self.item_ahead:
1521 return False
1522 return self.item_ahead.isHoldingFollowingChanges()
1523
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001524 def findJobsToRun(self, semaphore_handler):
James E. Blairdbfd3282016-07-21 10:46:19 -07001525 torun = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001526 if not self.live:
1527 return []
1528 if not self.job_graph:
1529 return []
James E. Blair791b5392016-08-03 11:25:56 -07001530 if self.item_ahead:
1531 # Only run jobs if any 'hold' jobs on the change ahead
1532 # have completed successfully.
1533 if self.item_ahead.isHoldingFollowingChanges():
1534 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001535
1536 successful_job_names = set()
1537 jobs_not_started = set()
1538 for job in self.job_graph.getJobs():
1539 build = self.current_build_set.getBuild(job.name)
1540 if build:
1541 if build.result == 'SUCCESS':
1542 successful_job_names.add(job.name)
1543 else:
1544 jobs_not_started.add(job)
1545
1546 # Attempt to request nodes for jobs in the order jobs appear
1547 # in configuration.
1548 for job in self.job_graph.getJobs():
1549 if job not in jobs_not_started:
1550 continue
1551 all_parent_jobs_successful = True
1552 for parent_job in self.job_graph.getParentJobsRecursively(
1553 job.name):
1554 if parent_job.name not in successful_job_names:
1555 all_parent_jobs_successful = False
1556 break
1557 if all_parent_jobs_successful:
1558 nodeset = self.current_build_set.getJobNodeSet(job.name)
1559 if nodeset is None:
1560 # The nodes for this job are not ready, skip
1561 # it for now.
James E. Blairdbfd3282016-07-21 10:46:19 -07001562 continue
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001563 if semaphore_handler.acquire(self, job):
1564 # If this job needs a semaphore, either acquire it or
1565 # make sure that we have it before running the job.
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001566 torun.append(job)
James E. Blairdbfd3282016-07-21 10:46:19 -07001567 return torun
1568
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001569 def findJobsToRequest(self):
James E. Blair6ab79e02017-01-06 10:10:17 -08001570 build_set = self.current_build_set
James E. Blairdbfd3282016-07-21 10:46:19 -07001571 toreq = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001572 if not self.live:
1573 return []
1574 if not self.job_graph:
1575 return []
James E. Blair6ab79e02017-01-06 10:10:17 -08001576 if self.item_ahead:
1577 if self.item_ahead.isHoldingFollowingChanges():
1578 return []
James E. Blairdbfd3282016-07-21 10:46:19 -07001579
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001580 successful_job_names = set()
1581 jobs_not_requested = set()
1582 for job in self.job_graph.getJobs():
1583 build = build_set.getBuild(job.name)
1584 if build and build.result == 'SUCCESS':
1585 successful_job_names.add(job.name)
1586 else:
1587 nodeset = build_set.getJobNodeSet(job.name)
1588 if nodeset is None:
1589 req = build_set.getJobNodeRequest(job.name)
1590 if req is None:
1591 jobs_not_requested.add(job)
1592
1593 # Attempt to request nodes for jobs in the order jobs appear
1594 # in configuration.
1595 for job in self.job_graph.getJobs():
1596 if job not in jobs_not_requested:
1597 continue
1598 all_parent_jobs_successful = True
1599 for parent_job in self.job_graph.getParentJobsRecursively(
1600 job.name):
1601 if parent_job.name not in successful_job_names:
1602 all_parent_jobs_successful = False
1603 break
1604 if all_parent_jobs_successful:
1605 toreq.append(job)
1606 return toreq
James E. Blairdbfd3282016-07-21 10:46:19 -07001607
1608 def setResult(self, build):
1609 if build.retry:
1610 self.removeBuild(build)
1611 elif build.result != 'SUCCESS':
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001612 for job in self.job_graph.getDependentJobsRecursively(
1613 build.job.name):
James E. Blairdbfd3282016-07-21 10:46:19 -07001614 fakebuild = Build(job, None)
1615 fakebuild.result = 'SKIPPED'
1616 self.addBuild(fakebuild)
1617
James E. Blair6ab79e02017-01-06 10:10:17 -08001618 def setNodeRequestFailure(self, job):
1619 fakebuild = Build(job, None)
1620 self.addBuild(fakebuild)
1621 fakebuild.result = 'NODE_FAILURE'
1622 self.setResult(fakebuild)
1623
James E. Blairdbfd3282016-07-21 10:46:19 -07001624 def setDequeuedNeedingChange(self):
1625 self.dequeued_needing_change = True
1626 self._setAllJobsSkipped()
1627
1628 def setUnableToMerge(self):
1629 self.current_build_set.unable_to_merge = True
1630 self._setAllJobsSkipped()
1631
James E. Blaire53250c2017-03-01 14:34:36 -08001632 def setConfigError(self, error):
1633 self.current_build_set.config_error = error
1634 self._setAllJobsSkipped()
1635
James E. Blairdbfd3282016-07-21 10:46:19 -07001636 def _setAllJobsSkipped(self):
1637 for job in self.getJobs():
1638 fakebuild = Build(job, None)
1639 fakebuild.result = 'SKIPPED'
1640 self.addBuild(fakebuild)
1641
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001642 def formatUrlPattern(self, url_pattern, job=None, build=None):
1643 url = None
1644 # Produce safe versions of objects which may be useful in
1645 # result formatting, but don't allow users to crawl through
1646 # the entire data structure where they might be able to access
1647 # secrets, etc.
1648 safe_change = self.change.getSafeAttributes()
1649 safe_pipeline = self.pipeline.getSafeAttributes()
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001650 safe_tenant = self.pipeline.layout.tenant.getSafeAttributes()
Jamie Lennox3f16de52017-05-09 14:24:11 +10001651 safe_buildset = self.current_build_set.getSafeAttributes()
K Jonathan Harkera7f390c2017-06-02 12:31:22 -07001652 safe_job = job.getSafeAttributes() if job else {}
1653 safe_build = build.getSafeAttributes() if build else {}
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001654 try:
1655 url = url_pattern.format(change=safe_change,
1656 pipeline=safe_pipeline,
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001657 tenant=safe_tenant,
Jamie Lennox3f16de52017-05-09 14:24:11 +10001658 buildset=safe_buildset,
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001659 job=safe_job,
1660 build=safe_build)
1661 except KeyError as e:
1662 self.log.error("Error while formatting url for job %s: unknown "
1663 "key %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001664 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001665 except AttributeError as e:
1666 self.log.error("Error while formatting url for job %s: unknown "
1667 "attribute %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001668 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001669 except Exception:
1670 self.log.exception("Error while formatting url for job %s with "
1671 "pattern %s:" % (job, url_pattern))
1672
1673 return url
1674
James E. Blair800e7ff2017-03-17 16:06:52 -07001675 def formatJobResult(self, job):
James E. Blairb7273ef2016-04-19 08:58:51 -07001676 build = self.current_build_set.getBuild(job.name)
1677 result = build.result
James E. Blair800e7ff2017-03-17 16:06:52 -07001678 pattern = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001679 if result == 'SUCCESS':
1680 if job.success_message:
1681 result = job.success_message
James E. Blaira5dba232016-08-08 15:53:24 -07001682 if job.success_url:
1683 pattern = job.success_url
Tobias Henkel077f2f32017-05-30 20:16:46 +02001684 else:
James E. Blairb7273ef2016-04-19 08:58:51 -07001685 if job.failure_message:
1686 result = job.failure_message
James E. Blaira5dba232016-08-08 15:53:24 -07001687 if job.failure_url:
1688 pattern = job.failure_url
James E. Blair88e79c02017-07-07 13:36:54 -07001689 url = None # The final URL
1690 default_url = build.result_data.get('zuul', {}).get('log_url')
James E. Blairb7273ef2016-04-19 08:58:51 -07001691 if pattern:
James E. Blair88e79c02017-07-07 13:36:54 -07001692 job_url = self.formatUrlPattern(pattern, job, build)
1693 else:
1694 job_url = None
1695 try:
1696 if job_url:
1697 u = urllib.parse.urlparse(job_url)
1698 if u.scheme:
1699 # The job success or failure url is absolute, so it's
1700 # our final url.
1701 url = job_url
1702 else:
1703 # We have a relative job url. Combine it with our
1704 # default url.
1705 if default_url:
1706 url = urllib.parse.urljoin(default_url, job_url)
1707 except Exception:
1708 self.log.exception("Error while parsing url for job %s:"
1709 % (job,))
James E. Blairb7273ef2016-04-19 08:58:51 -07001710 if not url:
James E. Blair88e79c02017-07-07 13:36:54 -07001711 url = default_url or build.url or job.name
James E. Blairb7273ef2016-04-19 08:58:51 -07001712 return (result, url)
1713
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001714 def formatJSON(self, websocket_url=None):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001715 ret = {}
1716 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -08001717 ret['live'] = self.live
Monty Taylor55d0f562017-05-17 14:30:21 -05001718 if hasattr(self.change, 'url') and self.change.url is not None:
1719 ret['url'] = self.change.url
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001720 else:
1721 ret['url'] = None
Monty Taylor55d0f562017-05-17 14:30:21 -05001722 ret['id'] = self.change._id()
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001723 if self.item_ahead:
1724 ret['item_ahead'] = self.item_ahead.change._id()
1725 else:
1726 ret['item_ahead'] = None
1727 ret['items_behind'] = [i.change._id() for i in self.items_behind]
1728 ret['failing_reasons'] = self.current_build_set.failing_reasons
1729 ret['zuul_ref'] = self.current_build_set.ref
Monty Taylor55d0f562017-05-17 14:30:21 -05001730 if self.change.project:
1731 ret['project'] = self.change.project.name
Ramy Asselin07cc33c2015-06-12 14:06:34 -07001732 else:
1733 # For cross-project dependencies with the depends-on
1734 # project not known to zuul, the project is None
1735 # Set it to a static value
1736 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001737 ret['enqueue_time'] = int(self.enqueue_time * 1000)
1738 ret['jobs'] = []
Monty Taylor55d0f562017-05-17 14:30:21 -05001739 if hasattr(self.change, 'owner'):
1740 ret['owner'] = self.change.owner
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001741 else:
1742 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001743 max_remaining = 0
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001744 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001745 now = time.time()
1746 build = self.current_build_set.getBuild(job.name)
1747 elapsed = None
1748 remaining = None
1749 result = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001750 build_url = None
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001751 finger_url = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001752 report_url = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001753 worker = None
1754 if build:
1755 result = build.result
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001756 finger_url = build.url
1757 # TODO(tobiash): add support for custom web root
1758 urlformat = 'static/stream.html?' \
1759 'uuid={build.uuid}&' \
1760 'logfile=console.log'
1761 if websocket_url:
1762 urlformat += '&websocket_url={websocket_url}'
1763 build_url = urlformat.format(
1764 build=build, websocket_url=websocket_url)
James E. Blair800e7ff2017-03-17 16:06:52 -07001765 (unused, report_url) = self.formatJobResult(job)
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001766 if build.start_time:
1767 if build.end_time:
1768 elapsed = int((build.end_time -
1769 build.start_time) * 1000)
1770 remaining = 0
1771 else:
1772 elapsed = int((now - build.start_time) * 1000)
1773 if build.estimated_time:
1774 remaining = max(
1775 int(build.estimated_time * 1000) - elapsed,
1776 0)
1777 worker = {
1778 'name': build.worker.name,
1779 'hostname': build.worker.hostname,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001780 }
1781 if remaining and remaining > max_remaining:
1782 max_remaining = remaining
1783
1784 ret['jobs'].append({
1785 'name': job.name,
Tobias Henkel65639f82017-07-10 10:25:42 +02001786 'dependencies': list(job.dependencies),
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001787 'elapsed_time': elapsed,
1788 'remaining_time': remaining,
James E. Blairb7273ef2016-04-19 08:58:51 -07001789 'url': build_url,
Tobias Henkelb4407fc2017-07-07 13:52:56 +02001790 'finger_url': finger_url,
James E. Blairb7273ef2016-04-19 08:58:51 -07001791 'report_url': report_url,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001792 'result': result,
1793 'voting': job.voting,
1794 'uuid': build.uuid if build else None,
Paul Belanger174a8272017-03-14 13:20:10 -04001795 'execute_time': build.execute_time if build else None,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001796 'start_time': build.start_time if build else None,
1797 'end_time': build.end_time if build else None,
1798 'estimated_time': build.estimated_time if build else None,
1799 'pipeline': build.pipeline.name if build else None,
1800 'canceled': build.canceled if build else None,
1801 'retry': build.retry if build else None,
Timothy Chavezb2332082015-08-07 20:08:04 -05001802 'node_labels': build.node_labels if build else [],
1803 'node_name': build.node_name if build else None,
1804 'worker': worker,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001805 })
1806
James E. Blairdbfd3282016-07-21 10:46:19 -07001807 if self.haveAllJobsStarted():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001808 ret['remaining_time'] = max_remaining
1809 else:
1810 ret['remaining_time'] = None
1811 return ret
1812
1813 def formatStatus(self, indent=0, html=False):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001814 indent_str = ' ' * indent
1815 ret = ''
Monty Taylor55d0f562017-05-17 14:30:21 -05001816 if html and getattr(self.change, 'url', None) is not None:
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001817 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
1818 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001819 self.change.project.name,
1820 self.change.url,
1821 self.change._id())
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001822 else:
1823 ret += '%sProject %s change %s based on %s\n' % (
1824 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001825 self.change.project.name,
1826 self.change._id(),
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001827 self.item_ahead)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001828 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001829 build = self.current_build_set.getBuild(job.name)
1830 if build:
1831 result = build.result
1832 else:
1833 result = None
1834 job_name = job.name
1835 if not job.voting:
1836 voting = ' (non-voting)'
1837 else:
1838 voting = ''
1839 if html:
1840 if build:
1841 url = build.url
1842 else:
1843 url = None
1844 if url is not None:
1845 job_name = '<a href="%s">%s</a>' % (url, job_name)
1846 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
1847 ret += '\n'
1848 return ret
1849
James E. Blaira04b0792017-04-27 09:59:06 -07001850 def makeMergerItem(self):
1851 # Create a dictionary with all info about the item needed by
1852 # the merger.
1853 number = None
1854 patchset = None
1855 oldrev = None
1856 newrev = None
James E. Blair21037782017-07-19 11:56:55 -07001857 branch = None
James E. Blaira04b0792017-04-27 09:59:06 -07001858 if hasattr(self.change, 'number'):
1859 number = self.change.number
1860 patchset = self.change.patchset
James E. Blair21037782017-07-19 11:56:55 -07001861 if hasattr(self.change, 'newrev'):
James E. Blaira04b0792017-04-27 09:59:06 -07001862 oldrev = self.change.oldrev
1863 newrev = self.change.newrev
James E. Blair21037782017-07-19 11:56:55 -07001864 if hasattr(self.change, 'branch'):
1865 branch = self.change.branch
1866
James E. Blaira04b0792017-04-27 09:59:06 -07001867 source = self.change.project.source
1868 connection_name = source.connection.connection_name
James E. Blair2a535672017-04-27 12:03:15 -07001869 project = self.change.project
James E. Blaira04b0792017-04-27 09:59:06 -07001870
James E. Blair2a535672017-04-27 12:03:15 -07001871 return dict(project=project.name,
1872 connection=connection_name,
James E. Blaira04b0792017-04-27 09:59:06 -07001873 merge_mode=self.current_build_set.getMergeMode(),
James E. Blair247cab72017-07-20 16:52:36 -07001874 ref=self.change.ref,
James E. Blaira04b0792017-04-27 09:59:06 -07001875 branch=branch,
James E. Blair247cab72017-07-20 16:52:36 -07001876 buildset_uuid=self.current_build_set.uuid,
James E. Blaira04b0792017-04-27 09:59:06 -07001877 number=number,
1878 patchset=patchset,
1879 oldrev=oldrev,
1880 newrev=newrev,
1881 )
1882
James E. Blairfee8d652013-06-07 08:57:52 -07001883
Clint Byrumf8cc9902017-03-22 22:38:25 -07001884class Ref(object):
1885 """An existing state of a Project."""
James E. Blairfee8d652013-06-07 08:57:52 -07001886
1887 def __init__(self, project):
1888 self.project = project
Clint Byrumf8cc9902017-03-22 22:38:25 -07001889 self.ref = None
1890 self.oldrev = None
1891 self.newrev = None
Jesse Keating71a47ff2017-06-06 11:36:43 -07001892 self.files = []
1893
Clint Byrumf8cc9902017-03-22 22:38:25 -07001894 def _id(self):
1895 return self.newrev
1896
1897 def __repr__(self):
1898 rep = None
1899 if self.newrev == '0000000000000000000000000000000000000000':
James E. Blair21037782017-07-19 11:56:55 -07001900 rep = '<%s 0x%x deletes %s from %s' % (
1901 type(self).__name__,
1902 id(self), self.ref, self.oldrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001903 elif self.oldrev == '0000000000000000000000000000000000000000':
James E. Blair21037782017-07-19 11:56:55 -07001904 rep = '<%s 0x%x creates %s on %s>' % (
1905 type(self).__name__,
1906 id(self), self.ref, self.newrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001907 else:
1908 # Catch all
James E. Blair21037782017-07-19 11:56:55 -07001909 rep = '<%s 0x%x %s updated %s..%s>' % (
1910 type(self).__name__,
1911 id(self), self.ref, self.oldrev, self.newrev)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001912 return rep
1913
James E. Blairfee8d652013-06-07 08:57:52 -07001914 def equals(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001915 if (self.project == other.project
1916 and self.ref == other.ref
1917 and self.newrev == other.newrev):
1918 return True
1919 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001920
1921 def isUpdateOf(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001922 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001923
1924 def filterJobs(self, jobs):
1925 return filter(lambda job: job.changeMatches(self), jobs)
1926
1927 def getRelatedChanges(self):
1928 return set()
1929
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001930 def updatesConfig(self):
Tristan Cacqueray829e6172017-06-13 06:49:36 +00001931 if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files or \
1932 [True for fn in self.files if fn.startswith("zuul.d/") or
1933 fn.startswith(".zuul.d/")]:
Jesse Keating71a47ff2017-06-06 11:36:43 -07001934 return True
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001935 return False
1936
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001937 def getSafeAttributes(self):
1938 return Attributes(project=self.project,
1939 ref=self.ref,
1940 oldrev=self.oldrev,
1941 newrev=self.newrev)
1942
James E. Blair1e8dd892012-05-30 09:15:05 -07001943
James E. Blair21037782017-07-19 11:56:55 -07001944class Branch(Ref):
1945 """An existing branch state for a Project."""
1946 def __init__(self, project):
1947 super(Branch, self).__init__(project)
1948 self.branch = None
1949
1950
1951class Tag(Ref):
1952 """An existing tag state for a Project."""
1953 def __init__(self, project):
1954 super(Tag, self).__init__(project)
1955 self.tag = None
1956
1957
1958class Change(Branch):
Monty Taylora42a55b2016-07-29 07:53:33 -07001959 """A proposed new state for a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001960 def __init__(self, project):
1961 super(Change, self).__init__(project)
James E. Blair4aea70c2012-07-26 14:23:24 -07001962 self.number = None
1963 self.url = None
1964 self.patchset = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001965
James E. Blair6965a4b2014-12-16 17:19:04 -08001966 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -07001967 self.needed_by_changes = []
1968 self.is_current_patchset = True
1969 self.can_merge = False
1970 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -07001971 self.failed_to_merge = False
James E. Blair11041d22014-05-02 14:49:53 -07001972 self.open = None
1973 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001974 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001975
Jan Hruban3b415922016-02-03 13:10:22 +01001976 self.source_event = None
1977
James E. Blair4aea70c2012-07-26 14:23:24 -07001978 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -07001979 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -07001980
1981 def __repr__(self):
1982 return '<Change 0x%x %s>' % (id(self), self._id())
1983
1984 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +08001985 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -07001986 return True
1987 return False
1988
James E. Blair2fa50962013-01-30 21:50:41 -08001989 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -08001990 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -07001991 (hasattr(other, 'patchset') and
1992 self.patchset is not None and
1993 other.patchset is not None and
1994 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -08001995 return True
1996 return False
1997
James E. Blairfee8d652013-06-07 08:57:52 -07001998 def getRelatedChanges(self):
1999 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -08002000 for c in self.needs_changes:
2001 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -07002002 for c in self.needed_by_changes:
2003 related.add(c)
2004 related.update(c.getRelatedChanges())
2005 return related
James E. Blair4aea70c2012-07-26 14:23:24 -07002006
Joshua Hesketh58419cb2017-02-24 13:09:22 -05002007 def getSafeAttributes(self):
2008 return Attributes(project=self.project,
2009 number=self.number,
2010 patchset=self.patchset)
2011
James E. Blair4aea70c2012-07-26 14:23:24 -07002012
James E. Blairee743612012-05-29 14:49:32 -07002013class TriggerEvent(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002014 """Incoming event from an external system."""
James E. Blairee743612012-05-29 14:49:32 -07002015 def __init__(self):
James E. Blairaad3ae22017-05-18 14:11:29 -07002016 # TODO(jeblair): further reduce this list
James E. Blairee743612012-05-29 14:49:32 -07002017 self.data = None
James E. Blair32663402012-06-01 10:04:18 -07002018 # common
James E. Blairee743612012-05-29 14:49:32 -07002019 self.type = None
Jesse Keating71a47ff2017-06-06 11:36:43 -07002020 self.branch_updated = False
James E. Blair72facdc2017-08-17 10:29:12 -07002021 self.branch_created = False
2022 self.branch_deleted = False
James E. Blair247cab72017-07-20 16:52:36 -07002023 self.ref = None
Paul Belangerbaca3132016-11-04 12:49:54 -04002024 # For management events (eg: enqueue / promote)
2025 self.tenant_name = None
James E. Blair6f284b42017-03-31 14:14:41 -07002026 self.project_hostname = None
James E. Blairee743612012-05-29 14:49:32 -07002027 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -07002028 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +01002029 # Representation of the user account that performed the event.
2030 self.account = None
James E. Blair32663402012-06-01 10:04:18 -07002031 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -07002032 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -07002033 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -07002034 self.patch_number = None
James E. Blairee743612012-05-29 14:49:32 -07002035 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07002036 self.comment = None
Jesse Keating5c05a9f2017-01-12 14:44:58 -08002037 self.state = None
James E. Blair32663402012-06-01 10:04:18 -07002038 # ref-updated
James E. Blair32663402012-06-01 10:04:18 -07002039 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07002040 self.newrev = None
James E. Blairad28e912013-11-27 10:43:22 -08002041 # For events that arrive with a destination pipeline (eg, from
2042 # an admin command, etc):
2043 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07002044
James E. Blair6f284b42017-03-31 14:14:41 -07002045 @property
2046 def canonical_project_name(self):
2047 return self.project_hostname + '/' + self.project_name
2048
Jan Hruban324ca5b2015-11-05 19:28:54 +01002049 def isPatchsetCreated(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01002050 return False
2051
2052 def isChangeAbandoned(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01002053 return False
2054
James E. Blair1e8dd892012-05-30 09:15:05 -07002055
James E. Blair9c17dbf2014-06-23 14:21:58 -07002056class BaseFilter(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002057 """Base Class for filtering which Changes and Events to process."""
James E. Blairaad3ae22017-05-18 14:11:29 -07002058 pass
Joshua Hesketh66c8e522014-06-26 15:30:08 +10002059
James E. Blair9c17dbf2014-06-23 14:21:58 -07002060
2061class EventFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002062 """Allows a Pipeline to only respond to certain events."""
James E. Blairaad3ae22017-05-18 14:11:29 -07002063 def __init__(self, trigger):
2064 super(EventFilter, self).__init__()
James E. Blairc0dedf82014-08-06 09:37:52 -07002065 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07002066
James E. Blairaad3ae22017-05-18 14:11:29 -07002067 def matches(self, event, ref):
2068 # TODO(jeblair): consider removing ref argument
James E. Blairee743612012-05-29 14:49:32 -07002069 return True
James E. Blaireff88162013-07-01 12:44:14 -04002070
2071
James E. Blairaad3ae22017-05-18 14:11:29 -07002072class RefFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07002073 """Allows a Manager to only enqueue Changes that meet certain criteria."""
Jesse Keating8c2eb572017-05-30 17:31:45 -07002074 def __init__(self, connection_name):
James E. Blairaad3ae22017-05-18 14:11:29 -07002075 super(RefFilter, self).__init__()
Jesse Keating8c2eb572017-05-30 17:31:45 -07002076 self.connection_name = connection_name
James E. Blair11041d22014-05-02 14:49:53 -07002077
2078 def matches(self, change):
James E. Blair11041d22014-05-02 14:49:53 -07002079 return True
2080
2081
James E. Blairb97ed802015-12-21 15:55:35 -08002082class ProjectPipelineConfig(object):
2083 # Represents a project cofiguration in the context of a pipeline
2084 def __init__(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002085 self.job_list = JobList()
James E. Blairb97ed802015-12-21 15:55:35 -08002086 self.queue_name = None
Adam Gandelman8bd57102016-12-02 12:58:42 -08002087 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08002088
2089
James E. Blair08d9b782017-06-29 14:22:48 -07002090class TenantProjectConfig(object):
2091 """A project in the context of a tenant.
2092
2093 A Project is globally unique in the system, however, when used in
2094 a tenant, some metadata about the project local to the tenant is
2095 stored in a TenantProjectConfig.
2096 """
2097
2098 def __init__(self, project):
2099 self.project = project
2100 self.load_classes = set()
James E. Blair6459db12017-06-29 14:57:20 -07002101 self.shadow_projects = set()
James E. Blair08d9b782017-06-29 14:22:48 -07002102
Tobias Henkeleca46202017-08-02 20:27:10 +02002103 # The tenant's default setting of exclude_unprotected_branches will
2104 # be overridden by this one if not None.
2105 self.exclude_unprotected_branches = None
2106
James E. Blair08d9b782017-06-29 14:22:48 -07002107
James E. Blairb97ed802015-12-21 15:55:35 -08002108class ProjectConfig(object):
2109 # Represents a project cofiguration
2110 def __init__(self, name):
2111 self.name = name
Adam Gandelman8bd57102016-12-02 12:58:42 -08002112 self.merge_mode = None
James E. Blair040b6502017-05-23 10:18:21 -07002113 self.default_branch = None
James E. Blairb97ed802015-12-21 15:55:35 -08002114 self.pipelines = {}
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002115 self.private_key_file = None
James E. Blairb97ed802015-12-21 15:55:35 -08002116
2117
James E. Blair97043882017-09-06 15:51:17 -07002118class ConfigItemNotListError(Exception):
2119 def __init__(self):
2120 message = textwrap.dedent("""\
2121 Configuration file is not a list. Each zuul.yaml configuration
2122 file must be a list of items, for example:
2123
2124 - job:
2125 name: foo
2126
2127 - project:
2128 name: bar
2129
2130 Ensure that every item starts with "- " so that it is parsed as a
2131 YAML list.
2132 """)
2133 super(ConfigItemNotListError, self).__init__(message)
2134
2135
2136class ConfigItemNotDictError(Exception):
2137 def __init__(self):
2138 message = textwrap.dedent("""\
2139 Configuration item is not a dictionary. Each zuul.yaml
2140 configuration file must be a list of dictionaries, for
2141 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 one
2150 key (in this example, 'job' and 'project').
2151 """)
2152 super(ConfigItemNotDictError, self).__init__(message)
2153
2154
2155class ConfigItemMultipleKeysError(Exception):
2156 def __init__(self):
2157 message = textwrap.dedent("""\
2158 Configuration item has more than one key. Each zuul.yaml
2159 configuration file must be a list of dictionaries with a
2160 single key, for example:
2161
2162 - job:
2163 name: foo
2164
2165 - project:
2166 name: bar
2167
2168 Ensure that every item in the list is a dictionary with only
2169 one key (in this example, 'job' and 'project'). This error
2170 may be caused by insufficient indentation of the keys under
2171 the configuration item ('name' in this example).
2172 """)
2173 super(ConfigItemMultipleKeysError, self).__init__(message)
2174
2175
2176class ConfigItemUnknownError(Exception):
2177 def __init__(self):
2178 message = textwrap.dedent("""\
2179 Configuration item not recognized. Each zuul.yaml
2180 configuration file must be a list of dictionaries, for
2181 example:
2182
2183 - job:
2184 name: foo
2185
2186 - project:
2187 name: bar
2188
2189 The dictionary keys must match one of the configuration item
2190 types recognized by zuul (for example, 'job' or 'project').
2191 """)
2192 super(ConfigItemUnknownError, self).__init__(message)
2193
2194
James E. Blaird8e778f2015-12-22 14:09:20 -08002195class UnparsedAbideConfig(object):
James E. Blair08d9b782017-06-29 14:22:48 -07002196
Monty Taylora42a55b2016-07-29 07:53:33 -07002197 """A collection of yaml lists that has not yet been parsed into objects.
2198
2199 An Abide is a collection of tenants.
2200 """
2201
James E. Blaird8e778f2015-12-22 14:09:20 -08002202 def __init__(self):
2203 self.tenants = []
2204
2205 def extend(self, conf):
2206 if isinstance(conf, UnparsedAbideConfig):
2207 self.tenants.extend(conf.tenants)
2208 return
2209
2210 if not isinstance(conf, list):
James E. Blair97043882017-09-06 15:51:17 -07002211 raise ConfigItemNotListError()
2212
James E. Blaird8e778f2015-12-22 14:09:20 -08002213 for item in conf:
2214 if not isinstance(item, dict):
James E. Blair97043882017-09-06 15:51:17 -07002215 raise ConfigItemNotDictError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002216 if len(item.keys()) > 1:
James E. Blair97043882017-09-06 15:51:17 -07002217 raise ConfigItemMultipleKeysError()
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002218 key, value = list(item.items())[0]
James E. Blaird8e778f2015-12-22 14:09:20 -08002219 if key == 'tenant':
2220 self.tenants.append(value)
2221 else:
James E. Blair97043882017-09-06 15:51:17 -07002222 raise ConfigItemUnknownError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002223
2224
2225class UnparsedTenantConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002226 """A collection of yaml lists that has not yet been parsed into objects."""
2227
James E. Blaird8e778f2015-12-22 14:09:20 -08002228 def __init__(self):
2229 self.pipelines = []
2230 self.jobs = []
2231 self.project_templates = []
James E. Blairff555742017-02-19 11:34:27 -08002232 self.projects = {}
James E. Blaira98340f2016-09-02 11:33:49 -07002233 self.nodesets = []
James E. Blair01f83b72017-03-15 13:03:40 -07002234 self.secrets = []
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002235 self.semaphores = []
James E. Blaird8e778f2015-12-22 14:09:20 -08002236
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002237 def copy(self):
2238 r = UnparsedTenantConfig()
2239 r.pipelines = copy.deepcopy(self.pipelines)
2240 r.jobs = copy.deepcopy(self.jobs)
2241 r.project_templates = copy.deepcopy(self.project_templates)
2242 r.projects = copy.deepcopy(self.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07002243 r.nodesets = copy.deepcopy(self.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002244 r.secrets = copy.deepcopy(self.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002245 r.semaphores = copy.deepcopy(self.semaphores)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002246 return r
2247
James E. Blairec7ff302017-03-04 07:31:32 -08002248 def extend(self, conf):
James E. Blaird8e778f2015-12-22 14:09:20 -08002249 if isinstance(conf, UnparsedTenantConfig):
2250 self.pipelines.extend(conf.pipelines)
2251 self.jobs.extend(conf.jobs)
2252 self.project_templates.extend(conf.project_templates)
James E. Blairff555742017-02-19 11:34:27 -08002253 for k, v in conf.projects.items():
2254 self.projects.setdefault(k, []).extend(v)
James E. Blaira98340f2016-09-02 11:33:49 -07002255 self.nodesets.extend(conf.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002256 self.secrets.extend(conf.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002257 self.semaphores.extend(conf.semaphores)
James E. Blaird8e778f2015-12-22 14:09:20 -08002258 return
2259
2260 if not isinstance(conf, list):
James E. Blair97043882017-09-06 15:51:17 -07002261 raise ConfigItemNotListError()
James E. Blaircdab2032017-02-01 09:09:29 -08002262
James E. Blaird8e778f2015-12-22 14:09:20 -08002263 for item in conf:
2264 if not isinstance(item, dict):
James E. Blair97043882017-09-06 15:51:17 -07002265 raise ConfigItemNotDictError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002266 if len(item.keys()) > 1:
James E. Blair97043882017-09-06 15:51:17 -07002267 raise ConfigItemMultipleKeysError()
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002268 key, value = list(item.items())[0]
James E. Blair66b274e2017-01-31 14:47:52 -08002269 if key == 'project':
James E. Blairff555742017-02-19 11:34:27 -08002270 name = value['name']
2271 self.projects.setdefault(name, []).append(value)
James E. Blair66b274e2017-01-31 14:47:52 -08002272 elif key == 'job':
James E. Blaird8e778f2015-12-22 14:09:20 -08002273 self.jobs.append(value)
2274 elif key == 'project-template':
2275 self.project_templates.append(value)
2276 elif key == 'pipeline':
2277 self.pipelines.append(value)
James E. Blaira98340f2016-09-02 11:33:49 -07002278 elif key == 'nodeset':
2279 self.nodesets.append(value)
James E. Blair01f83b72017-03-15 13:03:40 -07002280 elif key == 'secret':
2281 self.secrets.append(value)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002282 elif key == 'semaphore':
2283 self.semaphores.append(value)
James E. Blaird8e778f2015-12-22 14:09:20 -08002284 else:
James E. Blair97043882017-09-06 15:51:17 -07002285 raise ConfigItemUnknownError()
James E. Blaird8e778f2015-12-22 14:09:20 -08002286
2287
James E. Blaireff88162013-07-01 12:44:14 -04002288class Layout(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002289 """Holds all of the Pipelines."""
2290
James E. Blair6459db12017-06-29 14:57:20 -07002291 def __init__(self, tenant):
2292 self.tenant = tenant
James E. Blairb97ed802015-12-21 15:55:35 -08002293 self.project_configs = {}
2294 self.project_templates = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07002295 self.pipelines = OrderedDict()
James E. Blair83005782015-12-11 14:46:03 -08002296 # This is a dictionary of name -> [jobs]. The first element
2297 # of the list is the first job added with that name. It is
2298 # the reference definition for a given job. Subsequent
2299 # elements are aspects of that job with different matchers
2300 # that override some attribute of the job. These aspects all
2301 # inherit from the reference definition.
James E. Blairfef88ec2016-11-30 08:38:27 -08002302 self.jobs = {'noop': [Job('noop')]}
James E. Blaira98340f2016-09-02 11:33:49 -07002303 self.nodesets = {}
James E. Blair01f83b72017-03-15 13:03:40 -07002304 self.secrets = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002305 self.semaphores = {}
James E. Blaireff88162013-07-01 12:44:14 -04002306
2307 def getJob(self, name):
2308 if name in self.jobs:
James E. Blair83005782015-12-11 14:46:03 -08002309 return self.jobs[name][0]
James E. Blairb97ed802015-12-21 15:55:35 -08002310 raise Exception("Job %s not defined" % (name,))
James E. Blair83005782015-12-11 14:46:03 -08002311
James E. Blair2bab6e72017-08-07 09:52:45 -07002312 def hasJob(self, name):
2313 return name in self.jobs
2314
James E. Blair83005782015-12-11 14:46:03 -08002315 def getJobs(self, name):
2316 return self.jobs.get(name, [])
2317
2318 def addJob(self, job):
James E. Blair4317e9f2016-07-15 10:05:47 -07002319 # We can have multiple variants of a job all with the same
2320 # name, but these variants must all be defined in the same repo.
James E. Blaircdab2032017-02-01 09:09:29 -08002321 prior_jobs = [j for j in self.getJobs(job.name) if
2322 j.source_context.project !=
2323 job.source_context.project]
James E. Blair6459db12017-06-29 14:57:20 -07002324 # Unless the repo is permitted to shadow another. If so, and
2325 # the job we are adding is from a repo that is permitted to
2326 # shadow the one with the older jobs, skip adding this job.
2327 job_project = job.source_context.project
2328 job_tpc = self.tenant.project_configs[job_project.canonical_name]
2329 skip_add = False
2330 for prior_job in prior_jobs[:]:
2331 prior_project = prior_job.source_context.project
2332 if prior_project in job_tpc.shadow_projects:
2333 prior_jobs.remove(prior_job)
2334 skip_add = True
2335
James E. Blair4317e9f2016-07-15 10:05:47 -07002336 if prior_jobs:
2337 raise Exception("Job %s in %s is not permitted to shadow "
James E. Blaircdab2032017-02-01 09:09:29 -08002338 "job %s in %s" % (
2339 job,
2340 job.source_context.project,
2341 prior_jobs[0],
2342 prior_jobs[0].source_context.project))
James E. Blair6459db12017-06-29 14:57:20 -07002343 if skip_add:
2344 return False
James E. Blair83005782015-12-11 14:46:03 -08002345 if job.name in self.jobs:
2346 self.jobs[job.name].append(job)
2347 else:
2348 self.jobs[job.name] = [job]
James E. Blair6459db12017-06-29 14:57:20 -07002349 return True
James E. Blair83005782015-12-11 14:46:03 -08002350
James E. Blaira98340f2016-09-02 11:33:49 -07002351 def addNodeSet(self, nodeset):
2352 if nodeset.name in self.nodesets:
2353 raise Exception("NodeSet %s already defined" % (nodeset.name,))
2354 self.nodesets[nodeset.name] = nodeset
2355
James E. Blair01f83b72017-03-15 13:03:40 -07002356 def addSecret(self, secret):
2357 if secret.name in self.secrets:
2358 raise Exception("Secret %s already defined" % (secret.name,))
2359 self.secrets[secret.name] = secret
2360
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002361 def addSemaphore(self, semaphore):
2362 if semaphore.name in self.semaphores:
2363 raise Exception("Semaphore %s already defined" % (semaphore.name,))
2364 self.semaphores[semaphore.name] = semaphore
2365
James E. Blair83005782015-12-11 14:46:03 -08002366 def addPipeline(self, pipeline):
2367 self.pipelines[pipeline.name] = pipeline
James E. Blair59fdbac2015-12-07 17:08:06 -08002368
James E. Blairb97ed802015-12-21 15:55:35 -08002369 def addProjectTemplate(self, project_template):
2370 self.project_templates[project_template.name] = project_template
2371
James E. Blairf59f3cf2017-02-19 14:50:26 -08002372 def addProjectConfig(self, project_config):
James E. Blairb97ed802015-12-21 15:55:35 -08002373 self.project_configs[project_config.name] = project_config
James E. Blairb97ed802015-12-21 15:55:35 -08002374
James E. Blaird2348362017-03-17 13:59:35 -07002375 def _createJobGraph(self, item, job_list, job_graph):
2376 change = item.change
2377 pipeline = item.pipeline
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002378 for jobname in job_list.jobs:
2379 # This is the final job we are constructing
James E. Blaira7f51ca2017-02-07 16:01:26 -08002380 frozen_job = None
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002381 # Whether the change matches any globally defined variant
James E. Blaira7f51ca2017-02-07 16:01:26 -08002382 matched = False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002383 for variant in self.getJobs(jobname):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002384 if variant.changeMatches(change):
James E. Blaira7f51ca2017-02-07 16:01:26 -08002385 if frozen_job is None:
2386 frozen_job = variant.copy()
2387 frozen_job.setRun()
2388 else:
2389 frozen_job.applyVariant(variant)
2390 matched = True
2391 if not matched:
James E. Blair6e85c2b2016-11-21 16:47:01 -08002392 # A change must match at least one defined job variant
2393 # (that is to say that it must match more than just
2394 # the job that is defined in the tree).
2395 continue
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002396 # Whether the change matches any of the project pipeline
2397 # variants
2398 matched = False
2399 for variant in job_list.jobs[jobname]:
2400 if variant.changeMatches(change):
2401 frozen_job.applyVariant(variant)
2402 matched = True
2403 if not matched:
2404 # A change must match at least one project pipeline
2405 # job variant.
2406 continue
James E. Blairb3f5db12017-03-17 12:57:39 -07002407 if (frozen_job.allowed_projects and
2408 change.project.name not in frozen_job.allowed_projects):
2409 raise Exception("Project %s is not allowed to run job %s" %
2410 (change.project.name, frozen_job.name))
James E. Blair8eb564a2017-08-10 09:21:41 -07002411 if ((not pipeline.post_review) and frozen_job.post_review):
2412 raise Exception("Pre-review pipeline %s does not allow "
2413 "post-review job %s" % (
James E. Blaird2348362017-03-17 13:59:35 -07002414 pipeline.name, frozen_job.name))
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002415 job_graph.addJob(frozen_job)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002416
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002417 def createJobGraph(self, item):
Paul Belanger15e3e202016-10-14 16:27:34 -04002418 # NOTE(pabelanger): It is possible for a foreign project not to have a
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002419 # configured pipeline, if so return an empty JobGraph.
James E. Blairc9455002017-09-06 09:22:19 -07002420 ret = JobGraph()
2421 ppc = self.getProjectPipelineConfig(item.change.project,
2422 item.pipeline)
2423 if ppc:
2424 self._createJobGraph(item, ppc.job_list, ret)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002425 return ret
2426
James E. Blairc9455002017-09-06 09:22:19 -07002427 def getProjectPipelineConfig(self, project, pipeline):
2428 project_config = self.project_configs.get(
2429 project.canonical_name, None)
2430 if not project_config:
2431 return None
2432 return project_config.pipelines.get(pipeline.name, None)
James E. Blair0d3e83b2017-06-05 13:51:57 -07002433
James E. Blair59fdbac2015-12-07 17:08:06 -08002434
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002435class Semaphore(object):
2436 def __init__(self, name, max=1):
2437 self.name = name
2438 self.max = int(max)
2439
2440
2441class SemaphoreHandler(object):
2442 log = logging.getLogger("zuul.SemaphoreHandler")
2443
2444 def __init__(self):
2445 self.semaphores = {}
2446
2447 def acquire(self, item, job):
2448 if not job.semaphore:
2449 return True
2450
2451 semaphore_key = job.semaphore
2452
2453 m = self.semaphores.get(semaphore_key)
2454 if not m:
2455 # The semaphore is not held, acquire it
2456 self._acquire(semaphore_key, item, job.name)
2457 return True
2458 if (item, job.name) in m:
2459 # This item already holds the semaphore
2460 return True
2461
2462 # semaphore is there, check max
2463 if len(m) < self._max_count(item, job.semaphore):
2464 self._acquire(semaphore_key, item, job.name)
2465 return True
2466
2467 return False
2468
2469 def release(self, item, job):
2470 if not job.semaphore:
2471 return
2472
2473 semaphore_key = job.semaphore
2474
2475 m = self.semaphores.get(semaphore_key)
2476 if not m:
2477 # The semaphore is not held, nothing to do
2478 self.log.error("Semaphore can not be released for %s "
2479 "because the semaphore is not held" %
2480 item)
2481 return
2482 if (item, job.name) in m:
2483 # This item is a holder of the semaphore
2484 self._release(semaphore_key, item, job.name)
2485 return
2486 self.log.error("Semaphore can not be released for %s "
2487 "which does not hold it" % item)
2488
2489 def _acquire(self, semaphore_key, item, job_name):
2490 self.log.debug("Semaphore acquire {semaphore}: job {job}, item {item}"
2491 .format(semaphore=semaphore_key,
2492 job=job_name,
2493 item=item))
2494 if semaphore_key not in self.semaphores:
2495 self.semaphores[semaphore_key] = []
2496 self.semaphores[semaphore_key].append((item, job_name))
2497
2498 def _release(self, semaphore_key, item, job_name):
2499 self.log.debug("Semaphore release {semaphore}: job {job}, item {item}"
2500 .format(semaphore=semaphore_key,
2501 job=job_name,
2502 item=item))
2503 sem_item = (item, job_name)
2504 if sem_item in self.semaphores[semaphore_key]:
2505 self.semaphores[semaphore_key].remove(sem_item)
2506
2507 # cleanup if there is no user of the semaphore anymore
2508 if len(self.semaphores[semaphore_key]) == 0:
2509 del self.semaphores[semaphore_key]
2510
2511 @staticmethod
2512 def _max_count(item, semaphore_name):
2513 if not item.current_build_set.layout:
2514 # This should not occur as the layout of the item must already be
2515 # built when acquiring or releasing a semaphore for a job.
2516 raise Exception("Item {} has no layout".format(item))
2517
2518 # find the right semaphore
2519 default_semaphore = Semaphore(semaphore_name, 1)
2520 semaphores = item.current_build_set.layout.semaphores
2521 return semaphores.get(semaphore_name, default_semaphore).max
2522
2523
James E. Blair59fdbac2015-12-07 17:08:06 -08002524class Tenant(object):
2525 def __init__(self, name):
2526 self.name = name
Tristan Cacqueray82f864b2017-08-01 05:54:42 +00002527 self.max_nodes_per_job = 5
Tristan Cacquerayc98bff72017-09-10 15:25:26 +00002528 self.max_job_timeout = 10800
Tobias Henkeleca46202017-08-02 20:27:10 +02002529 self.exclude_unprotected_branches = False
James E. Blair2bab6e72017-08-07 09:52:45 -07002530 self.default_base_job = None
James E. Blair59fdbac2015-12-07 17:08:06 -08002531 self.layout = None
James E. Blair646322f2017-01-27 15:50:34 -08002532 # The unparsed configuration from the main zuul config for
2533 # this tenant.
2534 self.unparsed_config = None
James E. Blair109da3f2017-04-04 14:39:43 -07002535 # The list of projects from which we will read full
James E. Blair8a395f92017-03-30 11:15:33 -07002536 # configuration.
James E. Blair109da3f2017-04-04 14:39:43 -07002537 self.config_projects = []
2538 # The unparsed config from those projects.
2539 self.config_projects_config = None
2540 # The list of projects from which we will read untrusted
2541 # in-repo configuration.
2542 self.untrusted_projects = []
2543 # The unparsed config from those projects.
2544 self.untrusted_projects_config = None
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002545 self.semaphore_handler = SemaphoreHandler()
James E. Blair08d9b782017-06-29 14:22:48 -07002546 # Metadata about projects for this tenant
2547 # canonical project name -> TenantProjectConfig
2548 self.project_configs = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002549
James E. Blairc2a54fd2017-03-29 15:19:26 -07002550 # A mapping of project names to projects. project_name ->
2551 # VALUE where VALUE is a further dictionary of
2552 # canonical_hostname -> Project.
2553 self.projects = {}
2554 self.canonical_hostnames = set()
2555
James E. Blair08d9b782017-06-29 14:22:48 -07002556 def _addProject(self, tpc):
James E. Blairc2a54fd2017-03-29 15:19:26 -07002557 """Add a project to the project index
2558
James E. Blair08d9b782017-06-29 14:22:48 -07002559 :arg TenantProjectConfig tpc: The TenantProjectConfig (with
2560 associated project) to add.
2561
James E. Blairc2a54fd2017-03-29 15:19:26 -07002562 """
James E. Blair08d9b782017-06-29 14:22:48 -07002563 project = tpc.project
James E. Blairc2a54fd2017-03-29 15:19:26 -07002564 self.canonical_hostnames.add(project.canonical_hostname)
2565 hostname_dict = self.projects.setdefault(project.name, {})
2566 if project.canonical_hostname in hostname_dict:
2567 raise Exception("Project %s is already in project index" %
2568 (project,))
2569 hostname_dict[project.canonical_hostname] = project
James E. Blair08d9b782017-06-29 14:22:48 -07002570 self.project_configs[project.canonical_name] = tpc
James E. Blairc2a54fd2017-03-29 15:19:26 -07002571
2572 def getProject(self, name):
2573 """Return a project given its name.
2574
2575 :arg str name: The name of the project. It may be fully
2576 qualified (E.g., "git.example.com/subpath/project") or may
2577 contain only the project name name may be supplied (E.g.,
2578 "subpath/project").
2579
2580 :returns: A tuple (trusted, project) or (None, None) if the
2581 project is not found or ambiguous. The "trusted" boolean
2582 indicates whether or not the project is trusted by this
2583 tenant.
2584 :rtype: (bool, Project)
2585
2586 """
2587 path = name.split('/', 1)
2588 if path[0] in self.canonical_hostnames:
2589 hostname = path[0]
2590 project_name = path[1]
2591 else:
2592 hostname = None
2593 project_name = name
2594 hostname_dict = self.projects.get(project_name)
2595 project = None
2596 if hostname_dict:
2597 if hostname:
2598 project = hostname_dict.get(hostname)
2599 else:
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002600 values = list(hostname_dict.values())
James E. Blairc2a54fd2017-03-29 15:19:26 -07002601 if len(values) == 1:
2602 project = values[0]
2603 else:
2604 raise Exception("Project name '%s' is ambiguous, "
2605 "please fully qualify the project "
2606 "with a hostname" % (name,))
2607 if project is None:
2608 return (None, None)
James E. Blair109da3f2017-04-04 14:39:43 -07002609 if project in self.config_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002610 return (True, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002611 if project in self.untrusted_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002612 return (False, project)
2613 # This should never happen:
2614 raise Exception("Project %s is neither trusted nor untrusted" %
2615 (project,))
2616
James E. Blair08d9b782017-06-29 14:22:48 -07002617 def addConfigProject(self, tpc):
2618 self.config_projects.append(tpc.project)
2619 self._addProject(tpc)
James E. Blair5ac93842017-01-20 06:47:34 -08002620
James E. Blair08d9b782017-06-29 14:22:48 -07002621 def addUntrustedProject(self, tpc):
2622 self.untrusted_projects.append(tpc.project)
2623 self._addProject(tpc)
James E. Blair5ac93842017-01-20 06:47:34 -08002624
Jamie Lennoxbf997c82017-05-10 09:58:40 +10002625 def getSafeAttributes(self):
2626 return Attributes(name=self.name)
2627
James E. Blair59fdbac2015-12-07 17:08:06 -08002628
2629class Abide(object):
2630 def __init__(self):
2631 self.tenants = OrderedDict()
James E. Blairce8a2132016-05-19 15:21:52 -07002632
2633
2634class JobTimeData(object):
2635 format = 'B10H10H10B'
2636 version = 0
2637
2638 def __init__(self, path):
2639 self.path = path
2640 self.success_times = [0 for x in range(10)]
2641 self.failure_times = [0 for x in range(10)]
2642 self.results = [0 for x in range(10)]
2643
2644 def load(self):
2645 if not os.path.exists(self.path):
2646 return
Clint Byruma4471d12017-05-10 20:57:40 -04002647 with open(self.path, 'rb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002648 data = struct.unpack(self.format, f.read())
2649 version = data[0]
2650 if version != self.version:
2651 raise Exception("Unkown data version")
2652 self.success_times = list(data[1:11])
2653 self.failure_times = list(data[11:21])
2654 self.results = list(data[21:32])
2655
2656 def save(self):
2657 tmpfile = self.path + '.tmp'
2658 data = [self.version]
2659 data.extend(self.success_times)
2660 data.extend(self.failure_times)
2661 data.extend(self.results)
2662 data = struct.pack(self.format, *data)
Clint Byruma4471d12017-05-10 20:57:40 -04002663 with open(tmpfile, 'wb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002664 f.write(data)
2665 os.rename(tmpfile, self.path)
2666
2667 def add(self, elapsed, result):
2668 elapsed = int(elapsed)
2669 if result == 'SUCCESS':
2670 self.success_times.append(elapsed)
2671 self.success_times.pop(0)
2672 result = 0
2673 else:
2674 self.failure_times.append(elapsed)
2675 self.failure_times.pop(0)
2676 result = 1
2677 self.results.append(result)
2678 self.results.pop(0)
2679
2680 def getEstimatedTime(self):
2681 times = [x for x in self.success_times if x]
2682 if times:
2683 return float(sum(times)) / len(times)
2684 return 0.0
2685
2686
2687class TimeDataBase(object):
2688 def __init__(self, root):
2689 self.root = root
James E. Blairce8a2132016-05-19 15:21:52 -07002690
James E. Blairae0f23c2017-09-13 10:55:15 -06002691 def _getTD(self, build):
2692 if hasattr(build.build_set.item.change, 'branch'):
2693 branch = build.build_set.item.change.branch
2694 else:
2695 branch = ''
2696
2697 dir_path = os.path.join(
2698 self.root,
2699 build.build_set.item.pipeline.layout.tenant.name,
2700 build.build_set.item.change.project.canonical_name,
2701 branch)
2702 if not os.path.exists(dir_path):
2703 os.makedirs(dir_path)
2704 path = os.path.join(dir_path, build.job.name)
2705
2706 td = JobTimeData(path)
2707 td.load()
James E. Blairce8a2132016-05-19 15:21:52 -07002708 return td
2709
2710 def getEstimatedTime(self, name):
2711 return self._getTD(name).getEstimatedTime()
2712
James E. Blairae0f23c2017-09-13 10:55:15 -06002713 def update(self, build, elapsed, result):
2714 td = self._getTD(build)
James E. Blairce8a2132016-05-19 15:21:52 -07002715 td.add(elapsed, result)
2716 td.save()