Skip to content

Commit

Permalink
Merge 0736a88 into d83867b
Browse files Browse the repository at this point in the history
  • Loading branch information
yaroslavNqualisystems committed Nov 23, 2018
2 parents d83867b + 0736a88 commit e8a2289
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 1 deletion.
4 changes: 3 additions & 1 deletion cloudshell/devices/flows/cli_action_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from abc import abstractmethod
from cloudshell.devices.cli_handler_impl import CliHandlerImpl
from cloudshell.devices.helpers.custom_command_executor import CustomCommandExecutor


class BaseCliFlow(object):
Expand Down Expand Up @@ -141,7 +142,8 @@ def execute_flow(self, custom_command="", is_config=False):

with self._cli_handler.get_cli_service(mode) as session:
for cmd in commands:
responses.append(session.send_command(command=cmd))
command_executor = CustomCommandExecutor(cmd)
responses.append(command_executor.execute_commands(session, self._logger))
return '\n'.join(responses)


Expand Down
2 changes: 2 additions & 0 deletions cloudshell/devices/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
109 changes: 109 additions & 0 deletions cloudshell/devices/helpers/custom_command_executor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import re
from collections import OrderedDict


class ComplexCommand(object):
def __init__(self, command, action_map=None, error_map=None):
self.command = command.strip()
self.action_map = action_map or OrderedDict()
self.error_map = error_map or OrderedDict()

def add_error(self, pattern, error):
"""
:type pattern: basestring
:type error: basestring
"""
self.error_map[pattern.strip()] = error.strip()

def add_action(self, pattern, action):
"""
:type pattern: basestring
:type action: basestring
"""
self.action_map[pattern.strip()] = lambda session, logger: session.send_line(action.strip(), logger)

def execute(self, cli_service, logger):
"""
:type cli_service: cloudshell.cli.cli_service_impl.CliServiceImpl
:type logger: logging.Logger
:rtype: basestring
"""
output = cli_service.send_command(self.command, action_map=self.action_map, error_map=self.error_map,
logger=logger)
return output


class CustomCommandExecutor(object):
ERROR_MARKER = 'error_map'
ACTION_MARKER = 'action_map'
COMMAND_SEPARATOR = ';'
# PATTERN_SEPARATOR = '='

COMMAND_PATTERN = r'^\s*(.*?)\s*({})'.format('|'.join([ERROR_MARKER, ACTION_MARKER]))

ACTION_ERROR_PATTERN = r'(%s)\s*=\s*(\{.+?\})' % ('|'.join([ACTION_MARKER, ERROR_MARKER]))

def __init__(self, command):
"""
:type command: basestring
"""
self.command = command.strip(self.COMMAND_SEPARATOR)
self.commands = []
self.action_map = OrderedDict()
self.error_map = OrderedDict()
self._process_command()

def _process_command(self):
"""
Prepare commands with error_map and action_map
"""
for block in self.command.split(self.COMMAND_SEPARATOR):
command = self._process_command_block(block.strip())
self.commands.append(command)

def _process_command_block(self, command_block):
"""
:type command_block: basestring
:rtype: ComplexCommand
"""
command_match = re.search(self.COMMAND_PATTERN, command_block, re.IGNORECASE)
if command_match:
command_string = command_match.group(1)
else:
command_string = command_block
command = ComplexCommand(command_string)
action_blocks = re.findall(self.ACTION_ERROR_PATTERN, command_block, re.IGNORECASE | re.DOTALL)
for key, value in action_blocks:
action_dict = self._deserialize_action(value)
for pattern, action_error in action_dict.iteritems():
if key.lower() == self.ERROR_MARKER:
command.add_error(pattern, action_error)
elif key.lower() == self.ACTION_MARKER:
command.add_action(pattern, action_error)
else:
raise Exception(self.__class__.__name__, 'Cannot determine key {}'.format(key))
return command

def _deserialize_action(self, block):
"""
:param block: Block of Actions or errors {'pattern':'action', 'error_pattern':'error'}
:type block: str
:rtype: dict
"""
result_dict = OrderedDict()
c_block = re.sub(r'\{\s*[\'\"]{1}|[\'\"]{1}\}', '', block)
for sub_block in re.split(r'[\"\']{1}\s*\,\s*[\"\']{1}', c_block):
key, value = re.split(r'[\'\"]{1}\s*\:\s*[\'\"]{1}', sub_block)
result_dict[key] = value
return result_dict

def execute_commands(self, cli_service, logger):
"""
:type cli_service: cloudshell.cli.cli_service_impl.CliServiceImpl
:type logger: logging.Logger
:rtype: basestring
"""
output = ''
for command in self.commands:
output += command.execute(cli_service, logger)
return output
2 changes: 2 additions & 0 deletions tests/devices/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
212 changes: 212 additions & 0 deletions tests/devices/helpers/test_custom_command_executor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import unittest
from collections import OrderedDict

