#!/usr/bin/env python
# coding=utf-8

"""Python script to automate running commands on switches.
    Cisco Remote Automation via Secure Shell... or C.R.A.SSH for short!

.. currentmodule:: crassh
.. moduleauthor:: Nick Bettison - www.linickx.com
"""

# Import libs
import getpass              # Hide Password Entry
import socket               # TCP/Network/Socket
import time                 # Time
import datetime             # Date
import sys                  #
import getopt               # Command line options
import os                   #
import stat                 # File system
import re                   # Regex
import paramiko             # SSH

# I don't care about long line, deal with it ;)
# pylint: disable=C0301

# Global variables
crassh_version = "2.6"      # Version Control in a Variable
remote_conn = ""            # Paramiko Remote Connection
remote_conn_pre = ""        # Paramiko Remote Connection Settings (pre-connect)

# Python 2 & 3 input compatibility
# pylint: disable=W0622
try:
    input = raw_input
except NameError:
    pass

"""
    Functions
"""

def send_command(command="show ver", hostname="Switch", bail_timeout=0):
    """Sending commands to a switch, router, device, whatever!

        Args:
           command (str):  The Command you wish to run on the device.

           hostname (str): The hostname of the device (*expected in the* ``prompt``).

           bail_timeout (int): How long to wait for ``command`` to finish before giving up.

        Returns:
           str.  A text blob from the device, including line breaks.


        REF: http://blog.timmattison.com/archives/2014/06/25/automating-cisco-switch-interactions/
    """
    global remote_conn, remote_conn_pre

    # Start with empty var & loop
    output = ""
    keeplooping = True

    # Regex for either config or enable
    regex = '^' + hostname + '(.*)(\ )?#'
    theprompt = re.compile(regex)

    # Time when the command started, prepare for timeout.
    now = int(time.time())
    timeout = now + bail_timeout

    # Send the command
    remote_conn.send(command + "\n")

    # loop the output
    while keeplooping:

        # Setup bail timer
        now = int(time.time())
        if now == timeout:
            print("\n Command %s took %s secs to run, bailing!" % (command, str(bail_timeout)))
            output += "crassh bailed on command: " + command
            keeplooping = False
            break

        # update receive buffer whilst waiting for the prompt to come back
        if remote_conn.recv_ready():
            output += remote_conn.recv(2048).decode('utf-8')

            # Search the output for our prompt
            theoutput = output.splitlines()
            for lines in theoutput:
                myregmatch = theprompt.search(lines)

            if myregmatch:
                keeplooping = False

    return output

def do_no_harm(command):
    """Check Commands for dangerous things

    Args:
       command (str):  The Command you wish to run on the device.

    Returns:
       Nothing

    This function will ``sys.exit()`` if an *evil* command is found

    >>> crassh.do_no_harm("show ver")
    >>>

    So, good commands just pass through with no response... maybe I should oneday make it a True/False kind of thing.

    """

    # Innocent until proven guilty
    harmful = False

    # Regex match each "command"
    if re.match("rel", command):
        harmful = True
        error = "reload"

    if re.match("wr(.*)\ e", command):
        harmful = True
        error = "write erase"

    if re.match("del", command):
        harmful = True
        error = "delete"

    if harmful:
        print("")
        print("Harmful Command found - Aborting!")
        print("  \"%s\" tripped the do no harm sensor => %s" % (command, error))
        print("\n To force the use of dangerous things, use -X")
        print_help()

# Simple help print and exit
def print_help(exitcode=0):
    """Prints the Help for the CLI tool

    Args:
       exit (int):  Exit Code

    Returns:
       None

    When called this function will ``sys.exit()``

    """

    global crassh_version

    print("\n Usage: %s -s switches.txt -c commands.txt -p -w -t 45 -e" % sys.argv[0])
    print("   -s supply a text file of switch hostnames or IP addresses [optional]")
    print("   -c supply a text file of commands to run on switches [optional]")
    print("   -w write the output to a file [optional | Default: True]")
    print("   -p print the output to the screen [optional | Default: False]")
    print("   -pw is supported, print to both file & screen [optional]")
    print("   -t set a command timeout in seconds [optional | Default: 60]")
    print("   -T set a connection timeout in seconds [optional | Default: 10]")
    print("   -X disable \"do no harm\" [optional]")
    print("   -Q disable \"quit on failure\" [optional]")
    print("   -e set an enable password [optional]")
    print("   -d set a delay (in seconds) between commands [optional]")
    print("   -A set an Authentication file for SSH credentials [optional]")
    print("   -U set a Username for SSH Authentication [optional]")
    print("   -P set a Password for SSH Authentication [optional]")
    print("   -B set a BACKUP Username for SSH Authentication [optional]")
    print("   -b set a BACKUP Password for SSH Authentication [optional]")
    print("   -E set a BACKUP ENABLE Password [optional]")
    print(" ")
    print("Version: %s" % crassh_version)
    print(" ")
    sys.exit(exitcode)

