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
@@ -4,17 +4,21 @@ Support for paramiko remote objects.
4
4
 
5
5
  import teuthology.lock.query
6
6
  import teuthology.lock.util
7
+ from teuthology.contextutil import safe_while
7
8
  from teuthology.orchestra import run
8
9
  from teuthology.orchestra import connection
9
10
  from teuthology.orchestra import console
10
11
  from teuthology.orchestra.opsys import OS
12
+ import teuthology.provision
11
13
  from teuthology import misc
12
- from teuthology.exceptions import CommandFailedError
14
+ from teuthology.exceptions import CommandFailedError, UnitTestError
15
+ from teuthology.util.scanner import UnitTestScanner
13
16
  from teuthology.misc import host_shortname
14
- import time
17
+ import errno
15
18
  import re
16
19
  import logging
17
20
  from io import BytesIO
21
+ from io import StringIO
18
22
  import os
19
23
  import pwd
20
24
  import tempfile
@@ -23,8 +27,317 @@ import netaddr
23
27
  log = logging.getLogger(__name__)
24
28
 
25
29
 
26
- class Remote(object):
30
+ class RemoteShell(object):
31
+ """
32
+ Contains methods to run miscellaneous shell commands on remote machines.
33
+
34
+ These methods were originally part of orchestra.remote.Remote. The reason
35
+ for moving these methods from Remote is that applications that use
36
+ teuthology for testing usually have programs that can run tests locally on
37
+ a single node machine for development work (for example, vstart_runner.py
38
+ in case of Ceph). These programs can import and reuse these methods
39
+ without having to deal SSH stuff. In short, this class serves a shared
40
+ interface.
41
+
42
+ To use these methods, inherit the class here and implement "run()" method in
43
+ the subclass.
44
+ """
45
+
46
+ def remove(self, path):
47
+ self.run(args=['rm', '-fr', path])
48
+
49
+ def mkdtemp(self, suffix=None, parentdir=None):
50
+ """
51
+ Create a temporary directory on remote machine and return it's path.
52
+ """
53
+ args = ['mktemp', '-d']
54
+
55
+ if suffix:
56
+ args.append('--suffix=%s' % suffix)
57
+ if parentdir:
58
+ args.append('--tmpdir=%s' % parentdir)
59
+
60
+ return self.sh(args).strip()
61
+
62
+ def mktemp(self, suffix=None, parentdir=None, data=None):
63
+ """
64
+ Make a remote temporary file.
65
+
66
+ :param suffix: suffix for the temporary file
67
+ :param parentdir: parent dir where temp file should be created
68
+ :param data: write data to the file if provided
69
+
70
+ Returns: the path of the temp file created.
71
+ """
72
+ args = ['mktemp']
73
+ if suffix:
74
+ args.append('--suffix=%s' % suffix)
75
+ if parentdir:
76
+ args.append('--tmpdir=%s' % parentdir)
77
+
78
+ path = self.sh(args).strip()
79
+
80
+ if data:
81
+ self.write_file(path=path, data=data)
82
+
83
+ return path
84
+
85
+ def sh(self, script, **kwargs):
86
+ """
87
+ Shortcut for run method.
88
+
89
+ Usage:
90
+ my_name = remote.sh('whoami')
91
+ remote_date = remote.sh('date')
92
+ """
93
+ if 'stdout' not in kwargs:
94
+ kwargs['stdout'] = BytesIO()
95
+ if 'args' not in kwargs:
96
+ kwargs['args'] = script
97
+ proc = self.run(**kwargs)
98
+ out = proc.stdout.getvalue()
99
+ if isinstance(out, bytes):
100
+ return out.decode()
101
+ else:
102
+ return out
103
+
104
+ def sh_file(self, script, label="script", sudo=False, **kwargs):
105
+ """
106
+ Run shell script after copying its contents to a remote file
107
+
108
+ :param script: string with script text, or file object
109
+ :param sudo: run command with sudo if True,
110
+ run as user name if string value (defaults to False)
111
+ :param label: string value which will be part of file name
112
+ Returns: stdout
113
+ """
114
+ ftempl = '/tmp/teuthology-remote-$(date +%Y%m%d%H%M%S)-{}-XXXX'\
115
+ .format(label)
116
+ script_file = self.sh("mktemp %s" % ftempl).strip()
117
+ self.sh("cat - | tee {script} ; chmod a+rx {script}"\
118
+ .format(script=script_file), stdin=script)
119
+ if sudo:
120
+ if isinstance(sudo, str):
121
+ command="sudo -u %s %s" % (sudo, script_file)
122
+ else:
123
+ command="sudo %s" % script_file
124
+ else:
125
+ command="%s" % script_file
126
+
127
+ return self.sh(command, **kwargs)
128
+
129
+ def chmod(self, file_path, permissions):
130
+ """
131
+ As super-user, set permissions on the remote file specified.
132
+ """
133
+ args = [
134
+ 'sudo',
135
+ 'chmod',
136
+ permissions,
137
+ file_path,
138
+ ]
139
+ self.run(
140
+ args=args,
141
+ )
142
+
143
+ def chcon(self, file_path, context):
144
+ """
145
+ Set the SELinux context of a given file.
146
+
147
+ VMs and non-RPM-based hosts will skip this operation because ours
148
+ currently have SELinux disabled.
149
+
150
+ :param file_path: The path to the file
151
+ :param context: The SELinux context to be used
152
+ """
153
+ if self.os.package_type != 'rpm' or \
154
+ self.os.name in ['opensuse', 'sle']:
155
+ return
156
+ if teuthology.lock.query.is_vm(self.shortname):
157
+ return
158
+ self.run(args="sudo chcon {con} {path}".format(
159
+ con=context, path=file_path))
160
+
161
+ def copy_file(self, src, dst, sudo=False, mode=None, owner=None,
162
+ mkdir=False, append=False):
163
+ """
164
+ Copy data to remote file
165
+
166
+ :param src: source file path on remote host
167
+ :param dst: destination file path on remote host
168
+ :param sudo: use sudo to write file, defaults False
169
+ :param mode: set file mode bits if provided
170
+ :param owner: set file owner if provided
171
+ :param mkdir: ensure the destination directory exists, defaults
172
+ False
173
+ :param append: append data to the file, defaults False
174
+ """
175
+ dd = 'sudo dd' if sudo else 'dd'
176
+ args = dd + ' if=' + src + ' of=' + dst
177
+ if append:
178
+ args += ' conv=notrunc oflag=append'
179
+ if mkdir:
180
+ mkdirp = 'sudo mkdir -p' if sudo else 'mkdir -p'
181
+ dirpath = os.path.dirname(dst)
182
+ if dirpath:
183
+ args = mkdirp + ' ' + dirpath + '\n' + args
184
+ if mode:
185
+ chmod = 'sudo chmod' if sudo else 'chmod'
186
+ args += '\n' + chmod + ' ' + mode + ' ' + dst
187
+ if owner:
188
+ chown = 'sudo chown' if sudo else 'chown'
189
+ args += '\n' + chown + ' ' + owner + ' ' + dst
190
+ args = 'set -ex' + '\n' + args
191
+ self.run(args=args)
192
+
193
+ def move_file(self, src, dst, sudo=False, mode=None, owner=None,
194
+ mkdir=False):
195
+ """
196
+ Move data to remote file
197
+
198
+ :param src: source file path on remote host
199
+ :param dst: destination file path on remote host
200
+ :param sudo: use sudo to write file, defaults False
201
+ :param mode: set file mode bits if provided
202
+ :param owner: set file owner if provided
203
+ :param mkdir: ensure the destination directory exists, defaults
204
+ False
205
+ """
206
+ mv = 'sudo mv' if sudo else 'mv'
207
+ args = mv + ' ' + src + ' ' + dst
208
+ if mkdir:
209
+ mkdirp = 'sudo mkdir -p' if sudo else 'mkdir -p'
210
+ dirpath = os.path.dirname(dst)
211
+ if dirpath:
212
+ args = mkdirp + ' ' + dirpath + '\n' + args
213
+ if mode:
214
+ chmod = 'sudo chmod' if sudo else 'chmod'
215
+ args += ' && ' + chmod + ' ' + mode + ' ' + dst
216
+ if owner:
217
+ chown = 'sudo chown' if sudo else 'chown'
218
+ args += ' && ' + chown + ' ' + owner + ' ' + dst
219
+ self.run(args=args)
220
+
221
+ def read_file(self, path, sudo=False, stdout=None,
222
+ offset=0, length=0):
223
+ """
224
+ Read data from remote file
225
+
226
+ :param path: file path on remote host
227
+ :param sudo: use sudo to read the file, defaults False
228
+ :param stdout: output object, defaults to io.BytesIO()
229
+ :param offset: number of bytes to skip from the file
230
+ :param length: number of bytes to read from the file
231
+
232
+ :raises: :class:`FileNotFoundError`: there is no such file by the path
233
+ :raises: :class:`RuntimeError`: unexpected error occurred
234
+
235
+ :returns: the file contents in bytes, if stdout is `io.BytesIO`, by
236
+ default
237
+ :returns: the file contents in str, if stdout is `io.StringIO`
238
+ """
239
+ dd = 'sudo dd' if sudo else 'dd'
240
+ args = dd + ' if=' + path + ' of=/dev/stdout'
241
+ iflags=[]
242
+ # we have to set defaults here instead of the method's signature,
243
+ # because python is reusing the object from call to call
244
+ stdout = stdout or BytesIO()
245
+ if offset:
246
+ args += ' skip=' + str(offset)
247
+ iflags += 'skip_bytes'
248
+ if length:
249
+ args += ' count=' + str(length)
250
+ iflags += 'count_bytes'
251
+ if iflags:
252
+ args += ' iflag=' + ','.join(iflags)
253
+ args = 'set -ex' + '\n' + args
254
+ proc = self.run(args=args, stdout=stdout, stderr=StringIO(),
255
+ check_status=False, quiet=True)
256
+ if proc.returncode:
257
+ if 'No such file or directory' in proc.stderr.getvalue():
258
+ raise FileNotFoundError(errno.ENOENT,
259
+ f"Cannot find file on the remote '{self.name}'", path)
260
+ else:
261
+ raise RuntimeError("Unexpected error occurred while trying to "
262
+ f"read '{path}' file on the remote '{self.name}'")
263
+
264
+ return proc.stdout.getvalue()
265
+
266
+
267
+ def write_file(self, path, data, sudo=False, mode=None, owner=None,
268
+ mkdir=False, append=False):
269
+ """
270
+ Write data to remote file
271
+
272
+ :param path: file path on remote host
273
+ :param data: str, binary or fileobj to be written
274
+ :param sudo: use sudo to write file, defaults False
275
+ :param mode: set file mode bits if provided
276
+ :param owner: set file owner if provided
277
+ :param mkdir: preliminary create the file directory, defaults False
278
+ :param append: append data to the file, defaults False
279
+ """
280
+ dd = 'sudo dd' if sudo else 'dd'
281
+ args = dd + ' of=' + path
282
+ if append:
283
+ args += ' conv=notrunc oflag=append'
284
+ if mkdir:
285
+ mkdirp = 'sudo mkdir -p' if sudo else 'mkdir -p'
286
+ dirpath = os.path.dirname(path)
287
+ if dirpath:
288
+ args = mkdirp + ' ' + dirpath + '\n' + args
289
+ if mode:
290
+ chmod = 'sudo chmod' if sudo else 'chmod'
291
+ args += '\n' + chmod + ' ' + mode + ' ' + path
292
+ if owner:
293
+ chown = 'sudo chown' if sudo else 'chown'
294
+ args += '\n' + chown + ' ' + owner + ' ' + path
295
+ args = 'set -ex' + '\n' + args
296
+ self.run(args=args, stdin=data, quiet=True)
297
+
298
+ def sudo_write_file(self, path, data, **kwargs):
299
+ """
300
+ Write data to remote file with sudo, for more info see `write_file()`.
301
+ """
302
+ self.write_file(path, data, sudo=True, **kwargs)
303
+
304
+ def is_mounted(self, path):
305
+ """
306
+ Check if the given path is mounted on the remote machine.
307
+
308
+ This method checks the contents of "/proc/self/mounts" instead of
309
+ using "mount" or "findmnt" command since these commands hang when a
310
+ CephFS client is blocked and its mount point on the remote machine
311
+ is left unhandled/unmounted.
312
+
313
+ :param path: path on remote host
314
+ """
315
+ # XXX: matching newline too is crucial so that "/mnt" does not match
316
+ # "/mnt/cephfs" if it's present in the output.
317
+ return f'{path}\n' in self.sh("cat /proc/self/mounts | awk '{print $2}'")
318
+
319
+ @property
320
+ def os(self):
321
+ if not hasattr(self, '_os'):
322
+ try:
323
+ os_release = self.sh('cat /etc/os-release').strip()
324
+ self._os = OS.from_os_release(os_release)
325
+ return self._os
326
+ except CommandFailedError:
327
+ pass
328
+
329
+ lsb_release = self.sh('lsb_release -a').strip()
330
+ self._os = OS.from_lsb_release(lsb_release)
331
+ return self._os
27
332
 
