Example #1
0
def test_groups_at_regex_helper():
    import re
    from moler.cmd import RegexHelper
    regex_helper = RegexHelper()
    if regex_helper.search_compiled(re.compile(r"(\d+)_([A-Z]+)(\w+),(\d+)"),
                                    "111_ABCef,222"):
        ones, uppers, lowers, twos = regex_helper.groups()
    assert ones == '111'
    assert uppers == 'ABC'
    assert lowers == 'ef'
    assert twos == '222'
Example #2
0
class CommandTextualGeneric(Command):
    """Base class for textual commands."""

    _re_default_prompt = re.compile(r_default_prompt)  # When user provides no prompt
    _default_newline_chars = ("\n", "\r")  # New line chars on device, not system with script!

    def __init__(self, connection, prompt=None, newline_chars=None, runner=None):
        """
        Base class for textual commands.

        :param connection: connection to device.
        :param prompt: expected prompt sending by device after command execution. Maybe String or compiled re.
        :param newline_chars:  new line chars on device (a list).
        :param runner: runner to run command.
        """
        self.command_path = None  # path to command executable - allow non standard locations like /usr/local/bin/
        self._max_index_from_beginning = 20  # Right (from 0 to this) index of substring of command_string passed
        # as _cmd_escaped. Set 0 to disable functionality of substring.
        self._max_index_from_end = 20  # Left (from this to the end) index of substring of command_string passed
        # as _cmd_escaped. Set 0 to disable functionality of substring.
        self._multiline_cmd = False
        self.__command_string = None  # String representing command on device
        self._cmd_escaped = None  # Escaped regular expression string with command
        super(CommandTextualGeneric, self).__init__(connection=connection, runner=runner)
        self.terminating_timeout = 3.0  # value for terminating command if it timeouts. Set positive value for command
        #                                 if they can do anything if timeout. Set 0 for command if it cannot do
        #                                 anything if timeout.
        self.current_ret = dict()  # Placeholder for result as-it-grows, before final write into self._result
        self._cmd_output_started = False  # If false parsing is not passed to command
        self._regex_helper = RegexHelper()  # Object to regular expression matching
        self.ret_required = True  # # Set False for commands not returning parsed result
        self.break_on_timeout = True  # If True then Ctrl+c on timeout
        self._last_not_full_line = None  # Part of line
        self._re_prompt = CommandTextualGeneric._calculate_prompt(prompt)  # Expected prompt on device
        self._newline_chars = newline_chars  # New line characters on device
        self.do_not_process_after_done = True  # Set True if you want to break processing data when command is done. If
        # False then on_new_line will be called after done if more lines are in the same data package.
        self.newline_after_command_string = True  # Set True if you want to send a new line char(s) after command
        # string (sendline from connection)- most cases. Set False if you want to sent command string without adding
        # new line char(s) - send from connection.
        self.wait_for_prompt_on_exception = True  # Set True to wait for command prompt on failure. Set False to cancel
        # command immediately on failure.
        self._concatenate_before_command_starts = True  # Set True to concatenate all strings from connection before
        # command starts, False to split lines on every new line char
        self._stored_exception = None  # Exception stored before it is passed to base class when command is done.
        self._lock_is_done = Lock()
        self._ignore_unicode_errors = True  # If True then UnicodeDecodeError will be logged not raised in data_received
        self._last_recv_time_data_read_from_connection = None  # Time moment when data was really received from
        # connection (not when was passed to command).  Time is given as datetime.datetime instance
        self._remove_ctrlc_chars_for_prompt = True  # after sending Ctrl-C response might be concatenated ^Cprompt
        # This flag removes "^C" from prompt before processing prompt against self._re_prompt
        self._break_exec_regex = None  # Regex if not None then command will call break_cmd when this regex is caught
        # in on_new_line. Do not set directly, use setter break_exec_regex.
        self.break_exec_only_full_line = True  # Set True to consider only full lines to match _break_exec_regex or
        # False to consider also chunks.
        self.enter_on_prompt_without_anchors = False  # Set True to try to match prompt in line without ^ and $.

        if not self._newline_chars:
            self._newline_chars = CommandTextualGeneric._default_newline_chars

        self._re_prompt_without_anchors = regexp_without_anchors(self._re_prompt)

    @property
    def break_exec_regex(self):
        """
        Getter for break_exec_regex

        :return: Regex object or None
        """
        return self._break_exec_regex

    @break_exec_regex.setter
    def break_exec_regex(self, break_exec_regex):
        """
        Setterfor break_exec_regex
        :param break_exec_regex: String with regex, compiled regex object or None
        :return: None
        """
        if isinstance(break_exec_regex, six.string_types):
            break_exec_regex = re.compile(break_exec_regex)
        self._break_exec_regex = break_exec_regex

    @property
    def command_string(self):
        """
        Getter for command_string.

        :return: String with command_string.
        """
        if not self.__command_string or (self.command_path and not self.__command_string.startswith(self.command_path)):
            try:
                self.__command_string = "CANNOT BUILD COMMAND STRING"  # To avoid infinite recursion if
                # build_command_string raises an exception.
                command_string = self.build_command_string()
                if self.command_path:
                    self.__command_string = "{}{}".format(self.command_path, command_string)
                else:
                    self.__command_string = command_string
            finally:
                self._build_command_string_escaped()
        return self.__command_string

    @command_string.setter
    def command_string(self, command_string):
        """
        Setter for command_string.

        :param command_string: String with command to set.
        :return: None.
        """
        self.__command_string = command_string
        self._build_command_string_escaped()

    def _build_command_string_escaped(self):
        """
        Builds escaped command string for regular expression based on command_string property .

        :return: None
        """
        self._cmd_escaped = None
        if self.__command_string is not None:
            command_string_copy = self.__command_string
            if self._multiline_cmd:
                command_string_copy = re.sub('\n', '', self.__command_string)
            if self._max_index_from_beginning != 0 or self._max_index_from_end != 0:
                sub_command_string = self._build_command_string_slice(command_string_copy)
            else:
                sub_command_string = re.escape(command_string_copy)

            self._cmd_escaped = re.compile(sub_command_string)

    def _build_command_string_slice(self, command_string):
        """
        Builds slice of command string.

        :param command_string: command_string to slice.
        :return: String with regex with command slice.
        """
        sub_command_start_string = None
        sub_command_finish_string = None
        re_sub_command_string = None
        if self._max_index_from_beginning != 0:
            sub_command_start_string = re.escape(command_string[:self._max_index_from_beginning])
            re_sub_command_string = sub_command_start_string
        if self._max_index_from_end != 0:
            sub_command_finish_string = re.escape(command_string[-self._max_index_from_end:])
            re_sub_command_string = sub_command_finish_string
        if sub_command_finish_string and sub_command_start_string:
            re_sub_command_string = "{}|{}".format(sub_command_start_string, sub_command_finish_string)
        return re_sub_command_string

    @property
    def _is_done(self):
        return super(CommandTextualGeneric, self)._is_done

    @_is_done.setter
    def _is_done(self, value):
        with self._lock_is_done:
            if self._stored_exception:
                exception = self._stored_exception
                self._stored_exception = None
                super(CommandTextualGeneric, self)._set_exception_without_done(exception=exception)
            if value and not self._is_done:
                self.on_done()
                if self._stored_exception or self.cancelled():
                    self.on_failure()
                else:
                    self.on_success()
            super(CommandTextualGeneric, self.__class__)._is_done.fset(self, value)

    @staticmethod
    def _calculate_prompt(prompt):
        """
        Calculates prompt as regex from passed prompt.
        :param prompt: Prompt as regex  in string or as compiled regex object.
        :return: Compiled regex object.
        """
        if not prompt:
            prompt = CommandTextualGeneric._re_default_prompt
        if isinstance(prompt, six.string_types):
            prompt = re.compile(prompt)
        return prompt

    def has_endline_char(self, line):
        """
        Method to check if line has chars of new line at the right side.

        :param line: String to check.
        :return: True if any new line char was found, False otherwise.
        """
        if line.endswith(self._newline_chars):
            return True
        return False

    def data_received(self, data, recv_time):
        """
        Called by framework when any data are sent by device.

        :param data: List of strings sent by device.
        :param recv_time: time stamp with the moment when the data was read from connection.  Time is given as
         datatime.datetime instance.
        :return: None.
        """
        self._last_recv_time_data_read_from_connection = recv_time
        try:
            lines = data.splitlines(True)
            for current_chunk in lines:
                if self.__class__.__name__ == 'CmConnect':  # pragma: no cover
                    self.logger.debug("{} current_chunk = '{}'".format(self, current_chunk))
                line, is_full_line = self._update_from_cached_incomplete_line(current_chunk=current_chunk)
                if self._cmd_output_started:
                    self._process_line_from_command(line=line, current_chunk=current_chunk, is_full_line=is_full_line)
                else:
                    self._detect_start_of_cmd_output(self._decode_line(line=line), is_full_line)
                    self._cache_line_before_command_start(line=line, is_full_line=is_full_line)
                if self.done() and self.do_not_process_after_done:
                    if self.__class__.__name__ == 'CmConnect':  # pragma: no cover
                        self.logger.debug("{} is done".format(self))
                    break
        except UnicodeDecodeError as ex:
            if self._ignore_unicode_errors:
                self._log(lvl=logging.WARNING,
                          msg="Processing data from '{}' with unicode problem: '{}'.".format(self, ex))
            else:
                # log it just to catch that rare hanging thread issue
                self._log(lvl=logging.WARNING,
                          msg="Processing data from '{}' raised: '{}'.".format(self, ex))
                raise ex
        except Exception as ex:  # pragma: no cover # log it just to catch that rare hanging thread issue
            self._log(lvl=logging.WARNING,
                      msg="Processing data from '{}' raised: '{}'.".format(self, ex))
            raise ex
        finally:
            if self.__class__.__name__ == 'CmConnect':  # pragma: no cover
                self.logger.debug("{} exiting data processing of '{}'".format(self, data))

    def _process_line_from_command(self, current_chunk, line, is_full_line):
        """
        Processes line from command.

        :param current_chunk: Chunk of line sent by connection.
        :param line: Line of output (current_chunk plus previous chunks of this line - if any) without newline char(s).
        :param is_full_line: True if line had newline char(s). False otherwise.
        :return: None.
        """
        decoded_line = self._decode_line(line=line)
        if self.__class__.__name__ == 'CmConnect':  # pragma: no cover
            self.logger.debug("{} line = '{}', decoded_line = '{}', is_full_line={}".format(self, line, decoded_line, is_full_line))
        self.on_new_line(line=decoded_line, is_full_line=is_full_line)

    def _cache_line_before_command_start(self, line, is_full_line):
        """
        Stores output before command starts.

        :param line: Line from device.
        :param is_full_line: True if line had new line char at the end. False otherwise.
        :return: None.
        """
        if self._concatenate_before_command_starts and not self._cmd_output_started and is_full_line:
            self._last_not_full_line = line

    def _update_from_cached_incomplete_line(self, current_chunk):
        """
        Concatenates (if necessary) previous chunk(s) of line and current.

        :param current_chunk: line from connection (full line or incomplete one).
        :return: Concatenated (if necessary) line from connection without newline char(s). Flag: True if line had
         newline char(s), False otherwise.
        """
        line = current_chunk
        if self._last_not_full_line is not None:
            line = u"{}{}".format(self._last_not_full_line, line)
            self._last_not_full_line = None
        is_full_line = self.has_endline_char(line)
        if is_full_line:
            line = self._strip_new_lines_chars(line)
        else:
            self._last_not_full_line = line
        return line, is_full_line

    @abc.abstractmethod
    def build_command_string(self):
        """
        Returns string with command constructed with parameters of object.

        :return:  String with command.
        """

    def on_new_line(self, line, is_full_line):
        """
        Method to parse command output. Will be called after line with command echo.
        Write your own implementation but don't forget to call on_new_line from base class in most cases.

        :param line: Line to parse, new lines are trimmed
        :param is_full_line: True if new line character was removed from line, False otherwise
        :return: None
        """
        if self.is_end_of_cmd_output(line):
            if self._stored_exception:
                self._is_done = True
            elif (self.ret_required and self.has_any_result()) or not self.ret_required:
                if not self.done():
                    self.set_result(self.current_ret)
            else:
                self._log(lvl=logging.DEBUG,
                          msg="Found candidate for final prompt but current ret is None or empty, required not None"
                              " nor empty.")
        else:
            self._break_exec_on_regex(line=line, is_full_line=is_full_line)

    def is_end_of_cmd_output(self, line):
        """
        Checks if end of command is reached.

        :param line: Line from device.
        :return: True if end of command is reached, False otherwise.
        """
        if self._regex_helper.search_compiled(self._re_prompt, line):
            return True
        # when command is broken via Ctrl-C then ^C may be appended to start of prompt
        # if prompt regexp requires "at start of line" via r'^' then such ^C concatenation will falsify prompt
        if self._remove_ctrlc_chars_for_prompt and (len(line) > 2) and line.startswith("^C"):
            non_ctrl_c_started_line = line[2:]
            if self._regex_helper.search_compiled(self._re_prompt, non_ctrl_c_started_line):
                return True
        if self.enter_on_prompt_without_anchors is True:
            if self._regex_helper.search_compiled(self._re_prompt_without_anchors, line):
                self.logger.info("Candidate for prompt '{}' in line '{}'.".format(self._re_prompt.pattern, line))
                self.send_enter()
                self.enter_on_prompt_without_anchors = False
        return False

    def _strip_new_lines_chars(self, line):
        """
        Removes new line char(s) from line.

        :param line: line from device.
        :return: line without new lines chars.
        """
        if len(line) >= 1:
            last_char = line[-1]
            while last_char in self._newline_chars:
                line = line.rstrip(last_char)
                if len(line) >= 1:
                    last_char = line[-1]
                else:
                    last_char = None
        return line

    def _detect_start_of_cmd_output(self, line, is_full_line):
        """
        Checks if command stated.

        :param line: line to check if echo of command is sent by device.
        :param is_full_line: True if line ends with new line char, False otherwise.
        :return: None.
        """
        if (is_full_line and self.newline_after_command_string) or not self.newline_after_command_string:
            if self._regex_helper.search_compiled(self._cmd_escaped, line):
                self._cmd_output_started = True
        if self.__class__.__name__ == 'CmConnect':  # pragma: no cover
            self.logger.debug("{} line = '{}', is_full_line={}, _cmd_output_started={}".format(self, line, is_full_line, self._cmd_output_started))

    def break_cmd(self, silent=False):
        """
        Send ctrl+c to device to break command execution.

        :return: None
        """
        if self.running():
            self.connection.send("\x03")  # ctrl+c
        else:
            if silent is False:
                self._log(lvl=logging.WARNING, msg="Tried to break not running command '{}'. Ignored".format(self))

    def cancel(self):
        """
        Called by framework to cancel the command.

        :return: False if already cancelled or already done, True otherwise.
        """
        self.break_cmd(silent=True)
        return super(CommandTextualGeneric, self).cancel()

    def set_exception(self, exception):
        """
        Set exception object as failure for command object.

        :param exception: An exception object to set.
        :return: None.
        """
        if self.done() or not self.wait_for_prompt_on_exception:
            super(CommandTextualGeneric, self).set_exception(exception=exception)
        else:
            if self._stored_exception is None:
                self._log(logging.INFO,
                          "{}.{} has set exception {!r}".format(self.__class__.__module__, self, exception),
                          levels_to_go_up=2)
                self._stored_exception = exception
            else:
                self._log(logging.INFO,
                          "{}.{} tried set exception {!r} on already set exception {!r}".format(
                              self.__class__.__module__,
                              self, exception,
                              self._stored_exception),
                          levels_to_go_up=2)

    def on_failure(self):
        """
        Callback called by framework when command is just about to finish with failure. Set ret is called.

        :return: None
        """

    def on_success(self):
        """
        Callback called by framework when command is just about to finish with success. Set ret is called.

        :return: None
        """

    def on_done(self):
        """
        Callback called by framework when command is just about to finish.

        :return: None
        """

    def on_timeout(self):
        """
        Callback called by framework when timeout occurs.

        :return: None.
        """
        if self.break_on_timeout:
            self.break_cmd()
        msg = ("Timeout when command_string='{}', _cmd_escaped='{}', _cmd_output_started='{}', ret_required='{}', "
               "break_on_timeout='{}', _last_not_full_line='{}', _re_prompt='{}', do_not_process_after_done='{}', "
               "newline_after_command_string='{}', wait_for_prompt_on_exception='{}', _stored_exception='{}', "
               "current_ret='{}', _newline_chars='{}', _concatenate_before_command_starts='{}', "
               "_command_string_right_index='{}', _command_string_left_index='{}'.").format(
            self.__command_string, self._cmd_escaped.pattern, self._cmd_output_started, self.ret_required,
            self.break_on_timeout, self._last_not_full_line, self._re_prompt.pattern, self.do_not_process_after_done,
            self.newline_after_command_string, self.wait_for_prompt_on_exception, self._stored_exception,
            self.current_ret, self._newline_chars, self._concatenate_before_command_starts,
            self._max_index_from_beginning, self._max_index_from_end)
        self._log(lvl=logging.INFO, msg=msg, levels_to_go_up=2)

    def has_any_result(self):
        """
        Checks if any result was already set by command.

        :return: True if current_ret has collected any data. Otherwise False.
        """
        is_ret = False
        if self.current_ret:
            is_ret = True
        return is_ret

    def send_command(self):
        """
        Sends command string over connection.

        :return: None
        """
        if self.newline_after_command_string:
            self.connection.sendline(self.command_string)
        else:
            self.connection.send(self.command_string)

    def send_enter(self):
        """
        Sends enter over connection.
        :return: None
        """
        self.connection.send("\n")

    def _decode_line(self, line):
        """
        Decodes line if necessary. Put here code to remove colors from terminal etc.

        :param line: line from device to decode.
        :return: decoded line.
        """
        return line

    def _break_exec_on_regex(self, line, is_full_line):
        """
        Breaks the execution of the command if self._break_exec_regex matches line.

        :param line: line from connection.
        :param is_full_line: True if new line character was removed from line, False otherwise
        :return: None
        """
        if self.break_exec_only_full_line and not is_full_line:
            return
        if self.break_exec_regex is not None and self._regex_helper.search_compiled(self.break_exec_regex, line):
            self.break_cmd()
