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/report.py CHANGED
@@ -4,11 +4,14 @@ import json
4
4
  import re
5
5
  import requests
6
6
  import logging
7
+ import random
7
8
  import socket
8
9
  from datetime import datetime
9
10
 
10
11
  import teuthology
12
+ import teuthology.exporter
11
13
  from teuthology.config import config
14
+ from teuthology.contextutil import safe_while
12
15
  from teuthology.job_status import get_status, set_status
13
16
 
14
17
  report_exceptions = (requests.exceptions.RequestException, socket.error)
@@ -141,7 +144,7 @@ class ResultsSerializer(object):
141
144
  return {}
142
145
  jobs = {}
143
146
  for item in os.listdir(archive_dir):
144
- if not re.match('\d+$', item):
147
+ if not re.match(r'\d+$', item):
145
148
  continue
146
149
  job_id = item
147
150
  job_dir = os.path.join(archive_dir, job_id)
@@ -197,7 +200,7 @@ class ResultsReporter(object):
197
200
 
198
201
  if not self.base_uri:
199
202
  msg = "No results_server set in {yaml}; cannot report results"
200
- self.log.warn(msg.format(yaml=config.yaml_path))
203
+ self.log.warning(msg.format(yaml=config.yaml_path))
201
204
 
202
205
  def _make_session(self, max_retries=10):
203
206
  session = requests.Session()
@@ -289,37 +292,42 @@ class ResultsReporter(object):
289
292
  set_status(job_info, 'dead')
290
293
  job_json = json.dumps(job_info)
291
294
  headers = {'content-type': 'application/json'}
292
- response = self.session.post(run_uri, data=job_json, headers=headers)
293
295
 
294
- if response.status_code == 200:
295
- return job_id
296
+ inc = random.uniform(0, 1)
297
+ with safe_while(
298
+ sleep=1, increment=inc, action=f'report job {job_id}') as proceed:
299
+ while proceed():
300
+ response = self.session.post(run_uri, data=job_json, headers=headers)
296
301
 
297
- # This call is wrapped in a try/except because of:
298
- # http://tracker.ceph.com/issues/8166
299
- try:
300
- resp_json = response.json()
301
- except ValueError:
302
- resp_json = dict()
302
+ if response.status_code == 200:
303
+ return
303
304
 
304
- if resp_json:
305
- msg = resp_json.get('message', '')
306
- else:
307
- msg = response.text
308
-
309
- if msg and msg.endswith('already exists'):
310
- job_uri = os.path.join(run_uri, job_id, '')
311
- response = self.session.put(job_uri, data=job_json,
312
- headers=headers)
313
- elif msg:
314
- self.log.error(
315
- "POST to {uri} failed with status {status}: {msg}".format(
316
- uri=run_uri,
317
- status=response.status_code,
318
- msg=msg,
319
- ))
320
- response.raise_for_status()
305
+ # This call is wrapped in a try/except because of:
306
+ # http://tracker.ceph.com/issues/8166
307
+ try:
308
+ resp_json = response.json()
309
+ except ValueError:
310
+ resp_json = dict()
321
311
 
322
- return job_id
312
+ if resp_json:
313
+ msg = resp_json.get('message', '')
314
+ else:
315
+ msg = response.text
316
+
317
+ if msg and msg.endswith('already exists'):
318
+ job_uri = os.path.join(run_uri, job_id, '')
319
+ response = self.session.put(job_uri, data=job_json,
320
+ headers=headers)
321
+ if response.status_code == 200:
322
+ return
323
+ elif msg:
324
+ self.log.error(
325
+ "POST to {uri} failed with status {status}: {msg}".format(
326
+ uri=run_uri,
327
+ status=response.status_code,
328
+ msg=msg,
329
+ ))
330
+ response.raise_for_status()
323
331
 
324
332
  @property
325
333
  def last_run(self):
@@ -394,6 +402,7 @@ class ResultsReporter(object):
394
402
  log_path = os.path.join(self.archive_base, run_name, 'results.log')
395
403
  # parse the log file generated by teuthology.results.results()
396
404
  subset = None
405
+ no_nested_subset = None
397
406
  seed = None
398
407
  with open(log_path) as results_log:
399
408
  for line in results_log:
@@ -403,15 +412,17 @@ class ResultsReporter(object):
403
412
  line = line.strip()