333
+ @property
334
+ def arch(self):
335
+ if not hasattr(self, '_arch'):
336
+ self._arch = self.sh('uname -m').strip()
337
+ return self._arch
338
+
339
+
340
+ class Remote(RemoteShell):
28
341
  """
29
342
  A connection to a remote host.
30
343
 
@@ -33,6 +346,7 @@ class Remote(object):
33
346
 
34
347
  # for unit tests to hook into
35
348
  _runner = staticmethod(run.run)
349
+ _reimage_types = None
36
350
 
37
351
  def __init__(self, name, ssh=None, shortname=None, console=None,
38
352
  host_key=None, keep_alive=True):
@@ -53,6 +367,9 @@ class Remote(object):
53
367
  self._console = console
54
368
  self.ssh = ssh
55
369
 
370
+ if self._reimage_types is None:
371
+ Remote._reimage_types = teuthology.provision.get_reimage_types()
372
+
56
373
  def connect(self, timeout=None, create_key=None, context='connect'):
57
374
  args = dict(user_at_host=self.name, host_key=self._host_key,
58
375
  keep_alive=self.keep_alive, _create_key=create_key)
@@ -70,7 +387,7 @@ class Remote(object):
70
387
  self.ssh = connection.connect(**args)
71
388
  return self.ssh
72
389
 
73
- def reconnect(self, timeout=None, socket_timeout=None, sleep_time=30):
390
+ def reconnect(self, timeout=30, socket_timeout=None):
74
391
  """
