xref: /OK3568_Linux_fs/yocto/poky/meta/lib/oeqa/core/target/ssh.py (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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