404
413
  if subset is None:
405
414
  subset = self._parse_log_line(line, 'subset:')
406
- elif seed is None:
415
+ if no_nested_subset is None:
416
+ no_nested_subset = self._parse_log_line(line, 'no_nested_subset:')
417
+ if seed is None:
407
418
  seed = self._parse_log_line(line, 'seed:')
408
- else:
409
- break
410
419
  if subset is not None:
411
420
  subset = tuple(int(i) for i in subset.split('/'))
421
+ if no_nested_subset is not None:
422
+ no_nested_subset = bool(no_nested_subset)
412
423
  if seed is not None:
413
424
  seed = int(seed)
414
- return subset, seed
425
+ return subset, no_nested_subset, seed
415
426
 
416
427
  def delete_job(self, run_name, job_id):
417
428
  """
@@ -461,6 +472,12 @@ def push_job_info(run_name, job_id, job_info, base_uri=None):
461
472
  if not reporter.base_uri:
462
473
  return
463
474
  reporter.report_job(run_name, job_id, job_info)
475
+ status = get_status(job_info)
476
+ if status in ["pass", "fail", "dead"] and "machine_type" in job_info:
477
+ teuthology.exporter.JobResults().record(
478
+ machine_type=job_info["machine_type"],
479
+ status=status,
480
+ )
464
481
 
465
482
 
466
483
  def try_push_job_info(job_config, extra_info=None):
@@ -488,8 +505,8 @@ def try_push_job_info(job_config, extra_info=None):
488
505
  job_id = job_config['job_id']
489
506
 
490
507
  if extra_info is not None:
491
- job_info = extra_info.copy()
492
- job_info.update(job_config)
508
+ job_info = job_config.copy()
509
+ job_info.update(extra_info)
493
510
  else:
494
511
  job_info = job_config
495
512
 
@@ -569,6 +586,11 @@ def try_mark_run_dead(run_name):
569
586
  try:
570
587
  log.info("Marking job {job_id} as dead".format(job_id=job_id))
571
588
  reporter.report_job(run_name, job['job_id'], dead=True)
589
+ if "machine_type" in job:
590
+ teuthology.exporter.JobResults().record(
591
+ machine_type=job["machine_type"],
592
+ status=job["status"],
593
+ )
572
594
  except report_exceptions:
573
595
  log.exception("Could not mark job as dead: {job_id}".format(
574
596
  job_id=job_id))
teuthology/results.py CHANGED
@@ -28,7 +28,7 @@ def main(args):
28
28
 
29
29
  try:
30
30
  if args['--seed']:
31
- note_rerun_params(args['--subset'], args['--seed'])
31
+ note_rerun_params(args['--subset'], args['--no-nested-subset'], args['--seed'])
32
32
  else:
33
33
  results(args['--archive-dir'], args['--name'], args['--email'],
34
34
  int(args['--timeout']), args['--dry-run'])
@@ -37,9 +37,11 @@ def main(args):
37
37
  raise
38
38
 
39
39
 
40
- def note_rerun_params(subset, seed):
40
+ def note_rerun_params(subset, no_nested_subset, seed):
41
41
  if subset:
42
42
  log.info('subset: %r', subset)
43
+ if no_nested_subset:
44
+ log.info('no_nested_subset: %r', no_nested_subset)
43
45
  if seed:
44
46
  log.info('seed: %r', seed)
45
47
 
@@ -53,7 +55,7 @@ def results(archive_dir, name, email, timeout, dry_run):
53
55
  reporter = ResultsReporter()
54
56
  while timeout > 0:
55
57
  if time.time() - starttime > timeout:
56
- log.warn('test(s) did not finish before timeout of %d seconds',
58
+ log.warning('test(s) did not finish before timeout of %d seconds',
57
59
  timeout)
58
60
  break
59
61
  jobs = reporter.get_jobs(name, fields=['job_id', 'status'])
teuthology/run.py CHANGED
@@ -8,7 +8,6 @@ from teuthology import install_except_hook
8
8
  from teuthology import report
9
9
  from teuthology.job_status import get_status
10
10
  from teuthology.misc import get_user, merge_configs
11
- from teuthology.nuke import nuke
12
11
  from teuthology.run_tasks import run_tasks
13
12
  from teuthology.repo_utils import fetch_qa_suite
14
13
  from teuthology.results import email_results
@@ -80,20 +79,21 @@ def fetch_tasks_if_needed(job_config):
80
79
  except ImportError:
81
80
  log.info("Tasks not found; will attempt to fetch")
82
81
 
83
- ceph_branch = job_config.get('branch', 'master')
82
+ ceph_branch = job_config.get('branch', 'main')
84
83
  suite_repo = job_config.get('suite_repo')
85
84
  if suite_repo:
86
85
  teuth_config.ceph_qa_suite_git_url = suite_repo
87
86
  suite_branch = job_config.get('suite_branch', ceph_branch)
87
+ suite_sha1 = job_config.get('suite_sha1')
88
88
  suite_path = os.path.normpath(os.path.join(
89
- fetch_qa_suite(suite_branch),
90
- job_config.get('suite_relpath', ''),
89
+ fetch_qa_suite(suite_branch, commit=suite_sha1),
90
+ job_config.get('suite_relpath', 'qa'),
91
91
  ))
92
92
  sys.path.insert(1, suite_path)
93
93
  return suite_path
94
94
 
95
95
 
96
- def setup_config(config_paths):
96
+ def setup_config(config_paths) -> dict:
97
97
  """
