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        Description of argument(s):
28
29        hostname        Name/IP of the remote (targeting) host
30        username        User on the remote host with access to FFCD files
31        password        Password for user on remote host
32        """
33
34        self.ssh_output = None
35        self.ssh_error = None
36        self.sshclient = None
37        self.scpclient = None
38        self.hostname = hostname
39        self.username = username
40        self.password = password
41        self.port_ssh = port_ssh
42
43    def ssh_remoteclient_login(self):
44        r"""
45        Method to create a ssh connection to remote host.
46        """
47
48        is_ssh_login = True
49        try:
50            # SSHClient to make connections to the remote server
51            self.sshclient = paramiko.SSHClient()
52            # setting set_missing_host_key_policy() to allow any host
53            self.sshclient.set_missing_host_key_policy(
54                paramiko.AutoAddPolicy()
55            )
56            # Connect to the server
57            self.sshclient.connect(
58                hostname=self.hostname,
59                port=self.port_ssh,
60                username=self.username,
61                password=self.password,
62                banner_timeout=120,
63                timeout=60,
64                look_for_keys=False,
65            )
66
67        except (
68            BadHostKeyException,
69            AuthenticationException,
70            SSHException,
71            NoValidConnectionsError,
72            socket.error,
73        ) as e:
74            is_ssh_login = False
75
76        return is_ssh_login
77
78    def ssh_remoteclient_disconnect(self):
79        r"""
80        Clean up.
81        """
82
83        if self.sshclient:
84            self.sshclient.close()
85
86        if self.scpclient:
87            self.scpclient.close()
88
89    def execute_command(self, command, default_timeout=60):
90        """
91        Execute command on the remote host.
92
93        Description of argument(s):
94        command                Command string sent to remote host
95
96        """
97
98        empty = ""
99        cmd_start = time.time()
100        try:
101            stdin, stdout, stderr = self.sshclient.exec_command(
102                command, timeout=default_timeout
103            )
104            start = time.time()
105            while time.time() < start + default_timeout:
106                # Need to do read/write operation to trigger
107                # paramiko exec_command timeout mechanism.
108                xresults = stderr.readlines()
109                results = "".join(xresults)
110                time.sleep(1)
111                if stdout.channel.exit_status_ready():
112                    break
113            cmd_exit_code = stdout.channel.recv_exit_status()
114
115            # Convert list of string to one string
116            err = ""
117            out = ""
118            for item in results:
119                err += item
120            for item in stdout.readlines():
121                out += item
122
123            return cmd_exit_code, err, out
124
125        except (
126            paramiko.AuthenticationException,
127            paramiko.SSHException,
128            paramiko.ChannelException,
129            SocketTimeout,
130        ) as e:
131            # Log command with error. Return to caller for next command, if any.
132            logging.error(
133                "\n\tERROR: Fail remote command %s %s" % (e.__class__, e)
134            )
135            logging.error(
136                "\tCommand '%s' Elapsed Time %s"
137                % (
138                    command,
139                    time.strftime(
140                        "%H:%M:%S", time.gmtime(time.time() - cmd_start)
141                    ),
142                )
143            )
144            return 0, empty, empty
145
146    def scp_connection(self):
147        r"""
148        Create a scp connection for file transfer.
149        """
150        try:
151            self.scpclient = SCPClient(
152                self.sshclient.get_transport(), sanitize=lambda x: x
153            )
154            logging.info(
155                "\n\t[Check] %s SCP transport established.\t [OK]"
156                % self.hostname
157            )
158        except (SCPException, SocketTimeout, PipeTimeout) as e:
159            self.scpclient = None
160            logging.error(
161                "\n\tERROR: SCP get_transport has failed. %s %s"
162                % (e.__class__, e)
163            )
164            logging.info(
165                "\tScript continues generating FFDC on %s." % self.hostname
166            )
167            logging.info(
168                "\tCollected data will need to be manually offloaded."
169            )
170
171    def scp_file_from_remote(self, remote_file, local_file):
172        r"""
173        scp file in remote system to local with date-prefixed filename.
174
175        Description of argument(s):
176        remote_file            Full path filename on the remote host
177
178        local_file             Full path filename on the local host
179                               local filename = date-time_remote filename
180
181        """
182
183        try:
184            self.scpclient.get(remote_file, local_file, recursive=True)
185        except (SCPException, SocketTimeout, PipeTimeout, SSHException) as e:
186            # Log command with error. Return to caller for next file, if any.
187            logging.error(
188                "\n\tERROR: Fail scp %s from remotehost %s %s\n\n"
189                % (remote_file, e.__class__, e)
190            )
191            # Pause for 2 seconds allowing Paramiko to finish error processing before next fetch.
192            # Without the delay after SCPException,
193            #    next fetch will get 'paramiko.ssh_exception.SSHException'> Channel closed Error.
194            time.sleep(2)
195            return False
196        # Return True for file accounting
197        return True
198