def isgroupreadable(filepath):
    """Checks if a file is *Group* readable

    Args:
       filepath (str):  Full path to file

    Returns:
       bool.  True/False

    Example:

    >>> print(str(isgroupreadable("file.txt")))
    True

    REF: http://stackoverflow.com/questions/1861836/checking-file-permissions-in-linux-with-python

    """

    st = os.stat(filepath)
    return bool(st.st_mode & stat.S_IRGRP)

def isotherreadable(filepath):
    """Checks if a file is *Other* readable

    Args:
       filepath (str):  Full path to file

    Returns:
       bool.  True/False

    Example:

    >>> print(str(isotherreadable("file.txt")))
    True

    """

    st = os.stat(filepath)
    return bool(st.st_mode & stat.S_IROTH)

def readtxtfile(filepath):
    """Read lines of a text file into an array
    Each line is stripped of whitepace.

    Args:
       filepath (str):  Full path to file

    Returns:
       array.  Contents of file

    Example:

    >>> print(readtxtfile("./routers.txt"))
    1.1.1.1
    1.1.1.2
    1.1.1.3

    """
    # Check if file exists
    if os.path.isfile(filepath) is False:
        print("Cannot find %s" % filepath)
        sys.exit()
    # setup return array
    txtarray = []
    # open our file
    f = open(filepath, 'r')
    # Loop thru the array
    for line in f:
        # Append each line to array
        txtarray.append(line.strip())
    # Return results
    return txtarray

# Read a Crassh Authentication File
def readauthfile(filepath):
    """Read C.R.A.SSH Authentication File

    The file format is a simple, one entry per line, colon separated affair::

        username: nick
        password: cisco

    Args:
       filepath (str):  Full path to file

    Returns:
       tuple.  ``username`` and ``password``

    Example:

    >>> username, password = readauthfile("~/.crasshrc")
    >>> print(username)
    nick
    >>> print(password)
    cisco

    """

    # Check if file exists
    if os.path.isfile(filepath) is False:
        print("Cannot find %s" % filepath)
        sys.exit()
    # Open file
    f = open(filepath, 'r')
    # Loop thru the array
    for fline in f:
        thisline = fline.strip().split(":")
        if thisline[0].strip() == "username":
            username = thisline[1].strip()
        if thisline[0].strip() == "password":
            if isgroupreadable(filepath):
                print("** Password not read from %s - file is GROUP readable ** " % filepath)
            else:
                if isotherreadable(filepath):
                    print("** Password not read from %s - file is WORLD readable **"% filepath)
                else:
                    password = thisline[1].strip()
                    return username, password

