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