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.
Files changed (172) hide show
  1. scripts/describe.py +1 -0
  2. scripts/dispatcher.py +62 -0
  3. scripts/exporter.py +18 -0
  4. scripts/lock.py +1 -1
  5. scripts/node_cleanup.py +58 -0
  6. scripts/openstack.py +9 -9
  7. scripts/results.py +12 -11
  8. scripts/run.py +4 -0
  9. scripts/schedule.py +4 -0
  10. scripts/suite.py +61 -16
  11. scripts/supervisor.py +44 -0
  12. scripts/update_inventory.py +10 -4
  13. scripts/wait.py +31 -0
  14. teuthology/__init__.py +24 -21
  15. teuthology/beanstalk.py +4 -3
  16. teuthology/config.py +17 -6
  17. teuthology/contextutil.py +18 -14
  18. teuthology/describe_tests.py +25 -18
  19. teuthology/dispatcher/__init__.py +365 -0
  20. teuthology/dispatcher/supervisor.py +374 -0
  21. teuthology/exceptions.py +54 -0
  22. teuthology/exporter.py +347 -0
  23. teuthology/kill.py +76 -75
  24. teuthology/lock/cli.py +16 -7
  25. teuthology/lock/ops.py +276 -70
  26. teuthology/lock/query.py +61 -44
  27. teuthology/ls.py +9 -18
  28. teuthology/misc.py +152 -137
  29. teuthology/nuke/__init__.py +12 -351
  30. teuthology/openstack/__init__.py +4 -3
  31. teuthology/openstack/openstack-centos-7.0-user-data.txt +1 -1
  32. teuthology/openstack/openstack-centos-7.1-user-data.txt +1 -1
  33. teuthology/openstack/openstack-centos-7.2-user-data.txt +1 -1
  34. teuthology/openstack/openstack-debian-8.0-user-data.txt +1 -1
  35. teuthology/openstack/openstack-opensuse-42.1-user-data.txt +1 -1
  36. teuthology/openstack/openstack-teuthology.cron +0 -1
  37. teuthology/orchestra/cluster.py +51 -9
  38. teuthology/orchestra/connection.py +23 -16
  39. teuthology/orchestra/console.py +111 -50
  40. teuthology/orchestra/daemon/cephadmunit.py +23 -5
  41. teuthology/orchestra/daemon/state.py +10 -3
  42. teuthology/orchestra/daemon/systemd.py +10 -8
  43. teuthology/orchestra/opsys.py +32 -11
  44. teuthology/orchestra/remote.py +369 -152
  45. teuthology/orchestra/run.py +21 -12
  46. teuthology/packaging.py +54 -15
  47. teuthology/provision/__init__.py +30 -10
  48. teuthology/provision/cloud/openstack.py +12 -6
  49. teuthology/provision/cloud/util.py +1 -2
  50. teuthology/provision/downburst.py +83 -29
  51. teuthology/provision/fog.py +68 -20
  52. teuthology/provision/openstack.py +5 -4
  53. teuthology/provision/pelagos.py +13 -5
  54. teuthology/repo_utils.py +91 -44
  55. teuthology/report.py +57 -35
  56. teuthology/results.py +5 -3
  57. teuthology/run.py +21 -15
  58. teuthology/run_tasks.py +114 -40
  59. teuthology/schedule.py +4 -3
  60. teuthology/scrape.py +28 -22
  61. teuthology/suite/__init__.py +75 -46
  62. teuthology/suite/build_matrix.py +34 -24
  63. teuthology/suite/fragment-merge.lua +105 -0
  64. teuthology/suite/matrix.py +31 -2
  65. teuthology/suite/merge.py +175 -0
  66. teuthology/suite/placeholder.py +8 -8
  67. teuthology/suite/run.py +204 -102
  68. teuthology/suite/util.py +67 -211
  69. teuthology/task/__init__.py +1 -1
  70. teuthology/task/ansible.py +101 -31
  71. teuthology/task/buildpackages.py +2 -2
  72. teuthology/task/ceph_ansible.py +13 -6
  73. teuthology/task/cephmetrics.py +2 -1
  74. teuthology/task/clock.py +33 -14
  75. teuthology/task/exec.py +18 -0
  76. teuthology/task/hadoop.py +2 -2
  77. teuthology/task/install/__init__.py +51 -22
  78. teuthology/task/install/bin/adjust-ulimits +16 -0
  79. teuthology/task/install/bin/daemon-helper +114 -0
  80. teuthology/task/install/bin/stdin-killer +263 -0
  81. teuthology/task/install/deb.py +24 -4
  82. teuthology/task/install/redhat.py +36 -32
  83. teuthology/task/install/rpm.py +41 -14
  84. teuthology/task/install/util.py +48 -22
  85. teuthology/task/internal/__init__.py +69 -11
  86. teuthology/task/internal/edit_sudoers.sh +10 -0
  87. teuthology/task/internal/lock_machines.py +3 -133
  88. teuthology/task/internal/redhat.py +48 -28
  89. teuthology/task/internal/syslog.py +31 -8
  90. teuthology/task/kernel.py +155 -147
  91. teuthology/task/lockfile.py +1 -1
  92. teuthology/task/mpi.py +10 -10
  93. teuthology/task/pcp.py +1 -1
  94. teuthology/task/selinux.py +17 -8
  95. teuthology/task/ssh_keys.py +6 -6
  96. teuthology/task/tests/__init__.py +137 -77
  97. teuthology/task/tests/test_fetch_coredumps.py +116 -0
  98. teuthology/task/tests/test_run.py +4 -4
  99. teuthology/timer.py +3 -3
  100. teuthology/util/loggerfile.py +19 -0
  101. teuthology/util/scanner.py +159 -0
  102. teuthology/util/sentry.py +52 -0
  103. teuthology/util/time.py +52 -0
  104. teuthology-1.2.0.data/scripts/adjust-ulimits +16 -0
  105. teuthology-1.2.0.data/scripts/daemon-helper +114 -0
  106. teuthology-1.2.0.data/scripts/stdin-killer +263 -0
  107. teuthology-1.2.0.dist-info/METADATA +89 -0
  108. teuthology-1.2.0.dist-info/RECORD +174 -0
  109. {teuthology-1.0.0.dist-info → teuthology-1.2.0.dist-info}/WHEEL +1 -1
  110. {teuthology-1.0.0.dist-info → teuthology-1.2.0.dist-info}/entry_points.txt +5 -2
  111. scripts/nuke.py +0 -45
  112. scripts/worker.py +0 -37
  113. teuthology/nuke/actions.py +0 -456
  114. teuthology/openstack/test/__init__.py +0 -0
  115. teuthology/openstack/test/openstack-integration.py +0 -286
  116. teuthology/openstack/test/test_config.py +0 -35
  117. teuthology/openstack/test/test_openstack.py +0 -1695
  118. teuthology/orchestra/test/__init__.py +0 -0
  119. teuthology/orchestra/test/integration/__init__.py +0 -0
  120. teuthology/orchestra/test/integration/test_integration.py +0 -94
  121. teuthology/orchestra/test/test_cluster.py +0 -240
  122. teuthology/orchestra/test/test_connection.py +0 -106
  123. teuthology/orchestra/test/test_console.py +0 -217
  124. teuthology/orchestra/test/test_opsys.py +0 -404
  125. teuthology/orchestra/test/test_remote.py +0 -185
  126. teuthology/orchestra/test/test_run.py +0 -286
  127. teuthology/orchestra/test/test_systemd.py +0 -54
  128. teuthology/orchestra/test/util.py +0 -12
  129. teuthology/sentry.py +0 -18
  130. teuthology/test/__init__.py +0 -0
  131. teuthology/test/fake_archive.py +0 -107
  132. teuthology/test/fake_fs.py +0 -92
  133. teuthology/test/integration/__init__.py +0 -0
  134. teuthology/test/integration/test_suite.py +0 -86
  135. teuthology/test/task/__init__.py +0 -205
  136. teuthology/test/task/test_ansible.py +0 -624
  137. teuthology/test/task/test_ceph_ansible.py +0 -176
  138. teuthology/test/task/test_console_log.py +0 -88
  139. teuthology/test/task/test_install.py +0 -337
  140. teuthology/test/task/test_internal.py +0 -57
  141. teuthology/test/task/test_kernel.py +0 -243
  142. teuthology/test/task/test_pcp.py +0 -379
  143. teuthology/test/task/test_selinux.py +0 -35
  144. teuthology/test/test_config.py +0 -189
  145. teuthology/test/test_contextutil.py +0 -68
  146. teuthology/test/test_describe_tests.py +0 -316
  147. teuthology/test/test_email_sleep_before_teardown.py +0 -81
  148. teuthology/test/test_exit.py +0 -97
  149. teuthology/test/test_get_distro.py +0 -47
  150. teuthology/test/test_get_distro_version.py +0 -47
  151. teuthology/test/test_get_multi_machine_types.py +0 -27
  152. teuthology/test/test_job_status.py +0 -60
  153. teuthology/test/test_ls.py +0 -48
  154. teuthology/test/test_misc.py +0 -368
  155. teuthology/test/test_nuke.py +0 -232
  156. teuthology/test/test_packaging.py +0 -763
  157. teuthology/test/test_parallel.py +0 -28
  158. teuthology/test/test_repo_utils.py +0 -204
  159. teuthology/test/test_report.py +0 -77
  160. teuthology/test/test_results.py +0 -155
  161. teuthology/test/test_run.py +0 -238
  162. teuthology/test/test_safepath.py +0 -55
  163. teuthology/test/test_schedule.py +0 -45
  164. teuthology/test/test_scrape.py +0 -167
  165. teuthology/test/test_timer.py +0 -80
  166. teuthology/test/test_vps_os_vers_parameter_checking.py +0 -84
  167. teuthology/test/test_worker.py +0 -303
  168. teuthology/worker.py +0 -339
  169. teuthology-1.0.0.dist-info/METADATA +0 -76
  170. teuthology-1.0.0.dist-info/RECORD +0 -210
  171. {teuthology-1.0.0.dist-info → teuthology-1.2.0.dist-info}/LICENSE +0 -0
  172. {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 subprocess
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
- def fetch_repos(branch, test_name):
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 master branch
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 master branch in all cases right now
49
+ # We use teuthology's main branch in all cases right now
47
50
  if config.teuthology_path is None:
48
- fetch_teuthology('master')
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/master')
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
- os_type = 'centos'
133
- os_version = '7'
134
- elif distro in ('rhel', 'centos'):
135
- os_type = 'centos'
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
- os_version = '15.1'
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
- def package_version_for_hash(hash, kernel_flavor='basic', distro='rhel',
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=kernel_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
- return bp.version
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.warn("No machines found with machine_type %s!", machine_type)
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 get_package_versions(sha1, os_type, os_version, flavor,
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.info('{0}{1}'.format(
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
- subprocess.check_call(args=args)
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 find_git_parent(project, sha1):
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 None
348
+ return []
458
349
 
459
- def refresh(project):
460
- url = '%s/%s.git/refresh' % (base_url, project)
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 = '/'.join((base_url, '%s.git' % project,
468
- 'history/?committish=%s&count=%d' % (committish, count)))
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
- log.debug('got response: %s', resp.json())
474
- log.error('can''t find %d parents of %s in %s: %s',
475
- int(count), sha1, project, resp.json()['error'])
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
- # XXX don't do this every time?..
479
- refresh(project)
480
- # we want the one just before sha1; list two, return the second
481
- sha1s = get_sha1s(project, sha1, 2)
482
- if len(sha1s) == 2:
483
- return sha1s[1]
484
- else:
485
- return None
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 []
@@ -24,7 +24,7 @@ class Task(object):
24
24
  name = 'mytask.mysubtask'
25
25
  """
26
26
 
27
- def __init__(self, ctx=None, config=None):
27
+ def __init__(self, ctx, config=None):
28
28
  if not hasattr(self, 'name'):
29
29
  self.name = self.__class__.__name__.lower()
30
30
  self.log = log
@@ -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 LoggerFile(object):
22
- """
23
- A thin wrapper around a logging.Logger instance that provides a file-like
24
- interface.
25
-
26
- Used by Ansible.execute_playbook() when it calls pexpect.run()
27
- """
28
- def __init__(self, logger, level):
29
- self.logger = logger
30
- self.level = level
31
-
32
- def write(self, string):
33
- self.logger.log(self.level, string.decode('utf-8', 'ignore'))
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
- def flush(self):
36
- pass
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 'master'.
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', 'master'),
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.execute_playbook()
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(out_log, logging.INFO),
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 fail_log:
367
+ with open(self.failure_log.name, 'r') as fail_log_file:
368
+ fail_log = fail_log_file.read()
302
369
  try:
303
- failures = yaml.safe_load(fail_log)
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: {0} ({1})".format(
307
- self.failure_log.name, e
308
- )
374
+ f"Failed to parse ansible failure log: {self.failure_log.name} ({e})"
309
375
  )
310
- failures = fail_log.read().replace('\n', '')
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: master
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
@@ -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', 'basic'),
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', 'basic'),
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'))))