teuthology 1.0.0__py3-none-any.whl → 1.2.0__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 +62 -0
- 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/run.py +4 -0
- scripts/schedule.py +4 -0
- scripts/suite.py +61 -16
- scripts/supervisor.py +44 -0
- scripts/update_inventory.py +10 -4
- scripts/wait.py +31 -0
- teuthology/__init__.py +24 -21
- teuthology/beanstalk.py +4 -3
- teuthology/config.py +17 -6
- teuthology/contextutil.py +18 -14
- teuthology/describe_tests.py +25 -18
- teuthology/dispatcher/__init__.py +365 -0
- teuthology/dispatcher/supervisor.py +374 -0
- teuthology/exceptions.py +54 -0
- teuthology/exporter.py +347 -0
- teuthology/kill.py +76 -75
- teuthology/lock/cli.py +16 -7
- teuthology/lock/ops.py +276 -70
- teuthology/lock/query.py +61 -44
- teuthology/ls.py +9 -18
- teuthology/misc.py +152 -137
- teuthology/nuke/__init__.py +12 -351
- 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 +51 -9
- teuthology/orchestra/connection.py +23 -16
- teuthology/orchestra/console.py +111 -50
- teuthology/orchestra/daemon/cephadmunit.py +23 -5
- teuthology/orchestra/daemon/state.py +10 -3
- teuthology/orchestra/daemon/systemd.py +10 -8
- teuthology/orchestra/opsys.py +32 -11
- teuthology/orchestra/remote.py +369 -152
- teuthology/orchestra/run.py +21 -12
- teuthology/packaging.py +54 -15
- teuthology/provision/__init__.py +30 -10
- teuthology/provision/cloud/openstack.py +12 -6
- teuthology/provision/cloud/util.py +1 -2
- teuthology/provision/downburst.py +83 -29
- teuthology/provision/fog.py +68 -20
- teuthology/provision/openstack.py +5 -4
- teuthology/provision/pelagos.py +13 -5
- teuthology/repo_utils.py +91 -44
- teuthology/report.py +57 -35
- teuthology/results.py +5 -3
- teuthology/run.py +21 -15
- teuthology/run_tasks.py +114 -40
- teuthology/schedule.py +4 -3
- teuthology/scrape.py +28 -22
- teuthology/suite/__init__.py +75 -46
- 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 +8 -8
- teuthology/suite/run.py +204 -102
- teuthology/suite/util.py +67 -211
- teuthology/task/__init__.py +1 -1
- teuthology/task/ansible.py +101 -31
- 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 +51 -22
- 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 +24 -4
- teuthology/task/install/redhat.py +36 -32
- teuthology/task/install/rpm.py +41 -14
- teuthology/task/install/util.py +48 -22
- teuthology/task/internal/__init__.py +69 -11
- teuthology/task/internal/edit_sudoers.sh +10 -0
- teuthology/task/internal/lock_machines.py +3 -133
- teuthology/task/internal/redhat.py +48 -28
- teuthology/task/internal/syslog.py +31 -8
- teuthology/task/kernel.py +155 -147
- teuthology/task/lockfile.py +1 -1
- teuthology/task/mpi.py +10 -10
- teuthology/task/pcp.py +1 -1
- teuthology/task/selinux.py +17 -8
- teuthology/task/ssh_keys.py +6 -6
- teuthology/task/tests/__init__.py +137 -77
- teuthology/task/tests/test_fetch_coredumps.py +116 -0
- teuthology/task/tests/test_run.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.0.data/scripts/adjust-ulimits +16 -0
- teuthology-1.2.0.data/scripts/daemon-helper +114 -0
- teuthology-1.2.0.data/scripts/stdin-killer +263 -0
- teuthology-1.2.0.dist-info/METADATA +89 -0
- teuthology-1.2.0.dist-info/RECORD +174 -0
- {teuthology-1.0.0.dist-info → teuthology-1.2.0.dist-info}/WHEEL +1 -1
- {teuthology-1.0.0.dist-info → teuthology-1.2.0.dist-info}/entry_points.txt +5 -2
- scripts/nuke.py +0 -45
- scripts/worker.py +0 -37
- 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/sentry.py +0 -18
- 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 -368
- teuthology/test/test_nuke.py +0 -232
- teuthology/test/test_packaging.py +0 -763
- teuthology/test/test_parallel.py +0 -28
- teuthology/test/test_repo_utils.py +0 -204
- teuthology/test/test_report.py +0 -77
- teuthology/test/test_results.py +0 -155
- teuthology/test/test_run.py +0 -238
- 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 -339
- teuthology-1.0.0.dist-info/METADATA +0 -76
- teuthology-1.0.0.dist-info/RECORD +0 -210
- {teuthology-1.0.0.dist-info → teuthology-1.2.0.dist-info}/LICENSE +0 -0
- {teuthology-1.0.0.dist-info → teuthology-1.2.0.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,23 +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/9' # the one to check for build_complete
|
29
|
+
CONTAINER_FLAVOR = 'default'
|
28
30
|
|
29
|
-
|
31
|
+
|
32
|
+
def fetch_repos(branch, test_name, dry_run):
|
30
33
|
"""
|
31
34
|
Fetch the suite repo (and also the teuthology repo) so that we can use it
|
32
35
|
to build jobs. Repos are stored in ~/src/.
|
33
36
|
|
34
37
|
The reason the teuthology repo is also fetched is that currently we use
|
35
38
|
subprocess to call teuthology-schedule to schedule jobs so we need to make
|
36
|
-
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
|
37
40
|
for test scheduling, regardless of what teuthology branch is requested for
|
38
41
|
testing.
|
39
42
|
|
@@ -43,22 +46,23 @@ def fetch_repos(branch, test_name):
|
|
43
46
|
# When a user is scheduling a test run from their own copy of
|
44
47
|
# teuthology, let's not wreak havoc on it.
|
45
48
|
if config.automated_scheduling:
|
46
|
-
# We use teuthology's
|
49
|
+
# We use teuthology's main branch in all cases right now
|
47
50
|
if config.teuthology_path is None:
|
48
|
-
fetch_teuthology('
|
51
|
+
fetch_teuthology('main')
|
49
52
|
suite_repo_path = fetch_qa_suite(branch)
|
50
53
|
except BranchNotFoundError as exc:
|
51
|
-
schedule_fail(message=str(exc), name=test_name)
|
54
|
+
schedule_fail(message=str(exc), name=test_name, dry_run=dry_run)
|
52
55
|
return suite_repo_path
|
53
56
|
|
54
57
|
|
55
|
-
def schedule_fail(message, name=''):
|
58
|
+
def schedule_fail(message, name='', dry_run=None):
|
56
59
|
"""
|
57
60
|
If an email address has been specified anywhere, send an alert there. Then
|
58
61
|
raise a ScheduleFailError.
|
62
|
+
Don't send the mail if --dry-run has been passed.
|
59
63
|
"""
|
60
64
|
email = config.results_email
|
61
|
-
if email:
|
65
|
+
if email and not dry_run:
|
62
66
|
subject = "Failed to schedule {name}".format(name=name)
|
63
67
|
msg = MIMEText(message)
|
64
68
|
msg['Subject'] = subject
|
@@ -97,7 +101,7 @@ def get_gitbuilder_hash(project=None, branch=None, flavor=None,
|
|
97
101
|
# Alternate method for github-hosted projects - left here for informational
|
98
102
|
# purposes
|
99
103
|
# resp = requests.get(
|
100
|
-
# 'https://api.github.com/repos/ceph/ceph/git/refs/heads/
|
104
|
+
# 'https://api.github.com/repos/ceph/ceph/git/refs/heads/main')
|
101
105
|
# hash = .json()['object']['sha']
|
102
106
|
(arch, release, _os) = get_distro_defaults(distro, machine_type)
|
103
107
|
if distro is None:
|
@@ -119,38 +123,15 @@ def get_distro_defaults(distro, machine_type):
|
|
119
123
|
"""
|
120
124
|
Given a distro (e.g. 'ubuntu') and machine type, return:
|
121
125
|
(arch, release, pkg_type)
|
122
|
-
|
123
|
-
This is used to default to:
|
124
|
-
('x86_64', 'trusty', 'deb') when passed 'ubuntu' and 'plana'
|
125
|
-
('armv7l', 'saucy', 'deb') when passed 'ubuntu' and 'saya'
|
126
|
-
('x86_64', 'wheezy', 'deb') when passed 'debian'
|
127
|
-
('x86_64', 'fedora20', 'rpm') when passed 'fedora'
|
128
|
-
And ('x86_64', 'centos7', 'rpm') when passed anything else
|
129
126
|
"""
|
130
127
|
arch = 'x86_64'
|
131
|
-
if distro in (None, 'None'):
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
os_version = '7'
|
137
|
-
elif distro == 'ubuntu':
|
138
|
-
os_type = distro
|
139
|
-
if machine_type == 'saya':
|
140
|
-
os_version = '13.10'
|
141
|
-
arch = 'armv7l'
|
142
|
-
else:
|
143
|
-
os_version = '14.04'
|
144
|
-
elif distro == 'debian':
|
145
|
-
os_type = distro
|
146
|
-
os_version = '7'
|
147
|
-
elif distro == 'fedora':
|
148
|
-
os_type = distro
|
149
|
-
os_version = '20'
|
150
|
-
elif distro == 'opensuse':
|
128
|
+
if distro in (None, 'None', 'rhel'):
|
129
|
+
distro = 'centos'
|
130
|
+
|
131
|
+
try:
|
132
|
+
os_version = DEFAULT_OS_VERSION[distro]
|
151
133
|
os_type = distro
|
152
|
-
|
153
|
-
else:
|
134
|
+
except IndexError:
|
154
135
|
raise ValueError("Invalid distro value passed: %s", distro)
|
155
136
|
_os = OS(name=os_type, version=os_version)
|
156
137
|
release = get_builder_project()._get_distro(
|
@@ -158,15 +139,6 @@ def get_distro_defaults(distro, machine_type):
|
|
158
139
|
_os.version,
|
159
140
|
_os.codename,
|
160
141
|
)
|
161
|
-
template = "Defaults for machine_type {mtype} distro {distro}: " \
|
162
|
-
"arch={arch}, release={release}, pkg_type={pkg}"
|
163
|
-
log.debug(template.format(
|
164
|
-
mtype=machine_type,
|
165
|
-
distro=_os.name,
|
166
|
-
arch=arch,
|
167
|
-
release=release,
|
168
|
-
pkg=_os.package_type)
|
169
|
-
)
|
170
142
|
return (
|
171
143
|
arch,
|
172
144
|
release,
|
@@ -184,7 +156,7 @@ def git_ls_remote(project_or_url, branch, project_owner='ceph'):
|
|
184
156
|
name is passed; not when a URL is passed
|
185
157
|
:returns: The sha1 if found; else None
|
186
158
|
"""
|
187
|
-
if '://' in project_or_url:
|
159
|
+
if '://' in project_or_url or project_or_url.startswith('git@'):
|
188
160
|
url = project_or_url
|
189
161
|
else:
|
190
162
|
url = build_git_url(project_or_url, project_owner)
|
@@ -251,7 +223,8 @@ def get_branch_info(project, branch, project_owner='ceph'):
|
|
251
223
|
return resp.json()
|
252
224
|
|
253
225
|
|
254
|
-
|
226
|
+
@functools.lru_cache()
|
227
|
+
def package_version_for_hash(hash, flavor='default', distro='rhel',
|
255
228
|
distro_version='8.0', machine_type='smithi'):
|
256
229
|
"""
|
257
230
|
Does what it says on the tin. Uses gitbuilder repos.
|
@@ -264,14 +237,23 @@ def package_version_for_hash(hash, kernel_flavor='basic', distro='rhel',
|
|
264
237
|
bp = get_builder_project()(
|
265
238
|
'ceph',
|
266
239
|
dict(
|
267
|
-
flavor=
|
240
|
+
flavor=flavor,
|
268
241
|
os_type=distro,
|
269
242
|
os_version=distro_version,
|
270
243
|
arch=arch,
|
271
244
|
sha1=hash,
|
272
245
|
),
|
273
246
|
)
|
274
|
-
|
247
|
+
|
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
|
252
|
+
|
253
|
+
try:
|
254
|
+
return bp.version
|
255
|
+
except VersionNotFoundError:
|
256
|
+
return None
|
275
257
|
|
276
258
|
|
277
259
|
def get_arch(machine_type):
|
@@ -281,9 +263,9 @@ def get_arch(machine_type):
|
|
281
263
|
|
282
264
|
:returns: A string or None
|
283
265
|
"""
|
284
|
-
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)
|
285
267
|
if not result:
|
286
|
-
log.
|
268
|
+
log.warning("No machines found with machine_type %s!", machine_type)
|
287
269
|
else:
|
288
270
|
return result[0]['arch']
|
289
271
|
|
@@ -323,102 +305,7 @@ def get_install_task_flavor(job_config):
|
|
323
305
|
return get_flavor(first_install_config)
|
324
306
|
|
325
307
|
|
326
|
-
def
|
327
|
-
package_versions=None):
|
328
|
-
"""
|
329
|
-
Will retrieve the package versions for the given sha1, os_type/version,
|
330
|
-
and flavor from gitbuilder.
|
331
|
-
|
332
|
-
Optionally, a package_versions dict can be provided
|
333
|
-
from previous calls to this function to avoid calling gitbuilder for
|
334
|
-
information we've already retrieved.
|
335
|
-
|
336
|
-
The package_versions dict will be in the following format::
|
337
|
-
|
338
|
-
{
|
339
|
-
"sha1": {
|
340
|
-
"ubuntu": {
|
341
|
-
"14.04": {
|
342
|
-
"basic": "version",
|
343
|
-
}
|
344
|
-
"15.04": {
|
345
|
-
"notcmalloc": "version",
|
346
|
-
}
|
347
|
-
}
|
348
|
-
"rhel": {
|
349
|
-
"basic": "version",
|
350
|
-
}
|
351
|
-
},
|
352
|
-
"another-sha1": {
|
353
|
-
"ubuntu": {
|
354
|
-
"basic": "version",
|
355
|
-
}
|
356
|
-
}
|
357
|
-
}
|
358
|
-
|
359
|
-
:param sha1: The sha1 hash of the ceph version.
|
360
|
-
:param os_type: The distro we want to get packages for, given
|
361
|
-
the ceph sha1. Ex. 'ubuntu', 'rhel', etc.
|
362
|
-
:param os_version: The distro's version, e.g. '14.04', '7.0'
|
363
|
-
:param flavor: Package flavor ('testing', 'notcmalloc', etc.)
|
364
|
-
:param package_versions: Use this optionally to use cached results of
|
365
|
-
previous calls to gitbuilder.
|
366
|
-
:returns: A dict of package versions. Will return versions
|
367
|
-
for all hashes/distros/vers, not just for the given
|
368
|
-
hash/distro/ver.
|
369
|
-
"""
|
370
|
-
if package_versions is None:
|
371
|
-
package_versions = dict()
|
372
|
-
|
373
|
-
os_type = str(os_type)
|
374
|
-
|
375
|
-
os_types = package_versions.get(sha1, dict())
|
376
|
-
os_versions = os_types.get(os_type, dict())
|
377
|
-
flavors = os_versions.get(os_version, dict())
|
378
|
-
if flavor not in flavors:
|
379
|
-
package_version = package_version_for_hash(
|
380
|
-
sha1,
|
381
|
-
flavor,
|
382
|
-
distro=os_type,
|
383
|
-
distro_version=os_version,
|
384
|
-
)
|
385
|
-
flavors[flavor] = package_version
|
386
|
-
os_versions[os_version] = flavors
|
387
|
-
os_types[os_type] = os_versions
|
388
|
-
package_versions[sha1] = os_types
|
389
|
-
|
390
|
-
return package_versions
|
391
|
-
|
392
|
-
|
393
|
-
def has_packages_for_distro(sha1, os_type, os_version, flavor,
|
394
|
-
package_versions=None):
|
395
|
-
"""
|
396
|
-
Checks to see if gitbuilder has packages for the given sha1, os_type and
|
397
|
-
kernel_flavor.
|
398
|
-
|
399
|
-
See above for package_versions description.
|
400
|
-
|
401
|
-
:param sha1: The sha1 hash of the ceph version.
|
402
|
-
:param os_type: The distro we want to get packages for, given
|
403
|
-
the ceph sha1. Ex. 'ubuntu', 'rhel', etc.
|
404
|
-
:param kernel_flavor: The kernel flavor
|
405
|
-
:param package_versions: Use this optionally to use cached results of
|
406
|
-
previous calls to gitbuilder.
|
407
|
-
:returns: True, if packages are found. False otherwise.
|
408
|
-
"""
|
409
|
-
os_type = str(os_type)
|
410
|
-
if package_versions is None:
|
411
|
-
package_versions = get_package_versions(
|
412
|
-
sha1, os_type, os_version, flavor)
|
413
|
-
|
414
|
-
flavors = package_versions.get(sha1, dict()).get(
|
415
|
-
os_type, dict()).get(
|
416
|
-
os_version, dict())
|
417
|
-
# we want to return a boolean here, not the actual package versions
|
418
|
-
return bool(flavors.get(flavor, None))
|
419
|
-
|
420
|
-
|
421
|
-
def teuthology_schedule(args, verbose, dry_run, log_prefix=''):
|
308
|
+
def teuthology_schedule(args, verbose, dry_run, log_prefix='', stdin=None):
|
422
309
|
"""
|
423
310
|
Run teuthology-schedule to schedule individual jobs.
|
424
311
|
|
@@ -441,84 +328,53 @@ def teuthology_schedule(args, verbose, dry_run, log_prefix=''):
|
|
441
328
|
printable_args.append("'%s'" % item)
|
442
329
|
else:
|
443
330
|
printable_args.append(item)
|
444
|
-
log.
|
331
|
+
log.debug('{0} command: {1}'.format(
|
445
332
|
log_prefix,
|
446
333
|
' '.join(printable_args),
|
447
334
|
))
|
448
335
|
if not dry_run or (dry_run and verbose > 1):
|
449
|
-
|
450
|
-
|
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()
|
451
342
|
|
452
|
-
def
|
343
|
+
def find_git_parents(project: str, sha1: str, count=1):
|
453
344
|
|
454
345
|
base_url = config.githelper_base_url
|
455
346
|
if not base_url:
|
456
347
|
log.warning('githelper_base_url not set, --newest disabled')
|
457
|
-
return
|
348
|
+
return []
|
458
349
|
|
459
|
-
def refresh(
|
460
|
-
url =
|
350
|
+
def refresh():
|
351
|
+
url = f"{base_url}/{project}.git/refresh"
|
352
|
+
log.info(f"Forcing refresh of git mirror: {url}")
|
461
353
|
resp = requests.get(url)
|
462
354
|
if not resp.ok:
|
463
355
|
log.error('git refresh failed for %s: %s',
|
464
356
|
project, resp.content.decode())
|
465
357
|
|
466
358
|
def get_sha1s(project, committish, count):
|
467
|
-
url =
|
468
|
-
|
359
|
+
url = f"{base_url}/{project}.git/history?committish={committish}&count={count}"
|
360
|
+
log.info(f"Looking for parent commits: {url}")
|
469
361
|
resp = requests.get(url)
|
470
362
|
resp.raise_for_status()
|
471
363
|
sha1s = resp.json()['sha1s']
|
472
364
|
if len(sha1s) != count:
|
473
|
-
|
474
|
-
|
475
|
-
|
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}")
|
476
369
|
return sha1s
|
477
370
|
|
478
|
-
#
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
def filter_configs(configs, suite_name=None,
|
489
|
-
filter_in=None,
|
490
|
-
filter_out=None,
|
491
|
-
filter_all=None,
|
492
|
-
filter_fragments=True):
|
493
|
-
"""
|
494
|
-
Returns a generator for pairs of description and fragment paths.
|
495
|
-
|
496
|
-
Usage:
|
497
|
-
|
498
|
-
configs = build_matrix(path, subset, seed)
|
499
|
-
for description, fragments in filter_configs(configs):
|
500
|
-
pass
|
501
|
-
"""
|
502
|
-
for item in configs:
|
503
|
-
fragment_paths = item[1]
|
504
|
-
description = combine_path(suite_name, item[0]) \
|
505
|
-
if suite_name else item[0]
|
506
|
-
base_frag_paths = [strip_fragment_path(x)
|
507
|
-
for x in fragment_paths]
|
508
|
-
def matches(f):
|
509
|
-
if f in description:
|
510
|
-
return True
|
511
|
-
if filter_fragments and \
|
512
|
-
any(f in path for path in base_frag_paths):
|
513
|
-
return True
|
514
|
-
return False
|
515
|
-
if filter_all:
|
516
|
-
if not all(matches(f) for f in filter_all):
|
517
|
-
continue
|
518
|
-
if filter_in:
|
519
|
-
if not any(matches(f) for f in filter_in):
|
520
|
-
continue
|
521
|
-
if filter_out:
|
522
|
-
if any(matches(f) for f in filter_out):
|
523
|
-
continue
|
524
|
-
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,16 +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
|
-
|
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', '')
|
311
381
|
|
312
382
|
if failures:
|
313
383
|
self._archive_failures()
|
@@ -389,7 +459,7 @@ class CephLab(Ansible):
|
|
389
459
|
|
390
460
|
- ansible.cephlab:
|
391
461
|
repo: {git_base}ceph-cm-ansible.git
|
392
|
-
branch:
|
462
|
+
branch: main
|
393
463
|
playbook: cephlab.yml
|
394
464
|
|
395
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'))))
|