def connect(device="127.0.0.1", username="cisco", password="cisco", enable=False,enable_password="cisco", sysexit=False, timeout=10):
    """Connect and get Hostname of Cisco Device

    This function wraps up ``paramiko`` and returns the hostname of the **Cisco** device.
    The function creates two global variables ``remote_conn_pre`` and ``remote_conn`` which
    are the paramiko objects for direct manipulation if necessary.

    Args:
       device (str):  IP Address or Fully Qualifed Domain Name of Device

       username (str): Username for SSH Authentication

       password (str): Password for SSH Authentication

       enable (bool): Is enable going to be needed?

       enable_password (str): The enable password

       sysexit (bool): Should the connecton exit the script on failure?

    Returns:
       str.  The hostname of the device

    Example:

    >>> hostname = connect("10.10.10.10", "nick", "cisco")
    >>> print(hostname)
    r1

    REF:
        * https://pynet.twb-tech.com/blog/python/paramiko-ssh-part1.html
        * http://yenonn.blogspot.co.uk/2013/10/python-in-action-paramiko-handling-ssh.html

    """

    # Global variables - Paramiko Stuff.
    global remote_conn_pre, remote_conn
    hostname = False

    """


    """

    # Create paramiko object
    remote_conn_pre = paramiko.SSHClient()
    # Change default paramiko object settings
    remote_conn_pre.set_missing_host_key_policy(paramiko.AutoAddPolicy())

    print("Connecting to %s ... " % device)

    try:
        remote_conn_pre.connect(
            device, username=username, password=password, allow_agent=False, look_for_keys=False, timeout=timeout)
    except paramiko.AuthenticationException as e:
        print("Authentication Error: %s" % e)
        if sysexit:
            sys.exit()
        return False
    except paramiko.SSHException as e:
        print("SSH Error: %s" % e)
        if sysexit:
            sys.exit()
        return False
    except socket.error as e:
        print("Connection Failed: %s" % e)
        if sysexit:
            sys.exit()
        return False
    except:
        print("Unexpected error:", sys.exc_info()[0])
        if sysexit:
            sys.exit()
        return False

    # Connected! (invoke_shell)
    remote_conn = remote_conn_pre.invoke_shell()

    # Flush buffer.
    output = remote_conn.recv(1000).decode('utf-8')
    del output
    output = ""

    # If we have enable password, send it.
    if enable:
        remote_conn.send("enable\n")
        time.sleep(0.5)
        remote_conn.send(enable_password + "\n")

    # Disable <-- More --> on Output
    remote_conn.sendall("terminal length 0\n")
    time.sleep(0.5)

    while "#" not in output:
        # update receive buffer
        if remote_conn.recv_ready():
            output += remote_conn.recv(1024).decode('utf-8')
    # Clear the Var.
    del output
    output = ""

    # Ok, let's find the device hostname
    remote_conn.sendall("show run | inc hostname \n")
    time.sleep(0.5)

    keeplooping = True
    while keeplooping:
        if remote_conn.recv_ready():
            output += remote_conn.recv(1024).decode('utf-8')
            for subline in output.splitlines():
                if re.match("^hostname", subline):
                    #print("Match %s" % subline)
                    thisrow = subline.split()
                    try:
                        gotdata = thisrow[1]
                        if thisrow[0] == "hostname":
                            hostname = thisrow[1]
                            #prompt = hostname + "#"
                    except IndexError:
                        gotdata = 'null'
                    keeplooping = False

    # Catch looping failures.
    if hostname is False:
        print("Hostname Lookup Failed: \n %s \n" % output)
        if sysexit:
            sys.exit()

    # Found it! Return it!
    return hostname

def disconnect():
    """Disconnect an SSH Session

    Crassh wrapper for paramiko disconnect

    No Argumanets, disconnects the current global variable ``remote_conn_pre``
    """

    global remote_conn_pre
    remote_conn_pre.close()