75
392
  Attempts to re-establish connection. Returns True for success; False
76
393
  for failure.
@@ -79,21 +396,18 @@ class Remote(object):
79
396
  self.ssh.close()
80
397
  if not timeout:
81
398
  return self._reconnect(timeout=socket_timeout)
82
- start_time = time.time()
83
- elapsed_time = lambda: time.time() - start_time
84
- while elapsed_time() < timeout:
85
- success = self._reconnect(timeout=socket_timeout)
86
- if success:
87
- log.info('Successfully reconnected to host')
88
- break
89
- # Don't let time_remaining be < 0
90
- time_remaining = max(0, timeout - elapsed_time())
91
- sleep_val = min(time_remaining, sleep_time)
92
- time.sleep(sleep_val)
93
- return success
399
+ action = "reconnect to {self.shortname}"
400
+ with safe_while(action=action, timeout=timeout, increment=3, _raise=False) as proceed:
401
+ success = False
402
+ while proceed():
403
+ success = self._reconnect(timeout=socket_timeout)
404
+ if success:
405
+ log.info(f"Successfully reconnected to host '{self.name}'")
406
+ return success
407
+ return success
94
408
 
95
409
  def _reconnect(self, timeout=None):
96
- log.info("Trying to reconnect to host")
410
+ log.info(f"Trying to reconnect to host '{self.name}'")
97
411
  try:
98
412
  self.connect(timeout=timeout, context='reconnect')
99
413
  return self.is_online
@@ -150,6 +464,10 @@ class Remote(object):
150
464
  self._machine_type = remote_info.get("machine_type", None)
151
465
  return self._machine_type
152
466
 
467
+ @property
468
+ def is_reimageable(self):
469
+ return self.machine_type in self._reimage_types
470
+
153
471
  @property
154
472
  def shortname(self):
155
473
  if self._shortname is None:
@@ -173,7 +491,7 @@ class Remote(object):
173
491
  return
174
492
  self.connect()
175
493
  if not self.is_online:
176
- raise Exception('unable to connect')
494
+ raise ConnectionError(f'Failed to connect to {self.shortname}')
177
495
 
178
496
  @property
179
497
  def system_type(self):
@@ -200,114 +518,25 @@ class Remote(object):
200
518
  if not self.ssh or \
201
519
  not self.ssh.get_transport() or \
202
520
  not self.ssh.get_transport().is_active():
203
- self.reconnect()
521
+ if not self.reconnect():
522
+ raise ConnectionError(f'Failed to reconnect to {self.shortname}')
204
523
  r = self._runner(client=self.ssh, name=self.shortname, **kwargs)
205
524
  r.remote = self
206
525
  return r
