Brad Bishop | c342db3 | 2019-05-15 21:57:59 -0400 | [diff] [blame] | 1 | # |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 2 | # Copyright (C) 2016 Intel Corporation |
Brad Bishop | c342db3 | 2019-05-15 21:57:59 -0400 | [diff] [blame] | 3 | # |
| 4 | # SPDX-License-Identifier: MIT |
| 5 | # |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 6 | |
| 7 | import os |
| 8 | import time |
| 9 | import select |
| 10 | import logging |
| 11 | import subprocess |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 12 | import codecs |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 13 | |
| 14 | from . import OETarget |
| 15 | |
| 16 | class OESSHTarget(OETarget): |
| 17 | def __init__(self, logger, ip, server_ip, timeout=300, user='root', |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 18 | port=None, server_port=0, **kwargs): |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 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 |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 33 | self.server_port = server_port |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 34 | self.timeout = timeout |
| 35 | self.user = user |
| 36 | ssh_options = [ |
Andrew Geissler | 6aa7eec | 2023-03-03 12:41:14 -0600 | [diff] [blame^] | 37 | '-o', 'ServerAliveCountMax=2', |
| 38 | '-o', 'ServerAliveInterval=30', |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 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 ] |
Andrew Geissler | c926e17 | 2021-05-07 16:11:35 -0500 | [diff] [blame] | 48 | self._monitor_dumper = None |
Andrew Geissler | 5199d83 | 2021-09-24 16:47:35 -0500 | [diff] [blame] | 49 | self.target_dumper = None |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 50 | |
| 51 | def start(self, **kwargs): |
| 52 | pass |
| 53 | |
| 54 | def stop(self, **kwargs): |
| 55 | pass |
| 56 | |
Andrew Geissler | c926e17 | 2021-05-07 16:11:35 -0500 | [diff] [blame] | 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 | |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 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 | |
Patrick Williams | 2390b1b | 2022-11-03 13:47:49 -0500 | [diff] [blame] | 83 | def run(self, command, timeout=None, ignore_status=True): |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 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 | |
Patrick Williams | 2390b1b | 2022-11-03 13:47:49 -0500 | [diff] [blame] | 102 | status, output = self._run(sshCmd, processTimeout, ignore_status) |
Andrew Geissler | c926e17 | 2021-05-07 16:11:35 -0500 | [diff] [blame] | 103 | self.logger.debug('Command: %s\nStatus: %d Output: %s\n' % (command, status, output)) |
Andrew Geissler | c3d88e4 | 2020-10-02 09:45:00 -0500 | [diff] [blame] | 104 | if (status == 255) and (('No route to host') in output): |
Andrew Geissler | c926e17 | 2021-05-07 16:11:35 -0500 | [diff] [blame] | 105 | if self.monitor_dumper: |
| 106 | self.monitor_dumper.dump_monitor() |
| 107 | if status == 255: |
Andrew Geissler | 5199d83 | 2021-09-24 16:47:35 -0500 | [diff] [blame] | 108 | if self.target_dumper: |
| 109 | self.target_dumper.dump_target() |
Andrew Geissler | c926e17 | 2021-05-07 16:11:35 -0500 | [diff] [blame] | 110 | if self.monitor_dumper: |
| 111 | self.monitor_dumper.dump_monitor() |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 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 | |
Andrew Geissler | 635e0e4 | 2020-08-21 15:58:33 -0500 | [diff] [blame] | 131 | def copyFrom(self, remoteSrc, localDst, warn_on_failure=False): |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 132 | """ |
| 133 | Copy file from target. |
| 134 | """ |
| 135 | remotePath = '%s@%s:%s' % (self.user, self.ip, remoteSrc) |
| 136 | scpCmd = self.scp + [remotePath, localDst] |
Andrew Geissler | 635e0e4 | 2020-08-21 15:58:33 -0500 | [diff] [blame] | 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) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 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 | |
| 224 | def SSHCall(command, logger, timeout=None, **opts): |
| 225 | |
| 226 | def run(): |
| 227 | nonlocal output |
| 228 | nonlocal process |
Andrew Geissler | 6aa7eec | 2023-03-03 12:41:14 -0600 | [diff] [blame^] | 229 | output_raw = b'' |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 230 | starttime = time.time() |
| 231 | process = subprocess.Popen(command, **options) |
| 232 | if timeout: |
| 233 | endtime = starttime + timeout |
| 234 | eof = False |
Andrew Geissler | 6aa7eec | 2023-03-03 12:41:14 -0600 | [diff] [blame^] | 235 | os.set_blocking(process.stdout.fileno(), False) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 236 | while time.time() < endtime and not eof: |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 237 | try: |
Andrew Geissler | 6aa7eec | 2023-03-03 12:41:14 -0600 | [diff] [blame^] | 238 | logger.debug('Waiting for process output: time: %s, endtime: %s' % (time.time(), endtime)) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 239 | if select.select([process.stdout], [], [], 5)[0] != []: |
Andrew Geissler | 6aa7eec | 2023-03-03 12:41:14 -0600 | [diff] [blame^] | 240 | # wait a bit for more data, tries to avoid reading single characters |
| 241 | time.sleep(0.2) |
| 242 | data = process.stdout.read() |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 243 | if not data: |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 244 | eof = True |
| 245 | else: |
Andrew Geissler | 6aa7eec | 2023-03-03 12:41:14 -0600 | [diff] [blame^] | 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')) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 249 | endtime = time.time() + timeout |
| 250 | except InterruptedError: |
Andrew Geissler | 6aa7eec | 2023-03-03 12:41:14 -0600 | [diff] [blame^] | 251 | logger.debug('InterruptedError') |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 252 | continue |
| 253 | |
Andrew Geissler | 6aa7eec | 2023-03-03 12:41:14 -0600 | [diff] [blame^] | 254 | process.stdout.close() |
| 255 | |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 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: |
Andrew Geissler | 6aa7eec | 2023-03-03 12:41:14 -0600 | [diff] [blame^] | 263 | logger.debug('OSError when killing process') |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 264 | pass |
| 265 | endtime = time.time() - starttime |
| 266 | lastline = ("\nProcess killed - no output for %d seconds. Total" |
| 267 | " running time: %d seconds." % (timeout, endtime)) |
Andrew Geissler | 6aa7eec | 2023-03-03 12:41:14 -0600 | [diff] [blame^] | 268 | logger.debug('Received data from SSH call:\n%s ' % lastline) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 269 | output += lastline |
| 270 | |
| 271 | else: |
Andrew Geissler | 6aa7eec | 2023-03-03 12:41:14 -0600 | [diff] [blame^] | 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 |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 287 | |
| 288 | options = { |
| 289 | "stdout": subprocess.PIPE, |
| 290 | "stderr": subprocess.STDOUT, |
| 291 | "stdin": None, |
| 292 | "shell": False, |
| 293 | "bufsize": -1, |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 294 | "start_new_session": True, |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 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 |
Andrew Geissler | 6aa7eec | 2023-03-03 12:41:14 -0600 | [diff] [blame^] | 315 | |
| 316 | return (process.returncode, output.rstrip()) |