Example #3
0
class CommandTextualGeneric(Command):
    """Base class for textual commands."""

    _re_default_prompt = re.compile(r'^[^<]*[$%#>~]\s*$')  # When user provides no prompt
    _default_newline_chars = ("\n", "\r")  # New line chars on device, not system with script!

    def __init__(self, connection, prompt=None, newline_chars=None, runner=None):
        """
        Base class for textual commands.

        :param connection: connection to device.
        :param prompt: expected prompt sending by device after command execution. Maybe String or compiled re.
        :param newline_chars:  new line chars on device (a list).
        :param runner: runner to run command.
        """
        self._max_index_from_beginning = 20  # Right (from 0 to this) index of substring of command_string passed
        # as _cmd_escaped. Set 0 to disable functionality of substring.
        self._max_index_from_end = 20  # Left (from this to the end) index of substring of command_string passed
        # as _cmd_escaped. Set 0 to disable functionality of substring.
        self.__command_string = None  # String representing command on device
        self._cmd_escaped = None  # Escaped regular expression string with command
        super(CommandTextualGeneric, self).__init__(connection=connection, runner=runner)
        self.terminating_timeout = 3.0  # value for terminating command if it timeouts. Set positive value for command
        #                                 if they can do anything if timeout. Set 0 for command if it cannot do
        #                                 anything if timeout.
        self.current_ret = dict()  # Placeholder for result as-it-grows, before final write into self._result
        self._cmd_output_started = False  # If false parsing is not passed to command
        self._regex_helper = RegexHelper()  # Object to regular expression matching
        self.ret_required = True  # # Set False for commands not returning parsed result
        self.break_on_timeout = True  # If True then Ctrl+c on timeout
        self._last_not_full_line = None  # Part of line
        self._re_prompt = CommandTextualGeneric._calculate_prompt(prompt)  # Expected prompt on device
        self._newline_chars = newline_chars  # New line characters on device
        self.do_not_process_after_done = True  # Set True if you want to break processing data when command is done. If
        # False then on_new_line will be called after done if more lines are in the same data package.
        self.newline_after_command_string = True  # Set True if you want to send a new line char(s) after command
        # string (sendline from connection)- most cases. Set False if you want to sent command string without adding
        # new line char(s) - send from connection.
        self.wait_for_prompt_on_exception = True  # Set True to wait for command prompt on failure. Set False to cancel
        # command immediately on failure.
        self._concatenate_before_command_starts = True  # Set True to concatenate all strings from connection before
        # command starts, False to split lines on every new line char
        self._stored_exception = None  # Exception stored before it is passed to base class when command is done.
        self._lock_is_done = Lock()

        if not self._newline_chars:
            self._newline_chars = CommandTextualGeneric._default_newline_chars

    @property
    def command_string(self):
        """
        Getter for command_string.

        :return: String with command_string.
        """
        if not self.__command_string:
            try:
                self.__command_string = "CANNOT BUILD COMMAND STRING"  # To avoid infinite recursion if
                # build_command_string raises an exception.
                self.__command_string = self.build_command_string()
            finally:
                self._build_command_string_escaped()
        return self.__command_string

    @command_string.setter
    def command_string(self, command_string):
        """
        Setter for command_string.

        :param command_string: Stting with command to set.
        :return: None.
        """
        self.__command_string = command_string
        self._build_command_string_escaped()

    def _build_command_string_escaped(self):
        """
        Builds escaped command string for regular expression based on command_string property .

        :return: None
        """
        self._cmd_escaped = None
        if self.__command_string is not None:
            if self._max_index_from_beginning != 0 or self._max_index_from_end != 0:
                sub_command_string = self._build_command_string_slice()
            else:
                sub_command_string = re.escape(self.__command_string)
            self._cmd_escaped = re.compile(sub_command_string)

    def _build_command_string_slice(self):
        """
        Builds slice of command string.

        :return: String with regex with command slice.
        """
        sub_command_start_string = None
        sub_command_finish_string = None
        if self._max_index_from_beginning != 0:
            sub_command_start_string = re.escape(self.__command_string[:self._max_index_from_beginning])
            re_sub_command_string = sub_command_start_string
        if self._max_index_from_end != 0:
            sub_command_finish_string = re.escape(self.__command_string[-self._max_index_from_end:])
            re_sub_command_string = sub_command_finish_string
        if sub_command_finish_string and sub_command_start_string:
            re_sub_command_string = "{}|{}".format(sub_command_start_string, sub_command_finish_string)
        return re_sub_command_string

    @property
    def _is_done(self):
        return super(CommandTextualGeneric, self)._is_done

    @_is_done.setter
    def _is_done(self, value):
        with self._lock_is_done:
            if self._stored_exception:
                exception = self._stored_exception
                self._stored_exception = None
                super(CommandTextualGeneric, self)._set_exception_without_done(exception=exception)
            if value and not self._is_done:
                self.on_done()
                if self._stored_exception or self.cancelled():
                    self.on_failure()
                else:
                    self.on_success()
            super(CommandTextualGeneric, self.__class__)._is_done.fset(self, value)

    @staticmethod
    def _calculate_prompt(prompt):
        """
        Calculates prompt as regex from passed prompt.
        :param prompt: Prompt as regex  in string or as compiled regex object.
        :return: Compiled regex object.
        """
        if not prompt:
            prompt = CommandTextualGeneric._re_default_prompt
        if isinstance(prompt, six.string_types):
            prompt = re.compile(prompt)
        return prompt

    def has_endline_char(self, line):
        """
        Method to check if line has chars of new line at the right side.

        :param line: String to check.
        :return: True if any new line char was found, False otherwise.
        """
        if line.endswith(self._newline_chars):
            return True
        return False

    def data_received(self, data):
        """
        Called by framework when any data are sent by device.

        :param data: List of strings sent by device.
        :return: None.
        """
        lines = data.splitlines(True)
        for current_chunk in lines:
            line, is_full_line = self._update_from_cached_incomplete_line(current_chunk=current_chunk)
            if self._cmd_output_started:
                self._process_line_from_command(line=line, current_chunk=current_chunk, is_full_line=is_full_line)
            else:
                self._detect_start_of_cmd_output(self._decode_line(line=line), is_full_line)
                self._cache_line_before_command_start(line=line, is_full_line=is_full_line)
            if self.done() and self.do_not_process_after_done:
                break

    def _process_line_from_command(self, current_chunk, line, is_full_line):
        """
        Processes line from command.

        :param current_chunk: Chunk of line sent by connection.
        :param line: Line of output (current_chunk plus previous chunks of this line - if any) without newline char(s).
        :param is_full_line: True if line had newline char(s). False otherwise.
        :return: None.
        """
        decoded_line = self._decode_line(line=line)
        self.on_new_line(line=decoded_line, is_full_line=is_full_line)

    def _cache_line_before_command_start(self, line, is_full_line):
        """
        Stores output before command starts.

        :param line: Line from device.
        :param is_full_line: True if line had new line char at the end. False otherwise.
        :return: None.
        """
        if self._concatenate_before_command_starts and not self._cmd_output_started and is_full_line:
            self._last_not_full_line = line

    def _update_from_cached_incomplete_line(self, current_chunk):
        """
        Concatenates (if necessary) previous chunk(s) of line and current.

        :param current_chunk: line from connection (full line or incomplete one).
        :return: Concatenated (if necessary) line from connection without newline char(s). Flag: True if line had
         newline char(s), False otherwise.
        """
        line = current_chunk
        if self._last_not_full_line is not None:
            line = "{}{}".format(self._last_not_full_line, line)
            self._last_not_full_line = None
        is_full_line = self.has_endline_char(line)
        if is_full_line:
            line = self._strip_new_lines_chars(line)
        else:
            self._last_not_full_line = line
        return line, is_full_line

    @abc.abstractmethod
    def build_command_string(self):
        """
        Returns string with command constructed with parameters of object.

        :return:  String with command.
        """

    def on_new_line(self, line, is_full_line):
        """
        Method to parse command output. Will be called after line with command echo.
        Write your own implementation but don't forget to call on_new_line from base class in most cases.

        :param line: Line to parse, new lines are trimmed
        :param is_full_line: True if new line character was removed from line, False otherwise
        :return: None
        """
        if self.is_end_of_cmd_output(line):
            if self._stored_exception:
                self._is_done = True
            elif (self.ret_required and self.has_any_result()) or not self.ret_required:
                if not self.done():
                    self.set_result(self.current_ret)
            else:
                self._log(lvl=logging.DEBUG,
                          msg="Found candidate for final prompt but current ret is None or empty, required not None"
                              " nor empty.")

    def is_end_of_cmd_output(self, line):
        """
        Checks if end of command is reached.

        :param line: Line from device.
        :return:
        """
        if self._regex_helper.search_compiled(self._re_prompt, line):
            return True
        return False

    def _strip_new_lines_chars(self, line):
        """
        Removes new line char(s) from line.

        :param line: line from device.
        :return: line without new lines chars.
        """
        if len(line) >= 1:
            last_char = line[-1]
            while last_char in self._newline_chars:
                line = line.rstrip(last_char)
                if len(line) >= 1:
                    last_char = line[-1]
                else:
                    last_char = None
        return line

    def _detect_start_of_cmd_output(self, line, is_full_line):
        """
        Checks if command stated.

        :param line: line to check if echo of command is sent by device.
        :param is_full_line: True if line ends with new line char, False otherwise.
        :return: None.
        """
        if (is_full_line and self.newline_after_command_string) or not self.newline_after_command_string:
            if self._regex_helper.search_compiled(self._cmd_escaped, line):
                self._cmd_output_started = True

    def break_cmd(self):
        """
        Send ctrl+c to device to break command execution.

        :return: None
        """
        self.connection.send("\x03")  # ctrl+c

    def cancel(self):
        """
        Called by framework to cancel the command.

        :return: False if already cancelled or already done, True otherwise.
        """
        self.break_cmd()
        return super(CommandTextualGeneric, self).cancel()

    def set_exception(self, exception):
        """
        Set exception object as failure for command object.

        :param exception: An exception object to set.
        :return: None.
        """
        if self.done() or not self.wait_for_prompt_on_exception:
            super(CommandTextualGeneric, self).set_exception(exception=exception)
        else:
            if self._stored_exception is None:
                self._log(logging.INFO,
                          "{}.{} has set exception {!r}".format(self.__class__.__module__, self, exception),
                          levels_to_go_up=2)
                self._stored_exception = exception
            else:
                self._log(logging.INFO,
                          "{}.{} tried set exception {!r} on already set exception {!r}".format(
                              self.__class__.__module__,
                              self, exception,
                              self._stored_exception),
                          levels_to_go_up=2)

    def on_failure(self):
        """
        Callback called by framework when command is just about to finish with failure. Set ret is called.

        :return: None
        """

    def on_success(self):
        """
        Callback called by framework when command is just about to finish with success. Set ret is called.

        :return: None
        """

    def on_done(self):
        """
        Callback called by framework when command is just about to finish.

        :return: None
        """

    def on_timeout(self):
        """
        Callback called by framework when timeout occurs.

        :return: None.
        """
        if self.break_on_timeout:
            self.break_cmd()
        msg = ("Timeout when command_string='{}', _cmd_escaped='{}', _cmd_output_started='{}', ret_required='{}', "
               "break_on_timeout='{}', _last_not_full_line='{}', _re_prompt='{}', do_not_process_after_done='{}', "
               "newline_after_command_string='{}', wait_for_prompt_on_exception='{}', _stored_exception='{}', "
               "current_ret='{}', _newline_chars='{}', _concatenate_before_command_starts='{}', "
               "_command_string_right_index='{}', _command_string_left_index='{}'.").format(
            self.__command_string, self._cmd_escaped, self._cmd_output_started, self.ret_required,
            self.break_on_timeout, self._last_not_full_line, self._re_prompt, self.do_not_process_after_done,
            self.newline_after_command_string, self.wait_for_prompt_on_exception, self._stored_exception,
            self.current_ret, self._newline_chars, self._concatenate_before_command_starts,
            self._max_index_from_beginning, self._max_index_from_end)
        self._log(lvl=logging.INFO, msg=msg, levels_to_go_up=2)

    def has_any_result(self):
        """
        Checks if any result was already set by command.

        :return: True if current_ret has collected any data. Otherwise False.
        """
        is_ret = False
        if self.current_ret:
            is_ret = True
        return is_ret

    def send_command(self):
        """
        Sends command string over connection.

        :return: None
        """
        if self.newline_after_command_string:
            self.connection.sendline(self.command_string)
        else:
            self.connection.send(self.command_string)

    def _decode_line(self, line):
        """
        Decodes line if necessary. Put here code to remove colors from terminal etc.

        :param line: line from device to decode.
        :return: decoded line.
        """
        return line
