| #!/usr/bin/env python3 | 
 |  | 
 | import logging | 
 | import socket | 
 | import time | 
 | from socket import timeout as SocketTimeout | 
 |  | 
 | import paramiko | 
 | from paramiko.buffered_pipe import PipeTimeout as PipeTimeout | 
 | from paramiko.ssh_exception import ( | 
 |     AuthenticationException, | 
 |     BadHostKeyException, | 
 |     NoValidConnectionsError, | 
 |     SSHException, | 
 | ) | 
 | from scp import SCPClient, SCPException | 
 |  | 
 |  | 
 | class SSHRemoteclient: | 
 |     r""" | 
 |     Class to create ssh connection to remote host | 
 |     for remote host command execution and scp. | 
 |     """ | 
 |  | 
 |     def __init__(self, hostname, username, password, port_ssh): | 
 |         r""" | 
 |         Initialize the FFDCCollector object with the provided remote host | 
 |         details. | 
 |  | 
 |         This method initializes an FFDCCollector object with the given | 
 |         attributes, which represent the details of the remote (targeting) | 
 |         host. The attributes include the hostname, username, password, and | 
 |         SSH port. | 
 |  | 
 |         Parameters: | 
 |             hostname (str): Name or IP address of the remote (targeting) host. | 
 |             username (str): User on the remote host with access to FFDC files. | 
 |             password (str): Password for the user on the remote host. | 
 |             port_ssh (int): SSH port value. By default, 22. | 
 |  | 
 |         Returns: | 
 |             None | 
 |         """ | 
 |         self.ssh_output = None | 
 |         self.ssh_error = None | 
 |         self.sshclient = None | 
 |         self.scpclient = None | 
 |         self.hostname = hostname | 
 |         self.username = username | 
 |         self.password = password | 
 |         self.port_ssh = port_ssh | 
 |  | 
 |     def ssh_remoteclient_login(self): | 
 |         r""" | 
 |         Connect to remote host using the SSH client. | 
 |  | 
 |         Returns: | 
 |             bool: The method return True on success and False in failure. | 
 |         """ | 
 |         is_ssh_login = True | 
 |         try: | 
 |             # SSHClient to make connections to the remote server | 
 |             self.sshclient = paramiko.SSHClient() | 
 |             # setting set_missing_host_key_policy() to allow any host | 
 |             self.sshclient.set_missing_host_key_policy( | 
 |                 paramiko.AutoAddPolicy() | 
 |             ) | 
 |             # Connect to the server | 
 |             self.sshclient.connect( | 
 |                 hostname=self.hostname, | 
 |                 port=self.port_ssh, | 
 |                 username=self.username, | 
 |                 password=self.password, | 
 |                 banner_timeout=120, | 
 |                 timeout=60, | 
 |                 look_for_keys=False, | 
 |             ) | 
 |  | 
 |         except ( | 
 |             BadHostKeyException, | 
 |             AuthenticationException, | 
 |             SSHException, | 
 |             NoValidConnectionsError, | 
 |             socket.error, | 
 |         ) as e: | 
 |             is_ssh_login = False | 
 |             print("SSH Login: Exception: %s" % e) | 
 |  | 
 |         return is_ssh_login | 
 |  | 
 |     def ssh_remoteclient_disconnect(self): | 
 |         r""" | 
 |         Disconnect from the remote host using the SSH client. | 
 |  | 
 |         This method disconnects from the remote host using the SSH client | 
 |         established during the FFDC collection process. The method does not | 
 |         return any value. | 
 |  | 
 |         Returns: | 
 |             None | 
 |         """ | 
 |         if self.sshclient: | 
 |             self.sshclient.close() | 
 |  | 
 |         if self.scpclient: | 
 |             self.scpclient.close() | 
 |  | 
 |     def execute_command(self, command, default_timeout=60): | 
 |         r""" | 
 |         Execute a command on the remote host using the SSH client. | 
 |  | 
 |         This method executes a provided command on the remote host using the | 
 |         SSH client. The method takes the command string as an argument and an | 
 |         optional default_timeout parameter of 60 seconds, which specifies the | 
 |         timeout for the command execution. | 
 |  | 
 |         The method returns the output of the executed command as a string. | 
 |  | 
 |         Parameters: | 
 |             command (str):                   The command string to be executed | 
 |                                              on the remote host. | 
 |             default_timeout (int, optional): The timeout for the command | 
 |                                              execution. Defaults to 60 seconds. | 
 |  | 
 |         Returns: | 
 |             str: The output of the executed command as a string. | 
 |         """ | 
 |         empty = "" | 
 |         cmd_start = time.time() | 
 |         try: | 
 |             stdin, stdout, stderr = self.sshclient.exec_command( | 
 |                 command, timeout=default_timeout | 
 |             ) | 
 |             start = time.time() | 
 |             while time.time() < start + default_timeout: | 
 |                 # Need to do read/write operation to trigger | 
 |                 # paramiko exec_command timeout mechanism. | 
 |                 xresults = stderr.readlines() | 
 |                 results = "".join(xresults) | 
 |                 time.sleep(1) | 
 |                 if stdout.channel.exit_status_ready(): | 
 |                     break | 
 |             cmd_exit_code = stdout.channel.recv_exit_status() | 
 |  | 
 |             # Convert list of string to one string | 
 |             err = "" | 
 |             out = "" | 
 |             for item in results: | 
 |                 err += item | 
 |             for item in stdout.readlines(): | 
 |                 out += item | 
 |  | 
 |             return cmd_exit_code, err, out | 
 |  | 
 |         except ( | 
 |             paramiko.AuthenticationException, | 
 |             paramiko.SSHException, | 
 |             paramiko.ChannelException, | 
 |             SocketTimeout, | 
 |         ) as e: | 
 |             # Log command with error. | 
 |             # Return to caller for next command, if any. | 
 |             logging.error( | 
 |                 "\n\tERROR: Fail remote command %s %s" % (e.__class__, e) | 
 |             ) | 
 |             logging.error( | 
 |                 "\tCommand '%s' Elapsed Time %s" | 
 |                 % ( | 
 |                     command, | 
 |                     time.strftime( | 
 |                         "%H:%M:%S", time.gmtime(time.time() - cmd_start) | 
 |                     ), | 
 |                 ) | 
 |             ) | 
 |             return 0, empty, empty | 
 |  | 
 |     def scp_connection(self): | 
 |         r""" | 
 |         Establish an SCP connection for file transfer. | 
 |  | 
 |         This method creates an SCP connection for file transfer using the SSH | 
 |         client established during the FFDC collection process. | 
 |  | 
 |         Returns: | 
 |             None | 
 |         """ | 
 |         try: | 
 |             self.scpclient = SCPClient( | 
 |                 self.sshclient.get_transport(), sanitize=lambda x: x | 
 |             ) | 
 |             logging.info( | 
 |                 "\n\t[Check] %s SCP transport established.\t [OK]" | 
 |                 % self.hostname | 
 |             ) | 
 |         except (SCPException, SocketTimeout, PipeTimeout) as e: | 
 |             self.scpclient = None | 
 |             logging.error( | 
 |                 "\n\tERROR: SCP get_transport has failed. %s %s" | 
 |                 % (e.__class__, e) | 
 |             ) | 
 |             logging.info( | 
 |                 "\tScript continues generating FFDC on %s." % self.hostname | 
 |             ) | 
 |             logging.info( | 
 |                 "\tCollected data will need to be manually offloaded." | 
 |             ) | 
 |  | 
 |     def scp_file_from_remote(self, remote_file, local_file): | 
 |         r""" | 
 |         SCP a file from the remote host to the local host with a filename. | 
 |  | 
 |         This method copies a file from the remote host to the local host using | 
 |         the SCP protocol. The method takes the remote_file and local_file as | 
 |         arguments, which represent the full paths of the files on the remote | 
 |         and local hosts, respectively. | 
 |  | 
 |  | 
 |         Parameters: | 
 |             remote_file (str): The full path filename on the remote host. | 
 |             local_file (str):  The full path filename on the local host. | 
 |  | 
 |         Returns: | 
 |             bool: The method return True on success and False in failure. | 
 |         """ | 
 |         try: | 
 |             self.scpclient.get(remote_file, local_file, recursive=True) | 
 |         except (SCPException, SocketTimeout, PipeTimeout, SSHException) as e: | 
 |             # Log command with error. Return to caller for next file, if any. | 
 |             logging.error( | 
 |                 "\n\tERROR: Fail scp %s from remotehost %s %s\n\n" | 
 |                 % (remote_file, e.__class__, e) | 
 |             ) | 
 |             # Pause for 2 seconds allowing Paramiko to finish error processing | 
 |             # before next fetch. Without the delay after SCPException, next | 
 |             # fetch will get 'paramiko.ssh_exception.SSHException'> Channel | 
 |             # closed Error. | 
 |             time.sleep(2) | 
 |             return False | 
 |         # Return True for file accounting | 
 |         return True |