Exemplo n.º 1
0
    def exec_command(self,
                     command,
                     timeout=60,
                     output_expected=None,
                     prompt=None):
        """
        Executes the given command.

        This method handles detecting simple boolean conditions such as
        the presence of output or errors.

        :param command:  command to execute, newline appended automatically
        :param timeout:  maximum time, in seconds, to wait for the command to
            finish. 0 to wait forever.
        :param output_expected: If not None, indicates whether output is
            expected (True) or no output is expected (False).
            If the opposite occurs, raise UnexpectedOutput. Default is None.
        :type output_expected: bool or None
        :param prompt: Prompt regex for matching unusual prompts.  This should
            almost never needed.  This parameter is for unusual
            situations like an install config wizard.

        :return: output of the command, minus the command itself.

        :raises TypeError: if output_expected type is incorrect
        :raises CmdlineTimeout: on timeout
        :raises UnexpectedOutput: if output occurs when no output was
            expected, or no output occurs when output was expected
        """

        if output_expected not in (True, False, None):
            raise TypeError("exec_command: output_expected requires a boolean "
                            "value or None")

        self._log.debug('Executing cmd "%s"' % command)

        if prompt is None:
            prompt = self._prompt
        (output, match) = self._send_line_and_wait(command,
                                                   prompt,
                                                   timeout=timeout)

        # CLI adds on escape chars and such sometimes, so to remove the
        # command we just send from the output, split the output into lines,
        # then rejoin it with the first line removed.
        output = '\n'.join(output.splitlines()[1:])

        if ((output_expected is not None)
                and (bool(output) != bool(output_expected))):
            raise exceptions.UnexpectedOutput(command=command,
                                              output=output,
                                              expected_output=output_expected)
        return output
Exemplo n.º 2
0
    def exec_command(self,
                     command,
                     timeout=60,
                     mode=cli.CLIMode.UNDEF,
                     output_expected=None,
                     error_expected=False,
                     prompt=None):
        """Executes the given command.

        This method handles detecting simple boolean conditions such as
        the presence of output or errors.

        :param command:  command to execute, newline appended automatically
        :param timeout:  maximum time, in seconds, to wait for the command to
            finish. 0 to wait forever.
        :param mode:  mode to enter before running the command. The default
            is :func:`default_mode`.  To skip this step and execute directly
            in the cli's current mode, explicitly set this parameter to None.
        :param output_expected: If not None, indicates whether output is
            expected (True) or no output is expected (False).
            If the opposite occurs, raise UnexpectedOutput. Default is None.
        :type output_expected: bool or None
        :param error_expected: If true, cli error output (with a leading '%')
            is expected and will be returned as regular output instead of
            raising a CLIError.  Default is False, and error_expected always
            overrides output_expected.
        :type error_expected: bool
        :param prompt: Prompt regex for matching unusual prompts.  This should
            almost never be used as the ``mode`` parameter automatically
            handles all typical cases.  This parameter is for unusual
            situations like the install config wizard.

        :return: output of the command, minus the command itself.

        :raises CmdlineTimeout: on timeout
        :raises CLIError: if the output matches the cli's error format, and
            error output was not expected.
        :raises UnexpectedOutput: if output occurs when no output was
            expected, or no output occurs when output was expected
        """

        if not (output_expected is None or isinstance(output_expected, bool)):
            err_text = ("exec_command: output_expected requires a boolean "
                        "value or None. {cmd} called with output_expected "
                        "type:{type}, value:{val}")
            raise TypeError(
                err_text.format(cmd=command,
                                type=type(output_expected),
                                val=output_expected))

        if mode is cli.CLIMode.UNDEF:
            mode = self.default_mode
        if mode is not None:
            self.enter_mode(mode)

        self._log.debug('Executing cmd "%s"' % command)

        if prompt is None:
            prompt = self._prompt

        try:
            (output, match_res) = self._send_line_and_wait(command,
                                                           prompt,
                                                           timeout=timeout)
        except exceptions.ConnectionError as e:
            self._log.info("Connection channel in unexpected state. Flushing "
                           "and restarting. Error was {error}".format(error=e))
            self._cleanup_helper()
            self.start()
            (output, match_res) = self._send_line_and_wait(command,
                                                           prompt,
                                                           timeout=timeout)

        # CLI adds on escape chars and such sometimes and the result is that
        # some part of the command that was entered shows up as an extra
        # initial line of output.  Strip off that initial line.
        output = '\n'.join(output.splitlines()[1:])

        if output and (re.match(self.CLI_ERROR_PROMPT, output)):
            if error_expected:
                # Skip output_expected processing entirely.
                return output
            else:
                try:
                    mode = self.current_cli_mode()
                except exceptions.UnknownCLIMode:
                    mode = '<unrecognized>'
                raise exceptions.CLIError(command, output=output, mode=mode)

        if ((output_expected is not None)
                and (bool(output) != bool(output_expected))):
            raise exceptions.UnexpectedOutput(command=command,
                                              output=output,
                                              expected_output=output_expected)
        return output