def main():
    """Main Code Block

    This is the main script that Network Administrators will run.

    No Argumanets. Input is used for missing CLI Switches.
    """

    # import Global Vars
    global input

    # Main Vars (local scope)
    switches = [] # Switches, devices, routers, whatever!
    commands = []
    filenames = []
    sfile = '' # Switch File
    cfile = '' # Command File

    # Default variables (values)
    play_safe = True
    enable = False
    delay_command = False
    writeo = True
    printo = False
    bail_timeout = 60
    connect_timeout = 10
    sysexit = True
    backup_credz = False
    backup_enable = False

    # Default Authentication File Path
    crasshrc = os.path.expanduser("~") + "/.crasshrc"

    # Get script options - http://www.cyberciti.biz/faq/python-command-line-arguments-argv-example/
    try:
        myopts, args = getopt.getopt(sys.argv[1:], "c:s:t:T:d:A:U:P:B:b:E:hpwXeQ")
    except getopt.GetoptError as e:
        print("\n ERROR: %s" % str(e))
        print_help(2)

    for o, a in myopts:
        if o == '-s':
            sfile = a
            switches = readtxtfile(sfile)

        if o == '-c':
            cfile = a
            commands = readtxtfile(cfile)

        if o == '-t':
            bail_timeout = int(a)

        if o == '-T':
            connect_timeout = int(a)

        if o == '-h':
            print("\n Nick\'s Cisco Remote Automation via Secure Shell- Script, or C.R.A.SSH for short!")
            print_help()

        if o == '-p':
            writeo = False
            printo = True

        if o == '-w':
            writeo = True

        if o == '-X':
            play_safe = False

        if o == '-Q':
            sysexit = False

        if o == '-e':
            enable = True

        if o == '-d':
            delay_command = True
            delay_command_time = int(a)

        if o == '-A':
            crasshrc = str(a)

        if o == '-U':
            username = str(a)

        if o == '-P':
            password = str(a)

        if o == '-B':
            backup_credz = True
            backup_username = str(a)

        if o == '-b':
            backup_credz = True
            backup_password = str(a)

        if o == '-E':
            backup_enable = True
            backup_enable_password = str(a)

    # See if we have an Authentication File
    if os.path.isfile(crasshrc) is True:
        try:
            username, password = readauthfile(crasshrc)
        except:
            pass

    # Do we have any switches?
    if sfile == "":
        try:
            iswitch = input("Enter the switch to connect to: ")
            switches.append(iswitch)
        except:
            sys.exit()

    # Do we have any commands?
    if cfile == "":
        try:
            icommand = input("The switch command you want to run: ")
            commands.append(icommand)
        except:
            sys.exit()

    """
        Check the commands are safe
    """
    if play_safe:
        for command in commands:
            do_no_harm(command)
    else:
        print("\n--\n Do no Harm checking DISABLED! \n--\n")

    """
        Capture Switch log in credentials...
    """

    try:
        username
    except:
        try:
            username = input("Enter your username: ")
        except:
            sys.exit()

    try:
        password
    except:
        try:
            password = getpass.getpass("Enter your password:")
        except:
            sys.exit()

    if enable:
        try:
            enable_password = getpass.getpass("Enable password:")
        except:
            sys.exit()

    if backup_credz:
        try:
            backup_password
        except:
            try:
                backup_password = getpass.getpass("Enter your backup SSH password:")
            except:
                sys.exit()
    """
        Time estimations for those delaying commands
    """
    if delay_command:
        time_estimate = datetime.timedelta(0, (len(commands) * (len(switches) * 2) * delay_command_time)) + datetime.datetime.now()
        print(" Start Time: %s" % datetime.datetime.now().strftime("%H:%M:%S (%y-%m-%d)"))
        print(" Estimatated Completion Time: %s" % time_estimate.strftime("%H:%M:%S (%y-%m-%d)"))

    """
        Progress calculations - for big jobs only
    """
    if (len(commands) * len(switches)) > 100:
        counter = 0

    """
        Ready to loop thru switches
    """

    for switch in switches:

        if backup_credz:
            tmp_sysexit = sysexit # re-assign so, don't bail on authentication failure
            sysexit = False

        if enable:
            hostname = connect(switch, username, password, enable, enable_password, sysexit, connect_timeout)
        else:
            hostname = connect(switch, username, password, False, "", sysexit, connect_timeout)

        if isinstance(hostname, bool): # Connection failed, function returned False
            if backup_credz:
                sysexit = tmp_sysexit # put it back, so fail or not (-Q) works as expected on backup credz
                print("Trying backup credentials")
                if backup_enable:
                    hostname = connect(switch, backup_username, backup_password, enable, backup_enable_password, sysexit, connect_timeout)
                else:
                    hostname = connect(switch, backup_username, backup_password, False, "", sysexit, connect_timeout)

                if isinstance(hostname, bool): # Connection failed, function returned False
                    continue
            else:
                continue

        # Write the output to a file (optional) - prepare file + filename before CMD loop
        if writeo:
            filetime = datetime.datetime.now().strftime("%y%m%d-%H%M%S")
            filename = hostname + "-" + filetime + ".txt"
            filenames.append(filename)
            f = open(filename, 'a')

        # Command Loop
        for cmd in commands:

            # Send the Command
            print("%s: Running: %s" % (hostname, cmd))
            output = send_command(cmd, hostname, bail_timeout)

            # Print the output (optional)
            if printo:
                print(output)
            if writeo:
                f.write(output)

            # delay next command (optional)
            if delay_command:
                time.sleep(delay_command_time)

            # Print progress
            try:
                counter
                # Random calculation to find 10 percent
                if (counter % 10) == 0:
                    completion = ((float(counter) / (float(len(commands)) * float(len(switches)))) * 100)
                    if int(completion) > 9:
                        print("\n  %s%% Complete" % int(completion))
                        if delay_command:
                            time_left = datetime.timedelta(0, (((int(len(commands)) * int(len(switches))) + (len(switches) * 0.5)) - counter)) + datetime.datetime.now()
                            print("  Estimatated Completion Time: %s" % time_left.strftime("%H:%M:%S (%y-%m-%d)"))
                        print(" ")
                counter += 1
            except:
                pass


        # /end Command Loop

        if writeo:
            # Close the File
            f.close()


        # Disconnect from SSH
        disconnect()

        if writeo:
            print("Switch %s done, output: %s" % (switch, filename))
        else:
            print("Switch %s done" % switch)

        # Sleep between SSH connections
        time.sleep(1)

    print("\n") # Random line break

    print(" ********************************** ")
    if writeo:
        print("  Output files: ")

        for ofile in filenames:
            print("   - %s" % ofile)

        print(" ---------------------------------- ")
    print(" Script FINISHED ! ")
    if delay_command:
        print(" Finish Time: %s" % datetime.datetime.now().strftime("%H:%M:%S (%y-%m-%d)"))
    print(" ********************************** ")

# If run from interpreter, run main code function.
if __name__ == "__main__":
    main()