207
526
 
208
- def mkdtemp(self, suffix=None, parentdir=None):
209
- """
210
- Create a temporary directory on remote machine and return it's path.
211
- """
212
- args = ['mktemp', '-d']
213
-
214
- if suffix:
215
- args.append('--suffix=%s' % suffix)
216
- if parentdir:
217
- args.append('--tmpdir=%s' % parentdir)
218
-
219
- return self.sh(args).strip()
220
-
221
- def mktemp(self, suffix=None, parentdir=None):
222
- """
223
- Make a remote temporary file
224
-
225
- Returns: the path of the temp file created.
226
- """
227
- args = ['mktemp']
228
-
229
- if suffix:
230
- args.append('--suffix=%s' % suffix)
231
- if parentdir:
232
- args.append('--tmpdir=%s' % parentdir)
233
-
234
- return self.sh(args).strip()
235
-
236
- def sh(self, script, **kwargs):
237
- """
238
- Shortcut for run method.
239
-
240
- Usage:
241
- my_name = remote.sh('whoami')
242
- remote_date = remote.sh('date')
243
- """
244
- if 'stdout' not in kwargs:
245
- kwargs['stdout'] = BytesIO()
246
- if 'args' not in kwargs:
247
- kwargs['args'] = script
248
- proc = self.run(**kwargs)
249
- out = proc.stdout.getvalue()
250
- if isinstance(out, bytes):
251
- return out.decode()
252
- else:
253
- return out
254
-
255
- def sh_file(self, script, label="script", sudo=False, **kwargs):
256
- """
257
- Run shell script after copying its contents to a remote file
258
-
259
- :param script: string with script text, or file object
260
- :param sudo: run command with sudo if True,
261
- run as user name if string value (defaults to False)
262
- :param label: string value which will be part of file name
263
- Returns: stdout
264
- """
265
- ftempl = '/tmp/teuthology-remote-$(date +%Y%m%d%H%M%S)-{}-XXXX'\
266
- .format(label)
267
- script_file = self.sh("mktemp %s" % ftempl).strip()
268
- self.sh("cat - | tee {script} ; chmod a+rx {script}"\
269
- .format(script=script_file), stdin=script)
270
- if sudo:
271
- if isinstance(sudo, str):
272
- command="sudo -u %s %s" % (sudo, script_file)
273
- else:
274
- command="sudo %s" % script_file
275
- else:
276
- command="%s" % script_file
277
-
278
- return self.sh(command, **kwargs)
279
-
280
- def chmod(self, file_path, permissions):
281
- """
282
- As super-user, set permissions on the remote file specified.
283
- """
284
- args = [
285
- 'sudo',
286
- 'chmod',
287
- permissions,
288
- file_path,
289
- ]
290
- self.run(
291
- args=args,
292
- )
293
-
294
- def chcon(self, file_path, context):
295
- """
296
- Set the SELinux context of a given file.
297
-
298
- VMs and non-RPM-based hosts will skip this operation because ours
299
- currently have SELinux disabled.
300
-
301
- :param file_path: The path to the file
302
- :param context: The SELinux context to be used
303
- """
304
- if self.os.package_type != 'rpm' or \
305
- self.os.name in ['opensuse', 'sle']:
306
- return
307
- if teuthology.lock.query.is_vm(self.shortname):
308
- return
309
- self.run(args="sudo chcon {con} {path}".format(
310
- con=context, path=file_path))
527
+ def run_unit_test(self, xml_path_regex, output_yaml, **kwargs):
528
+ try:
529
+ r = self.run(**kwargs)
530
+ except CommandFailedError as exc:
531
+ if xml_path_regex:
532
+ error_msg = UnitTestScanner(remote=self).scan_and_write(xml_path_regex, output_yaml)
533
+ if error_msg:
534
+ raise UnitTestError(
535
+ exitstatus=exc.exitstatus, node=exc.node,
536
+ label=exc.label, message=error_msg
537
+ )
538
+ raise exc
539
+ return r
311
540
 
312
541
  def _sftp_put_file(self, local_path, remote_path):
313
542
  """
@@ -329,12 +558,14 @@ class Remote(object):
329
558
  sftp.get(remote_path, local_path)
330
559
  return local_path
331
560
 
332
- def _sftp_open_file(self, remote_path):
561
+ def _sftp_open_file(self, remote_path, mode=None):
333
562
  """