Exemplo n.º 3
0
    def exec_command(self,
                     command,
                     timeout=60,
                     mode=cli.CLIMode.CONFIG,
                     output_expected=None,
                     error_expected=False,
                     interface=None,
                     prompt=None):
        """Executes the given command.

        This method handles detecting simple boolean conditions such as
        the presence of output or errors.

        :param command:  command to execute, newline appended automatically
        :param timeout:  maximum time, in seconds, to wait for the command to
            finish. 0 to wait forever.
        :param mode:  mode to enter before running the command.  To skip this
            step and execute directly in the cli's current mode, explicitly
            set this parameter to None.  The default is "configure"
        :param output_expected: If not None, indicates whether output is
            expected (True) or no output is expected (False).
            If the opposite occurs, raise UnexpectedOutput. Default is None.
        :type output_expected: bool or None
        :param error_expected: If true, cli error output (with a leading '%')
            is expected and will be returned as regular output instead of
            raising a CLIError.  Default is False, and error_expected always
            overrides output_expected.
        :type error_expected: bool
        :param interface: if mode 'subif', interface to configure 'gi0/1.666'
            or 'vlan 691'
        :type interface: string
        :param prompt: Prompt regex for matching unusual prompts.  This should
            almost never be used as the ``mode`` parameter automatically
            handles all typical cases.  This parameter is for unusual
            situations like the install config wizard.

        :raises CmdlineTimeout: on timeout
        :raises CLIError: if the output matches the cli's error format, and
            error output was not expected.
        :raises UnexpectedOutput: if output occurs when no output was
            expected, or no output occurs when output was expected

        :return: output of the command, minus the command itself.
        """

        if output_expected is not None and not isinstance(
                output_expected, bool):
            raise TypeError("exec_command: output_expected requires a boolean "
                            "value or None")

        if mode is not None:
            self.enter_mode(mode, interface)

        self._log.debug('Executing cmd "%s"' % command)

        if prompt is None:
            prompt = self.CLI_ANY_PROMPT
        (output, match_res) = self._send_line_and_wait(command,
                                                       prompt,
                                                       timeout=timeout)

        output = '\n'.join(output.splitlines()[1:])

        if output and (re.match(self.CLI_ERROR_PROMPT, output)):
            if error_expected:
                # Skip output_expected processing entirely.
                return output
            else:
                try:
                    mode = self.current_cli_mode()
                except exceptions.UnknownCLIMode:
                    mode = '<unrecognized>'
                raise exceptions.CLIError(command, output=output, mode=mode)

        if ((output_expected is not None)
                and (bool(output) != bool(output_expected))):
            raise exceptions.UnexpectedOutput(command=command,
                                              output=output,
                                              expected_output=output_expected)
        return output
