teuthology 1.1.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 (168) hide show
  1. scripts/describe.py +1 -0
  2. scripts/dispatcher.py +55 -26
  3. scripts/exporter.py +18 -0
  4. scripts/lock.py +1 -1
  5. scripts/node_cleanup.py +58 -0
  6. scripts/openstack.py +9 -9
  7. scripts/results.py +12 -11
  8. scripts/schedule.py +4 -0
  9. scripts/suite.py +57 -16
  10. scripts/supervisor.py +44 -0
  11. scripts/update_inventory.py +10 -4
  12. teuthology/__init__.py +24 -26
  13. teuthology/beanstalk.py +4 -3
  14. teuthology/config.py +16 -6
  15. teuthology/contextutil.py +18 -14
  16. teuthology/describe_tests.py +25 -18
  17. teuthology/dispatcher/__init__.py +210 -35
  18. teuthology/dispatcher/supervisor.py +140 -58
  19. teuthology/exceptions.py +43 -0
  20. teuthology/exporter.py +347 -0
  21. teuthology/kill.py +76 -81
  22. teuthology/lock/cli.py +3 -3
  23. teuthology/lock/ops.py +135 -61
  24. teuthology/lock/query.py +61 -44
  25. teuthology/ls.py +1 -1
  26. teuthology/misc.py +61 -75
  27. teuthology/nuke/__init__.py +12 -353
  28. teuthology/openstack/__init__.py +4 -3
  29. teuthology/openstack/openstack-centos-7.0-user-data.txt +1 -1
  30. teuthology/openstack/openstack-centos-7.1-user-data.txt +1 -1
  31. teuthology/openstack/openstack-centos-7.2-user-data.txt +1 -1
  32. teuthology/openstack/openstack-debian-8.0-user-data.txt +1 -1
  33. teuthology/openstack/openstack-opensuse-42.1-user-data.txt +1 -1
  34. teuthology/openstack/openstack-teuthology.cron +0 -1
  35. teuthology/orchestra/cluster.py +49 -7
  36. teuthology/orchestra/connection.py +16 -5
  37. teuthology/orchestra/console.py +111 -50
  38. teuthology/orchestra/daemon/cephadmunit.py +17 -4
  39. teuthology/orchestra/daemon/state.py +8 -1
  40. teuthology/orchestra/daemon/systemd.py +4 -4
  41. teuthology/orchestra/opsys.py +30 -11
  42. teuthology/orchestra/remote.py +405 -338
  43. teuthology/orchestra/run.py +3 -3
  44. teuthology/packaging.py +19 -16
  45. teuthology/provision/__init__.py +30 -10
  46. teuthology/provision/cloud/openstack.py +12 -6
  47. teuthology/provision/cloud/util.py +1 -2
  48. teuthology/provision/downburst.py +4 -3
  49. teuthology/provision/fog.py +68 -20
  50. teuthology/provision/openstack.py +5 -4
  51. teuthology/provision/pelagos.py +1 -1
  52. teuthology/repo_utils.py +43 -13
  53. teuthology/report.py +57 -35
  54. teuthology/results.py +5 -3
  55. teuthology/run.py +13 -14
  56. teuthology/run_tasks.py +27 -43
  57. teuthology/schedule.py +4 -3
  58. teuthology/scrape.py +28 -22
  59. teuthology/suite/__init__.py +74 -45
  60. teuthology/suite/build_matrix.py +34 -24
  61. teuthology/suite/fragment-merge.lua +105 -0
  62. teuthology/suite/matrix.py +31 -2
  63. teuthology/suite/merge.py +175 -0
  64. teuthology/suite/placeholder.py +6 -9
  65. teuthology/suite/run.py +175 -100
  66. teuthology/suite/util.py +64 -218
  67. teuthology/task/__init__.py +1 -1
  68. teuthology/task/ansible.py +101 -32
  69. teuthology/task/buildpackages.py +2 -2
  70. teuthology/task/ceph_ansible.py +13 -6
  71. teuthology/task/cephmetrics.py +2 -1
  72. teuthology/task/clock.py +33 -14
  73. teuthology/task/exec.py +18 -0
  74. teuthology/task/hadoop.py +2 -2
  75. teuthology/task/install/__init__.py +29 -7
  76. teuthology/task/install/bin/adjust-ulimits +16 -0
  77. teuthology/task/install/bin/daemon-helper +114 -0
  78. teuthology/task/install/bin/stdin-killer +263 -0
  79. teuthology/task/install/deb.py +1 -1
  80. teuthology/task/install/rpm.py +17 -5
  81. teuthology/task/install/util.py +3 -3
  82. teuthology/task/internal/__init__.py +41 -10
  83. teuthology/task/internal/edit_sudoers.sh +10 -0
  84. teuthology/task/internal/lock_machines.py +2 -9
  85. teuthology/task/internal/redhat.py +31 -1
  86. teuthology/task/internal/syslog.py +31 -8
  87. teuthology/task/kernel.py +152 -145
  88. teuthology/task/lockfile.py +1 -1
  89. teuthology/task/mpi.py +10 -10
  90. teuthology/task/pcp.py +1 -1
  91. teuthology/task/selinux.py +16 -8
  92. teuthology/task/ssh_keys.py +4 -4
  93. teuthology/task/tests/__init__.py +137 -77
  94. teuthology/task/tests/test_fetch_coredumps.py +116 -0
  95. teuthology/task/tests/test_run.py +4 -4
  96. teuthology/timer.py +3 -3
  97. teuthology/util/loggerfile.py +19 -0
  98. teuthology/util/scanner.py +159 -0
  99. teuthology/util/sentry.py +52 -0
  100. teuthology/util/time.py +52 -0
  101. teuthology-1.2.0.data/scripts/adjust-ulimits +16 -0
  102. teuthology-1.2.0.data/scripts/daemon-helper +114 -0
  103. teuthology-1.2.0.data/scripts/stdin-killer +263 -0
  104. teuthology-1.2.0.dist-info/METADATA +89 -0
  105. teuthology-1.2.0.dist-info/RECORD +174 -0
  106. {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/WHEEL +1 -1
  107. {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/entry_points.txt +3 -2
  108. scripts/nuke.py +0 -47
  109. scripts/worker.py +0 -37
  110. teuthology/nuke/actions.py +0 -456
  111. teuthology/openstack/test/__init__.py +0 -0
  112. teuthology/openstack/test/openstack-integration.py +0 -286
  113. teuthology/openstack/test/test_config.py +0 -35
  114. teuthology/openstack/test/test_openstack.py +0 -1695
  115. teuthology/orchestra/test/__init__.py +0 -0
  116. teuthology/orchestra/test/integration/__init__.py +0 -0
  117. teuthology/orchestra/test/integration/test_integration.py +0 -94
  118. teuthology/orchestra/test/test_cluster.py +0 -240
  119. teuthology/orchestra/test/test_connection.py +0 -106
  120. teuthology/orchestra/test/test_console.py +0 -217
  121. teuthology/orchestra/test/test_opsys.py +0 -404
  122. teuthology/orchestra/test/test_remote.py +0 -185
  123. teuthology/orchestra/test/test_run.py +0 -286
  124. teuthology/orchestra/test/test_systemd.py +0 -54
  125. teuthology/orchestra/test/util.py +0 -12
  126. teuthology/test/__init__.py +0 -0
  127. teuthology/test/fake_archive.py +0 -107
  128. teuthology/test/fake_fs.py +0 -92
  129. teuthology/test/integration/__init__.py +0 -0
  130. teuthology/test/integration/test_suite.py +0 -86
  131. teuthology/test/task/__init__.py +0 -205
  132. teuthology/test/task/test_ansible.py +0 -624
  133. teuthology/test/task/test_ceph_ansible.py +0 -176
  134. teuthology/test/task/test_console_log.py +0 -88
  135. teuthology/test/task/test_install.py +0 -337
  136. teuthology/test/task/test_internal.py +0 -57
  137. teuthology/test/task/test_kernel.py +0 -243
  138. teuthology/test/task/test_pcp.py +0 -379
  139. teuthology/test/task/test_selinux.py +0 -35
  140. teuthology/test/test_config.py +0 -189
  141. teuthology/test/test_contextutil.py +0 -68
  142. teuthology/test/test_describe_tests.py +0 -316
  143. teuthology/test/test_email_sleep_before_teardown.py +0 -81
  144. teuthology/test/test_exit.py +0 -97
  145. teuthology/test/test_get_distro.py +0 -47
  146. teuthology/test/test_get_distro_version.py +0 -47
  147. teuthology/test/test_get_multi_machine_types.py +0 -27
  148. teuthology/test/test_job_status.py +0 -60
  149. teuthology/test/test_ls.py +0 -48
  150. teuthology/test/test_misc.py +0 -391
  151. teuthology/test/test_nuke.py +0 -290
  152. teuthology/test/test_packaging.py +0 -763
  153. teuthology/test/test_parallel.py +0 -28
  154. teuthology/test/test_repo_utils.py +0 -225
  155. teuthology/test/test_report.py +0 -77
  156. teuthology/test/test_results.py +0 -155
  157. teuthology/test/test_run.py +0 -239
  158. teuthology/test/test_safepath.py +0 -55
  159. teuthology/test/test_schedule.py +0 -45
  160. teuthology/test/test_scrape.py +0 -167
  161. teuthology/test/test_timer.py +0 -80
  162. teuthology/test/test_vps_os_vers_parameter_checking.py +0 -84
  163. teuthology/test/test_worker.py +0 -303
  164. teuthology/worker.py +0 -354
  165. teuthology-1.1.0.dist-info/METADATA +0 -76
  166. teuthology-1.1.0.dist-info/RECORD +0 -213
  167. {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/LICENSE +0 -0
  168. {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/top_level.txt +0 -0
@@ -4,16 +4,17 @@ 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
11
12
  import teuthology.provision
12
13
  from teuthology import misc
13
- from teuthology.exceptions import CommandFailedError
14
+ from teuthology.exceptions import CommandFailedError, UnitTestError
15
+ from teuthology.util.scanner import UnitTestScanner
14
16
  from teuthology.misc import host_shortname
15
17
  import errno
16
- import time
17
18
  import re
18
19
  import logging
19
20
  from io import BytesIO
@@ -26,8 +27,317 @@ import netaddr
26
27
  log = logging.getLogger(__name__)
27
28
 
28
29
 
29
- 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
30
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
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):
31
341
  """
32
342
  A connection to a remote host.
33
343
 
@@ -77,7 +387,7 @@ class Remote(object):
77
387
  self.ssh = connection.connect(**args)
78
388
  return self.ssh
79
389
 
80
- def reconnect(self, timeout=None, socket_timeout=None, sleep_time=30):
390
+ def reconnect(self, timeout=30, socket_timeout=None):
81
391
  """
82
392
  Attempts to re-establish connection. Returns True for success; False
83
393
  for failure.
@@ -86,21 +396,18 @@ class Remote(object):
86
396
  self.ssh.close()
87
397
  if not timeout:
88
398
  return self._reconnect(timeout=socket_timeout)
89
- start_time = time.time()
90
- elapsed_time = lambda: time.time() - start_time
91
- while elapsed_time() < timeout:
92
- success = self._reconnect(timeout=socket_timeout)
93
- if success:
94
- log.info('Successfully reconnected to host')
95
- break
96
- # Don't let time_remaining be < 0
97
- time_remaining = max(0, timeout - elapsed_time())
98
- sleep_val = min(time_remaining, sleep_time)
99
- time.sleep(sleep_val)
100
- 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
101
408
 
102
409
  def _reconnect(self, timeout=None):
103
- log.info("Trying to reconnect to host")
410
+ log.info(f"Trying to reconnect to host '{self.name}'")
104
411
  try:
105
412
  self.connect(timeout=timeout, context='reconnect')
106
413
  return self.is_online
@@ -157,168 +464,79 @@ class Remote(object):
157
464
  self._machine_type = remote_info.get("machine_type", None)
158
465
  return self._machine_type
159
466
 
160
- @property
161
- def is_reimageable(self):
162
- return self.machine_type in self._reimage_types
163
-
164
- @property
165
- def shortname(self):
166
- if self._shortname is None:
167
- self._shortname = host_shortname(self.hostname)
168
- return self._shortname
169
-
170
- @property
171
- def is_online(self):
172
- if self.ssh is None:
173
- return False
174
- if self.ssh.get_transport() is None:
175
- return False
176
- try:
177
- self.run(args="true")
178
- except Exception:
179
- return False
180
- return self.ssh.get_transport().is_active()
181
-
182
- def ensure_online(self):
183
- if self.is_online:
184
- return
185
- self.connect()
186
- if not self.is_online:
187
- raise Exception('unable to connect')
188
-
189
- @property
190
- def system_type(self):
191
- """
192
- System type decorator
193
- """
194
- return misc.get_system_type(self)
195
-
196
- def __str__(self):
197
- return self.name
198
-
199
- def __repr__(self):
200
- return '{classname}(name={name!r})'.format(
201
- classname=self.__class__.__name__,
202
- name=self.name,
203
- )
204
-
205
- def run(self, **kwargs):
206
- """
207
- This calls `orchestra.run.run` with our SSH client.
208
-
209
- TODO refactor to move run.run here?
210
- """
211
- if not self.ssh or \
212
- not self.ssh.get_transport() or \
213
- not self.ssh.get_transport().is_active():
214
- self.reconnect()
215
- r = self._runner(client=self.ssh, name=self.shortname, **kwargs)
216
- r.remote = self
217
- return r
218
-
219
- def mkdtemp(self, suffix=None, parentdir=None):
220
- """
221
- Create a temporary directory on remote machine and return it's path.
222
- """
223
- args = ['mktemp', '-d']
224
-
225
- if suffix:
226
- args.append('--suffix=%s' % suffix)
227
- if parentdir:
228
- args.append('--tmpdir=%s' % parentdir)
229
-
230
- return self.sh(args).strip()
231
-
232
- def mktemp(self, suffix=None, parentdir=None):
233
- """
234
- Make a remote temporary file
235
-
236
- Returns: the path of the temp file created.
237
- """
238
- args = ['mktemp']
239
-
240
- if suffix:
241
- args.append('--suffix=%s' % suffix)
242
- if parentdir:
243
- args.append('--tmpdir=%s' % parentdir)
244
-
245
- return self.sh(args).strip()
246
-
247
- def sh(self, script, **kwargs):
248
- """
249
- Shortcut for run method.
250
-
251
- Usage:
252
- my_name = remote.sh('whoami')
253
- remote_date = remote.sh('date')
254
- """
255
- if 'stdout' not in kwargs:
256
- kwargs['stdout'] = BytesIO()
257
- if 'args' not in kwargs:
258
- kwargs['args'] = script
259
- proc = self.run(**kwargs)
260
- out = proc.stdout.getvalue()
261
- if isinstance(out, bytes):
262
- return out.decode()
263
- else:
264
- return out
265
-
266
- def sh_file(self, script, label="script", sudo=False, **kwargs):
267
- """
268
- Run shell script after copying its contents to a remote file
467
+ @property
468
+ def is_reimageable(self):
469
+ return self.machine_type in self._reimage_types
269
470
 
270
- :param script: string with script text, or file object
271
- :param sudo: run command with sudo if True,
272
- run as user name if string value (defaults to False)
273
- :param label: string value which will be part of file name
274
- Returns: stdout
275
- """
276
- ftempl = '/tmp/teuthology-remote-$(date +%Y%m%d%H%M%S)-{}-XXXX'\
277
- .format(label)
278
- script_file = self.sh("mktemp %s" % ftempl).strip()
279
- self.sh("cat - | tee {script} ; chmod a+rx {script}"\
280
- .format(script=script_file), stdin=script)
281
- if sudo:
282
- if isinstance(sudo, str):
283
- command="sudo -u %s %s" % (sudo, script_file)
284
- else:
285
- command="sudo %s" % script_file
286
- else:
287
- command="%s" % script_file
471
+ @property
472
+ def shortname(self):
473
+ if self._shortname is None:
474
+ self._shortname = host_shortname(self.hostname)
475
+ return self._shortname
288
476
 
289
- return self.sh(command, **kwargs)
477
+ @property
478
+ def is_online(self):
479
+ if self.ssh is None:
480
+ return False
481
+ if self.ssh.get_transport() is None:
482
+ return False
483
+ try:
484
+ self.run(args="true")
485
+ except Exception:
486
+ return False
487
+ return self.ssh.get_transport().is_active()
290
488
 
291
- def chmod(self, file_path, permissions):
489
+ def ensure_online(self):
490
+ if self.is_online:
491
+ return
492
+ self.connect()
493
+ if not self.is_online:
494
+ raise ConnectionError(f'Failed to connect to {self.shortname}')
495
+
496
+ @property
497
+ def system_type(self):
292
498
  """
293
- As super-user, set permissions on the remote file specified.
499
+ System type decorator
294
500
  """
295
- args = [
296
- 'sudo',
297
- 'chmod',
298
- permissions,
299
- file_path,
300
- ]
301
- self.run(
302
- args=args,
501
+ return misc.get_system_type(self)
502
+
503
+ def __str__(self):
504
+ return self.name
505
+
506
+ def __repr__(self):
507
+ return '{classname}(name={name!r})'.format(
508
+ classname=self.__class__.__name__,
509
+ name=self.name,
303
510
  )
304
511
 
305
- def chcon(self, file_path, context):
512
+ def run(self, **kwargs):
306
513
  """
307
- Set the SELinux context of a given file.
308
-
309
- VMs and non-RPM-based hosts will skip this operation because ours
310
- currently have SELinux disabled.
514
+ This calls `orchestra.run.run` with our SSH client.
311
515
 
312
- :param file_path: The path to the file
313
- :param context: The SELinux context to be used
516
+ TODO refactor to move run.run here?
314
517
  """
315
- if self.os.package_type != 'rpm' or \
316
- self.os.name in ['opensuse', 'sle']:
317
- return
318
- if teuthology.lock.query.is_vm(self.shortname):
319
- return
320
- self.run(args="sudo chcon {con} {path}".format(
321
- con=context, path=file_path))
518
+ if not self.ssh or \
519
+ not self.ssh.get_transport() or \
520
+ not self.ssh.get_transport().is_active():
521
+ if not self.reconnect():
522
+ raise ConnectionError(f'Failed to reconnect to {self.shortname}')
523
+ r = self._runner(client=self.ssh, name=self.shortname, **kwargs)
524
+ r.remote = self
525
+ return r
526
+
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
322
540
 
323
541
  def _sftp_put_file(self, local_path, remote_path):
324
542
  """
@@ -340,12 +558,14 @@ class Remote(object):
340
558
  sftp.get(remote_path, local_path)
341
559
  return local_path
342
560
 
343
- def _sftp_open_file(self, remote_path):
561
+ def _sftp_open_file(self, remote_path, mode=None):
344
562
  """
345
563
  Use the paramiko.SFTPClient to open a file. Returns a
346
564
  paramiko.SFTPFile object.
347
565
  """
348
566
  sftp = self.ssh.open_sftp()
567
+ if mode:
568
+ return sftp.open(remote_path, mode)
349
569
  return sftp.open(remote_path)
350
570
 
351
571
  def _sftp_get_size(self, remote_path):
@@ -366,9 +586,6 @@ class Remote(object):
366
586
  file_size = file_size / 1024.0
367
587
  return "{:3.0f}{}".format(file_size, unit)
368
588
 
369
- def remove(self, path):
370
- self.run(args=['rm', '-fr', path])
371
-
372
589
  def put_file(self, path, dest_path, sudo=False):
373
590
  """
374
591
  Copy a local filename to a remote file
@@ -419,7 +636,7 @@ class Remote(object):
419
636
  self.remove(path)
420
637
  return local_path
421
638
 
422
- def get_tar(self, path, to_path, sudo=False):
639
+ def get_tar(self, path, to_path, sudo=False, compress=True):
423
640
  """
424
641
  Tar a remote directory and copy it locally
425
642
  """
@@ -429,7 +646,7 @@ class Remote(object):
429
646
  args.append('sudo')
430
647
  args.extend([
431
648
  'tar',
432
- 'cz',
649
+ 'cz' if compress else 'c',
433
650
  '-f', '-',
434
651
  '-C', path,
435
652
  '--',
@@ -442,7 +659,7 @@ class Remote(object):
442
659
  self._sftp_get_file(remote_temp_path, to_path)
443
660
  self.remove(remote_temp_path)
444
661
 
445
- def get_tar_stream(self, path, sudo=False):
662
+ def get_tar_stream(self, path, sudo=False, compress=True):
446
663
  """
447
664
  Tar-compress a remote directory and return the RemoteProcess
448
665
  for streaming
@@ -452,7 +669,7 @@ class Remote(object):
452
669
  args.append('sudo')
453
670
  args.extend([
454
671
  'tar',
455
- 'cz',
672
+ 'cz' if compress else 'c',
456
673
  '-f', '-',
457
674
  '-C', path,
458
675
  '--',
@@ -460,165 +677,6 @@ class Remote(object):
460
677
  ])
461
678
  return self.run(args=args, wait=False, stdout=run.PIPE)
462
679
 
463
- def copy_file(self, src, dst, sudo=False, mode=None, owner=None,
464
- mkdir=False, append=False):
465
- """
466
- Copy data to remote file
467
-
468
- :param src: source file path on remote host
469
- :param dst: destination file path on remote host
470
- :param sudo: use sudo to write file, defaults False
471
- :param mode: set file mode bits if provided
472
- :param owner: set file owner if provided
473
- :param mkdir: ensure the destination directory exists, defaults False
474
- :param append: append data to the file, defaults False
475
- """
476
- dd = 'sudo dd' if sudo else 'dd'
477
- args = dd + ' if=' + src + ' of=' + dst
478
- if append:
479
- args += ' conv=notrunc oflag=append'
480
- if mkdir:
481
- mkdirp = 'sudo mkdir -p' if sudo else 'mkdir -p'
482
- dirpath = os.path.dirname(dst)
483
- if dirpath:
484
- args = mkdirp + ' ' + dirpath + '\n' + args
485
- if mode:
486
- chmod = 'sudo chmod' if sudo else 'chmod'
487
- args += '\n' + chmod + ' ' + mode + ' ' + dst
488
- if owner:
489
- chown = 'sudo chown' if sudo else 'chown'
490
- args += '\n' + chown + ' ' + owner + ' ' + dst
491
- args = 'set -ex' + '\n' + args
492
- self.run(args=args)
493
-
494
- def move_file(self, src, dst, sudo=False, mode=None, owner=None,
495
- mkdir=False):
496
- """
497
- Move data to remote file
498
-
499
- :param src: source file path on remote host
500
- :param dst: destination file path on remote host
501
- :param sudo: use sudo to write file, defaults False
502
- :param mode: set file mode bits if provided
503
- :param owner: set file owner if provided
504
- :param mkdir: ensure the destination directory exists, defaults False
505
- """
506
- mv = 'sudo mv' if sudo else 'mv'
507
- args = mv + ' ' + src + ' ' + dst
508
- if mkdir:
509
- mkdirp = 'sudo mkdir -p' if sudo else 'mkdir -p'
510
- dirpath = os.path.dirname(dst)
511
- if dirpath:
512
- args = mkdirp + ' ' + dirpath + '\n' + args
513
- if mode:
514
- chmod = 'sudo chmod' if sudo else 'chmod'
515
- args += ' && ' + chmod + ' ' + mode + ' ' + dst
516
- if owner:
517
- chown = 'sudo chown' if sudo else 'chown'
518
- args += ' && ' + chown + ' ' + owner + ' ' + dst
519
- self.run(args=args)
520
-
521
- def read_file(self, path, sudo=False, stdout=None,
522
- offset=0, length=0):
523
- """
524
- Read data from remote file
525
-
526
- :param path: file path on remote host
527
- :param sudo: use sudo to read the file, defaults False
528
- :param stdout: output object, defaults to io.BytesIO()
529
- :param offset: number of bytes to skip from the file
530
- :param length: number of bytes to read from the file
531
-
532
- :raises: :class:`FileNotFoundError`: there is no such file by the path
533
- :raises: :class:`RuntimeError`: unexpected error occurred
534
-
535
- :returns: the file contents in bytes, if stdout is `io.BytesIO`, by default
536
- :returns: the file contents in str, if stdout is `io.StringIO`
537
- """
538
- dd = 'sudo dd' if sudo else 'dd'
539
- args = dd + ' if=' + path + ' of=/dev/stdout'
540
- iflags=[]
541
- # we have to set defaults here instead of the method's signature,
542
- # because python is reusing the object from call to call
543
- stdout = stdout or BytesIO()
544
- if offset:
545
- args += ' skip=' + str(offset)
546
- iflags += 'skip_bytes'
547
- if length:
548
- args += ' count=' + str(length)
549
- iflags += 'count_bytes'
550
- if iflags:
551
- args += ' iflag=' + ','.join(iflags)
552
- args = 'set -ex' + '\n' + args
553
- proc = self.run(args=args, stdout=stdout, stderr=StringIO(), check_status=False, quiet=True)
554
- if proc.returncode:
555
- if 'No such file or directory' in proc.stderr.getvalue():
556
- raise FileNotFoundError(errno.ENOENT,
557
- f"Cannot find file on the remote '{self.name}'", path)
558
- else:
559
- raise RuntimeError("Unexpected error occurred while trying to "
560
- f"read '{path}' file on the remote '{self.name}'")
561
-
562
- return proc.stdout.getvalue()
563
-
564
-
565
- def write_file(self, path, data, sudo=False, mode=None, owner=None,
566
- mkdir=False, append=False):
567
- """
568
- Write data to remote file
569
-
570
- :param path: file path on remote host
571
- :param data: str, binary or fileobj to be written
572
- :param sudo: use sudo to write file, defaults False
573
- :param mode: set file mode bits if provided
574
- :param owner: set file owner if provided
575
- :param mkdir: preliminary create the file directory, defaults False
576
- :param append: append data to the file, defaults False
577
- """
578
- dd = 'sudo dd' if sudo else 'dd'
579
- args = dd + ' of=' + path
580
- if append:
581
- args += ' conv=notrunc oflag=append'
582
- if mkdir:
583
- mkdirp = 'sudo mkdir -p' if sudo else 'mkdir -p'
584
- dirpath = os.path.dirname(path)
585
- if dirpath:
586
- args = mkdirp + ' ' + dirpath + '\n' + args
587
- if mode:
588
- chmod = 'sudo chmod' if sudo else 'chmod'
589
- args += '\n' + chmod + ' ' + mode + ' ' + path
590
- if owner:
591
- chown = 'sudo chown' if sudo else 'chown'
592
- args += '\n' + chown + ' ' + owner + ' ' + path
593
- args = 'set -ex' + '\n' + args
594
- self.run(args=args, stdin=data, quiet=True)
595
-
596
- def sudo_write_file(self, path, data, **kwargs):
597
- """
598
- Write data to remote file with sudo, for more info see `write_file()`.
599
- """
600
- self.write_file(path, data, sudo=True, **kwargs)
601
-
602
- @property
603
- def os(self):
604
- if not hasattr(self, '_os'):
605
- try:
606
- os_release = self.sh('cat /etc/os-release').strip()
607
- self._os = OS.from_os_release(os_release)
608
- return self._os
609
- except CommandFailedError:
610
- pass
611
-
612
- lsb_release = self.sh('lsb_release -a').strip()
613
- self._os = OS.from_lsb_release(lsb_release)
614
- return self._os
615
-
616
- @property
617
- def arch(self):
618
- if not hasattr(self, '_arch'):
619
- self._arch = self.sh('uname -m').strip()
620
- return self._arch
621
-
622
680
  @property
623
681
  def host_key(self):
624
682
  if not self._host_key:
@@ -651,6 +709,15 @@ class Remote(object):
651
709
  self._is_vm = teuthology.lock.query.is_vm(self.name)
652
710
  return self._is_vm
653
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
+
654
721
  @property
655
722
  def init_system(self):
656
723
  """
@@ -674,7 +741,7 @@ class Remote(object):
674
741
 
675
742
 
676
743
  def getRemoteConsole(name, ipmiuser=None, ipmipass=None, ipmidomain=None,
677
- logfile=None, timeout=60):
744
+ timeout=60):
678
745
  """
679
746
  Return either VirtualConsole or PhysicalConsole depending on name.
680
747
  """
@@ -684,4 +751,4 @@ def getRemoteConsole(name, ipmiuser=None, ipmipass=None, ipmidomain=None,
684
751
  except Exception:
685
752
  return None
686
753
  return console.PhysicalConsole(
687
- name, ipmiuser, ipmipass, ipmidomain, logfile, timeout)
754
+ name, ipmiuser, ipmipass, ipmidomain, timeout)