98
98
  Takes a list of config yaml files and combines them
99
99
  into a single dictionary. Processes / validates the dictionary and then
@@ -250,21 +250,21 @@ def get_initial_tasks(lock, config, machine_type):
250
250
  {'internal.setup_cdn_repo': None},
251
251
  {'internal.setup_base_repo': None},
252
252
  {'internal.setup_additional_repo': None},
253
- {'kernel.install_latest_rh_kernel': None}
253
+ {'internal.setup_container_registry': None},
254
+ {'install': None},
254
255
  ])
256
+ # Install latest kernel task for redhat downstream runs
257
+ if config.get('redhat').get('install_latest_rh_kernel', False):
258
+ init_tasks.extend({'kernel.install_latest_rh_kernel': None})
255
259
 
256
260
  return init_tasks
257
261
 
258
262
 
259
- def report_outcome(config, archive, summary, fake_ctx):
263
+ def report_outcome(config, archive, summary):
260
264
  """ Reports on the final outcome of the command. """
261
265
  status = get_status(summary)
262
266
  passed = status == 'pass'
263
267
 
264
- if not passed and bool(config.get('nuke-on-error')):
265
- # only unlock if we locked them in the first place
266
- nuke(fake_ctx, fake_ctx.lock)
267
-
268
268
  if archive is not None:
269
269
  with open(os.path.join(archive, 'summary.yaml'), 'w') as f:
270
270
  yaml.safe_dump(summary, f, default_flow_style=False)
@@ -324,8 +324,7 @@ def main(args):
324
324
  suite_path = args["--suite-path"]
325
325
  os_type = args["--os-type"]
326
326
  os_version = args["--os-version"]
327
-
328
- set_up_logging(verbose, archive)
327
+ interactive_on_error = args["--interactive-on-error"]
329
328
 
330
329
  # print the command being ran
331
330
  log.debug("Teuthology command: {0}".format(get_teuthology_command(args)))
@@ -337,6 +336,10 @@ def main(args):
337
336
 
338
337
  if archive is not None and 'archive_path' not in config:
339
338
  config['archive_path'] = archive
339
+ elif archive is None and 'archive_path' in config:
340
+ archive = args['--archive'] = config['archive_path']
341
+
342
+ set_up_logging(verbose, archive)
340
343
 
341
344
  write_initial_metadata(archive, config, name, description, owner)
342
345
  report.try_push_job_info(config, dict(status='running'))
@@ -386,6 +389,9 @@ def main(args):
386
389
  if config.get('use_shaman') is not None:
387
390
  teuth_config.use_shaman = config['use_shaman']
388
391
 
392
+ #could be refactored for setting and unsetting in hackish way
393
+ if interactive_on_error:
394
+ config['interactive-on-error'] = True
389
395
  # create a FakeNamespace instance that mimics the old argparse way of doing
390
396
  # things we do this so we can pass it to run_tasks without porting those
391
397
  # tasks to the new way of doing things right now
@@ -396,10 +402,10 @@ def main(args):
396
402
  # FIXME this should become more generic, and the keys should use
