diff --git a/.github/workflows/py2-py3-packages-ci.yml b/.github/workflows/py2-py3-packages-ci.yml index fb403cf..8ae3323 100644 --- a/.github/workflows/py2-py3-packages-ci.yml +++ b/.github/workflows/py2-py3-packages-ci.yml @@ -32,7 +32,14 @@ jobs: run: | python_version="${{ matrix.python-version }}" py_version="${python_version/./}" - branch=(`[[ 'refs/head/master' == ${{ github.ref }} ]] && echo 'master' || echo 'dev'`) + target_branch=${{ github.base_ref || github.ref }} + target_branch=(`[[ ${target_branch::10} == 'refs/heads' ]] && echo ${target_branch:11} || echo $target_branch`) + echo "target_branch =" $target_branch + is_master=(`[[ $target_branch == 'master' ]] && echo 'true' || echo 'false'`) + is_tag=${{ startsWith(github.ref, 'refs/tags') }} + echo "is_master =" $is_master + echo "is_tag =" $is_tag + branch=(`[[ $is_master == 'true' || $is_tag == 'true' ]] && echo 'master' || echo 'dev'`) TOXENV="py$py_version-$branch" echo $TOXENV echo "TOXENV=$TOXENV" >> $GITHUB_ENV diff --git a/cloudshell/cli/command_template/command_template.py b/cloudshell/cli/command_template/command_template.py index 6900b0a..a6a6b17 100644 --- a/cloudshell/cli/command_template/command_template.py +++ b/cloudshell/cli/command_template/command_template.py @@ -1,5 +1,8 @@ -import re from collections import OrderedDict +import re + +from cloudshell.cli.service.action_map import ActionMap +from cloudshell.cli.service.error_map import ErrorMap class CommandTemplate: @@ -8,19 +11,18 @@ def __init__(self, command, action_map=None, error_map=None): :type command: str :type action_map: dict - :param error_map: expected error map with subclass of CommandExecutionException - or str - :type error_map: dict[str, cloudshell.cli.session.session_exceptions.CommandExecutionException|str] # noqa: E501 + :param error_map: expected error map with subclass of CommandExecutionException or str + :type error_map: dict[str, cloudshell.cli.session.session_exceptions.CommandExecutionException|str] """ self._command = command - self._action_map = action_map or OrderedDict() - self._error_map = error_map or OrderedDict() + self._action_map = action_map or ActionMap() + self._error_map = error_map or ErrorMap() @property def action_map(self): """Property for action map. - :rtype: OrderedDict() + :rtype: cloudshell.cli.service.action_map.ActionMap """ return self._action_map @@ -28,20 +30,23 @@ def action_map(self): def error_map(self): """Property for error map. - :rtype: OrderedDict + :rtype: cloudshell.cli.service.error_map.ErrorMap """ return self._error_map # ToDo: Needs to be reviewed def get_command(self, **kwargs): - action_map = OrderedDict(kwargs.get("action_map", None) or OrderedDict()) - action_map.update(self._action_map) - error_map = OrderedDict(self._error_map) - error_map.update(kwargs.get("error_map", None) or OrderedDict()) + # todo: verify action map creation + action_map = kwargs.get('action_map') or ActionMap() + action_map.extend(self.action_map) + + error_map = kwargs.get("error_map") or ErrorMap() + error_map.extend(self.error_map) + return { - "command": self.prepare_command(**kwargs), - "action_map": action_map, - "error_map": error_map, + 'command': self.prepare_command(**kwargs), + 'action_map': action_map, + 'error_map': error_map } def prepare_command(self, **kwargs): @@ -52,7 +57,7 @@ def prepare_command(self, **kwargs): cmd = re.sub(r"\[[^[]*?{{{key}}}.*?\]".format(key=key), r"", cmd) if not cmd: - raise Exception(self.__class__.__name__, "Unable to prepare command") + raise Exception("Unable to prepare command") cmd = re.sub(r"\s+", " ", cmd).strip(" \t\n\r") result = re.sub(r"\[|\]", "", cmd).format(**kwargs) diff --git a/cloudshell/cli/command_template/command_template_executor.py b/cloudshell/cli/command_template/command_template_executor.py index 049129a..0397729 100644 --- a/cloudshell/cli/command_template/command_template_executor.py +++ b/cloudshell/cli/command_template/command_template_executor.py @@ -1,4 +1,5 @@ -from collections import OrderedDict +from cloudshell.cli.service.action_map import ActionMap +from cloudshell.cli.service.error_map import ErrorMap class CommandTemplateExecutor(object): @@ -14,57 +15,32 @@ def __init__( ): """Initialize Command template executor. - :param cli_service: - :type cli_service: CliService - :param command_template: - :type command_template: cloudshell.cli.command_template.command_template.CommandTemplate # noqa: E501 - :param error_map: expected error map with subclass of CommandExecutionException - or str - :type error_map: dict[str, cloudshell.cli.session.session_exceptions.CommandExecutionException|str] # noqa: E501 + :param cloudshell.cli.service.cli_service.CliService cli_service: + :param cloudshell.cli.command_template.command_template.CommandTemplate command_template: + :param cloudshell.cli.service.action_map.ActionMap action_map: + :param cloudshell.cli.service.error_map.ErrorMap error_map: + :return: """ self._cli_service = cli_service self._command_template = command_template - self._action_map = action_map or OrderedDict() - self._error_map = error_map or OrderedDict() - self._optional_kwargs = optional_kwargs - @property - def action_map(self): - """Return updated action.""" - action_map = self._action_map.copy() - action_map.update(self._command_template.action_map) - return action_map + self._action_map = action_map or ActionMap() + self._action_map.extend(command_template.action_map) - @property - def error_map(self): - error_map = self._error_map.copy() - error_map.update(self._command_template.error_map) - return error_map + self._error_map = error_map or ErrorMap() + self._error_map.extend(command_template.error_map) - @property - def optional_kwargs(self): - return self._optional_kwargs + self._optional_kwargs = optional_kwargs def execute_command(self, **command_kwargs): """Execute command. - :param command_kwargs: + :param dict command_kwargs: :return: Command output :rtype: str """ command = self._command_template.prepare_command(**command_kwargs) - return self._cli_service.send_command( - command, - action_map=self.action_map, - error_map=self.error_map, - **self.optional_kwargs - ) - - def update_action_map(self, action_map): - self._action_map.update(action_map) - - def update_error_map(self, error_map): - self._error_map.update(error_map) - - def update_optional_kwargs(self, **optional_kwargs): - self.optional_kwargs.update(optional_kwargs) + return self._cli_service.send_command(command, + action_map=self._action_map, + error_map=self._error_map, + **self._optional_kwargs) diff --git a/cloudshell/cli/configurator.py b/cloudshell/cli/configurator.py index d3d7ed5..277d562 100644 --- a/cloudshell/cli/configurator.py +++ b/cloudshell/cli/configurator.py @@ -2,7 +2,13 @@ # -*- coding: utf-8 -*- import sys from abc import ABCMeta, abstractmethod +from collections import defaultdict +from cloudshell.cli.factory.session_factory import ( + CloudInfoAccessKeySessionFactory, + GenericSessionFactory, + SessionFactory, +) from cloudshell.cli.service.cli import CLI from cloudshell.cli.session.ssh_session import SSHSession from cloudshell.cli.session.telnet_session import TelnetSession @@ -16,39 +22,30 @@ class CLIServiceConfigurator(object): - REGISTERED_SESSIONS = (SSHSession, TelnetSession) - - def __init__(self, resource_config, logger, cli=None, registered_sessions=None): + REGISTERED_SESSIONS = (CloudInfoAccessKeySessionFactory(SSHSession), TelnetSession) + """Using factories instead of """ + + def __init__( + self, + resource_config, + logger, + cli=None, + registered_sessions=None, + reservation_context=None, + ): """Initialize CLI service configurator. :param cloudshell.shell.standards.resource_config_generic_models.GenericCLIConfig resource_config: # noqa: E501 :param logging.Logger logger: :param cloudshell.cli.service.cli.CLI cli: :param registered_sessions: Session types and order + :param cloudshell.shell.core.driver_context.ReservationContextDetails reservation_context: """ self._cli = cli or CLI() self._resource_config = resource_config self._logger = logger self._registered_sessions = registered_sessions or self.REGISTERED_SESSIONS - - @property - def _username(self): - return self._resource_config.user - - @property - @lru_cache() - def _password(self): - return self._resource_config.password - - @property - def _resource_address(self): - """Resource IP.""" - return self._resource_config.address - - @property - def _port(self): - """Connection port property, to open socket on.""" - return self._resource_config.cli_tcp_port + self._reservation_context = reservation_context @property def _cli_type(self): @@ -58,31 +55,23 @@ def _cli_type(self): @property @lru_cache() def _session_dict(self): - return {sess.SESSION_TYPE.lower(): [sess] for sess in self._registered_sessions} - - def _on_session_start(self, session, logger): - """Perform some default commands when session just opened. - - Like 'no logging console' - """ - pass - - @property - @lru_cache() - def _session_kwargs(self): - return { - "host": self._resource_address, - "username": self._username, - "password": self._password, - "port": self._port, - "on_session_start": self._on_session_start, - } + session_dict = defaultdict(list) + for sess in self._registered_sessions: + session_dict[sess.SESSION_TYPE.lower()].append(sess) + return session_dict + + def initialize_session(self, session): + if not isinstance(session, SessionFactory): + session = GenericSessionFactory(session) + return session.init_session( + self._resource_config, self._logger, self._reservation_context + ) def _defined_sessions(self): return [ - sess(**self._session_kwargs) + self.initialize_session(sess) for sess in self._session_dict.get( - self._cli_type.lower(), self._registered_sessions + self._cli_type.lower(), sum(self._session_dict.values(), []) ) ] diff --git a/cloudshell/cli/factory/__init__.py b/cloudshell/cli/factory/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cloudshell/cli/factory/session_factory.py b/cloudshell/cli/factory/session_factory.py new file mode 100644 index 0000000..c3cb528 --- /dev/null +++ b/cloudshell/cli/factory/session_factory.py @@ -0,0 +1,69 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from abc import ABCMeta, abstractmethod + +ABC = ABCMeta("ABC", (object,), {"__slots__": ()}) + + +class SessionFactory(ABC): + """Session factory. + + Help to initialize session for specified session class. + """ + + def __init__(self, session_class: type): + """:param session_class: Session class.""" + self.session_class = session_class + + @abstractmethod + def init_session(self, resource_config, logger, reservation_context=None): + """Initialize session instance. + + Encapsulate the logic of the session instance creation. + :param resource_config: + :param logging.Logger logger: + :param ReservationContextDetails reservation_context: + """ + raise NotImplementedError + + +class GenericSessionFactory(SessionFactory): + def init_session(self, resource_config, logger, reservation_context=None): + return self.session_class( + **self._session_kwargs(resource_config, logger, reservation_context) + ) + + @property + def SESSION_TYPE(self): + return self.session_class.SESSION_TYPE + + def _on_session_start(self, session, logger): + """Perform some default commands when session just opened. + + Like 'no logging console' + """ + pass + + def _session_kwargs(self, resource_config, logger, reservation_context=None): + return { + "host": resource_config.address, + "username": resource_config.user, + "password": resource_config.password, + "port": resource_config.cli_tcp_port, + "on_session_start": self._on_session_start, + } + + +class CloudInfoAccessKeySessionFactory(GenericSessionFactory): + def _session_kwargs(self, resource_config, logger, reservation_context=None): + access_key = "" + if reservation_context and reservation_context.cloud_info_access_key: + access_key = reservation_context.cloud_info_access_key + return { + "host": resource_config.address, + "username": resource_config.user, + "password": resource_config.password, + "port": resource_config.cli_tcp_port, + "pkey": access_key, + "on_session_start": self._on_session_start, + } diff --git a/cloudshell/cli/service/action_map.py b/cloudshell/cli/service/action_map.py new file mode 100644 index 0000000..114e3e4 --- /dev/null +++ b/cloudshell/cli/service/action_map.py @@ -0,0 +1,194 @@ +from collections import OrderedDict +import re + +from cloudshell.cli.session.session_exceptions import SessionLoopDetectorException + + +class Action: + def __init__(self, pattern, callback, execute_once=False): + """ + + :param str pattern: + :param function callback: + :param bool execute_once: + """ + self.pattern = pattern + self.compiled_pattern = re.compile(pattern=pattern, flags=re.DOTALL) + self.callback = callback + self.execute_once = execute_once + + def __call__(self, session, logger): + """ + + :param cloudshell.cli.session.expect_session.ExpectSession session: + :param logging.Logger logger: + :return: + """ + return self.callback(session, logger) + + def __repr__(self): + """ + + :rtype: str + """ + return f"{super().__repr__()} pattern: {self.pattern}, execute once: {self.execute_once}" + + def match(self, output): + """ + + :param str output: + :rtype: bool + """ + return bool(self.compiled_pattern.search(output)) + + +class ActionMap: + def __init__(self, actions=None): + """ + + :param list[Action] actions: + """ + if actions is None: + actions = [] + + self.matched_patterns = set() + self._actions_dict = OrderedDict([(action.pattern, action) for action in actions]) + + @property + def actions(self): + """ + + :rtype: list[Action] + """ + return list(self._actions_dict.values()) + + @property + def active_actions(self): + """ + + :rtype: list[Action] + """ + return [action for action in self.actions if (not action.execute_once or + action.pattern not in self.matched_patterns)] + + def add(self, action): + """ + + :param Action action: + :return: + """ + self._actions_dict[action.pattern] = action + + def extend(self, action_map, override=False, extend_matched_patterns=True): + """ + + :param ActionMap action_map: + :param bool override: + :param bool extend_matched_patterns: + :return: + """ + for action in action_map.actions: + if not override and action.pattern in self._actions_dict: + continue + self.add(action) + + if extend_matched_patterns: + self.matched_patterns |= action_map.matched_patterns + + def process(self, session, logger, output, check_action_loop_detector, action_loop_detector): + """ + + :param cloudshell.cli.session.expect_session.ExpectSession session: + :param logging.Logger logger: + :param str output: + :param bool check_action_loop_detector: + :param ActionLoopDetector action_loop_detector: + :rtype: bool + """ + for action in self.active_actions: + if action.match(output): + logger.debug(f"Matched Action with pattern: {action.pattern}") + + if check_action_loop_detector: + logger.debug(f"Checking loops for Action with pattern : {action.pattern}") + + if action_loop_detector.loops_detected(action.pattern): + logger.error(f"Loops detected for action patter: {action.pattern}") + raise SessionLoopDetectorException("Expected actions loops detected") + + action(session, logger) + self.matched_patterns.add(action.pattern) + return True + + return False + + def __add__(self, other): + """ + + :param other: + :rtype: ActionMap + """ + action_map_class = type(self) + if isinstance(other, action_map_class): + action_map = action_map_class(actions=self.actions) + action_map.extend(other, extend_matched_patterns=False) + return action_map + + raise TypeError(f"unsupported operand type(s) for +: '{type(self)}' and '{type(other)}'") + + def __repr__(self): + """ + + :rtype: str + """ + return f"{super().__repr__()} matched patterns: {self.matched_patterns}, actions: {self.actions}" + + +class ActionLoopDetector: + """Help to detect loops for action combinations""" + + def __init__(self, max_loops, max_combination_length): + """ + + :param max_loops: + :param max_combination_length: + :return: + """ + self._max_action_loops = max_loops + self._max_combination_length = max_combination_length + self._action_history = [] + + def loops_detected(self, action_pattern): + """Add action key to the history and detect loops + + :param str action_pattern: + :return: + """ + self._action_history.append(action_pattern) + for combination_length in range(1, self._max_combination_length + 1): + if self._is_combination_compatible(combination_length): + if self._is_loop_exists(combination_length): + return True + return False + + def _is_combination_compatible(self, combination_length): + """Check if combinations may exist + + :param combination_length: + :return: + """ + return len(self._action_history) / combination_length >= self._max_action_loops + + def _is_loop_exists(self, combination_length): + """Detect loops for combination length + + :param combination_length: + :return: + """ + reversed_history = self._action_history[::-1] + combinations = [reversed_history[x:x + combination_length] for x in + range(0, len(reversed_history), combination_length)][:self._max_action_loops] + for x, y in [combinations[x:x + 2] for x in range(0, len(combinations) - 1)]: + if x != y: + return False + return True diff --git a/cloudshell/cli/service/cli_service_helpers.py b/cloudshell/cli/service/cli_service_helpers.py new file mode 100644 index 0000000..80aaa0a --- /dev/null +++ b/cloudshell/cli/service/cli_service_helpers.py @@ -0,0 +1,73 @@ +class SendCommandWithRetries(object): + """ + Help to execute command with retries. + + If command execution raise an exception it helps + to reconnect or create a new session from the list of + defined sessions, optional. + """ + + MAX_RECREATE_RETRIES = 3 + MAX_RECONNECT_RETRIES = 0 + RECONNECT_TIMEOUT = 30 + + def __init__( + self, + cli_configurator, + command_mode, + logger, + recreate_retries=MAX_RECREATE_RETRIES, + reconnect_retries=MAX_RECONNECT_RETRIES, + reconnect_timeout=RECONNECT_TIMEOUT, + ): + """ + Init method. + + :param cloudshell.cli.configurator.CLIServiceConfigurator cli_configurator: + :param cloudshell.cli.service.command_mode.CommandMode command_mode: + :param logging.Logger logger: + :param int recreate_retries: + :param int reconnect_retries: + :param int reconnect_timeout: + """ + self.cli_configurator = cli_configurator + self.command_mode = command_mode + self._logger = logger + self._recreate_retries = recreate_retries + self._reconnect_retries = reconnect_retries + self._reconnect_timeout = reconnect_timeout + + def _send_command_with_reconnect(self, cli_service, *args, **kwargs): + """Send command with reconnect retries. + + :param cloudshell.cli.service.cli_service.CliService cli_service: + :param args: + :param kwargs: + """ + retry = 0 + while True: + try: + return cli_service.send_command(*args, **kwargs) + except Exception: + self._logger.exception("Reconnect retry {}".format(retry)) + if retry < self._reconnect_retries: + cli_service.reconnect(self._reconnect_timeout) + retry += 1 + else: + raise + + def send_command(self, *args, **kwargs): + """Send command with retries on fail.""" + retry = 0 + while retry < self._recreate_retries: + try: + with self.cli_configurator.get_cli_service( + self.command_mode + ) as cli_service: + return self._send_command_with_reconnect( + cli_service, *args, **kwargs + ) + except Exception: + self._logger.exception("Recreate retry {}".format(retry)) + retry += 1 + raise Exception("Max retries exceeded") diff --git a/cloudshell/cli/service/cli_service_impl.py b/cloudshell/cli/service/cli_service_impl.py index 806d11f..0f39ffb 100644 --- a/cloudshell/cli/service/cli_service_impl.py +++ b/cloudshell/cli/service/cli_service_impl.py @@ -116,10 +116,8 @@ def send_command( :param command: :param expected_string: - :param action_map: - :param error_map: expected error map with subclass of CommandExecutionException - or str - :type error_map: dict[str, cloudshell.cli.session.session_exceptions.CommandExecutionException|str] # noqa: E501 + :param cloudshell.cli.service.action_map.ActionMap action_map: + :param cloudshell.cli.service.error_map.ErrorMap error_map: :param logger: :param remove_prompt: :param args: @@ -133,15 +131,25 @@ def send_command( if not logger: logger = self._logger self.session.logger = logger - output = self.session.hardware_expect( - command, - expected_string=expected_string, - action_map=action_map, - error_map=error_map, - logger=logger, - *args, - **kwargs - ) + + # option 1: + # one HardwareExpect instance per command + # output = HardwareExpect(session=session).hardware_expect(command=command) + + # option 2: + # HardwareExpect service without state that operates command ans session objects + # self.hardware_expect(command=command) + + # option 3: test + # output = CommandRunner(session=self.session).hardware_expect(command=command) + + # option 4: test + # output = CommandRunner(session=self.session, command=command, expected_string=expected_string, + # action_map=action_map, error_map=error_map, + # logger=logger, *args, **kwargs).hardware_expect() + + # output = self.session.hardware_expect(command, expected_string=expected_string, action_map=action_map, + # error_map=error_map, logger=logger, *args, **kwargs) if remove_prompt: output = re.sub( r"^.*{}.*$".format(expected_string), "", output, flags=re.MULTILINE diff --git a/cloudshell/cli/service/command_mode.py b/cloudshell/cli/service/command_mode.py index 5ad4ee1..7d9e6e8 100755 --- a/cloudshell/cli/service/command_mode.py +++ b/cloudshell/cli/service/command_mode.py @@ -1,5 +1,7 @@ import re +from cloudshell.cli.service.action_map import ActionMap +from cloudshell.cli.service.error_map import ErrorMap from cloudshell.cli.service.cli_exception import CliException from cloudshell.cli.service.node import Node @@ -28,34 +30,24 @@ def __init__( ): """Initialize Command Mode. - :param prompt: Prompt of this mode - :type prompt: str - :param enter_command: Command used to enter this mode - :type enter_command: str - :param exit_command: Command used to exit from this mode - :type exit_command: str + :param str prompt: Prompt of this mode + :param str enter_command: Command used to enter this mode + :param str exit_command: Command used to exit from this mode :param enter_actions: Actions which needs to be done when entering this mode - :param enter_action_map: Enter expected actions - :type enter_action_map: dict - :param enter_error_map: expected error map with subclass of - CommandExecutionException or str - :type enter_error_map: dict[str, cloudshell.cli.session.session_exceptions.CommandExecutionException|str] # noqa: E501 - :param exit_action_map: - :type exit_action_map: dict - :param exit_error_map: expected error map with subclass of - CommandExecutionException or str - :type exit_error_map: dict[str, cloudshell.cli.session.session_exceptions.CommandExecutionException|str] # noqa: E501 - :param + :param cloudshell.cli.service.action_map.ActionMap enter_action_map: Enter expected actions + :param cloudshell.cli.service.error_map.ErrorMap enter_error_map: + :param cloudshell.cli.service.action_map.ActionMap exit_action_map: + :param cloudshell.cli.service.error_map.ErrorMap exit_error_map: :param parent_mode: Connect parent mode """ if not exit_error_map: - exit_error_map = {} + exit_error_map = ErrorMap() if not enter_error_map: - enter_error_map = {} + enter_error_map = ErrorMap() if not exit_action_map: - exit_action_map = {} + exit_action_map = ActionMap() if not enter_action_map: - enter_action_map = {} + enter_action_map = ActionMap() super(CommandMode, self).__init__() self._prompt = prompt diff --git a/cloudshell/cli/service/error_map.py b/cloudshell/cli/service/error_map.py new file mode 100644 index 0000000..5dc82c0 --- /dev/null +++ b/cloudshell/cli/service/error_map.py @@ -0,0 +1,116 @@ +from collections import OrderedDict +import re + +from cloudshell.cli.session.session_exceptions import CommandExecutionException + + +class Error: + def __init__(self, pattern, error): + """ + + :param str pattern: + :param str|CommandExecutionException error: + """ + self.pattern = pattern + self.compiled_pattern = re.compile(pattern=pattern, flags=re.DOTALL) + self.error = error + + def __call__(self, output): + """ + + :param str output: + :raises: CommandExecutionException + """ + if isinstance(self.error, CommandExecutionException): + raise self.error + + raise CommandExecutionException(f"Session returned '{self.error}'") + + def __repr__(self): + """ + + :rtype: str + """ + return f"{super().__repr__()} pattern: {self.pattern}, error: {self.error}" + + def match(self, output): + """ + + :param str output: + :rtype: bool + """ + return bool(self.compiled_pattern.search(output)) + + +class ErrorMap: + def __init__(self, errors=None): + """ + + :param list[Error] errors: + """ + if errors is None: + errors = [] + + self._errors_dict = OrderedDict([(error.pattern, error) for error in errors]) + + @property + def errors(self): + """ + + :rtype: list[Error] + """ + return list(self._errors_dict.values()) + + def add(self, error): + """ + + :param Error error: + :return: + """ + self._errors_dict[error.pattern] = error + + def extend(self, error_map, override=False): + """ + + :param ErrorMap error_map: + :param bool override: + :return: + """ + for error in error_map.errors: + if not override and error.pattern in self._errors_dict: + continue + self.add(error) + + def process(self, output, logger): + """ + + :param str output: + :param logging.Logger logger: + :rtype: bool + """ + + for error in self.errors: + if error.match(output): + logger.debug(f"Matched Error with pattern: {error.pattern}") + error(output) + + def __add__(self, other): + """ + + :param other: + :rtype: ActionMap + """ + error_map_class = type(self) + if isinstance(other, error_map_class): + error_map = error_map_class(errors=self.errors) + error_map.extend(other) + return error_map + + raise TypeError(f"unsupported operand type(s) for +: '{type(self)}' and '{type(other)}'") + + def __repr__(self): + """ + + :rtype: str + """ + return f"{super().__repr__()} errors: {self.errors}" diff --git a/cloudshell/cli/service/hardware_expect.py b/cloudshell/cli/service/hardware_expect.py new file mode 100644 index 0000000..0f105a5 --- /dev/null +++ b/cloudshell/cli/service/hardware_expect.py @@ -0,0 +1,140 @@ +import re +import time + +from cloudshell.cli.service.action_map import ActionMap +from cloudshell.cli.service.error_map import ErrorMap +from cloudshell.cli.service.action_map import ActionLoopDetector +from cloudshell.cli.session.helper.normalize_buffer import normalize_buffer +from cloudshell.cli.session.session_exceptions import SessionLoopLimitException, ExpectedSessionException, \ + CommandExecutionException, SessionReadTimeout, SessionReadEmptyData + +# option 1: save state in Command object +# option 2: save state ib this class + + +class CommandRunner: + def __init__(self, session): + self._session = session + + def _send_command(self, command): + self.session._clear_buffer(self._clear_buffer_timeout, logger) + logger.debug(f'Command: {command}') + self.send_line(command, logger) + + def _remove_command_from_output(self, command, output): + """If option remove_command_from_output is set to True, look for command in output buffer, + + remove it in case of found + :param command: + :return: + """ + command_pattern = self._generate_command_pattern(command.command) + if re.search(command_pattern, output, flags=re.MULTILINE): + output = re.sub(command_pattern, '', output, count=1, flags=re.MULTILINE) + command.remove_command_from_output = False + + return output + + def _wait_for_response(self): + pass + + def hardware_expect(self, command, logger): + """Get response form the device and compare it to action_map, error_map and expected_string patterns, + + perform actions specified in action_map if any, and return output. + Raise Exception if receive empty response from device within a minute + """ + if command.command: + self._send_command(command.command) + + # Loop until one of the expressions is matched or MAX_RETRIES + # nothing is expected (usually used for exit) + output_list = list() + output_str = '' + retries_count = 0 + is_correct_exit = False + + while command.retries == 0 or retries_count < command.retries: + read_buffer = self._session._receive_all(command.timeout, logger) + + if read_buffer: + read_buffer = normalize_buffer(read_buffer) + logger.debug(read_buffer) + output_str += read_buffer + + if command.command and command.remove_command_from_output: + output_str = self._remove_command_from_output(output_str, command) + + retries_count = 0 + else: + retries_count += 1 + time.sleep(command.empty_loop_timeout) + continue + + if self.session.match_prompt(command.expected_string, output_str, logger): + # logger.debug('Expected str: {}'.format(expected_string)) + output_list.append(output_str) + is_correct_exit = True + + action_matched = command.action_map.process(session=self, + logger=logger, + output=output_str, + check_action_loop_detector=command.check_action_loop_detector, + action_loop_detector=command.action_loop_detector) + + if action_matched: + output_list.append(output_str) + output_str = '' + + if is_correct_exit: + break + + if not is_correct_exit: + raise SessionLoopLimitException(f'Session Loop limit exceeded, {retries_count} loops') + + result_output = ''.join(output_list) + command.error_map.process(output=result_output, logger=logger) + + # Read buffer to the end. Useful when expected_string isn't last in buffer + result_output += self._clear_buffer(self._clear_buffer_timeout, logger) + return result_output + + +class Command: + MAX_LOOP_RETRIES = 20 + READ_TIMEOUT = 30 + EMPTY_LOOP_TIMEOUT = 0.5 + CLEAR_BUFFER_TIMEOUT = 0.1 + LOOP_DETECTOR_MAX_ACTION_LOOPS = 3 + LOOP_DETECTOR_MAX_COMBINATION_LENGTH = 4 + RECONNECT_TIMEOUT = 30 + + def __init__(self, command, expected_string, action_map=None, error_map=None, + timeout=None, retries=MAX_LOOP_RETRIES, check_action_loop_detector=True, empty_loop_timeout=None, + remove_command_from_output=True): + """ + + :param str command: command to send + :param str expected_string: expected string + :param collections.OrderedDict action_map: dict with {re_str: action} to trigger some action on received string + :param error_map: expected error map with subclass of CommandExecutionException or str + :type error_map: dict[str, CommandExecutionException|str] + :param int timeout: session timeout + :param int retries: maximal retries count + :param bool check_action_loop_detector: + :param bool empty_loop_timeout: + :param bool remove_command_from_output: In some switches the output string includes the command which was called. + The flag used to verify whether the the command string removed from the output string. + :rtype: str + """ + self.command = command + self.expected_string = expected_string + self.action_map = action_map or ActionMap() + self.error_map = error_map or ErrorMap() + + if check_action_loop_detector: + self.action_loop_detector = ActionLoopDetector( + max_loops=self._loop_detector_max_action_loops, + max_combination_length=self._loop_detector_max_combination_length) + else: + self.action_loop_detector = None diff --git a/cloudshell/cli/service/session_pool.py b/cloudshell/cli/service/session_pool.py index 85f0ada..31c2db8 100644 --- a/cloudshell/cli/service/session_pool.py +++ b/cloudshell/cli/service/session_pool.py @@ -6,9 +6,9 @@ class SessionPool(ABC): @abstractmethod def get_session(self, new_sessions, prompt, logger): - """Get session from pool. - - :rtype: cloudshell.cli.session.session.Session + """ + Get session from pool + :rtype Session """ pass diff --git a/cloudshell/cli/session/expect_session.py b/cloudshell/cli/session/expect_session.py index 446cc04..e0c64b4 100644 --- a/cloudshell/cli/session/expect_session.py +++ b/cloudshell/cli/session/expect_session.py @@ -1,18 +1,14 @@ import re import time from abc import ABCMeta, abstractmethod -from collections import OrderedDict +from cloudshell.cli.service.action_map import ActionMap +from cloudshell.cli.service.error_map import ErrorMap +from cloudshell.cli.service.action_map import ActionLoopDetector from cloudshell.cli.session.helper.normalize_buffer import normalize_buffer from cloudshell.cli.session.session import Session -from cloudshell.cli.session.session_exceptions import ( - CommandExecutionException, - ExpectedSessionException, - SessionLoopDetectorException, - SessionLoopLimitException, - SessionReadEmptyData, - SessionReadTimeout, -) +from cloudshell.cli.session.session_exceptions import SessionLoopLimitException, \ + ExpectedSessionException, CommandExecutionException, SessionReadTimeout, SessionReadEmptyData ABC = ABCMeta("ABC", (object,), {"__slots__": ()}) @@ -217,24 +213,21 @@ def hardware_expect( :param command: command to send :param expected_string: expected string :param logger: logger - :param action_map: dict with {re_str: action} to trigger some action - on received string - :param error_map: expected error map with subclass of CommandExecutionException - or str - :type error_map: dict[str, CommandExecutionException|str] + :param action_map: ActionMap + :param error_map: expected error map with subclass of CommandExecutionException or str + :type error_map: ErrorMap :param timeout: session timeout :param retries: maximal retries count - :param remove_command_from_output: In some switches the output string includes - the command which was called. The flag used to verify whether the the - command string removed from the output string. + :param remove_command_from_output: In some switches the output string includes the command which was called. + The flag used to verify whether the the command string removed from the output string. :return: :rtype: str """ if not action_map: - action_map = OrderedDict() + action_map = ActionMap() if not error_map: - error_map = OrderedDict() + error_map = ErrorMap() retries = retries or self._max_loop_retries empty_loop_timeout = empty_loop_timeout or self._empty_loop_timeout @@ -288,22 +281,15 @@ def hardware_expect( output_list.append(output_str) is_correct_exit = True - for action_key in action_map: - result_match = re.search(action_key, output_str, re.DOTALL) - if result_match: - output_list.append(output_str) - - if check_action_loop_detector: - if action_loop_detector.loops_detected(action_key): - logger.error("Loops detected") - raise SessionLoopDetectorException( - self.__class__.__name__, - "Expected actions loops detected", - ) - logger.debug("Action key: {}".format(action_key)) - action_map[action_key](self, logger) - output_str = "" - break + action_matched = action_map.process(session=self, + logger=logger, + output=output_str, + check_action_loop_detector=check_action_loop_detector, + action_loop_detector=action_loop_detector) + + if action_matched: + output_list.append(output_str) + output_str = '' if is_correct_exit: break @@ -314,18 +300,8 @@ def hardware_expect( "Session Loop limit exceeded, {} loops".format(retries_count), ) - result_output = "".join(output_list) - - for error_pattern, error in error_map.items(): - result_match = re.search(error_pattern, result_output, re.DOTALL) - - if result_match: - if isinstance(error, CommandExecutionException): - raise error - else: - raise CommandExecutionException( - "Session returned '{}'".format(error) - ) + result_output = ''.join(output_list) + error_map.process(output=result_output, logger=logger) # Read buffer to the end. Useful when expected_string isn't last in buffer result_output += self._clear_buffer(self._clear_buffer_timeout, logger) @@ -353,64 +329,3 @@ def reconnect(self, prompt, logger, timeout=None): self.__class__.__name__, "Reconnect unsuccessful, timeout exceeded, see logs for more details", ) - - -class ActionLoopDetector(object): - """Help to detect loops for action combinations.""" - - def __init__(self, max_loops, max_combination_length): - """Help to detect loops for action combinations. - - :param max_loops: - :param max_combination_length: - :return: - """ - self._max_action_loops = max_loops - self._max_combination_length = max_combination_length - self._action_history = [] - - def loops_detected(self, action_key): - """Add action key to the history and detect loops. - - :param action_key: - :return: - """ - # """Added action key to the history and detect for loops""" - loops_detected = False - self._action_history.append(action_key) - for combination_length in range(1, self._max_combination_length + 1): - if self._is_combination_compatible(combination_length): - if self._detect_loops_for_combination_length(combination_length): - loops_detected = True - break - return loops_detected - - def _is_combination_compatible(self, combination_length): - """Check if combinations may exist. - - :param combination_length: - :return: - """ - if len(self._action_history) / combination_length >= self._max_action_loops: - is_compatible = True - else: - is_compatible = False - return is_compatible - - def _detect_loops_for_combination_length(self, combination_length): - """Detect loops for combination length. - - :param combination_length: - :return: - """ - reversed_history = self._action_history[::-1] - combinations = [ - reversed_history[x : x + combination_length] - for x in range(0, len(reversed_history), combination_length) - ][: self._max_action_loops] - is_loops_exist = True - for x, y in [combinations[x : x + 2] for x in range(0, len(combinations) - 1)]: - if x != y: - is_loops_exist = False - break - return is_loops_exist diff --git a/cloudshell/cli/session/ssh_session.py b/cloudshell/cli/session/ssh_session.py index ee18650..1e9c09c 100644 --- a/cloudshell/cli/session/ssh_session.py +++ b/cloudshell/cli/session/ssh_session.py @@ -1,4 +1,5 @@ import socket +from io import StringIO import paramiko from scp import SCPClient @@ -28,6 +29,7 @@ def __init__( port=None, on_session_start=None, pkey=None, + pkey_passphrase=None, *args, **kwargs ): @@ -42,6 +44,7 @@ def __init__( self.username = username self.password = password self.pkey = pkey + self.pkey_passphrase = pkey_passphrase self._handler = None self._current_channel = None @@ -58,6 +61,7 @@ def __eq__(self, other): self.username == other.username, self.password == other.password, self.pkey == other.pkey, + self.pkey_passphrase == other.pkey_passphrase, ] ) @@ -74,6 +78,7 @@ def _initialize_session(self, prompt, logger): :param str prompt: :param logging.Logger logger: + :return: """ self._create_handler() try: @@ -86,7 +91,7 @@ def _initialize_session(self, prompt, logger): banner_timeout=30, allow_agent=False, look_for_keys=False, - pkey=self.pkey, + pkey=self._get_pkey_object(self.pkey, self.pkey_passphrase, logger), ) except Exception as e: logger.exception("Failed to initialize session:") @@ -102,6 +107,7 @@ def _connect_actions(self, prompt, logger): :param str prompt: :param logging.Logger logger: + :return: """ self.hardware_expect( None, expected_string=prompt, timeout=self._timeout, logger=logger @@ -182,3 +188,15 @@ def upload_sftp( sftp.putfo(file_stream, dest_pathname) sftp.chmod(dest_pathname, int(dest_permissions, base=8)) sftp.close() + + @staticmethod + def _get_pkey_object(key_material, passphrase, logger): + """Try to detect private key type and return paramiko.PKey object.""" + for cls in [paramiko.RSAKey, paramiko.DSSKey, paramiko.ECDSAKey]: + try: + key = cls.from_private_key(StringIO(key_material), password=passphrase) + except paramiko.ssh_exception.SSHException as e: + # Invalid key, try other key type + logger.warning(e) + else: + return key diff --git a/cloudshell/cli/session/telnet_session.py b/cloudshell/cli/session/telnet_session.py index b5bd68a..8617598 100644 --- a/cloudshell/cli/session/telnet_session.py +++ b/cloudshell/cli/session/telnet_session.py @@ -1,7 +1,8 @@ import socket import telnetlib -from collections import OrderedDict +from cloudshell.cli.service.action_map import Action +from cloudshell.cli.service.action_map import ActionMap from cloudshell.cli.session.connection_params import ConnectionParams from cloudshell.cli.session.expect_session import ExpectSession from cloudshell.cli.session.session_exceptions import ( @@ -58,20 +59,14 @@ def __del__(self): self.disconnect() def _connect_actions(self, prompt, logger): - action_map = OrderedDict() - action_map[ - "[Ll]ogin:|[Uu]ser:|[Uu]sername:" - ] = lambda session, logger: session.send_line(session.username, logger) - action_map["[Pp]assword:"] = lambda session, logger: session.send_line( - session.password, logger - ) - self.hardware_expect( - None, - expected_string=prompt, - timeout=self._timeout, - logger=logger, - action_map=action_map, - ) + action_map = ActionMap(actions=[Action(pattern="[Ll]ogin:|[Uu]ser:|[Uu]sername:", + callback=lambda session, logger: + session.send_line(session.username, logger)), + Action(pattern="[Pp]assword:", + callback=lambda session, logger: + session.send_line(session.password, logger))]) + + self.hardware_expect(None, expected_string=prompt, timeout=self._timeout, logger=logger, action_map=action_map) self._on_session_start(logger) def _initialize_session(self, prompt, logger): diff --git a/tests/cli/service/__init__.py b/tests/cli/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cli/service/test_action_map.py b/tests/cli/service/test_action_map.py new file mode 100644 index 0000000..8f849b0 --- /dev/null +++ b/tests/cli/service/test_action_map.py @@ -0,0 +1,128 @@ +import unittest +from unittest import mock + +from cloudshell.cli.service.action_map import Action +from cloudshell.cli.service.action_map import ActionMap + + +class TestAction(unittest.TestCase): + def setUp(self): + self.session = mock.MagicMock() + self.logger = mock.MagicMock() + self.callback = mock.MagicMock() + self.action = Action(pattern="test pattern", callback=self.callback) + + def test_call(self): + """Check that method will call callback function with session and logger as attributes""" + # act + self.action(session=self.session, logger=self.logger) + # verify + self.callback.assert_called_once_with(self.session, self.logger) + + def test_match_return_true(self): + """Check that method will return True if output matches pattern""" + output = "test pattern" + # act + result = self.action.match(output) + # verify + self.assertTrue(result) + + def test_match_return_false(self): + """Check that method will return False if output doesn't match pattern""" + output = "missed pattern" + # act + result = self.action.match(output) + # verify + self.assertFalse(result) + + +class TestActionMap(unittest.TestCase): + def setUp(self): + self.session = mock.MagicMock() + self.logger = mock.MagicMock() + self.callback = mock.MagicMock() + self.actions = [Action(pattern="[Pp]attern 1", callback=self.callback), + Action(pattern="[Pp]attern 2", callback=self.callback, execute_once=True), + Action(pattern="[Pp]attern 3", callback=self.callback, execute_once=True)] + + self.action_map = ActionMap(actions=self.actions) + + def test_actions(self): + """Check that method will return all actions""" + action1, action2, action3 = self.actions + self.action_map.matched_patterns = {action1.pattern, action2.pattern} + # verify + self.assertEqual(self.action_map.actions, [action1, action2, action3]) + + def test_active_actions(self): + """Check that method will return only active actions""" + action1, action2, action3 = self.actions + self.action_map.matched_patterns = {action1.pattern, action2.pattern} + # verify + self.assertEqual(self.action_map.active_actions, [action1, action3]) + + def test_extend(self): + """Check that extend will add new actions and will not override existing one""" + default_actions = [Action(pattern="[Pp]attern 4", callback=self.callback), + Action(pattern="[Pp]attern 2", callback=self.callback)] + + default_action_map = ActionMap(actions=default_actions) + + default_action1, default_action2 = default_actions + action1, action2, action3 = self.actions + + default_action_map.matched_patterns = {default_action1.pattern, default_action2.pattern} + self.action_map.matched_patterns = {action3.pattern} + + # act + self.action_map.extend(action_map=default_action_map) + + # verify + self.assertEqual(self.action_map.matched_patterns, {default_action1.pattern, + default_action2.pattern, + action3.pattern}) + + self.assertEqual(self.action_map.actions, [action1, action2, action3, default_action1]) + + def test_extend_with_override_true(self): + """Check that extend will add new actions and will not override existing one""" + default_actions = [Action(pattern="[Pp]attern 4", callback=self.callback), + Action(pattern="[Pp]attern 2", callback=self.callback)] + + default_action_map = ActionMap(actions=default_actions) + + default_action1, default_action2 = default_actions + action1, action2, action3 = self.actions + + default_action_map.matched_patterns = {default_action1.pattern, default_action2.pattern} + self.action_map.matched_patterns = {action3.pattern} + + # act + self.action_map.extend(action_map=default_action_map, override=True) + + # verify + self.assertEqual(self.action_map.matched_patterns, {default_action1.pattern, + default_action2.pattern, + action3.pattern}) + + self.assertEqual(self.action_map.actions, [action1, default_action2, action3, default_action1]) + + def test_add(self): + """Check that __add__ method will create new ActionMap""" + default_actions = [Action(pattern="[Pp]attern 4", callback=self.callback), + Action(pattern="[Pp]attern 2", callback=self.callback)] + + default_action_map = ActionMap(actions=default_actions) + + default_action1, default_action2 = default_actions + action1, action2, action3 = self.actions + + default_action_map.matched_patterns = {default_action1.pattern, default_action2.pattern} + self.action_map.matched_patterns = {action3.pattern} + + # act + result = self.action_map + default_action_map + + # verify + self.assertEqual(result.matched_patterns, set()) + self.assertEqual(result.actions, [action1, action2, action3, default_action1]) diff --git a/tests/cli/service/test_error_map.py b/tests/cli/service/test_error_map.py new file mode 100644 index 0000000..abb9640 --- /dev/null +++ b/tests/cli/service/test_error_map.py @@ -0,0 +1,83 @@ +import unittest +from unittest import mock + +from cloudshell.cli.service.error_map import Error +from cloudshell.cli.service.error_map import ErrorMap +from cloudshell.cli.session.session_exceptions import CommandExecutionException + + +class TestError(unittest.TestCase): + def setUp(self): + self.session = mock.MagicMock() + self.logger = mock.MagicMock() + self.error_msg = "error message" + self.error = Error(pattern="test pattern", error=self.error_msg) + + def test_call(self): + """Check that method will raise CommandExecutionException""" + with self.assertRaisesRegex(CommandExecutionException, self.error_msg): + self.error(output="test output") + + def test_match_return_true(self): + """Check that method will return True if output matches pattern""" + output = "test pattern" + # act + result = self.error.match(output) + # verify + self.assertTrue(result) + + def test_match_return_false(self): + """Check that method will return False if output doesn't match pattern""" + output = "missed pattern" + # act + result = self.error.match(output) + # verify + self.assertFalse(result) + + +class TestErrorMap(unittest.TestCase): + def setUp(self): + self.logger = mock.MagicMock() + self.errors = [Error(pattern="[Pp]attern 1", error="error 1"), + Error(pattern="[Pp]attern 2", error="error 2")] + + self.error_map = ErrorMap(errors=self.errors) + + def test_errors(self): + """Check that method will return errors""" + # verify + self.assertEqual(self.error_map.errors, self.errors) + + def test_extend(self): + """Check that extend will add new errors and will not override existing one""" + default_error1 = Error(pattern="[Pp]attern 4", error="error 4") + default_error2 = Error(pattern="[Pp]attern 2", error="error 2") + default_error_map = ErrorMap(errors=[default_error1, default_error2]) + error1, error2 = self.errors + # act + self.error_map.extend(error_map=default_error_map) + # verify + self.assertEqual(self.error_map.errors, [error1, error2, default_error1]) + + def test_extend_with_override_true(self): + """Check that extend will add new errors and will not override existing one""" + default_error1 = Error(pattern="[Pp]attern 4", error="error 4") + default_error2 = Error(pattern="[Pp]attern 2", error="error 2") + default_error_map = ErrorMap(errors=[default_error1, default_error2]) + error1, error2 = self.errors + # act + self.error_map.extend(error_map=default_error_map, override=True) + # verify + self.assertEqual(self.error_map.errors, [error1, default_error2, default_error1]) + + def test_add(self): + """Check that __add__ method will create new ErrorMap""" + default_error1 = Error(pattern="[Pp]attern 4", error="error 4") + default_error2 = Error(pattern="[Pp]attern 2", error="error 2") + default_error_map = ErrorMap(errors=[default_error1, default_error2]) + error1, error2 = self.errors + # act + result = self.error_map + default_error_map + # verify + self.assertEqual(result.errors, [error1, error2, default_error1]) + diff --git a/tests/cli/session/test_expect_session.py b/tests/cli/session/test_abstract_session.py similarity index 96% rename from tests/cli/session/test_expect_session.py rename to tests/cli/session/test_abstract_session.py index 7a110a6..5ae75ba 100644 --- a/tests/cli/session/test_expect_session.py +++ b/tests/cli/session/test_abstract_session.py @@ -1,6 +1,9 @@ -from collections import OrderedDict from unittest import TestCase +from cloudshell.cli.service.action_map import Action +from cloudshell.cli.service.action_map import ActionMap +from cloudshell.cli.service.error_map import Error +from cloudshell.cli.service.error_map import ErrorMap from cloudshell.cli.session.expect_session import ActionLoopDetector, ExpectSession from cloudshell.cli.session.session_exceptions import ( CommandExecutionException, @@ -275,10 +278,8 @@ def test_hardware_expect_action_map_call( receive_all.side_effect = side_effect normalize_buffer.side_effect = side_effect test_func = Mock() - action_map = OrderedDict({fake_out: test_func}) - self._instance.hardware_expect( - command, expected_string, self._logger, action_map=action_map - ) + action_map = ActionMap(actions=[Action(pattern=fake_out, callback=test_func)]) + self._instance.hardware_expect(command, expected_string, self._logger, action_map=action_map) test_func.assert_called_once_with(self._instance, self._logger) @patch("cloudshell.cli.session.expect_session.ExpectSession.send_line") @@ -294,7 +295,7 @@ def test_hardware_expect_error_map_call( expected_string = "test_string" receive_all.return_value = expected_string normalize_buffer.return_value = expected_string - error_map = OrderedDict({expected_string: "test_error"}) + error_map = ErrorMap(errors=[Error(pattern=expected_string, error='test_error')]) exception = CommandExecutionException with self.assertRaises(exception): self._instance.hardware_expect( @@ -317,7 +318,8 @@ class TestException(CommandExecutionException): expected_string = "test_string" receive_all.return_value = expected_string normalize_buffer.return_value = expected_string - error_map = OrderedDict({expected_string: TestException("test_error")}) + error_map = ErrorMap(errors=[Error(pattern=expected_string, error=TestException('test_error'))]) + with self.assertRaises(TestException): self._instance.hardware_expect( command, expected_string, self._logger, error_map=error_map diff --git a/tests/cli/session/test_ssh_session.py b/tests/cli/session/test_ssh_session.py index 6ec39db..f8c9381 100644 --- a/tests/cli/session/test_ssh_session.py +++ b/tests/cli/session/test_ssh_session.py @@ -443,9 +443,6 @@ def test_eq(self, expect_session): ) ) - pkey = paramiko.RSAKey.from_private_key( - StringIO(KEY_WITH_PASSPHRASE), password=KEY_PASSPHRASE - ) self.assertFalse( self._instance.__eq__( SSHSession( @@ -454,23 +451,22 @@ def test_eq(self, expect_session): "", port=self._port, on_session_start=self._on_session_start, - pkey=pkey, + pkey=KEY_WITH_PASSPHRASE, + pkey_passphrase=KEY_PASSPHRASE, ) ) ) @patch("cloudshell.cli.session.ssh_session.ExpectSession") def test_eq_rsa(self, expect_session): - pkey = paramiko.RSAKey.from_private_key( - StringIO(KEY_WITH_PASSPHRASE), password=KEY_PASSPHRASE - ) self._instance = SSHSession( self._hostname, self._username, self._password, port=self._port, on_session_start=self._on_session_start, - pkey=pkey, + pkey=KEY_WITH_PASSPHRASE, + pkey_passphrase=KEY_PASSPHRASE, ) self.assertTrue( @@ -481,7 +477,8 @@ def test_eq_rsa(self, expect_session): self._password, port=self._port, on_session_start=self._on_session_start, - pkey=pkey, + pkey=KEY_WITH_PASSPHRASE, + pkey_passphrase=KEY_PASSPHRASE, ) ) ) @@ -640,16 +637,13 @@ def test_rsa(self): "", port=server.port, on_session_start=self._on_session_start, - pkey=pkey, + pkey=KEY_WITH_PASSPHRASE, + pkey_passphrase=KEY_PASSPHRASE, ) self._instance.connect(">", logger=Mock()) self._instance.hardware_expect("dummy command", ">", Mock()) def test_rsa_failure(self): - pkey = paramiko.RSAKey.from_private_key( - StringIO(KEY_WITH_PASSPHRASE), password=KEY_PASSPHRASE - ) - server = SSHServer(user2key={}) with self.assertRaises(SSHSessionException): @@ -659,7 +653,8 @@ def test_rsa_failure(self): "", port=server.port, on_session_start=self._on_session_start, - pkey=pkey, + pkey=KEY_WITH_PASSPHRASE, + pkey_passphrase=KEY_PASSPHRASE, ) self._instance.connect(">", logger=Mock()) self._instance.hardware_expect("dummy command", ">", Mock()) diff --git a/tests/cli/test_session_pool_context_manager.py b/tests/cli/test_session_pool_context_manager.py index 6a1a691..5e95bbe 100644 --- a/tests/cli/test_session_pool_context_manager.py +++ b/tests/cli/test_session_pool_context_manager.py @@ -137,7 +137,7 @@ def test_exit_return_session_on_ignored_exception(self, command_mode_helper): def test_exit_remove_session_on_inactive(self, command_mode_helper): self._instance._initialize_cli_service = Mock() session_value = Mock() - session_value.active.return_value = False + session_value.active = False self._session_pool_manager.get_session.return_value = session_value with self._instance: pass diff --git a/version.txt b/version.txt index c5106e6..0062ac9 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.0.4 +5.0.0