blob: 962b3e87cbf20850c772f9bc8e60d5d1ef4b5dfe [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
Monty Taylorb934c1a2017-06-16 19:31:47 -050018import configparser
Adam Gandelmand81dd762017-02-09 15:15:49 -080019import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070020import gc
21import hashlib
Monty Taylorb934c1a2017-06-16 19:31:47 -050022import importlib
23from io import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070024import json
25import logging
26import os
Monty Taylorb934c1a2017-06-16 19:31:47 -050027import queue
Clark Boylanb640e052014-04-03 16:41:46 -070028import random
29import re
30import select
31import shutil
32import socket
33import string
34import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080035import sys
James E. Blairf84026c2015-12-08 16:11:46 -080036import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070037import threading
Clark Boylan8208c192017-04-24 18:08:08 -070038import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070039import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060040import uuid
Monty Taylorb934c1a2017-06-16 19:31:47 -050041import urllib
Joshua Heskethd78b4482015-09-14 16:56:34 -060042
Clark Boylanb640e052014-04-03 16:41:46 -070043
44import git
45import gear
46import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080047import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080048import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060049import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070050import statsd
51import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080052import testtools.content
53import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080054from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000055import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070056
James E. Blaire511d2f2016-12-08 15:22:26 -080057import zuul.driver.gerrit.gerritsource as gerritsource
58import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070059import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070060import zuul.scheduler
61import zuul.webapp
62import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040063import zuul.executor.server
64import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080065import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070066import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070067import zuul.merger.merger
68import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020069import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070070import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080071import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010072from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070073
74FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
75 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080076
77KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070078
Clark Boylanb640e052014-04-03 16:41:46 -070079
80def repack_repo(path):
81 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
82 output = subprocess.Popen(cmd, close_fds=True,
83 stdout=subprocess.PIPE,
84 stderr=subprocess.PIPE)
85 out = output.communicate()
86 if output.returncode:
87 raise Exception("git repack returned %d" % output.returncode)
88 return out
89
90
91def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040092 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070093
94
James E. Blaira190f3b2015-01-05 14:56:54 -080095def iterate_timeout(max_seconds, purpose):
96 start = time.time()
97 count = 0
98 while (time.time() < start + max_seconds):
99 count += 1
100 yield count
101 time.sleep(0)
102 raise Exception("Timeout waiting for %s" % purpose)
103
104
Jesse Keating436a5452017-04-20 11:48:41 -0700105def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700106 """Specify a layout file for use by a test method.
107
108 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700109 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700110
111 Some tests require only a very simple configuration. For those,
112 establishing a complete config directory hierachy is too much
113 work. In those cases, you can add a simple zuul.yaml file to the
114 test fixtures directory (in fixtures/layouts/foo.yaml) and use
115 this decorator to indicate the test method should use that rather
116 than the tenant config file specified by the test class.
117
118 The decorator will cause that layout file to be added to a
119 config-project called "common-config" and each "project" instance
120 referenced in the layout file will have a git repo automatically
121 initialized.
122 """
123
124 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700125 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700126 return test
127 return decorator
128
129
Gregory Haynes4fc12542015-04-22 20:38:06 -0700130class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700131 _common_path_default = "refs/changes"
132 _points_to_commits_only = True
133
134
Gregory Haynes4fc12542015-04-22 20:38:06 -0700135class FakeGerritChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700136 categories = {'approved': ('Approved', -1, 1),
137 'code-review': ('Code-Review', -2, 2),
138 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700139
140 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700141 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700142 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700143 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.reported = 0
145 self.queried = 0
146 self.patchsets = []
147 self.number = number
148 self.project = project
149 self.branch = branch
150 self.subject = subject
151 self.latest_patchset = 0
152 self.depends_on_change = None
153 self.needed_by_changes = []
154 self.fail_merge = False
155 self.messages = []
156 self.data = {
157 'branch': branch,
158 'comments': [],
159 'commitMessage': subject,
160 'createdOn': time.time(),
161 'id': 'I' + random_sha1(),
162 'lastUpdated': time.time(),
163 'number': str(number),
164 'open': status == 'NEW',
165 'owner': {'email': 'user@example.com',
166 'name': 'User Name',
167 'username': 'username'},
168 'patchSets': self.patchsets,
169 'project': project,
170 'status': status,
171 'subject': subject,
172 'submitRecords': [],
173 'url': 'https://hostname/%s' % number}
174
175 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700176 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700177 self.data['submitRecords'] = self.getSubmitRecords()
178 self.open = status == 'NEW'
179
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700180 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700181 path = os.path.join(self.upstream_root, self.project)
182 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700183 ref = GerritChangeReference.create(
184 repo, '1/%s/%s' % (self.number, self.latest_patchset),
185 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700186 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700187 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700188 repo.git.clean('-x', '-f', '-d')
189
190 path = os.path.join(self.upstream_root, self.project)
191 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700192 for fn, content in files.items():
193 fn = os.path.join(path, fn)
194 with open(fn, 'w') as f:
195 f.write(content)
196 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700197 else:
198 for fni in range(100):
199 fn = os.path.join(path, str(fni))
200 f = open(fn, 'w')
201 for ci in range(4096):
202 f.write(random.choice(string.printable))
203 f.close()
204 repo.index.add([fn])
205
206 r = repo.index.commit(msg)
207 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700208 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700209 repo.git.clean('-x', '-f', '-d')
210 repo.heads['master'].checkout()
211 return r
212
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700213 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700214 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700215 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700216 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700217 data = ("test %s %s %s\n" %
218 (self.branch, self.number, self.latest_patchset))
219 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700220 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700221 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700222 ps_files = [{'file': '/COMMIT_MSG',
223 'type': 'ADDED'},
224 {'file': 'README',
225 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700226 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700227 ps_files.append({'file': f, 'type': 'ADDED'})
228 d = {'approvals': [],
229 'createdOn': time.time(),
230 'files': ps_files,
231 'number': str(self.latest_patchset),
232 'ref': 'refs/changes/1/%s/%s' % (self.number,
233 self.latest_patchset),
234 'revision': c.hexsha,
235 'uploader': {'email': 'user@example.com',
236 'name': 'User name',
237 'username': 'user'}}
238 self.data['currentPatchSet'] = d
239 self.patchsets.append(d)
240 self.data['submitRecords'] = self.getSubmitRecords()
241
242 def getPatchsetCreatedEvent(self, patchset):
243 event = {"type": "patchset-created",
244 "change": {"project": self.project,
245 "branch": self.branch,
246 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
247 "number": str(self.number),
248 "subject": self.subject,
249 "owner": {"name": "User Name"},
250 "url": "https://hostname/3"},
251 "patchSet": self.patchsets[patchset - 1],
252 "uploader": {"name": "User Name"}}
253 return event
254
255 def getChangeRestoredEvent(self):
256 event = {"type": "change-restored",
257 "change": {"project": self.project,
258 "branch": self.branch,
259 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
260 "number": str(self.number),
261 "subject": self.subject,
262 "owner": {"name": "User Name"},
263 "url": "https://hostname/3"},
264 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100265 "patchSet": self.patchsets[-1],
266 "reason": ""}
267 return event
268
269 def getChangeAbandonedEvent(self):
270 event = {"type": "change-abandoned",
271 "change": {"project": self.project,
272 "branch": self.branch,
273 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
274 "number": str(self.number),
275 "subject": self.subject,
276 "owner": {"name": "User Name"},
277 "url": "https://hostname/3"},
278 "abandoner": {"name": "User Name"},
279 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700280 "reason": ""}
281 return event
282
283 def getChangeCommentEvent(self, patchset):
284 event = {"type": "comment-added",
285 "change": {"project": self.project,
286 "branch": self.branch,
287 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
288 "number": str(self.number),
289 "subject": self.subject,
290 "owner": {"name": "User Name"},
291 "url": "https://hostname/3"},
292 "patchSet": self.patchsets[patchset - 1],
293 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700294 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700295 "description": "Code-Review",
296 "value": "0"}],
297 "comment": "This is a comment"}
298 return event
299
James E. Blairc2a5ed72017-02-20 14:12:01 -0500300 def getChangeMergedEvent(self):
301 event = {"submitter": {"name": "Jenkins",
302 "username": "jenkins"},
303 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
304 "patchSet": self.patchsets[-1],
305 "change": self.data,
306 "type": "change-merged",
307 "eventCreatedOn": 1487613810}
308 return event
309
James E. Blair8cce42e2016-10-18 08:18:36 -0700310 def getRefUpdatedEvent(self):
311 path = os.path.join(self.upstream_root, self.project)
312 repo = git.Repo(path)
313 oldrev = repo.heads[self.branch].commit.hexsha
314
315 event = {
316 "type": "ref-updated",
317 "submitter": {
318 "name": "User Name",
319 },
320 "refUpdate": {
321 "oldRev": oldrev,
322 "newRev": self.patchsets[-1]['revision'],
323 "refName": self.branch,
324 "project": self.project,
325 }
326 }
327 return event
328
Joshua Hesketh642824b2014-07-01 17:54:59 +1000329 def addApproval(self, category, value, username='reviewer_john',
330 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700331 if not granted_on:
332 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000333 approval = {
334 'description': self.categories[category][0],
335 'type': category,
336 'value': str(value),
337 'by': {
338 'username': username,
339 'email': username + '@example.com',
340 },
341 'grantedOn': int(granted_on)
342 }
Clark Boylanb640e052014-04-03 16:41:46 -0700343 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
344 if x['by']['username'] == username and x['type'] == category:
345 del self.patchsets[-1]['approvals'][i]
346 self.patchsets[-1]['approvals'].append(approval)
347 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000348 'author': {'email': 'author@example.com',
349 'name': 'Patchset Author',
350 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700351 'change': {'branch': self.branch,
352 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
353 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000354 'owner': {'email': 'owner@example.com',
355 'name': 'Change Owner',
356 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700357 'project': self.project,
358 'subject': self.subject,
359 'topic': 'master',
360 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000361 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700362 'patchSet': self.patchsets[-1],
363 'type': 'comment-added'}
364 self.data['submitRecords'] = self.getSubmitRecords()
365 return json.loads(json.dumps(event))
366
367 def getSubmitRecords(self):
368 status = {}
369 for cat in self.categories.keys():
370 status[cat] = 0
371
372 for a in self.patchsets[-1]['approvals']:
373 cur = status[a['type']]
374 cat_min, cat_max = self.categories[a['type']][1:]
375 new = int(a['value'])
376 if new == cat_min:
377 cur = new
378 elif abs(new) > abs(cur):
379 cur = new
380 status[a['type']] = cur
381
382 labels = []
383 ok = True
384 for typ, cat in self.categories.items():
385 cur = status[typ]
386 cat_min, cat_max = cat[1:]
387 if cur == cat_min:
388 value = 'REJECT'
389 ok = False
390 elif cur == cat_max:
391 value = 'OK'
392 else:
393 value = 'NEED'
394 ok = False
395 labels.append({'label': cat[0], 'status': value})
396 if ok:
397 return [{'status': 'OK'}]
398 return [{'status': 'NOT_READY',
399 'labels': labels}]
400
401 def setDependsOn(self, other, patchset):
402 self.depends_on_change = other
403 d = {'id': other.data['id'],
404 'number': other.data['number'],
405 'ref': other.patchsets[patchset - 1]['ref']
406 }
407 self.data['dependsOn'] = [d]
408
409 other.needed_by_changes.append(self)
410 needed = other.data.get('neededBy', [])
411 d = {'id': self.data['id'],
412 'number': self.data['number'],
413 'ref': self.patchsets[patchset - 1]['ref'],
414 'revision': self.patchsets[patchset - 1]['revision']
415 }
416 needed.append(d)
417 other.data['neededBy'] = needed
418
419 def query(self):
420 self.queried += 1
421 d = self.data.get('dependsOn')
422 if d:
423 d = d[0]
424 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
425 d['isCurrentPatchSet'] = True
426 else:
427 d['isCurrentPatchSet'] = False
428 return json.loads(json.dumps(self.data))
429
430 def setMerged(self):
431 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000432 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700433 return
434 if self.fail_merge:
435 return
436 self.data['status'] = 'MERGED'
437 self.open = False
438
439 path = os.path.join(self.upstream_root, self.project)
440 repo = git.Repo(path)
441 repo.heads[self.branch].commit = \
442 repo.commit(self.patchsets[-1]['revision'])
443
444 def setReported(self):
445 self.reported += 1
446
447
James E. Blaire511d2f2016-12-08 15:22:26 -0800448class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700449 """A Fake Gerrit connection for use in tests.
450
451 This subclasses
452 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
453 ability for tests to add changes to the fake Gerrit it represents.
454 """
455
Joshua Hesketh352264b2015-08-11 23:42:08 +1000456 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700457
James E. Blaire511d2f2016-12-08 15:22:26 -0800458 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700459 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800460 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000461 connection_config)
462
Monty Taylorb934c1a2017-06-16 19:31:47 -0500463 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700464 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
465 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000466 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700467 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200468 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700469
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700470 def addFakeChange(self, project, branch, subject, status='NEW',
471 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700472 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700473 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700474 c = FakeGerritChange(self, self.change_number, project, branch,
475 subject, upstream_root=self.upstream_root,
476 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700477 self.changes[self.change_number] = c
478 return c
479
Clark Boylanb640e052014-04-03 16:41:46 -0700480 def review(self, project, changeid, message, action):
481 number, ps = changeid.split(',')
482 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000483
484 # Add the approval back onto the change (ie simulate what gerrit would
485 # do).
486 # Usually when zuul leaves a review it'll create a feedback loop where
487 # zuul's review enters another gerrit event (which is then picked up by
488 # zuul). However, we can't mimic this behaviour (by adding this
489 # approval event into the queue) as it stops jobs from checking what
490 # happens before this event is triggered. If a job needs to see what
491 # happens they can add their own verified event into the queue.
492 # Nevertheless, we can update change with the new review in gerrit.
493
James E. Blair8b5408c2016-08-08 15:37:46 -0700494 for cat in action.keys():
495 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000496 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000497
Clark Boylanb640e052014-04-03 16:41:46 -0700498 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000499
Clark Boylanb640e052014-04-03 16:41:46 -0700500 if 'submit' in action:
501 change.setMerged()
502 if message:
503 change.setReported()
504
505 def query(self, number):
506 change = self.changes.get(int(number))
507 if change:
508 return change.query()
509 return {}
510
James E. Blairc494d542014-08-06 09:23:52 -0700511 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700512 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700513 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800514 if query.startswith('change:'):
515 # Query a specific changeid
516 changeid = query[len('change:'):]
517 l = [change.query() for change in self.changes.values()
518 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700519 elif query.startswith('message:'):
520 # Query the content of a commit message
521 msg = query[len('message:'):].strip()
522 l = [change.query() for change in self.changes.values()
523 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800524 else:
525 # Query all open changes
526 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700527 return l
James E. Blairc494d542014-08-06 09:23:52 -0700528
Joshua Hesketh352264b2015-08-11 23:42:08 +1000529 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700530 pass
531
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200532 def _uploadPack(self, project):
533 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
534 'multi_ack thin-pack side-band side-band-64k ofs-delta '
535 'shallow no-progress include-tag multi_ack_detailed no-done\n')
536 path = os.path.join(self.upstream_root, project.name)
537 repo = git.Repo(path)
538 for ref in repo.refs:
539 r = ref.object.hexsha + ' ' + ref.path + '\n'
540 ret += '%04x%s' % (len(r) + 4, r)
541 ret += '0000'
542 return ret
543
Joshua Hesketh352264b2015-08-11 23:42:08 +1000544 def getGitUrl(self, project):
545 return os.path.join(self.upstream_root, project.name)
546
Clark Boylanb640e052014-04-03 16:41:46 -0700547
Gregory Haynes4fc12542015-04-22 20:38:06 -0700548class GithubChangeReference(git.Reference):
549 _common_path_default = "refs/pull"
550 _points_to_commits_only = True
551
552
553class FakeGithubPullRequest(object):
554
555 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800556 subject, upstream_root, files=[], number_of_commits=1,
557 writers=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700558 """Creates a new PR with several commits.
559 Sends an event about opened PR."""
560 self.github = github
561 self.source = github
562 self.number = number
563 self.project = project
564 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100565 self.subject = subject
566 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700567 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100568 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700569 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100570 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100571 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800572 self.reviews = []
573 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700574 self.updated_at = None
575 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100576 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100577 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700578 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700579 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100580 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700581 self._updateTimeStamp()
582
Jan Hruban570d01c2016-03-10 21:51:32 +0100583 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700584 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100585 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700586 self._updateTimeStamp()
587
Jan Hruban570d01c2016-03-10 21:51:32 +0100588 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700589 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100590 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700591 self._updateTimeStamp()
592
593 def getPullRequestOpenedEvent(self):
594 return self._getPullRequestEvent('opened')
595
596 def getPullRequestSynchronizeEvent(self):
597 return self._getPullRequestEvent('synchronize')
598
599 def getPullRequestReopenedEvent(self):
600 return self._getPullRequestEvent('reopened')
601
602 def getPullRequestClosedEvent(self):
603 return self._getPullRequestEvent('closed')
604
605 def addComment(self, message):
606 self.comments.append(message)
607 self._updateTimeStamp()
608
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200609 def getCommentAddedEvent(self, text):
610 name = 'issue_comment'
611 data = {
612 'action': 'created',
613 'issue': {
614 'number': self.number
615 },
616 'comment': {
617 'body': text
618 },
619 'repository': {
620 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100621 },
622 'sender': {
623 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200624 }
625 }
626 return (name, data)
627
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800628 def getReviewAddedEvent(self, review):
629 name = 'pull_request_review'
630 data = {
631 'action': 'submitted',
632 'pull_request': {
633 'number': self.number,
634 'title': self.subject,
635 'updated_at': self.updated_at,
636 'base': {
637 'ref': self.branch,
638 'repo': {
639 'full_name': self.project
640 }
641 },
642 'head': {
643 'sha': self.head_sha
644 }
645 },
646 'review': {
647 'state': review
648 },
649 'repository': {
650 'full_name': self.project
651 },
652 'sender': {
653 'login': 'ghuser'
654 }
655 }
656 return (name, data)
657
Jan Hruban16ad31f2015-11-07 14:39:07 +0100658 def addLabel(self, name):
659 if name not in self.labels:
660 self.labels.append(name)
661 self._updateTimeStamp()
662 return self._getLabelEvent(name)
663
664 def removeLabel(self, name):
665 if name in self.labels:
666 self.labels.remove(name)
667 self._updateTimeStamp()
668 return self._getUnlabelEvent(name)
669
670 def _getLabelEvent(self, label):
671 name = 'pull_request'
672 data = {
673 'action': 'labeled',
674 'pull_request': {
675 'number': self.number,
676 'updated_at': self.updated_at,
677 'base': {
678 'ref': self.branch,
679 'repo': {
680 'full_name': self.project
681 }
682 },
683 'head': {
684 'sha': self.head_sha
685 }
686 },
687 'label': {
688 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100689 },
690 'sender': {
691 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100692 }
693 }
694 return (name, data)
695
696 def _getUnlabelEvent(self, label):
697 name = 'pull_request'
698 data = {
699 'action': 'unlabeled',
700 'pull_request': {
701 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100702 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100703 'updated_at': self.updated_at,
704 'base': {
705 'ref': self.branch,
706 'repo': {
707 'full_name': self.project
708 }
709 },
710 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800711 'sha': self.head_sha,
712 'repo': {
713 'full_name': self.project
714 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100715 }
716 },
717 'label': {
718 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100719 },
720 'sender': {
721 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100722 }
723 }
724 return (name, data)
725
Gregory Haynes4fc12542015-04-22 20:38:06 -0700726 def _getRepo(self):
727 repo_path = os.path.join(self.upstream_root, self.project)
728 return git.Repo(repo_path)
729
730 def _createPRRef(self):
731 repo = self._getRepo()
732 GithubChangeReference.create(
733 repo, self._getPRReference(), 'refs/tags/init')
734
Jan Hruban570d01c2016-03-10 21:51:32 +0100735 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700736 repo = self._getRepo()
737 ref = repo.references[self._getPRReference()]
738 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100739 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700740 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100741 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700742 repo.head.reference = ref
743 zuul.merger.merger.reset_repo_to_head(repo)
744 repo.git.clean('-x', '-f', '-d')
745
Jan Hruban570d01c2016-03-10 21:51:32 +0100746 if files:
747 fn = files[0]
748 self.files = files
749 else:
750 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
751 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100752 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700753 fn = os.path.join(repo.working_dir, fn)
754 f = open(fn, 'w')
755 with open(fn, 'w') as f:
756 f.write("test %s %s\n" %
757 (self.branch, self.number))
758 repo.index.add([fn])
759
760 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800761 # Create an empty set of statuses for the given sha,
762 # each sha on a PR may have a status set on it
763 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700764 repo.head.reference = 'master'
765 zuul.merger.merger.reset_repo_to_head(repo)
766 repo.git.clean('-x', '-f', '-d')
767 repo.heads['master'].checkout()
768
769 def _updateTimeStamp(self):
770 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
771
772 def getPRHeadSha(self):
773 repo = self._getRepo()
774 return repo.references[self._getPRReference()].commit.hexsha
775
Jesse Keatingae4cd272017-01-30 17:10:44 -0800776 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800777 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
778 # convert the timestamp to a str format that would be returned
779 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800780
Adam Gandelmand81dd762017-02-09 15:15:49 -0800781 if granted_on:
782 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
783 submitted_at = time.strftime(
784 gh_time_format, granted_on.timetuple())
785 else:
786 # github timestamps only down to the second, so we need to make
787 # sure reviews that tests add appear to be added over a period of
788 # time in the past and not all at once.
789 if not self.reviews:
790 # the first review happens 10 mins ago
791 offset = 600
792 else:
793 # subsequent reviews happen 1 minute closer to now
794 offset = 600 - (len(self.reviews) * 60)
795
796 granted_on = datetime.datetime.utcfromtimestamp(
797 time.time() - offset)
798 submitted_at = time.strftime(
799 gh_time_format, granted_on.timetuple())
800
Jesse Keatingae4cd272017-01-30 17:10:44 -0800801 self.reviews.append({
802 'state': state,
803 'user': {
804 'login': user,
805 'email': user + "@derp.com",
806 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800807 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800808 })
809
Gregory Haynes4fc12542015-04-22 20:38:06 -0700810 def _getPRReference(self):
811 return '%s/head' % self.number
812
813 def _getPullRequestEvent(self, action):
814 name = 'pull_request'
815 data = {
816 'action': action,
817 'number': self.number,
818 'pull_request': {
819 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100820 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700821 'updated_at': self.updated_at,
822 'base': {
823 'ref': self.branch,
824 'repo': {
825 'full_name': self.project
826 }
827 },
828 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800829 'sha': self.head_sha,
830 'repo': {
831 'full_name': self.project
832 }
Gregory Haynes4fc12542015-04-22 20:38:06 -0700833 }
Jan Hruban3b415922016-02-03 13:10:22 +0100834 },
835 'sender': {
836 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700837 }
838 }
839 return (name, data)
840
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800841 def getCommitStatusEvent(self, context, state='success', user='zuul'):
842 name = 'status'
843 data = {
844 'state': state,
845 'sha': self.head_sha,
846 'description': 'Test results for %s: %s' % (self.head_sha, state),
847 'target_url': 'http://zuul/%s' % self.head_sha,
848 'branches': [],
849 'context': context,
850 'sender': {
851 'login': user
852 }
853 }
854 return (name, data)
855
Gregory Haynes4fc12542015-04-22 20:38:06 -0700856
857class FakeGithubConnection(githubconnection.GithubConnection):
858 log = logging.getLogger("zuul.test.FakeGithubConnection")
859
860 def __init__(self, driver, connection_name, connection_config,
861 upstream_root=None):
862 super(FakeGithubConnection, self).__init__(driver, connection_name,
863 connection_config)
864 self.connection_name = connection_name
865 self.pr_number = 0
866 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700867 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700868 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100869 self.merge_failure = False
870 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700871
Jan Hruban570d01c2016-03-10 21:51:32 +0100872 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700873 self.pr_number += 1
874 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100875 self, self.pr_number, project, branch, subject, self.upstream_root,
876 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700877 self.pull_requests.append(pull_request)
878 return pull_request
879
Jesse Keating71a47ff2017-06-06 11:36:43 -0700880 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
881 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700882 if not old_rev:
883 old_rev = '00000000000000000000000000000000'
884 if not new_rev:
885 new_rev = random_sha1()
886 name = 'push'
887 data = {
888 'ref': ref,
889 'before': old_rev,
890 'after': new_rev,
891 'repository': {
892 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700893 },
894 'commits': [
895 {
896 'added': added_files,
897 'removed': removed_files,
898 'modified': modified_files
899 }
900 ]
Wayne1a78c612015-06-11 17:14:13 -0700901 }
902 return (name, data)
903
Gregory Haynes4fc12542015-04-22 20:38:06 -0700904 def emitEvent(self, event):
905 """Emulates sending the GitHub webhook event to the connection."""
906 port = self.webapp.server.socket.getsockname()[1]
907 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700908 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700909 headers = {'X-Github-Event': name}
910 req = urllib.request.Request(
911 'http://localhost:%s/connection/%s/payload'
912 % (port, self.connection_name),
913 data=payload, headers=headers)
914 urllib.request.urlopen(req)
915
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200916 def getPull(self, project, number):
917 pr = self.pull_requests[number - 1]
918 data = {
919 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100920 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200921 'updated_at': pr.updated_at,
922 'base': {
923 'repo': {
924 'full_name': pr.project
925 },
926 'ref': pr.branch,
927 },
Jan Hruban37615e52015-11-19 14:30:49 +0100928 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700929 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200930 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800931 'sha': pr.head_sha,
932 'repo': {
933 'full_name': pr.project
934 }
Jesse Keating61040e72017-06-08 15:08:27 -0700935 },
Jesse Keating19dfb492017-06-13 12:32:33 -0700936 'files': pr.files,
937 'labels': pr.labels
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200938 }
939 return data
940
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800941 def getPullBySha(self, sha):
942 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
943 if len(prs) > 1:
944 raise Exception('Multiple pulls found with head sha: %s' % sha)
945 pr = prs[0]
946 return self.getPull(pr.project, pr.number)
947
Jesse Keatingae4cd272017-01-30 17:10:44 -0800948 def _getPullReviews(self, owner, project, number):
949 pr = self.pull_requests[number - 1]
950 return pr.reviews
951
Jan Hruban3b415922016-02-03 13:10:22 +0100952 def getUser(self, login):
953 data = {
954 'username': login,
955 'name': 'Github User',
956 'email': 'github.user@example.com'
957 }
958 return data
959
Jesse Keatingae4cd272017-01-30 17:10:44 -0800960 def getRepoPermission(self, project, login):
961 owner, proj = project.split('/')
962 for pr in self.pull_requests:
963 pr_owner, pr_project = pr.project.split('/')
964 if (pr_owner == owner and proj == pr_project):
965 if login in pr.writers:
966 return 'write'
967 else:
968 return 'read'
969
Gregory Haynes4fc12542015-04-22 20:38:06 -0700970 def getGitUrl(self, project):
971 return os.path.join(self.upstream_root, str(project))
972
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200973 def real_getGitUrl(self, project):
974 return super(FakeGithubConnection, self).getGitUrl(project)
975
Gregory Haynes4fc12542015-04-22 20:38:06 -0700976 def getProjectBranches(self, project):
977 """Masks getProjectBranches since we don't have a real github"""
978
979 # just returns master for now
980 return ['master']
981
Jan Hrubane252a732017-01-03 15:03:09 +0100982 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700983 pull_request = self.pull_requests[pr_number - 1]
984 pull_request.addComment(message)
985
Jan Hruban3b415922016-02-03 13:10:22 +0100986 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100987 pull_request = self.pull_requests[pr_number - 1]
988 if self.merge_failure:
989 raise Exception('Pull request was not merged')
990 if self.merge_not_allowed_count > 0:
991 self.merge_not_allowed_count -= 1
992 raise MergeFailure('Merge was not successful due to mergeability'
993 ' conflict')
994 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +0100995 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +0100996
Jesse Keatingd96e5882017-01-19 13:55:50 -0800997 def getCommitStatuses(self, project, sha):
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700998 return self.statuses.get(project, {}).get(sha, [])
Jesse Keatingd96e5882017-01-19 13:55:50 -0800999
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001000 def setCommitStatus(self, project, sha, state, url='', description='',
1001 context='default', user='zuul'):
1002 # always insert a status to the front of the list, to represent
1003 # the last status provided for a commit.
1004 # Since we're bypassing github API, which would require a user, we
1005 # default the user as 'zuul' here.
1006 self.statuses.setdefault(project, {}).setdefault(sha, [])
1007 self.statuses[project][sha].insert(0, {
1008 'state': state,
1009 'url': url,
1010 'description': description,
1011 'context': context,
1012 'creator': {
1013 'login': user
1014 }
1015 })
Jan Hrubane252a732017-01-03 15:03:09 +01001016
Jan Hruban16ad31f2015-11-07 14:39:07 +01001017 def labelPull(self, project, pr_number, label):
1018 pull_request = self.pull_requests[pr_number - 1]
1019 pull_request.addLabel(label)
1020
1021 def unlabelPull(self, project, pr_number, label):
1022 pull_request = self.pull_requests[pr_number - 1]
1023 pull_request.removeLabel(label)
1024
Gregory Haynes4fc12542015-04-22 20:38:06 -07001025
Clark Boylanb640e052014-04-03 16:41:46 -07001026class BuildHistory(object):
1027 def __init__(self, **kw):
1028 self.__dict__.update(kw)
1029
1030 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -07001031 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1032 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001033
1034
Clark Boylanb640e052014-04-03 16:41:46 -07001035class FakeStatsd(threading.Thread):
1036 def __init__(self):
1037 threading.Thread.__init__(self)
1038 self.daemon = True
1039 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1040 self.sock.bind(('', 0))
1041 self.port = self.sock.getsockname()[1]
1042 self.wake_read, self.wake_write = os.pipe()
1043 self.stats = []
1044
1045 def run(self):
1046 while True:
1047 poll = select.poll()
1048 poll.register(self.sock, select.POLLIN)
1049 poll.register(self.wake_read, select.POLLIN)
1050 ret = poll.poll()
1051 for (fd, event) in ret:
1052 if fd == self.sock.fileno():
1053 data = self.sock.recvfrom(1024)
1054 if not data:
1055 return
1056 self.stats.append(data[0])
1057 if fd == self.wake_read:
1058 return
1059
1060 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001061 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001062
1063
James E. Blaire1767bc2016-08-02 10:00:27 -07001064class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001065 log = logging.getLogger("zuul.test")
1066
Paul Belanger174a8272017-03-14 13:20:10 -04001067 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001068 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001069 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001070 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001071 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001072 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001073 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001074 # TODOv3(jeblair): self.node is really "the label of the node
1075 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001076 # keep using it like this, or we may end up exposing more of
1077 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001078 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001079 self.node = None
1080 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001081 self.node = self.parameters['nodes'][0]['label']
Clark Boylanb640e052014-04-03 16:41:46 -07001082 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001083 self.pipeline = self.parameters['ZUUL_PIPELINE']
1084 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001085 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001086 self.wait_condition = threading.Condition()
1087 self.waiting = False
1088 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001089 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001090 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001091 self.changes = None
1092 if 'ZUUL_CHANGE_IDS' in self.parameters:
1093 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001094
James E. Blair3158e282016-08-19 09:34:11 -07001095 def __repr__(self):
1096 waiting = ''
1097 if self.waiting:
1098 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001099 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1100 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001101
Clark Boylanb640e052014-04-03 16:41:46 -07001102 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001103 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001104 self.wait_condition.acquire()
1105 self.wait_condition.notify()
1106 self.waiting = False
1107 self.log.debug("Build %s released" % self.unique)
1108 self.wait_condition.release()
1109
1110 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001111 """Return whether this build is being held.
1112
1113 :returns: Whether the build is being held.
1114 :rtype: bool
1115 """
1116
Clark Boylanb640e052014-04-03 16:41:46 -07001117 self.wait_condition.acquire()
1118 if self.waiting:
1119 ret = True
1120 else:
1121 ret = False
1122 self.wait_condition.release()
1123 return ret
1124
1125 def _wait(self):
1126 self.wait_condition.acquire()
1127 self.waiting = True
1128 self.log.debug("Build %s waiting" % self.unique)
1129 self.wait_condition.wait()
1130 self.wait_condition.release()
1131
1132 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001133 self.log.debug('Running build %s' % self.unique)
1134
Paul Belanger174a8272017-03-14 13:20:10 -04001135 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001136 self.log.debug('Holding build %s' % self.unique)
1137 self._wait()
1138 self.log.debug("Build %s continuing" % self.unique)
1139
James E. Blair412fba82017-01-26 15:00:50 -08001140 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001141 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001142 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001143 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001144 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001145 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001146 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001147
James E. Blaire1767bc2016-08-02 10:00:27 -07001148 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001149
James E. Blaira5dba232016-08-08 15:53:24 -07001150 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001151 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001152 for change in changes:
1153 if self.hasChanges(change):
1154 return True
1155 return False
1156
James E. Blaire7b99a02016-08-05 14:27:34 -07001157 def hasChanges(self, *changes):
1158 """Return whether this build has certain changes in its git repos.
1159
1160 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001161 are expected to be present (in order) in the git repository of
1162 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001163
1164 :returns: Whether the build has the indicated changes.
1165 :rtype: bool
1166
1167 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001168 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001169 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001170 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001171 try:
1172 repo = git.Repo(path)
1173 except NoSuchPathError as e:
1174 self.log.debug('%s' % e)
1175 return False
1176 ref = self.parameters['ZUUL_REF']
1177 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1178 commit_message = '%s-1' % change.subject
1179 self.log.debug("Checking if build %s has changes; commit_message "
1180 "%s; repo_messages %s" % (self, commit_message,
1181 repo_messages))
1182 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001183 self.log.debug(" messages do not match")
1184 return False
1185 self.log.debug(" OK")
1186 return True
1187
James E. Blaird8af5422017-05-24 13:59:40 -07001188 def getWorkspaceRepos(self, projects):
1189 """Return workspace git repo objects for the listed projects
1190
1191 :arg list projects: A list of strings, each the canonical name
1192 of a project.
1193
1194 :returns: A dictionary of {name: repo} for every listed
1195 project.
1196 :rtype: dict
1197
1198 """
1199
1200 repos = {}
1201 for project in projects:
1202 path = os.path.join(self.jobdir.src_root, project)
1203 repo = git.Repo(path)
1204 repos[project] = repo
1205 return repos
1206
Clark Boylanb640e052014-04-03 16:41:46 -07001207
Paul Belanger174a8272017-03-14 13:20:10 -04001208class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1209 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001210
Paul Belanger174a8272017-03-14 13:20:10 -04001211 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001212 they will report that they have started but then pause until
1213 released before reporting completion. This attribute may be
1214 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001215 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001216 be explicitly released.
1217
1218 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001219 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001220 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001221 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001222 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001223 self.hold_jobs_in_build = False
1224 self.lock = threading.Lock()
1225 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001226 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001227 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001228 self.job_builds = {}
Monty Taylorde8242c2017-02-23 20:29:53 -06001229 self.hostname = 'zl.example.com'
James E. Blairf5dbd002015-12-23 15:26:17 -08001230
James E. Blaira5dba232016-08-08 15:53:24 -07001231 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001232 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001233
1234 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001235 :arg Change change: The :py:class:`~tests.base.FakeChange`
1236 instance which should cause the job to fail. This job
1237 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001238
1239 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001240 l = self.fail_tests.get(name, [])
1241 l.append(change)
1242 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001243
James E. Blair962220f2016-08-03 11:22:38 -07001244 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001245 """Release a held build.
1246
1247 :arg str regex: A regular expression which, if supplied, will
1248 cause only builds with matching names to be released. If
1249 not supplied, all builds will be released.
1250
1251 """
James E. Blair962220f2016-08-03 11:22:38 -07001252 builds = self.running_builds[:]
1253 self.log.debug("Releasing build %s (%s)" % (regex,
1254 len(self.running_builds)))
1255 for build in builds:
1256 if not regex or re.match(regex, build.name):
1257 self.log.debug("Releasing build %s" %
1258 (build.parameters['ZUUL_UUID']))
1259 build.release()
1260 else:
1261 self.log.debug("Not releasing build %s" %
1262 (build.parameters['ZUUL_UUID']))
1263 self.log.debug("Done releasing builds %s (%s)" %
1264 (regex, len(self.running_builds)))
1265
Paul Belanger174a8272017-03-14 13:20:10 -04001266 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001267 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001268 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001269 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001270 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001271 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001272 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001273 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001274 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1275 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001276
1277 def stopJob(self, job):
1278 self.log.debug("handle stop")
1279 parameters = json.loads(job.arguments)
1280 uuid = parameters['uuid']
1281 for build in self.running_builds:
1282 if build.unique == uuid:
1283 build.aborted = True
1284 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001285 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001286
James E. Blaira002b032017-04-18 10:35:48 -07001287 def stop(self):
1288 for build in self.running_builds:
1289 build.release()
1290 super(RecordingExecutorServer, self).stop()
1291
Joshua Hesketh50c21782016-10-13 21:34:14 +11001292
Paul Belanger174a8272017-03-14 13:20:10 -04001293class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001294 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001295 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001296 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001297 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001298 if not commit: # merge conflict
1299 self.recordResult('MERGER_FAILURE')
1300 return commit
1301
1302 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001303 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001304 self.executor_server.lock.acquire()
1305 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001306 BuildHistory(name=build.name, result=result, changes=build.changes,
1307 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001308 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001309 pipeline=build.parameters['ZUUL_PIPELINE'])
1310 )
Paul Belanger174a8272017-03-14 13:20:10 -04001311 self.executor_server.running_builds.remove(build)
1312 del self.executor_server.job_builds[self.job.unique]
1313 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001314
1315 def runPlaybooks(self, args):
1316 build = self.executor_server.job_builds[self.job.unique]
1317 build.jobdir = self.jobdir
1318
1319 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1320 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001321 return result
1322
Monty Taylore6562aa2017-02-20 07:37:39 -05001323 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001324 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001325
Paul Belanger174a8272017-03-14 13:20:10 -04001326 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001327 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001328 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001329 else:
1330 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001331 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001332
James E. Blairad8dca02017-02-21 11:48:32 -05001333 def getHostList(self, args):
1334 self.log.debug("hostlist")
1335 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001336 for host in hosts:
1337 host['host_vars']['ansible_connection'] = 'local'
1338
1339 hosts.append(dict(
1340 name='localhost',
1341 host_vars=dict(ansible_connection='local'),
1342 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001343 return hosts
1344
James E. Blairf5dbd002015-12-23 15:26:17 -08001345
Clark Boylanb640e052014-04-03 16:41:46 -07001346class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001347 """A Gearman server for use in tests.
1348
1349 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1350 added to the queue but will not be distributed to workers
1351 until released. This attribute may be changed at any time and
1352 will take effect for subsequently enqueued jobs, but
1353 previously held jobs will still need to be explicitly
1354 released.
1355
1356 """
1357
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001358 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001359 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001360 if use_ssl:
1361 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1362 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1363 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1364 else:
1365 ssl_ca = None
1366 ssl_cert = None
1367 ssl_key = None
1368
1369 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1370 ssl_cert=ssl_cert,
1371 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001372
1373 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001374 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1375 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001376 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001377 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001378 job.waiting = self.hold_jobs_in_queue
1379 else:
1380 job.waiting = False
1381 if job.waiting:
1382 continue
1383 if job.name in connection.functions:
1384 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001385 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001386 connection.related_jobs[job.handle] = job
1387 job.worker_connection = connection
1388 job.running = True
1389 return job
1390 return None
1391
1392 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001393 """Release a held job.
1394
1395 :arg str regex: A regular expression which, if supplied, will
1396 cause only jobs with matching names to be released. If
1397 not supplied, all jobs will be released.
1398 """
Clark Boylanb640e052014-04-03 16:41:46 -07001399 released = False
1400 qlen = (len(self.high_queue) + len(self.normal_queue) +
1401 len(self.low_queue))
1402 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1403 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001404 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001405 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001406 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001407 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001408 self.log.debug("releasing queued job %s" %
1409 job.unique)
1410 job.waiting = False
1411 released = True
1412 else:
1413 self.log.debug("not releasing queued job %s" %
1414 job.unique)
1415 if released:
1416 self.wakeConnections()
1417 qlen = (len(self.high_queue) + len(self.normal_queue) +
1418 len(self.low_queue))
1419 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1420
1421
1422class FakeSMTP(object):
1423 log = logging.getLogger('zuul.FakeSMTP')
1424
1425 def __init__(self, messages, server, port):
1426 self.server = server
1427 self.port = port
1428 self.messages = messages
1429
1430 def sendmail(self, from_email, to_email, msg):
1431 self.log.info("Sending email from %s, to %s, with msg %s" % (
1432 from_email, to_email, msg))
1433
1434 headers = msg.split('\n\n', 1)[0]
1435 body = msg.split('\n\n', 1)[1]
1436
1437 self.messages.append(dict(
1438 from_email=from_email,
1439 to_email=to_email,
1440 msg=msg,
1441 headers=headers,
1442 body=body,
1443 ))
1444
1445 return True
1446
1447 def quit(self):
1448 return True
1449
1450
James E. Blairdce6cea2016-12-20 16:45:32 -08001451class FakeNodepool(object):
1452 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001453 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001454
1455 log = logging.getLogger("zuul.test.FakeNodepool")
1456
1457 def __init__(self, host, port, chroot):
1458 self.client = kazoo.client.KazooClient(
1459 hosts='%s:%s%s' % (host, port, chroot))
1460 self.client.start()
1461 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001462 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001463 self.thread = threading.Thread(target=self.run)
1464 self.thread.daemon = True
1465 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001466 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001467
1468 def stop(self):
1469 self._running = False
1470 self.thread.join()
1471 self.client.stop()
1472 self.client.close()
1473
1474 def run(self):
1475 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001476 try:
1477 self._run()
1478 except Exception:
1479 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001480 time.sleep(0.1)
1481
1482 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001483 if self.paused:
1484 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001485 for req in self.getNodeRequests():
1486 self.fulfillRequest(req)
1487
1488 def getNodeRequests(self):
1489 try:
1490 reqids = self.client.get_children(self.REQUEST_ROOT)
1491 except kazoo.exceptions.NoNodeError:
1492 return []
1493 reqs = []
1494 for oid in sorted(reqids):
1495 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001496 try:
1497 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001498 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001499 data['_oid'] = oid
1500 reqs.append(data)
1501 except kazoo.exceptions.NoNodeError:
1502 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001503 return reqs
1504
James E. Blaire18d4602017-01-05 11:17:28 -08001505 def getNodes(self):
1506 try:
1507 nodeids = self.client.get_children(self.NODE_ROOT)
1508 except kazoo.exceptions.NoNodeError:
1509 return []
1510 nodes = []
1511 for oid in sorted(nodeids):
1512 path = self.NODE_ROOT + '/' + oid
1513 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001514 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001515 data['_oid'] = oid
1516 try:
1517 lockfiles = self.client.get_children(path + '/lock')
1518 except kazoo.exceptions.NoNodeError:
1519 lockfiles = []
1520 if lockfiles:
1521 data['_lock'] = True
1522 else:
1523 data['_lock'] = False
1524 nodes.append(data)
1525 return nodes
1526
James E. Blaira38c28e2017-01-04 10:33:20 -08001527 def makeNode(self, request_id, node_type):
1528 now = time.time()
1529 path = '/nodepool/nodes/'
1530 data = dict(type=node_type,
1531 provider='test-provider',
1532 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001533 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001534 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001535 public_ipv4='127.0.0.1',
1536 private_ipv4=None,
1537 public_ipv6=None,
1538 allocated_to=request_id,
1539 state='ready',
1540 state_time=now,
1541 created_time=now,
1542 updated_time=now,
1543 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001544 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001545 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001546 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001547 path = self.client.create(path, data,
1548 makepath=True,
1549 sequence=True)
1550 nodeid = path.split("/")[-1]
1551 return nodeid
1552
James E. Blair6ab79e02017-01-06 10:10:17 -08001553 def addFailRequest(self, request):
1554 self.fail_requests.add(request['_oid'])
1555
James E. Blairdce6cea2016-12-20 16:45:32 -08001556 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001557 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001558 return
1559 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001560 oid = request['_oid']
1561 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001562
James E. Blair6ab79e02017-01-06 10:10:17 -08001563 if oid in self.fail_requests:
1564 request['state'] = 'failed'
1565 else:
1566 request['state'] = 'fulfilled'
1567 nodes = []
1568 for node in request['node_types']:
1569 nodeid = self.makeNode(oid, node)
1570 nodes.append(nodeid)
1571 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001572
James E. Blaira38c28e2017-01-04 10:33:20 -08001573 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001574 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001575 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001576 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001577 try:
1578 self.client.set(path, data)
1579 except kazoo.exceptions.NoNodeError:
1580 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001581
1582
James E. Blair498059b2016-12-20 13:50:13 -08001583class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001584 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001585 super(ChrootedKazooFixture, self).__init__()
1586
1587 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1588 if ':' in zk_host:
1589 host, port = zk_host.split(':')
1590 else:
1591 host = zk_host
1592 port = None
1593
1594 self.zookeeper_host = host
1595
1596 if not port:
1597 self.zookeeper_port = 2181
1598 else:
1599 self.zookeeper_port = int(port)
1600
Clark Boylan621ec9a2017-04-07 17:41:33 -07001601 self.test_id = test_id
1602
James E. Blair498059b2016-12-20 13:50:13 -08001603 def _setUp(self):
1604 # Make sure the test chroot paths do not conflict
1605 random_bits = ''.join(random.choice(string.ascii_lowercase +
1606 string.ascii_uppercase)
1607 for x in range(8))
1608
Clark Boylan621ec9a2017-04-07 17:41:33 -07001609 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001610 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1611
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001612 self.addCleanup(self._cleanup)
1613
James E. Blair498059b2016-12-20 13:50:13 -08001614 # Ensure the chroot path exists and clean up any pre-existing znodes.
1615 _tmp_client = kazoo.client.KazooClient(
1616 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1617 _tmp_client.start()
1618
1619 if _tmp_client.exists(self.zookeeper_chroot):
1620 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1621
1622 _tmp_client.ensure_path(self.zookeeper_chroot)
1623 _tmp_client.stop()
1624 _tmp_client.close()
1625
James E. Blair498059b2016-12-20 13:50:13 -08001626 def _cleanup(self):
1627 '''Remove the chroot path.'''
1628 # Need a non-chroot'ed client to remove the chroot path
1629 _tmp_client = kazoo.client.KazooClient(
1630 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1631 _tmp_client.start()
1632 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1633 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001634 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001635
1636
Joshua Heskethd78b4482015-09-14 16:56:34 -06001637class MySQLSchemaFixture(fixtures.Fixture):
1638 def setUp(self):
1639 super(MySQLSchemaFixture, self).setUp()
1640
1641 random_bits = ''.join(random.choice(string.ascii_lowercase +
1642 string.ascii_uppercase)
1643 for x in range(8))
1644 self.name = '%s_%s' % (random_bits, os.getpid())
1645 self.passwd = uuid.uuid4().hex
1646 db = pymysql.connect(host="localhost",
1647 user="openstack_citest",
1648 passwd="openstack_citest",
1649 db="openstack_citest")
1650 cur = db.cursor()
1651 cur.execute("create database %s" % self.name)
1652 cur.execute(
1653 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1654 (self.name, self.name, self.passwd))
1655 cur.execute("flush privileges")
1656
1657 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1658 self.passwd,
1659 self.name)
1660 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1661 self.addCleanup(self.cleanup)
1662
1663 def cleanup(self):
1664 db = pymysql.connect(host="localhost",
1665 user="openstack_citest",
1666 passwd="openstack_citest",
1667 db="openstack_citest")
1668 cur = db.cursor()
1669 cur.execute("drop database %s" % self.name)
1670 cur.execute("drop user '%s'@'localhost'" % self.name)
1671 cur.execute("flush privileges")
1672
1673
Maru Newby3fe5f852015-01-13 04:22:14 +00001674class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001675 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001676 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001677
James E. Blair1c236df2017-02-01 14:07:24 -08001678 def attachLogs(self, *args):
1679 def reader():
1680 self._log_stream.seek(0)
1681 while True:
1682 x = self._log_stream.read(4096)
1683 if not x:
1684 break
1685 yield x.encode('utf8')
1686 content = testtools.content.content_from_reader(
1687 reader,
1688 testtools.content_type.UTF8_TEXT,
1689 False)
1690 self.addDetail('logging', content)
1691
Clark Boylanb640e052014-04-03 16:41:46 -07001692 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001693 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001694 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1695 try:
1696 test_timeout = int(test_timeout)
1697 except ValueError:
1698 # If timeout value is invalid do not set a timeout.
1699 test_timeout = 0
1700 if test_timeout > 0:
1701 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1702
1703 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1704 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1705 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1706 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1707 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1708 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1709 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1710 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1711 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1712 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001713 self._log_stream = StringIO()
1714 self.addOnException(self.attachLogs)
1715 else:
1716 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001717
James E. Blair73b41772017-05-22 13:22:55 -07001718 # NOTE(jeblair): this is temporary extra debugging to try to
1719 # track down a possible leak.
1720 orig_git_repo_init = git.Repo.__init__
1721
1722 def git_repo_init(myself, *args, **kw):
1723 orig_git_repo_init(myself, *args, **kw)
1724 self.log.debug("Created git repo 0x%x %s" %
1725 (id(myself), repr(myself)))
1726
1727 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1728 git_repo_init))
1729
James E. Blair1c236df2017-02-01 14:07:24 -08001730 handler = logging.StreamHandler(self._log_stream)
1731 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1732 '%(levelname)-8s %(message)s')
1733 handler.setFormatter(formatter)
1734
1735 logger = logging.getLogger()
1736 logger.setLevel(logging.DEBUG)
1737 logger.addHandler(handler)
1738
Clark Boylan3410d532017-04-25 12:35:29 -07001739 # Make sure we don't carry old handlers around in process state
1740 # which slows down test runs
1741 self.addCleanup(logger.removeHandler, handler)
1742 self.addCleanup(handler.close)
1743 self.addCleanup(handler.flush)
1744
James E. Blair1c236df2017-02-01 14:07:24 -08001745 # NOTE(notmorgan): Extract logging overrides for specific
1746 # libraries from the OS_LOG_DEFAULTS env and create loggers
1747 # for each. This is used to limit the output during test runs
1748 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001749 log_defaults_from_env = os.environ.get(
1750 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001751 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001752
James E. Blairdce6cea2016-12-20 16:45:32 -08001753 if log_defaults_from_env:
1754 for default in log_defaults_from_env.split(','):
1755 try:
1756 name, level_str = default.split('=', 1)
1757 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001758 logger = logging.getLogger(name)
1759 logger.setLevel(level)
1760 logger.addHandler(handler)
1761 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001762 except ValueError:
1763 # NOTE(notmorgan): Invalid format of the log default,
1764 # skip and don't try and apply a logger for the
1765 # specified module
1766 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001767
Maru Newby3fe5f852015-01-13 04:22:14 +00001768
1769class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001770 """A test case with a functioning Zuul.
1771
1772 The following class variables are used during test setup and can
1773 be overidden by subclasses but are effectively read-only once a
1774 test method starts running:
1775
1776 :cvar str config_file: This points to the main zuul config file
1777 within the fixtures directory. Subclasses may override this
1778 to obtain a different behavior.
1779
1780 :cvar str tenant_config_file: This is the tenant config file
1781 (which specifies from what git repos the configuration should
1782 be loaded). It defaults to the value specified in
1783 `config_file` but can be overidden by subclasses to obtain a
1784 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001785 configuration. See also the :py:func:`simple_layout`
1786 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001787
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001788 :cvar bool create_project_keys: Indicates whether Zuul should
1789 auto-generate keys for each project, or whether the test
1790 infrastructure should insert dummy keys to save time during
1791 startup. Defaults to False.
1792
James E. Blaire7b99a02016-08-05 14:27:34 -07001793 The following are instance variables that are useful within test
1794 methods:
1795
1796 :ivar FakeGerritConnection fake_<connection>:
1797 A :py:class:`~tests.base.FakeGerritConnection` will be
1798 instantiated for each connection present in the config file
1799 and stored here. For instance, `fake_gerrit` will hold the
1800 FakeGerritConnection object for a connection named `gerrit`.
1801
1802 :ivar FakeGearmanServer gearman_server: An instance of
1803 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1804 server that all of the Zuul components in this test use to
1805 communicate with each other.
1806
Paul Belanger174a8272017-03-14 13:20:10 -04001807 :ivar RecordingExecutorServer executor_server: An instance of
1808 :py:class:`~tests.base.RecordingExecutorServer` which is the
1809 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001810
1811 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1812 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001813 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001814 list upon completion.
1815
1816 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1817 objects representing completed builds. They are appended to
1818 the list in the order they complete.
1819
1820 """
1821
James E. Blair83005782015-12-11 14:46:03 -08001822 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001823 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001824 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001825 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001826
1827 def _startMerger(self):
1828 self.merge_server = zuul.merger.server.MergeServer(self.config,
1829 self.connections)
1830 self.merge_server.start()
1831
Maru Newby3fe5f852015-01-13 04:22:14 +00001832 def setUp(self):
1833 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001834
1835 self.setupZK()
1836
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001837 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001838 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001839 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1840 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001841 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001842 tmp_root = tempfile.mkdtemp(
1843 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001844 self.test_root = os.path.join(tmp_root, "zuul-test")
1845 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001846 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001847 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001848 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001849 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1850 self.executor_state_root = os.path.join(self.test_root, "executor-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)
James E. Blair01d733e2017-06-23 20:47:51 +01001857 os.makedirs(self.merger_state_root)
1858 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001859
1860 # Make per test copy of Configuration.
1861 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001862 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1863 if not os.path.exists(self.private_key_file):
1864 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1865 shutil.copy(src_private_key_file, self.private_key_file)
1866 shutil.copy('{}.pub'.format(src_private_key_file),
1867 '{}.pub'.format(self.private_key_file))
1868 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001869 self.config.set('scheduler', 'tenant_config',
1870 os.path.join(
1871 FIXTURE_DIR,
1872 self.config.get('scheduler', 'tenant_config')))
1873 self.config.set('zuul', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05001874 self.config.set('merger', 'git_dir', self.merger_src_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001875 self.config.set('merger', 'state_dir', self.merger_state_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001876 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001877 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001878 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001879
Clark Boylanb640e052014-04-03 16:41:46 -07001880 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001881 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1882 # see: https://github.com/jsocol/pystatsd/issues/61
1883 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001884 os.environ['STATSD_PORT'] = str(self.statsd.port)
1885 self.statsd.start()
1886 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05001887 importlib.reload(statsd)
1888 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001889
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001890 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001891
1892 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001893 self.log.info("Gearman server on port %s" %
1894 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001895 if self.use_ssl:
1896 self.log.info('SSL enabled for gearman')
1897 self.config.set(
1898 'gearman', 'ssl_ca',
1899 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1900 self.config.set(
1901 'gearman', 'ssl_cert',
1902 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1903 self.config.set(
1904 'gearman', 'ssl_key',
1905 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001906
James E. Blaire511d2f2016-12-08 15:22:26 -08001907 gerritsource.GerritSource.replication_timeout = 1.5
1908 gerritsource.GerritSource.replication_retry_interval = 0.5
1909 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001910
Joshua Hesketh352264b2015-08-11 23:42:08 +10001911 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001912
Jan Hruban7083edd2015-08-21 14:00:54 +02001913 self.webapp = zuul.webapp.WebApp(
1914 self.sched, port=0, listen_address='127.0.0.1')
1915
Jan Hruban6b71aff2015-10-22 16:58:08 +02001916 self.event_queues = [
1917 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001918 self.sched.trigger_event_queue,
1919 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001920 ]
1921
James E. Blairfef78942016-03-11 16:28:56 -08001922 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001923 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001924
Paul Belanger174a8272017-03-14 13:20:10 -04001925 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001926 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001927 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001928 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001929 _test_root=self.test_root,
1930 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001931 self.executor_server.start()
1932 self.history = self.executor_server.build_history
1933 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001934
Paul Belanger174a8272017-03-14 13:20:10 -04001935 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001936 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001937 self.merge_client = zuul.merger.client.MergeClient(
1938 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001939 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001940 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001941 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001942
James E. Blair0d5a36e2017-02-21 10:53:44 -05001943 self.fake_nodepool = FakeNodepool(
1944 self.zk_chroot_fixture.zookeeper_host,
1945 self.zk_chroot_fixture.zookeeper_port,
1946 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001947
Paul Belanger174a8272017-03-14 13:20:10 -04001948 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001949 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001950 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001951 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001952
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001953 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001954
1955 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001956 self.webapp.start()
1957 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001958 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001959 # Cleanups are run in reverse order
1960 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001961 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001962 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001963
James E. Blairb9c0d772017-03-03 14:34:49 -08001964 self.sched.reconfigure(self.config)
1965 self.sched.resume()
1966
Tobias Henkel7df274b2017-05-26 17:41:11 +02001967 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08001968 # Set up gerrit related fakes
1969 # Set a changes database so multiple FakeGerrit's can report back to
1970 # a virtual canonical database given by the configured hostname
1971 self.gerrit_changes_dbs = {}
1972
1973 def getGerritConnection(driver, name, config):
1974 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1975 con = FakeGerritConnection(driver, name, config,
1976 changes_db=db,
1977 upstream_root=self.upstream_root)
1978 self.event_queues.append(con.event_queue)
1979 setattr(self, 'fake_' + name, con)
1980 return con
1981
1982 self.useFixture(fixtures.MonkeyPatch(
1983 'zuul.driver.gerrit.GerritDriver.getConnection',
1984 getGerritConnection))
1985
Gregory Haynes4fc12542015-04-22 20:38:06 -07001986 def getGithubConnection(driver, name, config):
1987 con = FakeGithubConnection(driver, name, config,
1988 upstream_root=self.upstream_root)
1989 setattr(self, 'fake_' + name, con)
1990 return con
1991
1992 self.useFixture(fixtures.MonkeyPatch(
1993 'zuul.driver.github.GithubDriver.getConnection',
1994 getGithubConnection))
1995
James E. Blaire511d2f2016-12-08 15:22:26 -08001996 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001997 # TODO(jhesketh): This should come from lib.connections for better
1998 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001999 # Register connections from the config
2000 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002001
Joshua Hesketh352264b2015-08-11 23:42:08 +10002002 def FakeSMTPFactory(*args, **kw):
2003 args = [self.smtp_messages] + list(args)
2004 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002005
Joshua Hesketh352264b2015-08-11 23:42:08 +10002006 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002007
James E. Blaire511d2f2016-12-08 15:22:26 -08002008 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002009 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002010 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002011
James E. Blair83005782015-12-11 14:46:03 -08002012 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002013 # This creates the per-test configuration object. It can be
2014 # overriden by subclasses, but should not need to be since it
2015 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002016 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002017 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002018
James E. Blair39840362017-06-23 20:34:02 +01002019 sections = ['zuul', 'scheduler', 'executor', 'merger']
2020 for section in sections:
2021 if not self.config.has_section(section):
2022 self.config.add_section(section)
2023
James E. Blair06cc3922017-04-19 10:08:10 -07002024 if not self.setupSimpleLayout():
2025 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002026 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002027 self.tenant_config_file)
2028 git_path = os.path.join(
2029 os.path.dirname(
2030 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2031 'git')
2032 if os.path.exists(git_path):
2033 for reponame in os.listdir(git_path):
2034 project = reponame.replace('_', '/')
2035 self.copyDirToRepo(project,
2036 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002037 # Make test_root persist after ansible run for .flag test
2038 self.config.set('executor', 'trusted_rw_dirs', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002039 self.setupAllProjectKeys()
2040
James E. Blair06cc3922017-04-19 10:08:10 -07002041 def setupSimpleLayout(self):
2042 # If the test method has been decorated with a simple_layout,
2043 # use that instead of the class tenant_config_file. Set up a
2044 # single config-project with the specified layout, and
2045 # initialize repos for all of the 'project' entries which
2046 # appear in the layout.
2047 test_name = self.id().split('.')[-1]
2048 test = getattr(self, test_name)
2049 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002050 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002051 else:
2052 return False
2053
James E. Blairb70e55a2017-04-19 12:57:02 -07002054 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002055 path = os.path.join(FIXTURE_DIR, path)
2056 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002057 data = f.read()
2058 layout = yaml.safe_load(data)
2059 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002060 untrusted_projects = []
2061 for item in layout:
2062 if 'project' in item:
2063 name = item['project']['name']
2064 untrusted_projects.append(name)
2065 self.init_repo(name)
2066 self.addCommitToRepo(name, 'initial commit',
2067 files={'README': ''},
2068 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002069 if 'job' in item:
2070 jobname = item['job']['name']
2071 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002072
2073 root = os.path.join(self.test_root, "config")
2074 if not os.path.exists(root):
2075 os.makedirs(root)
2076 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2077 config = [{'tenant':
2078 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002079 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002080 {'config-projects': ['common-config'],
2081 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002082 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002083 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002084 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002085 os.path.join(FIXTURE_DIR, f.name))
2086
2087 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002088 self.addCommitToRepo('common-config', 'add content from fixture',
2089 files, branch='master', tag='init')
2090
2091 return True
2092
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002093 def setupAllProjectKeys(self):
2094 if self.create_project_keys:
2095 return
2096
James E. Blair39840362017-06-23 20:34:02 +01002097 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002098 with open(os.path.join(FIXTURE_DIR, path)) as f:
2099 tenant_config = yaml.safe_load(f.read())
2100 for tenant in tenant_config:
2101 sources = tenant['tenant']['source']
2102 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002103 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002104 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002105 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002106 self.setupProjectKeys(source, project)
2107
2108 def setupProjectKeys(self, source, project):
2109 # Make sure we set up an RSA key for the project so that we
2110 # don't spend time generating one:
2111
2112 key_root = os.path.join(self.state_root, 'keys')
2113 if not os.path.isdir(key_root):
2114 os.mkdir(key_root, 0o700)
2115 private_key_file = os.path.join(key_root, source, project + '.pem')
2116 private_key_dir = os.path.dirname(private_key_file)
2117 self.log.debug("Installing test keys for project %s at %s" % (
2118 project, private_key_file))
2119 if not os.path.isdir(private_key_dir):
2120 os.makedirs(private_key_dir)
2121 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2122 with open(private_key_file, 'w') as o:
2123 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002124
James E. Blair498059b2016-12-20 13:50:13 -08002125 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002126 self.zk_chroot_fixture = self.useFixture(
2127 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002128 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002129 self.zk_chroot_fixture.zookeeper_host,
2130 self.zk_chroot_fixture.zookeeper_port,
2131 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002132
James E. Blair96c6bf82016-01-15 16:20:40 -08002133 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002134 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002135
2136 files = {}
2137 for (dirpath, dirnames, filenames) in os.walk(source_path):
2138 for filename in filenames:
2139 test_tree_filepath = os.path.join(dirpath, filename)
2140 common_path = os.path.commonprefix([test_tree_filepath,
2141 source_path])
2142 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2143 with open(test_tree_filepath, 'r') as f:
2144 content = f.read()
2145 files[relative_filepath] = content
2146 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002147 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002148
James E. Blaire18d4602017-01-05 11:17:28 -08002149 def assertNodepoolState(self):
2150 # Make sure that there are no pending requests
2151
2152 requests = self.fake_nodepool.getNodeRequests()
2153 self.assertEqual(len(requests), 0)
2154
2155 nodes = self.fake_nodepool.getNodes()
2156 for node in nodes:
2157 self.assertFalse(node['_lock'], "Node %s is locked" %
2158 (node['_oid'],))
2159
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002160 def assertNoGeneratedKeys(self):
2161 # Make sure that Zuul did not generate any project keys
2162 # (unless it was supposed to).
2163
2164 if self.create_project_keys:
2165 return
2166
2167 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2168 test_key = i.read()
2169
2170 key_root = os.path.join(self.state_root, 'keys')
2171 for root, dirname, files in os.walk(key_root):
2172 for fn in files:
2173 with open(os.path.join(root, fn)) as f:
2174 self.assertEqual(test_key, f.read())
2175
Clark Boylanb640e052014-04-03 16:41:46 -07002176 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002177 self.log.debug("Assert final state")
2178 # Make sure no jobs are running
2179 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002180 # Make sure that git.Repo objects have been garbage collected.
2181 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002182 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002183 gc.collect()
2184 for obj in gc.get_objects():
2185 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002186 self.log.debug("Leaked git repo object: 0x%x %s" %
2187 (id(obj), repr(obj)))
2188 for ref in gc.get_referrers(obj):
2189 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002190 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002191 if repos:
2192 for obj in gc.garbage:
2193 self.log.debug(" Garbage %s" % (repr(obj)))
2194 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002195 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002196 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002197 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002198 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002199 for tenant in self.sched.abide.tenants.values():
2200 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002201 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002202 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002203
2204 def shutdown(self):
2205 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002206 self.executor_server.hold_jobs_in_build = False
2207 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002208 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002209 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002210 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002211 self.sched.stop()
2212 self.sched.join()
2213 self.statsd.stop()
2214 self.statsd.join()
2215 self.webapp.stop()
2216 self.webapp.join()
2217 self.rpc.stop()
2218 self.rpc.join()
2219 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002220 self.fake_nodepool.stop()
2221 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002222 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002223 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002224 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002225 # Further the pydevd threads also need to be whitelisted so debugging
2226 # e.g. in PyCharm is possible without breaking shutdown.
2227 whitelist = ['executor-watchdog',
2228 'pydevd.CommandThread',
2229 'pydevd.Reader',
2230 'pydevd.Writer',
2231 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002232 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002233 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002234 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002235 log_str = ""
2236 for thread_id, stack_frame in sys._current_frames().items():
2237 log_str += "Thread: %s\n" % thread_id
2238 log_str += "".join(traceback.format_stack(stack_frame))
2239 self.log.debug(log_str)
2240 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002241
James E. Blaira002b032017-04-18 10:35:48 -07002242 def assertCleanShutdown(self):
2243 pass
2244
James E. Blairc4ba97a2017-04-19 16:26:24 -07002245 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002246 parts = project.split('/')
2247 path = os.path.join(self.upstream_root, *parts[:-1])
2248 if not os.path.exists(path):
2249 os.makedirs(path)
2250 path = os.path.join(self.upstream_root, project)
2251 repo = git.Repo.init(path)
2252
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002253 with repo.config_writer() as config_writer:
2254 config_writer.set_value('user', 'email', 'user@example.com')
2255 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002256
Clark Boylanb640e052014-04-03 16:41:46 -07002257 repo.index.commit('initial commit')
2258 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002259 if tag:
2260 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002261
James E. Blair97d902e2014-08-21 13:25:56 -07002262 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002263 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002264 repo.git.clean('-x', '-f', '-d')
2265
James E. Blair97d902e2014-08-21 13:25:56 -07002266 def create_branch(self, project, branch):
2267 path = os.path.join(self.upstream_root, project)
2268 repo = git.Repo.init(path)
2269 fn = os.path.join(path, 'README')
2270
2271 branch_head = repo.create_head(branch)
2272 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002273 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002274 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002275 f.close()
2276 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002277 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002278
James E. Blair97d902e2014-08-21 13:25:56 -07002279 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002280 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002281 repo.git.clean('-x', '-f', '-d')
2282
Sachi King9f16d522016-03-16 12:20:45 +11002283 def create_commit(self, project):
2284 path = os.path.join(self.upstream_root, project)
2285 repo = git.Repo(path)
2286 repo.head.reference = repo.heads['master']
2287 file_name = os.path.join(path, 'README')
2288 with open(file_name, 'a') as f:
2289 f.write('creating fake commit\n')
2290 repo.index.add([file_name])
2291 commit = repo.index.commit('Creating a fake commit')
2292 return commit.hexsha
2293
James E. Blairf4a5f022017-04-18 14:01:10 -07002294 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002295 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002296 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002297 while len(self.builds):
2298 self.release(self.builds[0])
2299 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002300 i += 1
2301 if count is not None and i >= count:
2302 break
James E. Blairb8c16472015-05-05 14:55:26 -07002303
Clark Boylanb640e052014-04-03 16:41:46 -07002304 def release(self, job):
2305 if isinstance(job, FakeBuild):
2306 job.release()
2307 else:
2308 job.waiting = False
2309 self.log.debug("Queued job %s released" % job.unique)
2310 self.gearman_server.wakeConnections()
2311
2312 def getParameter(self, job, name):
2313 if isinstance(job, FakeBuild):
2314 return job.parameters[name]
2315 else:
2316 parameters = json.loads(job.arguments)
2317 return parameters[name]
2318
Clark Boylanb640e052014-04-03 16:41:46 -07002319 def haveAllBuildsReported(self):
2320 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002321 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002322 return False
2323 # Find out if every build that the worker has completed has been
2324 # reported back to Zuul. If it hasn't then that means a Gearman
2325 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002326 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002327 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002328 if not zbuild:
2329 # It has already been reported
2330 continue
2331 # It hasn't been reported yet.
2332 return False
2333 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002334 worker = self.executor_server.executor_worker
2335 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002336 if connection.state == 'GRAB_WAIT':
2337 return False
2338 return True
2339
2340 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002341 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002342 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002343 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002344 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002345 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002346 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002347 for j in conn.related_jobs.values():
2348 if j.unique == build.uuid:
2349 client_job = j
2350 break
2351 if not client_job:
2352 self.log.debug("%s is not known to the gearman client" %
2353 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002354 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002355 if not client_job.handle:
2356 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002357 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002358 server_job = self.gearman_server.jobs.get(client_job.handle)
2359 if not server_job:
2360 self.log.debug("%s is not known to the gearman server" %
2361 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002362 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002363 if not hasattr(server_job, 'waiting'):
2364 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002365 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002366 if server_job.waiting:
2367 continue
James E. Blair17302972016-08-10 16:11:42 -07002368 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002369 self.log.debug("%s has not reported start" % build)
2370 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002371 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002372 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002373 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002374 if worker_build:
2375 if worker_build.isWaiting():
2376 continue
2377 else:
2378 self.log.debug("%s is running" % worker_build)
2379 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002380 else:
James E. Blair962220f2016-08-03 11:22:38 -07002381 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002382 return False
James E. Blaira002b032017-04-18 10:35:48 -07002383 for (build_uuid, job_worker) in \
2384 self.executor_server.job_workers.items():
2385 if build_uuid not in seen_builds:
2386 self.log.debug("%s is not finalized" % build_uuid)
2387 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002388 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002389
James E. Blairdce6cea2016-12-20 16:45:32 -08002390 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002391 if self.fake_nodepool.paused:
2392 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002393 if self.sched.nodepool.requests:
2394 return False
2395 return True
2396
Jan Hruban6b71aff2015-10-22 16:58:08 +02002397 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002398 for event_queue in self.event_queues:
2399 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002400
2401 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002402 for event_queue in self.event_queues:
2403 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002404
Clark Boylanb640e052014-04-03 16:41:46 -07002405 def waitUntilSettled(self):
2406 self.log.debug("Waiting until settled...")
2407 start = time.time()
2408 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002409 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002410 self.log.error("Timeout waiting for Zuul to settle")
2411 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002412 for event_queue in self.event_queues:
2413 self.log.error(" %s: %s" %
2414 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002415 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002416 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002417 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002418 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002419 self.log.error("All requests completed: %s" %
2420 (self.areAllNodeRequestsComplete(),))
2421 self.log.error("Merge client jobs: %s" %
2422 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002423 raise Exception("Timeout waiting for Zuul to settle")
2424 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002425
Paul Belanger174a8272017-03-14 13:20:10 -04002426 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002427 # have all build states propogated to zuul?
2428 if self.haveAllBuildsReported():
2429 # Join ensures that the queue is empty _and_ events have been
2430 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002431 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002432 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002433 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002434 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002435 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002436 self.areAllNodeRequestsComplete() and
2437 all(self.eventQueuesEmpty())):
2438 # The queue empty check is placed at the end to
2439 # ensure that if a component adds an event between
2440 # when locked the run handler and checked that the
2441 # components were stable, we don't erroneously
2442 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002443 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002444 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002445 self.log.debug("...settled.")
2446 return
2447 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002448 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002449 self.sched.wake_event.wait(0.1)
2450
2451 def countJobResults(self, jobs, result):
2452 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002453 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002454
Monty Taylor0d926122017-05-24 08:07:56 -05002455 def getBuildByName(self, name):
2456 for build in self.builds:
2457 if build.name == name:
2458 return build
2459 raise Exception("Unable to find build %s" % name)
2460
James E. Blair96c6bf82016-01-15 16:20:40 -08002461 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002462 for job in self.history:
2463 if (job.name == name and
2464 (project is None or
2465 job.parameters['ZUUL_PROJECT'] == project)):
2466 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002467 raise Exception("Unable to find job %s in history" % name)
2468
2469 def assertEmptyQueues(self):
2470 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002471 for tenant in self.sched.abide.tenants.values():
2472 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002473 for pipeline_queue in pipeline.queues:
2474 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002475 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002476 pipeline.name, pipeline_queue.name,
2477 pipeline_queue.queue))
2478 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002479 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002480
2481 def assertReportedStat(self, key, value=None, kind=None):
2482 start = time.time()
2483 while time.time() < (start + 5):
2484 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002485 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002486 if key == k:
2487 if value is None and kind is None:
2488 return
2489 elif value:
2490 if value == v:
2491 return
2492 elif kind:
2493 if v.endswith('|' + kind):
2494 return
2495 time.sleep(0.1)
2496
Clark Boylanb640e052014-04-03 16:41:46 -07002497 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002498
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002499 def assertBuilds(self, builds):
2500 """Assert that the running builds are as described.
2501
2502 The list of running builds is examined and must match exactly
2503 the list of builds described by the input.
2504
2505 :arg list builds: A list of dictionaries. Each item in the
2506 list must match the corresponding build in the build
2507 history, and each element of the dictionary must match the
2508 corresponding attribute of the build.
2509
2510 """
James E. Blair3158e282016-08-19 09:34:11 -07002511 try:
2512 self.assertEqual(len(self.builds), len(builds))
2513 for i, d in enumerate(builds):
2514 for k, v in d.items():
2515 self.assertEqual(
2516 getattr(self.builds[i], k), v,
2517 "Element %i in builds does not match" % (i,))
2518 except Exception:
2519 for build in self.builds:
2520 self.log.error("Running build: %s" % build)
2521 else:
2522 self.log.error("No running builds")
2523 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002524
James E. Blairb536ecc2016-08-31 10:11:42 -07002525 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002526 """Assert that the completed builds are as described.
2527
2528 The list of completed builds is examined and must match
2529 exactly the list of builds described by the input.
2530
2531 :arg list history: A list of dictionaries. Each item in the
2532 list must match the corresponding build in the build
2533 history, and each element of the dictionary must match the
2534 corresponding attribute of the build.
2535
James E. Blairb536ecc2016-08-31 10:11:42 -07002536 :arg bool ordered: If true, the history must match the order
2537 supplied, if false, the builds are permitted to have
2538 arrived in any order.
2539
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002540 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002541 def matches(history_item, item):
2542 for k, v in item.items():
2543 if getattr(history_item, k) != v:
2544 return False
2545 return True
James E. Blair3158e282016-08-19 09:34:11 -07002546 try:
2547 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002548 if ordered:
2549 for i, d in enumerate(history):
2550 if not matches(self.history[i], d):
2551 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002552 "Element %i in history does not match %s" %
2553 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002554 else:
2555 unseen = self.history[:]
2556 for i, d in enumerate(history):
2557 found = False
2558 for unseen_item in unseen:
2559 if matches(unseen_item, d):
2560 found = True
2561 unseen.remove(unseen_item)
2562 break
2563 if not found:
2564 raise Exception("No match found for element %i "
2565 "in history" % (i,))
2566 if unseen:
2567 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002568 except Exception:
2569 for build in self.history:
2570 self.log.error("Completed build: %s" % build)
2571 else:
2572 self.log.error("No completed builds")
2573 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002574
James E. Blair6ac368c2016-12-22 18:07:20 -08002575 def printHistory(self):
2576 """Log the build history.
2577
2578 This can be useful during tests to summarize what jobs have
2579 completed.
2580
2581 """
2582 self.log.debug("Build history:")
2583 for build in self.history:
2584 self.log.debug(build)
2585
James E. Blair59fdbac2015-12-07 17:08:06 -08002586 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002587 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2588
James E. Blair9ea70072017-04-19 16:05:30 -07002589 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002590 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002591 if not os.path.exists(root):
2592 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002593 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2594 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002595- tenant:
2596 name: openstack
2597 source:
2598 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002599 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002600 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002601 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002602 - org/project
2603 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002604 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002605 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002606 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002607 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002608 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002609
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002610 def addCommitToRepo(self, project, message, files,
2611 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002612 path = os.path.join(self.upstream_root, project)
2613 repo = git.Repo(path)
2614 repo.head.reference = branch
2615 zuul.merger.merger.reset_repo_to_head(repo)
2616 for fn, content in files.items():
2617 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002618 try:
2619 os.makedirs(os.path.dirname(fn))
2620 except OSError:
2621 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002622 with open(fn, 'w') as f:
2623 f.write(content)
2624 repo.index.add([fn])
2625 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002626 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002627 repo.heads[branch].commit = commit
2628 repo.head.reference = branch
2629 repo.git.clean('-x', '-f', '-d')
2630 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002631 if tag:
2632 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002633 return before
2634
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002635 def commitConfigUpdate(self, project_name, source_name):
2636 """Commit an update to zuul.yaml
2637
2638 This overwrites the zuul.yaml in the specificed project with
2639 the contents specified.
2640
2641 :arg str project_name: The name of the project containing
2642 zuul.yaml (e.g., common-config)
2643
2644 :arg str source_name: The path to the file (underneath the
2645 test fixture directory) whose contents should be used to
2646 replace zuul.yaml.
2647 """
2648
2649 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002650 files = {}
2651 with open(source_path, 'r') as f:
2652 data = f.read()
2653 layout = yaml.safe_load(data)
2654 files['zuul.yaml'] = data
2655 for item in layout:
2656 if 'job' in item:
2657 jobname = item['job']['name']
2658 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002659 before = self.addCommitToRepo(
2660 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002661 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002662 return before
2663
James E. Blair7fc8daa2016-08-08 15:37:15 -07002664 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002665
James E. Blair7fc8daa2016-08-08 15:37:15 -07002666 """Inject a Fake (Gerrit) event.
2667
2668 This method accepts a JSON-encoded event and simulates Zuul
2669 having received it from Gerrit. It could (and should)
2670 eventually apply to any connection type, but is currently only
2671 used with Gerrit connections. The name of the connection is
2672 used to look up the corresponding server, and the event is
2673 simulated as having been received by all Zuul connections
2674 attached to that server. So if two Gerrit connections in Zuul
2675 are connected to the same Gerrit server, and you invoke this
2676 method specifying the name of one of them, the event will be
2677 received by both.
2678
2679 .. note::
2680
2681 "self.fake_gerrit.addEvent" calls should be migrated to
2682 this method.
2683
2684 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002685 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002686 :arg str event: The JSON-encoded event.
2687
2688 """
2689 specified_conn = self.connections.connections[connection]
2690 for conn in self.connections.connections.values():
2691 if (isinstance(conn, specified_conn.__class__) and
2692 specified_conn.server == conn.server):
2693 conn.addEvent(event)
2694
James E. Blaird8af5422017-05-24 13:59:40 -07002695 def getUpstreamRepos(self, projects):
2696 """Return upstream git repo objects for the listed projects
2697
2698 :arg list projects: A list of strings, each the canonical name
2699 of a project.
2700
2701 :returns: A dictionary of {name: repo} for every listed
2702 project.
2703 :rtype: dict
2704
2705 """
2706
2707 repos = {}
2708 for project in projects:
2709 # FIXME(jeblair): the upstream root does not yet have a
2710 # hostname component; that needs to be added, and this
2711 # line removed:
2712 tmp_project_name = '/'.join(project.split('/')[1:])
2713 path = os.path.join(self.upstream_root, tmp_project_name)
2714 repo = git.Repo(path)
2715 repos[project] = repo
2716 return repos
2717
James E. Blair3f876d52016-07-22 13:07:14 -07002718
2719class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002720 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002721 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002722
Joshua Heskethd78b4482015-09-14 16:56:34 -06002723
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002724class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002725 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002726 use_ssl = True
2727
2728
Joshua Heskethd78b4482015-09-14 16:56:34 -06002729class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002730 def setup_config(self):
2731 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002732 for section_name in self.config.sections():
2733 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2734 section_name, re.I)
2735 if not con_match:
2736 continue
2737
2738 if self.config.get(section_name, 'driver') == 'sql':
2739 f = MySQLSchemaFixture()
2740 self.useFixture(f)
2741 if (self.config.get(section_name, 'dburi') ==
2742 '$MYSQL_FIXTURE_DBURI$'):
2743 self.config.set(section_name, 'dburi', f.dburi)