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
@@ -0,0 +1,374 @@
1
+ import datetime
2
+ import logging
3
+ import os
4
+ import subprocess
5
+ import time
6
+ import yaml
7
+ import requests
8
+
9
+ from urllib.parse import urljoin
10
+
11
+ from teuthology import exporter, dispatcher, kill, report, safepath
12
+ from teuthology.config import config as teuth_config
13
+ from teuthology.exceptions import SkipJob, MaxWhileTries
14
+ from teuthology import setup_log_file, install_except_hook
15
+ from teuthology.misc import get_user, archive_logs, compress_logs
16
+ from teuthology.config import FakeNamespace
17
+ from teuthology.lock import ops as lock_ops
18
+ from teuthology.task import internal
19
+ from teuthology.misc import decanonicalize_hostname as shortname
20
+ from teuthology.lock import query
21
+ from teuthology.util import sentry
22
+
23
+ log = logging.getLogger(__name__)
24
+
25
+
26
+ def main(args):
27
+ with open(args.job_config, 'r') as config_file:
28
+ job_config = yaml.safe_load(config_file)
29
+
30
+ loglevel = logging.INFO
31
+ if args.verbose:
32
+ loglevel = logging.DEBUG
33
+ logging.getLogger().setLevel(loglevel)
34
+ log.setLevel(loglevel)
35
+
36
+ log_file_path = os.path.join(job_config['archive_path'],
37
+ f"supervisor.{job_config['job_id']}.log")
38
+ setup_log_file(log_file_path)
39
+ install_except_hook()
40
+ try:
41
+ dispatcher.check_job_expiration(job_config)
42
+ except SkipJob:
43
+ return 0
44
+
45
+ # reimage target machines before running the job
46
+ if 'targets' in job_config:
47
+ node_count = len(job_config["targets"])
48
+ # If a job (e.g. from the nop suite) doesn't need nodes, avoid
49
+ # submitting a zero here.
50
+ if node_count:
51
+ with exporter.NodeReimagingTime().time(
52
+ machine_type=job_config["machine_type"],
53
+ node_count=node_count,
54
+ ):
55
+ reimage(job_config)
56
+ else:
57
+ reimage(job_config)
58
+ with open(args.job_config, 'w') as f:
59
+ yaml.safe_dump(job_config, f, default_flow_style=False)
60
+
61
+ suite = job_config.get("suite")
62
+ if suite:
63
+ with exporter.JobTime().time(suite=suite):
64
+ return run_job(
65
+ job_config,
66
+ args.bin_path,
67
+ args.archive_dir,
68
+ args.verbose
69
+ )
70
+ else:
71
+ return run_job(
72
+ job_config,
73
+ args.bin_path,
74
+ args.archive_dir,
75
+ args.verbose
76
+ )
77
+
78
+
79
+ def run_job(job_config, teuth_bin_path, archive_dir, verbose):
80
+ safe_archive = safepath.munge(job_config['name'])
81
+ if job_config.get('first_in_suite') or job_config.get('last_in_suite'):
82
+ job_archive = os.path.join(archive_dir, safe_archive)
83
+ args = [
84
+ os.path.join(teuth_bin_path, 'teuthology-results'),
85
+ '--archive-dir', job_archive,
86
+ '--name', job_config['name'],
87
+ ]
88
+ if job_config.get('first_in_suite'):
89
+ log.info('Generating memo for %s', job_config['name'])
90
+ if job_config.get('seed'):
91
+ args.extend(['--seed', job_config['seed']])
92
+ if job_config.get('subset'):
93
+ args.extend(['--subset', job_config['subset']])
94
+ if job_config.get('no_nested_subset'):
95
+ args.extend(['--no-nested-subset'])
96
+ else:
97
+ log.info('Generating results for %s', job_config['name'])
98
+ timeout = job_config.get('results_timeout',
99
+ teuth_config.results_timeout)
100
+ args.extend(['--timeout', str(timeout)])
101
+ if job_config.get('email'):
102
+ args.extend(['--email', job_config['email']])
103
+ # Execute teuthology-results, passing 'preexec_fn=os.setpgrp' to
104
+ # make sure that it will continue to run if this worker process
105
+ # dies (e.g. because of a restart)
106
+ result_proc = subprocess.Popen(args=args, preexec_fn=os.setpgrp)
107
+ log.info("teuthology-results PID: %s", result_proc.pid)
108
+ # Remove unnecessary logs for first and last jobs in run
109
+ log.info('Deleting job\'s archive dir %s', job_config['archive_path'])
110
+ for f in os.listdir(job_config['archive_path']):
111
+ os.remove(os.path.join(job_config['archive_path'], f))
112
+ os.rmdir(job_config['archive_path'])
113
+ return
114
+
115
+ log.info('Running job %s', job_config['job_id'])
116
+
117
+ arg = [
118
+ os.path.join(teuth_bin_path, 'teuthology'),
119
+ ]
120
+ # The following is for compatibility with older schedulers, from before we
121
+ # started merging the contents of job_config['config'] into job_config
122
+ # itself.
123
+ if 'config' in job_config:
124
+ inner_config = job_config.pop('config')
125
+ if not isinstance(inner_config, dict):
126
+ log.warning("run_job: job_config['config'] isn't a dict, it's a %s",
127
+ str(type(inner_config)))
128
+ else:
129
+ job_config.update(inner_config)
130
+
131
+ if verbose or job_config['verbose']:
132
+ arg.append('-v')
133
+
134
+ arg.extend([
135
+ '--owner', job_config['owner'],
136
+ '--archive', job_config['archive_path'],
137
+ '--name', job_config['name'],
138
+ ])
139
+ if job_config['description'] is not None:
140
+ arg.extend(['--description', job_config['description']])
141
+ job_archive = os.path.join(job_config['archive_path'], 'orig.config.yaml')
142
+ arg.extend(['--', job_archive])
143
+
144
+ log.debug("Running: %s" % ' '.join(arg))
145
+ p = subprocess.Popen(
146
+ args=arg,
147
+ stdout=subprocess.DEVNULL,
148
+ stderr=subprocess.DEVNULL,
149
+ )
150
+ log.info("Job archive: %s", job_config['archive_path'])
151
+ log.info("Job PID: %s", str(p.pid))
152
+
153
+ if teuth_config.results_server:
154
+ log.info("Running with watchdog")
155
+ try:
156
+ run_with_watchdog(p, job_config)
157
+ except Exception:
158
+ log.exception("run_with_watchdog had an unhandled exception")
159
+ raise
160
+ else:
161
+ log.info("Running without watchdog")
162
+ # This sleep() is to give the child time to start up and create the
163
+ # archive dir.
164
+ time.sleep(5)
165
+ p.wait()
166
+
167
+ if p.returncode != 0:
168
+ log.error('Child exited with code %d', p.returncode)
169
+ else:
170
+ log.info('Success!')
171
+ if 'targets' in job_config:
172
+ unlock_targets(job_config)
173
+ return p.returncode
174
+
175
+ def failure_is_reimage(failure_reason):
176
+ if not failure_reason:
177
+ return False
178
+ reimage_failure = "Error reimaging machines:"
179
+ if reimage_failure in failure_reason:
180
+ return True
181
+ else:
182
+ return False
183
+
184
+
185
+ def check_for_reimage_failures_and_mark_down(targets, count=10):
186
+ # Grab paddles history of jobs in the machine
187
+ # and count the number of reimaging errors
188
+ # if it fails N times then mark the machine down
189
+ base_url = teuth_config.results_server
190
+ for k, _ in targets.items():
191
+ machine = k.split('@')[-1]
192
+ url = urljoin(
193
+ base_url,
194
+ '/nodes/{0}/jobs/?count={1}'.format(machine, count)
195
+ )
196
+ resp = requests.get(url)
197
+ jobs = resp.json()
198
+ if len(jobs) < count:
199
+ continue
200
+ reimage_failures = list(filter(
201
+ lambda j: failure_is_reimage(j['failure_reason']),
202
+ jobs
203
+ ))
204
+ if len(reimage_failures) < count:
205
+ continue
206
+ # Mark machine down
207
+ machine_name = shortname(k)
208
+ lock_ops.update_lock(
209
+ machine_name,
210
+ description='reimage failed {0} times'.format(count),
211
+ status='down',
212
+ )
213
+ log.error(
214
+ 'Reimage failed {0} times ... marking machine down'.format(count)
215
+ )
216
+
217
+
218
+ def reimage(job_config):
219
+ # Reimage the targets specified in job config
220
+ # and update their keys in config after reimaging
221
+ ctx = create_fake_context(job_config)
222
+ # change the status during the reimaging process
223
+ report.try_push_job_info(ctx.config, dict(status='waiting'))
224
+ targets = job_config['targets']
225
+ try:
226
+ reimaged = lock_ops.reimage_machines(ctx, targets, job_config['machine_type'])
227
+ except Exception as e:
228
+ log.exception('Reimaging error. Nuking machines...')
229
+ # Reimage failures should map to the 'dead' status instead of 'fail'
230
+ report.try_push_job_info(
231
+ ctx.config,
232
+ dict(status='dead', failure_reason='Error reimaging machines: ' + str(e))
233
+ )
234
+ # There isn't an actual task called "reimage", but it doesn't seem
235
+ # necessary to create a whole new Sentry tag for this.
236
+ ctx.summary = {
237
+ 'sentry_event': sentry.report_error(job_config, e, task_name="reimage")
238
+ }
239
+ # Machine that fails to reimage after 10 times will be marked down
240
+ check_for_reimage_failures_and_mark_down(targets)
241
+ raise
242
+ ctx.config['targets'] = reimaged
243
+ # change the status to running after the reimaging process
244
+ report.try_push_job_info(ctx.config, dict(status='running'))
245
+
246
+
247
+ def unlock_targets(job_config):
248
+ serializer = report.ResultsSerializer(teuth_config.archive_base)
249
+ job_info = serializer.job_info(job_config['name'], job_config['job_id'])
250
+ machine_statuses = query.get_statuses(job_info['targets'].keys())
251
+ # only unlock targets if locked and description matches
252
+ locked = []
253
+ for status in machine_statuses:
254
+ name = shortname(status['name'])
255
+ description = status['description']
256
+ if not status['locked']:
257
+ continue
258
+ if description != job_info['archive_path']:
259
+ log.warning(
260
+ "Was going to unlock %s but it was locked by another job: %s",
261
+ name, description
262
+ )
263
+ continue
264
+ locked.append(name)
265
+ if not locked:
266
+ return
267
+ if job_config.get("unlock_on_failure", True):
268
+ log.info('Unlocking machines...')
269
+ lock_ops.unlock_safe(locked, job_info["owner"], job_info["name"], job_info["job_id"])
270
+
271
+
272
+ def run_with_watchdog(process, job_config):
273
+ job_start_time = datetime.datetime.now(datetime.timezone.utc)
274
+
275
+ # Only push the information that's relevant to the watchdog, to save db
276
+ # load
277
+ job_info = dict(
278
+ name=job_config['name'],
279
+ job_id=job_config['job_id'],
280
+ )
281
+
282
+ # Sleep once outside of the loop to avoid double-posting jobs
283
+ time.sleep(teuth_config.watchdog_interval)
284
+ hit_max_timeout = False
285
+ while process.poll() is None:
286
+ # Kill jobs that have been running longer than the global max
287
+ run_time = datetime.datetime.now(datetime.timezone.utc) - job_start_time
288
+ total_seconds = run_time.days * 60 * 60 * 24 + run_time.seconds
289
+ if total_seconds > teuth_config.max_job_time:
290
+ hit_max_timeout = True
291
+ log.warning("Job ran longer than {max}s. Killing...".format(
292
+ max=teuth_config.max_job_time))
293
+ try:
294
+ # kill processes but do not unlock yet so we can save
295
+ # the logs, coredumps, etc.
296
+ kill.kill_job(
297
+ job_info['name'], job_info['job_id'],
298
+ teuth_config.archive_base, job_config['owner'],
299
+ skip_unlock=True
300
+ )
301
+ except Exception:
302
+ log.exception('Failed to kill job')
303
+
304
+ try:
305
+ transfer_archives(job_info['name'], job_info['job_id'],
306
+ teuth_config.archive_base, job_config)
307
+ except Exception:
308
+ log.exception('Could not save logs')
309
+
310
+ try:
311
+ # this time remove everything and unlock the machines
312
+ kill.kill_job(
313
+ job_info['name'], job_info['job_id'],
314
+ teuth_config.archive_base, job_config['owner']
315
+ )
316
+ except Exception:
317
+ log.exception('Failed to kill job and unlock machines')
318
+
319
+ # calling this without a status just updates the jobs updated time
320
+ try:
321
+ report.try_push_job_info(job_info)
322
+ except MaxWhileTries:
323
+ log.exception("Failed to report job status; ignoring")
324
+ time.sleep(teuth_config.watchdog_interval)
325
+
326
+ # we no longer support testing theses old branches
327
+ assert(job_config.get('teuthology_branch') not in ('argonaut', 'bobtail',
328
+ 'cuttlefish', 'dumpling'))
329
+
330
+ # Let's make sure that paddles knows the job is finished. We don't know
331
+ # the status, but if it was a pass or fail it will have already been
332
+ # reported to paddles. In that case paddles ignores the 'dead' status.
333
+ # If the job was killed, paddles will use the 'dead' status.
334
+ extra_info = dict(status='dead')
335
+ if hit_max_timeout:
336
+ extra_info['failure_reason'] = 'hit max job timeout'
337
+ if not (job_config.get('first_in_suite') or job_config.get('last_in_suite')):
338
+ report.try_push_job_info(job_info, extra_info)
339
+
340
+
341
+ def create_fake_context(job_config, block=False):
342
+ owner = job_config.get('owner', get_user())
343
+ os_version = job_config.get('os_version', None)
344
+
345
+ ctx_args = {
346
+ 'config': job_config,
347
+ 'block': block,
348
+ 'owner': owner,
349
+ 'archive': job_config['archive_path'],
350
+ 'machine_type': job_config['machine_type'],
351
+ 'os_type': job_config.get('os_type', 'ubuntu'),
352
+ 'os_version': os_version,
353
+ 'name': job_config['name'],
354
+ 'job_id': job_config['job_id'],
355
+ }
356
+
357
+ return FakeNamespace(ctx_args)
358
+
359
+
360
+ def transfer_archives(run_name, job_id, archive_base, job_config):
361
+ serializer = report.ResultsSerializer(archive_base)
362
+ job_info = serializer.job_info(run_name, job_id, simple=True)
363
+
364
+ if 'archive' in job_info:
365
+ ctx = create_fake_context(job_config)
366
+ internal.add_remotes(ctx, job_config)
367
+
368
+ for log_type, log_path in job_info['archive'].items():
369
+ if log_type == 'init':
370
+ log_type = ''
371
+ compress_logs(ctx, log_path)
372
+ archive_logs(ctx, log_path, log_type)
373
+ else:
374
+ log.info('No archives to transfer.')
teuthology/exceptions.py CHANGED
@@ -12,6 +12,18 @@ class BranchNotFoundError(ValueError):
12
12
  branch=self.branch, repo_str=repo_str)
