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
@@ -9,7 +9,7 @@ from paramiko import ChannelFile
9
9
  import gevent
10
10
  import gevent.event
11
11
  import socket
12
- import pipes
12
+ import shlex
13
13
  import logging
14
14
  import shutil
15
15
 
@@ -90,7 +90,7 @@ class RemoteProcess(object):
90
90
  Execute remote command
91
91
  """
92
92
  for line in self.command.split('\n'):
93
- log.getChild(self.hostname).info('%s> %s' % (self.label or '', line))
93
+ log.getChild(self.hostname).debug('%s> %s' % (self.label or '', line))
94
94
 
95
95
  if hasattr(self, 'timeout'):
96
96
  (self._stdin_buf, self._stdout_buf, self._stderr_buf) = \
@@ -114,7 +114,7 @@ class RemoteProcess(object):
114
114
  # FIXME: Is this actually true?
115
115
  raise RuntimeError(self.deadlock_warning % 'stdin')
116
116
 
117
- def setup_output_stream(self, stream_obj, stream_name):
117
+ def setup_output_stream(self, stream_obj, stream_name, quiet=False):
118
118
  if stream_obj is not PIPE:
119
119
  # Log the stream
120
120
  host_log = self.logger.getChild(self.hostname)
@@ -125,6 +125,7 @@ class RemoteProcess(object):
125
125
  getattr(self, stream_name),
126
126
  stream_log,
127
127
  stream_obj,
128
+ quiet,
128
129
  )
129
130
  )
130
131
  setattr(self, stream_name, stream_obj)
@@ -251,14 +252,14 @@ def quote(args):
251
252
  if isinstance(a, Raw):
252
253
  yield a.value
253
254
  else:
254
- yield pipes.quote(a)
255
+ yield shlex.quote(a)
255
256
  if isinstance(args, list):
256
257
  return ' '.join(_quote(args))
257
258
  else:
258
259
  return args
259
260
 
260
261
 
261
- def copy_to_log(f, logger, loglevel=logging.INFO, capture=None):
262
+ def copy_to_log(f, logger, loglevel=logging.INFO, capture=None, quiet=False):
262
263
  """
263
264
  Copy line by line from file in f to the log from logger
264
265
 
@@ -266,6 +267,8 @@ def copy_to_log(f, logger, loglevel=logging.INFO, capture=None):
266
267
  :param logger: the destination logger object
267
268
  :param loglevel: the level of logging data
268
269
  :param capture: an optional stream object for data copy
270
+ :param quiet: suppress `logger` usage if True, this is useful only
271
+ in combination with `capture`, defaults False
269
272
  """
270
273
  # Work-around for http://tracker.ceph.com/issues/8313
271
274
  if isinstance(f, ChannelFile):
@@ -284,6 +287,8 @@ def copy_to_log(f, logger, loglevel=logging.INFO, capture=None):
284
287
  capture.write(line)
285
288
  line = line.rstrip()
286
289
  # Second part of work-around for http://tracker.ceph.com/issues/8313
290
+ if quiet:
291
+ continue
287
292
  try:
288
293
  if isinstance(line, bytes):
289
294
  line = line.decode('utf-8', 'replace')
@@ -305,15 +310,17 @@ def copy_and_close(src, fdst):
305
310
  fdst.close()
306
311
 
307
312
 
308
- def copy_file_to(src, logger, stream=None):
313
+ def copy_file_to(src, logger, stream=None, quiet=False):
309
314
  """
310
315
  Copy file
311
316
  :param src: file to be copied.
312
317
  :param logger: the logger object
313
- :param stream: an optional file-like object which will receive a copy of
314
- src.
318
+ :param stream: an optional file-like object which will receive
319
+ a copy of src.
320
+ :param quiet: disable logger usage if True, useful in combination
321
+ with `stream` parameter, defaults False.
315
322
  """
316
- copy_to_log(src, logger, capture=stream)
323
+ copy_to_log(src, logger, capture=stream, quiet=quiet)
317
324
 
318
325
  def spawn_asyncresult(fn, *args, **kwargs):
319
326
  """
