class FSM(object):
    """This class represents Finite State Machine for the current device connection.

    Here is the example of usage::

        to be done


    The example action::

        def send_newline(ctx):
            ctx.ctrl.sendline()
            return True

        def error(ctx):
            ctx.message = "Filesystem error"
            return False

        def readonly(ctx):
            ctx.message = "Filesystem is readonly"
            return False

    The ctx object description refer to :class:`condoor.controllers.fsm.FSM`.

    If the action returns True then the FSM continues processing. If the action returns False then FSM stops
    and the error message passed back to the ctx object is posted to the log.


    The FSM state is the integer number. The FSM starts with initial ``state=0`` and finishes if the ``next_state``
    is set to -1.

    If action returns False then FSM returns False. FSM returns True if reaches the -1 state.

    """

    class Context(object):
        """FSM Context class."""

        _slots__ = ('fsm_name', 'ctrl', 'event', 'state', 'finished', 'msg', 'pattern', 'device')

        def __init__(self, fsm_name, device):
            """Initialize the FSM context object.

            Args:
                fsm_name (str): Name of the FSM. This is used for logging.
                device (object): The device object.
            """
            self.device = device
            self.ctrl = device.ctrl
            self.fsm_name = fsm_name
            self.event = None
            self.state = 0
            self.finished = False
            self.msg = ""
            self.pattern = None

        def __str__(self):
            """Return the string representing the FSM context."""
            return "FSM Context:E={},S={},FI={},M='{}'".format(
                self.event, self.state, self.finished, self.msg)

    def __init__(self, name, device, events, transitions, init_pattern=None, timeout=300, searchwindowsize=None,
                 max_transitions=20):
        """Initialize FSM object.

        Args:
            name (str): Name of the state machine used for logging purposes. Can't be *None*
            ctrl (object): Controller class representing the connection to the device
            events (list): List of expected strings or pexpect.TIMEOUT exception expected from the device.
            transitions (list): List of tuples in defining the state machine transitions.
            init_pattern (str): The pattern that was expected in the previous operation.
            timeout (int): Timeout between states in seconds. Defaults to 300 seconds.
            searchwindowsize (int): The size of search window. Defaults to -1.
            max_transitions (int): Max number of transitions allowed before quiting the FSM.

        The transition tuple format is as follows::

            (event, [list_of_states], next_state, action, timeout)

        - event (str): string from the `events` list which is expected to be received from device.
        - list_of_states (list): List of FSM states that triggers the action in case of event occurrence.
        - next_state (int): Next state for FSM transition.
        - action (func): function to be executed if the current FSM state belongs to `list_of_states` and the `event`
          occurred. The action can be also *None* then FSM transits to the next state without any action. Action
          can be also the exception, which is raised and FSM stops.
        """
        self.events = events
        self.device = device
        self.ctrl = device.ctrl
        self.timeout = timeout
        self.searchwindowsize = searchwindowsize
        self.name = name
        self.init_pattern = init_pattern
        self.max_transitions = max_transitions
        self.log = device.chain.connection.log

        self.transition_table = self._compile(transitions, events)

    def _compile(self, transitions, events):
        compiled = {}
        for transition in transitions:
            event, states, new_state, act, timeout = transition
            if not isinstance(states, list):
                states = list(states)
            try:
                event_index = events.index(event)
            except ValueError:
                self.log("Transition for non-existing event: {}".format(
                    event if isinstance(event, str) else event.pattern))
            else:
                for state in states:
                    key = (event_index, state)
                    compiled[key] = (new_state, act, timeout)

        return compiled

    def run(self):
        """Start the FSM.

        Returns:
            boolean: True if FSM reaches the last state or false if the exception or error message was raised

        """
        ctx = FSM.Context(self.name, self.device)
        transition_counter = 0
        timeout = self.timeout
        self.log("{} Start".format(self.name))
        while transition_counter < self.max_transitions:
            transition_counter += 1
            try:
                start_time = time()
                if self.init_pattern is None:
                    ctx.event = self.ctrl.expect(self.events, searchwindowsize=self.searchwindowsize, timeout=timeout)
                else:
                    self.log("INIT_PATTERN={}".format(pattern_to_str(self.init_pattern)))
                    try:
                        ctx.event = self.events.index(self.init_pattern)
                    except ValueError:
                        self.log("INIT_PATTERN unknown.")
                        continue
                    finally:
                        self.init_pattern = None

                finish_time = time() - start_time
                key = (ctx.event, ctx.state)
                ctx.pattern = self.events[ctx.event]

                if key in self.transition_table:
                    transition = self.transition_table[key]
                    next_state, action_instance, next_timeout = transition
                    self.log("E={},S={},T={},RT={:.2f}".format(ctx.event, ctx.state, timeout, finish_time))
                    if callable(action_instance) and not isclass(action_instance):
                        if not action_instance(ctx):
                            self.log("Error: {}".format(ctx.msg))
                            return False
                    elif isinstance(action_instance, Exception):
                        self.log("A=Exception {}".format(action_instance))
                        raise action_instance
                    elif action_instance is None:
                        self.log("A=None")
                    else:
                        self.log("FSM Action is not callable: {}".format(str(action_instance)))
                        raise RuntimeWarning("FSM Action is not callable")

                    if next_timeout != 0:  # no change if set to 0
                        timeout = next_timeout
                    ctx.state = next_state
                    self.log("NS={},NT={}".format(next_state, timeout))

                else:
                    self.log("Unknown transition: EVENT={},STATE={}".format(ctx.event, ctx.state))
                    continue

            except EOF:
                raise ConnectionError("Session closed unexpectedly", self.ctrl.hostname)

            if ctx.finished or next_state == -1:
                self.log("{} Stop at E={},S={}".format(self.name, ctx.event, ctx.state))
                return True

        # check while else if even exists
        self.log("FSM looped. Exiting")
        return False def pattern_to_str(pattern):
    """Convert regex pattern to string.

    If pattern is string it returns itself,
    if pattern is SRE_Pattern then return pattern attribute
    :param pattern: pattern object or string
    :return: str: pattern sttring
    """
    if isinstance(pattern, str):
        return repr(pattern)
    else:
        return repr(pattern.pattern) if pattern else None class Protocol(object):
    """Base Protocol class implementation."""

    def __init__(self, device):
        """Initialize the protocol object."""
        self.device = device
        self.port = self.device.node_info.port
        self.password = self.device.node_info.password
        self.username = self.device.node_info.username

        self.last_pattern = None
        self.matched_string = None
        self.log = device.chain.connection.log

    def connect(self, driver):
        """Connect using specific protocol."""
        raise NotImplementedError("Connection method not implemented")

    def authenticate(self, driver):
        """Authenticate using specific protocol."""
        raise NotImplementedError("Authentication method not implemented")

    def disconnect(self, driver):
        """Disconnect using specific protocol."""
        raise NotImplementedError("Disconnect method not implemented")

    @property
    def hostname(self):
        """Return hostname from url."""
        return self.device.node_info.hostname

    def _acquire_password(self):
        # TODO: Remove
        password = self.password
        # if not password:
        #     if self.account_manager:
        #         self._dbg(20,
        #                   "{}: {}: Acquiring password for {} from system KeyRing".format(
        #                       self.protocol, self.hostname, self.username))
        #         password = self.account_manager.get_password(self.hostname, self.username, interact=True)
        #         if not password:
        #             self._dbg(30, "{}: {}: Password for {} does not exists in KeyRing".format(
        #                 self.protocol, self.hostname, self.username))
        return password @action