397
403
  # '_' uniformly
398
404
  if fake_ctx.config.get('interactive-on-error'):
399
- teuthology.config.config.ctx = fake_ctx
405
+ teuth_config.config.ctx = fake_ctx
400
406
 
401
407
  try:
402
408
  run_tasks(tasks=config['tasks'], ctx=fake_ctx)
403
409
  finally:
404
410
  # print to stdout the results and possibly send an email on any errors
405
- report_outcome(config, archive, fake_ctx.summary, fake_ctx)
411
+ report_outcome(config, archive, fake_ctx.summary)
teuthology/run_tasks.py CHANGED
@@ -1,19 +1,22 @@
1
+ import importlib
1
2
  import jinja2
2
3
  import logging
3
4
  import os
4
5
  import sys
5
6
  import time
6
7
  import types
8
+ import yaml
7
9
 
8
- from copy import deepcopy
9
10
  from humanfriendly import format_timespan
10
11
 
12
+ import teuthology.exporter as exporter
13
+
11
14
  from teuthology.config import config as teuth_config
12
15
  from teuthology.exceptions import ConnectionLostError
13
16
  from teuthology.job_status import set_status, get_status
14
17
  from teuthology.misc import get_http_log_path, get_results_url
15
- from teuthology.sentry import get_client as get_sentry_client
16
18
  from teuthology.timer import Timer
19
+ from teuthology.util import sentry
17
20
 
18
21
  log = logging.getLogger(__name__)
19
22
 
@@ -59,6 +62,17 @@ def _import(from_package, module_name, task_name, fail_on_import_error=False):
59
62
  if fail_on_import_error:
60
63
  raise
61
64
  else:
65
+ if (
66
+ importlib.util.find_spec(from_package) is not None and
67
+ importlib.util.find_spec(full_module_name) is not None
68
+ ):
69
+ # If we get here, it means we could _find_ both the module and
70
+ # the package that contains it, but still got an ImportError.
71
+ # Typically that means the module failed to import because it
72
+ # could not find one of its dependencies; if we don't raise
73
+ # here it will look like we just could't find the module,
74
+ # making the dependency issue difficult to discover.
75
+ raise
62
76
  return None
63
77
  return module
64
78
 
@@ -79,6 +93,7 @@ def run_tasks(tasks, ctx):
79
93
  else:
80
94
  timer = Timer()
81
95
  stack = []
96
+ taskname = ""
82
97
  try:
83
98
  for taskdict in tasks:
84
99
  try:
@@ -90,7 +105,11 @@ def run_tasks(tasks, ctx):
90
105
  manager = run_one_task(taskname, ctx=ctx, config=config)
91
106
  if hasattr(manager, '__enter__'):
92
107
  stack.append((taskname, manager))
93
- manager.__enter__()
108
+ with exporter.TaskTime().time(
109
+ name=taskname,
110
+ phase="enter"
111
+ ):
112
+ manager.__enter__()
94
113
  except BaseException as e:
95
114
  if isinstance(e, ConnectionLostError):
96
115
  # Prevent connection issues being flagged as failures
@@ -103,41 +122,7 @@ def run_tasks(tasks, ctx):
103
122
  ctx.summary['failure_reason'] = str(e)
104
123
  log.exception('Saw exception from tasks.')
105
124
 
106
- sentry = get_sentry_client()
107
- if sentry:
108
- config = deepcopy(ctx.config)
109
-
110
- tags = {
111
- 'task': taskname,
112
- 'owner': ctx.owner,
113
- }
114
- if 'teuthology_branch' in config:
115
- tags['teuthology_branch'] = config['teuthology_branch']
116
- if 'branch' in config:
117
- tags['branch'] = config['branch']
118
-
119
- # Remove ssh keys from reported config
120
- if 'targets' in config:
121
- targets = config['targets']
122
- for host in targets.keys():
123
- targets[host] = '<redacted>'
124
-
125
- job_id = ctx.config.get('job_id')
126
- archive_path = ctx.config.get('archive_path')
127
- extra = dict(config=config,
128
- )
129
- if job_id:
130
- extra['logs'] = get_http_log_path(archive_path, job_id)
131
-
132
- exc_id = sentry.get_ident(sentry.captureException(
133
- tags=tags,
134
- extra=extra,
135
- ))
136
- event_url = "{server}/?q={id}".format(
137
- server=teuth_config.sentry_server.strip('/'), id=exc_id)
138
- log.exception(" Sentry event: %s" % event_url)
139
- ctx.summary['sentry_event'] = event_url
140
-
125
+ ctx.summary['sentry_event'] = sentry.report_error(ctx.config, e, taskname)
141
126
  if ctx.config.get('interactive-on-error'):