Exemplo n.º 4
0
def cli_parse_table(input_string, headers):
    """
    Parser for Generic Table style output.  More complex tables outputs
    may require a custom parser.

    Parses output such as::

      Destination       Mask              Gateway           Interface
      10.3.0.0          255.255.248.0     0.0.0.0           aux
      default           0.0.0.0           10.3.0.1

    The left/right bounds of each data field are expected to fall underneath
    exactly 1 header.  If a data item falls under none or more than 1, an
    error will be raised.

    Data fields are initially divided by 2 spaces.  This allows single spaces
    within the data fields.  However, if the data crosses more than 1 header,
    it is then divided by single spaces and each piece will be part of whatever
    header it falls under.  If any part doesn't fall underneath a header, an
    error is raised.

    The example output above would produce the following structure:

    .. code-block:: python

        [
            {
                destination: 10.3.0.0,
                mask: 255.255.248.0,
                gateway: 0.0.0.0,
                interface: aux
            },
            {
                destination: default,
                mask: 0.0.0.0,
                gateway: 10.3.0.1
            },
        ]

    :param input_string: A string of CLI output to be parsed
    :param headers: array of headers in-order starting from the left.
    :return: an array of dictionaries of parsed output
    """
    parsed_output = []

    # Sanitize headers.  Lowercase and must all be unique
    headers = [x.lower() for x in headers]
    if len(headers) != len(set(headers)):
        raise ValueError("You cannot have duplicate headers!")

    lines = (x for x in input_string.splitlines() if re.search(r'\w', x))

    # Validate the header
    header_line = next(lines).lower()
    if not all(x in header_line for x in headers):
        raise exceptions.UnexpectedOutput(
            'Unknown',
            header_line,
            expected_output=True,
            notes=["not all headers found "
                   "in: '%s'" % header_line])

    # Get the header 'domains' (left,right indexes) for each header.
    last_index = 0
    domains = []
    for header in headers:
        header_left = header_line.index(header, last_index)
        header_right = header_left + len(header)
        domains.append((header_left, header_right))
        last_index = header_right

    Column = collections.namedtuple('Column', 'data, left, right')

    # Now we have the domain for each header.  We require that each data item
    # falls within exactly 1 header domain, else we can't know where it belongs
    for line in lines:
        # array of namedtuples with data, start index, and end index.
        # e.g (data="Column", left=0, right=5)
        columns = [
            Column(m.group(0), m.start(),
                   m.end() - 1) for m in re.finditer(r'(?:\S\s\S|\S)+', line)
        ]

        # Data items with 2 spaces are definitely different columns; however,
        # if we get a conflict, we split for every space and divide each item
        # into what domain it best matches.  We then append the items within
        # a domain into 1 item.  If some item crosses 2 domains, Error out.
        row = {}
        for column in columns:
            # Check if this crosses only 1 header domain.  left index would be
            # less than the right bounds of headerX and right index would be
            # greater than the left bounds of headerX+1 if crossing 2+ headers.
            (leftmost, rightmost) =\
                _find_left_right_headers(left_index=column.left,
                                         right_index=column.right,
                                         domains=domains)

            if leftmost < rightmost:
                # Splitting with double spaces spanned too many columns, split
                # into single-spaced strings and try again.
                extra_words = [
                    Column(m.group(0), m.start(),
                           m.end() - 1)
                    for m in re.finditer(r'\S+', column.data)
                ]
                for word in extra_words:
                    # We have to add column.left to get the position
                    # in the original string.
                    (word_leftmost, word_rightmost) =\
                        _find_left_right_headers(
                            left_index=word.left + column.left,
                            right_index=word.right + column.left,
                            domains=domains)

                    if word_leftmost < word_rightmost:
                        raise exceptions.UnexpectedOutput(
                            'Unknown',
                            '%s\n%s' % (header_line, line),
                            expected_output=True,
                            notes=[
                                "Data item '%s' crosses headers "
                                "'%s' and '%s'." %
                                (column.data, headers[leftmost],
                                 headers[rightmost]),
                                "Word '%s' crosses headers "
                                "'%s' and '%s'" %
                                (word.data, headers[word_leftmost],
                                 headers[word_rightmost])
                            ])
                    if word_leftmost > word_rightmost:
                        raise exceptions.UnexpectedOutput(
                            'Unknown',
                            '%s\n%s' % (header_line, line),
                            expected_output=True,
                            notes=[
                                "Data item '%s' crosses headers "
                                "'%s' and '%s'." %
                                (column.data, headers[leftmost],
                                 headers[rightmost]),
                                "Word '%s' does not fall under a header" %
                                word.data
                            ])

                    # This word falls under 1 header, we append all the
                    # words together that fall under 1 header.
                    key = headers[word_leftmost].lower()
                    if key in row:
                        row[key] += " " + word.data
                    else:
                        row[key] = word.data

            elif leftmost > rightmost:
                # This data item didn't fall within any header
                raise exceptions.UnexpectedOutput(
                    'Unknown',
                    '%s\n%s' % (header_line, line),
                    expected_output=True,
                    notes=[
                        "Data item '%s' does not fall under a header" %
                        column.data
                    ])
            else:
                # Now we know this data item crosses 1 header.
                key = headers[leftmost].lower()
                if key in row:
                    raise exceptions.UnexpectedOutput(
                        'Unknown',
                        '%s\n%s' % (header_line, line),
                        expected_output=True,
                        notes=[
                            "Multiple items under the same header: "
                            "'%s' and '%s'" % (row[key], column.data)
                        ])
                else:
                    row[key] = column.data

        parsed_output.append(row)
    return parsed_output