from mock import Mock, patch, call

from cloudshell.devices.helpers.custom_command_executor import ComplexCommand, CustomCommandExecutor


class TestComplexCommand(unittest.TestCase):
def setUp(self):
self.logger = Mock()
self.command = Mock()
self.error_map = Mock()
self.action_map = Mock()

def _initialize_without_args(self):
return ComplexCommand(self.command)

def _initialize_with_args(self):
return ComplexCommand(self.command, self.action_map, self.error_map)

def test_init_without_error_action_map_args(self):
instance = self._initialize_without_args()
self.command.strip.assert_called_once()
self.assertIsInstance(instance.error_map, OrderedDict)
self.assertIsInstance(instance.action_map, OrderedDict)

def test_init_with_error_action_map_args(self):
instance = self._initialize_with_args()
self.command.strip.assert_called_once()
self.assertIs(instance.action_map, self.action_map)
self.assertIs(instance.error_map, self.error_map)

def test_add_error(self):
instance = self._initialize_without_args()
pattern_value = Mock()
pattern = Mock(strip=Mock(return_value=pattern_value))
error_value = Mock()
error = Mock(strip=Mock(return_value=error_value))
instance.add_error(pattern, error)
self.assertTrue(instance.error_map.get(pattern_value) == error_value)

def test_add_action(self):
instance = self._initialize_without_args()
pattern_value = Mock()
pattern = Mock(strip=Mock(return_value=pattern_value))
action_value = Mock()
action = Mock(strip=Mock(return_value=action_value))
instance.add_action(pattern, action)
action_func = instance.action_map.get(pattern_value, None)
self.assertIsNotNone(action_func)
session_result = Mock()
session = Mock(send_line=Mock(return_value=session_result))
self.assertIs(action_func(session, self.logger), session_result)
session.send_line.assert_called_once_with(action_value, self.logger)

def test_execute(self):
command_value = Mock()
self.command.strip.return_value = command_value
instance = self._initialize_with_args()
result = Mock()
cli_service = Mock(send_command=Mock(return_value=result))
self.assertIs(instance.execute(cli_service, self.logger), result)
cli_service.send_command.assert_called_once_with(command_value, action_map=self.action_map,
error_map=self.error_map, logger=self.logger)


class TestCustomCommandExecutor(unittest.TestCase):
def setUp(self):
self.block1 = Mock()
self.block2 = Mock()
self.command_value = Mock(split=Mock(return_value=[self.block1, self.block2]))
self.command = Mock(strip=Mock(return_value=self.command_value))

def _initialize_instance(self):
return CustomCommandExecutor(self.command)

@patch("cloudshell.devices.helpers.custom_command_executor.CustomCommandExecutor._process_command")
def test_init(self, _process_command):
instance = self._initialize_instance()
self.assertIs(instance.command, self.command_value)
self.assertIsInstance(instance.commands, list)
_process_command.assert_called_once_with()

@patch("cloudshell.devices.helpers.custom_command_executor.CustomCommandExecutor._process_command_block")
def test_process_command(self, _process_command_block):
commands = [Mock(), Mock()]
_process_command_block.side_effect = commands
block1_value = Mock()
block2_value = Mock()
self.block1.strip.return_value = block1_value
self.block2.strip.return_value = block2_value
instance = self._initialize_instance()
self.command_value.split.assert_called_once_with(CustomCommandExecutor.COMMAND_SEPARATOR)
_process_command_block.assert_has_calls([call(block1_value), call(block2_value)])
self.assertEqual(instance.commands, commands)

@patch("cloudshell.devices.helpers.custom_command_executor.ComplexCommand")
@patch("cloudshell.devices.helpers.custom_command_executor.CustomCommandExecutor._process_command")
@patch("cloudshell.devices.helpers.custom_command_executor.CustomCommandExecutor._deserialize_action")
@patch("cloudshell.devices.helpers.custom_command_executor.re")
def test_process_command_block_command_partial_string(self, _re, _deserialize_action, _process_command,
complex_command):
command_block = Mock()
command_value = Mock()
command_match = Mock(group=Mock(return_value=command_value))
_re.search.return_value = command_match
instance = self._initialize_instance()
instance._process_command_block(command_block)
_re.search.assert_called_once_with(instance.COMMAND_PATTERN, command_block, _re.IGNORECASE)
command_match.group.assert_called_once_with(1)
complex_command.assert_called_once_with(command_value)

@patch("cloudshell.devices.helpers.custom_command_executor.ComplexCommand")
@patch("cloudshell.devices.helpers.custom_command_executor.CustomCommandExecutor._process_command")
@patch("cloudshell.devices.helpers.custom_command_executor.CustomCommandExecutor._deserialize_action")
@patch("cloudshell.devices.helpers.custom_command_executor.re")
def test_process_command_block_command_whole_string(self, _re, _deserialize_action, _process_command,
complex_command):
command_block = Mock()
_re.search.return_value = None
instance = self._initialize_instance()
instance._process_command_block(command_block)
_re.search.assert_called_once_with(instance.COMMAND_PATTERN, command_block, _re.IGNORECASE)
complex_command.assert_called_once_with(command_block)

