blob: 59f5531272fbcfbf25cabd6bf04519be595bda0b [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
James E. Blair1b265312014-06-24 09:35:21 -070016import copy
Tobias Henkel9a0e1942017-03-20 16:16:02 +010017import logging
James E. Blairce8a2132016-05-19 15:21:52 -070018import os
James E. Blairce8a2132016-05-19 15:21:52 -070019import struct
James E. Blairff986a12012-05-30 14:56:51 -070020import time
James E. Blair4886cc12012-07-18 15:39:41 -070021from uuid import uuid4
James E. Blair5a9918a2013-08-27 10:06:27 -070022import extras
23
James E. Blair5ac93842017-01-20 06:47:34 -080024import six
25
James E. Blair5a9918a2013-08-27 10:06:27 -070026OrderedDict = extras.try_imports(['collections.OrderedDict',
27 'ordereddict.OrderedDict'])
James E. Blair4886cc12012-07-18 15:39:41 -070028
29
James E. Blair19deff22013-08-25 13:17:35 -070030MERGER_MERGE = 1 # "git merge"
31MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
32MERGER_CHERRY_PICK = 3 # "git cherry-pick"
33
34MERGER_MAP = {
35 'merge': MERGER_MERGE,
36 'merge-resolve': MERGER_MERGE_RESOLVE,
37 'cherry-pick': MERGER_CHERRY_PICK,
38}
James E. Blairee743612012-05-29 14:49:32 -070039
James E. Blair64ed6f22013-07-10 14:07:23 -070040PRECEDENCE_NORMAL = 0
41PRECEDENCE_LOW = 1
42PRECEDENCE_HIGH = 2
43
44PRECEDENCE_MAP = {
45 None: PRECEDENCE_NORMAL,
46 'low': PRECEDENCE_LOW,
47 'normal': PRECEDENCE_NORMAL,
48 'high': PRECEDENCE_HIGH,
49}
50
James E. Blair803e94f2017-01-06 09:18:59 -080051# Request states
52STATE_REQUESTED = 'requested'
53STATE_PENDING = 'pending'
54STATE_FULFILLED = 'fulfilled'
55STATE_FAILED = 'failed'
56REQUEST_STATES = set([STATE_REQUESTED,
57 STATE_PENDING,
58 STATE_FULFILLED,
59 STATE_FAILED])
60
61# Node states
62STATE_BUILDING = 'building'
63STATE_TESTING = 'testing'
64STATE_READY = 'ready'
65STATE_IN_USE = 'in-use'
66STATE_USED = 'used'
67STATE_HOLD = 'hold'
68STATE_DELETING = 'deleting'
69NODE_STATES = set([STATE_BUILDING,
70 STATE_TESTING,
71 STATE_READY,
72 STATE_IN_USE,
73 STATE_USED,
74 STATE_HOLD,
75 STATE_DELETING])
76
James E. Blair1e8dd892012-05-30 09:15:05 -070077
Joshua Hesketh58419cb2017-02-24 13:09:22 -050078class Attributes(object):
79 """A class to hold attributes for string formatting."""
80
81 def __init__(self, **kw):
82 setattr(self, '__dict__', kw)
83
84
James E. Blair4aea70c2012-07-26 14:23:24 -070085class Pipeline(object):
James E. Blair6053de42017-04-05 11:27:11 -070086 """A configuration that ties together triggers, reporters and managers
Monty Taylor82dfd412016-07-29 12:01:28 -070087
88 Trigger
89 A description of which events should be processed
90
91 Manager
92 Responsible for enqueing and dequeing Changes
93
94 Reporter
95 Communicates success and failure results somewhere
Monty Taylora42a55b2016-07-29 07:53:33 -070096 """
James E. Blair83005782015-12-11 14:46:03 -080097 def __init__(self, name, layout):
James E. Blair4aea70c2012-07-26 14:23:24 -070098 self.name = name
James E. Blair83005782015-12-11 14:46:03 -080099 self.layout = layout
James E. Blair8dbd56a2012-12-22 10:55:10 -0800100 self.description = None
James E. Blair56370192013-01-14 15:47:28 -0800101 self.failure_message = None
Joshua Heskethb7179772014-01-30 23:30:46 +1100102 self.merge_failure_message = None
James E. Blair56370192013-01-14 15:47:28 -0800103 self.success_message = None
Joshua Hesketh3979e3e2014-03-04 11:21:10 +1100104 self.footer_message = None
James E. Blair60af7f42016-03-11 16:11:06 -0800105 self.start_message = None
James E. Blaird2348362017-03-17 13:59:35 -0700106 self.allow_secrets = False
James E. Blair2fa50962013-01-30 21:50:41 -0800107 self.dequeue_on_new_patchset = True
James E. Blair17dd6772015-02-09 14:45:18 -0800108 self.ignore_dependencies = False
James E. Blair4aea70c2012-07-26 14:23:24 -0700109 self.manager = None
James E. Blaire0487072012-08-29 17:38:31 -0700110 self.queues = []
James E. Blair64ed6f22013-07-10 14:07:23 -0700111 self.precedence = PRECEDENCE_NORMAL
James E. Blair83005782015-12-11 14:46:03 -0800112 self.triggers = []
Joshua Hesketh352264b2015-08-11 23:42:08 +1000113 self.start_actions = []
114 self.success_actions = []
115 self.failure_actions = []
116 self.merge_failure_actions = []
117 self.disabled_actions = []
Joshua Hesketh89e829d2015-02-10 16:29:45 +1100118 self.disable_at = None
119 self._consecutive_failures = 0
120 self._disabled = False
Clark Boylan7603a372014-01-21 11:43:20 -0800121 self.window = None
122 self.window_floor = None
123 self.window_increase_type = None
124 self.window_increase_factor = None
125 self.window_decrease_type = None
126 self.window_decrease_factor = None
James E. Blair4aea70c2012-07-26 14:23:24 -0700127
James E. Blair83005782015-12-11 14:46:03 -0800128 @property
129 def actions(self):
130 return (
131 self.start_actions +
132 self.success_actions +
133 self.failure_actions +
134 self.merge_failure_actions +
135 self.disabled_actions
136 )
137
James E. Blaird09c17a2012-08-07 09:23:14 -0700138 def __repr__(self):
139 return '<Pipeline %s>' % self.name
140
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100141 def getSafeAttributes(self):
142 return Attributes(name=self.name)
143
James E. Blair4aea70c2012-07-26 14:23:24 -0700144 def setManager(self, manager):
145 self.manager = manager
146
James E. Blaire0487072012-08-29 17:38:31 -0700147 def addQueue(self, queue):
148 self.queues.append(queue)
149
150 def getQueue(self, project):
151 for queue in self.queues:
152 if project in queue.projects:
153 return queue
154 return None
155
James E. Blairbfb8e042014-12-30 17:01:44 -0800156 def removeQueue(self, queue):
157 self.queues.remove(queue)
158
James E. Blaire0487072012-08-29 17:38:31 -0700159 def getChangesInQueue(self):
160 changes = []
161 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700162 changes.extend([x.change for x in shared_queue.queue])
James E. Blaire0487072012-08-29 17:38:31 -0700163 return changes
164
James E. Blairfee8d652013-06-07 08:57:52 -0700165 def getAllItems(self):
166 items = []
James E. Blaire0487072012-08-29 17:38:31 -0700167 for shared_queue in self.queues:
James E. Blairfee8d652013-06-07 08:57:52 -0700168 items.extend(shared_queue.queue)
James E. Blairfee8d652013-06-07 08:57:52 -0700169 return items
James E. Blaire0487072012-08-29 17:38:31 -0700170
James E. Blair800e7ff2017-03-17 16:06:52 -0700171 def formatStatusJSON(self):
James E. Blair8dbd56a2012-12-22 10:55:10 -0800172 j_pipeline = dict(name=self.name,
173 description=self.description)
174 j_queues = []
175 j_pipeline['change_queues'] = j_queues
176 for queue in self.queues:
177 j_queue = dict(name=queue.name)
178 j_queues.append(j_queue)
179 j_queue['heads'] = []
Clark Boylanaf2476f2014-01-23 14:47:36 -0800180 j_queue['window'] = queue.window
James E. Blair972e3c72013-08-29 12:04:55 -0700181
182 j_changes = []
183 for e in queue.queue:
184 if not e.item_ahead:
185 if j_changes:
186 j_queue['heads'].append(j_changes)
187 j_changes = []
James E. Blair800e7ff2017-03-17 16:06:52 -0700188 j_changes.append(e.formatJSON())
James E. Blair972e3c72013-08-29 12:04:55 -0700189 if (len(j_changes) > 1 and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000190 (j_changes[-2]['remaining_time'] is not None) and
191 (j_changes[-1]['remaining_time'] is not None)):
James E. Blair972e3c72013-08-29 12:04:55 -0700192 j_changes[-1]['remaining_time'] = max(
193 j_changes[-2]['remaining_time'],
194 j_changes[-1]['remaining_time'])
195 if j_changes:
James E. Blair8dbd56a2012-12-22 10:55:10 -0800196 j_queue['heads'].append(j_changes)
197 return j_pipeline
198
James E. Blair4aea70c2012-07-26 14:23:24 -0700199
James E. Blairee743612012-05-29 14:49:32 -0700200class ChangeQueue(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700201 """A ChangeQueue contains Changes to be processed related projects.
202
Monty Taylor82dfd412016-07-29 12:01:28 -0700203 A Pipeline with a DependentPipelineManager has multiple parallel
204 ChangeQueues shared by different projects. For instance, there may a
205 ChangeQueue shared by interrelated projects foo and bar, and a second queue
206 for independent project baz.
Monty Taylora42a55b2016-07-29 07:53:33 -0700207
Monty Taylor82dfd412016-07-29 12:01:28 -0700208 A Pipeline with an IndependentPipelineManager puts every Change into its
209 own ChangeQueue
Monty Taylora42a55b2016-07-29 07:53:33 -0700210
211 The ChangeQueue Window is inspired by TCP windows and controlls how many
212 Changes in a given ChangeQueue will be considered active and ready to
213 be processed. If a Change succeeds, the Window is increased by
214 `window_increase_factor`. If a Change fails, the Window is decreased by
215 `window_decrease_factor`.
216 """
James E. Blairbfb8e042014-12-30 17:01:44 -0800217 def __init__(self, pipeline, window=0, window_floor=1,
Clark Boylan7603a372014-01-21 11:43:20 -0800218 window_increase_type='linear', window_increase_factor=1,
James E. Blair0dcef7a2016-08-19 09:35:17 -0700219 window_decrease_type='exponential', window_decrease_factor=2,
220 name=None):
James E. Blair4aea70c2012-07-26 14:23:24 -0700221 self.pipeline = pipeline
James E. Blair0dcef7a2016-08-19 09:35:17 -0700222 if name:
223 self.name = name
224 else:
225 self.name = ''
James E. Blairee743612012-05-29 14:49:32 -0700226 self.projects = []
227 self._jobs = set()
228 self.queue = []
Clark Boylan7603a372014-01-21 11:43:20 -0800229 self.window = window
230 self.window_floor = window_floor
231 self.window_increase_type = window_increase_type
232 self.window_increase_factor = window_increase_factor
233 self.window_decrease_type = window_decrease_type
234 self.window_decrease_factor = window_decrease_factor
James E. Blairee743612012-05-29 14:49:32 -0700235
James E. Blair9f9667e2012-06-12 17:51:08 -0700236 def __repr__(self):
James E. Blair4aea70c2012-07-26 14:23:24 -0700237 return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
James E. Blairee743612012-05-29 14:49:32 -0700238
239 def getJobs(self):
240 return self._jobs
241
242 def addProject(self, project):
243 if project not in self.projects:
244 self.projects.append(project)
James E. Blairc8a1e052014-02-25 09:29:26 -0800245
James E. Blair0dcef7a2016-08-19 09:35:17 -0700246 if not self.name:
247 self.name = project.name
James E. Blairee743612012-05-29 14:49:32 -0700248
249 def enqueueChange(self, change):
James E. Blairbfb8e042014-12-30 17:01:44 -0800250 item = QueueItem(self, change)
James E. Blaircdccd972013-07-01 12:10:22 -0700251 self.enqueueItem(item)
252 item.enqueue_time = time.time()
253 return item
254
255 def enqueueItem(self, item):
James E. Blair4a035d92014-01-23 13:10:48 -0800256 item.pipeline = self.pipeline
James E. Blairbfb8e042014-12-30 17:01:44 -0800257 item.queue = self
258 if self.queue:
James E. Blairfee8d652013-06-07 08:57:52 -0700259 item.item_ahead = self.queue[-1]
James E. Blair972e3c72013-08-29 12:04:55 -0700260 item.item_ahead.items_behind.append(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700261 self.queue.append(item)
James E. Blairee743612012-05-29 14:49:32 -0700262
James E. Blairfee8d652013-06-07 08:57:52 -0700263 def dequeueItem(self, item):
264 if item in self.queue:
265 self.queue.remove(item)
James E. Blairfee8d652013-06-07 08:57:52 -0700266 if item.item_ahead:
James E. Blair972e3c72013-08-29 12:04:55 -0700267 item.item_ahead.items_behind.remove(item)
268 for item_behind in item.items_behind:
269 if item.item_ahead:
270 item.item_ahead.items_behind.append(item_behind)
271 item_behind.item_ahead = item.item_ahead
James E. Blairfee8d652013-06-07 08:57:52 -0700272 item.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -0700273 item.items_behind = []
James E. Blairfee8d652013-06-07 08:57:52 -0700274 item.dequeue_time = time.time()
James E. Blaire0487072012-08-29 17:38:31 -0700275
James E. Blair972e3c72013-08-29 12:04:55 -0700276 def moveItem(self, item, item_ahead):
James E. Blair972e3c72013-08-29 12:04:55 -0700277 if item.item_ahead == item_ahead:
278 return False
279 # Remove from current location
280 if item.item_ahead:
281 item.item_ahead.items_behind.remove(item)
282 for item_behind in item.items_behind:
283 if item.item_ahead:
284 item.item_ahead.items_behind.append(item_behind)
285 item_behind.item_ahead = item.item_ahead
286 # Add to new location
287 item.item_ahead = item_ahead
James E. Blair00451262013-09-20 11:40:17 -0700288 item.items_behind = []
James E. Blair972e3c72013-08-29 12:04:55 -0700289 if item.item_ahead:
290 item.item_ahead.items_behind.append(item)
291 return True
James E. Blairee743612012-05-29 14:49:32 -0700292
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800293 def isActionable(self, item):
James E. Blairbfb8e042014-12-30 17:01:44 -0800294 if self.window:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800295 return item in self.queue[:self.window]
Clark Boylan7603a372014-01-21 11:43:20 -0800296 else:
Clark Boylan3d2f7a72014-01-23 11:07:42 -0800297 return True
Clark Boylan7603a372014-01-21 11:43:20 -0800298
299 def increaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800300 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800301 if self.window_increase_type == 'linear':
302 self.window += self.window_increase_factor
303 elif self.window_increase_type == 'exponential':
304 self.window *= self.window_increase_factor
305
306 def decreaseWindowSize(self):
James E. Blairbfb8e042014-12-30 17:01:44 -0800307 if self.window:
Clark Boylan7603a372014-01-21 11:43:20 -0800308 if self.window_decrease_type == 'linear':
309 self.window = max(
310 self.window_floor,
311 self.window - self.window_decrease_factor)
312 elif self.window_decrease_type == 'exponential':
313 self.window = max(
314 self.window_floor,
Morgan Fainbergfdae2252016-06-07 20:13:20 -0700315 int(self.window / self.window_decrease_factor))
James E. Blairee743612012-05-29 14:49:32 -0700316
James E. Blair1e8dd892012-05-30 09:15:05 -0700317
James E. Blair4aea70c2012-07-26 14:23:24 -0700318class Project(object):
Monty Taylora42a55b2016-07-29 07:53:33 -0700319 """A Project represents a git repository such as openstack/nova."""
320
James E. Blaircf440a22016-07-15 09:11:58 -0700321 # NOTE: Projects should only be instantiated via a Source object
322 # so that they are associated with and cached by their Connection.
323 # This makes a Project instance a unique identifier for a given
324 # project from a given source.
325
James E. Blair0a899752017-03-29 13:22:16 -0700326 def __init__(self, name, source, foreign=False):
James E. Blair4aea70c2012-07-26 14:23:24 -0700327 self.name = name
James E. Blair8a395f92017-03-30 11:15:33 -0700328 self.source = source
James E. Blair0a899752017-03-29 13:22:16 -0700329 self.connection_name = source.connection.connection_name
330 self.canonical_hostname = source.canonical_hostname
James E. Blairc2a54fd2017-03-29 15:19:26 -0700331 self.canonical_name = source.canonical_hostname + '/' + name
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000332 # foreign projects are those referenced in dependencies
333 # of layout projects, this should matter
334 # when deciding whether to enqueue their changes
James E. Blaircf440a22016-07-15 09:11:58 -0700335 # TODOv3 (jeblair): re-add support for foreign projects if needed
Evgeny Antyshev0deaaad2015-08-03 20:22:56 +0000336 self.foreign = foreign
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700337 self.unparsed_config = None
James E. Blaire3162022017-02-20 16:47:27 -0500338 self.unparsed_branch_config = {} # branch -> UnparsedTenantConfig
James E. Blair4aea70c2012-07-26 14:23:24 -0700339
340 def __str__(self):
341 return self.name
342
343 def __repr__(self):
344 return '<Project %s>' % (self.name)
345
346
James E. Blair34776ee2016-08-25 13:53:54 -0700347class Node(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700348 """A single node for use by a job.
349
350 This may represent a request for a node, or an actual node
351 provided by Nodepool.
352 """
353
James E. Blair34776ee2016-08-25 13:53:54 -0700354 def __init__(self, name, image):
355 self.name = name
356 self.image = image
James E. Blaircbf43672017-01-04 14:33:41 -0800357 self.id = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800358 self.lock = None
359 # Attributes from Nodepool
360 self._state = 'unknown'
361 self.state_time = time.time()
Monty Taylor56f61332017-04-11 05:38:12 -0500362 self.interface_ip = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800363 self.public_ipv4 = None
364 self.private_ipv4 = None
365 self.public_ipv6 = None
James E. Blaircacdf2b2017-01-04 13:14:37 -0800366 self._keys = []
Paul Belanger30ba93a2017-03-16 16:28:10 -0400367 self.az = None
368 self.provider = None
369 self.region = None
James E. Blaira38c28e2017-01-04 10:33:20 -0800370
371 @property
372 def state(self):
373 return self._state
374
375 @state.setter
376 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800377 if value not in NODE_STATES:
378 raise TypeError("'%s' is not a valid state" % value)
James E. Blaira38c28e2017-01-04 10:33:20 -0800379 self._state = value
380 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700381
382 def __repr__(self):
James E. Blaircbf43672017-01-04 14:33:41 -0800383 return '<Node %s %s:%s>' % (self.id, self.name, self.image)
James E. Blair34776ee2016-08-25 13:53:54 -0700384
James E. Blair0d952152017-02-07 17:14:44 -0800385 def __ne__(self, other):
386 return not self.__eq__(other)
387
388 def __eq__(self, other):
389 if not isinstance(other, Node):
390 return False
391 return (self.name == other.name and
392 self.image == other.image and
393 self.id == other.id)
394
James E. Blaircacdf2b2017-01-04 13:14:37 -0800395 def toDict(self):
396 d = {}
397 d['state'] = self.state
398 for k in self._keys:
399 d[k] = getattr(self, k)
400 return d
401
James E. Blaira38c28e2017-01-04 10:33:20 -0800402 def updateFromDict(self, data):
403 self._state = data['state']
James E. Blaircacdf2b2017-01-04 13:14:37 -0800404 keys = []
405 for k, v in data.items():
406 if k == 'state':
407 continue
408 keys.append(k)
409 setattr(self, k, v)
410 self._keys = keys
James E. Blaira38c28e2017-01-04 10:33:20 -0800411
James E. Blair34776ee2016-08-25 13:53:54 -0700412
Monty Taylor7b19ba72017-05-24 07:42:54 -0500413class Group(object):
414 """A logical group of nodes for use by a job.
415
416 A Group is a named set of node names that will be provided to
417 jobs in the inventory to describe logical units where some subset of tasks
418 run.
419 """
420
421 def __init__(self, name, nodes):
422 self.name = name
423 self.nodes = nodes
424
425 def __repr__(self):
426 return '<Group %s %s>' % (self.name, str(self.nodes))
427
428 def __ne__(self, other):
429 return not self.__eq__(other)
430
431 def __eq__(self, other):
432 if not isinstance(other, Group):
433 return False
434 return (self.name == other.name and
435 self.nodes == other.nodes)
436
437 def toDict(self):
438 return {
439 'name': self.name,
440 'nodes': self.nodes
441 }
442
443
James E. Blaira98340f2016-09-02 11:33:49 -0700444class NodeSet(object):
445 """A set of nodes.
446
447 In configuration, NodeSets are attributes of Jobs indicating that
448 a Job requires nodes matching this description.
449
450 They may appear as top-level configuration objects and be named,
451 or they may appears anonymously in in-line job definitions.
452 """
453
454 def __init__(self, name=None):
455 self.name = name or ''
456 self.nodes = OrderedDict()
Monty Taylor7b19ba72017-05-24 07:42:54 -0500457 self.groups = OrderedDict()
James E. Blaira98340f2016-09-02 11:33:49 -0700458
James E. Blair1774dd52017-02-03 10:52:32 -0800459 def __ne__(self, other):
460 return not self.__eq__(other)
461
462 def __eq__(self, other):
463 if not isinstance(other, NodeSet):
464 return False
465 return (self.name == other.name and
466 self.nodes == other.nodes)
467
James E. Blaircbf43672017-01-04 14:33:41 -0800468 def copy(self):
469 n = NodeSet(self.name)
470 for name, node in self.nodes.items():
471 n.addNode(Node(node.name, node.image))
Monty Taylor7b19ba72017-05-24 07:42:54 -0500472 for name, group in self.groups.items():
473 n.addGroup(Group(group.name, group.nodes[:]))
James E. Blaircbf43672017-01-04 14:33:41 -0800474 return n
475
James E. Blaira98340f2016-09-02 11:33:49 -0700476 def addNode(self, node):
477 if node.name in self.nodes:
478 raise Exception("Duplicate node in %s" % (self,))
479 self.nodes[node.name] = node
480
James E. Blair0eaad552016-09-02 12:09:54 -0700481 def getNodes(self):
Clint Byruma4471d12017-05-10 20:57:40 -0400482 return list(self.nodes.values())
James E. Blair0eaad552016-09-02 12:09:54 -0700483
Monty Taylor7b19ba72017-05-24 07:42:54 -0500484 def addGroup(self, group):
485 if group.name in self.groups:
486 raise Exception("Duplicate group in %s" % (self,))
487 self.groups[group.name] = group
488
489 def getGroups(self):
490 return list(self.groups.values())
491
James E. Blaira98340f2016-09-02 11:33:49 -0700492 def __repr__(self):
493 if self.name:
494 name = self.name + ' '
495 else:
496 name = ''
Monty Taylor7b19ba72017-05-24 07:42:54 -0500497 return '<NodeSet %s%s%s>' % (name, self.nodes, self.groups)
James E. Blaira98340f2016-09-02 11:33:49 -0700498
499
James E. Blair34776ee2016-08-25 13:53:54 -0700500class NodeRequest(object):
James E. Blaira98340f2016-09-02 11:33:49 -0700501 """A request for a set of nodes."""
502
James E. Blair8b2a1472017-02-19 15:33:55 -0800503 def __init__(self, requestor, build_set, job, nodeset):
504 self.requestor = requestor
James E. Blair34776ee2016-08-25 13:53:54 -0700505 self.build_set = build_set
506 self.job = job
James E. Blair0eaad552016-09-02 12:09:54 -0700507 self.nodeset = nodeset
James E. Blair803e94f2017-01-06 09:18:59 -0800508 self._state = STATE_REQUESTED
James E. Blairdce6cea2016-12-20 16:45:32 -0800509 self.state_time = time.time()
510 self.stat = None
511 self.uid = uuid4().hex
James E. Blaircbf43672017-01-04 14:33:41 -0800512 self.id = None
James E. Blaircbbce0d2017-05-19 07:28:29 -0700513 # Zuul internal flags (not stored in ZK so they are not
James E. Blaira38c28e2017-01-04 10:33:20 -0800514 # overwritten).
515 self.failed = False
James E. Blaircbbce0d2017-05-19 07:28:29 -0700516 self.canceled = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800517
518 @property
James E. Blair6ab79e02017-01-06 10:10:17 -0800519 def fulfilled(self):
520 return (self._state == STATE_FULFILLED) and not self.failed
521
522 @property
James E. Blairdce6cea2016-12-20 16:45:32 -0800523 def state(self):
524 return self._state
525
526 @state.setter
527 def state(self, value):
James E. Blair803e94f2017-01-06 09:18:59 -0800528 if value not in REQUEST_STATES:
529 raise TypeError("'%s' is not a valid state" % value)
James E. Blairdce6cea2016-12-20 16:45:32 -0800530 self._state = value
531 self.state_time = time.time()
James E. Blair34776ee2016-08-25 13:53:54 -0700532
533 def __repr__(self):
James E. Blaircbf43672017-01-04 14:33:41 -0800534 return '<NodeRequest %s %s>' % (self.id, self.nodeset)
James E. Blair34776ee2016-08-25 13:53:54 -0700535
James E. Blairdce6cea2016-12-20 16:45:32 -0800536 def toDict(self):
537 d = {}
538 nodes = [n.image for n in self.nodeset.getNodes()]
539 d['node_types'] = nodes
James E. Blair8b2a1472017-02-19 15:33:55 -0800540 d['requestor'] = self.requestor
James E. Blairdce6cea2016-12-20 16:45:32 -0800541 d['state'] = self.state
542 d['state_time'] = self.state_time
543 return d
544
545 def updateFromDict(self, data):
546 self._state = data['state']
547 self.state_time = data['state_time']
548
James E. Blair34776ee2016-08-25 13:53:54 -0700549
James E. Blair01f83b72017-03-15 13:03:40 -0700550class Secret(object):
551 """A collection of private data.
552
553 In configuration, Secrets are collections of private data in
554 key-value pair format. They are defined as top-level
555 configuration objects and then referenced by Jobs.
556
557 """
558
James E. Blair8525e2b2017-03-15 14:05:47 -0700559 def __init__(self, name, source_context):
James E. Blair01f83b72017-03-15 13:03:40 -0700560 self.name = name
James E. Blair8525e2b2017-03-15 14:05:47 -0700561 self.source_context = source_context
James E. Blair01f83b72017-03-15 13:03:40 -0700562 # The secret data may or may not be encrypted. This attribute
563 # is named 'secret_data' to make it easy to search for and
564 # spot where it is directly used.
565 self.secret_data = {}
566
567 def __ne__(self, other):
568 return not self.__eq__(other)
569
570 def __eq__(self, other):
571 if not isinstance(other, Secret):
572 return False
573 return (self.name == other.name and
James E. Blair8525e2b2017-03-15 14:05:47 -0700574 self.source_context == other.source_context and
James E. Blair01f83b72017-03-15 13:03:40 -0700575 self.secret_data == other.secret_data)
576
577 def __repr__(self):
578 return '<Secret %s>' % (self.name,)
579
James E. Blair18f86a32017-03-15 14:43:26 -0700580 def decrypt(self, private_key):
581 """Return a copy of this secret with any encrypted data decrypted.
582 Note that the original remains encrypted."""
583
584 r = copy.deepcopy(self)
585 decrypted_secret_data = {}
586 for k, v in r.secret_data.items():
587 if hasattr(v, 'decrypt'):
588 decrypted_secret_data[k] = v.decrypt(private_key)
589 else:
590 decrypted_secret_data[k] = v
591 r.secret_data = decrypted_secret_data
592 return r
593
James E. Blair01f83b72017-03-15 13:03:40 -0700594
James E. Blaircdab2032017-02-01 09:09:29 -0800595class SourceContext(object):
596 """A reference to the branch of a project in configuration.
597
598 Jobs and playbooks reference this to keep track of where they
599 originate."""
600
James E. Blair6f140c72017-03-03 10:32:07 -0800601 def __init__(self, project, branch, path, trusted):
James E. Blaircdab2032017-02-01 09:09:29 -0800602 self.project = project
603 self.branch = branch
James E. Blair6f140c72017-03-03 10:32:07 -0800604 self.path = path
Monty Taylore6562aa2017-02-20 07:37:39 -0500605 self.trusted = trusted
James E. Blaircdab2032017-02-01 09:09:29 -0800606
James E. Blair6f140c72017-03-03 10:32:07 -0800607 def __str__(self):
608 return '%s/%s@%s' % (self.project, self.path, self.branch)
609
James E. Blaircdab2032017-02-01 09:09:29 -0800610 def __repr__(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800611 return '<SourceContext %s trusted:%s>' % (str(self),
612 self.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800613
James E. Blaira7f51ca2017-02-07 16:01:26 -0800614 def __deepcopy__(self, memo):
615 return self.copy()
616
617 def copy(self):
James E. Blair6f140c72017-03-03 10:32:07 -0800618 return self.__class__(self.project, self.branch, self.path,
619 self.trusted)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800620
James E. Blaircdab2032017-02-01 09:09:29 -0800621 def __ne__(self, other):
622 return not self.__eq__(other)
623
624 def __eq__(self, other):
625 if not isinstance(other, SourceContext):
626 return False
627 return (self.project == other.project and
628 self.branch == other.branch and
James E. Blair6f140c72017-03-03 10:32:07 -0800629 self.path == other.path and
Monty Taylore6562aa2017-02-20 07:37:39 -0500630 self.trusted == other.trusted)
James E. Blaircdab2032017-02-01 09:09:29 -0800631
632
James E. Blair66b274e2017-01-31 14:47:52 -0800633class PlaybookContext(object):
James E. Blaircdab2032017-02-01 09:09:29 -0800634
James E. Blair66b274e2017-01-31 14:47:52 -0800635 """A reference to a playbook in the context of a project.
636
637 Jobs refer to objects of this class for their main, pre, and post
638 playbooks so that we can keep track of which repos and security
639 contexts are needed in order to run them."""
640
James E. Blaircdab2032017-02-01 09:09:29 -0800641 def __init__(self, source_context, path):
642 self.source_context = source_context
James E. Blair66b274e2017-01-31 14:47:52 -0800643 self.path = path
James E. Blair66b274e2017-01-31 14:47:52 -0800644
645 def __repr__(self):
James E. Blaircdab2032017-02-01 09:09:29 -0800646 return '<PlaybookContext %s %s>' % (self.source_context,
647 self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800648
649 def __ne__(self, other):
650 return not self.__eq__(other)
651
652 def __eq__(self, other):
653 if not isinstance(other, PlaybookContext):
654 return False
James E. Blaircdab2032017-02-01 09:09:29 -0800655 return (self.source_context == other.source_context and
656 self.path == other.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800657
658 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400659 # Render to a dict to use in passing json to the executor
James E. Blair66b274e2017-01-31 14:47:52 -0800660 return dict(
James E. Blaircdab2032017-02-01 09:09:29 -0800661 connection=self.source_context.project.connection_name,
662 project=self.source_context.project.name,
663 branch=self.source_context.branch,
Monty Taylore6562aa2017-02-20 07:37:39 -0500664 trusted=self.source_context.trusted,
James E. Blaircdab2032017-02-01 09:09:29 -0800665 path=self.path)
James E. Blair66b274e2017-01-31 14:47:52 -0800666
667
James E. Blair5ac93842017-01-20 06:47:34 -0800668@six.add_metaclass(abc.ABCMeta)
669class Role(object):
670 """A reference to an ansible role."""
671
672 def __init__(self, target_name):
673 self.target_name = target_name
674
675 @abc.abstractmethod
676 def __repr__(self):
677 pass
678
679 def __ne__(self, other):
680 return not self.__eq__(other)
681
682 @abc.abstractmethod
683 def __eq__(self, other):
684 if not isinstance(other, Role):
685 return False
686 return (self.target_name == other.target_name)
687
688 @abc.abstractmethod
689 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400690 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800691 return dict(target_name=self.target_name)
692
693
694class ZuulRole(Role):
695 """A reference to an ansible role in a Zuul project."""
696
James E. Blair6563e4b2017-04-28 08:14:48 -0700697 def __init__(self, target_name, connection_name, project_name):
James E. Blair5ac93842017-01-20 06:47:34 -0800698 super(ZuulRole, self).__init__(target_name)
699 self.connection_name = connection_name
700 self.project_name = project_name
James E. Blair5ac93842017-01-20 06:47:34 -0800701
702 def __repr__(self):
703 return '<ZuulRole %s %s>' % (self.project_name, self.target_name)
704
Clint Byrumaf7438f2017-05-10 17:26:57 -0400705 __hash__ = object.__hash__
706
James E. Blair5ac93842017-01-20 06:47:34 -0800707 def __eq__(self, other):
708 if not isinstance(other, ZuulRole):
709 return False
710 return (super(ZuulRole, self).__eq__(other) and
711 self.connection_name == other.connection_name,
James E. Blair6563e4b2017-04-28 08:14:48 -0700712 self.project_name == other.project_name)
James E. Blair5ac93842017-01-20 06:47:34 -0800713
714 def toDict(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400715 # Render to a dict to use in passing json to the executor
James E. Blair5ac93842017-01-20 06:47:34 -0800716 d = super(ZuulRole, self).toDict()
717 d['type'] = 'zuul'
718 d['connection'] = self.connection_name
719 d['project'] = self.project_name
James E. Blair5ac93842017-01-20 06:47:34 -0800720 return d
721
722
James E. Blair8525e2b2017-03-15 14:05:47 -0700723class AuthContext(object):
724 """The authentication information for a job.
725
726 Authentication information (both the actual data and metadata such
727 as whether it should be inherited) for a job is grouped together
728 in this object.
729 """
730
731 def __init__(self, inherit=False):
732 self.inherit = inherit
733 self.secrets = []
734
735 def __ne__(self, other):
736 return not self.__eq__(other)
737
738 def __eq__(self, other):
739 if not isinstance(other, AuthContext):
740 return False
741 return (self.inherit == other.inherit and
742 self.secrets == other.secrets)
743
744
James E. Blairee743612012-05-29 14:49:32 -0700745class Job(object):
James E. Blair66b274e2017-01-31 14:47:52 -0800746
James E. Blaira7f51ca2017-02-07 16:01:26 -0800747 """A Job represents the defintion of actions to perform.
748
James E. Blaird4ade8c2017-02-19 15:25:46 -0800749 A Job is an abstract configuration concept. It describes what,
750 where, and under what circumstances something should be run
751 (contrast this with Build which is a concrete single execution of
752 a Job).
753
James E. Blaira7f51ca2017-02-07 16:01:26 -0800754 NB: Do not modify attributes of this class, set them directly
755 (e.g., "job.run = ..." rather than "job.run.append(...)").
756 """
Monty Taylora42a55b2016-07-29 07:53:33 -0700757
James E. Blairee743612012-05-29 14:49:32 -0700758 def __init__(self, name):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800759 # These attributes may override even the final form of a job
760 # in the context of a project-pipeline. They can not affect
761 # the execution of the job, but only whether the job is run
762 # and how it is reported.
763 self.context_attributes = dict(
764 voting=True,
765 hold_following_changes=False,
James E. Blair1774dd52017-02-03 10:52:32 -0800766 failure_message=None,
767 success_message=None,
768 failure_url=None,
769 success_url=None,
770 # Matchers. These are separate so they can be individually
771 # overidden.
772 branch_matcher=None,
773 file_matcher=None,
774 irrelevant_file_matcher=None, # skip-if
James E. Blaira7f51ca2017-02-07 16:01:26 -0800775 tags=frozenset(),
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200776 dependencies=frozenset(),
James E. Blair1774dd52017-02-03 10:52:32 -0800777 )
778
James E. Blaira7f51ca2017-02-07 16:01:26 -0800779 # These attributes affect how the job is actually run and more
780 # care must be taken when overriding them. If a job is
781 # declared "final", these may not be overriden in a
782 # project-pipeline.
783 self.execution_attributes = dict(
784 timeout=None,
James E. Blair490cf042017-02-24 23:07:21 -0500785 variables={},
James E. Blaira7f51ca2017-02-07 16:01:26 -0800786 nodeset=NodeSet(),
James E. Blair8525e2b2017-03-15 14:05:47 -0700787 auth=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800788 workspace=None,
789 pre_run=(),
790 post_run=(),
791 run=(),
792 implied_run=(),
Tobias Henkel9a0e1942017-03-20 16:16:02 +0100793 semaphore=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800794 attempts=3,
795 final=False,
James E. Blair5ac93842017-01-20 06:47:34 -0800796 roles=frozenset(),
James E. Blair912322f2017-05-23 13:11:25 -0700797 required_projects={},
James E. Blairb3f5db12017-03-17 12:57:39 -0700798 allowed_projects=None,
James E. Blair7391ecb2017-05-23 13:32:40 -0700799 override_branch=None,
James E. Blaira7f51ca2017-02-07 16:01:26 -0800800 )
801
802 # These are generally internal attributes which are not
803 # accessible via configuration.
804 self.other_attributes = dict(
805 name=None,
806 source_context=None,
807 inheritance_path=(),
808 )
809
810 self.inheritable_attributes = {}
811 self.inheritable_attributes.update(self.context_attributes)
812 self.inheritable_attributes.update(self.execution_attributes)
813 self.attributes = {}
814 self.attributes.update(self.inheritable_attributes)
815 self.attributes.update(self.other_attributes)
816
James E. Blairee743612012-05-29 14:49:32 -0700817 self.name = name
James E. Blair83005782015-12-11 14:46:03 -0800818
James E. Blair66b274e2017-01-31 14:47:52 -0800819 def __ne__(self, other):
820 return not self.__eq__(other)
821
Paul Belangere22baea2016-11-03 16:59:27 -0400822 def __eq__(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800823 # Compare the name and all inheritable attributes to determine
824 # whether two jobs with the same name are identically
825 # configured. Useful upon reconfiguration.
826 if not isinstance(other, Job):
827 return False
828 if self.name != other.name:
829 return False
830 for k, v in self.attributes.items():
831 if getattr(self, k) != getattr(other, k):
832 return False
833 return True
James E. Blairee743612012-05-29 14:49:32 -0700834
Clint Byrumaf7438f2017-05-10 17:26:57 -0400835 __hash__ = object.__hash__
836
James E. Blairee743612012-05-29 14:49:32 -0700837 def __str__(self):
838 return self.name
839
840 def __repr__(self):
James E. Blair72ae9da2017-02-03 14:21:30 -0800841 return '<Job %s branches: %s source: %s>' % (self.name,
842 self.branch_matcher,
843 self.source_context)
James E. Blair83005782015-12-11 14:46:03 -0800844
James E. Blaira7f51ca2017-02-07 16:01:26 -0800845 def __getattr__(self, name):
846 v = self.__dict__.get(name)
847 if v is None:
848 return copy.deepcopy(self.attributes[name])
849 return v
850
851 def _get(self, name):
852 return self.__dict__.get(name)
853
Joshua Heskethcd96ec02017-03-28 08:05:56 +1100854 def getSafeAttributes(self):
855 return Attributes(name=self.name)
856
James E. Blaira7f51ca2017-02-07 16:01:26 -0800857 def setRun(self):
858 if not self.run:
859 self.run = self.implied_run
860
James E. Blair490cf042017-02-24 23:07:21 -0500861 def updateVariables(self, other_vars):
862 v = self.variables
863 Job._deepUpdate(v, other_vars)
864 self.variables = v
865
James E. Blair912322f2017-05-23 13:11:25 -0700866 def updateProjects(self, other_projects):
867 required_projects = self.required_projects
868 Job._deepUpdate(required_projects, other_projects)
869 self.required_projects = required_projects
James E. Blair27f3dfc2017-05-23 13:07:28 -0700870
James E. Blair490cf042017-02-24 23:07:21 -0500871 @staticmethod
872 def _deepUpdate(a, b):
873 # Merge nested dictionaries if possible, otherwise, overwrite
874 # the value in 'a' with the value in 'b'.
875 for k, bv in b.items():
876 av = a.get(k)
877 if isinstance(av, dict) and isinstance(bv, dict):
878 Job._deepUpdate(av, bv)
879 else:
880 a[k] = bv
881
James E. Blaira7f51ca2017-02-07 16:01:26 -0800882 def inheritFrom(self, other):
James E. Blair83005782015-12-11 14:46:03 -0800883 """Copy the inheritable attributes which have been set on the other
884 job to this job."""
James E. Blaira7f51ca2017-02-07 16:01:26 -0800885 if not isinstance(other, Job):
886 raise Exception("Job unable to inherit from %s" % (other,))
887
888 do_not_inherit = set()
James E. Blair8525e2b2017-03-15 14:05:47 -0700889 if other.auth and not other.auth.inherit:
James E. Blaira7f51ca2017-02-07 16:01:26 -0800890 do_not_inherit.add('auth')
891
892 # copy all attributes
893 for k in self.inheritable_attributes:
894 if (other._get(k) is not None and k not in do_not_inherit):
895 setattr(self, k, copy.deepcopy(getattr(other, k)))
896
897 msg = 'inherit from %s' % (repr(other),)
898 self.inheritance_path = other.inheritance_path + (msg,)
899
900 def copy(self):
901 job = Job(self.name)
902 for k in self.attributes:
903 if self._get(k) is not None:
904 setattr(job, k, copy.deepcopy(self._get(k)))
905 return job
906
907 def applyVariant(self, other):
908 """Copy the attributes which have been set on the other job to this
909 job."""
James E. Blair83005782015-12-11 14:46:03 -0800910
911 if not isinstance(other, Job):
912 raise Exception("Job unable to inherit from %s" % (other,))
James E. Blaira7f51ca2017-02-07 16:01:26 -0800913
914 for k in self.execution_attributes:
915 if (other._get(k) is not None and
916 k not in set(['final'])):
917 if self.final:
918 raise Exception("Unable to modify final job %s attribute "
919 "%s=%s with variant %s" % (
920 repr(self), k, other._get(k),
921 repr(other)))
James E. Blair27f3dfc2017-05-23 13:07:28 -0700922 if k not in set(['pre_run', 'post_run', 'roles', 'variables',
James E. Blair912322f2017-05-23 13:11:25 -0700923 'required_projects']):
James E. Blaira7f51ca2017-02-07 16:01:26 -0800924 setattr(self, k, copy.deepcopy(other._get(k)))
925
926 # Don't set final above so that we don't trip an error halfway
927 # through assignment.
928 if other.final != self.attributes['final']:
929 self.final = other.final
930
931 if other._get('pre_run') is not None:
932 self.pre_run = self.pre_run + other.pre_run
933 if other._get('post_run') is not None:
934 self.post_run = other.post_run + self.post_run
James E. Blair5ac93842017-01-20 06:47:34 -0800935 if other._get('roles') is not None:
936 self.roles = self.roles.union(other.roles)
James E. Blair490cf042017-02-24 23:07:21 -0500937 if other._get('variables') is not None:
938 self.updateVariables(other.variables)
James E. Blair912322f2017-05-23 13:11:25 -0700939 if other._get('required_projects') is not None:
940 self.updateProjects(other.required_projects)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800941
942 for k in self.context_attributes:
943 if (other._get(k) is not None and
944 k not in set(['tags'])):
945 setattr(self, k, copy.deepcopy(other._get(k)))
946
947 if other._get('tags') is not None:
948 self.tags = self.tags.union(other.tags)
949
950 msg = 'apply variant %s' % (repr(other),)
951 self.inheritance_path = self.inheritance_path + (msg,)
James E. Blairee743612012-05-29 14:49:32 -0700952
James E. Blaire421a232012-07-25 16:59:21 -0700953 def changeMatches(self, change):
James E. Blair83005782015-12-11 14:46:03 -0800954 if self.branch_matcher and not self.branch_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800955 return False
956
James E. Blair83005782015-12-11 14:46:03 -0800957 if self.file_matcher and not self.file_matcher.matches(change):
James E. Blair70c71582013-03-06 08:50:50 -0800958 return False
959
James E. Blair83005782015-12-11 14:46:03 -0800960 # NB: This is a negative match.
961 if (self.irrelevant_file_matcher and
962 self.irrelevant_file_matcher.matches(change)):
Maru Newby3fe5f852015-01-13 04:22:14 +0000963 return False
964
James E. Blair70c71582013-03-06 08:50:50 -0800965 return True
James E. Blaire5a847f2012-07-10 15:29:14 -0700966
James E. Blair1e8dd892012-05-30 09:15:05 -0700967
James E. Blair912322f2017-05-23 13:11:25 -0700968class JobProject(object):
James E. Blair27f3dfc2017-05-23 13:07:28 -0700969 """ A reference to a project from a job. """
970
971 def __init__(self, project_name, override_branch=None):
972 self.project_name = project_name
973 self.override_branch = override_branch
974
975
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200976class JobList(object):
977 """ A list of jobs in a project's pipeline. """
Monty Taylora42a55b2016-07-29 07:53:33 -0700978
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200979 def __init__(self):
980 self.jobs = OrderedDict() # job.name -> [job, ...]
James E. Blair12748b52017-02-07 17:17:53 -0800981
James E. Blairee743612012-05-29 14:49:32 -0700982 def addJob(self, job):
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200983 if job.name in self.jobs:
984 self.jobs[job.name].append(job)
985 else:
986 self.jobs[job.name] = [job]
James E. Blairee743612012-05-29 14:49:32 -0700987
James E. Blaira7f51ca2017-02-07 16:01:26 -0800988 def inheritFrom(self, other):
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200989 for jobname, jobs in other.jobs.items():
990 if jobname in self.jobs:
Jesse Keatingd1f434a2017-05-16 20:28:35 -0700991 self.jobs[jobname].extend(jobs)
James E. Blaira7f51ca2017-02-07 16:01:26 -0800992 else:
Fredrik Medleyf8aec832015-09-28 13:40:20 +0200993 self.jobs[jobname] = jobs
994
995
996class JobGraph(object):
997 """ A JobGraph represents the dependency graph between Job."""
998
999 def __init__(self):
1000 self.jobs = OrderedDict() # job_name -> Job
1001 self._dependencies = {} # dependent_job_name -> set(parent_job_names)
1002
1003 def __repr__(self):
1004 return '<JobGraph %s>' % (self.jobs)
1005
1006 def addJob(self, job):
1007 # A graph must be created after the job list is frozen,
1008 # therefore we should only get one job with the same name.
1009 if job.name in self.jobs:
1010 raise Exception("Job %s already added" % (job.name,))
1011 self.jobs[job.name] = job
1012 # Append the dependency information
1013 self._dependencies.setdefault(job.name, set())
1014 try:
1015 for dependency in job.dependencies:
1016 # Make sure a circular dependency is never created
1017 ancestor_jobs = self._getParentJobNamesRecursively(
1018 dependency, soft=True)
1019 ancestor_jobs.add(dependency)
1020 if any((job.name == anc_job) for anc_job in ancestor_jobs):
1021 raise Exception("Dependency cycle detected in job %s" %
1022 (job.name,))
1023 self._dependencies[job.name].add(dependency)
1024 except Exception:
1025 del self.jobs[job.name]
1026 del self._dependencies[job.name]
1027 raise
1028
1029 def getJobs(self):
Clint Byruma4471d12017-05-10 20:57:40 -04001030 return list(self.jobs.values()) # Report in the order of layout cfg
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001031
1032 def _getDirectDependentJobs(self, parent_job):
1033 ret = set()
1034 for dependent_name, parent_names in self._dependencies.items():
1035 if parent_job in parent_names:
1036 ret.add(dependent_name)
1037 return ret
1038
1039 def getDependentJobsRecursively(self, parent_job):
1040 all_dependent_jobs = set()
1041 jobs_to_iterate = set([parent_job])
1042 while len(jobs_to_iterate) > 0:
1043 current_job = jobs_to_iterate.pop()
1044 current_dependent_jobs = self._getDirectDependentJobs(current_job)
1045 new_dependent_jobs = current_dependent_jobs - all_dependent_jobs
1046 jobs_to_iterate |= new_dependent_jobs
1047 all_dependent_jobs |= new_dependent_jobs
1048 return [self.jobs[name] for name in all_dependent_jobs]
1049
1050 def getParentJobsRecursively(self, dependent_job):
1051 return [self.jobs[name] for name in
1052 self._getParentJobNamesRecursively(dependent_job)]
1053
1054 def _getParentJobNamesRecursively(self, dependent_job, soft=False):
1055 all_parent_jobs = set()
1056 jobs_to_iterate = set([dependent_job])
1057 while len(jobs_to_iterate) > 0:
1058 current_job = jobs_to_iterate.pop()
1059 current_parent_jobs = self._dependencies.get(current_job)
1060 if current_parent_jobs is None:
1061 if soft:
1062 current_parent_jobs = set()
1063 else:
1064 raise Exception("Dependent job %s not found: " %
1065 (dependent_job,))
1066 new_parent_jobs = current_parent_jobs - all_parent_jobs
1067 jobs_to_iterate |= new_parent_jobs
1068 all_parent_jobs |= new_parent_jobs
1069 return all_parent_jobs
James E. Blairb97ed802015-12-21 15:55:35 -08001070
James E. Blair1e8dd892012-05-30 09:15:05 -07001071
James E. Blair4aea70c2012-07-26 14:23:24 -07001072class Build(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001073 """A Build is an instance of a single execution of a Job.
1074
1075 While a Job describes what to run, a Build describes an actual
1076 execution of that Job. Each build is associated with exactly one
1077 Job (related builds are grouped together in a BuildSet).
1078 """
Monty Taylora42a55b2016-07-29 07:53:33 -07001079
James E. Blair4aea70c2012-07-26 14:23:24 -07001080 def __init__(self, job, uuid):
1081 self.job = job
1082 self.uuid = uuid
James E. Blair4aea70c2012-07-26 14:23:24 -07001083 self.url = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001084 self.result = None
1085 self.build_set = None
Paul Belanger174a8272017-03-14 13:20:10 -04001086 self.execute_time = time.time()
James E. Blair71e94122012-12-24 17:53:08 -08001087 self.start_time = None
1088 self.end_time = None
James E. Blairbea9ef12013-07-15 11:52:23 -07001089 self.estimated_time = None
James E. Blair66eeebf2013-07-27 17:44:32 -07001090 self.pipeline = None
James E. Blair0aac4872013-08-23 14:02:38 -07001091 self.canceled = False
James E. Blair4a28a882013-08-23 15:17:33 -07001092 self.retry = False
James E. Blaird78576a2013-07-09 10:39:17 -07001093 self.parameters = {}
Joshua Heskethba8776a2014-01-12 14:35:40 +08001094 self.worker = Worker()
Timothy Chavezb2332082015-08-07 20:08:04 -05001095 self.node_labels = []
1096 self.node_name = None
James E. Blairee743612012-05-29 14:49:32 -07001097
1098 def __repr__(self):
Joshua Heskethba8776a2014-01-12 14:35:40 +08001099 return ('<Build %s of %s on %s>' %
1100 (self.uuid, self.job.name, self.worker))
1101
Joshua Heskethcd96ec02017-03-28 08:05:56 +11001102 def getSafeAttributes(self):
1103 return Attributes(uuid=self.uuid)
1104
Joshua Heskethba8776a2014-01-12 14:35:40 +08001105
1106class Worker(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001107 """Information about the specific worker executing a Build."""
Joshua Heskethba8776a2014-01-12 14:35:40 +08001108 def __init__(self):
1109 self.name = "Unknown"
1110 self.hostname = None
1111 self.ips = []
1112 self.fqdn = None
1113 self.program = None
1114 self.version = None
1115 self.extra = {}
1116
1117 def updateFromData(self, data):
1118 """Update worker information if contained in the WORK_DATA response."""
1119 self.name = data.get('worker_name', self.name)
1120 self.hostname = data.get('worker_hostname', self.hostname)
1121 self.ips = data.get('worker_ips', self.ips)
1122 self.fqdn = data.get('worker_fqdn', self.fqdn)
1123 self.program = data.get('worker_program', self.program)
1124 self.version = data.get('worker_version', self.version)
1125 self.extra = data.get('worker_extra', self.extra)
1126
1127 def __repr__(self):
1128 return '<Worker %s>' % self.name
James E. Blairee743612012-05-29 14:49:32 -07001129
James E. Blair1e8dd892012-05-30 09:15:05 -07001130
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001131class RepoFiles(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001132 """RepoFiles holds config-file content for per-project job config.
1133
1134 When Zuul asks a merger to prepare a future multiple-repo state
1135 and collect Zuul configuration files so that we can dynamically
1136 load our configuration, this class provides cached access to that
1137 data for use by the Change which updated the config files and any
1138 changes that follow it in a ChangeQueue.
1139
1140 It is attached to a BuildSet since the content of Zuul
1141 configuration files can change with each new BuildSet.
1142 """
1143
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001144 def __init__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001145 self.connections = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001146
1147 def __repr__(self):
James E. Blair2a535672017-04-27 12:03:15 -07001148 return '<RepoFiles %s>' % self.connections
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001149
1150 def setFiles(self, items):
James E. Blair2a535672017-04-27 12:03:15 -07001151 self.hostnames = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001152 for item in items:
James E. Blair2a535672017-04-27 12:03:15 -07001153 connection = self.connections.setdefault(
1154 item['connection'], {})
1155 project = connection.setdefault(item['project'], {})
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001156 branch = project.setdefault(item['branch'], {})
1157 branch.update(item['files'])
1158
James E. Blair2a535672017-04-27 12:03:15 -07001159 def getFile(self, connection_name, project_name, branch, fn):
1160 host = self.connections.get(connection_name, {})
1161 return host.get(project_name, {}).get(branch, {}).get(fn)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001162
1163
James E. Blair7e530ad2012-07-03 16:12:28 -07001164class BuildSet(object):
James E. Blaird4ade8c2017-02-19 15:25:46 -08001165 """A collection of Builds for one specific potential future repository
1166 state.
Monty Taylora42a55b2016-07-29 07:53:33 -07001167
Paul Belanger174a8272017-03-14 13:20:10 -04001168 When Zuul executes Builds for a change, it creates a Build to
James E. Blaird4ade8c2017-02-19 15:25:46 -08001169 represent each execution of each job and a BuildSet to keep track
Paul Belanger174a8272017-03-14 13:20:10 -04001170 of all the Builds running for that Change. When Zuul re-executes
James E. Blaird4ade8c2017-02-19 15:25:46 -08001171 Builds for a Change with a different configuration, all of the
1172 running Builds in the BuildSet for that change are aborted, and a
1173 new BuildSet is created to hold the Builds for the Jobs being
1174 run with the new configuration.
1175
1176 A BuildSet also holds the UUID used to produce the Zuul Ref that
1177 builders check out.
1178
Monty Taylora42a55b2016-07-29 07:53:33 -07001179 """
James E. Blair4076e2b2014-01-28 12:42:20 -08001180 # Merge states:
1181 NEW = 1
1182 PENDING = 2
1183 COMPLETE = 3
1184
Antoine Musso9b229282014-08-18 23:45:43 +02001185 states_map = {
1186 1: 'NEW',
1187 2: 'PENDING',
1188 3: 'COMPLETE',
1189 }
1190
James E. Blairfee8d652013-06-07 08:57:52 -07001191 def __init__(self, item):
1192 self.item = item
James E. Blair7e530ad2012-07-03 16:12:28 -07001193 self.builds = {}
James E. Blair11700c32012-07-05 17:50:05 -07001194 self.result = None
1195 self.next_build_set = None
1196 self.previous_build_set = None
James E. Blair4886cc12012-07-18 15:39:41 -07001197 self.ref = None
James E. Blair81515ad2012-10-01 18:29:08 -07001198 self.commit = None
James E. Blair4076e2b2014-01-28 12:42:20 -08001199 self.zuul_url = None
James E. Blair1960d682017-04-28 15:44:14 -07001200 self.dependent_items = None
1201 self.merger_items = None
James E. Blair973721f2012-08-15 10:19:43 -07001202 self.unable_to_merge = False
James E. Blaire53250c2017-03-01 14:34:36 -08001203 self.config_error = None # None or an error message string.
James E. Blair972e3c72013-08-29 12:04:55 -07001204 self.failing_reasons = []
James E. Blair4076e2b2014-01-28 12:42:20 -08001205 self.merge_state = self.NEW
James E. Blair0eaad552016-09-02 12:09:54 -07001206 self.nodesets = {} # job -> nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001207 self.node_requests = {} # job -> reqs
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001208 self.files = RepoFiles()
James E. Blair1960d682017-04-28 15:44:14 -07001209 self.repo_state = {}
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001210 self.layout = None
Paul Belanger71d98172016-11-08 10:56:31 -05001211 self.tries = {}
James E. Blair7e530ad2012-07-03 16:12:28 -07001212
Antoine Musso9b229282014-08-18 23:45:43 +02001213 def __repr__(self):
1214 return '<BuildSet item: %s #builds: %s merge state: %s>' % (
1215 self.item,
1216 len(self.builds),
1217 self.getStateName(self.merge_state))
1218
James E. Blair4886cc12012-07-18 15:39:41 -07001219 def setConfiguration(self):
James E. Blair11700c32012-07-05 17:50:05 -07001220 # The change isn't enqueued until after it's created
1221 # so we don't know what the other changes ahead will be
1222 # until jobs start.
James E. Blair1960d682017-04-28 15:44:14 -07001223 if self.dependent_items is None:
1224 items = []
James E. Blairfee8d652013-06-07 08:57:52 -07001225 next_item = self.item.item_ahead
1226 while next_item:
James E. Blair1960d682017-04-28 15:44:14 -07001227 items.append(next_item)
James E. Blairfee8d652013-06-07 08:57:52 -07001228 next_item = next_item.item_ahead
James E. Blair1960d682017-04-28 15:44:14 -07001229 self.dependent_items = items
James E. Blair4886cc12012-07-18 15:39:41 -07001230 if not self.ref:
1231 self.ref = 'Z' + uuid4().hex
James E. Blair1960d682017-04-28 15:44:14 -07001232 if self.merger_items is None:
1233 items = [self.item] + self.dependent_items
1234 items.reverse()
1235 self.merger_items = [i.makeMergerItem() for i in items]
James E. Blair4886cc12012-07-18 15:39:41 -07001236
Antoine Musso9b229282014-08-18 23:45:43 +02001237 def getStateName(self, state_num):
1238 return self.states_map.get(
1239 state_num, 'UNKNOWN (%s)' % state_num)
1240
James E. Blair4886cc12012-07-18 15:39:41 -07001241 def addBuild(self, build):
1242 self.builds[build.job.name] = build
Paul Belanger71d98172016-11-08 10:56:31 -05001243 if build.job.name not in self.tries:
James E. Blair5aed1112016-12-15 09:18:05 -08001244 self.tries[build.job.name] = 1
James E. Blair4886cc12012-07-18 15:39:41 -07001245 build.build_set = self
James E. Blair11700c32012-07-05 17:50:05 -07001246
James E. Blair4a28a882013-08-23 15:17:33 -07001247 def removeBuild(self, build):
Paul Belanger71d98172016-11-08 10:56:31 -05001248 self.tries[build.job.name] += 1
James E. Blair4a28a882013-08-23 15:17:33 -07001249 del self.builds[build.job.name]
1250
James E. Blair7e530ad2012-07-03 16:12:28 -07001251 def getBuild(self, job_name):
1252 return self.builds.get(job_name)
1253
James E. Blair11700c32012-07-05 17:50:05 -07001254 def getBuilds(self):
Clint Byrum1d0c7d12017-05-10 19:40:53 -07001255 keys = list(self.builds.keys())
James E. Blair11700c32012-07-05 17:50:05 -07001256 keys.sort()
1257 return [self.builds.get(x) for x in keys]
1258
James E. Blair0eaad552016-09-02 12:09:54 -07001259 def getJobNodeSet(self, job_name):
1260 # Return None if not provisioned; empty NodeSet if no nodes
1261 # required
1262 return self.nodesets.get(job_name)
James E. Blair8d692392016-04-08 17:47:58 -07001263
James E. Blaire18d4602017-01-05 11:17:28 -08001264 def removeJobNodeSet(self, job_name):
1265 if job_name not in self.nodesets:
1266 raise Exception("No job set for %s" % (job_name))
1267 del self.nodesets[job_name]
1268
James E. Blair8d692392016-04-08 17:47:58 -07001269 def setJobNodeRequest(self, job_name, req):
1270 if job_name in self.node_requests:
1271 raise Exception("Prior node request for %s" % (job_name))
1272 self.node_requests[job_name] = req
1273
1274 def getJobNodeRequest(self, job_name):
1275 return self.node_requests.get(job_name)
1276
James E. Blair0eaad552016-09-02 12:09:54 -07001277 def jobNodeRequestComplete(self, job_name, req, nodeset):
1278 if job_name in self.nodesets:
James E. Blair8d692392016-04-08 17:47:58 -07001279 raise Exception("Prior node request for %s" % (job_name))
James E. Blair0eaad552016-09-02 12:09:54 -07001280 self.nodesets[job_name] = nodeset
James E. Blair8d692392016-04-08 17:47:58 -07001281 del self.node_requests[job_name]
1282
Paul Belanger71d98172016-11-08 10:56:31 -05001283 def getTries(self, job_name):
Clint Byrum804073b2017-05-10 17:47:45 -04001284 return self.tries.get(job_name, 0)
Paul Belanger71d98172016-11-08 10:56:31 -05001285
James E. Blair0ffa0102017-03-30 13:11:33 -07001286 def getMergeMode(self):
James E. Blair1960d682017-04-28 15:44:14 -07001287 # We may be called before this build set has a shadow layout
1288 # (ie, we are called to perform the merge to create that
1289 # layout). It's possible that the change we are merging will
1290 # update the merge-mode for the project, but there's not much
1291 # we can do about that here. Instead, do the best we can by
1292 # using the nearest shadow layout to determine the merge mode,
1293 # or if that fails, the current live layout, or if that fails,
1294 # use the default: merge-resolve.
1295 item = self.item
1296 layout = None
1297 while item:
1298 layout = item.current_build_set.layout
1299 if layout:
1300 break
1301 item = item.item_ahead
1302 if not layout:
1303 layout = self.item.pipeline.layout
1304 if layout:
James E. Blair0ffa0102017-03-30 13:11:33 -07001305 project = self.item.change.project
James E. Blair1960d682017-04-28 15:44:14 -07001306 project_config = layout.project_configs.get(
James E. Blair0ffa0102017-03-30 13:11:33 -07001307 project.canonical_name)
1308 if project_config:
1309 return project_config.merge_mode
1310 return MERGER_MERGE_RESOLVE
Adam Gandelman8bd57102016-12-02 12:58:42 -08001311
James E. Blair7e530ad2012-07-03 16:12:28 -07001312
James E. Blairfee8d652013-06-07 08:57:52 -07001313class QueueItem(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001314 """Represents the position of a Change in a ChangeQueue.
1315
1316 All Changes are enqueued into ChangeQueue in a QueueItem. The QueueItem
1317 holds the current `BuildSet` as well as all previous `BuildSets` that were
1318 produced for this `QueueItem`.
1319 """
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001320 log = logging.getLogger("zuul.QueueItem")
James E. Blair32663402012-06-01 10:04:18 -07001321
James E. Blairbfb8e042014-12-30 17:01:44 -08001322 def __init__(self, queue, change):
1323 self.pipeline = queue.pipeline
1324 self.queue = queue
Monty Taylor55d0f562017-05-17 14:30:21 -05001325 self.change = change # a ref
James E. Blair7e530ad2012-07-03 16:12:28 -07001326 self.build_sets = []
James E. Blaircaec0c52012-08-22 14:52:22 -07001327 self.dequeued_needing_change = False
James E. Blair11700c32012-07-05 17:50:05 -07001328 self.current_build_set = BuildSet(self)
1329 self.build_sets.append(self.current_build_set)
James E. Blairfee8d652013-06-07 08:57:52 -07001330 self.item_ahead = None
James E. Blair972e3c72013-08-29 12:04:55 -07001331 self.items_behind = []
James E. Blair8fa16972013-01-15 16:57:20 -08001332 self.enqueue_time = None
1333 self.dequeue_time = None
James E. Blairfee8d652013-06-07 08:57:52 -07001334 self.reported = False
Tobias Henkel9842bd72017-05-16 13:40:03 +02001335 self.reported_start = False
1336 self.quiet = False
James E. Blairbfb8e042014-12-30 17:01:44 -08001337 self.active = False # Whether an item is within an active window
1338 self.live = True # Whether an item is intended to be processed at all
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001339 self.layout = None # This item's shadow layout
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001340 self.job_graph = None
James E. Blaire5a847f2012-07-10 15:29:14 -07001341
James E. Blair972e3c72013-08-29 12:04:55 -07001342 def __repr__(self):
1343 if self.pipeline:
1344 pipeline = self.pipeline.name
1345 else:
1346 pipeline = None
1347 return '<QueueItem 0x%x for %s in %s>' % (
1348 id(self), self.change, pipeline)
1349
James E. Blairee743612012-05-29 14:49:32 -07001350 def resetAllBuilds(self):
James E. Blair11700c32012-07-05 17:50:05 -07001351 old = self.current_build_set
1352 self.current_build_set.result = 'CANCELED'
1353 self.current_build_set = BuildSet(self)
1354 old.next_build_set = self.current_build_set
1355 self.current_build_set.previous_build_set = old
James E. Blair7e530ad2012-07-03 16:12:28 -07001356 self.build_sets.append(self.current_build_set)
James E. Blairee743612012-05-29 14:49:32 -07001357
1358 def addBuild(self, build):
James E. Blair7e530ad2012-07-03 16:12:28 -07001359 self.current_build_set.addBuild(build)
James E. Blair66eeebf2013-07-27 17:44:32 -07001360 build.pipeline = self.pipeline
James E. Blairee743612012-05-29 14:49:32 -07001361
James E. Blair4a28a882013-08-23 15:17:33 -07001362 def removeBuild(self, build):
1363 self.current_build_set.removeBuild(build)
1364
James E. Blairfee8d652013-06-07 08:57:52 -07001365 def setReportedResult(self, result):
1366 self.current_build_set.result = result
1367
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001368 def freezeJobGraph(self):
James E. Blair83005782015-12-11 14:46:03 -08001369 """Find or create actual matching jobs for this item's change and
1370 store the resulting job tree."""
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001371 layout = self.current_build_set.layout
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001372 job_graph = layout.createJobGraph(self)
1373 for job in job_graph.getJobs():
1374 # Ensure that each jobs's dependencies are fully
1375 # accessible. This will raise an exception if not.
1376 job_graph.getParentJobsRecursively(job.name)
1377 self.job_graph = job_graph
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001378
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001379 def hasJobGraph(self):
1380 """Returns True if the item has a job graph."""
1381 return self.job_graph is not None
James E. Blair83005782015-12-11 14:46:03 -08001382
1383 def getJobs(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001384 if not self.live or not self.job_graph:
James E. Blair83005782015-12-11 14:46:03 -08001385 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001386 return self.job_graph.getJobs()
1387
1388 def getJob(self, name):
1389 if not self.job_graph:
1390 return None
1391 return self.job_graph.jobs.get(name)
James E. Blair83005782015-12-11 14:46:03 -08001392
James E. Blairdbfd3282016-07-21 10:46:19 -07001393 def haveAllJobsStarted(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001394 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001395 return False
1396 for job in self.getJobs():
1397 build = self.current_build_set.getBuild(job.name)
1398 if not build or not build.start_time:
1399 return False
1400 return True
1401
1402 def areAllJobsComplete(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001403 if (self.current_build_set.config_error or
1404 self.current_build_set.unable_to_merge):
1405 return True
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001406 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001407 return False
1408 for job in self.getJobs():
1409 build = self.current_build_set.getBuild(job.name)
1410 if not build or not build.result:
1411 return False
1412 return True
1413
1414 def didAllJobsSucceed(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001415 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001416 return False
1417 for job in self.getJobs():
1418 if not job.voting:
1419 continue
1420 build = self.current_build_set.getBuild(job.name)
1421 if not build:
1422 return False
1423 if build.result != 'SUCCESS':
1424 return False
1425 return True
1426
1427 def didAnyJobFail(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001428 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001429 return False
1430 for job in self.getJobs():
1431 if not job.voting:
1432 continue
1433 build = self.current_build_set.getBuild(job.name)
1434 if build and build.result and (build.result != 'SUCCESS'):
1435 return True
1436 return False
1437
1438 def didMergerFail(self):
James E. Blaire53250c2017-03-01 14:34:36 -08001439 return self.current_build_set.unable_to_merge
1440
1441 def getConfigError(self):
1442 return self.current_build_set.config_error
James E. Blairdbfd3282016-07-21 10:46:19 -07001443
James E. Blair0d3e83b2017-06-05 13:51:57 -07001444 def wasDequeuedNeedingChange(self):
1445 return self.dequeued_needing_change
1446
James E. Blairdbfd3282016-07-21 10:46:19 -07001447 def isHoldingFollowingChanges(self):
1448 if not self.live:
1449 return False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001450 if not self.hasJobGraph():
James E. Blairdbfd3282016-07-21 10:46:19 -07001451 return False
1452 for job in self.getJobs():
1453 if not job.hold_following_changes:
1454 continue
1455 build = self.current_build_set.getBuild(job.name)
1456 if not build:
1457 return True
1458 if build.result != 'SUCCESS':
1459 return True
1460
1461 if not self.item_ahead:
1462 return False
1463 return self.item_ahead.isHoldingFollowingChanges()
1464
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001465 def findJobsToRun(self, semaphore_handler):
James E. Blairdbfd3282016-07-21 10:46:19 -07001466 torun = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001467 if not self.live:
1468 return []
1469 if not self.job_graph:
1470 return []
James E. Blair791b5392016-08-03 11:25:56 -07001471 if self.item_ahead:
1472 # Only run jobs if any 'hold' jobs on the change ahead
1473 # have completed successfully.
1474 if self.item_ahead.isHoldingFollowingChanges():
1475 return []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001476
1477 successful_job_names = set()
1478 jobs_not_started = set()
1479 for job in self.job_graph.getJobs():
1480 build = self.current_build_set.getBuild(job.name)
1481 if build:
1482 if build.result == 'SUCCESS':
1483 successful_job_names.add(job.name)
1484 else:
1485 jobs_not_started.add(job)
1486
1487 # Attempt to request nodes for jobs in the order jobs appear
1488 # in configuration.
1489 for job in self.job_graph.getJobs():
1490 if job not in jobs_not_started:
1491 continue
1492 all_parent_jobs_successful = True
1493 for parent_job in self.job_graph.getParentJobsRecursively(
1494 job.name):
1495 if parent_job.name not in successful_job_names:
1496 all_parent_jobs_successful = False
1497 break
1498 if all_parent_jobs_successful:
1499 nodeset = self.current_build_set.getJobNodeSet(job.name)
1500 if nodeset is None:
1501 # The nodes for this job are not ready, skip
1502 # it for now.
James E. Blairdbfd3282016-07-21 10:46:19 -07001503 continue
Tobias Henkel9a0e1942017-03-20 16:16:02 +01001504 if semaphore_handler.acquire(self, job):
1505 # If this job needs a semaphore, either acquire it or
1506 # make sure that we have it before running the job.
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001507 torun.append(job)
James E. Blairdbfd3282016-07-21 10:46:19 -07001508 return torun
1509
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001510 def findJobsToRequest(self):
James E. Blair6ab79e02017-01-06 10:10:17 -08001511 build_set = self.current_build_set
James E. Blairdbfd3282016-07-21 10:46:19 -07001512 toreq = []
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001513 if not self.live:
1514 return []
1515 if not self.job_graph:
1516 return []
James E. Blair6ab79e02017-01-06 10:10:17 -08001517 if self.item_ahead:
1518 if self.item_ahead.isHoldingFollowingChanges():
1519 return []
James E. Blairdbfd3282016-07-21 10:46:19 -07001520
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001521 successful_job_names = set()
1522 jobs_not_requested = set()
1523 for job in self.job_graph.getJobs():
1524 build = build_set.getBuild(job.name)
1525 if build and build.result == 'SUCCESS':
1526 successful_job_names.add(job.name)
1527 else:
1528 nodeset = build_set.getJobNodeSet(job.name)
1529 if nodeset is None:
1530 req = build_set.getJobNodeRequest(job.name)
1531 if req is None:
1532 jobs_not_requested.add(job)
1533
1534 # Attempt to request nodes for jobs in the order jobs appear
1535 # in configuration.
1536 for job in self.job_graph.getJobs():
1537 if job not in jobs_not_requested:
1538 continue
1539 all_parent_jobs_successful = True
1540 for parent_job in self.job_graph.getParentJobsRecursively(
1541 job.name):
1542 if parent_job.name not in successful_job_names:
1543 all_parent_jobs_successful = False
1544 break
1545 if all_parent_jobs_successful:
1546 toreq.append(job)
1547 return toreq
James E. Blairdbfd3282016-07-21 10:46:19 -07001548
1549 def setResult(self, build):
1550 if build.retry:
1551 self.removeBuild(build)
1552 elif build.result != 'SUCCESS':
Fredrik Medleyf8aec832015-09-28 13:40:20 +02001553 for job in self.job_graph.getDependentJobsRecursively(
1554 build.job.name):
James E. Blairdbfd3282016-07-21 10:46:19 -07001555 fakebuild = Build(job, None)
1556 fakebuild.result = 'SKIPPED'
1557 self.addBuild(fakebuild)
1558
James E. Blair6ab79e02017-01-06 10:10:17 -08001559 def setNodeRequestFailure(self, job):
1560 fakebuild = Build(job, None)
1561 self.addBuild(fakebuild)
1562 fakebuild.result = 'NODE_FAILURE'
1563 self.setResult(fakebuild)
1564
James E. Blairdbfd3282016-07-21 10:46:19 -07001565 def setDequeuedNeedingChange(self):
1566 self.dequeued_needing_change = True
1567 self._setAllJobsSkipped()
1568
1569 def setUnableToMerge(self):
1570 self.current_build_set.unable_to_merge = True
1571 self._setAllJobsSkipped()
1572
James E. Blaire53250c2017-03-01 14:34:36 -08001573 def setConfigError(self, error):
1574 self.current_build_set.config_error = error
1575 self._setAllJobsSkipped()
1576
James E. Blairdbfd3282016-07-21 10:46:19 -07001577 def _setAllJobsSkipped(self):
1578 for job in self.getJobs():
1579 fakebuild = Build(job, None)
1580 fakebuild.result = 'SKIPPED'
1581 self.addBuild(fakebuild)
1582
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001583 def formatUrlPattern(self, url_pattern, job=None, build=None):
1584 url = None
1585 # Produce safe versions of objects which may be useful in
1586 # result formatting, but don't allow users to crawl through
1587 # the entire data structure where they might be able to access
1588 # secrets, etc.
1589 safe_change = self.change.getSafeAttributes()
1590 safe_pipeline = self.pipeline.getSafeAttributes()
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001591 safe_tenant = self.pipeline.layout.tenant.getSafeAttributes()
K Jonathan Harkera7f390c2017-06-02 12:31:22 -07001592 safe_job = job.getSafeAttributes() if job else {}
1593 safe_build = build.getSafeAttributes() if build else {}
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001594 try:
1595 url = url_pattern.format(change=safe_change,
1596 pipeline=safe_pipeline,
Jamie Lennoxbf997c82017-05-10 09:58:40 +10001597 tenant=safe_tenant,
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001598 job=safe_job,
1599 build=safe_build)
1600 except KeyError as e:
1601 self.log.error("Error while formatting url for job %s: unknown "
1602 "key %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001603 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001604 except AttributeError as e:
1605 self.log.error("Error while formatting url for job %s: unknown "
1606 "attribute %s in pattern %s"
Clint Byrume0093db2017-05-10 20:53:43 -07001607 % (job, e.args[0], url_pattern))
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001608 except Exception:
1609 self.log.exception("Error while formatting url for job %s with "
1610 "pattern %s:" % (job, url_pattern))
1611
1612 return url
1613
James E. Blair800e7ff2017-03-17 16:06:52 -07001614 def formatJobResult(self, job):
James E. Blairb7273ef2016-04-19 08:58:51 -07001615 build = self.current_build_set.getBuild(job.name)
1616 result = build.result
James E. Blair800e7ff2017-03-17 16:06:52 -07001617 pattern = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001618 if result == 'SUCCESS':
1619 if job.success_message:
1620 result = job.success_message
James E. Blaira5dba232016-08-08 15:53:24 -07001621 if job.success_url:
1622 pattern = job.success_url
James E. Blairb7273ef2016-04-19 08:58:51 -07001623 elif result == 'FAILURE':
1624 if job.failure_message:
1625 result = job.failure_message
James E. Blaira5dba232016-08-08 15:53:24 -07001626 if job.failure_url:
1627 pattern = job.failure_url
James E. Blairb7273ef2016-04-19 08:58:51 -07001628 url = None
1629 if pattern:
Joshua Heskethc0c012b2017-03-28 08:45:50 +11001630 url = self.formatUrlPattern(pattern, job, build)
James E. Blairb7273ef2016-04-19 08:58:51 -07001631 if not url:
1632 url = build.url or job.name
1633 return (result, url)
1634
James E. Blair800e7ff2017-03-17 16:06:52 -07001635 def formatJSON(self):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001636 ret = {}
1637 ret['active'] = self.active
James E. Blair107c3852015-02-07 08:23:10 -08001638 ret['live'] = self.live
Monty Taylor55d0f562017-05-17 14:30:21 -05001639 if hasattr(self.change, 'url') and self.change.url is not None:
1640 ret['url'] = self.change.url
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001641 else:
1642 ret['url'] = None
Monty Taylor55d0f562017-05-17 14:30:21 -05001643 ret['id'] = self.change._id()
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001644 if self.item_ahead:
1645 ret['item_ahead'] = self.item_ahead.change._id()
1646 else:
1647 ret['item_ahead'] = None
1648 ret['items_behind'] = [i.change._id() for i in self.items_behind]
1649 ret['failing_reasons'] = self.current_build_set.failing_reasons
1650 ret['zuul_ref'] = self.current_build_set.ref
Monty Taylor55d0f562017-05-17 14:30:21 -05001651 if self.change.project:
1652 ret['project'] = self.change.project.name
Ramy Asselin07cc33c2015-06-12 14:06:34 -07001653 else:
1654 # For cross-project dependencies with the depends-on
1655 # project not known to zuul, the project is None
1656 # Set it to a static value
1657 ret['project'] = "Unknown Project"
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001658 ret['enqueue_time'] = int(self.enqueue_time * 1000)
1659 ret['jobs'] = []
Monty Taylor55d0f562017-05-17 14:30:21 -05001660 if hasattr(self.change, 'owner'):
1661 ret['owner'] = self.change.owner
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001662 else:
1663 ret['owner'] = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001664 max_remaining = 0
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001665 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001666 now = time.time()
1667 build = self.current_build_set.getBuild(job.name)
1668 elapsed = None
1669 remaining = None
1670 result = None
James E. Blairb7273ef2016-04-19 08:58:51 -07001671 build_url = None
1672 report_url = None
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001673 worker = None
1674 if build:
1675 result = build.result
James E. Blairb7273ef2016-04-19 08:58:51 -07001676 build_url = build.url
James E. Blair800e7ff2017-03-17 16:06:52 -07001677 (unused, report_url) = self.formatJobResult(job)
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001678 if build.start_time:
1679 if build.end_time:
1680 elapsed = int((build.end_time -
1681 build.start_time) * 1000)
1682 remaining = 0
1683 else:
1684 elapsed = int((now - build.start_time) * 1000)
1685 if build.estimated_time:
1686 remaining = max(
1687 int(build.estimated_time * 1000) - elapsed,
1688 0)
1689 worker = {
1690 'name': build.worker.name,
1691 'hostname': build.worker.hostname,
1692 'ips': build.worker.ips,
1693 'fqdn': build.worker.fqdn,
1694 'program': build.worker.program,
1695 'version': build.worker.version,
1696 'extra': build.worker.extra
1697 }
1698 if remaining and remaining > max_remaining:
1699 max_remaining = remaining
1700
1701 ret['jobs'].append({
1702 'name': job.name,
1703 'elapsed_time': elapsed,
1704 'remaining_time': remaining,
James E. Blairb7273ef2016-04-19 08:58:51 -07001705 'url': build_url,
1706 'report_url': report_url,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001707 'result': result,
1708 'voting': job.voting,
1709 'uuid': build.uuid if build else None,
Paul Belanger174a8272017-03-14 13:20:10 -04001710 'execute_time': build.execute_time if build else None,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001711 'start_time': build.start_time if build else None,
1712 'end_time': build.end_time if build else None,
1713 'estimated_time': build.estimated_time if build else None,
1714 'pipeline': build.pipeline.name if build else None,
1715 'canceled': build.canceled if build else None,
1716 'retry': build.retry if build else None,
Timothy Chavezb2332082015-08-07 20:08:04 -05001717 'node_labels': build.node_labels if build else [],
1718 'node_name': build.node_name if build else None,
1719 'worker': worker,
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001720 })
1721
James E. Blairdbfd3282016-07-21 10:46:19 -07001722 if self.haveAllJobsStarted():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001723 ret['remaining_time'] = max_remaining
1724 else:
1725 ret['remaining_time'] = None
1726 return ret
1727
1728 def formatStatus(self, indent=0, html=False):
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001729 indent_str = ' ' * indent
1730 ret = ''
Monty Taylor55d0f562017-05-17 14:30:21 -05001731 if html and getattr(self.change, 'url', None) is not None:
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001732 ret += '%sProject %s change <a href="%s">%s</a>\n' % (
1733 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001734 self.change.project.name,
1735 self.change.url,
1736 self.change._id())
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001737 else:
1738 ret += '%sProject %s change %s based on %s\n' % (
1739 indent_str,
Monty Taylor55d0f562017-05-17 14:30:21 -05001740 self.change.project.name,
1741 self.change._id(),
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001742 self.item_ahead)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001743 for job in self.getJobs():
Joshua Hesketh85af4e92014-02-21 08:28:58 -08001744 build = self.current_build_set.getBuild(job.name)
1745 if build:
1746 result = build.result
1747 else:
1748 result = None
1749 job_name = job.name
1750 if not job.voting:
1751 voting = ' (non-voting)'
1752 else:
1753 voting = ''
1754 if html:
1755 if build:
1756 url = build.url
1757 else:
1758 url = None
1759 if url is not None:
1760 job_name = '<a href="%s">%s</a>' % (url, job_name)
1761 ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
1762 ret += '\n'
1763 return ret
1764
James E. Blaira04b0792017-04-27 09:59:06 -07001765 def makeMergerItem(self):
1766 # Create a dictionary with all info about the item needed by
1767 # the merger.
1768 number = None
1769 patchset = None
1770 oldrev = None
1771 newrev = None
1772 refspec = None
1773 if hasattr(self.change, 'number'):
1774 number = self.change.number
1775 patchset = self.change.patchset
1776 refspec = self.change.refspec
1777 branch = self.change.branch
1778 elif hasattr(self.change, 'newrev'):
1779 oldrev = self.change.oldrev
1780 newrev = self.change.newrev
1781 branch = self.change.ref
1782 else:
1783 oldrev = None
1784 newrev = None
1785 branch = None
1786 source = self.change.project.source
1787 connection_name = source.connection.connection_name
James E. Blair2a535672017-04-27 12:03:15 -07001788 project = self.change.project
James E. Blaira04b0792017-04-27 09:59:06 -07001789
James E. Blair2a535672017-04-27 12:03:15 -07001790 return dict(project=project.name,
1791 connection=connection_name,
James E. Blaira04b0792017-04-27 09:59:06 -07001792 merge_mode=self.current_build_set.getMergeMode(),
1793 refspec=refspec,
1794 branch=branch,
1795 ref=self.current_build_set.ref,
1796 number=number,
1797 patchset=patchset,
1798 oldrev=oldrev,
1799 newrev=newrev,
1800 )
1801
James E. Blairfee8d652013-06-07 08:57:52 -07001802
Clint Byrumf8cc9902017-03-22 22:38:25 -07001803class Ref(object):
1804 """An existing state of a Project."""
James E. Blairfee8d652013-06-07 08:57:52 -07001805
1806 def __init__(self, project):
1807 self.project = project
Clint Byrumf8cc9902017-03-22 22:38:25 -07001808 self.ref = None
1809 self.oldrev = None
1810 self.newrev = None
James E. Blairfee8d652013-06-07 08:57:52 -07001811
Joshua Hesketh36c3fa52014-01-22 11:40:52 +11001812 def getBasePath(self):
1813 base_path = ''
Clint Byrumf8cc9902017-03-22 22:38:25 -07001814 if hasattr(self, 'ref'):
Joshua Hesketh36c3fa52014-01-22 11:40:52 +11001815 base_path = "%s/%s" % (self.newrev[:2], self.newrev)
1816
1817 return base_path
1818
Clint Byrumf8cc9902017-03-22 22:38:25 -07001819 def _id(self):
1820 return self.newrev
1821
1822 def __repr__(self):
1823 rep = None
1824 if self.newrev == '0000000000000000000000000000000000000000':
1825 rep = '<Ref 0x%x deletes %s from %s' % (
1826 id(self), self.ref, self.oldrev)
1827 elif self.oldrev == '0000000000000000000000000000000000000000':
1828 rep = '<Ref 0x%x creates %s on %s>' % (
1829 id(self), self.ref, self.newrev)
1830 else:
1831 # Catch all
1832 rep = '<Ref 0x%x %s updated %s..%s>' % (
1833 id(self), self.ref, self.oldrev, self.newrev)
1834
1835 return rep
1836
James E. Blairfee8d652013-06-07 08:57:52 -07001837 def equals(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001838 if (self.project == other.project
1839 and self.ref == other.ref
1840 and self.newrev == other.newrev):
1841 return True
1842 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001843
1844 def isUpdateOf(self, other):
Clint Byrumf8cc9902017-03-22 22:38:25 -07001845 return False
James E. Blairfee8d652013-06-07 08:57:52 -07001846
1847 def filterJobs(self, jobs):
1848 return filter(lambda job: job.changeMatches(self), jobs)
1849
1850 def getRelatedChanges(self):
1851 return set()
1852
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001853 def updatesConfig(self):
1854 return False
1855
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001856 def getSafeAttributes(self):
1857 return Attributes(project=self.project,
1858 ref=self.ref,
1859 oldrev=self.oldrev,
1860 newrev=self.newrev)
1861
James E. Blair1e8dd892012-05-30 09:15:05 -07001862
Clint Byrumf8cc9902017-03-22 22:38:25 -07001863class Change(Ref):
Monty Taylora42a55b2016-07-29 07:53:33 -07001864 """A proposed new state for a Project."""
James E. Blair4aea70c2012-07-26 14:23:24 -07001865 def __init__(self, project):
1866 super(Change, self).__init__(project)
1867 self.branch = None
1868 self.number = None
1869 self.url = None
1870 self.patchset = None
1871 self.refspec = None
1872
James E. Blair70c71582013-03-06 08:50:50 -08001873 self.files = []
James E. Blair6965a4b2014-12-16 17:19:04 -08001874 self.needs_changes = []
James E. Blair4aea70c2012-07-26 14:23:24 -07001875 self.needed_by_changes = []
1876 self.is_current_patchset = True
1877 self.can_merge = False
1878 self.is_merged = False
James E. Blairfee8d652013-06-07 08:57:52 -07001879 self.failed_to_merge = False
James E. Blair11041d22014-05-02 14:49:53 -07001880 self.open = None
1881 self.status = None
Davanum Srinivasb6bfbcc2014-11-18 13:26:52 -05001882 self.owner = None
James E. Blair4aea70c2012-07-26 14:23:24 -07001883
Jan Hruban3b415922016-02-03 13:10:22 +01001884 self.source_event = None
1885
James E. Blair4aea70c2012-07-26 14:23:24 -07001886 def _id(self):
James E. Blairbe765db2012-08-07 08:36:20 -07001887 return '%s,%s' % (self.number, self.patchset)
James E. Blair4aea70c2012-07-26 14:23:24 -07001888
1889 def __repr__(self):
1890 return '<Change 0x%x %s>' % (id(self), self._id())
1891
Clint Byrumf8cc9902017-03-22 22:38:25 -07001892 def getBasePath(self):
1893 if hasattr(self, 'refspec'):
1894 return "%s/%s/%s" % (
Gregory Haynes4fc12542015-04-22 20:38:06 -07001895 str(self.number)[-2:], self.number, self.patchset)
Clint Byrumf8cc9902017-03-22 22:38:25 -07001896 return super(Change, self).getBasePath()
1897
James E. Blair4aea70c2012-07-26 14:23:24 -07001898 def equals(self, other):
Zhongyue Luoaa85ebf2012-09-21 16:38:33 +08001899 if self.number == other.number and self.patchset == other.patchset:
James E. Blair4aea70c2012-07-26 14:23:24 -07001900 return True
1901 return False
1902
James E. Blair2fa50962013-01-30 21:50:41 -08001903 def isUpdateOf(self, other):
Clark Boylan01976242013-02-17 18:41:48 -08001904 if ((hasattr(other, 'number') and self.number == other.number) and
James E. Blair7a192e42013-07-11 14:10:36 -07001905 (hasattr(other, 'patchset') and
1906 self.patchset is not None and
1907 other.patchset is not None and
1908 int(self.patchset) > int(other.patchset))):
James E. Blair2fa50962013-01-30 21:50:41 -08001909 return True
1910 return False
1911
James E. Blairfee8d652013-06-07 08:57:52 -07001912 def getRelatedChanges(self):
1913 related = set()
James E. Blair6965a4b2014-12-16 17:19:04 -08001914 for c in self.needs_changes:
1915 related.add(c)
James E. Blairfee8d652013-06-07 08:57:52 -07001916 for c in self.needed_by_changes:
1917 related.add(c)
1918 related.update(c.getRelatedChanges())
1919 return related
James E. Blair4aea70c2012-07-26 14:23:24 -07001920
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001921 def updatesConfig(self):
1922 if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files:
1923 return True
1924 return False
1925
Joshua Hesketh58419cb2017-02-24 13:09:22 -05001926 def getSafeAttributes(self):
1927 return Attributes(project=self.project,
1928 number=self.number,
1929 patchset=self.patchset)
1930
James E. Blair4aea70c2012-07-26 14:23:24 -07001931
James E. Blairee743612012-05-29 14:49:32 -07001932class TriggerEvent(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001933 """Incoming event from an external system."""
James E. Blairee743612012-05-29 14:49:32 -07001934 def __init__(self):
James E. Blairaad3ae22017-05-18 14:11:29 -07001935 # TODO(jeblair): further reduce this list
James E. Blairee743612012-05-29 14:49:32 -07001936 self.data = None
James E. Blair32663402012-06-01 10:04:18 -07001937 # common
James E. Blairee743612012-05-29 14:49:32 -07001938 self.type = None
Paul Belangerbaca3132016-11-04 12:49:54 -04001939 # For management events (eg: enqueue / promote)
1940 self.tenant_name = None
James E. Blair6f284b42017-03-31 14:14:41 -07001941 self.project_hostname = None
James E. Blairee743612012-05-29 14:49:32 -07001942 self.project_name = None
James E. Blair6c358e72013-07-29 17:06:47 -07001943 self.trigger_name = None
Antoine Mussob4e809e2012-12-06 16:58:06 +01001944 # Representation of the user account that performed the event.
1945 self.account = None
James E. Blair32663402012-06-01 10:04:18 -07001946 # patchset-created, comment-added, etc.
James E. Blairee743612012-05-29 14:49:32 -07001947 self.change_number = None
Clark Boylanfc56df32012-06-28 15:25:57 -07001948 self.change_url = None
James E. Blairee743612012-05-29 14:49:32 -07001949 self.patch_number = None
James E. Blaira03262c2012-05-30 09:41:16 -07001950 self.refspec = None
James E. Blairee743612012-05-29 14:49:32 -07001951 self.branch = None
Clark Boylanb9bcb402012-06-29 17:44:05 -07001952 self.comment = None
Jesse Keating5c05a9f2017-01-12 14:44:58 -08001953 self.state = None
James E. Blair32663402012-06-01 10:04:18 -07001954 # ref-updated
James E. Blairee743612012-05-29 14:49:32 -07001955 self.ref = None
James E. Blair32663402012-06-01 10:04:18 -07001956 self.oldrev = None
James E. Blair89cae0f2012-07-18 11:18:32 -07001957 self.newrev = None
James E. Blairad28e912013-11-27 10:43:22 -08001958 # For events that arrive with a destination pipeline (eg, from
1959 # an admin command, etc):
1960 self.forced_pipeline = None
James E. Blairee743612012-05-29 14:49:32 -07001961
James E. Blair6f284b42017-03-31 14:14:41 -07001962 @property
1963 def canonical_project_name(self):
1964 return self.project_hostname + '/' + self.project_name
1965
Jan Hruban324ca5b2015-11-05 19:28:54 +01001966 def isPatchsetCreated(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01001967 return False
1968
1969 def isChangeAbandoned(self):
Jan Hruban324ca5b2015-11-05 19:28:54 +01001970 return False
1971
James E. Blair1e8dd892012-05-30 09:15:05 -07001972
James E. Blair9c17dbf2014-06-23 14:21:58 -07001973class BaseFilter(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07001974 """Base Class for filtering which Changes and Events to process."""
James E. Blairaad3ae22017-05-18 14:11:29 -07001975 pass
Joshua Hesketh66c8e522014-06-26 15:30:08 +10001976
James E. Blair9c17dbf2014-06-23 14:21:58 -07001977
1978class EventFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07001979 """Allows a Pipeline to only respond to certain events."""
James E. Blairaad3ae22017-05-18 14:11:29 -07001980 def __init__(self, trigger):
1981 super(EventFilter, self).__init__()
James E. Blairc0dedf82014-08-06 09:37:52 -07001982 self.trigger = trigger
James E. Blairee743612012-05-29 14:49:32 -07001983
James E. Blairaad3ae22017-05-18 14:11:29 -07001984 def matches(self, event, ref):
1985 # TODO(jeblair): consider removing ref argument
James E. Blairee743612012-05-29 14:49:32 -07001986 return True
James E. Blaireff88162013-07-01 12:44:14 -04001987
1988
James E. Blairaad3ae22017-05-18 14:11:29 -07001989class RefFilter(BaseFilter):
Monty Taylora42a55b2016-07-29 07:53:33 -07001990 """Allows a Manager to only enqueue Changes that meet certain criteria."""
Jesse Keating8c2eb572017-05-30 17:31:45 -07001991 def __init__(self, connection_name):
James E. Blairaad3ae22017-05-18 14:11:29 -07001992 super(RefFilter, self).__init__()
Jesse Keating8c2eb572017-05-30 17:31:45 -07001993 self.connection_name = connection_name
James E. Blair11041d22014-05-02 14:49:53 -07001994
1995 def matches(self, change):
James E. Blair11041d22014-05-02 14:49:53 -07001996 return True
1997
1998
James E. Blairb97ed802015-12-21 15:55:35 -08001999class ProjectPipelineConfig(object):
2000 # Represents a project cofiguration in the context of a pipeline
2001 def __init__(self):
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002002 self.job_list = JobList()
James E. Blairb97ed802015-12-21 15:55:35 -08002003 self.queue_name = None
Adam Gandelman8bd57102016-12-02 12:58:42 -08002004 self.merge_mode = None
James E. Blairb97ed802015-12-21 15:55:35 -08002005
2006
2007class ProjectConfig(object):
2008 # Represents a project cofiguration
2009 def __init__(self, name):
2010 self.name = name
Adam Gandelman8bd57102016-12-02 12:58:42 -08002011 self.merge_mode = None
James E. Blair040b6502017-05-23 10:18:21 -07002012 self.default_branch = None
James E. Blairb97ed802015-12-21 15:55:35 -08002013 self.pipelines = {}
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002014 self.private_key_file = None
James E. Blairb97ed802015-12-21 15:55:35 -08002015
2016
James E. Blaird8e778f2015-12-22 14:09:20 -08002017class UnparsedAbideConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002018 """A collection of yaml lists that has not yet been parsed into objects.
2019
2020 An Abide is a collection of tenants.
2021 """
2022
James E. Blaird8e778f2015-12-22 14:09:20 -08002023 def __init__(self):
2024 self.tenants = []
2025
2026 def extend(self, conf):
2027 if isinstance(conf, UnparsedAbideConfig):
2028 self.tenants.extend(conf.tenants)
2029 return
2030
2031 if not isinstance(conf, list):
2032 raise Exception("Configuration items must be in the form of "
2033 "a list of dictionaries (when parsing %s)" %
2034 (conf,))
2035 for item in conf:
2036 if not isinstance(item, dict):
2037 raise Exception("Configuration items must be in the form of "
2038 "a list of dictionaries (when parsing %s)" %
2039 (conf,))
2040 if len(item.keys()) > 1:
2041 raise Exception("Configuration item dictionaries must have "
2042 "a single key (when parsing %s)" %
2043 (conf,))
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002044 key, value = list(item.items())[0]
James E. Blaird8e778f2015-12-22 14:09:20 -08002045 if key == 'tenant':
2046 self.tenants.append(value)
2047 else:
2048 raise Exception("Configuration item not recognized "
2049 "(when parsing %s)" %
2050 (conf,))
2051
2052
2053class UnparsedTenantConfig(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002054 """A collection of yaml lists that has not yet been parsed into objects."""
2055
James E. Blaird8e778f2015-12-22 14:09:20 -08002056 def __init__(self):
2057 self.pipelines = []
2058 self.jobs = []
2059 self.project_templates = []
James E. Blairff555742017-02-19 11:34:27 -08002060 self.projects = {}
James E. Blaira98340f2016-09-02 11:33:49 -07002061 self.nodesets = []
James E. Blair01f83b72017-03-15 13:03:40 -07002062 self.secrets = []
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002063 self.semaphores = []
James E. Blaird8e778f2015-12-22 14:09:20 -08002064
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002065 def copy(self):
2066 r = UnparsedTenantConfig()
2067 r.pipelines = copy.deepcopy(self.pipelines)
2068 r.jobs = copy.deepcopy(self.jobs)
2069 r.project_templates = copy.deepcopy(self.project_templates)
2070 r.projects = copy.deepcopy(self.projects)
James E. Blaira98340f2016-09-02 11:33:49 -07002071 r.nodesets = copy.deepcopy(self.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002072 r.secrets = copy.deepcopy(self.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002073 r.semaphores = copy.deepcopy(self.semaphores)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002074 return r
2075
James E. Blairec7ff302017-03-04 07:31:32 -08002076 def extend(self, conf):
James E. Blaird8e778f2015-12-22 14:09:20 -08002077 if isinstance(conf, UnparsedTenantConfig):
2078 self.pipelines.extend(conf.pipelines)
2079 self.jobs.extend(conf.jobs)
2080 self.project_templates.extend(conf.project_templates)
James E. Blairff555742017-02-19 11:34:27 -08002081 for k, v in conf.projects.items():
2082 self.projects.setdefault(k, []).extend(v)
James E. Blaira98340f2016-09-02 11:33:49 -07002083 self.nodesets.extend(conf.nodesets)
James E. Blair01f83b72017-03-15 13:03:40 -07002084 self.secrets.extend(conf.secrets)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002085 self.semaphores.extend(conf.semaphores)
James E. Blaird8e778f2015-12-22 14:09:20 -08002086 return
2087
2088 if not isinstance(conf, list):
2089 raise Exception("Configuration items must be in the form of "
2090 "a list of dictionaries (when parsing %s)" %
2091 (conf,))
James E. Blaircdab2032017-02-01 09:09:29 -08002092
James E. Blaird8e778f2015-12-22 14:09:20 -08002093 for item in conf:
2094 if not isinstance(item, dict):
2095 raise Exception("Configuration items must be in the form of "
2096 "a list of dictionaries (when parsing %s)" %
2097 (conf,))
2098 if len(item.keys()) > 1:
2099 raise Exception("Configuration item dictionaries must have "
2100 "a single key (when parsing %s)" %
2101 (conf,))
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002102 key, value = list(item.items())[0]
James E. Blair66b274e2017-01-31 14:47:52 -08002103 if key == 'project':
James E. Blairff555742017-02-19 11:34:27 -08002104 name = value['name']
2105 self.projects.setdefault(name, []).append(value)
James E. Blair66b274e2017-01-31 14:47:52 -08002106 elif key == 'job':
James E. Blaird8e778f2015-12-22 14:09:20 -08002107 self.jobs.append(value)
2108 elif key == 'project-template':
2109 self.project_templates.append(value)
2110 elif key == 'pipeline':
2111 self.pipelines.append(value)
James E. Blaira98340f2016-09-02 11:33:49 -07002112 elif key == 'nodeset':
2113 self.nodesets.append(value)
James E. Blair01f83b72017-03-15 13:03:40 -07002114 elif key == 'secret':
2115 self.secrets.append(value)
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002116 elif key == 'semaphore':
2117 self.semaphores.append(value)
James E. Blaird8e778f2015-12-22 14:09:20 -08002118 else:
Morgan Fainberg4245a422016-08-05 16:20:12 -07002119 raise Exception("Configuration item `%s` not recognized "
James E. Blaird8e778f2015-12-22 14:09:20 -08002120 "(when parsing %s)" %
Morgan Fainberg4245a422016-08-05 16:20:12 -07002121 (item, conf,))
James E. Blaird8e778f2015-12-22 14:09:20 -08002122
2123
James E. Blaireff88162013-07-01 12:44:14 -04002124class Layout(object):
Monty Taylora42a55b2016-07-29 07:53:33 -07002125 """Holds all of the Pipelines."""
2126
James E. Blaireff88162013-07-01 12:44:14 -04002127 def __init__(self):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002128 self.tenant = None
James E. Blairb97ed802015-12-21 15:55:35 -08002129 self.project_configs = {}
2130 self.project_templates = {}
James E. Blair5a9918a2013-08-27 10:06:27 -07002131 self.pipelines = OrderedDict()
James E. Blair83005782015-12-11 14:46:03 -08002132 # This is a dictionary of name -> [jobs]. The first element
2133 # of the list is the first job added with that name. It is
2134 # the reference definition for a given job. Subsequent
2135 # elements are aspects of that job with different matchers
2136 # that override some attribute of the job. These aspects all
2137 # inherit from the reference definition.
James E. Blairfef88ec2016-11-30 08:38:27 -08002138 self.jobs = {'noop': [Job('noop')]}
James E. Blaira98340f2016-09-02 11:33:49 -07002139 self.nodesets = {}
James E. Blair01f83b72017-03-15 13:03:40 -07002140 self.secrets = {}
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002141 self.semaphores = {}
James E. Blaireff88162013-07-01 12:44:14 -04002142
2143 def getJob(self, name):
2144 if name in self.jobs:
James E. Blair83005782015-12-11 14:46:03 -08002145 return self.jobs[name][0]
James E. Blairb97ed802015-12-21 15:55:35 -08002146 raise Exception("Job %s not defined" % (name,))
James E. Blair83005782015-12-11 14:46:03 -08002147
2148 def getJobs(self, name):
2149 return self.jobs.get(name, [])
2150
2151 def addJob(self, job):
James E. Blair4317e9f2016-07-15 10:05:47 -07002152 # We can have multiple variants of a job all with the same
2153 # name, but these variants must all be defined in the same repo.
James E. Blaircdab2032017-02-01 09:09:29 -08002154 prior_jobs = [j for j in self.getJobs(job.name) if
2155 j.source_context.project !=
2156 job.source_context.project]
James E. Blair4317e9f2016-07-15 10:05:47 -07002157 if prior_jobs:
2158 raise Exception("Job %s in %s is not permitted to shadow "
James E. Blaircdab2032017-02-01 09:09:29 -08002159 "job %s in %s" % (
2160 job,
2161 job.source_context.project,
2162 prior_jobs[0],
2163 prior_jobs[0].source_context.project))
James E. Blair4317e9f2016-07-15 10:05:47 -07002164
James E. Blair83005782015-12-11 14:46:03 -08002165 if job.name in self.jobs:
2166 self.jobs[job.name].append(job)
2167 else:
2168 self.jobs[job.name] = [job]
2169
James E. Blaira98340f2016-09-02 11:33:49 -07002170 def addNodeSet(self, nodeset):
2171 if nodeset.name in self.nodesets:
2172 raise Exception("NodeSet %s already defined" % (nodeset.name,))
2173 self.nodesets[nodeset.name] = nodeset
2174
James E. Blair01f83b72017-03-15 13:03:40 -07002175 def addSecret(self, secret):
2176 if secret.name in self.secrets:
2177 raise Exception("Secret %s already defined" % (secret.name,))
2178 self.secrets[secret.name] = secret
2179
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002180 def addSemaphore(self, semaphore):
2181 if semaphore.name in self.semaphores:
2182 raise Exception("Semaphore %s already defined" % (semaphore.name,))
2183 self.semaphores[semaphore.name] = semaphore
2184
James E. Blair83005782015-12-11 14:46:03 -08002185 def addPipeline(self, pipeline):
2186 self.pipelines[pipeline.name] = pipeline
James E. Blair59fdbac2015-12-07 17:08:06 -08002187
James E. Blairb97ed802015-12-21 15:55:35 -08002188 def addProjectTemplate(self, project_template):
2189 self.project_templates[project_template.name] = project_template
2190
James E. Blairf59f3cf2017-02-19 14:50:26 -08002191 def addProjectConfig(self, project_config):
James E. Blairb97ed802015-12-21 15:55:35 -08002192 self.project_configs[project_config.name] = project_config
James E. Blairb97ed802015-12-21 15:55:35 -08002193
James E. Blaird2348362017-03-17 13:59:35 -07002194 def _createJobGraph(self, item, job_list, job_graph):
2195 change = item.change
2196 pipeline = item.pipeline
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002197 for jobname in job_list.jobs:
2198 # This is the final job we are constructing
James E. Blaira7f51ca2017-02-07 16:01:26 -08002199 frozen_job = None
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002200 # Whether the change matches any globally defined variant
James E. Blaira7f51ca2017-02-07 16:01:26 -08002201 matched = False
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002202 for variant in self.getJobs(jobname):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002203 if variant.changeMatches(change):
James E. Blaira7f51ca2017-02-07 16:01:26 -08002204 if frozen_job is None:
2205 frozen_job = variant.copy()
2206 frozen_job.setRun()
2207 else:
2208 frozen_job.applyVariant(variant)
2209 matched = True
2210 if not matched:
James E. Blair6e85c2b2016-11-21 16:47:01 -08002211 # A change must match at least one defined job variant
2212 # (that is to say that it must match more than just
2213 # the job that is defined in the tree).
2214 continue
James E. Blaira7f51ca2017-02-07 16:01:26 -08002215 # If the job does not allow auth inheritance, do not allow
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002216 # the project-pipeline variants to update its execution
James E. Blaira7f51ca2017-02-07 16:01:26 -08002217 # attributes.
James E. Blair8525e2b2017-03-15 14:05:47 -07002218 if frozen_job.auth and not frozen_job.auth.inherit:
James E. Blaira7f51ca2017-02-07 16:01:26 -08002219 frozen_job.final = True
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002220 # Whether the change matches any of the project pipeline
2221 # variants
2222 matched = False
2223 for variant in job_list.jobs[jobname]:
2224 if variant.changeMatches(change):
2225 frozen_job.applyVariant(variant)
2226 matched = True
2227 if not matched:
2228 # A change must match at least one project pipeline
2229 # job variant.
2230 continue
James E. Blairb3f5db12017-03-17 12:57:39 -07002231 if (frozen_job.allowed_projects and
2232 change.project.name not in frozen_job.allowed_projects):
2233 raise Exception("Project %s is not allowed to run job %s" %
2234 (change.project.name, frozen_job.name))
James E. Blaird2348362017-03-17 13:59:35 -07002235 if ((not pipeline.allow_secrets) and frozen_job.auth and
2236 frozen_job.auth.secrets):
2237 raise Exception("Pipeline %s does not allow jobs with "
2238 "secrets (job %s)" % (
2239 pipeline.name, frozen_job.name))
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002240 job_graph.addJob(frozen_job)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002241
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002242 def createJobGraph(self, item):
Paul Belanger160cb8e2016-11-11 19:04:24 -05002243 project_config = self.project_configs.get(
James E. Blair0ffa0102017-03-30 13:11:33 -07002244 item.change.project.canonical_name, None)
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002245 ret = JobGraph()
Paul Belanger15e3e202016-10-14 16:27:34 -04002246 # NOTE(pabelanger): It is possible for a foreign project not to have a
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002247 # configured pipeline, if so return an empty JobGraph.
Paul Belanger160cb8e2016-11-11 19:04:24 -05002248 if project_config and item.pipeline.name in project_config.pipelines:
Fredrik Medleyf8aec832015-09-28 13:40:20 +02002249 project_job_list = \
2250 project_config.pipelines[item.pipeline.name].job_list
James E. Blaird2348362017-03-17 13:59:35 -07002251 self._createJobGraph(item, project_job_list, ret)
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002252 return ret
2253
James E. Blair0d3e83b2017-06-05 13:51:57 -07002254 def hasProject(self, project):
2255 return project.canonical_name in self.project_configs
2256
James E. Blair59fdbac2015-12-07 17:08:06 -08002257
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002258class Semaphore(object):
2259 def __init__(self, name, max=1):
2260 self.name = name
2261 self.max = int(max)
2262
2263
2264class SemaphoreHandler(object):
2265 log = logging.getLogger("zuul.SemaphoreHandler")
2266
2267 def __init__(self):
2268 self.semaphores = {}
2269
2270 def acquire(self, item, job):
2271 if not job.semaphore:
2272 return True
2273
2274 semaphore_key = job.semaphore
2275
2276 m = self.semaphores.get(semaphore_key)
2277 if not m:
2278 # The semaphore is not held, acquire it
2279 self._acquire(semaphore_key, item, job.name)
2280 return True
2281 if (item, job.name) in m:
2282 # This item already holds the semaphore
2283 return True
2284
2285 # semaphore is there, check max
2286 if len(m) < self._max_count(item, job.semaphore):
2287 self._acquire(semaphore_key, item, job.name)
2288 return True
2289
2290 return False
2291
2292 def release(self, item, job):
2293 if not job.semaphore:
2294 return
2295
2296 semaphore_key = job.semaphore
2297
2298 m = self.semaphores.get(semaphore_key)
2299 if not m:
2300 # The semaphore is not held, nothing to do
2301 self.log.error("Semaphore can not be released for %s "
2302 "because the semaphore is not held" %
2303 item)
2304 return
2305 if (item, job.name) in m:
2306 # This item is a holder of the semaphore
2307 self._release(semaphore_key, item, job.name)
2308 return
2309 self.log.error("Semaphore can not be released for %s "
2310 "which does not hold it" % item)
2311
2312 def _acquire(self, semaphore_key, item, job_name):
2313 self.log.debug("Semaphore acquire {semaphore}: job {job}, item {item}"
2314 .format(semaphore=semaphore_key,
2315 job=job_name,
2316 item=item))
2317 if semaphore_key not in self.semaphores:
2318 self.semaphores[semaphore_key] = []
2319 self.semaphores[semaphore_key].append((item, job_name))
2320
2321 def _release(self, semaphore_key, item, job_name):
2322 self.log.debug("Semaphore release {semaphore}: job {job}, item {item}"
2323 .format(semaphore=semaphore_key,
2324 job=job_name,
2325 item=item))
2326 sem_item = (item, job_name)
2327 if sem_item in self.semaphores[semaphore_key]:
2328 self.semaphores[semaphore_key].remove(sem_item)
2329
2330 # cleanup if there is no user of the semaphore anymore
2331 if len(self.semaphores[semaphore_key]) == 0:
2332 del self.semaphores[semaphore_key]
2333
2334 @staticmethod
2335 def _max_count(item, semaphore_name):
2336 if not item.current_build_set.layout:
2337 # This should not occur as the layout of the item must already be
2338 # built when acquiring or releasing a semaphore for a job.
2339 raise Exception("Item {} has no layout".format(item))
2340
2341 # find the right semaphore
2342 default_semaphore = Semaphore(semaphore_name, 1)
2343 semaphores = item.current_build_set.layout.semaphores
2344 return semaphores.get(semaphore_name, default_semaphore).max
2345
2346
James E. Blair59fdbac2015-12-07 17:08:06 -08002347class Tenant(object):
2348 def __init__(self, name):
2349 self.name = name
2350 self.layout = None
James E. Blair646322f2017-01-27 15:50:34 -08002351 # The unparsed configuration from the main zuul config for
2352 # this tenant.
2353 self.unparsed_config = None
James E. Blair109da3f2017-04-04 14:39:43 -07002354 # The list of projects from which we will read full
James E. Blair8a395f92017-03-30 11:15:33 -07002355 # configuration.
James E. Blair109da3f2017-04-04 14:39:43 -07002356 self.config_projects = []
2357 # The unparsed config from those projects.
2358 self.config_projects_config = None
2359 # The list of projects from which we will read untrusted
2360 # in-repo configuration.
2361 self.untrusted_projects = []
2362 # The unparsed config from those projects.
2363 self.untrusted_projects_config = None
Tobias Henkel9a0e1942017-03-20 16:16:02 +01002364 self.semaphore_handler = SemaphoreHandler()
2365
James E. Blairc2a54fd2017-03-29 15:19:26 -07002366 # A mapping of project names to projects. project_name ->
2367 # VALUE where VALUE is a further dictionary of
2368 # canonical_hostname -> Project.
2369 self.projects = {}
2370 self.canonical_hostnames = set()
2371
2372 def _addProject(self, project):
2373 """Add a project to the project index
2374
2375 :arg Project project: The project to add.
2376 """
2377 self.canonical_hostnames.add(project.canonical_hostname)
2378 hostname_dict = self.projects.setdefault(project.name, {})
2379 if project.canonical_hostname in hostname_dict:
2380 raise Exception("Project %s is already in project index" %
2381 (project,))
2382 hostname_dict[project.canonical_hostname] = project
2383
2384 def getProject(self, name):
2385 """Return a project given its name.
2386
2387 :arg str name: The name of the project. It may be fully
2388 qualified (E.g., "git.example.com/subpath/project") or may
2389 contain only the project name name may be supplied (E.g.,
2390 "subpath/project").
2391
2392 :returns: A tuple (trusted, project) or (None, None) if the
2393 project is not found or ambiguous. The "trusted" boolean
2394 indicates whether or not the project is trusted by this
2395 tenant.
2396 :rtype: (bool, Project)
2397
2398 """
2399 path = name.split('/', 1)
2400 if path[0] in self.canonical_hostnames:
2401 hostname = path[0]
2402 project_name = path[1]
2403 else:
2404 hostname = None
2405 project_name = name
2406 hostname_dict = self.projects.get(project_name)
2407 project = None
2408 if hostname_dict:
2409 if hostname:
2410 project = hostname_dict.get(hostname)
2411 else:
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002412 values = list(hostname_dict.values())
James E. Blairc2a54fd2017-03-29 15:19:26 -07002413 if len(values) == 1:
2414 project = values[0]
2415 else:
2416 raise Exception("Project name '%s' is ambiguous, "
2417 "please fully qualify the project "
2418 "with a hostname" % (name,))
2419 if project is None:
2420 return (None, None)
James E. Blair109da3f2017-04-04 14:39:43 -07002421 if project in self.config_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002422 return (True, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002423 if project in self.untrusted_projects:
James E. Blairc2a54fd2017-03-29 15:19:26 -07002424 return (False, project)
2425 # This should never happen:
2426 raise Exception("Project %s is neither trusted nor untrusted" %
2427 (project,))
2428
James E. Blair109da3f2017-04-04 14:39:43 -07002429 def addConfigProject(self, project):
2430 self.config_projects.append(project)
James E. Blairc2a54fd2017-03-29 15:19:26 -07002431 self._addProject(project)
James E. Blair5ac93842017-01-20 06:47:34 -08002432
James E. Blair109da3f2017-04-04 14:39:43 -07002433 def addUntrustedProject(self, project):
2434 self.untrusted_projects.append(project)
James E. Blairc2a54fd2017-03-29 15:19:26 -07002435 self._addProject(project)
James E. Blair5ac93842017-01-20 06:47:34 -08002436
Jamie Lennoxbf997c82017-05-10 09:58:40 +10002437 def getSafeAttributes(self):
2438 return Attributes(name=self.name)
2439
James E. Blair59fdbac2015-12-07 17:08:06 -08002440
2441class Abide(object):
2442 def __init__(self):
2443 self.tenants = OrderedDict()
James E. Blairce8a2132016-05-19 15:21:52 -07002444
2445
2446class JobTimeData(object):
2447 format = 'B10H10H10B'
2448 version = 0
2449
2450 def __init__(self, path):
2451 self.path = path
2452 self.success_times = [0 for x in range(10)]
2453 self.failure_times = [0 for x in range(10)]
2454 self.results = [0 for x in range(10)]
2455
2456 def load(self):
2457 if not os.path.exists(self.path):
2458 return
Clint Byruma4471d12017-05-10 20:57:40 -04002459 with open(self.path, 'rb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002460 data = struct.unpack(self.format, f.read())
2461 version = data[0]
2462 if version != self.version:
2463 raise Exception("Unkown data version")
2464 self.success_times = list(data[1:11])
2465 self.failure_times = list(data[11:21])
2466 self.results = list(data[21:32])
2467
2468 def save(self):
2469 tmpfile = self.path + '.tmp'
2470 data = [self.version]
2471 data.extend(self.success_times)
2472 data.extend(self.failure_times)
2473 data.extend(self.results)
2474 data = struct.pack(self.format, *data)
Clint Byruma4471d12017-05-10 20:57:40 -04002475 with open(tmpfile, 'wb') as f:
James E. Blairce8a2132016-05-19 15:21:52 -07002476 f.write(data)
2477 os.rename(tmpfile, self.path)
2478
2479 def add(self, elapsed, result):
2480 elapsed = int(elapsed)
2481 if result == 'SUCCESS':
2482 self.success_times.append(elapsed)
2483 self.success_times.pop(0)
2484 result = 0
2485 else:
2486 self.failure_times.append(elapsed)
2487 self.failure_times.pop(0)
2488 result = 1
2489 self.results.append(result)
2490 self.results.pop(0)
2491
2492 def getEstimatedTime(self):
2493 times = [x for x in self.success_times if x]
2494 if times:
2495 return float(sum(times)) / len(times)
2496 return 0.0
2497
2498
2499class TimeDataBase(object):
2500 def __init__(self, root):
2501 self.root = root
2502 self.jobs = {}
2503
2504 def _getTD(self, name):
2505 td = self.jobs.get(name)
2506 if not td:
2507 td = JobTimeData(os.path.join(self.root, name))
2508 self.jobs[name] = td
2509 td.load()
2510 return td
2511
2512 def getEstimatedTime(self, name):
2513 return self._getTD(name).getEstimatedTime()
2514
2515 def update(self, name, elapsed, result):
2516 td = self._getTD(name)
2517 td.add(elapsed, result)
2518 td.save()