blob: 93b5785de98d0e8fca2f5aa17f6775edfe750b49 [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Christian Berendtffba5df2014-06-07 21:30:22 +020018from six.moves import configparser as ConfigParser
Adam Gandelmand81dd762017-02-09 15:15:49 -080019import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070020import gc
21import hashlib
22import json
23import logging
24import os
Christian Berendt12d4d722014-06-07 21:03:45 +020025from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070026from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070027import random
28import re
29import select
30import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030031from six.moves import reload_module
Clark Boylan21a2c812017-04-24 15:44:55 -070032try:
33 from cStringIO import StringIO
34except Exception:
35 from six import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070036import socket
37import string
38import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080039import sys
James E. Blairf84026c2015-12-08 16:11:46 -080040import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070041import threading
Clark Boylan8208c192017-04-24 18:08:08 -070042import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070043import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060044import uuid
45
Clark Boylanb640e052014-04-03 16:41:46 -070046
47import git
48import gear
49import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080050import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080051import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060052import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070053import statsd
54import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080055import testtools.content
56import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080057from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000058import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070059
James E. Blaire511d2f2016-12-08 15:22:26 -080060import zuul.driver.gerrit.gerritsource as gerritsource
61import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070062import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070063import zuul.scheduler
64import zuul.webapp
65import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040066import zuul.executor.server
67import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080068import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070069import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070070import zuul.merger.merger
71import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020072import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070073import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080074import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010075from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070076
77FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
78 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080079
80KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070081
Clark Boylanb640e052014-04-03 16:41:46 -070082
83def repack_repo(path):
84 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
85 output = subprocess.Popen(cmd, close_fds=True,
86 stdout=subprocess.PIPE,
87 stderr=subprocess.PIPE)
88 out = output.communicate()
89 if output.returncode:
90 raise Exception("git repack returned %d" % output.returncode)
91 return out
92
93
94def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040095 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070096
97
James E. Blaira190f3b2015-01-05 14:56:54 -080098def iterate_timeout(max_seconds, purpose):
99 start = time.time()
100 count = 0
101 while (time.time() < start + max_seconds):
102 count += 1
103 yield count
104 time.sleep(0)
105 raise Exception("Timeout waiting for %s" % purpose)
106
107
Jesse Keating436a5452017-04-20 11:48:41 -0700108def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700109 """Specify a layout file for use by a test method.
110
111 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700112 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700113
114 Some tests require only a very simple configuration. For those,
115 establishing a complete config directory hierachy is too much
116 work. In those cases, you can add a simple zuul.yaml file to the
117 test fixtures directory (in fixtures/layouts/foo.yaml) and use
118 this decorator to indicate the test method should use that rather
119 than the tenant config file specified by the test class.
120
121 The decorator will cause that layout file to be added to a
122 config-project called "common-config" and each "project" instance
123 referenced in the layout file will have a git repo automatically
124 initialized.
125 """
126
127 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700128 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700129 return test
130 return decorator
131
132
Gregory Haynes4fc12542015-04-22 20:38:06 -0700133class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700134 _common_path_default = "refs/changes"
135 _points_to_commits_only = True
136
137
Gregory Haynes4fc12542015-04-22 20:38:06 -0700138class FakeGerritChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700139 categories = {'approved': ('Approved', -1, 1),
140 'code-review': ('Code-Review', -2, 2),
141 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700142
143 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700144 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700145 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700146 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700147 self.reported = 0
148 self.queried = 0
149 self.patchsets = []
150 self.number = number
151 self.project = project
152 self.branch = branch
153 self.subject = subject
154 self.latest_patchset = 0
155 self.depends_on_change = None
156 self.needed_by_changes = []
157 self.fail_merge = False
158 self.messages = []
159 self.data = {
160 'branch': branch,
161 'comments': [],
162 'commitMessage': subject,
163 'createdOn': time.time(),
164 'id': 'I' + random_sha1(),
165 'lastUpdated': time.time(),
166 'number': str(number),
167 'open': status == 'NEW',
168 'owner': {'email': 'user@example.com',
169 'name': 'User Name',
170 'username': 'username'},
171 'patchSets': self.patchsets,
172 'project': project,
173 'status': status,
174 'subject': subject,
175 'submitRecords': [],
176 'url': 'https://hostname/%s' % number}
177
178 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700179 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700180 self.data['submitRecords'] = self.getSubmitRecords()
181 self.open = status == 'NEW'
182
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700183 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700184 path = os.path.join(self.upstream_root, self.project)
185 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700186 ref = GerritChangeReference.create(
187 repo, '1/%s/%s' % (self.number, self.latest_patchset),
188 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700189 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700190 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700191 repo.git.clean('-x', '-f', '-d')
192
193 path = os.path.join(self.upstream_root, self.project)
194 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700195 for fn, content in files.items():
196 fn = os.path.join(path, fn)
197 with open(fn, 'w') as f:
198 f.write(content)
199 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700200 else:
201 for fni in range(100):
202 fn = os.path.join(path, str(fni))
203 f = open(fn, 'w')
204 for ci in range(4096):
205 f.write(random.choice(string.printable))
206 f.close()
207 repo.index.add([fn])
208
209 r = repo.index.commit(msg)
210 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700211 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700212 repo.git.clean('-x', '-f', '-d')
213 repo.heads['master'].checkout()
214 return r
215
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700216 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700217 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700218 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700219 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700220 data = ("test %s %s %s\n" %
221 (self.branch, self.number, self.latest_patchset))
222 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700223 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700224 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700225 ps_files = [{'file': '/COMMIT_MSG',
226 'type': 'ADDED'},
227 {'file': 'README',
228 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700229 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700230 ps_files.append({'file': f, 'type': 'ADDED'})
231 d = {'approvals': [],
232 'createdOn': time.time(),
233 'files': ps_files,
234 'number': str(self.latest_patchset),
235 'ref': 'refs/changes/1/%s/%s' % (self.number,
236 self.latest_patchset),
237 'revision': c.hexsha,
238 'uploader': {'email': 'user@example.com',
239 'name': 'User name',
240 'username': 'user'}}
241 self.data['currentPatchSet'] = d
242 self.patchsets.append(d)
243 self.data['submitRecords'] = self.getSubmitRecords()
244
245 def getPatchsetCreatedEvent(self, patchset):
246 event = {"type": "patchset-created",
247 "change": {"project": self.project,
248 "branch": self.branch,
249 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
250 "number": str(self.number),
251 "subject": self.subject,
252 "owner": {"name": "User Name"},
253 "url": "https://hostname/3"},
254 "patchSet": self.patchsets[patchset - 1],
255 "uploader": {"name": "User Name"}}
256 return event
257
258 def getChangeRestoredEvent(self):
259 event = {"type": "change-restored",
260 "change": {"project": self.project,
261 "branch": self.branch,
262 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
263 "number": str(self.number),
264 "subject": self.subject,
265 "owner": {"name": "User Name"},
266 "url": "https://hostname/3"},
267 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100268 "patchSet": self.patchsets[-1],
269 "reason": ""}
270 return event
271
272 def getChangeAbandonedEvent(self):
273 event = {"type": "change-abandoned",
274 "change": {"project": self.project,
275 "branch": self.branch,
276 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
277 "number": str(self.number),
278 "subject": self.subject,
279 "owner": {"name": "User Name"},
280 "url": "https://hostname/3"},
281 "abandoner": {"name": "User Name"},
282 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700283 "reason": ""}
284 return event
285
286 def getChangeCommentEvent(self, patchset):
287 event = {"type": "comment-added",
288 "change": {"project": self.project,
289 "branch": self.branch,
290 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
291 "number": str(self.number),
292 "subject": self.subject,
293 "owner": {"name": "User Name"},
294 "url": "https://hostname/3"},
295 "patchSet": self.patchsets[patchset - 1],
296 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700297 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700298 "description": "Code-Review",
299 "value": "0"}],
300 "comment": "This is a comment"}
301 return event
302
James E. Blairc2a5ed72017-02-20 14:12:01 -0500303 def getChangeMergedEvent(self):
304 event = {"submitter": {"name": "Jenkins",
305 "username": "jenkins"},
306 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
307 "patchSet": self.patchsets[-1],
308 "change": self.data,
309 "type": "change-merged",
310 "eventCreatedOn": 1487613810}
311 return event
312
James E. Blair8cce42e2016-10-18 08:18:36 -0700313 def getRefUpdatedEvent(self):
314 path = os.path.join(self.upstream_root, self.project)
315 repo = git.Repo(path)
316 oldrev = repo.heads[self.branch].commit.hexsha
317
318 event = {
319 "type": "ref-updated",
320 "submitter": {
321 "name": "User Name",
322 },
323 "refUpdate": {
324 "oldRev": oldrev,
325 "newRev": self.patchsets[-1]['revision'],
326 "refName": self.branch,
327 "project": self.project,
328 }
329 }
330 return event
331
Joshua Hesketh642824b2014-07-01 17:54:59 +1000332 def addApproval(self, category, value, username='reviewer_john',
333 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700334 if not granted_on:
335 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000336 approval = {
337 'description': self.categories[category][0],
338 'type': category,
339 'value': str(value),
340 'by': {
341 'username': username,
342 'email': username + '@example.com',
343 },
344 'grantedOn': int(granted_on)
345 }
Clark Boylanb640e052014-04-03 16:41:46 -0700346 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
347 if x['by']['username'] == username and x['type'] == category:
348 del self.patchsets[-1]['approvals'][i]
349 self.patchsets[-1]['approvals'].append(approval)
350 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000351 'author': {'email': 'author@example.com',
352 'name': 'Patchset Author',
353 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700354 'change': {'branch': self.branch,
355 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
356 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000357 'owner': {'email': 'owner@example.com',
358 'name': 'Change Owner',
359 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700360 'project': self.project,
361 'subject': self.subject,
362 'topic': 'master',
363 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000364 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700365 'patchSet': self.patchsets[-1],
366 'type': 'comment-added'}
367 self.data['submitRecords'] = self.getSubmitRecords()
368 return json.loads(json.dumps(event))
369
370 def getSubmitRecords(self):
371 status = {}
372 for cat in self.categories.keys():
373 status[cat] = 0
374
375 for a in self.patchsets[-1]['approvals']:
376 cur = status[a['type']]
377 cat_min, cat_max = self.categories[a['type']][1:]
378 new = int(a['value'])
379 if new == cat_min:
380 cur = new
381 elif abs(new) > abs(cur):
382 cur = new
383 status[a['type']] = cur
384
385 labels = []
386 ok = True
387 for typ, cat in self.categories.items():
388 cur = status[typ]
389 cat_min, cat_max = cat[1:]
390 if cur == cat_min:
391 value = 'REJECT'
392 ok = False
393 elif cur == cat_max:
394 value = 'OK'
395 else:
396 value = 'NEED'
397 ok = False
398 labels.append({'label': cat[0], 'status': value})
399 if ok:
400 return [{'status': 'OK'}]
401 return [{'status': 'NOT_READY',
402 'labels': labels}]
403
404 def setDependsOn(self, other, patchset):
405 self.depends_on_change = other
406 d = {'id': other.data['id'],
407 'number': other.data['number'],
408 'ref': other.patchsets[patchset - 1]['ref']
409 }
410 self.data['dependsOn'] = [d]
411
412 other.needed_by_changes.append(self)
413 needed = other.data.get('neededBy', [])
414 d = {'id': self.data['id'],
415 'number': self.data['number'],
416 'ref': self.patchsets[patchset - 1]['ref'],
417 'revision': self.patchsets[patchset - 1]['revision']
418 }
419 needed.append(d)
420 other.data['neededBy'] = needed
421
422 def query(self):
423 self.queried += 1
424 d = self.data.get('dependsOn')
425 if d:
426 d = d[0]
427 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
428 d['isCurrentPatchSet'] = True
429 else:
430 d['isCurrentPatchSet'] = False
431 return json.loads(json.dumps(self.data))
432
433 def setMerged(self):
434 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000435 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700436 return
437 if self.fail_merge:
438 return
439 self.data['status'] = 'MERGED'
440 self.open = False
441
442 path = os.path.join(self.upstream_root, self.project)
443 repo = git.Repo(path)
444 repo.heads[self.branch].commit = \
445 repo.commit(self.patchsets[-1]['revision'])
446
447 def setReported(self):
448 self.reported += 1
449
450
James E. Blaire511d2f2016-12-08 15:22:26 -0800451class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700452 """A Fake Gerrit connection for use in tests.
453
454 This subclasses
455 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
456 ability for tests to add changes to the fake Gerrit it represents.
457 """
458
Joshua Hesketh352264b2015-08-11 23:42:08 +1000459 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700460
James E. Blaire511d2f2016-12-08 15:22:26 -0800461 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700462 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800463 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000464 connection_config)
465
James E. Blair7fc8daa2016-08-08 15:37:15 -0700466 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700467 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
468 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000469 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700470 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200471 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700472
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700473 def addFakeChange(self, project, branch, subject, status='NEW',
474 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700475 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700476 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700477 c = FakeGerritChange(self, self.change_number, project, branch,
478 subject, upstream_root=self.upstream_root,
479 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700480 self.changes[self.change_number] = c
481 return c
482
Clark Boylanb640e052014-04-03 16:41:46 -0700483 def review(self, project, changeid, message, action):
484 number, ps = changeid.split(',')
485 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000486
487 # Add the approval back onto the change (ie simulate what gerrit would
488 # do).
489 # Usually when zuul leaves a review it'll create a feedback loop where
490 # zuul's review enters another gerrit event (which is then picked up by
491 # zuul). However, we can't mimic this behaviour (by adding this
492 # approval event into the queue) as it stops jobs from checking what
493 # happens before this event is triggered. If a job needs to see what
494 # happens they can add their own verified event into the queue.
495 # Nevertheless, we can update change with the new review in gerrit.
496
James E. Blair8b5408c2016-08-08 15:37:46 -0700497 for cat in action.keys():
498 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000499 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000500
Clark Boylanb640e052014-04-03 16:41:46 -0700501 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000502
Clark Boylanb640e052014-04-03 16:41:46 -0700503 if 'submit' in action:
504 change.setMerged()
505 if message:
506 change.setReported()
507
508 def query(self, number):
509 change = self.changes.get(int(number))
510 if change:
511 return change.query()
512 return {}
513
James E. Blairc494d542014-08-06 09:23:52 -0700514 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700515 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700516 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800517 if query.startswith('change:'):
518 # Query a specific changeid
519 changeid = query[len('change:'):]
520 l = [change.query() for change in self.changes.values()
521 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700522 elif query.startswith('message:'):
523 # Query the content of a commit message
524 msg = query[len('message:'):].strip()
525 l = [change.query() for change in self.changes.values()
526 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800527 else:
528 # Query all open changes
529 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700530 return l
James E. Blairc494d542014-08-06 09:23:52 -0700531
Joshua Hesketh352264b2015-08-11 23:42:08 +1000532 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700533 pass
534
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200535 def _uploadPack(self, project):
536 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
537 'multi_ack thin-pack side-band side-band-64k ofs-delta '
538 'shallow no-progress include-tag multi_ack_detailed no-done\n')
539 path = os.path.join(self.upstream_root, project.name)
540 repo = git.Repo(path)
541 for ref in repo.refs:
542 r = ref.object.hexsha + ' ' + ref.path + '\n'
543 ret += '%04x%s' % (len(r) + 4, r)
544 ret += '0000'
545 return ret
546
Joshua Hesketh352264b2015-08-11 23:42:08 +1000547 def getGitUrl(self, project):
548 return os.path.join(self.upstream_root, project.name)
549
Clark Boylanb640e052014-04-03 16:41:46 -0700550
Gregory Haynes4fc12542015-04-22 20:38:06 -0700551class GithubChangeReference(git.Reference):
552 _common_path_default = "refs/pull"
553 _points_to_commits_only = True
554
555
556class FakeGithubPullRequest(object):
557
558 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800559 subject, upstream_root, files=[], number_of_commits=1,
560 writers=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700561 """Creates a new PR with several commits.
562 Sends an event about opened PR."""
563 self.github = github
564 self.source = github
565 self.number = number
566 self.project = project
567 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100568 self.subject = subject
569 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700570 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100571 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700572 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100573 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100574 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800575 self.reviews = []
576 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700577 self.updated_at = None
578 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100579 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100580 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700581 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700582 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100583 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700584 self._updateTimeStamp()
585
Jan Hruban570d01c2016-03-10 21:51:32 +0100586 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700587 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100588 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700589 self._updateTimeStamp()
590
Jan Hruban570d01c2016-03-10 21:51:32 +0100591 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700592 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100593 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700594 self._updateTimeStamp()
595
596 def getPullRequestOpenedEvent(self):
597 return self._getPullRequestEvent('opened')
598
599 def getPullRequestSynchronizeEvent(self):
600 return self._getPullRequestEvent('synchronize')
601
602 def getPullRequestReopenedEvent(self):
603 return self._getPullRequestEvent('reopened')
604
605 def getPullRequestClosedEvent(self):
606 return self._getPullRequestEvent('closed')
607
608 def addComment(self, message):
609 self.comments.append(message)
610 self._updateTimeStamp()
611
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200612 def getCommentAddedEvent(self, text):
613 name = 'issue_comment'
614 data = {
615 'action': 'created',
616 'issue': {
617 'number': self.number
618 },
619 'comment': {
620 'body': text
621 },
622 'repository': {
623 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100624 },
625 'sender': {
626 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200627 }
628 }
629 return (name, data)
630
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800631 def getReviewAddedEvent(self, review):
632 name = 'pull_request_review'
633 data = {
634 'action': 'submitted',
635 'pull_request': {
636 'number': self.number,
637 'title': self.subject,
638 'updated_at': self.updated_at,
639 'base': {
640 'ref': self.branch,
641 'repo': {
642 'full_name': self.project
643 }
644 },
645 'head': {
646 'sha': self.head_sha
647 }
648 },
649 'review': {
650 'state': review
651 },
652 'repository': {
653 'full_name': self.project
654 },
655 'sender': {
656 'login': 'ghuser'
657 }
658 }
659 return (name, data)
660
Jan Hruban16ad31f2015-11-07 14:39:07 +0100661 def addLabel(self, name):
662 if name not in self.labels:
663 self.labels.append(name)
664 self._updateTimeStamp()
665 return self._getLabelEvent(name)
666
667 def removeLabel(self, name):
668 if name in self.labels:
669 self.labels.remove(name)
670 self._updateTimeStamp()
671 return self._getUnlabelEvent(name)
672
673 def _getLabelEvent(self, label):
674 name = 'pull_request'
675 data = {
676 'action': 'labeled',
677 'pull_request': {
678 'number': self.number,
679 'updated_at': self.updated_at,
680 'base': {
681 'ref': self.branch,
682 'repo': {
683 'full_name': self.project
684 }
685 },
686 'head': {
687 'sha': self.head_sha
688 }
689 },
690 'label': {
691 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100692 },
693 'sender': {
694 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100695 }
696 }
697 return (name, data)
698
699 def _getUnlabelEvent(self, label):
700 name = 'pull_request'
701 data = {
702 'action': 'unlabeled',
703 'pull_request': {
704 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100705 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100706 'updated_at': self.updated_at,
707 'base': {
708 'ref': self.branch,
709 'repo': {
710 'full_name': self.project
711 }
712 },
713 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800714 'sha': self.head_sha,
715 'repo': {
716 'full_name': self.project
717 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100718 }
719 },
720 'label': {
721 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100722 },
723 'sender': {
724 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100725 }
726 }
727 return (name, data)
728
Gregory Haynes4fc12542015-04-22 20:38:06 -0700729 def _getRepo(self):
730 repo_path = os.path.join(self.upstream_root, self.project)
731 return git.Repo(repo_path)
732
733 def _createPRRef(self):
734 repo = self._getRepo()
735 GithubChangeReference.create(
736 repo, self._getPRReference(), 'refs/tags/init')
737
Jan Hruban570d01c2016-03-10 21:51:32 +0100738 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700739 repo = self._getRepo()
740 ref = repo.references[self._getPRReference()]
741 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100742 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700743 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100744 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700745 repo.head.reference = ref
746 zuul.merger.merger.reset_repo_to_head(repo)
747 repo.git.clean('-x', '-f', '-d')
748
Jan Hruban570d01c2016-03-10 21:51:32 +0100749 if files:
750 fn = files[0]
751 self.files = files
752 else:
753 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
754 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100755 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700756 fn = os.path.join(repo.working_dir, fn)
757 f = open(fn, 'w')
758 with open(fn, 'w') as f:
759 f.write("test %s %s\n" %
760 (self.branch, self.number))
761 repo.index.add([fn])
762
763 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800764 # Create an empty set of statuses for the given sha,
765 # each sha on a PR may have a status set on it
766 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700767 repo.head.reference = 'master'
768 zuul.merger.merger.reset_repo_to_head(repo)
769 repo.git.clean('-x', '-f', '-d')
770 repo.heads['master'].checkout()
771
772 def _updateTimeStamp(self):
773 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
774
775 def getPRHeadSha(self):
776 repo = self._getRepo()
777 return repo.references[self._getPRReference()].commit.hexsha
778
Jesse Keatingae4cd272017-01-30 17:10:44 -0800779 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800780 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
781 # convert the timestamp to a str format that would be returned
782 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800783
Adam Gandelmand81dd762017-02-09 15:15:49 -0800784 if granted_on:
785 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
786 submitted_at = time.strftime(
787 gh_time_format, granted_on.timetuple())
788 else:
789 # github timestamps only down to the second, so we need to make
790 # sure reviews that tests add appear to be added over a period of
791 # time in the past and not all at once.
792 if not self.reviews:
793 # the first review happens 10 mins ago
794 offset = 600
795 else:
796 # subsequent reviews happen 1 minute closer to now
797 offset = 600 - (len(self.reviews) * 60)
798
799 granted_on = datetime.datetime.utcfromtimestamp(
800 time.time() - offset)
801 submitted_at = time.strftime(
802 gh_time_format, granted_on.timetuple())
803
Jesse Keatingae4cd272017-01-30 17:10:44 -0800804 self.reviews.append({
805 'state': state,
806 'user': {
807 'login': user,
808 'email': user + "@derp.com",
809 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800810 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800811 })
812
Gregory Haynes4fc12542015-04-22 20:38:06 -0700813 def _getPRReference(self):
814 return '%s/head' % self.number
815
816 def _getPullRequestEvent(self, action):
817 name = 'pull_request'
818 data = {
819 'action': action,
820 'number': self.number,
821 'pull_request': {
822 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100823 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700824 'updated_at': self.updated_at,
825 'base': {
826 'ref': self.branch,
827 'repo': {
828 'full_name': self.project
829 }
830 },
831 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800832 'sha': self.head_sha,
833 'repo': {
834 'full_name': self.project
835 }
Gregory Haynes4fc12542015-04-22 20:38:06 -0700836 }
Jan Hruban3b415922016-02-03 13:10:22 +0100837 },
838 'sender': {
839 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700840 }
841 }
842 return (name, data)
843
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800844 def getCommitStatusEvent(self, context, state='success', user='zuul'):
845 name = 'status'
846 data = {
847 'state': state,
848 'sha': self.head_sha,
849 'description': 'Test results for %s: %s' % (self.head_sha, state),
850 'target_url': 'http://zuul/%s' % self.head_sha,
851 'branches': [],
852 'context': context,
853 'sender': {
854 'login': user
855 }
856 }
857 return (name, data)
858
Gregory Haynes4fc12542015-04-22 20:38:06 -0700859
860class FakeGithubConnection(githubconnection.GithubConnection):
861 log = logging.getLogger("zuul.test.FakeGithubConnection")
862
863 def __init__(self, driver, connection_name, connection_config,
864 upstream_root=None):
865 super(FakeGithubConnection, self).__init__(driver, connection_name,
866 connection_config)
867 self.connection_name = connection_name
868 self.pr_number = 0
869 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700870 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700871 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100872 self.merge_failure = False
873 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700874
Jan Hruban570d01c2016-03-10 21:51:32 +0100875 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700876 self.pr_number += 1
877 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100878 self, self.pr_number, project, branch, subject, self.upstream_root,
879 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700880 self.pull_requests.append(pull_request)
881 return pull_request
882
Jesse Keating71a47ff2017-06-06 11:36:43 -0700883 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
884 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700885 if not old_rev:
886 old_rev = '00000000000000000000000000000000'
887 if not new_rev:
888 new_rev = random_sha1()
889 name = 'push'
890 data = {
891 'ref': ref,
892 'before': old_rev,
893 'after': new_rev,
894 'repository': {
895 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700896 },
897 'commits': [
898 {
899 'added': added_files,
900 'removed': removed_files,
901 'modified': modified_files
902 }
903 ]
Wayne1a78c612015-06-11 17:14:13 -0700904 }
905 return (name, data)
906
Gregory Haynes4fc12542015-04-22 20:38:06 -0700907 def emitEvent(self, event):
908 """Emulates sending the GitHub webhook event to the connection."""
909 port = self.webapp.server.socket.getsockname()[1]
910 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700911 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700912 headers = {'X-Github-Event': name}
913 req = urllib.request.Request(
914 'http://localhost:%s/connection/%s/payload'
915 % (port, self.connection_name),
916 data=payload, headers=headers)
917 urllib.request.urlopen(req)
918
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200919 def getPull(self, project, number):
920 pr = self.pull_requests[number - 1]
921 data = {
922 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100923 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200924 'updated_at': pr.updated_at,
925 'base': {
926 'repo': {
927 'full_name': pr.project
928 },
929 'ref': pr.branch,
930 },
Jan Hruban37615e52015-11-19 14:30:49 +0100931 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700932 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200933 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800934 'sha': pr.head_sha,
935 'repo': {
936 'full_name': pr.project
937 }
Jesse Keating61040e72017-06-08 15:08:27 -0700938 },
939 'files': pr.files
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200940 }
941 return data
942
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800943 def getPullBySha(self, sha):
944 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
945 if len(prs) > 1:
946 raise Exception('Multiple pulls found with head sha: %s' % sha)
947 pr = prs[0]
948 return self.getPull(pr.project, pr.number)
949
Jesse Keatingae4cd272017-01-30 17:10:44 -0800950 def _getPullReviews(self, owner, project, number):
951 pr = self.pull_requests[number - 1]
952 return pr.reviews
953
Jan Hruban3b415922016-02-03 13:10:22 +0100954 def getUser(self, login):
955 data = {
956 'username': login,
957 'name': 'Github User',
958 'email': 'github.user@example.com'
959 }
960 return data
961
Jesse Keatingae4cd272017-01-30 17:10:44 -0800962 def getRepoPermission(self, project, login):
963 owner, proj = project.split('/')
964 for pr in self.pull_requests:
965 pr_owner, pr_project = pr.project.split('/')
966 if (pr_owner == owner and proj == pr_project):
967 if login in pr.writers:
968 return 'write'
969 else:
970 return 'read'
971
Gregory Haynes4fc12542015-04-22 20:38:06 -0700972 def getGitUrl(self, project):
973 return os.path.join(self.upstream_root, str(project))
974
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200975 def real_getGitUrl(self, project):
976 return super(FakeGithubConnection, self).getGitUrl(project)
977
Gregory Haynes4fc12542015-04-22 20:38:06 -0700978 def getProjectBranches(self, project):
979 """Masks getProjectBranches since we don't have a real github"""
980
981 # just returns master for now
982 return ['master']
983
Jan Hrubane252a732017-01-03 15:03:09 +0100984 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700985 pull_request = self.pull_requests[pr_number - 1]
986 pull_request.addComment(message)
987
Jan Hruban3b415922016-02-03 13:10:22 +0100988 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100989 pull_request = self.pull_requests[pr_number - 1]
990 if self.merge_failure:
991 raise Exception('Pull request was not merged')
992 if self.merge_not_allowed_count > 0:
993 self.merge_not_allowed_count -= 1
994 raise MergeFailure('Merge was not successful due to mergeability'
995 ' conflict')
996 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +0100997 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +0100998
Jesse Keatingd96e5882017-01-19 13:55:50 -0800999 def getCommitStatuses(self, project, sha):
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001000 return self.statuses.get(project, {}).get(sha, [])
Jesse Keatingd96e5882017-01-19 13:55:50 -08001001
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001002 def setCommitStatus(self, project, sha, state, url='', description='',
1003 context='default', user='zuul'):
1004 # always insert a status to the front of the list, to represent
1005 # the last status provided for a commit.
1006 # Since we're bypassing github API, which would require a user, we
1007 # default the user as 'zuul' here.
1008 self.statuses.setdefault(project, {}).setdefault(sha, [])
1009 self.statuses[project][sha].insert(0, {
1010 'state': state,
1011 'url': url,
1012 'description': description,
1013 'context': context,
1014 'creator': {
1015 'login': user
1016 }
1017 })
Jan Hrubane252a732017-01-03 15:03:09 +01001018
Jan Hruban16ad31f2015-11-07 14:39:07 +01001019 def labelPull(self, project, pr_number, label):
1020 pull_request = self.pull_requests[pr_number - 1]
1021 pull_request.addLabel(label)
1022
1023 def unlabelPull(self, project, pr_number, label):
1024 pull_request = self.pull_requests[pr_number - 1]
1025 pull_request.removeLabel(label)
1026
Gregory Haynes4fc12542015-04-22 20:38:06 -07001027
Clark Boylanb640e052014-04-03 16:41:46 -07001028class BuildHistory(object):
1029 def __init__(self, **kw):
1030 self.__dict__.update(kw)
1031
1032 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -07001033 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1034 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001035
1036
Clark Boylanb640e052014-04-03 16:41:46 -07001037class FakeStatsd(threading.Thread):
1038 def __init__(self):
1039 threading.Thread.__init__(self)
1040 self.daemon = True
1041 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1042 self.sock.bind(('', 0))
1043 self.port = self.sock.getsockname()[1]
1044 self.wake_read, self.wake_write = os.pipe()
1045 self.stats = []
1046
1047 def run(self):
1048 while True:
1049 poll = select.poll()
1050 poll.register(self.sock, select.POLLIN)
1051 poll.register(self.wake_read, select.POLLIN)
1052 ret = poll.poll()
1053 for (fd, event) in ret:
1054 if fd == self.sock.fileno():
1055 data = self.sock.recvfrom(1024)
1056 if not data:
1057 return
1058 self.stats.append(data[0])
1059 if fd == self.wake_read:
1060 return
1061
1062 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001063 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001064
1065
James E. Blaire1767bc2016-08-02 10:00:27 -07001066class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001067 log = logging.getLogger("zuul.test")
1068
Paul Belanger174a8272017-03-14 13:20:10 -04001069 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001070 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001071 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001072 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001073 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001074 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001075 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001076 # TODOv3(jeblair): self.node is really "the label of the node
1077 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001078 # keep using it like this, or we may end up exposing more of
1079 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001080 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001081 self.node = None
1082 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001083 self.node = self.parameters['nodes'][0]['label']
Clark Boylanb640e052014-04-03 16:41:46 -07001084 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001085 self.pipeline = self.parameters['ZUUL_PIPELINE']
1086 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001087 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001088 self.wait_condition = threading.Condition()
1089 self.waiting = False
1090 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001091 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001092 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001093 self.changes = None
1094 if 'ZUUL_CHANGE_IDS' in self.parameters:
1095 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001096
James E. Blair3158e282016-08-19 09:34:11 -07001097 def __repr__(self):
1098 waiting = ''
1099 if self.waiting:
1100 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001101 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1102 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001103
Clark Boylanb640e052014-04-03 16:41:46 -07001104 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001105 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001106 self.wait_condition.acquire()
1107 self.wait_condition.notify()
1108 self.waiting = False
1109 self.log.debug("Build %s released" % self.unique)
1110 self.wait_condition.release()
1111
1112 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001113 """Return whether this build is being held.
1114
1115 :returns: Whether the build is being held.
1116 :rtype: bool
1117 """
1118
Clark Boylanb640e052014-04-03 16:41:46 -07001119 self.wait_condition.acquire()
1120 if self.waiting:
1121 ret = True
1122 else:
1123 ret = False
1124 self.wait_condition.release()
1125 return ret
1126
1127 def _wait(self):
1128 self.wait_condition.acquire()
1129 self.waiting = True
1130 self.log.debug("Build %s waiting" % self.unique)
1131 self.wait_condition.wait()
1132 self.wait_condition.release()
1133
1134 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001135 self.log.debug('Running build %s' % self.unique)
1136
Paul Belanger174a8272017-03-14 13:20:10 -04001137 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001138 self.log.debug('Holding build %s' % self.unique)
1139 self._wait()
1140 self.log.debug("Build %s continuing" % self.unique)
1141
James E. Blair412fba82017-01-26 15:00:50 -08001142 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001143 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001144 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001145 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001146 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001147 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001148 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001149
James E. Blaire1767bc2016-08-02 10:00:27 -07001150 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001151
James E. Blaira5dba232016-08-08 15:53:24 -07001152 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001153 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001154 for change in changes:
1155 if self.hasChanges(change):
1156 return True
1157 return False
1158
James E. Blaire7b99a02016-08-05 14:27:34 -07001159 def hasChanges(self, *changes):
1160 """Return whether this build has certain changes in its git repos.
1161
1162 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001163 are expected to be present (in order) in the git repository of
1164 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001165
1166 :returns: Whether the build has the indicated changes.
1167 :rtype: bool
1168
1169 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001170 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001171 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001172 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001173 try:
1174 repo = git.Repo(path)
1175 except NoSuchPathError as e:
1176 self.log.debug('%s' % e)
1177 return False
1178 ref = self.parameters['ZUUL_REF']
1179 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1180 commit_message = '%s-1' % change.subject
1181 self.log.debug("Checking if build %s has changes; commit_message "
1182 "%s; repo_messages %s" % (self, commit_message,
1183 repo_messages))
1184 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001185 self.log.debug(" messages do not match")
1186 return False
1187 self.log.debug(" OK")
1188 return True
1189
James E. Blaird8af5422017-05-24 13:59:40 -07001190 def getWorkspaceRepos(self, projects):
1191 """Return workspace git repo objects for the listed projects
1192
1193 :arg list projects: A list of strings, each the canonical name
1194 of a project.
1195
1196 :returns: A dictionary of {name: repo} for every listed
1197 project.
1198 :rtype: dict
1199
1200 """
1201
1202 repos = {}
1203 for project in projects:
1204 path = os.path.join(self.jobdir.src_root, project)
1205 repo = git.Repo(path)
1206 repos[project] = repo
1207 return repos
1208
Clark Boylanb640e052014-04-03 16:41:46 -07001209
Paul Belanger174a8272017-03-14 13:20:10 -04001210class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1211 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001212
Paul Belanger174a8272017-03-14 13:20:10 -04001213 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001214 they will report that they have started but then pause until
1215 released before reporting completion. This attribute may be
1216 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001217 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001218 be explicitly released.
1219
1220 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001221 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001222 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001223 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001224 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001225 self.hold_jobs_in_build = False
1226 self.lock = threading.Lock()
1227 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001228 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001229 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001230 self.job_builds = {}
Monty Taylorde8242c2017-02-23 20:29:53 -06001231 self.hostname = 'zl.example.com'
James E. Blairf5dbd002015-12-23 15:26:17 -08001232
James E. Blaira5dba232016-08-08 15:53:24 -07001233 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001234 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001235
1236 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001237 :arg Change change: The :py:class:`~tests.base.FakeChange`
1238 instance which should cause the job to fail. This job
1239 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001240
1241 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001242 l = self.fail_tests.get(name, [])
1243 l.append(change)
1244 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001245
James E. Blair962220f2016-08-03 11:22:38 -07001246 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001247 """Release a held build.
1248
1249 :arg str regex: A regular expression which, if supplied, will
1250 cause only builds with matching names to be released. If
1251 not supplied, all builds will be released.
1252
1253 """
James E. Blair962220f2016-08-03 11:22:38 -07001254 builds = self.running_builds[:]
1255 self.log.debug("Releasing build %s (%s)" % (regex,
1256 len(self.running_builds)))
1257 for build in builds:
1258 if not regex or re.match(regex, build.name):
1259 self.log.debug("Releasing build %s" %
1260 (build.parameters['ZUUL_UUID']))
1261 build.release()
1262 else:
1263 self.log.debug("Not releasing build %s" %
1264 (build.parameters['ZUUL_UUID']))
1265 self.log.debug("Done releasing builds %s (%s)" %
1266 (regex, len(self.running_builds)))
1267
Paul Belanger174a8272017-03-14 13:20:10 -04001268 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001269 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001270 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001271 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001272 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001273 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001274 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001275 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001276 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1277 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001278
1279 def stopJob(self, job):
1280 self.log.debug("handle stop")
1281 parameters = json.loads(job.arguments)
1282 uuid = parameters['uuid']
1283 for build in self.running_builds:
1284 if build.unique == uuid:
1285 build.aborted = True
1286 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001287 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001288
James E. Blaira002b032017-04-18 10:35:48 -07001289 def stop(self):
1290 for build in self.running_builds:
1291 build.release()
1292 super(RecordingExecutorServer, self).stop()
1293
Joshua Hesketh50c21782016-10-13 21:34:14 +11001294
Paul Belanger174a8272017-03-14 13:20:10 -04001295class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001296 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001297 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001298 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001299 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001300 if not commit: # merge conflict
1301 self.recordResult('MERGER_FAILURE')
1302 return commit
1303
1304 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001305 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001306 self.executor_server.lock.acquire()
1307 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001308 BuildHistory(name=build.name, result=result, changes=build.changes,
1309 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001310 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001311 pipeline=build.parameters['ZUUL_PIPELINE'])
1312 )
Paul Belanger174a8272017-03-14 13:20:10 -04001313 self.executor_server.running_builds.remove(build)
1314 del self.executor_server.job_builds[self.job.unique]
1315 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001316
1317 def runPlaybooks(self, args):
1318 build = self.executor_server.job_builds[self.job.unique]
1319 build.jobdir = self.jobdir
1320
1321 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1322 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001323 return result
1324
Monty Taylore6562aa2017-02-20 07:37:39 -05001325 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001326 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001327
Paul Belanger174a8272017-03-14 13:20:10 -04001328 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001329 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001330 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001331 else:
1332 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001333 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001334
James E. Blairad8dca02017-02-21 11:48:32 -05001335 def getHostList(self, args):
1336 self.log.debug("hostlist")
1337 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001338 for host in hosts:
1339 host['host_vars']['ansible_connection'] = 'local'
1340
1341 hosts.append(dict(
1342 name='localhost',
1343 host_vars=dict(ansible_connection='local'),
1344 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001345 return hosts
1346
James E. Blairf5dbd002015-12-23 15:26:17 -08001347
Clark Boylanb640e052014-04-03 16:41:46 -07001348class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001349 """A Gearman server for use in tests.
1350
1351 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1352 added to the queue but will not be distributed to workers
1353 until released. This attribute may be changed at any time and
1354 will take effect for subsequently enqueued jobs, but
1355 previously held jobs will still need to be explicitly
1356 released.
1357
1358 """
1359
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001360 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001361 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001362 if use_ssl:
1363 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1364 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1365 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1366 else:
1367 ssl_ca = None
1368 ssl_cert = None
1369 ssl_key = None
1370
1371 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1372 ssl_cert=ssl_cert,
1373 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001374
1375 def getJobForConnection(self, connection, peek=False):
1376 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1377 for job in queue:
1378 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001379 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001380 job.waiting = self.hold_jobs_in_queue
1381 else:
1382 job.waiting = False
1383 if job.waiting:
1384 continue
1385 if job.name in connection.functions:
1386 if not peek:
1387 queue.remove(job)
1388 connection.related_jobs[job.handle] = job
1389 job.worker_connection = connection
1390 job.running = True
1391 return job
1392 return None
1393
1394 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001395 """Release a held job.
1396
1397 :arg str regex: A regular expression which, if supplied, will
1398 cause only jobs with matching names to be released. If
1399 not supplied, all jobs will be released.
1400 """
Clark Boylanb640e052014-04-03 16:41:46 -07001401 released = False
1402 qlen = (len(self.high_queue) + len(self.normal_queue) +
1403 len(self.low_queue))
1404 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1405 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001406 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001407 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001408 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001409 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001410 self.log.debug("releasing queued job %s" %
1411 job.unique)
1412 job.waiting = False
1413 released = True
1414 else:
1415 self.log.debug("not releasing queued job %s" %
1416 job.unique)
1417 if released:
1418 self.wakeConnections()
1419 qlen = (len(self.high_queue) + len(self.normal_queue) +
1420 len(self.low_queue))
1421 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1422
1423
1424class FakeSMTP(object):
1425 log = logging.getLogger('zuul.FakeSMTP')
1426
1427 def __init__(self, messages, server, port):
1428 self.server = server
1429 self.port = port
1430 self.messages = messages
1431
1432 def sendmail(self, from_email, to_email, msg):
1433 self.log.info("Sending email from %s, to %s, with msg %s" % (
1434 from_email, to_email, msg))
1435
1436 headers = msg.split('\n\n', 1)[0]
1437 body = msg.split('\n\n', 1)[1]
1438
1439 self.messages.append(dict(
1440 from_email=from_email,
1441 to_email=to_email,
1442 msg=msg,
1443 headers=headers,
1444 body=body,
1445 ))
1446
1447 return True
1448
1449 def quit(self):
1450 return True
1451
1452
James E. Blairdce6cea2016-12-20 16:45:32 -08001453class FakeNodepool(object):
1454 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001455 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001456
1457 log = logging.getLogger("zuul.test.FakeNodepool")
1458
1459 def __init__(self, host, port, chroot):
1460 self.client = kazoo.client.KazooClient(
1461 hosts='%s:%s%s' % (host, port, chroot))
1462 self.client.start()
1463 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001464 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001465 self.thread = threading.Thread(target=self.run)
1466 self.thread.daemon = True
1467 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001468 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001469
1470 def stop(self):
1471 self._running = False
1472 self.thread.join()
1473 self.client.stop()
1474 self.client.close()
1475
1476 def run(self):
1477 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001478 try:
1479 self._run()
1480 except Exception:
1481 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001482 time.sleep(0.1)
1483
1484 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001485 if self.paused:
1486 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001487 for req in self.getNodeRequests():
1488 self.fulfillRequest(req)
1489
1490 def getNodeRequests(self):
1491 try:
1492 reqids = self.client.get_children(self.REQUEST_ROOT)
1493 except kazoo.exceptions.NoNodeError:
1494 return []
1495 reqs = []
1496 for oid in sorted(reqids):
1497 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001498 try:
1499 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001500 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001501 data['_oid'] = oid
1502 reqs.append(data)
1503 except kazoo.exceptions.NoNodeError:
1504 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001505 return reqs
1506
James E. Blaire18d4602017-01-05 11:17:28 -08001507 def getNodes(self):
1508 try:
1509 nodeids = self.client.get_children(self.NODE_ROOT)
1510 except kazoo.exceptions.NoNodeError:
1511 return []
1512 nodes = []
1513 for oid in sorted(nodeids):
1514 path = self.NODE_ROOT + '/' + oid
1515 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001516 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001517 data['_oid'] = oid
1518 try:
1519 lockfiles = self.client.get_children(path + '/lock')
1520 except kazoo.exceptions.NoNodeError:
1521 lockfiles = []
1522 if lockfiles:
1523 data['_lock'] = True
1524 else:
1525 data['_lock'] = False
1526 nodes.append(data)
1527 return nodes
1528
James E. Blaira38c28e2017-01-04 10:33:20 -08001529 def makeNode(self, request_id, node_type):
1530 now = time.time()
1531 path = '/nodepool/nodes/'
1532 data = dict(type=node_type,
1533 provider='test-provider',
1534 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001535 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001536 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001537 public_ipv4='127.0.0.1',
1538 private_ipv4=None,
1539 public_ipv6=None,
1540 allocated_to=request_id,
1541 state='ready',
1542 state_time=now,
1543 created_time=now,
1544 updated_time=now,
1545 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001546 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001547 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001548 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001549 path = self.client.create(path, data,
1550 makepath=True,
1551 sequence=True)
1552 nodeid = path.split("/")[-1]
1553 return nodeid
1554
James E. Blair6ab79e02017-01-06 10:10:17 -08001555 def addFailRequest(self, request):
1556 self.fail_requests.add(request['_oid'])
1557
James E. Blairdce6cea2016-12-20 16:45:32 -08001558 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001559 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001560 return
1561 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001562 oid = request['_oid']
1563 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001564
James E. Blair6ab79e02017-01-06 10:10:17 -08001565 if oid in self.fail_requests:
1566 request['state'] = 'failed'
1567 else:
1568 request['state'] = 'fulfilled'
1569 nodes = []
1570 for node in request['node_types']:
1571 nodeid = self.makeNode(oid, node)
1572 nodes.append(nodeid)
1573 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001574
James E. Blaira38c28e2017-01-04 10:33:20 -08001575 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001576 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001577 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001578 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001579 try:
1580 self.client.set(path, data)
1581 except kazoo.exceptions.NoNodeError:
1582 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001583
1584
James E. Blair498059b2016-12-20 13:50:13 -08001585class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001586 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001587 super(ChrootedKazooFixture, self).__init__()
1588
1589 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1590 if ':' in zk_host:
1591 host, port = zk_host.split(':')
1592 else:
1593 host = zk_host
1594 port = None
1595
1596 self.zookeeper_host = host
1597
1598 if not port:
1599 self.zookeeper_port = 2181
1600 else:
1601 self.zookeeper_port = int(port)
1602
Clark Boylan621ec9a2017-04-07 17:41:33 -07001603 self.test_id = test_id
1604
James E. Blair498059b2016-12-20 13:50:13 -08001605 def _setUp(self):
1606 # Make sure the test chroot paths do not conflict
1607 random_bits = ''.join(random.choice(string.ascii_lowercase +
1608 string.ascii_uppercase)
1609 for x in range(8))
1610
Clark Boylan621ec9a2017-04-07 17:41:33 -07001611 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001612 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1613
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001614 self.addCleanup(self._cleanup)
1615
James E. Blair498059b2016-12-20 13:50:13 -08001616 # Ensure the chroot path exists and clean up any pre-existing znodes.
1617 _tmp_client = kazoo.client.KazooClient(
1618 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1619 _tmp_client.start()
1620
1621 if _tmp_client.exists(self.zookeeper_chroot):
1622 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1623
1624 _tmp_client.ensure_path(self.zookeeper_chroot)
1625 _tmp_client.stop()
1626 _tmp_client.close()
1627
James E. Blair498059b2016-12-20 13:50:13 -08001628 def _cleanup(self):
1629 '''Remove the chroot path.'''
1630 # Need a non-chroot'ed client to remove the chroot path
1631 _tmp_client = kazoo.client.KazooClient(
1632 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1633 _tmp_client.start()
1634 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1635 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001636 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001637
1638
Joshua Heskethd78b4482015-09-14 16:56:34 -06001639class MySQLSchemaFixture(fixtures.Fixture):
1640 def setUp(self):
1641 super(MySQLSchemaFixture, self).setUp()
1642
1643 random_bits = ''.join(random.choice(string.ascii_lowercase +
1644 string.ascii_uppercase)
1645 for x in range(8))
1646 self.name = '%s_%s' % (random_bits, os.getpid())
1647 self.passwd = uuid.uuid4().hex
1648 db = pymysql.connect(host="localhost",
1649 user="openstack_citest",
1650 passwd="openstack_citest",
1651 db="openstack_citest")
1652 cur = db.cursor()
1653 cur.execute("create database %s" % self.name)
1654 cur.execute(
1655 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1656 (self.name, self.name, self.passwd))
1657 cur.execute("flush privileges")
1658
1659 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1660 self.passwd,
1661 self.name)
1662 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1663 self.addCleanup(self.cleanup)
1664
1665 def cleanup(self):
1666 db = pymysql.connect(host="localhost",
1667 user="openstack_citest",
1668 passwd="openstack_citest",
1669 db="openstack_citest")
1670 cur = db.cursor()
1671 cur.execute("drop database %s" % self.name)
1672 cur.execute("drop user '%s'@'localhost'" % self.name)
1673 cur.execute("flush privileges")
1674
1675
Maru Newby3fe5f852015-01-13 04:22:14 +00001676class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001677 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001678 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001679
James E. Blair1c236df2017-02-01 14:07:24 -08001680 def attachLogs(self, *args):
1681 def reader():
1682 self._log_stream.seek(0)
1683 while True:
1684 x = self._log_stream.read(4096)
1685 if not x:
1686 break
1687 yield x.encode('utf8')
1688 content = testtools.content.content_from_reader(
1689 reader,
1690 testtools.content_type.UTF8_TEXT,
1691 False)
1692 self.addDetail('logging', content)
1693
Clark Boylanb640e052014-04-03 16:41:46 -07001694 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001695 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001696 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1697 try:
1698 test_timeout = int(test_timeout)
1699 except ValueError:
1700 # If timeout value is invalid do not set a timeout.
1701 test_timeout = 0
1702 if test_timeout > 0:
1703 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1704
1705 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1706 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1707 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1708 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1709 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1710 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1711 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1712 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1713 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1714 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001715 self._log_stream = StringIO()
1716 self.addOnException(self.attachLogs)
1717 else:
1718 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001719
James E. Blair73b41772017-05-22 13:22:55 -07001720 # NOTE(jeblair): this is temporary extra debugging to try to
1721 # track down a possible leak.
1722 orig_git_repo_init = git.Repo.__init__
1723
1724 def git_repo_init(myself, *args, **kw):
1725 orig_git_repo_init(myself, *args, **kw)
1726 self.log.debug("Created git repo 0x%x %s" %
1727 (id(myself), repr(myself)))
1728
1729 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1730 git_repo_init))
1731
James E. Blair1c236df2017-02-01 14:07:24 -08001732 handler = logging.StreamHandler(self._log_stream)
1733 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1734 '%(levelname)-8s %(message)s')
1735 handler.setFormatter(formatter)
1736
1737 logger = logging.getLogger()
1738 logger.setLevel(logging.DEBUG)
1739 logger.addHandler(handler)
1740
Clark Boylan3410d532017-04-25 12:35:29 -07001741 # Make sure we don't carry old handlers around in process state
1742 # which slows down test runs
1743 self.addCleanup(logger.removeHandler, handler)
1744 self.addCleanup(handler.close)
1745 self.addCleanup(handler.flush)
1746
James E. Blair1c236df2017-02-01 14:07:24 -08001747 # NOTE(notmorgan): Extract logging overrides for specific
1748 # libraries from the OS_LOG_DEFAULTS env and create loggers
1749 # for each. This is used to limit the output during test runs
1750 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001751 log_defaults_from_env = os.environ.get(
1752 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001753 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001754
James E. Blairdce6cea2016-12-20 16:45:32 -08001755 if log_defaults_from_env:
1756 for default in log_defaults_from_env.split(','):
1757 try:
1758 name, level_str = default.split('=', 1)
1759 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001760 logger = logging.getLogger(name)
1761 logger.setLevel(level)
1762 logger.addHandler(handler)
1763 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001764 except ValueError:
1765 # NOTE(notmorgan): Invalid format of the log default,
1766 # skip and don't try and apply a logger for the
1767 # specified module
1768 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001769
Maru Newby3fe5f852015-01-13 04:22:14 +00001770
1771class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001772 """A test case with a functioning Zuul.
1773
1774 The following class variables are used during test setup and can
1775 be overidden by subclasses but are effectively read-only once a
1776 test method starts running:
1777
1778 :cvar str config_file: This points to the main zuul config file
1779 within the fixtures directory. Subclasses may override this
1780 to obtain a different behavior.
1781
1782 :cvar str tenant_config_file: This is the tenant config file
1783 (which specifies from what git repos the configuration should
1784 be loaded). It defaults to the value specified in
1785 `config_file` but can be overidden by subclasses to obtain a
1786 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001787 configuration. See also the :py:func:`simple_layout`
1788 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001789
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001790 :cvar bool create_project_keys: Indicates whether Zuul should
1791 auto-generate keys for each project, or whether the test
1792 infrastructure should insert dummy keys to save time during
1793 startup. Defaults to False.
1794
James E. Blaire7b99a02016-08-05 14:27:34 -07001795 The following are instance variables that are useful within test
1796 methods:
1797
1798 :ivar FakeGerritConnection fake_<connection>:
1799 A :py:class:`~tests.base.FakeGerritConnection` will be
1800 instantiated for each connection present in the config file
1801 and stored here. For instance, `fake_gerrit` will hold the
1802 FakeGerritConnection object for a connection named `gerrit`.
1803
1804 :ivar FakeGearmanServer gearman_server: An instance of
1805 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1806 server that all of the Zuul components in this test use to
1807 communicate with each other.
1808
Paul Belanger174a8272017-03-14 13:20:10 -04001809 :ivar RecordingExecutorServer executor_server: An instance of
1810 :py:class:`~tests.base.RecordingExecutorServer` which is the
1811 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001812
1813 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1814 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001815 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001816 list upon completion.
1817
1818 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1819 objects representing completed builds. They are appended to
1820 the list in the order they complete.
1821
1822 """
1823
James E. Blair83005782015-12-11 14:46:03 -08001824 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001825 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001826 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001827 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001828
1829 def _startMerger(self):
1830 self.merge_server = zuul.merger.server.MergeServer(self.config,
1831 self.connections)
1832 self.merge_server.start()
1833
Maru Newby3fe5f852015-01-13 04:22:14 +00001834 def setUp(self):
1835 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001836
1837 self.setupZK()
1838
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001839 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001840 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001841 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1842 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001843 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001844 tmp_root = tempfile.mkdtemp(
1845 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001846 self.test_root = os.path.join(tmp_root, "zuul-test")
1847 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001848 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001849 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001850 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001851
1852 if os.path.exists(self.test_root):
1853 shutil.rmtree(self.test_root)
1854 os.makedirs(self.test_root)
1855 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001856 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001857
1858 # Make per test copy of Configuration.
1859 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001860 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1861 if not os.path.exists(self.private_key_file):
1862 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1863 shutil.copy(src_private_key_file, self.private_key_file)
1864 shutil.copy('{}.pub'.format(src_private_key_file),
1865 '{}.pub'.format(self.private_key_file))
1866 os.chmod(self.private_key_file, 0o0600)
James E. Blair59fdbac2015-12-07 17:08:06 -08001867 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001868 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001869 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001870 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001871 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001872 self.config.set('zuul', 'state_dir', self.state_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001873 self.config.set('executor', 'private_key_file', self.private_key_file)
Clark Boylanb640e052014-04-03 16:41:46 -07001874
Clark Boylanb640e052014-04-03 16:41:46 -07001875 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001876 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1877 # see: https://github.com/jsocol/pystatsd/issues/61
1878 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001879 os.environ['STATSD_PORT'] = str(self.statsd.port)
1880 self.statsd.start()
1881 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001882 reload_module(statsd)
1883 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001884
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001885 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001886
1887 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001888 self.log.info("Gearman server on port %s" %
1889 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001890 if self.use_ssl:
1891 self.log.info('SSL enabled for gearman')
1892 self.config.set(
1893 'gearman', 'ssl_ca',
1894 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1895 self.config.set(
1896 'gearman', 'ssl_cert',
1897 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1898 self.config.set(
1899 'gearman', 'ssl_key',
1900 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001901
James E. Blaire511d2f2016-12-08 15:22:26 -08001902 gerritsource.GerritSource.replication_timeout = 1.5
1903 gerritsource.GerritSource.replication_retry_interval = 0.5
1904 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001905
Joshua Hesketh352264b2015-08-11 23:42:08 +10001906 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001907
Jan Hruban7083edd2015-08-21 14:00:54 +02001908 self.webapp = zuul.webapp.WebApp(
1909 self.sched, port=0, listen_address='127.0.0.1')
1910
Jan Hruban6b71aff2015-10-22 16:58:08 +02001911 self.event_queues = [
1912 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001913 self.sched.trigger_event_queue,
1914 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001915 ]
1916
James E. Blairfef78942016-03-11 16:28:56 -08001917 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001918 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001919
Paul Belanger174a8272017-03-14 13:20:10 -04001920 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001921 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001922 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001923 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001924 _test_root=self.test_root,
1925 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001926 self.executor_server.start()
1927 self.history = self.executor_server.build_history
1928 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001929
Paul Belanger174a8272017-03-14 13:20:10 -04001930 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001931 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001932 self.merge_client = zuul.merger.client.MergeClient(
1933 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001934 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001935 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001936 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001937
James E. Blair0d5a36e2017-02-21 10:53:44 -05001938 self.fake_nodepool = FakeNodepool(
1939 self.zk_chroot_fixture.zookeeper_host,
1940 self.zk_chroot_fixture.zookeeper_port,
1941 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001942
Paul Belanger174a8272017-03-14 13:20:10 -04001943 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001944 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001945 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001946 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001947
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001948 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001949
1950 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001951 self.webapp.start()
1952 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001953 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001954 # Cleanups are run in reverse order
1955 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001956 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001957 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001958
James E. Blairb9c0d772017-03-03 14:34:49 -08001959 self.sched.reconfigure(self.config)
1960 self.sched.resume()
1961
Tobias Henkel7df274b2017-05-26 17:41:11 +02001962 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08001963 # Set up gerrit related fakes
1964 # Set a changes database so multiple FakeGerrit's can report back to
1965 # a virtual canonical database given by the configured hostname
1966 self.gerrit_changes_dbs = {}
1967
1968 def getGerritConnection(driver, name, config):
1969 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1970 con = FakeGerritConnection(driver, name, config,
1971 changes_db=db,
1972 upstream_root=self.upstream_root)
1973 self.event_queues.append(con.event_queue)
1974 setattr(self, 'fake_' + name, con)
1975 return con
1976
1977 self.useFixture(fixtures.MonkeyPatch(
1978 'zuul.driver.gerrit.GerritDriver.getConnection',
1979 getGerritConnection))
1980
Gregory Haynes4fc12542015-04-22 20:38:06 -07001981 def getGithubConnection(driver, name, config):
1982 con = FakeGithubConnection(driver, name, config,
1983 upstream_root=self.upstream_root)
1984 setattr(self, 'fake_' + name, con)
1985 return con
1986
1987 self.useFixture(fixtures.MonkeyPatch(
1988 'zuul.driver.github.GithubDriver.getConnection',
1989 getGithubConnection))
1990
James E. Blaire511d2f2016-12-08 15:22:26 -08001991 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001992 # TODO(jhesketh): This should come from lib.connections for better
1993 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001994 # Register connections from the config
1995 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001996
Joshua Hesketh352264b2015-08-11 23:42:08 +10001997 def FakeSMTPFactory(*args, **kw):
1998 args = [self.smtp_messages] + list(args)
1999 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002000
Joshua Hesketh352264b2015-08-11 23:42:08 +10002001 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002002
James E. Blaire511d2f2016-12-08 15:22:26 -08002003 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002004 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002005 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002006
James E. Blair83005782015-12-11 14:46:03 -08002007 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002008 # This creates the per-test configuration object. It can be
2009 # overriden by subclasses, but should not need to be since it
2010 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07002011 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002012 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002013
2014 if not self.setupSimpleLayout():
2015 if hasattr(self, 'tenant_config_file'):
2016 self.config.set('zuul', 'tenant_config',
2017 self.tenant_config_file)
2018 git_path = os.path.join(
2019 os.path.dirname(
2020 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2021 'git')
2022 if os.path.exists(git_path):
2023 for reponame in os.listdir(git_path):
2024 project = reponame.replace('_', '/')
2025 self.copyDirToRepo(project,
2026 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002027 self.setupAllProjectKeys()
2028
James E. Blair06cc3922017-04-19 10:08:10 -07002029 def setupSimpleLayout(self):
2030 # If the test method has been decorated with a simple_layout,
2031 # use that instead of the class tenant_config_file. Set up a
2032 # single config-project with the specified layout, and
2033 # initialize repos for all of the 'project' entries which
2034 # appear in the layout.
2035 test_name = self.id().split('.')[-1]
2036 test = getattr(self, test_name)
2037 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002038 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002039 else:
2040 return False
2041
James E. Blairb70e55a2017-04-19 12:57:02 -07002042 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002043 path = os.path.join(FIXTURE_DIR, path)
2044 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002045 data = f.read()
2046 layout = yaml.safe_load(data)
2047 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002048 untrusted_projects = []
2049 for item in layout:
2050 if 'project' in item:
2051 name = item['project']['name']
2052 untrusted_projects.append(name)
2053 self.init_repo(name)
2054 self.addCommitToRepo(name, 'initial commit',
2055 files={'README': ''},
2056 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002057 if 'job' in item:
2058 jobname = item['job']['name']
2059 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002060
2061 root = os.path.join(self.test_root, "config")
2062 if not os.path.exists(root):
2063 os.makedirs(root)
2064 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2065 config = [{'tenant':
2066 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002067 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002068 {'config-projects': ['common-config'],
2069 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002070 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002071 f.close()
2072 self.config.set('zuul', 'tenant_config',
2073 os.path.join(FIXTURE_DIR, f.name))
2074
2075 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002076 self.addCommitToRepo('common-config', 'add content from fixture',
2077 files, branch='master', tag='init')
2078
2079 return True
2080
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002081 def setupAllProjectKeys(self):
2082 if self.create_project_keys:
2083 return
2084
2085 path = self.config.get('zuul', 'tenant_config')
2086 with open(os.path.join(FIXTURE_DIR, path)) as f:
2087 tenant_config = yaml.safe_load(f.read())
2088 for tenant in tenant_config:
2089 sources = tenant['tenant']['source']
2090 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002091 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002092 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002093 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002094 self.setupProjectKeys(source, project)
2095
2096 def setupProjectKeys(self, source, project):
2097 # Make sure we set up an RSA key for the project so that we
2098 # don't spend time generating one:
2099
2100 key_root = os.path.join(self.state_root, 'keys')
2101 if not os.path.isdir(key_root):
2102 os.mkdir(key_root, 0o700)
2103 private_key_file = os.path.join(key_root, source, project + '.pem')
2104 private_key_dir = os.path.dirname(private_key_file)
2105 self.log.debug("Installing test keys for project %s at %s" % (
2106 project, private_key_file))
2107 if not os.path.isdir(private_key_dir):
2108 os.makedirs(private_key_dir)
2109 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2110 with open(private_key_file, 'w') as o:
2111 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002112
James E. Blair498059b2016-12-20 13:50:13 -08002113 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002114 self.zk_chroot_fixture = self.useFixture(
2115 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002116 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002117 self.zk_chroot_fixture.zookeeper_host,
2118 self.zk_chroot_fixture.zookeeper_port,
2119 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002120
James E. Blair96c6bf82016-01-15 16:20:40 -08002121 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002122 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002123
2124 files = {}
2125 for (dirpath, dirnames, filenames) in os.walk(source_path):
2126 for filename in filenames:
2127 test_tree_filepath = os.path.join(dirpath, filename)
2128 common_path = os.path.commonprefix([test_tree_filepath,
2129 source_path])
2130 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2131 with open(test_tree_filepath, 'r') as f:
2132 content = f.read()
2133 files[relative_filepath] = content
2134 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002135 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002136
James E. Blaire18d4602017-01-05 11:17:28 -08002137 def assertNodepoolState(self):
2138 # Make sure that there are no pending requests
2139
2140 requests = self.fake_nodepool.getNodeRequests()
2141 self.assertEqual(len(requests), 0)
2142
2143 nodes = self.fake_nodepool.getNodes()
2144 for node in nodes:
2145 self.assertFalse(node['_lock'], "Node %s is locked" %
2146 (node['_oid'],))
2147
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002148 def assertNoGeneratedKeys(self):
2149 # Make sure that Zuul did not generate any project keys
2150 # (unless it was supposed to).
2151
2152 if self.create_project_keys:
2153 return
2154
2155 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2156 test_key = i.read()
2157
2158 key_root = os.path.join(self.state_root, 'keys')
2159 for root, dirname, files in os.walk(key_root):
2160 for fn in files:
2161 with open(os.path.join(root, fn)) as f:
2162 self.assertEqual(test_key, f.read())
2163
Clark Boylanb640e052014-04-03 16:41:46 -07002164 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002165 self.log.debug("Assert final state")
2166 # Make sure no jobs are running
2167 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002168 # Make sure that git.Repo objects have been garbage collected.
2169 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002170 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002171 gc.collect()
2172 for obj in gc.get_objects():
2173 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002174 self.log.debug("Leaked git repo object: 0x%x %s" %
2175 (id(obj), repr(obj)))
2176 for ref in gc.get_referrers(obj):
2177 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002178 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002179 if repos:
2180 for obj in gc.garbage:
2181 self.log.debug(" Garbage %s" % (repr(obj)))
2182 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002183 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002184 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002185 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002186 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002187 for tenant in self.sched.abide.tenants.values():
2188 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002189 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002190 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002191
2192 def shutdown(self):
2193 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002194 self.executor_server.hold_jobs_in_build = False
2195 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002196 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002197 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002198 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002199 self.sched.stop()
2200 self.sched.join()
2201 self.statsd.stop()
2202 self.statsd.join()
2203 self.webapp.stop()
2204 self.webapp.join()
2205 self.rpc.stop()
2206 self.rpc.join()
2207 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002208 self.fake_nodepool.stop()
2209 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002210 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002211 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002212 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002213 # Further the pydevd threads also need to be whitelisted so debugging
2214 # e.g. in PyCharm is possible without breaking shutdown.
2215 whitelist = ['executor-watchdog',
2216 'pydevd.CommandThread',
2217 'pydevd.Reader',
2218 'pydevd.Writer',
2219 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002220 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002221 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002222 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002223 log_str = ""
2224 for thread_id, stack_frame in sys._current_frames().items():
2225 log_str += "Thread: %s\n" % thread_id
2226 log_str += "".join(traceback.format_stack(stack_frame))
2227 self.log.debug(log_str)
2228 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002229
James E. Blaira002b032017-04-18 10:35:48 -07002230 def assertCleanShutdown(self):
2231 pass
2232
James E. Blairc4ba97a2017-04-19 16:26:24 -07002233 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002234 parts = project.split('/')
2235 path = os.path.join(self.upstream_root, *parts[:-1])
2236 if not os.path.exists(path):
2237 os.makedirs(path)
2238 path = os.path.join(self.upstream_root, project)
2239 repo = git.Repo.init(path)
2240
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002241 with repo.config_writer() as config_writer:
2242 config_writer.set_value('user', 'email', 'user@example.com')
2243 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002244
Clark Boylanb640e052014-04-03 16:41:46 -07002245 repo.index.commit('initial commit')
2246 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002247 if tag:
2248 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002249
James E. Blair97d902e2014-08-21 13:25:56 -07002250 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002251 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002252 repo.git.clean('-x', '-f', '-d')
2253
James E. Blair97d902e2014-08-21 13:25:56 -07002254 def create_branch(self, project, branch):
2255 path = os.path.join(self.upstream_root, project)
2256 repo = git.Repo.init(path)
2257 fn = os.path.join(path, 'README')
2258
2259 branch_head = repo.create_head(branch)
2260 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002261 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002262 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002263 f.close()
2264 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002265 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002266
James E. Blair97d902e2014-08-21 13:25:56 -07002267 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002268 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002269 repo.git.clean('-x', '-f', '-d')
2270
Sachi King9f16d522016-03-16 12:20:45 +11002271 def create_commit(self, project):
2272 path = os.path.join(self.upstream_root, project)
2273 repo = git.Repo(path)
2274 repo.head.reference = repo.heads['master']
2275 file_name = os.path.join(path, 'README')
2276 with open(file_name, 'a') as f:
2277 f.write('creating fake commit\n')
2278 repo.index.add([file_name])
2279 commit = repo.index.commit('Creating a fake commit')
2280 return commit.hexsha
2281
James E. Blairf4a5f022017-04-18 14:01:10 -07002282 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002283 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002284 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002285 while len(self.builds):
2286 self.release(self.builds[0])
2287 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002288 i += 1
2289 if count is not None and i >= count:
2290 break
James E. Blairb8c16472015-05-05 14:55:26 -07002291
Clark Boylanb640e052014-04-03 16:41:46 -07002292 def release(self, job):
2293 if isinstance(job, FakeBuild):
2294 job.release()
2295 else:
2296 job.waiting = False
2297 self.log.debug("Queued job %s released" % job.unique)
2298 self.gearman_server.wakeConnections()
2299
2300 def getParameter(self, job, name):
2301 if isinstance(job, FakeBuild):
2302 return job.parameters[name]
2303 else:
2304 parameters = json.loads(job.arguments)
2305 return parameters[name]
2306
Clark Boylanb640e052014-04-03 16:41:46 -07002307 def haveAllBuildsReported(self):
2308 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002309 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002310 return False
2311 # Find out if every build that the worker has completed has been
2312 # reported back to Zuul. If it hasn't then that means a Gearman
2313 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002314 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002315 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002316 if not zbuild:
2317 # It has already been reported
2318 continue
2319 # It hasn't been reported yet.
2320 return False
2321 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002322 worker = self.executor_server.executor_worker
2323 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002324 if connection.state == 'GRAB_WAIT':
2325 return False
2326 return True
2327
2328 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002329 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002330 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002331 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002332 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002333 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002334 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002335 for j in conn.related_jobs.values():
2336 if j.unique == build.uuid:
2337 client_job = j
2338 break
2339 if not client_job:
2340 self.log.debug("%s is not known to the gearman client" %
2341 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002342 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002343 if not client_job.handle:
2344 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002345 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002346 server_job = self.gearman_server.jobs.get(client_job.handle)
2347 if not server_job:
2348 self.log.debug("%s is not known to the gearman server" %
2349 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002350 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002351 if not hasattr(server_job, 'waiting'):
2352 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002353 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002354 if server_job.waiting:
2355 continue
James E. Blair17302972016-08-10 16:11:42 -07002356 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002357 self.log.debug("%s has not reported start" % build)
2358 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002359 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002360 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002361 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002362 if worker_build:
2363 if worker_build.isWaiting():
2364 continue
2365 else:
2366 self.log.debug("%s is running" % worker_build)
2367 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002368 else:
James E. Blair962220f2016-08-03 11:22:38 -07002369 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002370 return False
James E. Blaira002b032017-04-18 10:35:48 -07002371 for (build_uuid, job_worker) in \
2372 self.executor_server.job_workers.items():
2373 if build_uuid not in seen_builds:
2374 self.log.debug("%s is not finalized" % build_uuid)
2375 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002376 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002377
James E. Blairdce6cea2016-12-20 16:45:32 -08002378 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002379 if self.fake_nodepool.paused:
2380 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002381 if self.sched.nodepool.requests:
2382 return False
2383 return True
2384
Jan Hruban6b71aff2015-10-22 16:58:08 +02002385 def eventQueuesEmpty(self):
2386 for queue in self.event_queues:
2387 yield queue.empty()
2388
2389 def eventQueuesJoin(self):
2390 for queue in self.event_queues:
2391 queue.join()
2392
Clark Boylanb640e052014-04-03 16:41:46 -07002393 def waitUntilSettled(self):
2394 self.log.debug("Waiting until settled...")
2395 start = time.time()
2396 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002397 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002398 self.log.error("Timeout waiting for Zuul to settle")
2399 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002400 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002401 self.log.error(" %s: %s" % (queue, queue.empty()))
2402 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002403 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002404 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002405 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002406 self.log.error("All requests completed: %s" %
2407 (self.areAllNodeRequestsComplete(),))
2408 self.log.error("Merge client jobs: %s" %
2409 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002410 raise Exception("Timeout waiting for Zuul to settle")
2411 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002412
Paul Belanger174a8272017-03-14 13:20:10 -04002413 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002414 # have all build states propogated to zuul?
2415 if self.haveAllBuildsReported():
2416 # Join ensures that the queue is empty _and_ events have been
2417 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002418 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002419 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002420 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002421 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002422 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002423 self.areAllNodeRequestsComplete() and
2424 all(self.eventQueuesEmpty())):
2425 # The queue empty check is placed at the end to
2426 # ensure that if a component adds an event between
2427 # when locked the run handler and checked that the
2428 # components were stable, we don't erroneously
2429 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002430 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002431 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002432 self.log.debug("...settled.")
2433 return
2434 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002435 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002436 self.sched.wake_event.wait(0.1)
2437
2438 def countJobResults(self, jobs, result):
2439 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002440 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002441
Monty Taylor0d926122017-05-24 08:07:56 -05002442 def getBuildByName(self, name):
2443 for build in self.builds:
2444 if build.name == name:
2445 return build
2446 raise Exception("Unable to find build %s" % name)
2447
James E. Blair96c6bf82016-01-15 16:20:40 -08002448 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002449 for job in self.history:
2450 if (job.name == name and
2451 (project is None or
2452 job.parameters['ZUUL_PROJECT'] == project)):
2453 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002454 raise Exception("Unable to find job %s in history" % name)
2455
2456 def assertEmptyQueues(self):
2457 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002458 for tenant in self.sched.abide.tenants.values():
2459 for pipeline in tenant.layout.pipelines.values():
2460 for queue in pipeline.queues:
2461 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002462 print('pipeline %s queue %s contents %s' % (
2463 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002464 self.assertEqual(len(queue.queue), 0,
2465 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002466
2467 def assertReportedStat(self, key, value=None, kind=None):
2468 start = time.time()
2469 while time.time() < (start + 5):
2470 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002471 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002472 if key == k:
2473 if value is None and kind is None:
2474 return
2475 elif value:
2476 if value == v:
2477 return
2478 elif kind:
2479 if v.endswith('|' + kind):
2480 return
2481 time.sleep(0.1)
2482
Clark Boylanb640e052014-04-03 16:41:46 -07002483 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002484
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002485 def assertBuilds(self, builds):
2486 """Assert that the running builds are as described.
2487
2488 The list of running builds is examined and must match exactly
2489 the list of builds described by the input.
2490
2491 :arg list builds: A list of dictionaries. Each item in the
2492 list must match the corresponding build in the build
2493 history, and each element of the dictionary must match the
2494 corresponding attribute of the build.
2495
2496 """
James E. Blair3158e282016-08-19 09:34:11 -07002497 try:
2498 self.assertEqual(len(self.builds), len(builds))
2499 for i, d in enumerate(builds):
2500 for k, v in d.items():
2501 self.assertEqual(
2502 getattr(self.builds[i], k), v,
2503 "Element %i in builds does not match" % (i,))
2504 except Exception:
2505 for build in self.builds:
2506 self.log.error("Running build: %s" % build)
2507 else:
2508 self.log.error("No running builds")
2509 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002510
James E. Blairb536ecc2016-08-31 10:11:42 -07002511 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002512 """Assert that the completed builds are as described.
2513
2514 The list of completed builds is examined and must match
2515 exactly the list of builds described by the input.
2516
2517 :arg list history: A list of dictionaries. Each item in the
2518 list must match the corresponding build in the build
2519 history, and each element of the dictionary must match the
2520 corresponding attribute of the build.
2521
James E. Blairb536ecc2016-08-31 10:11:42 -07002522 :arg bool ordered: If true, the history must match the order
2523 supplied, if false, the builds are permitted to have
2524 arrived in any order.
2525
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002526 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002527 def matches(history_item, item):
2528 for k, v in item.items():
2529 if getattr(history_item, k) != v:
2530 return False
2531 return True
James E. Blair3158e282016-08-19 09:34:11 -07002532 try:
2533 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002534 if ordered:
2535 for i, d in enumerate(history):
2536 if not matches(self.history[i], d):
2537 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002538 "Element %i in history does not match %s" %
2539 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002540 else:
2541 unseen = self.history[:]
2542 for i, d in enumerate(history):
2543 found = False
2544 for unseen_item in unseen:
2545 if matches(unseen_item, d):
2546 found = True
2547 unseen.remove(unseen_item)
2548 break
2549 if not found:
2550 raise Exception("No match found for element %i "
2551 "in history" % (i,))
2552 if unseen:
2553 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002554 except Exception:
2555 for build in self.history:
2556 self.log.error("Completed build: %s" % build)
2557 else:
2558 self.log.error("No completed builds")
2559 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002560
James E. Blair6ac368c2016-12-22 18:07:20 -08002561 def printHistory(self):
2562 """Log the build history.
2563
2564 This can be useful during tests to summarize what jobs have
2565 completed.
2566
2567 """
2568 self.log.debug("Build history:")
2569 for build in self.history:
2570 self.log.debug(build)
2571
James E. Blair59fdbac2015-12-07 17:08:06 -08002572 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002573 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2574
James E. Blair9ea70072017-04-19 16:05:30 -07002575 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002576 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002577 if not os.path.exists(root):
2578 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002579 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2580 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002581- tenant:
2582 name: openstack
2583 source:
2584 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002585 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002586 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002587 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002588 - org/project
2589 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002590 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002591 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002592 self.config.set('zuul', 'tenant_config',
2593 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002594 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002595
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002596 def addCommitToRepo(self, project, message, files,
2597 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002598 path = os.path.join(self.upstream_root, project)
2599 repo = git.Repo(path)
2600 repo.head.reference = branch
2601 zuul.merger.merger.reset_repo_to_head(repo)
2602 for fn, content in files.items():
2603 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002604 try:
2605 os.makedirs(os.path.dirname(fn))
2606 except OSError:
2607 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002608 with open(fn, 'w') as f:
2609 f.write(content)
2610 repo.index.add([fn])
2611 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002612 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002613 repo.heads[branch].commit = commit
2614 repo.head.reference = branch
2615 repo.git.clean('-x', '-f', '-d')
2616 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002617 if tag:
2618 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002619 return before
2620
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002621 def commitConfigUpdate(self, project_name, source_name):
2622 """Commit an update to zuul.yaml
2623
2624 This overwrites the zuul.yaml in the specificed project with
2625 the contents specified.
2626
2627 :arg str project_name: The name of the project containing
2628 zuul.yaml (e.g., common-config)
2629
2630 :arg str source_name: The path to the file (underneath the
2631 test fixture directory) whose contents should be used to
2632 replace zuul.yaml.
2633 """
2634
2635 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002636 files = {}
2637 with open(source_path, 'r') as f:
2638 data = f.read()
2639 layout = yaml.safe_load(data)
2640 files['zuul.yaml'] = data
2641 for item in layout:
2642 if 'job' in item:
2643 jobname = item['job']['name']
2644 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002645 before = self.addCommitToRepo(
2646 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002647 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002648 return before
2649
James E. Blair7fc8daa2016-08-08 15:37:15 -07002650 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002651
James E. Blair7fc8daa2016-08-08 15:37:15 -07002652 """Inject a Fake (Gerrit) event.
2653
2654 This method accepts a JSON-encoded event and simulates Zuul
2655 having received it from Gerrit. It could (and should)
2656 eventually apply to any connection type, but is currently only
2657 used with Gerrit connections. The name of the connection is
2658 used to look up the corresponding server, and the event is
2659 simulated as having been received by all Zuul connections
2660 attached to that server. So if two Gerrit connections in Zuul
2661 are connected to the same Gerrit server, and you invoke this
2662 method specifying the name of one of them, the event will be
2663 received by both.
2664
2665 .. note::
2666
2667 "self.fake_gerrit.addEvent" calls should be migrated to
2668 this method.
2669
2670 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002671 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002672 :arg str event: The JSON-encoded event.
2673
2674 """
2675 specified_conn = self.connections.connections[connection]
2676 for conn in self.connections.connections.values():
2677 if (isinstance(conn, specified_conn.__class__) and
2678 specified_conn.server == conn.server):
2679 conn.addEvent(event)
2680
James E. Blaird8af5422017-05-24 13:59:40 -07002681 def getUpstreamRepos(self, projects):
2682 """Return upstream git repo objects for the listed projects
2683
2684 :arg list projects: A list of strings, each the canonical name
2685 of a project.
2686
2687 :returns: A dictionary of {name: repo} for every listed
2688 project.
2689 :rtype: dict
2690
2691 """
2692
2693 repos = {}
2694 for project in projects:
2695 # FIXME(jeblair): the upstream root does not yet have a
2696 # hostname component; that needs to be added, and this
2697 # line removed:
2698 tmp_project_name = '/'.join(project.split('/')[1:])
2699 path = os.path.join(self.upstream_root, tmp_project_name)
2700 repo = git.Repo(path)
2701 repos[project] = repo
2702 return repos
2703
James E. Blair3f876d52016-07-22 13:07:14 -07002704
2705class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002706 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002707 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002708
Joshua Heskethd78b4482015-09-14 16:56:34 -06002709
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002710class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002711 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002712 use_ssl = True
2713
2714
Joshua Heskethd78b4482015-09-14 16:56:34 -06002715class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002716 def setup_config(self):
2717 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002718 for section_name in self.config.sections():
2719 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2720 section_name, re.I)
2721 if not con_match:
2722 continue
2723
2724 if self.config.get(section_name, 'driver') == 'sql':
2725 f = MySQLSchemaFixture()
2726 self.useFixture(f)
2727 if (self.config.get(section_name, 'dburi') ==
2728 '$MYSQL_FIXTURE_DBURI$'):
2729 self.config.set(section_name, 'dburi', f.dburi)