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.
Files changed (170) hide show
  1. scripts/describe.py +1 -0
  2. scripts/dispatcher.py +55 -26
  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/schedule.py +4 -0
  9. scripts/suite.py +57 -16
  10. scripts/supervisor.py +44 -0
  11. scripts/update_inventory.py +10 -4
  12. teuthology/__init__.py +24 -26
  13. teuthology/beanstalk.py +4 -3
  14. teuthology/config.py +16 -6
  15. teuthology/contextutil.py +18 -14
  16. teuthology/describe_tests.py +25 -18
  17. teuthology/dispatcher/__init__.py +210 -35
  18. teuthology/dispatcher/supervisor.py +140 -58
  19. teuthology/exceptions.py +43 -0
  20. teuthology/exporter.py +347 -0
  21. teuthology/kill.py +76 -81
  22. teuthology/lock/cli.py +3 -3
  23. teuthology/lock/ops.py +135 -61
  24. teuthology/lock/query.py +61 -44
  25. teuthology/ls.py +1 -1
  26. teuthology/misc.py +61 -75
  27. teuthology/nuke/__init__.py +12 -353
  28. teuthology/openstack/__init__.py +4 -3
  29. teuthology/openstack/openstack-centos-7.0-user-data.txt +1 -1
  30. teuthology/openstack/openstack-centos-7.1-user-data.txt +1 -1
  31. teuthology/openstack/openstack-centos-7.2-user-data.txt +1 -1
  32. teuthology/openstack/openstack-debian-8.0-user-data.txt +1 -1
  33. teuthology/openstack/openstack-opensuse-42.1-user-data.txt +1 -1
  34. teuthology/openstack/openstack-teuthology.cron +0 -1
  35. teuthology/orchestra/cluster.py +49 -7
  36. teuthology/orchestra/connection.py +17 -4
  37. teuthology/orchestra/console.py +111 -50
  38. teuthology/orchestra/daemon/cephadmunit.py +15 -2
  39. teuthology/orchestra/daemon/state.py +8 -1
  40. teuthology/orchestra/daemon/systemd.py +4 -4
  41. teuthology/orchestra/opsys.py +30 -11
  42. teuthology/orchestra/remote.py +405 -338
  43. teuthology/orchestra/run.py +3 -3
  44. teuthology/packaging.py +19 -16
  45. teuthology/provision/__init__.py +30 -10
  46. teuthology/provision/cloud/openstack.py +12 -6
  47. teuthology/provision/cloud/util.py +1 -2
  48. teuthology/provision/downburst.py +4 -3
  49. teuthology/provision/fog.py +68 -20
  50. teuthology/provision/openstack.py +5 -4
  51. teuthology/provision/pelagos.py +1 -1
  52. teuthology/repo_utils.py +43 -13
  53. teuthology/report.py +57 -35
  54. teuthology/results.py +5 -3
  55. teuthology/run.py +13 -14
  56. teuthology/run_tasks.py +27 -43
  57. teuthology/schedule.py +4 -3
  58. teuthology/scrape.py +28 -22
  59. teuthology/suite/__init__.py +74 -45
  60. teuthology/suite/build_matrix.py +34 -24
  61. teuthology/suite/fragment-merge.lua +105 -0
  62. teuthology/suite/matrix.py +31 -2
  63. teuthology/suite/merge.py +175 -0
  64. teuthology/suite/placeholder.py +6 -9
  65. teuthology/suite/run.py +175 -100
  66. teuthology/suite/util.py +64 -218
  67. teuthology/task/__init__.py +1 -1
  68. teuthology/task/ansible.py +101 -32
  69. teuthology/task/buildpackages.py +2 -2
  70. teuthology/task/ceph_ansible.py +13 -6
  71. teuthology/task/cephmetrics.py +2 -1
  72. teuthology/task/clock.py +33 -14
  73. teuthology/task/exec.py +18 -0
  74. teuthology/task/hadoop.py +2 -2
  75. teuthology/task/install/__init__.py +29 -7
  76. teuthology/task/install/bin/adjust-ulimits +16 -0
  77. teuthology/task/install/bin/daemon-helper +114 -0
  78. teuthology/task/install/bin/stdin-killer +263 -0
  79. teuthology/task/install/deb.py +1 -1
  80. teuthology/task/install/rpm.py +17 -5
  81. teuthology/task/install/util.py +3 -3
  82. teuthology/task/internal/__init__.py +41 -10
  83. teuthology/task/internal/edit_sudoers.sh +10 -0
  84. teuthology/task/internal/lock_machines.py +2 -9
  85. teuthology/task/internal/redhat.py +31 -1
  86. teuthology/task/internal/syslog.py +31 -8
  87. teuthology/task/kernel.py +152 -145
  88. teuthology/task/lockfile.py +1 -1
  89. teuthology/task/mpi.py +10 -10
  90. teuthology/task/pcp.py +1 -1
  91. teuthology/task/selinux.py +16 -8
  92. teuthology/task/ssh_keys.py +4 -4
  93. teuthology/timer.py +3 -3
  94. teuthology/util/loggerfile.py +19 -0
  95. teuthology/util/scanner.py +159 -0
  96. teuthology/util/sentry.py +52 -0
  97. teuthology/util/time.py +52 -0
  98. teuthology-1.2.1.data/scripts/adjust-ulimits +16 -0
  99. teuthology-1.2.1.data/scripts/daemon-helper +114 -0
  100. teuthology-1.2.1.data/scripts/stdin-killer +263 -0
  101. teuthology-1.2.1.dist-info/METADATA +88 -0
  102. teuthology-1.2.1.dist-info/RECORD +168 -0
  103. {teuthology-1.1.0.dist-info → teuthology-1.2.1.dist-info}/WHEEL +1 -1
  104. {teuthology-1.1.0.dist-info → teuthology-1.2.1.dist-info}/entry_points.txt +3 -2
  105. scripts/nuke.py +0 -47
  106. scripts/worker.py +0 -37
  107. teuthology/lock/test/__init__.py +0 -0
  108. teuthology/lock/test/test_lock.py +0 -7
  109. teuthology/nuke/actions.py +0 -456
  110. teuthology/openstack/test/__init__.py +0 -0
  111. teuthology/openstack/test/openstack-integration.py +0 -286
  112. teuthology/openstack/test/test_config.py +0 -35
  113. teuthology/openstack/test/test_openstack.py +0 -1695
  114. teuthology/orchestra/test/__init__.py +0 -0
  115. teuthology/orchestra/test/integration/__init__.py +0 -0
  116. teuthology/orchestra/test/integration/test_integration.py +0 -94
  117. teuthology/orchestra/test/test_cluster.py +0 -240
  118. teuthology/orchestra/test/test_connection.py +0 -106
  119. teuthology/orchestra/test/test_console.py +0 -217
  120. teuthology/orchestra/test/test_opsys.py +0 -404
  121. teuthology/orchestra/test/test_remote.py +0 -185
  122. teuthology/orchestra/test/test_run.py +0 -286
  123. teuthology/orchestra/test/test_systemd.py +0 -54
  124. teuthology/orchestra/test/util.py +0 -12
  125. teuthology/task/tests/__init__.py +0 -110
  126. teuthology/task/tests/test_locking.py +0 -25
  127. teuthology/task/tests/test_run.py +0 -40
  128. teuthology/test/__init__.py +0 -0
  129. teuthology/test/fake_archive.py +0 -107
  130. teuthology/test/fake_fs.py +0 -92
  131. teuthology/test/integration/__init__.py +0 -0
  132. teuthology/test/integration/test_suite.py +0 -86
  133. teuthology/test/task/__init__.py +0 -205
  134. teuthology/test/task/test_ansible.py +0 -624
  135. teuthology/test/task/test_ceph_ansible.py +0 -176
  136. teuthology/test/task/test_console_log.py +0 -88
  137. teuthology/test/task/test_install.py +0 -337
  138. teuthology/test/task/test_internal.py +0 -57
  139. teuthology/test/task/test_kernel.py +0 -243
  140. teuthology/test/task/test_pcp.py +0 -379
  141. teuthology/test/task/test_selinux.py +0 -35
  142. teuthology/test/test_config.py +0 -189
  143. teuthology/test/test_contextutil.py +0 -68
  144. teuthology/test/test_describe_tests.py +0 -316
  145. teuthology/test/test_email_sleep_before_teardown.py +0 -81
  146. teuthology/test/test_exit.py +0 -97
  147. teuthology/test/test_get_distro.py +0 -47
  148. teuthology/test/test_get_distro_version.py +0 -47
  149. teuthology/test/test_get_multi_machine_types.py +0 -27
  150. teuthology/test/test_job_status.py +0 -60
  151. teuthology/test/test_ls.py +0 -48
  152. teuthology/test/test_misc.py +0 -391
  153. teuthology/test/test_nuke.py +0 -290
  154. teuthology/test/test_packaging.py +0 -763
  155. teuthology/test/test_parallel.py +0 -28
  156. teuthology/test/test_repo_utils.py +0 -225
  157. teuthology/test/test_report.py +0 -77
  158. teuthology/test/test_results.py +0 -155
  159. teuthology/test/test_run.py +0 -239
  160. teuthology/test/test_safepath.py +0 -55
  161. teuthology/test/test_schedule.py +0 -45
  162. teuthology/test/test_scrape.py +0 -167
  163. teuthology/test/test_timer.py +0 -80
  164. teuthology/test/test_vps_os_vers_parameter_checking.py +0 -84
  165. teuthology/test/test_worker.py +0 -303
  166. teuthology/worker.py +0 -354
  167. teuthology-1.1.0.dist-info/METADATA +0 -76
  168. teuthology-1.1.0.dist-info/RECORD +0 -213
  169. {teuthology-1.1.0.dist-info → teuthology-1.2.1.dist-info}/LICENSE +0 -0
  170. {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 subprocess
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/8' # the one to check for build_complete
29
- CONTAINER_FLAVOR = 'basic' # basic maps to default on shaman
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 master branch
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 master branch in all cases right now
49
+ # We use teuthology's main branch in all cases right now
50
50
  if config.teuthology_path is None:
51
- fetch_teuthology('master')
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/master')
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
- os_type = 'centos'
136
- os_version = '7'
137
- elif distro in ('rhel', 'centos'):
138
- os_type = 'centos'
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
- os_version = '15.1'
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
- 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',
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=kernel_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
- log.info('container build %s, checking for build_complete' % bp.distro)
280
- if not bp.build_complete:
281
- log.info('build not complete')
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
- return bp.version
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.warn("No machines found with machine_type %s!", machine_type)
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 get_package_versions(sha1, os_type, os_version, flavor,
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.info('{0}{1}'.format(
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
- subprocess.check_call(args=args)
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 find_git_parent(project, sha1):
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 None
348
+ return []
468
349
 
469
- def refresh(project):
470
- 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}")
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 = '/'.join((base_url, '%s.git' % project,
478
- '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}")
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
- log.debug('got response: %s', resp.json())
484
- log.error('can''t find %d parents of %s in %s: %s',
485
- 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}")
486
369
  return sha1s
487
370
 
488
- # XXX don't do this every time?..
489
- refresh(project)
490
- # we want the one just before sha1; list two, return the second
491
- sha1s = get_sha1s(project, sha1, 2)
492
- if len(sha1s) == 2:
493
- return sha1s[1]
494
- else:
495
- return None
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 []
@@ -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,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 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
- fail_log.seek(0)
311
- 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', '')
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: master
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
@@ -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'))))