# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

from paramiko import (BadHostKeyException,
                      AuthenticationException,
                      SSHException,
                      SSHClient,
                      AutoAddPolicy,
                      Transport,
                      SFTPClient)
import socket
import time
from marvin.cloudstackException import (
    internalError,
    GetDetailExceptionInfo
)
import contextlib
import logging
from marvin.codes import (
    SUCCESS, FAILED, INVALID_INPUT
)


class SshClient(object):

    '''
    @Desc : SSH Library for Marvin.
    Facilitates SSH,SCP services to marvin users
    @Input: host: Host to connect
            port: port on host to connect
            user: Username to be used for connecting
            passwd: Password for connection
            retries and delay applies for establishing connection
            timeout : Applies while executing command
    '''

    def __init__(self, host, port, user, passwd, retries=60, delay=10,
                 log_lvl=logging.DEBUG, keyPairFiles=None, timeout=10.0):
        self.host = None
        self.port = 22
        self.user = user
        self.passwd = passwd
        self.keyPairFiles = keyPairFiles
        self.ssh = SSHClient()
        self.ssh.set_missing_host_key_policy(AutoAddPolicy())
        self.logger = logging.getLogger('sshClient')
        self.retryCnt = 0
        self.delay = 0
        self.timeout = 3.0
        self.ch = logging.StreamHandler()
        self.ch.setLevel(log_lvl)
        self.logger.addHandler(self.ch)

        # Check invalid host value and raise exception
        # Atleast host is required for connection
        if host is not None and host != '':
            self.host = host
        if retries is not None and retries > 0:
            self.retryCnt = retries
        if delay is not None and delay > 0:
            self.delay = delay
        if timeout is not None and timeout > 0:
            self.timeout = timeout
        if port is not None and port >= 0:
            self.port = port
        if self.createConnection() == FAILED:
            raise internalError("SSH Connection Failed")

    def execute(self, command):
        stdin, stdout, stderr = self.ssh.exec_command(command)
        output = stdout.readlines()
        errors = stderr.readlines()
        results = []
        if output is not None and len(output) == 0:
            if errors is not None and len(errors) > 0:
                for error in errors:
                    results.append(error.rstrip())

        else:
            for strOut in output:
                results.append(strOut.rstrip())
        self.logger.debug("{Cmd: %s via Host: %s} {returns: %s}" %
                          (command, str(self.host), results))
        return results

    def createConnection(self):
        '''
        @Name: createConnection
        @Desc: Creates an ssh connection for
               retries mentioned,along with sleep mentioned
        @Output: SUCCESS on successful connection
                 FAILED If connection through ssh failed
        '''
        ret = FAILED
        except_msg = ''
        while self.retryCnt >= 0:
            try:
                self.logger.debug("====Trying SSH Connection: Host:%s User:%s\
                                   Port:%s RetryCnt:%s===" %
                                  (self.host, self.user, str(self.port),
                                   str(self.retryCnt)))
                if self.keyPairFiles is None:
                    self.ssh.connect(hostname=self.host,
                                     port=self.port,
                                     username=self.user,
                                     password=self.passwd,
                                     timeout=self.timeout,
                                     allow_agent=False)
                else:
                    self.ssh.connect(hostname=self.host,
                                     port=self.port,
                                     username=self.user,
                                     password=self.passwd,
                                     key_filename=self.keyPairFiles,
                                     timeout=self.timeout,
                                     look_for_keys=False
                                     )
                self.logger.debug("===SSH to Host %s port : %s SUCCESSFUL==="
                                  % (str(self.host), str(self.port)))
                ret = SUCCESS
                break
            except BadHostKeyException as e:
                except_msg = GetDetailExceptionInfo(e)
            except AuthenticationException as e:
                except_msg = GetDetailExceptionInfo(e)
            except SSHException as e:
                except_msg = GetDetailExceptionInfo(e)
            except socket.error as e:
                except_msg = GetDetailExceptionInfo(e)
            except Exception as e:
                except_msg = GetDetailExceptionInfo(e)
            finally:
                if self.retryCnt == 0 or ret == SUCCESS:
                    break
                if except_msg != '':
                    self.logger.\
                        exception("SshClient: Exception under "
                                  "createConnection: %s" % except_msg)
                self.retryCnt -= 1
                time.sleep(self.delay)
        return ret

    def runCommand(self, command):
        '''
        @Name: runCommand
        @Desc: Runs a command over ssh and
               returns the result along with status code
        @Input: command to execute
        @Output: 1: status of command executed.
                 SUCCESS : If command execution is successful
                 FAILED    : If command execution has failed
                 2: stdin,stdout,stderr values of command output
        '''
        ret = {"status": FAILED, "stdin": None, "stdout": None,
               "stderr": INVALID_INPUT}
        if command is None or command == '':
            return ret
        try:
            status_check = 1
            stdin, stdout, stderr = self.ssh.\
                exec_command(command, timeout=self.timeout)
            if stdout is not None:
                status_check = stdout.channel.recv_exit_status()
                if status_check == 0:
                    ret["status"] = SUCCESS
                ret["stdout"] = stdout.readlines()
                if stderr is not None:
                    ret["stderr"] = stderr.readlines()
        except Exception as e:
            ret["stderr"] = GetDetailExceptionInfo(e)
            self.logger.exception("SshClient: Exception under runCommand :%s" %
                                  GetDetailExceptionInfo(e))
        finally:
            self.logger.debug(" Host: %s Cmd: %s Output:%s" %
                              (self.host, command, str(ret)))
            return ret

    def scp(self, srcFile, destPath):
        transport = Transport((self.host, int(self.port)))
        transport.connect(username=self.user, password=self.passwd)
        sftp = SFTPClient.from_transport(transport)
        try:
            sftp.put(srcFile, destPath)
        except IOError as e:
            raise e

    def __del__(self):
        self.close()

    def close(self):
        if self.ssh is not None:
            self.ssh.close()
            self.ssh = None
        if self.ch is not None:
            self.logger.removeHandler(self.ch)


if __name__ == "__main__":
    with contextlib.closing(SshClient("127.0.0.1", 22, "root",
                                      "asdf!@34")) as ssh:
        ret = ssh.runCommand("ls -l")
        print(ret)