@@ -384,6 +391,7 @@ def run(
384
391
  wait=True,
385
392
  name=None,
386
393
  label=None,
394
+ quiet=False,
387
395
  timeout=None,
388
396
  cwd=None,
389
397
  # omit_sudo is used by vstart_runner.py
@@ -392,7 +400,7 @@ def run(
392
400
  """
393
401
  Run a command remotely. If any of 'args' contains shell metacharacters
394
402
  that you want to pass unquoted, pass it as an instance of Raw(); otherwise
395
- it will be quoted with pipes.quote() (single quote, and single quotes
403
+ it will be quoted with shlex.quote() (single quote, and single quotes
396
404
  enclosed in double quotes).
397
405
 
398
406
  :param client: SSHConnection to run the command with
@@ -417,6 +425,7 @@ def run(
417
425
  :param name: Human readable name (probably hostname) of the destination
418
426
  host
419
427
  :param label: Can be used to label or describe what the command is doing.
428
+ :param quiet: Do not log command's stdout and stderr, defaults False.
420
429
  :param timeout: timeout value for args to complete on remote channel of
421
430
  paramiko
422
431
  :param cwd: Directory in which the command should be executed.
@@ -440,8 +449,8 @@ def run(
440
449
  cwd=cwd)
441
450
  r.execute()
442
451
  r.setup_stdin(stdin)
443
- r.setup_output_stream(stderr, 'stderr')
444
- r.setup_output_stream(stdout, 'stdout')
452
+ r.setup_output_stream(stderr, 'stderr', quiet)
453
+ r.setup_output_stream(stdout, 'stdout', quiet)
445
454
  if wait:
446
455
  r.wait()
447
456
  return r
teuthology/packaging.py CHANGED
@@ -395,12 +395,12 @@ def _get_config_value_for_remote(ctx, remote, config, key):
395
395
 
396
396
  config = {
397
397
  'all':
398
- {'branch': 'master'},
398
+ {'branch': 'main'},
399
399
  'branch': 'next'
400
400
  }
401
401
  _get_config_value_for_remote(ctx, remote, config, 'branch')
402
402
 
403
- would return 'master'.
403
+ would return 'main'.
404
404
 
405
405
  :param ctx: the argparse.Namespace object
406
406
  :param remote: the teuthology.orchestra.remote.Remote object
@@ -479,7 +479,7 @@ class GitbuilderProject(object):
479
479
  )
480
480
  # when we're initializing with a remote we most likely have
481
481
  # a task config, not the entire teuthology job config
482
- self.flavor = self.job_config.get("flavor", "basic")
482
+ self.flavor = self.job_config.get("flavor", "default")
483
483
  self.tag = self.job_config.get("tag")
484
484
 
485
485
  def _init_from_config(self):
@@ -549,10 +549,6 @@ class GitbuilderProject(object):
549
549
  """
550
550
  The base url that points at this project on gitbuilder.
551
551
 
552
- For example::
553
-
554
- http://gitbuilder.ceph.com/ceph-deb-raring-x86_64-basic/ref/master
555
-
556
552
  :returns: A string of the base url for this project
557
553
  """
558
554
  return self._get_base_url()
@@ -656,9 +652,9 @@ class GitbuilderProject(object):
656
652
  remote, the sha1 from the config will be used.
657
653
 
658
654
  If a tag, branch or sha1 can't be found it will default to use the
659
- build from the master branch.
655
+ build from the main branch.
660
656
 
661
- :returns: A string URI. Ex: ref/master
657
+ :returns: A string URI. Ex: ref/main
662
658
  """
663
659
  ref_name, ref_val = next(iter(self._choose_reference().items()))
664
660
  if ref_name == 'sha1':
@@ -673,7 +669,7 @@ class GitbuilderProject(object):
673
669
  Decide which to use.
674
670
 
675
671
  :returns: a single-key dict containing the name and value of the
676
- reference to use, e.g. {'branch': 'master'}
672
+ reference to use, e.g. {'branch': 'main'}
677
673
  """
678
674
  tag = branch = sha1 = None
679
675
  if self.remote:
@@ -716,8 +712,8 @@ class GitbuilderProject(object):
716
712
  warn('sha1')
717
713
  return dict(sha1=sha1)
718
714
  else:
719
- log.warning("defaulting to master branch")
720
- return dict(branch='master')
715
+ log.warning("defaulting to main branch")
716
+ return dict(branch='main')
721
717
 
722
718
  def _get_base_url(self):
723
719
  """
@@ -853,6 +849,9 @@ class ShamanProject(GitbuilderProject):
853
849
  super(ShamanProject, self).__init__(project, job_config, ctx, remote)
854
850
  self.query_url = 'https://%s/api/' % config.shaman_host
855
851
 
852
+ # Force to use the "noarch" instead to build the uri.
853
+ self.force_noarch = self.job_config.get("shaman", {}).get("force_noarch", False)
854
+
856
855
  def _get_base_url(self):
857
856
  self.assert_result()
858
857
  return self._result.json()[0]['url']
@@ -876,13 +875,12 @@ class ShamanProject(GitbuilderProject):
876
875
  @property
877
876
  def _search_uri(self):
878
877
  flavor = self.flavor
879
- if flavor == 'basic':
880
- flavor = 'default'
881
878
  req_obj = OrderedDict()
882
879
  req_obj['status'] = 'ready'
883
880
  req_obj['project'] = self.project
884
881
  req_obj['flavor'] = flavor
885
- req_obj['distros'] = '%s/%s' % (self.distro, self.arch)
882
+ arch = "noarch" if self.force_noarch else self.arch
883
+ req_obj['distros'] = '%s/%s' % (self.distro, arch)
886
884
  ref_name, ref_val = list(self._choose_reference().items())[0]
887
885
  if ref_name == 'tag':
888
886
  req_obj['sha1'] = self._sha1 = self._tag_to_sha1()
@@ -970,6 +968,47 @@ class ShamanProject(GitbuilderProject):
970
968
  'repo',
971
969
  )
972
970
 
971
+ @property
972
+ def build_complete(self):
973
+ # use the repo search results to get a ref and a sha1; the
974
+ # input to teuthology-suite doesn't contain both
975
+ try:
976
+ self.assert_result()
977
+ except VersionNotFoundError:
978
+ return False
979
+
980
+ # self._result has status, project, flavor, distros, arch, and sha1
981
+ # restrictions, so the only reason for multiples should be "multiple
982
+ # builds of the same sha1 etc."; the first entry is the newest
983
+ search_result = self._result.json()[0]
984
+
985
+ # now look for the build complete status
986
+ path = '/'.join(
987
+ ('builds/ceph', search_result['ref'], search_result['sha1'])
988
+ )
989
+ build_url = urljoin(self.query_url, path)
990
+
991
+ try:
992
+ resp = requests.get(build_url)
993
+ resp.raise_for_status()
994
+ except requests.HttpError:
995
+ return False
996
+ log.debug(f'looking for {self.distro} {self.arch} {self.flavor}')
997
+ for build in resp.json():
998
+ log.debug(f'build: {build["distro"]}/{build["distro_version"]} {build["distro_arch"]} {build["flavor"]}')
999
+ if (
1000
+ # we must compare build arch to self.arch, since shaman's
1001
+ # results can have multiple arches but we're searching
1002
+ # for precisely one here
1003
+ build['distro'] == search_result['distro'] and
1004
+ build['distro_version'] == search_result['distro_version'] and
1005
+ build['flavor'] == search_result['flavor'] and
1006
+ build['distro_arch'] == self.arch and
1007
+ build['status'] == 'completed'
1008
+ ):
1009
+ return True
1010
+ return False
1011
+
973
1012
  def _get_repo(self):
974
1013
  resp = requests.get(self.repo_url)
975
1014
  resp.raise_for_status()
@@ -1,5 +1,7 @@
1
1
  import logging
2
+ import os
2
3
 
4
+ import teuthology.exporter
3
5
  import teuthology.lock.query
4
6
  from teuthology.misc import decanonicalize_hostname, get_distro, get_distro_version
5
7
 
@@ -8,19 +10,19 @@ from teuthology.provision import downburst
8
10
  from teuthology.provision import fog
9
11
  from teuthology.provision import openstack
10
12
  from teuthology.provision import pelagos
11
- import os
12
13
 
13
14
  log = logging.getLogger(__name__)
14
15
 
15
16
 
16
- def _logfile(ctx, shortname):
17
- if hasattr(ctx, 'config') and ctx.config.get('archive_path'):
18
- return os.path.join(ctx.config['archive_path'],
19
- shortname + '.downburst.log')
17
+ def _logfile(shortname: str, archive_path: str = ""):
18
+ if os.path.isfile(archive_path):
19
+ return f"{archive_path}/{shortname}.downburst.log"
20
+
20
21
 
21
22
  def get_reimage_types():
22
23
  return pelagos.get_types() + fog.get_types()
23
24
 
25
+
24
26
  def reimage(ctx, machine_name, machine_type):
25
27
  os_type = get_distro(ctx)
26
28
  os_version = get_distro_version(ctx)
@@ -36,7 +38,21 @@ def reimage(ctx, machine_name, machine_type):
36
38
  else:
37
39
  raise Exception("The machine_type '%s' is not known to any "
38
40
  "of configured provisioners" % machine_type)
39
- return obj.create()
41
+ status = "fail"
42
+ try:
43
+ result = obj.create()
44
+ status = "success"
45
+ except Exception:
46
+ # We only need this clause so that we avoid triggering the finally
47
+ # clause below in cases where the exception raised is KeyboardInterrupt
48
+ # or SystemExit
49
+ raise
50
+ finally:
51
+ teuthology.exporter.NodeReimagingResults().record(
52
+ machine_type=machine_type,
53
+ status=status,
54
+ )
55
+ return result
40
56
 
41
57
 
42
58
  def create_if_vm(ctx, machine_name, _downburst=None):
@@ -78,8 +94,12 @@ def create_if_vm(ctx, machine_name, _downburst=None):
78
94
  return dbrst.create()
79
95
 
80
96
 
81
- def destroy_if_vm(ctx, machine_name, user=None, description=None,
82
- _downburst=None):
97
+ def destroy_if_vm(
98
+ machine_name: str,
99
+ user: str = "",
100
+ description: str = "",
101
+ _downburst=None
102
+ ):
83
103
  """
84
104
  Use downburst to destroy a virtual machine
85
105
 
@@ -99,7 +119,7 @@ def destroy_if_vm(ctx, machine_name, user=None, description=None,
99
119
  log.error(msg.format(node=machine_name, as_user=user,
100
120
  locked_by=status_info['locked_by']))
101
121
  return False
102
- if (description is not None and description !=
122
+ if (description and description !=
103
123
  status_info['description']):
104
124
  msg = "Tried to destroy {node} with description {desc_arg} " + \
105
125
  "but it is locked with description {desc_lock}"
@@ -117,5 +137,5 @@ def destroy_if_vm(ctx, machine_name, user=None, description=None,
117
137
  dbrst = _downburst or \
118
138
  downburst.Downburst(name=machine_name, os_type=None,
119
139
  os_version=None, status=status_info,
120
- logfile=_logfile(ctx, shortname))
140
+ logfile=_logfile(description, shortname))
121
141
  return dbrst.destroy()
@@ -84,7 +84,13 @@ class OpenStackProvider(Provider):
84
84
  @property
85
85
  def images(self):
86
86
  if not hasattr(self, '_images'):
87
- self._images = retry(self.driver.list_images)
87
+ exclude_image = self.conf.get('exclude_image', [])
88
+ if exclude_image and not isinstance(exclude_image, list):
89
+ exclude_image = [exclude_image]
90
+ exclude_re = [re.compile(x) for x in exclude_image]
91
+ images = retry(self.driver.list_images)
92
+ self._images = [_ for _ in images
93
+ if not any(x.match(_.name) for x in exclude_re)]
88
94
  return self._images
89
95
 
90
96
  @property
@@ -126,7 +132,7 @@ class OpenStackProvider(Provider):
126
132
  else:
127
133
  self._networks = list()
128
134
  except AttributeError:
129
- log.warn("Unable to list networks for %s", self.driver)
135
+ log.warning("Unable to list networks for %s", self.driver)
130
136
  self._networks = list()
131
137
  return self._networks
132
138
 
@@ -144,7 +150,7 @@ class OpenStackProvider(Provider):
144
150
  self.driver.ex_list_security_groups
145
151
  )
146
152
  except AttributeError:
147
- log.warn("Unable to list security groups for %s", self.driver)
153
+ log.warning("Unable to list security groups for %s", self.driver)
148
154
  self._security_groups = list()
149
155
  return self._security_groups
150
156
 
@@ -420,7 +426,7 @@ class OpenStackProvisioner(base.Provisioner):
420
426
  msg = "Unknown error locating %s"
421
427
  if not matches:
422
428
  msg = "No nodes found with name '%s'" % self.name
423
- log.warn(msg)
429
+ log.warning(msg)
424
430
  return
425
431
  elif len(matches) > 1:
426
432
  msg = "More than one node found with name '%s'"
@@ -438,9 +444,9 @@ class OpenStackProvisioner(base.Provisioner):
438
444
  self._destroy_volumes()
439
445
  nodes = self._find_nodes()
440
446
  if not nodes:
441
- log.warn("Didn't find any nodes named '%s' to destroy!", self.name)
447
+ log.warning("Didn't find any nodes named '%s' to destroy!", self.name)
442
448
  return True
443
449
  if len(nodes) > 1:
444
- log.warn("Found multiple nodes named '%s' to destroy!", self.name)
450
+ log.warning("Found multiple nodes named '%s' to destroy!", self.name)
445
451
  log.info("Destroying nodes: %s", nodes)
446
452
  return all([node.destroy() for node in nodes])
@@ -1,5 +1,4 @@
1
1
  import datetime
2
- import dateutil.tz
3
2
  import dateutil.parser
4
3
  import json
5
4
  import os
@@ -103,7 +102,7 @@ class AuthToken(object):
103
102
  def expired(self):
104
103
  if self.expires is None:
105
104
  return True
106
- utcnow = datetime.datetime.now(dateutil.tz.tzutc())
105
+ utcnow = datetime.datetime.now(datetime.timezone.utc)
107
106
  offset = datetime.timedelta(minutes=30)
108
107
  return self.expires < (utcnow + offset)
109
108
 
@@ -8,6 +8,7 @@ import yaml
8
8
  from teuthology.config import config
9
9
  from teuthology.contextutil import safe_while
10
10
  from teuthology.misc import decanonicalize_hostname
11
+ from teuthology.misc import deep_merge
11
12
  from teuthology.lock import query
12
13
 
13
14
  log = logging.getLogger(__name__)
@@ -20,7 +21,11 @@ def downburst_executable():
20
21
  Return '' if no executable downburst is found.
21
22
  """
22
23
  if config.downburst:
23
- return config.downburst
24
+ if isinstance(config.downburst, dict):
25
+ if 'path' in config.downburst:
26
+ return config.downburst['path']
27
+ else:
28
+ return config.downburst
24
29
  path = os.environ.get('PATH', None)
25
30
  if path:
26
31
  for p in os.environ.get('PATH', '').split(os.pathsep):
@@ -37,6 +42,18 @@ def downburst_executable():
37
42
  return ''
38
43
 
39
44
 
45
+ def downburst_environment():
46
+ env = dict()
47
+ env['PATH'] = os.environ.get('PATH')
48
+ discover_url = os.environ.get('DOWNBURST_DISCOVER_URL')
49
+ if config.downburst and not discover_url:
50
+ if isinstance(config.downburst, dict):
51
+ discover_url = config.downburst.get('discover_url')
52
+ if discover_url:
53
+ env['DOWNBURST_DISCOVER_URL'] = discover_url
54
+ return env
55
+
56
+
40
57
  class Downburst(object):
41
58
  """
42
59
  A class that provides methods for creating and destroying virtual machine
@@ -55,6 +72,7 @@ class Downburst(object):
55
72
  self.logfile = logfile
56
73
  self.host = decanonicalize_hostname(self.status['vm_host']['name'])
57
74
  self.executable = downburst_executable()
75
+ self.environment = downburst_environment()
58
76
 
59
77
  def create(self):
60
78
  """
@@ -118,7 +136,10 @@ class Downburst(object):
118
136
  distro=self.os_type,
119
137
  distroversion=self.os_version
120
138
  ))