class CommandTextualGeneric(Command):
    _re_default_prompt = re.compile(
        r'^[^<]*[\$|%|#|>|~]\s*$')  # When user provides no prompt
    _default_new_line_chars = (
        "\n", "\r")  # New line chars on device, not system with script!

    def __init__(self, connection, prompt=None, new_line_chars=None):
        """
        :param connection: connection to device
        :param prompt: expected prompt sending by device after command execution. Maybe String or compiled re
        :param new_line_chars:  new line chars on device
        """
        super(CommandTextualGeneric, self).__init__(connection)
        self.logger = logging.getLogger('moler.conn-observer')
        self.__command_string = None  # String representing command on device
        self.current_ret = dict(
        )  # Placeholder for result as-it-grows, before final write into self._result
        self._cmd_escaped = None  # Escaped regular expression string with command
        self._cmd_output_started = False  # If false parsing is not passed to command
        self._regex_helper = RegexHelper(
        )  # Object to regular expression matching
        self.ret_required = True  # # Set False for commands not returning parsed result
        self.break_on_timeout = True  # If True then Ctrl+c on timeout
        self._last_not_full_line = None  # Part of line
        self._re_prompt = CommandTextualGeneric._calculate_prompt(
            prompt)  # Expected prompt on device
        self._new_line_chars = new_line_chars  # New line characters on device
        if not self._new_line_chars:
            self._new_line_chars = CommandTextualGeneric._default_new_line_chars

    @property
    def command_string(self):
        if not self.__command_string:
            self.__command_string = self.build_command_string()
            self._cmd_escaped = re.escape(self.__command_string)
        return self.__command_string

    @command_string.setter
    def command_string(self, command_string):
        self.__command_string = command_string
        self._cmd_escaped = re.escape(command_string)

    @staticmethod
    def _calculate_prompt(prompt):
        if not prompt:
            prompt = CommandTextualGeneric._re_default_prompt
        if isinstance(prompt, six.string_types):
            prompt = re.compile(prompt)
        return prompt

    def has_endline_char(self, line):
        """
        Method to check if line has chars of new line at the right side
        :param line: String to check
        :return: True if any new line char was found, False otherwise
        """
        if line.endswith(self._new_line_chars):
            return True
        return False

    def data_received(self, data):
        """
        Called by framework when any data are sent by device
        :param data: List of strings sent by device
        :return: Nothing
        """
        lines = data.splitlines(True)
        for line in lines:
            if self._last_not_full_line is not None:
                line = self._last_not_full_line + line
                self._last_not_full_line = None
            is_full_line = self.has_endline_char(line)
            if is_full_line:
                line = self._strip_new_lines_chars(line)
            else:
                self._last_not_full_line = line
            if self._cmd_output_started:
                self.on_new_line(line, is_full_line)
            elif is_full_line:
                self._detect_start_of_cmd_output(line)

    @abc.abstractmethod
    def build_command_string(self):
        """
        :return:  String with command
        """
        pass

    def on_new_line(self, line, is_full_line):
        """
        Method to parse command output. Will be called after line with command echo.
        Write your own implementation but don't forget to call on_new_line from base class
        :param line: Line to parse, new lines are trimmed
        :param is_full_line: True if new line character was removed from line, False otherwise
        :return: Nothing
        """
        if self.is_end_of_cmd_output(line):
            if (self.ret_required
                    and self.has_any_result()) or not self.ret_required:
                if not self.done():
                    self.set_result(self.current_ret)
            else:
                self.logger.debug(
                    "Found candidate for final prompt but current ret is None or empty, required not None nor empty."
                )

    def is_end_of_cmd_output(self, line):
        if self._regex_helper.search_compiled(self._re_prompt, line):
            return True
        return False

    def _strip_new_lines_chars(self, line):
        """
        :param line: line from device
        :return: line without new lines chars
        """
        for char in self._new_line_chars:
            line = line.rstrip(char)
        return line

    def _detect_start_of_cmd_output(self, line):
        """
        :param line: line to check if echo of command is sent by device
        :return: Nothing
        """
        if self._regex_helper.search(self._cmd_escaped, line):
            self._cmd_output_started = True

    def break_cmd(self):
        """
        Send ctrl+c to device to break command execution
        :return:
        """
        self.connection.send("\x03")  # ctrl+c

    def cancel(self):
        """
        Called by framework to cancel the command
        :return:
        """
        self.break_cmd()
        return super(CommandTextualGeneric, self).cancel()

    def on_timeout(self):
        """
        Callback called by framework when timeout occurs
        :return: Nothing
        """
        if self.break_on_timeout:
            self.break_cmd()

    def has_any_result(self):
        """
        :return: True if current_ret has collected any data. Otherwise False
        """
        is_ret = False
        if self.current_ret:
            is_ret = True
        return is_ret