@patch("cloudshell.devices.helpers.custom_command_executor.ComplexCommand")
@patch("cloudshell.devices.helpers.custom_command_executor.CustomCommandExecutor._process_command")
@patch("cloudshell.devices.helpers.custom_command_executor.CustomCommandExecutor._deserialize_action")
@patch("cloudshell.devices.helpers.custom_command_executor.re")
def test_process_command_block_error_marker(self, _re, _deserialize_action, _process_command, complex_command):
command = Mock()
complex_command.return_value = command
command_block = Mock()
key = CustomCommandExecutor.ERROR_MARKER
value = Mock()
_re.findall.return_value = [(key, value)]
pattern = Mock()
action = Mock()
_deserialize_action.return_value = {pattern: action}
instance = self._initialize_instance()
self.assertIs(instance._process_command_block(command_block), command)
_re.findall.assert_called_once_with(instance.ACTION_ERROR_PATTERN, command_block, _re.IGNORECASE | _re.DATALL)
_deserialize_action.assert_called_once_with(value)
command.add_error.assert_called_once_with(pattern, action)

@patch("cloudshell.devices.helpers.custom_command_executor.ComplexCommand")
@patch("cloudshell.devices.helpers.custom_command_executor.CustomCommandExecutor._process_command")
@patch("cloudshell.devices.helpers.custom_command_executor.CustomCommandExecutor._deserialize_action")
@patch("cloudshell.devices.helpers.custom_command_executor.re")
def test_process_command_block_action_marker(self, _re, _deserialize_action, _process_command, complex_command):
command = Mock()
complex_command.return_value = command
command_block = Mock()
key = CustomCommandExecutor.ACTION_MARKER
value = Mock()
_re.findall.return_value = [(key, value)]
pattern = Mock()
action = Mock()
_deserialize_action.return_value = {pattern: action}
instance = self._initialize_instance()
self.assertIs(instance._process_command_block(command_block), command)
_re.findall.assert_called_once_with(instance.ACTION_ERROR_PATTERN, command_block, _re.IGNORECASE | _re.DATALL)
_deserialize_action.assert_called_once_with(value)
command.add_action.assert_called_once_with(pattern, action)

@patch("cloudshell.devices.helpers.custom_command_executor.ComplexCommand")
@patch("cloudshell.devices.helpers.custom_command_executor.CustomCommandExecutor._process_command")
@patch("cloudshell.devices.helpers.custom_command_executor.CustomCommandExecutor._deserialize_action")
@patch("cloudshell.devices.helpers.custom_command_executor.re")
def test_process_command_block_exception(self, _re, _deserialize_action, _process_command, complex_command):
command = Mock()
complex_command.return_value = command
command_block = Mock()
key = Mock()
value = Mock()
_re.findall.return_value = [(key, value)]
pattern = Mock()
action = Mock()
_deserialize_action.return_value = {pattern: action}
instance = self._initialize_instance()
with self.assertRaisesRegexp(Exception, "Cannot determine key"):
instance._process_command_block(command_block)
_re.findall.assert_called_once_with(instance.ACTION_ERROR_PATTERN, command_block, _re.IGNORECASE | _re.DATALL)
_deserialize_action.assert_called_once_with(value)

@patch("cloudshell.devices.helpers.custom_command_executor.CustomCommandExecutor._process_command")
@patch("cloudshell.devices.helpers.custom_command_executor.re")
def test_deserialize_action(self, _re, _process_command):
block = Mock()
c_block = Mock()
sub_block = Mock()
key = Mock()
value = Mock()
_re.sub.return_value = c_block
_re.split.side_effect = [[sub_block], (key, value)]
instance = self._initialize_instance()
self.assertEqual(instance._deserialize_action(block), OrderedDict([(key, value)]))
_re.sub.assert_called_once_with(r'\{\s*[\'\"]{1}|[\'\"]{1}\}', '', block)
_re.split.assert_has_calls(
[call(r'[\"\']{1}\s*\,\s*[\"\']{1}', c_block), call(r'[\'\"]{1}\s*\:\s*[\'\"]{1}', sub_block)])

@patch("cloudshell.devices.helpers.custom_command_executor.CustomCommandExecutor._process_command")
def test_execute_command(self, _process_command):
command_otput = 'test_output'
command = Mock(execute=Mock(return_value=command_otput))
cli_service = Mock()
logger = Mock()
instance = self._initialize_instance()
instance.commands = [command]
self.assertEqual(instance.execute_commands(cli_service, logger), command_otput)
command.execute.assert_called_once_with(cli_service, logger)

0 comments on commit e8a2289

Please sign in to comment.