blob: 7a10ba9763519b55b64ec885f52e285c045f0dc1 [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 = [
37 '-o', 'UserKnownHostsFile=/dev/null',
38 '-o', 'StrictHostKeyChecking=no',
39 '-o', 'LogLevel=ERROR'
40 ]
41 self.ssh = ['ssh', '-l', self.user ] + ssh_options
42 self.scp = ['scp'] + ssh_options
43 if port:
44 self.ssh = self.ssh + [ '-p', port ]
45 self.scp = self.scp + [ '-P', port ]
Andrew Geisslerc926e172021-05-07 16:11:35 -050046 self._monitor_dumper = None
Andrew Geissler5199d832021-09-24 16:47:35 -050047 self.target_dumper = None
Brad Bishop6e60e8b2018-02-01 10:27:11 -050048
49 def start(self, **kwargs):
50 pass
51
52 def stop(self, **kwargs):
53 pass
54
Andrew Geisslerc926e172021-05-07 16:11:35 -050055 @property
56 def monitor_dumper(self):
57 return self._monitor_dumper
58
59 @monitor_dumper.setter
60 def monitor_dumper(self, dumper):
61 self._monitor_dumper = dumper
62 self.monitor_dumper.dump_monitor()
63
Brad Bishop6e60e8b2018-02-01 10:27:11 -050064 def _run(self, command, timeout=None, ignore_status=True):
65 """
66 Runs command in target using SSHProcess.
67 """
68 self.logger.debug("[Running]$ %s" % " ".join(command))
69
70 starttime = time.time()
71 status, output = SSHCall(command, self.logger, timeout)
72 self.logger.debug("[Command returned '%d' after %.2f seconds]"
73 "" % (status, time.time() - starttime))
74
75 if status and not ignore_status:
76 raise AssertionError("Command '%s' returned non-zero exit "
77 "status %d:\n%s" % (command, status, output))
78
79 return (status, output)
80
Patrick Williams2390b1b2022-11-03 13:47:49 -050081 def run(self, command, timeout=None, ignore_status=True):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050082 """
83 Runs command in target.
84
85 command: Command to run on target.
86 timeout: <value>: Kill command after <val> seconds.
87 None: Kill command default value seconds.
88 0: No timeout, runs until return.
89 """
90 targetCmd = 'export PATH=/usr/sbin:/sbin:/usr/bin:/bin; %s' % command
91 sshCmd = self.ssh + [self.ip, targetCmd]
92
93 if timeout:
94 processTimeout = timeout
95 elif timeout==0:
96 processTimeout = None
97 else:
98 processTimeout = self.timeout
99
Patrick Williams2390b1b2022-11-03 13:47:49 -0500100 status, output = self._run(sshCmd, processTimeout, ignore_status)
Andrew Geisslerc926e172021-05-07 16:11:35 -0500101 self.logger.debug('Command: %s\nStatus: %d Output: %s\n' % (command, status, output))
Andrew Geisslerc3d88e42020-10-02 09:45:00 -0500102 if (status == 255) and (('No route to host') in output):
Andrew Geisslerc926e172021-05-07 16:11:35 -0500103 if self.monitor_dumper:
104 self.monitor_dumper.dump_monitor()
105 if status == 255:
Andrew Geissler5199d832021-09-24 16:47:35 -0500106 if self.target_dumper:
107 self.target_dumper.dump_target()
Andrew Geisslerc926e172021-05-07 16:11:35 -0500108 if self.monitor_dumper:
109 self.monitor_dumper.dump_monitor()
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500110 return (status, output)
111
112 def copyTo(self, localSrc, remoteDst):
113 """
114 Copy file to target.
115
116 If local file is symlink, recreate symlink in target.
117 """
118 if os.path.islink(localSrc):
119 link = os.readlink(localSrc)
120 dstDir, dstBase = os.path.split(remoteDst)
121 sshCmd = 'cd %s; ln -s %s %s' % (dstDir, link, dstBase)
122 return self.run(sshCmd)
123
124 else:
125 remotePath = '%s@%s:%s' % (self.user, self.ip, remoteDst)
126 scpCmd = self.scp + [localSrc, remotePath]
127 return self._run(scpCmd, ignore_status=False)
128
Andrew Geissler635e0e42020-08-21 15:58:33 -0500129 def copyFrom(self, remoteSrc, localDst, warn_on_failure=False):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500130 """
131 Copy file from target.
132 """
133 remotePath = '%s@%s:%s' % (self.user, self.ip, remoteSrc)
134 scpCmd = self.scp + [remotePath, localDst]
Andrew Geissler635e0e42020-08-21 15:58:33 -0500135 (status, output) = self._run(scpCmd, ignore_status=warn_on_failure)
136 if warn_on_failure and status:
137 self.logger.warning("Copy returned non-zero exit status %d:\n%s" % (status, output))
138 return (status, output)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500139
140 def copyDirTo(self, localSrc, remoteDst):
141 """
142 Copy recursively localSrc directory to remoteDst in target.
143 """
144
145 for root, dirs, files in os.walk(localSrc):
146 # Create directories in the target as needed
147 for d in dirs:
148 tmpDir = os.path.join(root, d).replace(localSrc, "")
149 newDir = os.path.join(remoteDst, tmpDir.lstrip("/"))
150 cmd = "mkdir -p %s" % newDir
151 self.run(cmd)
152
153 # Copy files into the target
154 for f in files:
155 tmpFile = os.path.join(root, f).replace(localSrc, "")
156 dstFile = os.path.join(remoteDst, tmpFile.lstrip("/"))
157 srcFile = os.path.join(root, f)
158 self.copyTo(srcFile, dstFile)
159
160 def deleteFiles(self, remotePath, files):
161 """
162 Deletes files in target's remotePath.
163 """
164
165 cmd = "rm"
166 if not isinstance(files, list):
167 files = [files]
168
169 for f in files:
170 cmd = "%s %s" % (cmd, os.path.join(remotePath, f))
171
172 self.run(cmd)
173
174
175 def deleteDir(self, remotePath):
176 """
177 Deletes target's remotePath directory.
178 """
179
180 cmd = "rmdir %s" % remotePath
181 self.run(cmd)
182
183
184 def deleteDirStructure(self, localPath, remotePath):
185 """
186 Delete recursively localPath structure directory in target's remotePath.
187
188 This function is very usefult to delete a package that is installed in
189 the DUT and the host running the test has such package extracted in tmp
190 directory.
191
192 Example:
193 pwd: /home/user/tmp
194 tree: .
195 └── work
196 ├── dir1
197 │   └── file1
198 └── dir2
199
200 localpath = "/home/user/tmp" and remotepath = "/home/user"
201
202 With the above variables this function will try to delete the
203 directory in the DUT in this order:
204 /home/user/work/dir1/file1
205 /home/user/work/dir1 (if dir is empty)
206 /home/user/work/dir2 (if dir is empty)
207 /home/user/work (if dir is empty)
208 """
209
210 for root, dirs, files in os.walk(localPath, topdown=False):
211 # Delete files first
212 tmpDir = os.path.join(root).replace(localPath, "")
213 remoteDir = os.path.join(remotePath, tmpDir.lstrip("/"))
214 self.deleteFiles(remoteDir, files)
215
216 # Remove dirs if empty
217 for d in dirs:
218 tmpDir = os.path.join(root, d).replace(localPath, "")
219 remoteDir = os.path.join(remotePath, tmpDir.lstrip("/"))
220 self.deleteDir(remoteDir)
221
222def SSHCall(command, logger, timeout=None, **opts):
223
224 def run():
225 nonlocal output
226 nonlocal process
227 starttime = time.time()
228 process = subprocess.Popen(command, **options)
229 if timeout:
230 endtime = starttime + timeout
231 eof = False
232 while time.time() < endtime and not eof:
233 logger.debug('time: %s, endtime: %s' % (time.time(), endtime))
234 try:
235 if select.select([process.stdout], [], [], 5)[0] != []:
Brad Bishopc342db32019-05-15 21:57:59 -0400236 reader = codecs.getreader('utf-8')(process.stdout, 'ignore')
Brad Bishopd5ae7d92018-06-14 09:52:03 -0700237 data = reader.read(1024, 4096)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500238 if not data:
239 process.stdout.close()
240 eof = True
241 else:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500242 output += data
243 logger.debug('Partial data from SSH call: %s' % data)
244 endtime = time.time() + timeout
245 except InterruptedError:
246 continue
247
248 # process hasn't returned yet
249 if not eof:
250 process.terminate()
251 time.sleep(5)
252 try:
253 process.kill()
254 except OSError:
255 pass
256 endtime = time.time() - starttime
257 lastline = ("\nProcess killed - no output for %d seconds. Total"
258 " running time: %d seconds." % (timeout, endtime))
259 logger.debug('Received data from SSH call %s ' % lastline)
260 output += lastline
261
262 else:
Brad Bishopc342db32019-05-15 21:57:59 -0400263 output = process.communicate()[0].decode('utf-8', errors='ignore')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500264 logger.debug('Data from SSH call: %s' % output.rstrip())
265
266 options = {
267 "stdout": subprocess.PIPE,
268 "stderr": subprocess.STDOUT,
269 "stdin": None,
270 "shell": False,
271 "bufsize": -1,
Andrew Geissler82c905d2020-04-13 13:39:40 -0500272 "start_new_session": True,
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500273 }
274 options.update(opts)
275 output = ''
276 process = None
277
278 # Unset DISPLAY which means we won't trigger SSH_ASKPASS
279 env = os.environ.copy()
280 if "DISPLAY" in env:
281 del env['DISPLAY']
282 options['env'] = env
283
284 try:
285 run()
286 except:
287 # Need to guard against a SystemExit or other exception ocurring
288 # whilst running and ensure we don't leave a process behind.
289 if process.poll() is None:
290 process.kill()
291 logger.debug('Something went wrong, killing SSH process')
292 raise
293 return (process.wait(), output.rstrip())