blob: 51079075b5bd6b468af5d9e10d0180af3859c27d [file] [log] [blame]
Brad Bishopc342db32019-05-15 21:57:59 -04001#
Brad Bishop6e60e8b2018-02-01 10:27:11 -05002# Copyright (C) 2016 Intel Corporation
Brad Bishopc342db32019-05-15 21:57:59 -04003#
4# SPDX-License-Identifier: MIT
5#
Brad Bishop6e60e8b2018-02-01 10:27:11 -05006
7import os
8import time
9import select
10import logging
11import subprocess
Brad Bishopd7bf8c12018-02-25 22:55:05 -050012import codecs
Brad Bishop6e60e8b2018-02-01 10:27:11 -050013
14from . import OETarget
15
16class OESSHTarget(OETarget):
17 def __init__(self, logger, ip, server_ip, timeout=300, user='root',
Andrew Geissler82c905d2020-04-13 13:39:40 -050018 port=None, server_port=0, **kwargs):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050019 if not logger:
20 logger = logging.getLogger('target')
21 logger.setLevel(logging.INFO)
22 filePath = os.path.join(os.getcwd(), 'remoteTarget.log')
23 fileHandler = logging.FileHandler(filePath, 'w', 'utf-8')
24 formatter = logging.Formatter(
25 '%(asctime)s.%(msecs)03d %(levelname)s: %(message)s',
26 '%H:%M:%S')
27 fileHandler.setFormatter(formatter)
28 logger.addHandler(fileHandler)
29
30 super(OESSHTarget, self).__init__(logger)
31 self.ip = ip
32 self.server_ip = server_ip
Andrew Geissler82c905d2020-04-13 13:39:40 -050033 self.server_port = server_port
Brad Bishop6e60e8b2018-02-01 10:27:11 -050034 self.timeout = timeout
35 self.user = user
36 ssh_options = [
Andrew Geissler6aa7eec2023-03-03 12:41:14 -060037 '-o', 'ServerAliveCountMax=2',
38 '-o', 'ServerAliveInterval=30',
Brad Bishop6e60e8b2018-02-01 10:27:11 -050039 '-o', 'UserKnownHostsFile=/dev/null',
40 '-o', 'StrictHostKeyChecking=no',
41 '-o', 'LogLevel=ERROR'
42 ]
43 self.ssh = ['ssh', '-l', self.user ] + ssh_options
44 self.scp = ['scp'] + ssh_options
45 if port:
46 self.ssh = self.ssh + [ '-p', port ]
47 self.scp = self.scp + [ '-P', port ]
Andrew Geisslerc926e172021-05-07 16:11:35 -050048 self._monitor_dumper = None
Andrew Geissler5199d832021-09-24 16:47:35 -050049 self.target_dumper = None
Brad Bishop6e60e8b2018-02-01 10:27:11 -050050
51 def start(self, **kwargs):
52 pass
53
54 def stop(self, **kwargs):
55 pass
56
Andrew Geisslerc926e172021-05-07 16:11:35 -050057 @property
58 def monitor_dumper(self):
59 return self._monitor_dumper
60
61 @monitor_dumper.setter
62 def monitor_dumper(self, dumper):
63 self._monitor_dumper = dumper
64 self.monitor_dumper.dump_monitor()
65
Brad Bishop6e60e8b2018-02-01 10:27:11 -050066 def _run(self, command, timeout=None, ignore_status=True):
67 """
68 Runs command in target using SSHProcess.
69 """
70 self.logger.debug("[Running]$ %s" % " ".join(command))
71
72 starttime = time.time()
73 status, output = SSHCall(command, self.logger, timeout)
74 self.logger.debug("[Command returned '%d' after %.2f seconds]"
75 "" % (status, time.time() - starttime))
76
77 if status and not ignore_status:
78 raise AssertionError("Command '%s' returned non-zero exit "
79 "status %d:\n%s" % (command, status, output))
80
81 return (status, output)
82
Patrick Williams2390b1b2022-11-03 13:47:49 -050083 def run(self, command, timeout=None, ignore_status=True):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050084 """
85 Runs command in target.
86
87 command: Command to run on target.
88 timeout: <value>: Kill command after <val> seconds.
89 None: Kill command default value seconds.
90 0: No timeout, runs until return.
91 """
92 targetCmd = 'export PATH=/usr/sbin:/sbin:/usr/bin:/bin; %s' % command
93 sshCmd = self.ssh + [self.ip, targetCmd]
94
95 if timeout:
96 processTimeout = timeout
97 elif timeout==0:
98 processTimeout = None
99 else:
100 processTimeout = self.timeout
101
Patrick Williams2390b1b2022-11-03 13:47:49 -0500102 status, output = self._run(sshCmd, processTimeout, ignore_status)
Andrew Geisslerc926e172021-05-07 16:11:35 -0500103 self.logger.debug('Command: %s\nStatus: %d Output: %s\n' % (command, status, output))
Andrew Geisslerc3d88e42020-10-02 09:45:00 -0500104 if (status == 255) and (('No route to host') in output):
Andrew Geisslerc926e172021-05-07 16:11:35 -0500105 if self.monitor_dumper:
106 self.monitor_dumper.dump_monitor()
107 if status == 255:
Andrew Geissler5199d832021-09-24 16:47:35 -0500108 if self.target_dumper:
109 self.target_dumper.dump_target()
Andrew Geisslerc926e172021-05-07 16:11:35 -0500110 if self.monitor_dumper:
111 self.monitor_dumper.dump_monitor()
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500112 return (status, output)
113
114 def copyTo(self, localSrc, remoteDst):
115 """
116 Copy file to target.
117
118 If local file is symlink, recreate symlink in target.
119 """
120 if os.path.islink(localSrc):
121 link = os.readlink(localSrc)
122 dstDir, dstBase = os.path.split(remoteDst)
123 sshCmd = 'cd %s; ln -s %s %s' % (dstDir, link, dstBase)
124 return self.run(sshCmd)
125
126 else:
127 remotePath = '%s@%s:%s' % (self.user, self.ip, remoteDst)
128 scpCmd = self.scp + [localSrc, remotePath]
129 return self._run(scpCmd, ignore_status=False)
130
Andrew Geissler635e0e42020-08-21 15:58:33 -0500131 def copyFrom(self, remoteSrc, localDst, warn_on_failure=False):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500132 """
133 Copy file from target.
134 """
135 remotePath = '%s@%s:%s' % (self.user, self.ip, remoteSrc)
136 scpCmd = self.scp + [remotePath, localDst]
Andrew Geissler635e0e42020-08-21 15:58:33 -0500137 (status, output) = self._run(scpCmd, ignore_status=warn_on_failure)
138 if warn_on_failure and status:
139 self.logger.warning("Copy returned non-zero exit status %d:\n%s" % (status, output))
140 return (status, output)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500141
142 def copyDirTo(self, localSrc, remoteDst):
143 """
144 Copy recursively localSrc directory to remoteDst in target.
145 """
146
147 for root, dirs, files in os.walk(localSrc):
148 # Create directories in the target as needed
149 for d in dirs:
150 tmpDir = os.path.join(root, d).replace(localSrc, "")
151 newDir = os.path.join(remoteDst, tmpDir.lstrip("/"))
152 cmd = "mkdir -p %s" % newDir
153 self.run(cmd)
154
155 # Copy files into the target
156 for f in files:
157 tmpFile = os.path.join(root, f).replace(localSrc, "")
158 dstFile = os.path.join(remoteDst, tmpFile.lstrip("/"))
159 srcFile = os.path.join(root, f)
160 self.copyTo(srcFile, dstFile)
161
162 def deleteFiles(self, remotePath, files):
163 """
164 Deletes files in target's remotePath.
165 """
166
167 cmd = "rm"
168 if not isinstance(files, list):
169 files = [files]
170
171 for f in files:
172 cmd = "%s %s" % (cmd, os.path.join(remotePath, f))
173
174 self.run(cmd)
175
176
177 def deleteDir(self, remotePath):
178 """
179 Deletes target's remotePath directory.
180 """
181
182 cmd = "rmdir %s" % remotePath
183 self.run(cmd)
184
185
186 def deleteDirStructure(self, localPath, remotePath):
187 """
188 Delete recursively localPath structure directory in target's remotePath.
189
190 This function is very usefult to delete a package that is installed in
191 the DUT and the host running the test has such package extracted in tmp
192 directory.
193
194 Example:
195 pwd: /home/user/tmp
196 tree: .
197 └── work
198 ├── dir1
199 │   └── file1
200 └── dir2
201
202 localpath = "/home/user/tmp" and remotepath = "/home/user"
203
204 With the above variables this function will try to delete the
205 directory in the DUT in this order:
206 /home/user/work/dir1/file1
207 /home/user/work/dir1 (if dir is empty)
208 /home/user/work/dir2 (if dir is empty)
209 /home/user/work (if dir is empty)
210 """
211
212 for root, dirs, files in os.walk(localPath, topdown=False):
213 # Delete files first
214 tmpDir = os.path.join(root).replace(localPath, "")
215 remoteDir = os.path.join(remotePath, tmpDir.lstrip("/"))
216 self.deleteFiles(remoteDir, files)
217
218 # Remove dirs if empty
219 for d in dirs:
220 tmpDir = os.path.join(root, d).replace(localPath, "")
221 remoteDir = os.path.join(remotePath, tmpDir.lstrip("/"))
222 self.deleteDir(remoteDir)
223
224def SSHCall(command, logger, timeout=None, **opts):
225
226 def run():
227 nonlocal output
228 nonlocal process
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600229 output_raw = b''
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500230 starttime = time.time()
231 process = subprocess.Popen(command, **options)
232 if timeout:
233 endtime = starttime + timeout
234 eof = False
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600235 os.set_blocking(process.stdout.fileno(), False)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500236 while time.time() < endtime and not eof:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500237 try:
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600238 logger.debug('Waiting for process output: time: %s, endtime: %s' % (time.time(), endtime))
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500239 if select.select([process.stdout], [], [], 5)[0] != []:
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600240 # wait a bit for more data, tries to avoid reading single characters
241 time.sleep(0.2)
242 data = process.stdout.read()
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500243 if not data:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500244 eof = True
245 else:
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600246 output_raw += data
247 # ignore errors to capture as much as possible
248 logger.debug('Partial data from SSH call:\n%s' % data.decode('utf-8', errors='ignore'))
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500249 endtime = time.time() + timeout
250 except InterruptedError:
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600251 logger.debug('InterruptedError')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500252 continue
253
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600254 process.stdout.close()
255
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500256 # process hasn't returned yet
257 if not eof:
258 process.terminate()
259 time.sleep(5)
260 try:
261 process.kill()
262 except OSError:
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600263 logger.debug('OSError when killing process')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500264 pass
265 endtime = time.time() - starttime
266 lastline = ("\nProcess killed - no output for %d seconds. Total"
267 " running time: %d seconds." % (timeout, endtime))
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600268 logger.debug('Received data from SSH call:\n%s ' % lastline)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500269 output += lastline
270
271 else:
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600272 output_raw = process.communicate()[0]
273
274 output = output_raw.decode('utf-8', errors='ignore')
275 logger.debug('Data from SSH call:\n%s' % output.rstrip())
276
277 # timout or not, make sure process exits and is not hanging
278 if process.returncode == None:
279 try:
280 process.wait(timeout=5)
281 except TimeoutExpired:
282 try:
283 process.kill()
284 except OSError:
285 logger.debug('OSError')
286 pass
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500287
288 options = {
289 "stdout": subprocess.PIPE,
290 "stderr": subprocess.STDOUT,
291 "stdin": None,
292 "shell": False,
293 "bufsize": -1,
Andrew Geissler82c905d2020-04-13 13:39:40 -0500294 "start_new_session": True,
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500295 }
296 options.update(opts)
297 output = ''
298 process = None
299
300 # Unset DISPLAY which means we won't trigger SSH_ASKPASS
301 env = os.environ.copy()
302 if "DISPLAY" in env:
303 del env['DISPLAY']
304 options['env'] = env
305
306 try:
307 run()
308 except:
309 # Need to guard against a SystemExit or other exception ocurring
310 # whilst running and ensure we don't leave a process behind.
311 if process.poll() is None:
312 process.kill()
313 logger.debug('Something went wrong, killing SSH process')
314 raise
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600315
316 return (process.returncode, output.rstrip())