142
127
  ctx.config['interactive-on-error'] = False
143
128
  from teuthology.task import interactive
@@ -168,7 +153,11 @@ def run_tasks(tasks, ctx):
168
153
  log.debug('Unwinding manager %s', taskname)
169
154
  timer.mark('%s exit' % taskname)
170
155
  try:
171
- suppress = manager.__exit__(*exc_info)
156
+ with exporter.TaskTime().time(
157
+ name=taskname,
158
+ phase="exit"
159
+ ):
160
+ suppress = manager.__exit__(*exc_info)
172
161
  except Exception as e:
173
162
  if isinstance(e, ConnectionLostError):
174
163
  # Prevent connection issues being flagged as failures
@@ -185,7 +174,7 @@ def run_tasks(tasks, ctx):
185
174
  exc_info = sys.exc_info()
186
175
 
187
176
  if ctx.config.get('interactive-on-error'):
188
- from tuethology.task import interactive
177
+ from teuthology.task import interactive
189
178
  log.warning(
190
179
  'Saw failure during task cleanup, going into interactive mode...')
191
180
  interactive.task(ctx=ctx, config=None)
@@ -202,6 +191,41 @@ def run_tasks(tasks, ctx):
202
191
  del exc_info
203
192
  timer.mark("tasks complete")
204
193
 
194
+
195
+ def build_rocketchat_message(ctx, stack, sleep_time_sec, template_path=None):
196
+ message_template_path = template_path or os.path.dirname(__file__) + \
197
+ '/templates/rocketchat-sleep-before-teardown.jinja2'
198
+
199
+ with open(message_template_path) as f:
200
+ template_text = f.read()
201
+
202
+ template = jinja2.Template(template_text)
203
+ archive_path = ctx.config.get('archive_path')
204
+ job_id = ctx.config.get('job_id')
205
+ status = get_status(ctx.summary)
206
+ stack_path = ' -> '.join(task for task, _ in stack)
207
+ suite_name=ctx.config.get('suite')
208
+ sleep_date=time.time()
209
+ sleep_date_str=time.strftime('%Y-%m-%d %H:%M:%S',
210
+ time.gmtime(sleep_date))
211
+
212
+ message = template.render(
213
+ sleep_time=format_timespan(sleep_time_sec),
214
+ sleep_time_sec=sleep_time_sec,
215
+ sleep_date=sleep_date_str,
216
+ owner=ctx.owner,
217
+ run_name=ctx.name,
218
+ job_id=ctx.config.get('job_id'),
219
+ job_desc=ctx.config.get('description'),
220
+ job_info=get_results_url(ctx.name, job_id),
221
+ job_logs=get_http_log_path(archive_path, job_id),
222
+ suite_name=suite_name,
223
+ status=status,
224
+ task_stack=stack_path,
225
+ )
226
+ return message
227
+
228
+
205
229
  def build_email_body(ctx, stack, sleep_time_sec):
206
230
  email_template_path = os.path.dirname(__file__) + \
207
231
  '/templates/email-sleep-before-teardown.jinja2'
@@ -238,7 +262,57 @@ def build_email_body(ctx, stack, sleep_time_sec):
238
262
  )
239
263
  return (subject.strip(), body.strip())
240
264
 
