diff --git a/VERSION b/VERSION index 6f4eebd..100435b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.8.1 +0.8.2 diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 8cad20a..8934cd6 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -64,8 +64,20 @@ provides some helpful methods and attributes to work with the switch. # send one or more commands to the node node.enable('show hostname') - [{'command': 'show hostname', 'result': {u'hostname': u'veos01', u'fqdn': - u'veos01.arista.com'}, 'encoding': 'json'}] + [{'command': 'show hostname', + 'encoding': 'json', + 'result': {u'hostname': u'veos01', + u'fqdn': u'veos01.arista.com'}}] + + # Request a specific revision of a command that has been updated + node.enable({'cmd': 'show cvx', 'revision': 2}) + [{'command': {'cmd': 'show cvx', 'revision': 2}, + 'encoding': 'json', + 'result': {u'clusterMode': False, + u'controllerUUID': u'', + u'enabled': False, + u'heartbeatInterval': 20.0, + u'heartbeatTimeout': 60.0}}] # use the config method to send configuration commands node.config('hostname veos01') diff --git a/docs/release-notes-0.8.2.rst b/docs/release-notes-0.8.2.rst new file mode 100644 index 0000000..2bb2be6 --- /dev/null +++ b/docs/release-notes-0.8.2.rst @@ -0,0 +1,41 @@ +Release 0.8.2 +------------- + +2018-02-09 + +New Modules +^^^^^^^^^^^ + + +Enhancements +^^^^^^^^^^^^ + +* Support eapi command revision syntax (`158 `_) [`jerearista `_] + Support requests for specific revisions of EOS command output + + .. code-block:: python + + >>> node.enable({'cmd': 'show cvx', 'revision': 2}) + [{'command': {'cmd': 'show cvx', 'revision': 2}, + 'encoding': 'json', + 'result': {u'clusterMode': False, + u'controllerUUID': u'', + u'enabled': False, + u'heartbeatInterval': 20.0, + u'heartbeatTimeout': 60.0}}] + +* Add clearer error message for bad user credentials. (`152 `_) [`mharista `_] + .. comment +* Reformat EapiConnection send methods exception handling. (`148 `_) [`mharista `_] + .. comment + +Fixed +^^^^^ + +* Fix route map getall function to find route maps with a hyphen in the name. (`154 `_) [`mharista `_] + .. comment + +Known Caveats +^^^^^^^^^^^^^ + + diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 88b7c50..84d6746 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -6,6 +6,7 @@ Release Notes :maxdepth: 2 :titlesonly: + release-notes-0.8.2.rst release-notes-0.8.1.rst release-notes-0.8.0.rst release-notes-0.7.0.rst diff --git a/pyeapi/__init__.py b/pyeapi/__init__.py index dbcff3b..0c069f4 100644 --- a/pyeapi/__init__.py +++ b/pyeapi/__init__.py @@ -29,7 +29,7 @@ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # -__version__ = '0.8.1' +__version__ = '0.8.2' __author__ = 'Arista EOS+' diff --git a/pyeapi/api/routemaps.py b/pyeapi/api/routemaps.py index bdf64e1..6c513c8 100644 --- a/pyeapi/api/routemaps.py +++ b/pyeapi/api/routemaps.py @@ -100,7 +100,7 @@ def get(self, name): def getall(self): resources = dict() - routemaps_re = re.compile(r'^route-map\s(\w+)\s\w+\s\d+$', re.M) + routemaps_re = re.compile(r'^route-map\s([\w-]+)\s\w+\s\d+$', re.M) for name in routemaps_re.findall(self.config): routemap = self.get(name) if routemap: diff --git a/pyeapi/eapilib.py b/pyeapi/eapilib.py index 2e0dab6..26eed35 100644 --- a/pyeapi/eapilib.py +++ b/pyeapi/eapilib.py @@ -395,6 +395,10 @@ def send(self, data): reason=response.reason)) _LOGGER.debug('Response content: {}'.format(response_content)) + if response.status == 401: + raise ConnectionError(str(self), '%s. %s' % (response.reason, + response_content)) + # Work around for Python 2.7/3.x compatibility if not type(response_content) == str: # For Python 3.x - decode bytes into string @@ -416,17 +420,17 @@ def send(self, data): return decoded # socket.error is deprecated in python 3 and replaced with OSError. - except (socket.error, OSError, ValueError) as exc: - if isinstance(exc, socket.error) or isinstance(exc, OSError): - self.socket_error = exc + except (socket.error, OSError) as exc: _LOGGER.exception(exc) + self.socket_error = exc self.error = exc - error_msg = 'unable to connect to eAPI' - if self.socket_error: - error_msg = ('Socket error during eAPI connection: %s' - % str(exc)) + error_msg = 'Socket error during eAPI connection: %s' % str(exc) raise ConnectionError(str(self), error_msg) - + except ValueError as exc: + _LOGGER.exception(exc) + self.socket_error = None + self.error = exc + raise ConnectionError(str(self), 'unable to connect to eAPI') finally: self.transport.close() diff --git a/pyeapi/utils.py b/pyeapi/utils.py index 658fa2f..4386cad 100644 --- a/pyeapi/utils.py +++ b/pyeapi/utils.py @@ -184,7 +184,7 @@ def make_iterable(value): # Convert unicode values to strings for Python 2 if isinstance(value, unicode): value = str(value) - if isinstance(value, str): + if isinstance(value, str) or isinstance(value, dict): value = [value] if not isinstance(value, collections.Iterable): diff --git a/test/fixtures/running_config.routemaps b/test/fixtures/running_config.routemaps index e6ccd54..0e331f1 100644 --- a/test/fixtures/running_config.routemaps +++ b/test/fixtures/running_config.routemaps @@ -33,3 +33,15 @@ route-map FOOBAR permit 20 match interface Ethernet2 continue 200 ! +route-map FOO-BAR permit 10 + match as 2000 + match source-protocol ospf + match interface Ethernet2 + continue 200 +! +route-map FOO-BAR deny 20 + match as 2000 + match source-protocol ospf + match interface Ethernet2 + continue 200 +! diff --git a/test/system/test_api_routemaps.py b/test/system/test_api_routemaps.py index 615355e..4e2fa19 100644 --- a/test/system/test_api_routemaps.py +++ b/test/system/test_api_routemaps.py @@ -61,12 +61,12 @@ def test_getall(self): dut.config(['no route-map TEST deny 10', 'route-map TEST deny 10', 'set weight 100', - 'no route-map TEST2 permit 50', - 'route-map TEST2 permit 50', + 'no route-map TEST-2 permit 50', + 'route-map TEST-2 permit 50', 'match tag 50']) result = dut.api('routemaps').getall() self.assertIn(('TEST'), result) - self.assertIn(('TEST2'), result) + self.assertIn(('TEST-2'), result) def test_create(self): for dut in self.duts: diff --git a/test/system/test_client.py b/test/system/test_client.py index 49def36..200ef59 100644 --- a/test/system/test_client.py +++ b/test/system/test_client.py @@ -59,6 +59,20 @@ def setUp(self): # enable password on the dut and clear it on tearDown dut.config("enable secret %s" % dut._enablepwd) + def test_unauthorized_user(self): + error_string = ('Unauthorized. Unable to authenticate user: Bad' + ' username/password combination') + for dut in self.duts: + temp_node = pyeapi.connect(host=dut.settings['host'], + transport=dut.settings['transport'], + username='wrong', password='nope', + port=dut.settings['port'], + return_node=True) + try: + temp_node.run_commands('show version') + except pyeapi.eapilib.ConnectionError as err: + self.assertEqual(err.message, error_string) + def test_populate_version_properties(self): for dut in self.duts: result = dut.run_commands('show version') @@ -72,6 +86,18 @@ def test_enable_single_command(self): self.assertIsInstance(result, list, 'dut=%s' % dut) self.assertEqual(len(result), 1, 'dut=%s' % dut) + def test_enable_single_extended_command(self): + for dut in self.duts: + result = dut.run_commands({'cmd': 'show cvx', 'revision': 1}) + self.assertIsInstance(result, list, 'dut=%s' % dut) + self.assertEqual(len(result), 1, 'dut=%s' % dut) + self.assertTrue('clusterMode' not in result[0].keys()) + + result2 = dut.run_commands({'cmd': 'show cvx', 'revision': 2}) + self.assertIsInstance(result2, list, 'dut=%s' % dut) + self.assertEqual(len(result2), 1, 'dut=%s' % dut) + self.assertTrue('clusterMode' in result2[0].keys()) + def test_enable_single_unicode_command(self): for dut in self.duts: result = dut.run_commands(u'show version') diff --git a/test/unit/test_api_routemaps.py b/test/unit/test_api_routemaps.py index 0413375..b1b1ea7 100644 --- a/test/unit/test_api_routemaps.py +++ b/test/unit/test_api_routemaps.py @@ -61,8 +61,11 @@ def test_get_not_configured(self): self.assertIsNone(self.instance.get('blah')) def test_getall(self): + # Review fixtures/running_config.routemaps to see the default + # running-config that is the basis for this test result = self.instance.getall() self.assertIsInstance(result, dict) + self.assertEqual(len(result.keys()), 4) def test_routemaps_functions(self): for name in ['create', 'delete', 'default']: diff --git a/test/unit/test_client.py b/test/unit/test_client.py index 62ef8cc..1cf3977 100644 --- a/test/unit/test_client.py +++ b/test/unit/test_client.py @@ -101,6 +101,16 @@ def test_enable_with_single_unicode_command(self): self.connection.execute.assert_called_once_with(response, 'json') self.assertEqual(command, result[0]['result']) + def test_enable_with_single_extended_command(self): + command = {'cmd': 'show cvx', 'revision': 2} + response = ['enable', command] + + self.connection.execute.return_value = {'result': list(response)} + result = self.node.enable(command) + + self.connection.execute.assert_called_once_with(response, 'json') + self.assertEqual(command, result[0]['result']) + def test_no_enable_with_single_command(self): command = random_string() response = [command] diff --git a/test/unit/test_eapilib.py b/test/unit/test_eapilib.py index 0066e1f..1bafef8 100644 --- a/test/unit/test_eapilib.py +++ b/test/unit/test_eapilib.py @@ -104,6 +104,25 @@ def test_send_with_authentication(self): self.assertTrue(mock_transport.close.called) + def test_send_unauthorized_user(self): + error_string = ('Unauthorized. Unable to authenticate user: Bad' + ' username/password combination') + response_str = ('Unable to authenticate user: Bad username/password' + ' combination') + mock_transport = Mock(name='transport') + mockcfg = {'getresponse.return_value.read.return_value': response_str, + 'getresponse.return_value.status': 401, + 'getresponse.return_value.reason': 'Unauthorized'} + mock_transport.configure_mock(**mockcfg) + + instance = pyeapi.eapilib.EapiConnection() + instance.authentication('username', 'password') + instance.transport = mock_transport + try: + instance.send('test') + except pyeapi.eapilib.ConnectionError as err: + self.assertEqual(err.message, error_string) + def test_send_raises_connection_error(self): mock_transport = Mock(name='transport') mockcfg = {'getresponse.return_value.read.side_effect': ValueError}