blob: 090b40a8143503f321e0017245e7e1b518648ef2 [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 ]
46
47 def start(self, **kwargs):
48 pass
49
50 def stop(self, **kwargs):
51 pass
52
53 def _run(self, command, timeout=None, ignore_status=True):
54 """
55 Runs command in target using SSHProcess.
56 """
57 self.logger.debug("[Running]$ %s" % " ".join(command))
58
59 starttime = time.time()
60 status, output = SSHCall(command, self.logger, timeout)
61 self.logger.debug("[Command returned '%d' after %.2f seconds]"
62 "" % (status, time.time() - starttime))
63
64 if status and not ignore_status:
65 raise AssertionError("Command '%s' returned non-zero exit "
66 "status %d:\n%s" % (command, status, output))
67
68 return (status, output)
69
70 def run(self, command, timeout=None):
71 """
72 Runs command in target.
73
74 command: Command to run on target.
75 timeout: <value>: Kill command after <val> seconds.
76 None: Kill command default value seconds.
77 0: No timeout, runs until return.
78 """
79 targetCmd = 'export PATH=/usr/sbin:/sbin:/usr/bin:/bin; %s' % command
80 sshCmd = self.ssh + [self.ip, targetCmd]
81
82 if timeout:
83 processTimeout = timeout
84 elif timeout==0:
85 processTimeout = None
86 else:
87 processTimeout = self.timeout
88
89 status, output = self._run(sshCmd, processTimeout, True)
Brad Bishopd7bf8c12018-02-25 22:55:05 -050090 self.logger.debug('Command: %s\nOutput: %s\n' % (command, output))
Brad Bishop6e60e8b2018-02-01 10:27:11 -050091 return (status, output)
92
93 def copyTo(self, localSrc, remoteDst):
94 """
95 Copy file to target.
96
97 If local file is symlink, recreate symlink in target.
98 """
99 if os.path.islink(localSrc):
100 link = os.readlink(localSrc)
101 dstDir, dstBase = os.path.split(remoteDst)
102 sshCmd = 'cd %s; ln -s %s %s' % (dstDir, link, dstBase)
103 return self.run(sshCmd)
104
105 else:
106 remotePath = '%s@%s:%s' % (self.user, self.ip, remoteDst)
107 scpCmd = self.scp + [localSrc, remotePath]
108 return self._run(scpCmd, ignore_status=False)
109
110 def copyFrom(self, remoteSrc, localDst):
111 """
112 Copy file from target.
113 """
114 remotePath = '%s@%s:%s' % (self.user, self.ip, remoteSrc)
115 scpCmd = self.scp + [remotePath, localDst]
116 return self._run(scpCmd, ignore_status=False)
117
118 def copyDirTo(self, localSrc, remoteDst):
119 """
120 Copy recursively localSrc directory to remoteDst in target.
121 """
122
123 for root, dirs, files in os.walk(localSrc):
124 # Create directories in the target as needed
125 for d in dirs:
126 tmpDir = os.path.join(root, d).replace(localSrc, "")
127 newDir = os.path.join(remoteDst, tmpDir.lstrip("/"))
128 cmd = "mkdir -p %s" % newDir
129 self.run(cmd)
130
131 # Copy files into the target
132 for f in files:
133 tmpFile = os.path.join(root, f).replace(localSrc, "")
134 dstFile = os.path.join(remoteDst, tmpFile.lstrip("/"))
135 srcFile = os.path.join(root, f)
136 self.copyTo(srcFile, dstFile)
137
138 def deleteFiles(self, remotePath, files):
139 """
140 Deletes files in target's remotePath.
141 """
142
143 cmd = "rm"
144 if not isinstance(files, list):
145 files = [files]
146
147 for f in files:
148 cmd = "%s %s" % (cmd, os.path.join(remotePath, f))
149
150 self.run(cmd)
151
152
153 def deleteDir(self, remotePath):
154 """
155 Deletes target's remotePath directory.
156 """
157
158 cmd = "rmdir %s" % remotePath
159 self.run(cmd)
160
161
162 def deleteDirStructure(self, localPath, remotePath):
163 """
164 Delete recursively localPath structure directory in target's remotePath.
165
166 This function is very usefult to delete a package that is installed in
167 the DUT and the host running the test has such package extracted in tmp
168 directory.
169
170 Example:
171 pwd: /home/user/tmp
172 tree: .
173 └── work
174 ├── dir1
175 │   └── file1
176 └── dir2
177
178 localpath = "/home/user/tmp" and remotepath = "/home/user"
179
180 With the above variables this function will try to delete the
181 directory in the DUT in this order:
182 /home/user/work/dir1/file1
183 /home/user/work/dir1 (if dir is empty)
184 /home/user/work/dir2 (if dir is empty)
185 /home/user/work (if dir is empty)
186 """
187
188 for root, dirs, files in os.walk(localPath, topdown=False):
189 # Delete files first
190 tmpDir = os.path.join(root).replace(localPath, "")
191 remoteDir = os.path.join(remotePath, tmpDir.lstrip("/"))
192 self.deleteFiles(remoteDir, files)
193
194 # Remove dirs if empty
195 for d in dirs:
196 tmpDir = os.path.join(root, d).replace(localPath, "")
197 remoteDir = os.path.join(remotePath, tmpDir.lstrip("/"))
198 self.deleteDir(remoteDir)
199
200def SSHCall(command, logger, timeout=None, **opts):
201
202 def run():
203 nonlocal output
204 nonlocal process
205 starttime = time.time()
206 process = subprocess.Popen(command, **options)
207 if timeout:
208 endtime = starttime + timeout
209 eof = False
210 while time.time() < endtime and not eof:
211 logger.debug('time: %s, endtime: %s' % (time.time(), endtime))
212 try:
213 if select.select([process.stdout], [], [], 5)[0] != []:
Brad Bishopc342db32019-05-15 21:57:59 -0400214 reader = codecs.getreader('utf-8')(process.stdout, 'ignore')
Brad Bishopd5ae7d92018-06-14 09:52:03 -0700215 data = reader.read(1024, 4096)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500216 if not data:
217 process.stdout.close()
218 eof = True
219 else:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500220 output += data
221 logger.debug('Partial data from SSH call: %s' % data)
222 endtime = time.time() + timeout
223 except InterruptedError:
224 continue
225
226 # process hasn't returned yet
227 if not eof:
228 process.terminate()
229 time.sleep(5)
230 try:
231 process.kill()
232 except OSError:
233 pass
234 endtime = time.time() - starttime
235 lastline = ("\nProcess killed - no output for %d seconds. Total"
236 " running time: %d seconds." % (timeout, endtime))
237 logger.debug('Received data from SSH call %s ' % lastline)
238 output += lastline
239
240 else:
Brad Bishopc342db32019-05-15 21:57:59 -0400241 output = process.communicate()[0].decode('utf-8', errors='ignore')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500242 logger.debug('Data from SSH call: %s' % output.rstrip())
243
244 options = {
245 "stdout": subprocess.PIPE,
246 "stderr": subprocess.STDOUT,
247 "stdin": None,
248 "shell": False,
249 "bufsize": -1,
Andrew Geissler82c905d2020-04-13 13:39:40 -0500250 "start_new_session": True,
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500251 }
252 options.update(opts)
253 output = ''
254 process = None
255
256 # Unset DISPLAY which means we won't trigger SSH_ASKPASS
257 env = os.environ.copy()
258 if "DISPLAY" in env:
259 del env['DISPLAY']
260 options['env'] = env
261
262 try:
263 run()
264 except:
265 # Need to guard against a SystemExit or other exception ocurring
266 # whilst running and ensure we don't leave a process behind.
267 if process.poll() is None:
268 process.kill()
269 logger.debug('Something went wrong, killing SSH process')
270 raise
271 return (process.wait(), output.rstrip())