teuthology 1.1.0__py3-none-any.whl → 1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scripts/describe.py +1 -0
- scripts/dispatcher.py +55 -26
- scripts/exporter.py +18 -0
- scripts/lock.py +1 -1
- scripts/node_cleanup.py +58 -0
- scripts/openstack.py +9 -9
- scripts/results.py +12 -11
- scripts/schedule.py +4 -0
- scripts/suite.py +57 -16
- scripts/supervisor.py +44 -0
- scripts/update_inventory.py +10 -4
- teuthology/__init__.py +24 -26
- teuthology/beanstalk.py +4 -3
- teuthology/config.py +16 -6
- teuthology/contextutil.py +18 -14
- teuthology/describe_tests.py +25 -18
- teuthology/dispatcher/__init__.py +210 -35
- teuthology/dispatcher/supervisor.py +140 -58
- teuthology/exceptions.py +43 -0
- teuthology/exporter.py +347 -0
- teuthology/kill.py +76 -81
- teuthology/lock/cli.py +3 -3
- teuthology/lock/ops.py +135 -61
- teuthology/lock/query.py +61 -44
- teuthology/ls.py +1 -1
- teuthology/misc.py +61 -75
- teuthology/nuke/__init__.py +12 -353
- teuthology/openstack/__init__.py +4 -3
- teuthology/openstack/openstack-centos-7.0-user-data.txt +1 -1
- teuthology/openstack/openstack-centos-7.1-user-data.txt +1 -1
- teuthology/openstack/openstack-centos-7.2-user-data.txt +1 -1
- teuthology/openstack/openstack-debian-8.0-user-data.txt +1 -1
- teuthology/openstack/openstack-opensuse-42.1-user-data.txt +1 -1
- teuthology/openstack/openstack-teuthology.cron +0 -1
- teuthology/orchestra/cluster.py +49 -7
- teuthology/orchestra/connection.py +17 -4
- teuthology/orchestra/console.py +111 -50
- teuthology/orchestra/daemon/cephadmunit.py +15 -2
- teuthology/orchestra/daemon/state.py +8 -1
- teuthology/orchestra/daemon/systemd.py +4 -4
- teuthology/orchestra/opsys.py +30 -11
- teuthology/orchestra/remote.py +405 -338
- teuthology/orchestra/run.py +3 -3
- teuthology/packaging.py +19 -16
- teuthology/provision/__init__.py +30 -10
- teuthology/provision/cloud/openstack.py +12 -6
- teuthology/provision/cloud/util.py +1 -2
- teuthology/provision/downburst.py +4 -3
- teuthology/provision/fog.py +68 -20
- teuthology/provision/openstack.py +5 -4
- teuthology/provision/pelagos.py +1 -1
- teuthology/repo_utils.py +43 -13
- teuthology/report.py +57 -35
- teuthology/results.py +5 -3
- teuthology/run.py +13 -14
- teuthology/run_tasks.py +27 -43
- teuthology/schedule.py +4 -3
- teuthology/scrape.py +28 -22
- teuthology/suite/__init__.py +74 -45
- teuthology/suite/build_matrix.py +34 -24
- teuthology/suite/fragment-merge.lua +105 -0
- teuthology/suite/matrix.py +31 -2
- teuthology/suite/merge.py +175 -0
- teuthology/suite/placeholder.py +6 -9
- teuthology/suite/run.py +175 -100
- teuthology/suite/util.py +64 -218
- teuthology/task/__init__.py +1 -1
- teuthology/task/ansible.py +101 -32
- teuthology/task/buildpackages.py +2 -2
- teuthology/task/ceph_ansible.py +13 -6
- teuthology/task/cephmetrics.py +2 -1
- teuthology/task/clock.py +33 -14
- teuthology/task/exec.py +18 -0
- teuthology/task/hadoop.py +2 -2
- teuthology/task/install/__init__.py +29 -7
- teuthology/task/install/bin/adjust-ulimits +16 -0
- teuthology/task/install/bin/daemon-helper +114 -0
- teuthology/task/install/bin/stdin-killer +263 -0
- teuthology/task/install/deb.py +1 -1
- teuthology/task/install/rpm.py +17 -5
- teuthology/task/install/util.py +3 -3
- teuthology/task/internal/__init__.py +41 -10
- teuthology/task/internal/edit_sudoers.sh +10 -0
- teuthology/task/internal/lock_machines.py +2 -9
- teuthology/task/internal/redhat.py +31 -1
- teuthology/task/internal/syslog.py +31 -8
- teuthology/task/kernel.py +152 -145
- teuthology/task/lockfile.py +1 -1
- teuthology/task/mpi.py +10 -10
- teuthology/task/pcp.py +1 -1
- teuthology/task/selinux.py +16 -8
- teuthology/task/ssh_keys.py +4 -4
- teuthology/timer.py +3 -3
- teuthology/util/loggerfile.py +19 -0
- teuthology/util/scanner.py +159 -0
- teuthology/util/sentry.py +52 -0
- teuthology/util/time.py +52 -0
- teuthology-1.2.1.data/scripts/adjust-ulimits +16 -0
- teuthology-1.2.1.data/scripts/daemon-helper +114 -0
- teuthology-1.2.1.data/scripts/stdin-killer +263 -0
- teuthology-1.2.1.dist-info/METADATA +88 -0
- teuthology-1.2.1.dist-info/RECORD +168 -0
- {teuthology-1.1.0.dist-info → teuthology-1.2.1.dist-info}/WHEEL +1 -1
- {teuthology-1.1.0.dist-info → teuthology-1.2.1.dist-info}/entry_points.txt +3 -2
- scripts/nuke.py +0 -47
- scripts/worker.py +0 -37
- teuthology/lock/test/__init__.py +0 -0
- teuthology/lock/test/test_lock.py +0 -7
- teuthology/nuke/actions.py +0 -456
- teuthology/openstack/test/__init__.py +0 -0
- teuthology/openstack/test/openstack-integration.py +0 -286
- teuthology/openstack/test/test_config.py +0 -35
- teuthology/openstack/test/test_openstack.py +0 -1695
- teuthology/orchestra/test/__init__.py +0 -0
- teuthology/orchestra/test/integration/__init__.py +0 -0
- teuthology/orchestra/test/integration/test_integration.py +0 -94
- teuthology/orchestra/test/test_cluster.py +0 -240
- teuthology/orchestra/test/test_connection.py +0 -106
- teuthology/orchestra/test/test_console.py +0 -217
- teuthology/orchestra/test/test_opsys.py +0 -404
- teuthology/orchestra/test/test_remote.py +0 -185
- teuthology/orchestra/test/test_run.py +0 -286
- teuthology/orchestra/test/test_systemd.py +0 -54
- teuthology/orchestra/test/util.py +0 -12
- teuthology/task/tests/__init__.py +0 -110
- teuthology/task/tests/test_locking.py +0 -25
- teuthology/task/tests/test_run.py +0 -40
- teuthology/test/__init__.py +0 -0
- teuthology/test/fake_archive.py +0 -107
- teuthology/test/fake_fs.py +0 -92
- teuthology/test/integration/__init__.py +0 -0
- teuthology/test/integration/test_suite.py +0 -86
- teuthology/test/task/__init__.py +0 -205
- teuthology/test/task/test_ansible.py +0 -624
- teuthology/test/task/test_ceph_ansible.py +0 -176
- teuthology/test/task/test_console_log.py +0 -88
- teuthology/test/task/test_install.py +0 -337
- teuthology/test/task/test_internal.py +0 -57
- teuthology/test/task/test_kernel.py +0 -243
- teuthology/test/task/test_pcp.py +0 -379
- teuthology/test/task/test_selinux.py +0 -35
- teuthology/test/test_config.py +0 -189
- teuthology/test/test_contextutil.py +0 -68
- teuthology/test/test_describe_tests.py +0 -316
- teuthology/test/test_email_sleep_before_teardown.py +0 -81
- teuthology/test/test_exit.py +0 -97
- teuthology/test/test_get_distro.py +0 -47
- teuthology/test/test_get_distro_version.py +0 -47
- teuthology/test/test_get_multi_machine_types.py +0 -27
- teuthology/test/test_job_status.py +0 -60
- teuthology/test/test_ls.py +0 -48
- teuthology/test/test_misc.py +0 -391
- teuthology/test/test_nuke.py +0 -290
- teuthology/test/test_packaging.py +0 -763
- teuthology/test/test_parallel.py +0 -28
- teuthology/test/test_repo_utils.py +0 -225
- teuthology/test/test_report.py +0 -77
- teuthology/test/test_results.py +0 -155
- teuthology/test/test_run.py +0 -239
- teuthology/test/test_safepath.py +0 -55
- teuthology/test/test_schedule.py +0 -45
- teuthology/test/test_scrape.py +0 -167
- teuthology/test/test_timer.py +0 -80
- teuthology/test/test_vps_os_vers_parameter_checking.py +0 -84
- teuthology/test/test_worker.py +0 -303
- teuthology/worker.py +0 -354
- teuthology-1.1.0.dist-info/METADATA +0 -76
- teuthology-1.1.0.dist-info/RECORD +0 -213
- {teuthology-1.1.0.dist-info → teuthology-1.2.1.dist-info}/LICENSE +0 -0
- {teuthology-1.1.0.dist-info → teuthology-1.2.1.dist-info}/top_level.txt +0 -0
teuthology/suite/util.py
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
import copy
|
2
|
+
import functools
|
2
3
|
import logging
|
3
4
|
import os
|
4
5
|
import requests
|
5
6
|
import smtplib
|
6
7
|
import socket
|
7
|
-
import
|
8
|
+
from subprocess import Popen, PIPE, DEVNULL
|
8
9
|
import sys
|
9
10
|
|
10
11
|
from email.mime.text import MIMEText
|
@@ -17,26 +18,25 @@ from teuthology.config import config
|
|
17
18
|
from teuthology.exceptions import BranchNotFoundError, ScheduleFailError
|
18
19
|
from teuthology.misc import deep_merge
|
19
20
|
from teuthology.repo_utils import fetch_qa_suite, fetch_teuthology
|
20
|
-
from teuthology.orchestra.opsys import OS
|
21
|
-
from teuthology.packaging import get_builder_project
|
21
|
+
from teuthology.orchestra.opsys import OS, DEFAULT_OS_VERSION
|
22
|
+
from teuthology.packaging import get_builder_project, VersionNotFoundError
|
22
23
|
from teuthology.repo_utils import build_git_url
|
23
|
-
from teuthology.suite.build_matrix import combine_path
|
24
24
|
from teuthology.task.install import get_flavor
|
25
25
|
|
26
26
|
log = logging.getLogger(__name__)
|
27
27
|
|
28
|
-
CONTAINER_DISTRO = 'centos/
|
29
|
-
CONTAINER_FLAVOR = '
|
28
|
+
CONTAINER_DISTRO = 'centos/9' # the one to check for build_complete
|
29
|
+
CONTAINER_FLAVOR = 'default'
|
30
30
|
|
31
31
|
|
32
|
-
def fetch_repos(branch, test_name):
|
32
|
+
def fetch_repos(branch, test_name, dry_run):
|
33
33
|
"""
|
34
34
|
Fetch the suite repo (and also the teuthology repo) so that we can use it
|
35
35
|
to build jobs. Repos are stored in ~/src/.
|
36
36
|
|
37
37
|
The reason the teuthology repo is also fetched is that currently we use
|
38
38
|
subprocess to call teuthology-schedule to schedule jobs so we need to make
|
39
|
-
sure it is up-to-date. For that reason we always fetch the
|
39
|
+
sure it is up-to-date. For that reason we always fetch the main branch
|
40
40
|
for test scheduling, regardless of what teuthology branch is requested for
|
41
41
|
testing.
|
42
42
|
|
@@ -46,22 +46,23 @@ def fetch_repos(branch, test_name):
|
|
46
46
|
# When a user is scheduling a test run from their own copy of
|
47
47
|
# teuthology, let's not wreak havoc on it.
|
48
48
|
if config.automated_scheduling:
|
49
|
-
# We use teuthology's
|
49
|
+
# We use teuthology's main branch in all cases right now
|
50
50
|
if config.teuthology_path is None:
|
51
|
-
fetch_teuthology('
|
51
|
+
fetch_teuthology('main')
|
52
52
|
suite_repo_path = fetch_qa_suite(branch)
|
53
53
|
except BranchNotFoundError as exc:
|
54
|
-
schedule_fail(message=str(exc), name=test_name)
|
54
|
+
schedule_fail(message=str(exc), name=test_name, dry_run=dry_run)
|
55
55
|
return suite_repo_path
|
56
56
|
|
57
57
|
|
58
|
-
def schedule_fail(message, name=''):
|
58
|
+
def schedule_fail(message, name='', dry_run=None):
|
59
59
|
"""
|
60
60
|
If an email address has been specified anywhere, send an alert there. Then
|
61
61
|
raise a ScheduleFailError.
|
62
|
+
Don't send the mail if --dry-run has been passed.
|
62
63
|
"""
|
63
64
|
email = config.results_email
|
64
|
-
if email:
|
65
|
+
if email and not dry_run:
|
65
66
|
subject = "Failed to schedule {name}".format(name=name)
|
66
67
|
msg = MIMEText(message)
|
67
68
|
msg['Subject'] = subject
|
@@ -100,7 +101,7 @@ def get_gitbuilder_hash(project=None, branch=None, flavor=None,
|
|
100
101
|
# Alternate method for github-hosted projects - left here for informational
|
101
102
|
# purposes
|
102
103
|
# resp = requests.get(
|
103
|
-
# 'https://api.github.com/repos/ceph/ceph/git/refs/heads/
|
104
|
+
# 'https://api.github.com/repos/ceph/ceph/git/refs/heads/main')
|
104
105
|
# hash = .json()['object']['sha']
|
105
106
|
(arch, release, _os) = get_distro_defaults(distro, machine_type)
|
106
107
|
if distro is None:
|
@@ -122,38 +123,15 @@ def get_distro_defaults(distro, machine_type):
|
|
122
123
|
"""
|
123
124
|
Given a distro (e.g. 'ubuntu') and machine type, return:
|
124
125
|
(arch, release, pkg_type)
|
125
|
-
|
126
|
-
This is used to default to:
|
127
|
-
('x86_64', 'trusty', 'deb') when passed 'ubuntu' and 'plana'
|
128
|
-
('armv7l', 'saucy', 'deb') when passed 'ubuntu' and 'saya'
|
129
|
-
('x86_64', 'wheezy', 'deb') when passed 'debian'
|
130
|
-
('x86_64', 'fedora20', 'rpm') when passed 'fedora'
|
131
|
-
And ('x86_64', 'centos7', 'rpm') when passed anything else
|
132
126
|
"""
|
133
127
|
arch = 'x86_64'
|
134
|
-
if distro in (None, 'None'):
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
os_version = '7'
|
140
|
-
elif distro == 'ubuntu':
|
141
|
-
os_type = distro
|
142
|
-
if machine_type == 'saya':
|
143
|
-
os_version = '13.10'
|
144
|
-
arch = 'armv7l'
|
145
|
-
else:
|
146
|
-
os_version = '16.04'
|
147
|
-
elif distro == 'debian':
|
148
|
-
os_type = distro
|
149
|
-
os_version = '7'
|
150
|
-
elif distro == 'fedora':
|
151
|
-
os_type = distro
|
152
|
-
os_version = '20'
|
153
|
-
elif distro == 'opensuse':
|
128
|
+
if distro in (None, 'None', 'rhel'):
|
129
|
+
distro = 'centos'
|
130
|
+
|
131
|
+
try:
|
132
|
+
os_version = DEFAULT_OS_VERSION[distro]
|
154
133
|
os_type = distro
|
155
|
-
|
156
|
-
else:
|
134
|
+
except IndexError:
|
157
135
|
raise ValueError("Invalid distro value passed: %s", distro)
|
158
136
|
_os = OS(name=os_type, version=os_version)
|
159
137
|
release = get_builder_project()._get_distro(
|
@@ -161,15 +139,6 @@ def get_distro_defaults(distro, machine_type):
|
|
161
139
|
_os.version,
|
162
140
|
_os.codename,
|
163
141
|
)
|
164
|
-
template = "Defaults for machine_type {mtype} distro {distro}: " \
|
165
|
-
"arch={arch}, release={release}, pkg_type={pkg}"
|
166
|
-
log.debug(template.format(
|
167
|
-
mtype=machine_type,
|
168
|
-
distro=_os.name,
|
169
|
-
arch=arch,
|
170
|
-
release=release,
|
171
|
-
pkg=_os.package_type)
|
172
|
-
)
|
173
142
|
return (
|
174
143
|
arch,
|
175
144
|
release,
|
@@ -187,7 +156,7 @@ def git_ls_remote(project_or_url, branch, project_owner='ceph'):
|
|
187
156
|
name is passed; not when a URL is passed
|
188
157
|
:returns: The sha1 if found; else None
|
189
158
|
"""
|
190
|
-
if '://' in project_or_url:
|
159
|
+
if '://' in project_or_url or project_or_url.startswith('git@'):
|
191
160
|
url = project_or_url
|
192
161
|
else:
|
193
162
|
url = build_git_url(project_or_url, project_owner)
|
@@ -254,7 +223,8 @@ def get_branch_info(project, branch, project_owner='ceph'):
|
|
254
223
|
return resp.json()
|
255
224
|
|
256
225
|
|
257
|
-
|
226
|
+
@functools.lru_cache()
|
227
|
+
def package_version_for_hash(hash, flavor='default', distro='rhel',
|
258
228
|
distro_version='8.0', machine_type='smithi'):
|
259
229
|
"""
|
260
230
|
Does what it says on the tin. Uses gitbuilder repos.
|
@@ -267,7 +237,7 @@ def package_version_for_hash(hash, kernel_flavor='basic', distro='rhel',
|
|
267
237
|
bp = get_builder_project()(
|
268
238
|
'ceph',
|
269
239
|
dict(
|
270
|
-
flavor=
|
240
|
+
flavor=flavor,
|
271
241
|
os_type=distro,
|
272
242
|
os_version=distro_version,
|
273
243
|
arch=arch,
|
@@ -275,13 +245,15 @@ def package_version_for_hash(hash, kernel_flavor='basic', distro='rhel',
|
|
275
245
|
),
|
276
246
|
)
|
277
247
|
|
278
|
-
if bp.distro == CONTAINER_DISTRO and bp.flavor == CONTAINER_FLAVOR
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
return None
|
248
|
+
if (bp.distro == CONTAINER_DISTRO and bp.flavor == CONTAINER_FLAVOR and
|
249
|
+
not bp.build_complete):
|
250
|
+
log.info("Container build incomplete")
|
251
|
+
return None
|
283
252
|
|
284
|
-
|
253
|
+
try:
|
254
|
+
return bp.version
|
255
|
+
except VersionNotFoundError:
|
256
|
+
return None
|
285
257
|
|
286
258
|
|
287
259
|
def get_arch(machine_type):
|
@@ -291,9 +263,9 @@ def get_arch(machine_type):
|
|
291
263
|
|
292
264
|
:returns: A string or None
|
293
265
|
"""
|
294
|
-
result = teuthology.lock.query.list_locks(machine_type=machine_type, count=1)
|
266
|
+
result = teuthology.lock.query.list_locks(machine_type=machine_type, count=1, tries=1)
|
295
267
|
if not result:
|
296
|
-
log.
|
268
|
+
log.warning("No machines found with machine_type %s!", machine_type)
|
297
269
|
else:
|
298
270
|
return result[0]['arch']
|
299
271
|
|
@@ -333,102 +305,7 @@ def get_install_task_flavor(job_config):
|
|
333
305
|
return get_flavor(first_install_config)
|
334
306
|
|
335
307
|
|
336
|
-
def
|
337
|
-
package_versions=None):
|
338
|
-
"""
|
339
|
-
Will retrieve the package versions for the given sha1, os_type/version,
|
340
|
-
and flavor from gitbuilder.
|
341
|
-
|
342
|
-
Optionally, a package_versions dict can be provided
|
343
|
-
from previous calls to this function to avoid calling gitbuilder for
|
344
|
-
information we've already retrieved.
|
345
|
-
|
346
|
-
The package_versions dict will be in the following format::
|
347
|
-
|
348
|
-
{
|
349
|
-
"sha1": {
|
350
|
-
"ubuntu": {
|
351
|
-
"14.04": {
|
352
|
-
"basic": "version",
|
353
|
-
}
|
354
|
-
"15.04": {
|
355
|
-
"notcmalloc": "version",
|
356
|
-
}
|
357
|
-
}
|
358
|
-
"rhel": {
|
359
|
-
"basic": "version",
|
360
|
-
}
|
361
|
-
},
|
362
|
-
"another-sha1": {
|
363
|
-
"ubuntu": {
|
364
|
-
"basic": "version",
|
365
|
-
}
|
366
|
-
}
|
367
|
-
}
|
368
|
-
|
369
|
-
:param sha1: The sha1 hash of the ceph version.
|
370
|
-
:param os_type: The distro we want to get packages for, given
|
371
|
-
the ceph sha1. Ex. 'ubuntu', 'rhel', etc.
|
372
|
-
:param os_version: The distro's version, e.g. '14.04', '7.0'
|
373
|
-
:param flavor: Package flavor ('testing', 'notcmalloc', etc.)
|
374
|
-
:param package_versions: Use this optionally to use cached results of
|
375
|
-
previous calls to gitbuilder.
|
376
|
-
:returns: A dict of package versions. Will return versions
|
377
|
-
for all hashes/distros/vers, not just for the given
|
378
|
-
hash/distro/ver.
|
379
|
-
"""
|
380
|
-
if package_versions is None:
|
381
|
-
package_versions = dict()
|
382
|
-
|
383
|
-
os_type = str(os_type)
|
384
|
-
|
385
|
-
os_types = package_versions.get(sha1, dict())
|
386
|
-
os_versions = os_types.get(os_type, dict())
|
387
|
-
flavors = os_versions.get(os_version, dict())
|
388
|
-
if flavor not in flavors:
|
389
|
-
package_version = package_version_for_hash(
|
390
|
-
sha1,
|
391
|
-
flavor,
|
392
|
-
distro=os_type,
|
393
|
-
distro_version=os_version,
|
394
|
-
)
|
395
|
-
flavors[flavor] = package_version
|
396
|
-
os_versions[os_version] = flavors
|
397
|
-
os_types[os_type] = os_versions
|
398
|
-
package_versions[sha1] = os_types
|
399
|
-
|
400
|
-
return package_versions
|
401
|
-
|
402
|
-
|
403
|
-
def has_packages_for_distro(sha1, os_type, os_version, flavor,
|
404
|
-
package_versions=None):
|
405
|
-
"""
|
406
|
-
Checks to see if gitbuilder has packages for the given sha1, os_type and
|
407
|
-
kernel_flavor.
|
408
|
-
|
409
|
-
See above for package_versions description.
|
410
|
-
|
411
|
-
:param sha1: The sha1 hash of the ceph version.
|
412
|
-
:param os_type: The distro we want to get packages for, given
|
413
|
-
the ceph sha1. Ex. 'ubuntu', 'rhel', etc.
|
414
|
-
:param kernel_flavor: The kernel flavor
|
415
|
-
:param package_versions: Use this optionally to use cached results of
|
416
|
-
previous calls to gitbuilder.
|
417
|
-
:returns: True, if packages are found. False otherwise.
|
418
|
-
"""
|
419
|
-
os_type = str(os_type)
|
420
|
-
if package_versions is None:
|
421
|
-
package_versions = get_package_versions(
|
422
|
-
sha1, os_type, os_version, flavor)
|
423
|
-
|
424
|
-
flavors = package_versions.get(sha1, dict()).get(
|
425
|
-
os_type, dict()).get(
|
426
|
-
os_version, dict())
|
427
|
-
# we want to return a boolean here, not the actual package versions
|
428
|
-
return bool(flavors.get(flavor, None))
|
429
|
-
|
430
|
-
|
431
|
-
def teuthology_schedule(args, verbose, dry_run, log_prefix=''):
|
308
|
+
def teuthology_schedule(args, verbose, dry_run, log_prefix='', stdin=None):
|
432
309
|
"""
|
433
310
|
Run teuthology-schedule to schedule individual jobs.
|
434
311
|
|
@@ -451,84 +328,53 @@ def teuthology_schedule(args, verbose, dry_run, log_prefix=''):
|
|
451
328
|
printable_args.append("'%s'" % item)
|
452
329
|
else:
|
453
330
|
printable_args.append(item)
|
454
|
-
log.
|
331
|
+
log.debug('{0} command: {1}'.format(
|
455
332
|
log_prefix,
|
456
333
|
' '.join(printable_args),
|
457
334
|
))
|
458
335
|
if not dry_run or (dry_run and verbose > 1):
|
459
|
-
|
460
|
-
|
336
|
+
astdin = DEVNULL if stdin is None else PIPE
|
337
|
+
p = Popen(args, stdin=astdin)
|
338
|
+
if stdin is not None:
|
339
|
+
p.communicate(input=stdin.encode('utf-8'))
|
340
|
+
else:
|
341
|
+
p.communicate()
|
461
342
|
|
462
|
-
def
|
343
|
+
def find_git_parents(project: str, sha1: str, count=1):
|
463
344
|
|
464
345
|
base_url = config.githelper_base_url
|
465
346
|
if not base_url:
|
466
347
|
log.warning('githelper_base_url not set, --newest disabled')
|
467
|
-
return
|
348
|
+
return []
|
468
349
|
|
469
|
-
def refresh(
|
470
|
-
url =
|
350
|
+
def refresh():
|
351
|
+
url = f"{base_url}/{project}.git/refresh"
|
352
|
+
log.info(f"Forcing refresh of git mirror: {url}")
|
471
353
|
resp = requests.get(url)
|
472
354
|
if not resp.ok:
|
473
355
|
log.error('git refresh failed for %s: %s',
|
474
356
|
project, resp.content.decode())
|
475
357
|
|
476
358
|
def get_sha1s(project, committish, count):
|
477
|
-
url =
|
478
|
-
|
359
|
+
url = f"{base_url}/{project}.git/history?committish={committish}&count={count}"
|
360
|
+
log.info(f"Looking for parent commits: {url}")
|
479
361
|
resp = requests.get(url)
|
480
362
|
resp.raise_for_status()
|
481
363
|
sha1s = resp.json()['sha1s']
|
482
364
|
if len(sha1s) != count:
|
483
|
-
|
484
|
-
|
485
|
-
|
365
|
+
resp_json = resp.json()
|
366
|
+
err_msg = resp_json.get("error") or resp_json.get("err")
|
367
|
+
log.debug(f"Got {resp.status_code} response: {resp_json}")
|
368
|
+
log.error(f"Can't find {count} parents of {sha1} in {project}: {err_msg}")
|
486
369
|
return sha1s
|
487
370
|
|
488
|
-
#
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
def filter_configs(configs, suite_name=None,
|
499
|
-
filter_in=None,
|
500
|
-
filter_out=None,
|
501
|
-
filter_all=None,
|
502
|
-
filter_fragments=True):
|
503
|
-
"""
|
504
|
-
Returns a generator for pairs of description and fragment paths.
|
505
|
-
|
506
|
-
Usage:
|
507
|
-
|
508
|
-
configs = build_matrix(path, subset, seed)
|
509
|
-
for description, fragments in filter_configs(configs):
|
510
|
-
pass
|
511
|
-
"""
|
512
|
-
for item in configs:
|
513
|
-
fragment_paths = item[1]
|
514
|
-
description = combine_path(suite_name, item[0]) \
|
515
|
-
if suite_name else item[0]
|
516
|
-
base_frag_paths = [strip_fragment_path(x)
|
517
|
-
for x in fragment_paths]
|
518
|
-
def matches(f):
|
519
|
-
if f in description:
|
520
|
-
return True
|
521
|
-
if filter_fragments and \
|
522
|
-
any(f in path for path in base_frag_paths):
|
523
|
-
return True
|
524
|
-
return False
|
525
|
-
if filter_all:
|
526
|
-
if not all(matches(f) for f in filter_all):
|
527
|
-
continue
|
528
|
-
if filter_in:
|
529
|
-
if not any(matches(f) for f in filter_in):
|
530
|
-
continue
|
531
|
-
if filter_out:
|
532
|
-
if any(matches(f) for f in filter_out):
|
533
|
-
continue
|
534
|
-
yield([description, fragment_paths])
|
371
|
+
# index 0 will be the commit whose parents we want to find.
|
372
|
+
# So we will query for count+1, and strip index 0 from the result.
|
373
|
+
sha1s = get_sha1s(project, sha1, count + 1)
|
374
|
+
if not sha1s:
|
375
|
+
log.error("Will try to refresh git mirror and try again")
|
376
|
+
refresh()
|
377
|
+
sha1s = get_sha1s(project, sha1, count + 1)
|
378
|
+
if sha1s:
|
379
|
+
return sha1s[1:]
|
380
|
+
return []
|
teuthology/task/__init__.py
CHANGED
teuthology/task/ansible.py
CHANGED
@@ -1,39 +1,96 @@
|
|
1
1
|
import json
|
2
2
|
import logging
|
3
|
+
import re
|
3
4
|
import requests
|
4
5
|
import os
|
6
|
+
import pathlib
|
5
7
|
import pexpect
|
6
8
|
import yaml
|
7
9
|
import shutil
|
8
10
|
|
9
11
|
from tempfile import mkdtemp, NamedTemporaryFile
|
10
12
|
|
13
|
+
from teuthology import repo_utils
|
11
14
|
from teuthology.config import config as teuth_config
|
12
15
|
from teuthology.exceptions import CommandFailedError, AnsibleFailedError
|
13
16
|
from teuthology.job_status import set_status
|
14
|
-
from teuthology.repo_utils import fetch_repo
|
15
|
-
|
16
17
|
from teuthology.task import Task
|
18
|
+
from teuthology.util.loggerfile import LoggerFile
|
17
19
|
|
18
20
|
log = logging.getLogger(__name__)
|
19
21
|
|
20
22
|
|
21
|
-
class
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
23
|
+
class FailureAnalyzer:
|
24
|
+
def analyze(self, failure_log):
|
25
|
+
failure_obj = yaml.safe_load(failure_log)
|
26
|
+
lines = set()
|
27
|
+
if failure_obj is None:
|
28
|
+
return lines
|
29
|
+
for host_obj in failure_obj.values():
|
30
|
+
if not isinstance(host_obj, dict):
|
31
|
+
continue
|
32
|
+
lines = lines.union(self.analyze_host_record(host_obj))
|
33
|
+
return sorted(lines)
|
34
|
+
|
35
|
+
def analyze_host_record(self, record):
|
36
|
+
lines = set()
|
37
|
+
for result in record.get("results", [record]):
|
38
|
+
cmd = result.get("cmd", "")
|
39
|
+
# When a CPAN task fails, we get _lots_ of stderr_lines, and they
|
40
|
+
# aren't practical to reduce meaningfully. Instead of analyzing lines,
|
41
|
+
# just report the command that failed.
|
42
|
+
if "cpan" in cmd:
|
43
|
+
lines.add(f"CPAN command failed: {cmd}")
|
44
|
+
continue
|
45
|
+
lines_to_analyze = []
|
46
|
+
if "stderr_lines" in result:
|
47
|
+
lines_to_analyze = result["stderr_lines"]
|
48
|
+
elif "msg" in result:
|
49
|
+
lines_to_analyze = result["msg"].split("\n")
|
50
|
+
lines_to_analyze.extend(result.get("err", "").split("\n"))
|
51
|
+
for line in lines_to_analyze:
|
52
|
+
line = self.analyze_line(line.strip())
|
53
|
+
if line:
|
54
|
+
lines.add(line)
|
55
|
+
return list(lines)
|
56
|
+
|
57
|
+
def analyze_line(self, line):
|
58
|
+
if line.startswith("W: ") or line.endswith("?"):
|
59
|
+
return ""
|
60
|
+
drop_phrases = [
|
61
|
+
# apt output sometimes contains warnings or suggestions. Those won't be
|
62
|
+
# helpful, so throw them out.
|
63
|
+
r"^W: ",
|
64
|
+
r"\?$",
|
65
|
+
# some output from SSH is not useful
|
66
|
+
r"Warning: Permanently added .+ to the list of known hosts.",
|
67
|
+
r"^@+$",
|
68
|
+
]
|
69
|
+
for phrase in drop_phrases:
|
70
|
+
match = re.search(rf"({phrase})", line, flags=re.IGNORECASE)
|
71
|
+
if match:
|
72
|
+
return ""
|
73
|
+
|
74
|
+
# Next, we can normalize some common phrases.
|
75
|
+
phrases = [
|
76
|
+
"connection timed out",
|
77
|
+
r"(unable to|could not) connect to [^ ]+",
|
78
|
+
r"temporary failure resolving [^ ]+",
|
79
|
+
r"Permissions \d+ for '.+' are too open.",
|
80
|
+
]
|
81
|
+
for phrase in phrases:
|
82
|
+
match = re.search(rf"({phrase})", line, flags=re.IGNORECASE)
|
83
|
+
if match:
|
84
|
+
line = match.groups()[0]
|
85
|
+
break
|
34
86
|
|
35
|
-
|
36
|
-
|
87
|
+
# Strip out URLs for specific packages
|
88
|
+
package_re = re.compile(r"https?://.*\.(deb|rpm)")
|
89
|
+
line = package_re.sub("<package>", line)
|
90
|
+
# Strip out IP addresses
|
91
|
+
ip_re = re.compile(r"\[IP: \d+\.\d+\.\d+\.\d+( \d+)?\]")
|
92
|
+
line = ip_re.sub("", line)
|
93
|
+
return line
|
37
94
|
|
38
95
|
|
39
96
|
class Ansible(Task):
|
@@ -50,7 +107,7 @@ class Ansible(Task):
|
|
50
107
|
repo: A path or URL to a repo (defaults to '.'). Given a repo
|
51
108
|
value of 'foo', ANSIBLE_ROLES_PATH is set to 'foo/roles'
|
52
109
|
branch: If pointing to a remote git repo, use this branch. Defaults
|
53
|
-
to '
|
110
|
+
to 'main'.
|
54
111
|
hosts: A list of teuthology roles or partial hostnames (or a
|
55
112
|
combination of the two). ansible-playbook will only be run
|
56
113
|
against hosts that match.
|
@@ -112,9 +169,12 @@ class Ansible(Task):
|
|
112
169
|
|
113
170
|
def __init__(self, ctx, config):
|
114
171
|
super(Ansible, self).__init__(ctx, config)
|
115
|
-
self.log = log
|
116
172
|
self.generated_inventory = False
|
117
173
|
self.generated_playbook = False
|
174
|
+
self.log = logging.Logger(__name__)
|
175
|
+
if ctx.archive:
|
176
|
+
self.log.addHandler(logging.FileHandler(
|
177
|
+
os.path.join(ctx.archive, "ansible.log")))
|
118
178
|
|
119
179
|
def setup(self):
|
120
180
|
super(Ansible, self).setup()
|
@@ -139,9 +199,9 @@ class Ansible(Task):
|
|
139
199
|
"""
|
140
200
|
repo = self.config.get('repo', '.')
|
141
201
|
if repo.startswith(('http://', 'https://', 'git@', 'git://')):
|
142
|
-
repo_path = fetch_repo(
|
202
|
+
repo_path = repo_utils.fetch_repo(
|
143
203
|
repo,
|
144
|
-
self.config.get('branch', '
|
204
|
+
self.config.get('branch', 'main'),
|
145
205
|
)
|
146
206
|
else:
|
147
207
|
repo_path = os.path.abspath(os.path.expanduser(repo))
|
@@ -260,7 +320,10 @@ class Ansible(Task):
|
|
260
320
|
|
261
321
|
def begin(self):
|
262
322
|
super(Ansible, self).begin()
|
263
|
-
self.
|
323
|
+
if len(self.cluster.remotes) > 0:
|
324
|
+
self.execute_playbook()
|
325
|
+
else:
|
326
|
+
log.info("There are no remotes; skipping playbook execution")
|
264
327
|
|
265
328
|
def execute_playbook(self, _logfile=None):
|
266
329
|
"""
|
@@ -274,15 +337,18 @@ class Ansible(Task):
|
|
274
337
|
environ['ANSIBLE_FAILURE_LOG'] = self.failure_log.name
|
275
338
|
environ['ANSIBLE_ROLES_PATH'] = "%s/roles" % self.repo_path
|
276
339
|
environ['ANSIBLE_NOCOLOR'] = "1"
|
340
|
+
# Store collections in <repo root>/.ansible/
|
341
|
+
# This is the same path used in <repo root>/ansible.cfg
|
342
|
+
environ['ANSIBLE_COLLECTIONS_PATH'] = str(
|
343
|
+
pathlib.Path(__file__).parents[2] / ".ansible")
|
277
344
|
args = self._build_args()
|
278
345
|
command = ' '.join(args)
|
279
346
|
log.debug("Running %s", command)
|
280
347
|
|
281
|
-
out_log = self.log.getChild('out')
|
282
348
|
out, status = pexpect.run(
|
283
349
|
command,
|
284
350
|
cwd=self.repo_path,
|
285
|
-
logfile=_logfile or LoggerFile(
|
351
|
+
logfile=_logfile or LoggerFile(self.log, logging.INFO),
|
286
352
|
withexitstatus=True,
|
287
353
|
timeout=None,
|
288
354
|
)
|
@@ -298,17 +364,20 @@ class Ansible(Task):
|
|
298
364
|
def _handle_failure(self, command, status):
|
299
365
|
self._set_status('dead')
|
300
366
|
failures = None
|
301
|
-
with open(self.failure_log.name, 'r') as
|
367
|
+
with open(self.failure_log.name, 'r') as fail_log_file:
|
368
|
+
fail_log = fail_log_file.read()
|
302
369
|
try:
|
303
|
-
|
370
|
+
analyzer = FailureAnalyzer()
|
371
|
+
failures = analyzer.analyze(fail_log)
|
304
372
|
except yaml.YAMLError as e:
|
305
373
|
log.error(
|
306
|
-
"Failed to parse ansible failure log: {
|
307
|
-
self.failure_log.name, e
|
308
|
-
)
|
374
|
+
f"Failed to parse ansible failure log: {self.failure_log.name} ({e})"
|
309
375
|
)
|
310
|
-
|
311
|
-
|
376
|
+
except Exception:
|
377
|
+
log.exception(f"Failed to analyze ansible failure log: {self.failure_log.name}")
|
378
|
+
# If we hit an exception, or if analyze() returned nothing, use the log as-is
|
379
|
+
if not failures:
|
380
|
+
failures = fail_log.replace('\n', '')
|
312
381
|
|
313
382
|
if failures:
|
314
383
|
self._archive_failures()
|
@@ -390,7 +459,7 @@ class CephLab(Ansible):
|
|
390
459
|
|
391
460
|
- ansible.cephlab:
|
392
461
|
repo: {git_base}ceph-cm-ansible.git
|
393
|
-
branch:
|
462
|
+
branch: main
|
394
463
|
playbook: cephlab.yml
|
395
464
|
|
396
465
|
If a dynamic inventory is used, all hosts will be assigned to the
|
teuthology/task/buildpackages.py
CHANGED
@@ -53,7 +53,7 @@ def apply_overrides(ctx, config):
|
|
53
53
|
def get_config_install(ctx, config):
|
54
54
|
config = apply_overrides(ctx, config)
|
55
55
|
log.debug('install config %s' % config)
|
56
|
-
return [(config.get('flavor', '
|
56
|
+
return [(config.get('flavor', 'default'),
|
57
57
|
config.get('tag', ''),
|
58
58
|
config.get('branch', ''),
|
59
59
|
config.get('sha1'))]
|
@@ -69,7 +69,7 @@ def get_config_install_upgrade(ctx, config):
|
|
69
69
|
log.debug('install.upgrade config ' + str(role_config) +
|
70
70
|
' and with overrides ' + str(o))
|
71
71
|
# for install.upgrade overrides are actually defaults
|
72
|
-
configs.append((o.get('flavor', '
|
72
|
+
configs.append((o.get('flavor', 'default'),
|
73
73
|
role_config.get('tag', o.get('tag', '')),
|
74
74
|
role_config.get('branch', o.get('branch', '')),
|
75
75
|
role_config.get('sha1', o.get('sha1'))))
|