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.
- scripts/describe.py +1 -0
- scripts/dispatcher.py +55 -26
- scripts/exporter.py +18 -0
- scripts/lock.py +1 -1
- scripts/node_cleanup.py +58 -0
- scripts/openstack.py +9 -9
- scripts/results.py +12 -11
- scripts/schedule.py +4 -0
- scripts/suite.py +57 -16
- scripts/supervisor.py +44 -0
- scripts/update_inventory.py +10 -4
- teuthology/__init__.py +24 -26
- teuthology/beanstalk.py +4 -3
- teuthology/config.py +16 -6
- teuthology/contextutil.py +18 -14
- teuthology/describe_tests.py +25 -18
- teuthology/dispatcher/__init__.py +210 -35
- teuthology/dispatcher/supervisor.py +140 -58
- teuthology/exceptions.py +43 -0
- teuthology/exporter.py +347 -0
- teuthology/kill.py +76 -81
- teuthology/lock/cli.py +3 -3
- teuthology/lock/ops.py +135 -61
- teuthology/lock/query.py +61 -44
- teuthology/ls.py +1 -1
- teuthology/misc.py +61 -75
- teuthology/nuke/__init__.py +12 -353
- teuthology/openstack/__init__.py +4 -3
- teuthology/openstack/openstack-centos-7.0-user-data.txt +1 -1
- teuthology/openstack/openstack-centos-7.1-user-data.txt +1 -1
- teuthology/openstack/openstack-centos-7.2-user-data.txt +1 -1
- teuthology/openstack/openstack-debian-8.0-user-data.txt +1 -1
- teuthology/openstack/openstack-opensuse-42.1-user-data.txt +1 -1
- teuthology/openstack/openstack-teuthology.cron +0 -1
- teuthology/orchestra/cluster.py +49 -7
- teuthology/orchestra/connection.py +16 -5
- teuthology/orchestra/console.py +111 -50
- teuthology/orchestra/daemon/cephadmunit.py +17 -4
- teuthology/orchestra/daemon/state.py +8 -1
- teuthology/orchestra/daemon/systemd.py +4 -4
- teuthology/orchestra/opsys.py +30 -11
- teuthology/orchestra/remote.py +405 -338
- teuthology/orchestra/run.py +3 -3
- teuthology/packaging.py +19 -16
- teuthology/provision/__init__.py +30 -10
- teuthology/provision/cloud/openstack.py +12 -6
- teuthology/provision/cloud/util.py +1 -2
- teuthology/provision/downburst.py +4 -3
- teuthology/provision/fog.py +68 -20
- teuthology/provision/openstack.py +5 -4
- teuthology/provision/pelagos.py +1 -1
- teuthology/repo_utils.py +43 -13
- teuthology/report.py +57 -35
- teuthology/results.py +5 -3
- teuthology/run.py +13 -14
- teuthology/run_tasks.py +27 -43
- teuthology/schedule.py +4 -3
- teuthology/scrape.py +28 -22
- teuthology/suite/__init__.py +74 -45
- teuthology/suite/build_matrix.py +34 -24
- teuthology/suite/fragment-merge.lua +105 -0
- teuthology/suite/matrix.py +31 -2
- teuthology/suite/merge.py +175 -0
- teuthology/suite/placeholder.py +6 -9
- teuthology/suite/run.py +175 -100
- teuthology/suite/util.py +64 -218
- teuthology/task/__init__.py +1 -1
- teuthology/task/ansible.py +101 -32
- teuthology/task/buildpackages.py +2 -2
- teuthology/task/ceph_ansible.py +13 -6
- teuthology/task/cephmetrics.py +2 -1
- teuthology/task/clock.py +33 -14
- teuthology/task/exec.py +18 -0
- teuthology/task/hadoop.py +2 -2
- teuthology/task/install/__init__.py +29 -7
- teuthology/task/install/bin/adjust-ulimits +16 -0
- teuthology/task/install/bin/daemon-helper +114 -0
- teuthology/task/install/bin/stdin-killer +263 -0
- teuthology/task/install/deb.py +1 -1
- teuthology/task/install/rpm.py +17 -5
- teuthology/task/install/util.py +3 -3
- teuthology/task/internal/__init__.py +41 -10
- teuthology/task/internal/edit_sudoers.sh +10 -0
- teuthology/task/internal/lock_machines.py +2 -9
- teuthology/task/internal/redhat.py +31 -1
- teuthology/task/internal/syslog.py +31 -8
- teuthology/task/kernel.py +152 -145
- teuthology/task/lockfile.py +1 -1
- teuthology/task/mpi.py +10 -10
- teuthology/task/pcp.py +1 -1
- teuthology/task/selinux.py +16 -8
- teuthology/task/ssh_keys.py +4 -4
- teuthology/task/tests/__init__.py +137 -77
- teuthology/task/tests/test_fetch_coredumps.py +116 -0
- teuthology/task/tests/test_run.py +4 -4
- teuthology/timer.py +3 -3
- teuthology/util/loggerfile.py +19 -0
- teuthology/util/scanner.py +159 -0
- teuthology/util/sentry.py +52 -0
- teuthology/util/time.py +52 -0
- teuthology-1.2.0.data/scripts/adjust-ulimits +16 -0
- teuthology-1.2.0.data/scripts/daemon-helper +114 -0
- teuthology-1.2.0.data/scripts/stdin-killer +263 -0
- teuthology-1.2.0.dist-info/METADATA +89 -0
- teuthology-1.2.0.dist-info/RECORD +174 -0
- {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/WHEEL +1 -1
- {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/entry_points.txt +3 -2
- scripts/nuke.py +0 -47
- scripts/worker.py +0 -37
- teuthology/nuke/actions.py +0 -456
- teuthology/openstack/test/__init__.py +0 -0
- teuthology/openstack/test/openstack-integration.py +0 -286
- teuthology/openstack/test/test_config.py +0 -35
- teuthology/openstack/test/test_openstack.py +0 -1695
- teuthology/orchestra/test/__init__.py +0 -0
- teuthology/orchestra/test/integration/__init__.py +0 -0
- teuthology/orchestra/test/integration/test_integration.py +0 -94
- teuthology/orchestra/test/test_cluster.py +0 -240
- teuthology/orchestra/test/test_connection.py +0 -106
- teuthology/orchestra/test/test_console.py +0 -217
- teuthology/orchestra/test/test_opsys.py +0 -404
- teuthology/orchestra/test/test_remote.py +0 -185
- teuthology/orchestra/test/test_run.py +0 -286
- teuthology/orchestra/test/test_systemd.py +0 -54
- teuthology/orchestra/test/util.py +0 -12
- teuthology/test/__init__.py +0 -0
- teuthology/test/fake_archive.py +0 -107
- teuthology/test/fake_fs.py +0 -92
- teuthology/test/integration/__init__.py +0 -0
- teuthology/test/integration/test_suite.py +0 -86
- teuthology/test/task/__init__.py +0 -205
- teuthology/test/task/test_ansible.py +0 -624
- teuthology/test/task/test_ceph_ansible.py +0 -176
- teuthology/test/task/test_console_log.py +0 -88
- teuthology/test/task/test_install.py +0 -337
- teuthology/test/task/test_internal.py +0 -57
- teuthology/test/task/test_kernel.py +0 -243
- teuthology/test/task/test_pcp.py +0 -379
- teuthology/test/task/test_selinux.py +0 -35
- teuthology/test/test_config.py +0 -189
- teuthology/test/test_contextutil.py +0 -68
- teuthology/test/test_describe_tests.py +0 -316
- teuthology/test/test_email_sleep_before_teardown.py +0 -81
- teuthology/test/test_exit.py +0 -97
- teuthology/test/test_get_distro.py +0 -47
- teuthology/test/test_get_distro_version.py +0 -47
- teuthology/test/test_get_multi_machine_types.py +0 -27
- teuthology/test/test_job_status.py +0 -60
- teuthology/test/test_ls.py +0 -48
- teuthology/test/test_misc.py +0 -391
- teuthology/test/test_nuke.py +0 -290
- teuthology/test/test_packaging.py +0 -763
- teuthology/test/test_parallel.py +0 -28
- teuthology/test/test_repo_utils.py +0 -225
- teuthology/test/test_report.py +0 -77
- teuthology/test/test_results.py +0 -155
- teuthology/test/test_run.py +0 -239
- teuthology/test/test_safepath.py +0 -55
- teuthology/test/test_schedule.py +0 -45
- teuthology/test/test_scrape.py +0 -167
- teuthology/test/test_timer.py +0 -80
- teuthology/test/test_vps_os_vers_parameter_checking.py +0 -84
- teuthology/test/test_worker.py +0 -303
- teuthology/worker.py +0 -354
- teuthology-1.1.0.dist-info/METADATA +0 -76
- teuthology-1.1.0.dist-info/RECORD +0 -213
- {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/LICENSE +0 -0
- {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/top_level.txt +0 -0
teuthology/orchestra/remote.py
CHANGED
@@ -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
|
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=
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
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
|
-
|
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
|
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
|
-
|
499
|
+
System type decorator
|
294
500
|
"""
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
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
|
512
|
+
def run(self, **kwargs):
|
306
513
|
"""
|
307
|
-
|
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
|
-
|
313
|
-
:param context: The SELinux context to be used
|
516
|
+
TODO refactor to move run.run here?
|
314
517
|
"""
|
315
|
-
if self.
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
self.
|
321
|
-
|
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
|
-
|
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,
|
754
|
+
name, ipmiuser, ipmipass, ipmidomain, timeout)
|