13
13
 
14
14
 
15
+ class BranchMismatchError(ValueError):
16
+ def __init__(self, branch, repo, reason=None):
17
+ self.branch = branch
18
+ self.repo = repo
19
+ self.reason = reason
20
+
21
+ def __str__(self):
22
+ msg = f"Cannot use branch {self.branch} with repo {self.repo}"
23
+ if self.reason:
24
+ msg = f"{msg} because {self.reason}"
25
+ return msg
26
+
15
27
  class CommitNotFoundError(ValueError):
16
28
  def __init__(self, commit, repo=None):
17
29
  self.commit = commit
@@ -68,6 +80,17 @@ class CommandFailedError(Exception):
68
80
  prefix=prefix,
69
81
  )
70
82
 
83
+ def fingerprint(self):
84
+ """
85
+ Returns a list of strings to group failures with.
86
+ Used by sentry instead of grouping by backtrace.
87
+ """
88
+ return [
89
+ self.label or self.command,
90
+ 'exit status {}'.format(self.exitstatus),
91
+ '{{ type }}',
92
+ ]
93
+
71
94
 
72
95
  class AnsibleFailedError(Exception):
73
96
 
@@ -82,6 +105,13 @@ class AnsibleFailedError(Exception):
82
105
  failures=self.failures,