265
+
266
+ def rocketchat_send_message(ctx, message, channels):
267
+ """
268
+ Send the message to the given RocketChat channels
269
+
270
+ Before sending the message we read the config file
271
+ from `~/.config/rocketchat.api/settings.yaml` which
272
+ must include next records:
273
+
274
+ username: 'userloginname'
275
+ password: 'userbigsecret'
276
+ domain: 'https://chat.suse.de'
277
+
278
+ :param message: plain text message content in the Rocket.Chat
279
+ messaging format
280
+ :param channels: a list of channels where to send the message,
281
+ the user private channel should be prefixed
282
+ with '@' symbol
283
+ """
284
+ try:
285
+ from rocketchat.api import RocketChatAPI
286
+ except Exception as e:
287
+ log.warning(f'rocketchat: Failed to import rocketchat.api: {e}')
288
+ return
289
+
290
+ settings_path = \
291
+ os.environ.get('HOME') + '/.config/rocketchat.api/settings.yaml'
292
+
293
+ try:
294
+ with open(settings_path) as f:
295
+ settings = yaml.safe_load(f)
296
+ except Exception as e:
297
+ log.warning(f'rocketchat: Failed to load settings from {settings_path}: {e}')
298
+
299
+ r = RocketChatAPI(settings=settings)
300
+ for channel in channels:
301
+ try:
302
+ r.send_message(message, channel)
303
+ except Exception as e:
304
+ log.warning(f'rocketchat: Failed to send message to "{channel}" channel: {e}')
305
+
306
+
241
307
  def notify_sleep_before_teardown(ctx, stack, sleep_time):
308
+ rocketchat = ctx.config.get('rocketchat', None)
309
+
310
+ if rocketchat:
311
+ channels = [_ for _ in [_.strip() for _ in rocketchat.split(',')] if _]
312
+ log.info("Sending a message to Rocket.Chat channels: %s", channels)
313
+ message = build_rocketchat_message(ctx, stack, sleep_time)
314
+ rocketchat_send_message(ctx, message, channels)
315
+
242
316
  email = ctx.config.get('email', None)
243
317
  if not email:
244
318
  # we have no email configured, return silently
teuthology/schedule.py CHANGED
@@ -8,11 +8,11 @@ from teuthology import report
8
8
 
9
9
  def main(args):
10
10
  if not args['--first-in-suite']:
11
- first_job_args = ['subset', 'seed']
11
+ first_job_args = ['subset', 'no-nested-subset', 'seed']
12
12
  for arg in first_job_args:
13
13
  opt = '--{arg}'.format(arg=arg)
14
14
  msg_fmt = '{opt} is only applicable to the first job in a suite'
15
- if args[opt]:
15
+ if args.get(opt):
16
16
  raise ValueError(msg_fmt.format(opt=opt))
17
17
 
18
18
  if not args['--last-in-suite']:
@@ -78,7 +78,8 @@ def build_config(args):
78
78
  job_config.update(conf_dict)
79
79
  for arg,conf in {'--timeout':'results_timeout',
80
80
  '--seed': 'seed',
81
- '--subset': 'subset'}.items():
81
+ '--subset': 'subset',
82
+ '--no-nested-subset': 'no_nested_subset'}.items():
82
83
  val = args.get(arg, None)
83
84
  if val is not None:
84
85
  job_config[conf] = val
teuthology/scrape.py CHANGED
@@ -12,7 +12,6 @@ import re
12
12
  import logging
13
13
  import subprocess
14
14
 
15
- import six
16
15
 
17
16
  log = logging.getLogger('scrape')
18
17
  log.addHandler(logging.StreamHandler())
@@ -31,11 +30,12 @@ def grep(path, expr):
31
30
  """
32
31
  Call out to native grep rather than feeding massive log files through python line by line
33
32
  """
34
- p = subprocess.Popen(["grep", expr, path], stdout=subprocess.PIPE)
33
+ p = subprocess.Popen(["grep", expr, path], stdout=subprocess.PIPE,
34
+ universal_newlines=True)
35
35
  p.wait()
36
36
  out, err = p.communicate()
37
37
  if p.returncode == 0:
38
- return six.ensure_str(out).split("\n")
38
+ return out.split("\n")
39
39
  else:
40
40
  return []
41
41
 
@@ -83,9 +83,9 @@ class GenericReason(Reason):
83
83
  else:
84
84
  if "Test failure:" in self.failure_reason:
85
85
  return self.failure_reason == job.get_failure_reason()
86
- elif re.search("workunit test (.*)\) on ", self.failure_reason):
87
- workunit_name = re.search("workunit test (.*)\) on ", self.failure_reason).group(1)
88
- other_match = re.search("workunit test (.*)\) on ", job.get_failure_reason())
86
+ elif re.search(r"workunit test (.*)\) on ", self.failure_reason):
87
+ workunit_name = re.search(r"workunit test (.*)\) on ", self.failure_reason).group(1)
88
+ other_match = re.search(r"workunit test (.*)\) on ", job.get_failure_reason())
89
89
  return other_match is not None and workunit_name == other_match.group(1)