121
- proc = subprocess.Popen(args, stdout=subprocess.PIPE,
139
+ log.debug(args)
140
+ proc = subprocess.Popen(args, universal_newlines=True,
141
+ env=self.environment,
142
+ stdout=subprocess.PIPE,
122
143
  stderr=subprocess.PIPE)
123
144
  out, err = proc.communicate()
124
145
  return (proc.returncode, out, err)
@@ -135,7 +156,9 @@ class Downburst(object):
135
156
  if self.logfile:
136
157
  args.extend(['-l', self.logfile])
137
158
  args.extend(['destroy', self.shortname])
138
- proc = subprocess.Popen(args, stdout=subprocess.PIPE,
159
+ log.debug(args)
160
+ proc = subprocess.Popen(args, universal_newlines=True,
161
+ stdout=subprocess.PIPE,
139
162
  stderr=subprocess.PIPE,)
140
163
  out, err = proc.communicate()
141
164
  log.info(out)
@@ -143,7 +166,7 @@ class Downburst(object):
143
166
  if proc.returncode != 0:
144
167
  not_found_msg = "no domain with matching name '%s'" % self.shortname
145
168
  if not_found_msg in err:
146
- log.warn("Ignoring error during destroy: %s", err)
169
+ log.warning("Ignoring error during destroy: %s", err)
147
170
  return True
148
171
  log.error("Error destroying %s: %s", self.name, err)
149
172
  return False
@@ -156,24 +179,44 @@ class Downburst(object):
156
179
  """
157
180
  Assemble a configuration to pass to downburst, and write it to a file.
158
181
  """
159
- config_fd = tempfile.NamedTemporaryFile(delete=False)
182
+ config_fd = tempfile.NamedTemporaryFile(delete=False, mode='wt')
160
183
 
161
184
  os_type = self.os_type.lower()
162
- mac_address = self.status['mac_address']
185
+ os_version = self.os_version.lower()
163
186
 
187
+ mac_address = self.status['mac_address']
188
+ defaults = dict(
189
+ downburst=dict(
190
+ machine=dict(
191
+ disk=os.environ.get('DOWNBURST_DISK_SIZE', '100G'),
192
+ ram=os.environ.get('DOWNBURST_RAM_SIZE', '3.8G'),
193
+ cpus=int(os.environ.get('DOWNBURST_CPUS', 1)),
194
+ volumes=dict(
195
+ count=int(os.environ.get('DOWNBURST_EXTRA_DISK_NUMBER', 4)),
196
+ size=os.environ.get('DOWNBURST_EXTRA_DISK_SIZE', '100G'),
197
+ ),
198
+ ),
199
+ )
200
+ )
201
+ downburst_config = defaults['downburst']
202
+ if config.downburst and isinstance(config.downburst, dict):
203
+ deep_merge(downburst_config, config.downburst)
204
+ log.debug('downburst_config: %s', downburst_config)
205
+ machine = downburst_config['machine']
206
+ log.debug('Using machine config: %s', machine)
164
207
  file_info = {
165
- 'disk-size': '100G',
166
- 'ram': '3.8G',
167
- 'cpus': 1,
208
+ 'disk-size': machine['disk'],
209
+ 'ram': machine['ram'],
210
+ 'cpus': machine['cpus'],
168
211
  'networks': [
169
212
  {'source': 'front', 'mac': mac_address}],
170
213
  'distro': os_type,
171
214
  'distroversion': self.os_version,
172
- 'additional-disks': 4,
173
- 'additional-disks-size': '100G',
215
+ 'additional-disks': machine['volumes']['count'],
216
+ 'additional-disks-size': machine['volumes']['size'],
174
217
  'arch': 'x86_64',
175
218
  }
176
- fqdn = self.name.split('@')[1]
219
+ fqdn = self.name.split('@')[-1]
177
220
  file_out = {
178
221
  'downburst': file_info,
179
222
  'local-hostname': fqdn,
@@ -188,6 +231,12 @@ class Downburst(object):
188
231
  ['passwd', '-d', self.user],
189
232
  ]
190
233
  }
234
+ # for opensuse-15.2 we need to replace systemd-logger with rsyslog for teuthology
235
+ if os_type == 'opensuse' and os_version == '15.2':
236
+ user_info['runcmd'].extend([
237
+ ['zypper', 'rm', '-y', 'systemd-logger'],
238
+ ['zypper', 'in', '-y', 'rsyslog'],
239
+ ])
191
240
  # Install git on downbursted VMs to clone upstream linux-firmware.
192
241
  # Issue #17154
193
242
  if 'packages' not in user_info:
@@ -208,7 +257,7 @@ class Downburst(object):
208
257
  # to install 'python' to get python2.7, which ansible needs
209
258
  if os_type in ('ubuntu', 'fedora'):
210
259
  user_info['packages'].append('python')
211
- user_fd = tempfile.NamedTemporaryFile(delete=False)
260
+ user_fd = tempfile.NamedTemporaryFile(delete=False, mode='wt')
212
261
  user_str = "#cloud-config\n" + yaml.safe_dump(user_info)
213
262
  user_fd.write(user_str)
214
263
  self.user_path = user_fd.name
@@ -240,27 +289,32 @@ def get_distro_from_downburst():
240
289
  or if downburst is unable to produce a json list, then use a default
241
290
  table.
242
291
  """
243
- default_table = {u'rhel_minimal': [u'6.4', u'6.5'],
244
- u'fedora': [u'17', u'18', u'19', u'20', u'22'],
245
- u'centos': [u'6.3', u'6.4', u'6.5', u'7.0',
246
- u'7.2'],
247
- u'centos_minimal': [u'6.4', u'6.5'],
248
- u'ubuntu': [u'8.04(hardy)', u'9.10(karmic)',
249
- u'10.04(lucid)', u'10.10(maverick)',
250
- u'11.04(natty)', u'11.10(oneiric)',
251
- u'12.04(precise)', u'12.10(quantal)',
252
- u'13.04(raring)', u'13.10(saucy)',
253
- u'14.04(trusty)', u'utopic(utopic)',
254
- u'16.04(xenial)'],
255
- u'sles': [u'11-sp2'],
256
- u'debian': [u'6.0', u'7.0', u'8.0']}
292
+ default_table = {'rhel_minimal': ['6.4', '6.5'],
293
+ 'fedora': ['17', '18', '19', '20', '22'],
294
+ 'centos': ['6.3', '6.4', '6.5', '7.0',
295
+ '7.2', '7.4', '8.2'],
296
+ 'centos_minimal': ['6.4', '6.5'],
297
+ 'ubuntu': ['8.04(hardy)', '9.10(karmic)',
298
+ '10.04(lucid)', '10.10(maverick)',
299
+ '11.04(natty)', '11.10(oneiric)',
300
+ '12.04(precise)', '12.10(quantal)',
301
+ '13.04(raring)', '13.10(saucy)',
302
+ '14.04(trusty)', 'utopic(utopic)',
303
+ '16.04(xenial)', '18.04(bionic)',
304
+ '20.04(focal)'],
305
+ 'sles': ['12-sp3', '15-sp1', '15-sp2'],
306
+ 'opensuse': ['12.3', '15.1', '15.2'],
307
+ 'debian': ['6.0', '7.0', '8.0']}
257
308
  executable_cmd = downburst_executable()
309
+ environment_dict = downburst_environment()
258
310
  if not executable_cmd:
259
- log.warn("Downburst not found!")
311
+ log.warning("Downburst not found!")
260
312
  log.info('Using default values for supported os_type/os_version')
261
313
  return default_table
262
314
  try:
263
- output = subprocess.check_output([executable_cmd, 'list-json'])
315
+ log.debug(executable_cmd)
316
+ output = subprocess.check_output([executable_cmd, 'list-json'],
317
+ env=environment_dict)
264
318
  downburst_data = json.loads(output)
265
319
  return downburst_data
266
320
  except (subprocess.CalledProcessError, OSError):