83
106
  )
84
107
 
108
+ def fingerprint(self):
109
+ """
110
+ Sentry will use this to group events by their failure reasons, rather
111
+ than lumping all AnsibleFailedErrors together
112
+ """
113
+ return self.failures
114
+
85
115
 
86
116
  class CommandCrashedError(Exception):
87
117
 
@@ -182,3 +212,27 @@ class NoRemoteError(Exception):
182
212
 
183
213
  def __str__(self):
184
214
  return self.message
215
+
216
+
217
+ class UnitTestError(Exception):
218
+ """
219
+ Exception thrown on unit test failure
220
+ """
221
+ def __init__(self, exitstatus=None, node=None, label=None, message=None):
222
+ self.exitstatus = exitstatus
223
+ self.node = node
224
+ self.label = label
225
+ self.message = message
226
+
227
+ def __str__(self):
228
+ prefix = "Unit test failed"
229
+ if self.label:
230
+ prefix += " ({label})".format(label=self.label)
231
+ if self.node:
232
+ prefix += " on {node}".format(node=self.node)
233
+ if self.exitstatus:
234
+ prefix += " with status {status}".format(status=self.exitstatus)
235
+ return "{prefix}: '{message}'".format(
236
+ prefix=prefix,
237
+ message=self.message,
238
+ )