Example #5
0
class CommandTextualGeneric(Command):
    """Base class for textual commands."""

    _re_default_prompt = re.compile(
        r'^[^<]*[\$|%|#|>|~]\s*$')  # When user provides no prompt
    _default_newline_chars = (
        "\n", "\r")  # New line chars on device, not system with script!

    def __init__(self,
                 connection,
                 prompt=None,
                 newline_chars=None,
                 runner=None):
        """
        Base class for textual commands.

        :param connection: connection to device.
        :param prompt: expected prompt sending by device after command execution. Maybe String or compiled re.
        :param newline_chars:  new line chars on device (a list).
        :param runner: runner to run command.
        """
        super(CommandTextualGeneric, self).__init__(connection=connection,
                                                    runner=runner)
        self.__command_string = None  # String representing command on device
        self.current_ret = dict(
        )  # Placeholder for result as-it-grows, before final write into self._result
        self._cmd_escaped = None  # Escaped regular expression string with command
        self._cmd_output_started = False  # If false parsing is not passed to command
        self._regex_helper = RegexHelper(
        )  # Object to regular expression matching
        self.ret_required = True  # # Set False for commands not returning parsed result
        self.break_on_timeout = True  # If True then Ctrl+c on timeout
        self._last_not_full_line = None  # Part of line
        self._re_prompt = CommandTextualGeneric._calculate_prompt(
            prompt)  # Expected prompt on device
        self._newline_chars = newline_chars  # New line characters on device
        self.do_not_process_after_done = True  # Set True if you want to break processing data when command is done. If
        # False then on_new_line will be called after done if more lines are in the same data package.
        self.newline_after_command_string = True  # Set True if you want to send a new line char(s) after command
        # string (sendline from connection)- most cases. Set False if you want to sent command string without adding
        # new line char(s) - send from connection.

        if not self._newline_chars:
            self._newline_chars = CommandTextualGeneric._default_newline_chars

    @property
    def command_string(self):
        """
        Getter for command_string.

        :return: String with command_string.
        """
        if not self.__command_string:
            self.__command_string = self.build_command_string()
            self._cmd_escaped = re.compile(re.escape(self.__command_string))
        return self.__command_string

    @command_string.setter
    def command_string(self, command_string):
        """
        Setter for command_string.

        :param command_string: Stting with command to set.
        :return: Nothing.
        """
        self.__command_string = command_string
        self._cmd_escaped = re.compile(re.escape(command_string))

    @staticmethod
    def _calculate_prompt(prompt):
        if not prompt:
            prompt = CommandTextualGeneric._re_default_prompt
        if isinstance(prompt, six.string_types):
            prompt = re.compile(prompt)
        return prompt

    def has_endline_char(self, line):
        """
        Method to check if line has chars of new line at the right side.

        :param line: String to check.
        :return: True if any new line char was found, False otherwise.
        """
        if line.endswith(self._newline_chars):
            return True
        return False

    def data_received(self, data):
        """
        Called by framework when any data are sent by device.

        :param data: List of strings sent by device.
        :return: Nothing.
        """
        lines = data.splitlines(True)
        for line in lines:
            if self._last_not_full_line is not None:
                line = self._last_not_full_line + line
                self._last_not_full_line = None
            is_full_line = self.has_endline_char(line)
            if is_full_line:
                line = self._strip_new_lines_chars(line)
            else:
                self._last_not_full_line = line
            if self._cmd_output_started:
                self.on_new_line(line, is_full_line)
            elif is_full_line:
                self._detect_start_of_cmd_output(line)
            if self.done() and self.do_not_process_after_done:
                break

    @abc.abstractmethod
    def build_command_string(self):
        """
        Returns string with command constructed with parameters of object.

        :return:  String with command.
        """
        pass

    def on_new_line(self, line, is_full_line):
        """
        Method to parse command output. Will be called after line with command echo.
        Write your own implementation but don't forget to call on_new_line from base class in most cases.

        :param line: Line to parse, new lines are trimmed
        :param is_full_line: True if new line character was removed from line, False otherwise
        :return: Nothing
        """
        if self.is_end_of_cmd_output(line):
            if (self.ret_required
                    and self.has_any_result()) or not self.ret_required:
                if not self.done():
                    self.set_result(self.current_ret)
            else:
                self._log(
                    lvl=logging.DEBUG,
                    msg=
                    "Found candidate for final prompt but current ret is None or empty, required not None nor empty."
                )

    def is_end_of_cmd_output(self, line):
        """
        Checks if end of command is reached.

        :param line: Line from device.
        :return:
        """
        if self._regex_helper.search_compiled(self._re_prompt, line):
            return True
        return False

    def _strip_new_lines_chars(self, line):
        """
        Removes new line char(s) from line.

        :param line: line from device.
        :return: line without new lines chars.
        """
        for char in self._newline_chars:
            line = line.rstrip(char)
        return line

    def _detect_start_of_cmd_output(self, line):
        """
        Checks if command stated.

        :param line: line to check if echo of command is sent by device.
        :return: Nothing.
        """
        if self._regex_helper.search_compiled(self._cmd_escaped, line):
            self._cmd_output_started = True

    def break_cmd(self):
        """
        Send ctrl+c to device to break command execution.

        :return: Nothing
        """
        self.connection.send("\x03")  # ctrl+c

    def cancel(self):
        """
        Called by framework to cancel the command.

        :return: False if already cancelled or already done, True otherwise.
        """
        self.break_cmd()
        return super(CommandTextualGeneric, self).cancel()

    def on_timeout(self):
        """
        Callback called by framework when timeout occurs.

        :return: Nothing.
        """
        if self.break_on_timeout:
            self.break_cmd()

    def has_any_result(self):
        """
        Checks if any result was already set by command.

        :return: True if current_ret has collected any data. Otherwise False.
        """
        is_ret = False
        if self.current_ret:
            is_ret = True
        return is_ret

    def send_command(self):
        """
        Sends command string over connection.

        :return: Nothing
        """
        # TODO: Update runner after asyncio merge.
        if self.newline_after_command_string:
            self.connection.sendline(self.command_string)
        else:
            self.connection.send(self.command_string)
Example #6
0
def test_search_compiled_none():
    from moler.cmd import RegexHelper
    regex_helper = RegexHelper()
    with pytest.raises(WrongUsage) as exc:
        regex_helper.search_compiled(None, '123')
    assert "search_compiled is None" in str(exc)