334
563
  Use the paramiko.SFTPClient to open a file. Returns a
335
564
  paramiko.SFTPFile object.
336
565
  """
337
566
  sftp = self.ssh.open_sftp()
567
+ if mode:
568
+ return sftp.open(remote_path, mode)
338
569
  return sftp.open(remote_path)
339
570
 
340
571
  def _sftp_get_size(self, remote_path):
@@ -355,9 +586,6 @@ class Remote(object):
355
586
  file_size = file_size / 1024.0
356
587
  return "{:3.0f}{}".format(file_size, unit)
357
588
 
358
- def remove(self, path):
359
- self.run(args=['rm', '-fr', path])
360
-
361
589
  def put_file(self, path, dest_path, sudo=False):
362
590
  """
363
591
  Copy a local filename to a remote file
@@ -408,7 +636,7 @@ class Remote(object):
408
636
  self.remove(path)
409
637
  return local_path
410
638
 
411
- def get_tar(self, path, to_path, sudo=False):
639
+ def get_tar(self, path, to_path, sudo=False, compress=True):
412
640
  """
413
641
  Tar a remote directory and copy it locally
414
642
  """
@@ -418,7 +646,7 @@ class Remote(object):
418
646
  args.append('sudo')
419
647
  args.extend([
420
648
  'tar',
421
- 'cz',
649
+ 'cz' if compress else 'c',
422
650
  '-f', '-',
423
651
  '-C', path,
424
652
  '--',
@@ -431,7 +659,7 @@ class Remote(object):
431
659
  self._sftp_get_file(remote_temp_path, to_path)
432
660
  self.remove(remote_temp_path)
433
661
 
434
- def get_tar_stream(self, path, sudo=False):
662
+ def get_tar_stream(self, path, sudo=False, compress=True):
435
663
  """
436
664
  Tar-compress a remote directory and return the RemoteProcess
437
665
  for streaming
@@ -441,7 +669,7 @@ class Remote(object):
441
669
  args.append('sudo')
442
670
  args.extend([
443
671
  'tar',
444
- 'cz',
672
+ 'cz' if compress else 'c',
445
673
  '-f', '-',
446
674
  '-C', path,
447
675
  '--',
@@ -449,26 +677,6 @@ class Remote(object):
449
677
  ])
450
678
  return self.run(args=args, wait=False, stdout=run.PIPE)
451
679
 
452
- @property
453
- def os(self):
454
- if not hasattr(self, '_os'):
455
- try:
456
- os_release = self.sh('cat /etc/os-release').strip()
457
- self._os = OS.from_os_release(os_release)
458
- return self._os
459
- except CommandFailedError:
460
- pass
461
-
462
- lsb_release = self.sh('lsb_release -a').strip()
463
- self._os = OS.from_lsb_release(lsb_release)
464
- return self._os
465
-
466
- @property
467
- def arch(self):
468
- if not hasattr(self, '_arch'):
469
- self._arch = self.sh('uname -m').strip()
470
- return self._arch
471
-
472
680
  @property
473
681
  def host_key(self):
474
682
  if not self._host_key:
@@ -501,6 +709,15 @@ class Remote(object):
501
709
  self._is_vm = teuthology.lock.query.is_vm(self.name)
502
710
  return self._is_vm
503
711
 
712
+ @property
713
+ def is_container(self):
714
+ if not hasattr(self, '_is_container'):
715
+ self._is_container = not bool(self.run(
716
+ args="test -f /run/.containerenv -o -f /.dockerenv",
717
+ check_status=False,
718
+ ).returncode)
719
+ return self._is_container
720
+
504
721
  @property
505
722
  def init_system(self):
506
723
  """
@@ -524,7 +741,7 @@ class Remote(object):
524
741
 
525
742
 
526
743
  def getRemoteConsole(name, ipmiuser=None, ipmipass=None, ipmidomain=None,
527
- logfile=None, timeout=20):
744
+ timeout=60):
528
745
  """
529
746
  Return either VirtualConsole or PhysicalConsole depending on name.
530
747
  """
@@ -534,4 +751,4 @@ def getRemoteConsole(name, ipmiuser=None, ipmipass=None, ipmidomain=None,
534
751
  except Exception:
535
752
  return None
536
753
  return console.PhysicalConsole(
537
- name, ipmiuser, ipmipass, ipmidomain, logfile, timeout)
754
+ name, ipmiuser, ipmipass, ipmidomain, timeout)