90
90
  else:
91
91
  reason_ratio = difflib.SequenceMatcher(None, self.failure_reason, job.get_failure_reason()).ratio()
@@ -198,12 +198,13 @@ class DeadReason(Reason):
198
198
  else:
199
199
  # I have BT but he doesn't, so we're different
200
200
  return False
201
-
202
- if self.last_tlog_line or job.get_last_tlog_line():
201
+ last_tlog_line = job.get_last_tlog_line()
202
+ if self.last_tlog_line is not None and last_tlog_line is not None:
203
203
  ratio = difflib.SequenceMatcher(None, self.last_tlog_line,
204
- job.get_last_tlog_line()).ratio()
204
+ last_tlog_line).ratio()
205
205
  return ratio > 0.5
206
- return True
206
+ else:
207
+ return self.last_tlog_line == last_tlog_line
207
208
 
208
209
 
209
210
  class TimeoutReason(Reason):
@@ -302,29 +303,34 @@ class Job(object):
302
303
  for line in file_obj:
303
304
  # Log prefix from teuthology.log
304
305
  if ".stderr:" in line:
306
+ # Only captures the error and disregard what ever comes before
305
307
  line = line.split(".stderr:")[1]
306
308
 
307
- if "FAILED assert" in line:
309
+ if "FAILED assert" in line or "__ceph_assert_fail" in line:
308
310
  assertion = line.strip()
309
311
 
310
312
  if line.startswith(" ceph version"):
311
313
  # The start of a backtrace!
312
314
  bt_lines = [line]
313
- elif line.startswith(" NOTE: a copy of the executable"):
314
- # The backtrace terminated, if we have a buffer return it
315
- if len(bt_lines):
316
- return ("".join(bt_lines)).strip(), assertion
317
- else:
318
- log.warning("Saw end of BT but not start")
315
+ elif line.startswith(" NOTE: a copy of the executable") or \
316
+ line.strip().endswith("clone()") or \
317
+ "clone()+0x" in line:
318
+ # The backtrace terminated, if we have a buffer return it
319
+ if len(bt_lines):
320
+ return ("".join(bt_lines)).strip(), assertion
321
+ else:
322
+ log.warning("Saw end of BT but not start")
319
323
  elif bt_lines:
320
324
  # We're in a backtrace, push the line onto the list
321
325
  if len(bt_lines) > MAX_BT_LINES:
322
- # Something wrong with our parsing, drop it
323
- log.warning("Ignoring malparsed backtrace: {0}".format(
326
+ # We exceeded MAX_BT_LINES, drop it
327
+ log.warning("Ignoring backtrace that exceeds MAX_BACKTRACE_LINES: {0}".format(
324
328
  ", ".join(bt_lines[0:3])
325
329
  ))
330
+ log.warning("Either the backtrace is too long or we did a bad job of checking for end of backtrace!")
326
331
  bt_lines = []
327
- bt_lines.append(line)
332
+ else:
333
+ bt_lines.append(line)
328
334
 
329
335
  return None, assertion
330
336
 
@@ -357,7 +363,7 @@ class Job(object):
357
363
  for line in grep(tlog_path, "command crashed with signal"):
358
364
  log.debug("Found a crash indication: {0}".format(line))
359
365
  # tasks.ceph.osd.1.plana82.stderr
360
- match = re.search("tasks.ceph.([^\.]+).([^\.]+).([^\.]+).stderr", line)
366
+ match = re.search(r"tasks.ceph.([^\.]+).([^\.]+).([^\.]+).stderr", line)
361
367
  if not match:
362
368
  log.warning("Not-understood crash indication {0}".format(line))
363
369
  continue
@@ -410,7 +416,7 @@ class ValgrindReason(Reason):
410
416
  log.warning("Misunderstood line: {0}".format(line))
411
417
  continue
412
418
  err_typ, log_basename = match.groups()
413
- svc_typ = six.ensure_str(log_basename).split(".")[0]
419
+ svc_typ = log_basename.split(".")[0]
414
420
  if err_typ not in result[svc_typ]:
415
421
  result[svc_typ].append(err_typ)
416
422
  result[svc_typ] = sorted(result[svc_typ])