def a_send(text, ctx):
    """Send text line to the controller."""
    ctx.ctrl.send(text)
    return True @action
def a_send_password(password, ctx):
    """Send the password text.

    Before sending the password local echo is disabled.
    If password not provided it disconnects from the device and raises ConnectionAuthenticationError exception.
    """
    if password:
        ctx.ctrl.send_command(password, password=True)
        return True
    else:
        ctx.ctrl.disconnect()
        raise ConnectionAuthenticationError("Password not provided", ctx.ctrl.hostname) @action
def a_authentication_error(ctx):
    """Raise ConnectionAuthenticationError exception and disconnect."""
    ctx.ctrl.disconnect()
    raise ConnectionAuthenticationError("Authentication failed", ctx.ctrl.hostname) @action
def a_unable_to_connect(ctx):
    """Provide detailed information about the session (before, after) when unable to connect.

    The state machine finishes without exception
    """
    message = "{}{}".format(ctx.ctrl.before, ctx.ctrl.after)
    ctx.msg = message.strip().splitlines()[-1]
    ctx.device.last_error_msg = ctx.msg
    # ctx.msg = "{}{}".format(ctx.ctrl.before, ctx.ctrl.after)
    return False @action
def a_save_last_pattern(obj, ctx):
    """Save last pattern in the context."""
    obj.last_pattern = ctx.pattern
    return True @action