Exemplo n.º 5
0
    def exec_command(
            self,
            command,
            timeout=60,
            output_expected=None,
            error_expected=False,
            exit_info=None,
            retry_count=3,
            retry_delay=5,
            # Deprecated parameters. Remove for SteelScript.
            expect_output=None,
            expect_error=None):
        """Executes the given command statelessly.

        Since this is stateless, an exec_command cannot use environment
        variables/directory changes/whatever from a previous exec_command.

        This method handles detecting simple boolean conditions such as
        the presence of output or errors.

        :param command: command to send
        :param timeout: seconds to wait for command to finish. None to disable
        :param output_expected: If not None, indicates whether output is
            expected (True) or no output is expected (False).
            If the opposite occurs, raise UnexpectedOutput. Default is None.
        :type output_expected: bool or None
        :param error_expected: If true, a nonzero exit status will **not**
            trigger an exception as it normally would.
            Default is False, and error_expected always
            overrides output_expected.
        :type error_expected: bool
        :param exit_info: If set to a dict, the exit status is added to
            the dictionary under the key 'status'.  Primarily used in
            conjunction with ``error_expected`` when multiple nonzero statuses
            are possible.
        :type exit_info: dict or None
        :param retry_count: the number of tries to reconnect if underlying
            connection is disconnected. Default is 3
        :type retry_count: int
        :param retry_delay: delay in seconds between each retry to connect.
            Default is 5
        :type retry_delay: int

        :return: output from the command

        :raises ConnectionError: if the connection is lost
        :raises CmdlineTimeout: on timeout
        :raises ShellError: on an unexpected nonzero exit status
        :raises UnexpectedOutput: if output occurs when no output was
            expected, or no output occurs when output was expected
        """

        logging.debug('Executing command "%s"' % command)

        # connect if ssh is not connected
        if (not self.sshprocess.is_connected()):
            self.sshprocess.connect()

        output, exit_status = self._exec_paramiko_command(
            command,
            timeout=timeout,
            retry_count=retry_count,
            retry_delay=retry_delay)

        if isinstance(exit_info, dict):
            exit_info['status'] = exit_status

        # If the command failed and the user wants an exception, do it!
        if exit_status != 0 and not error_expected:
            raise exceptions.ShellError(command=command,
                                        output=output,
                                        exit_status=exit_status)
        if ((output_expected is not None)
                and (bool(output) != bool(output_expected))):
            raise exceptions.UnexpectedOutput(command=command,
                                              output=output,
                                              expected_output=output_expected)
        return output
Exemplo n.º 6
0
    def exec_command(self, command, timeout=60, mode=cli.CLIMode.CONFIG,
                     force=False, output_expected=None, prompt=None):
        """
        Executes the given command.

        This method handles detecting simple boolean conditions such as
        the presence of output or errors.

        :param command:  command to execute, newline appended automatically
        :param timeout:  maximum time, in seconds, to wait for the command to
            finish. 0 to wait forever.
        :param mode:  mode to enter before running the command.  To skip this
            step and execute directly in the cli's current mode, explicitly
            set this parameter to None.  The default is "configure"
        :param force: Will force enter mode, discarding all changes
                     that haven't been committed.
        :type force: Boolean
        :param output_expected: If not None, indicates whether output is
            expected (True) or no output is expected (False).
            If the opposite occurs, raise UnexpectedOutput. Default is None.
        :type output_expected: bool or None
        :param prompt: Prompt regex for matching unusual prompts.  This should
            almost never be used as the ``mode`` parameter automatically
            handles all typical cases.  This parameter is for unusual
            situations like the install config wizard.

        :return: output of the command, minus the command itself.

        :raises TypeError: if output_expected type is incorrect
        :raises CmdlineTimeout: on timeout
        :raises UnexpectedOutput: if output occurs when no output was
            expected, or no output occurs when output was expected
        """
        if output_expected is not None and not isinstance(
                output_expected, bool):
            raise TypeError("exec_command: output_expected requires a boolean "
                            "value or None")
        if mode is not None:
            self.enter_mode(mode, force)

        self._log.debug('Executing cmd "%s"' % command)

        if prompt is None:
            prompt = self.CLI_ANY_PROMPT
        (output, match_res) = self._send_line_and_wait(command,
                                                       prompt,
                                                       timeout=timeout)
        output = output.splitlines()[1:]

        # Vyatta does not have a standard error prompt
        # In config mode, each command (erroneous or not) is
        # followed with '[edit]'. This skews the result for
        # output_expected flag

        # To address this remove line with '[edit]' when in config mode

        if mode == cli.CLIMode.CONFIG:
            output = [line for line in output if self.DISCARD_PROMPT != line]

        output = '\n'.join(output)

        if ((output_expected is not None) and (bool(output) !=
                                               bool(output_expected))):
            raise exceptions.UnexpectedOutput(command=command,
                                              output=output,
                                              expected_output=output_expected)
        return output