xref: /openbmc/openbmc-test-automation/ffdc/lib/ssh_utility.py (revision e5c4439f14a8fc199a34e9a250a071855c96b1ef)
1#!/usr/bin/env python3
2
3import logging
4import socket
5import time
6from socket import timeout as SocketTimeout
7
8import paramiko
9from paramiko.buffered_pipe import PipeTimeout as PipeTimeout
10from paramiko.ssh_exception import (
11    AuthenticationException,
12    BadHostKeyException,
13    NoValidConnectionsError,
14    SSHException,
15)
16from scp import SCPClient, SCPException
17
18
19class SSHRemoteclient:
20    r"""
21    Class to create ssh connection to remote host
22    for remote host command execution and scp.
23    """
24
25    def __init__(self, hostname, username, password, port_ssh):
26        r"""
27        Initialize the FFDCCollector object with the provided remote host
28        details.
29
30        This method initializes an FFDCCollector object with the given
31        attributes, which represent the details of the remote (targeting)
32        host. The attributes include the hostname, username, password, and
33        SSH port.
34
35        Parameters:
36            hostname (str): Name or IP address of the remote (targeting) host.
37            username (str): User on the remote host with access to FFDC files.
38            password (str): Password for the user on the remote host.
39            port_ssh (int): SSH port value. By default, 22.
40
41        Returns:
42            None
43        """
44        self.ssh_output = None
45        self.ssh_error = None
46        self.sshclient = None
47        self.scpclient = None
48        self.hostname = hostname
49        self.username = username
50        self.password = password
51        self.port_ssh = port_ssh
52
53    def ssh_remoteclient_login(self):
54        r"""
55        Connect to remote host using the SSH client.
56
57        Returns:
58            bool: The method return True on success and False in failure.
59        """
60        is_ssh_login = True
61        try:
62            # SSHClient to make connections to the remote server
63            self.sshclient = paramiko.SSHClient()
64            # setting set_missing_host_key_policy() to allow any host
65            self.sshclient.set_missing_host_key_policy(
66                paramiko.AutoAddPolicy()
67            )
68            # Connect to the server
69            self.sshclient.connect(
70                hostname=self.hostname,
71                port=self.port_ssh,
72                username=self.username,
73                password=self.password,
74                banner_timeout=120,
75                timeout=60,
76                look_for_keys=False,
77            )
78
79        except (
80            BadHostKeyException,
81            AuthenticationException,
82            SSHException,
83            NoValidConnectionsError,
84            socket.error,
85        ) as e:
86            is_ssh_login = False
87            print("SSH Login: Exception: %s" % e)
88
89        return is_ssh_login
90
91    def ssh_remoteclient_disconnect(self):
92        r"""
93        Disconnect from the remote host using the SSH client.
94
95        This method disconnects from the remote host using the SSH client
96        established during the FFDC collection process. The method does not
97        return any value.
98
99        Returns:
100            None
101        """
102        if self.sshclient:
103            self.sshclient.close()
104
105        if self.scpclient:
106            self.scpclient.close()
107
108    def execute_command(self, command, default_timeout=60):
109        r"""
110        Execute a command on the remote host using the SSH client.
111
112        This method executes a provided command on the remote host using the
113        SSH client. The method takes the command string as an argument and an
114        optional default_timeout parameter of 60 seconds, which specifies the
115        timeout for the command execution.
116
117        The method returns the output of the executed command as a string.
118
119        Parameters:
120            command (str):                   The command string to be executed
121                                             on the remote host.
122            default_timeout (int, optional): The timeout for the command
123                                             execution. Defaults to 60 seconds.
124
125        Returns:
126            str: The output of the executed command as a string.
127        """
128        empty = ""
129        cmd_start = time.time()
130        try:
131            stdin, stdout, stderr = self.sshclient.exec_command(
132                command, timeout=default_timeout
133            )
134            start = time.time()
135            while time.time() < start + default_timeout:
136                # Need to do read/write operation to trigger
137                # paramiko exec_command timeout mechanism.
138                xresults = stderr.readlines()
139                results = "".join(xresults)
140                time.sleep(1)
141                if stdout.channel.exit_status_ready():
142                    break
143            cmd_exit_code = stdout.channel.recv_exit_status()
144
145            # Convert list of string to one string
146            err = ""
147            out = ""
148            for item in results:
149                err += item
150            for item in stdout.readlines():
151                out += item
152
153            return cmd_exit_code, err, out
154
155        except (
156            paramiko.AuthenticationException,
157            paramiko.SSHException,
158            paramiko.ChannelException,
159            SocketTimeout,
160        ) as e:
161            # Log command with error.
162            # Return to caller for next command, if any.
163            logging.error(
164                "\n\tERROR: Fail remote command %s %s" % (e.__class__, e)
165            )
166            logging.error(
167                "\tCommand '%s' Elapsed Time %s"
168                % (
169                    command,
170                    time.strftime(
171                        "%H:%M:%S", time.gmtime(time.time() - cmd_start)
172                    ),
173                )
174            )
175            return 0, empty, empty
176
177    def scp_connection(self):
178        r"""
179        Establish an SCP connection for file transfer.
180
181        This method creates an SCP connection for file transfer using the SSH
182        client established during the FFDC collection process.
183
184        Returns:
185            None
186        """
187        try:
188            self.scpclient = SCPClient(
189                self.sshclient.get_transport(), sanitize=lambda x: x
190            )
191            logging.info(
192                "\n\t[Check] %s SCP transport established.\t [OK]"
193                % self.hostname
194            )
195        except (SCPException, SocketTimeout, PipeTimeout) as e:
196            self.scpclient = None
197            logging.error(
198                "\n\tERROR: SCP get_transport has failed. %s %s"
199                % (e.__class__, e)
200            )
201            logging.info(
202                "\tScript continues generating FFDC on %s." % self.hostname
203            )
204            logging.info(
205                "\tCollected data will need to be manually offloaded."
206            )
207
208    def scp_file_from_remote(self, remote_file, local_file):
209        r"""
210        SCP a file from the remote host to the local host with a filename.
211
212        This method copies a file from the remote host to the local host using
213        the SCP protocol. The method takes the remote_file and local_file as
214        arguments, which represent the full paths of the files on the remote
215        and local hosts, respectively.
216
217
218        Parameters:
219            remote_file (str): The full path filename on the remote host.
220            local_file (str):  The full path filename on the local host.
221
222        Returns:
223            bool: The method return True on success and False in failure.
224        """
225        try:
226            self.scpclient.get(remote_file, local_file, recursive=True)
227        except (SCPException, SocketTimeout, PipeTimeout, SSHException) as e:
228            # Log command with error. Return to caller for next file, if any.
229            logging.error(
230                "\n\tERROR: Fail scp %s from remotehost %s %s\n\n"
231                % (remote_file, e.__class__, e)
232            )
233            # Pause for 2 seconds allowing Paramiko to finish error processing
234            # before next fetch. Without the delay after SCPException, next
235            # fetch will get 'paramiko.ssh_exception.SSHException'> Channel
236            # closed Error.
237            time.sleep(2)
238            return False
239        # Return True for file accounting
240        return True
241