def a_standby_console(ctx):
    """Raise ConnectionError exception when connected to standby console."""
    ctx.device.is_console = True
    ctx.ctrl.disconnect()
    raise ConnectionError("Standby console", ctx.ctrl.hostname) @action
def a_send_username(username, ctx):
    """Sent the username text."""
    if username:
        ctx.ctrl.sendline(username)
        return True
    else:
        ctx.ctrl.disconnect()
        raise ConnectionAuthenticationError("Username not provided", ctx.ctrl.hostname) @action
def a_send_newline(ctx):
    """Send the CR/LF."""
    ctx.ctrl.send("\r\n")
    return True class ConnectionError(GeneralError):
    """General connection error.""" class ConnectionTimeoutError(ConnectionError):
    """Connection timeout error.""" class CommandSyntaxError(CommandError):
    """Command syntax error."""

    pass
from functools import partial
from condoor.fsm import FSM
from condoor.utils import pattern_to_str
from condoor.protocols.base import Protocol
from condoor.actions import a_send, a_send_password, a_authentication_error, a_unable_to_connect,\
    a_save_last_pattern, a_standby_console, a_send_username, a_send_newline
from condoor.exceptions import ConnectionError, ConnectionTimeoutError, CommandSyntaxError
from condoor.config import CONF
import re
import pexpect
"""Provides Telnet driver class."""





# Telnet connection initiated
ESCAPE_CHAR = "Escape character is|Open"
# Connection refused i.e. line busy on TS
CONNECTION_REFUSED = re.compile("Connection refused")
PASSWORD_OK = "[Pp]assword [Oo][Kk]"

_C = CONF['protocol']['telnet']


class Telnet(Protocol):
    """Telnet protocol implementation."""

    def __init__(self, device):
        """Initialize the Telnet object."""
        super(Telnet, self).__init__(device)

    def get_command(self):
        """Return the Telnet protocol specific command to connect."""
        return "telnet {} {}".format(self.hostname, self.port)

    def connect(self, driver):
        """Connect using the Telnet protocol specific FSM."""
        #              0            1                              2                      3
        events = [ESCAPE_CHAR, driver.press_return_re, driver.standby_re, driver.username_re,
                  #            4                   5                  6                     7
                  driver.password_re, driver.more_re, self.device.prompt_re, driver.rommon_re,
                  #       8                              9              10            11              12
                  driver.unable_to_connect_re, driver.timeout_re, pexpect.TIMEOUT, PASSWORD_OK, driver.syntax_error_re]

        transitions = [
            (ESCAPE_CHAR, [0], 1, None, _C['esc_char_timeout']),
            (driver.syntax_error_re, [0], -1, CommandSyntaxError("Command syntax error"), 0),
            (driver.press_return_re, [0, 1], 1, a_send_newline, 10),
            (PASSWORD_OK, [0, 1], 1, a_send_newline, 10),
            (driver.standby_re, [0, 5], -1, partial(a_standby_console), 0),
            (driver.username_re, [0, 1, 5, 6], -1, partial(a_save_last_pattern, self), 0),
            (driver.password_re, [0, 1, 5], -1, partial(a_save_last_pattern, self), 0),
            (driver.more_re, [0, 5], 7, partial(a_send, "q"), 10),
            # router sends it again to delete
            (driver.more_re, [7], 8, None, 10),
            # (prompt, [0, 1, 5], 6, a_send_newline, 10),
            (self.device.prompt_re, [0, 1, 5], 0, None, 10),
            (self.device.prompt_re, [6, 8, 5], -1, partial(a_save_last_pattern, self), 0),
            (driver.rommon_re, [0, 1, 5], -1, partial(a_save_last_pattern, self), 0),

            (driver.unable_to_connect_re, [0, 1, 5], -1, a_unable_to_connect, 0),