1# 2# Copyright (C) 2016 Intel Corporation 3# 4# SPDX-License-Identifier: MIT 5# 6 7import os 8import time 9import select 10import logging 11import subprocess 12import codecs 13 14from . import OETarget 15 16class OESSHTarget(OETarget): 17 def __init__(self, logger, ip, server_ip, timeout=300, user='root', 18 port=None, server_port=0, **kwargs): 19 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 33 self.server_port = server_port 34 self.timeout = timeout 35 self.user = user 36 ssh_options = [ 37 '-o', 'ServerAliveCountMax=2', 38 '-o', 'ServerAliveInterval=30', 39 '-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 ] 48 self._monitor_dumper = None 49 self.target_dumper = None 50 51 def start(self, **kwargs): 52 pass 53 54 def stop(self, **kwargs): 55 pass 56 57 @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 66 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 83 def run(self, command, timeout=None): 84 """ 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 102 status, output = self._run(sshCmd, processTimeout, True) 103 self.logger.debug('Command: %s\nStatus: %d Output: %s\n' % (command, status, output)) 104 if (status == 255) and (('No route to host') in output): 105 if self.monitor_dumper: 106 self.monitor_dumper.dump_monitor() 107 if status == 255: 108 if self.target_dumper: 109 self.target_dumper.dump_target() 110 if self.monitor_dumper: 111 self.monitor_dumper.dump_monitor() 112 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 131 def copyFrom(self, remoteSrc, localDst, warn_on_failure=False): 132 """ 133 Copy file from target. 134 """ 135 remotePath = '%s@%s:%s' % (self.user, self.ip, remoteSrc) 136 scpCmd = self.scp + [remotePath, localDst] 137 (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) 141 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 229 output_raw = b'' 230 starttime = time.time() 231 process = subprocess.Popen(command, **options) 232 if timeout: 233 endtime = starttime + timeout 234 eof = False 235 os.set_blocking(process.stdout.fileno(), False) 236 while time.time() < endtime and not eof: 237 try: 238 logger.debug('Waiting for process output: time: %s, endtime: %s' % (time.time(), endtime)) 239 if select.select([process.stdout], [], [], 5)[0] != []: 240 # wait a bit for more data, tries to avoid reading single characters 241 time.sleep(0.2) 242 data = process.stdout.read() 243 if not data: 244 eof = True 245 else: 246 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')) 249 endtime = time.time() + timeout 250 except InterruptedError: 251 logger.debug('InterruptedError') 252 continue 253 254 process.stdout.close() 255 256 # process hasn't returned yet 257 if not eof: 258 process.terminate() 259 time.sleep(5) 260 try: 261 process.kill() 262 except OSError: 263 logger.debug('OSError when killing process') 264 pass 265 endtime = time.time() - starttime 266 lastline = ("\nProcess killed - no output for %d seconds. Total" 267 " running time: %d seconds." % (timeout, endtime)) 268 logger.debug('Received data from SSH call:\n%s ' % lastline) 269 output += lastline 270 271 else: 272 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 287 288 options = { 289 "stdout": subprocess.PIPE, 290 "stderr": subprocess.STDOUT, 291 "stdin": None, 292 "shell": False, 293 "bufsize": -1, 294 "start_new_session": True, 295 } 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 315 316 return (process.returncode, output.rstrip()) 317