From 1c1a5bc6996d863149021ae38aafe9b06a647784 Mon Sep 17 00:00:00 2001 From: Michael Ben-Ami Date: Thu, 10 Dec 2015 11:31:17 -0500 Subject: [PATCH 1/5] setup.py print statement for python 3 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8c65f1f..3048963 100644 --- a/setup.py +++ b/setup.py @@ -81,7 +81,7 @@ def install(): # Use the following to dynamically build pyeapi module documentation if install() and environ.get('READTHEDOCS'): - print 'This method is only called by READTHEDOCS.' + print('This method is only called by READTHEDOCS.') from subprocess import Popen proc = Popen(['make', 'modules'], cwd='docs/') (_, err) = proc.communicate() From db666bcf1de220e074662c247b4908caa833316e Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Thu, 10 Dec 2015 11:56:43 -0700 Subject: [PATCH 2/5] empty string values give error message instead of simple failure --- pyeapi/api/bgp.py | 4 ++++ test/unit/test_api_bgp.py | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/pyeapi/api/bgp.py b/pyeapi/api/bgp.py index 52d7497..0bfa62e 100644 --- a/pyeapi/api/bgp.py +++ b/pyeapi/api/bgp.py @@ -161,12 +161,16 @@ def set_shutdown(self, default=False, disable=True): return self.configure_bgp(cmd) def add_network(self, prefix, length, route_map=None): + if prefix == '' or length == '': + raise ValueError('network prefix/length values may not be null') cmd = 'network {}/{}'.format(prefix, length) if route_map: cmd += ' route-map {}'.format(route_map) return self.configure_bgp(cmd) def remove_network(self, prefix, masklen, route_map=None): + if prefix == '' or masklen == '': + raise ValueError('network prefix/masklen values may not be null') cmd = 'no network {}/{}'.format(prefix, masklen) if route_map: cmd += ' route-map {}'.format(route_map) diff --git a/test/unit/test_api_bgp.py b/test/unit/test_api_bgp.py index 5da1a13..27fa922 100644 --- a/test/unit/test_api_bgp.py +++ b/test/unit/test_api_bgp.py @@ -80,11 +80,23 @@ def test_add_network(self): cmds = ['router bgp 65000', 'network 172.16.10.1/24 route-map test'] self.eapi_positive_config_test(func, cmds) + func = function('add_network', '', '24', 'test') + self.eapi_exception_config_test(func, ValueError) + + func = function('add_network', '172.16.10.1', '', 'test') + self.eapi_exception_config_test(func, ValueError) + def test_remove_network(self): func = function('remove_network', '172.16.10.1', '24', 'test') cmds = ['router bgp 65000', 'no network 172.16.10.1/24 route-map test'] self.eapi_positive_config_test(func, cmds) + func = function('remove_network', '', '24', 'test') + self.eapi_exception_config_test(func, ValueError) + + func = function('remove_network', '172.16.10.1', '', 'test') + self.eapi_exception_config_test(func, ValueError) + def test_set_router_id(self): for state in ['config', 'negate', 'default']: rid = '1.1.1.1' From efe7c7eb1317bac20113227813c9fab7b61ee6cd Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Fri, 11 Dec 2015 16:30:51 -0700 Subject: [PATCH 3/5] implement NTP module and test cases --- pyeapi/api/ntp.py | 196 ++++++++++++++++++++++++++++++ test/fixtures/running_config.text | 6 + test/system/test_api_ntp.py | 157 ++++++++++++++++++++++++ test/unit/test_api_ntp.py | 118 ++++++++++++++++++ 4 files changed, 477 insertions(+) create mode 100644 pyeapi/api/ntp.py create mode 100644 test/system/test_api_ntp.py create mode 100644 test/unit/test_api_ntp.py diff --git a/pyeapi/api/ntp.py b/pyeapi/api/ntp.py new file mode 100644 index 0000000..d7160d4 --- /dev/null +++ b/pyeapi/api/ntp.py @@ -0,0 +1,196 @@ +# +# Copyright (c) 2015, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +"""Module for managing the NTP configuration in EOS + +This module provides an API for configuring NTP resources using +EOS and eAPI. + +Arguments: + name (string): The interface port that specifies the NTP source. +""" + +import re + +from pyeapi.api import Entity + + +class Ntp(Entity): + """The Ntp class implements global NTP router configuration + """ + + def __init__(self, *args, **kwargs): + super(Ntp, self).__init__(*args, **kwargs) + + def get(self): + """Returns the current NTP configuration + + The Ntp resourc returns the following: + + * source_interface (str): The interface port that specifies + NTP server + * servers (list): A list of the NTP servers that have been + assigned to the node. Each entry in the + list is a key/value pair of the name of + the server as the key and None or 'prefer' + as the value if the server is preferred. + + Returns: + A Python dictionary object of key/value pairs that represents + the current NTP configuration of the node. + + { + "source_interface": 'Loopback0', + 'servers': [ + { '1.1.1.1': None }, + { '1.1.1.2': 'prefer' }, + { '1.1.1.3': 'prefer' }, + { '1.1.1.4': None }, + ] + } + """ + config = self.config + if not config: + return None + + response = dict() + response.update(self._parse_source_interface(config)) + response.update(self._parse_servers(config)) + + return response + + def _parse_source_interface(self, config): + match = re.search(r'^ntp source ([^\s]+)', config, re.M) + value = match.group(1) if match else None + return dict(source_interface=value) + + def _parse_servers(self, config): + matches = re.findall(r'ntp server ([\S]+) ?(prefer)?', config, re.M) + value = [] + for match in matches: + server = match[0] + prefer = match[1] if match[1] == 'prefer' else None + value.append({server: prefer}) + return dict(servers=value) + + def create(self, name): + """Instantiate the NTP by setting the source interface. + + Args: + name (string): The interface port that specifies the NTP source. + + Returns: + True if the operation succeeds, otherwise False. + """ + return self.set_source_interface(name) + + def delete(self): + """Delete the NTP source entry from the node. + + Returns: + True if the operation succeeds, otherwise False. + """ + cmd = self.command_builder('ntp source', disable=True) + return self.configure(cmd) + + def default(self): + """Default the NTP source entry from the node. + + Returns: + True if the operation succeeds, otherwise False. + """ + cmd = self.command_builder('ntp source', default=True) + return self.configure(cmd) + + def set_source_interface(self, name): + """Assign the NTP source on the node + + Args: + name (string): The interface port that specifies the NTP source. + + Returns: + True if the operation succeeds, otherwise False. + """ + cmd = self.command_builder('ntp source', value=name) + return self.configure(cmd) + + def add_server(self, name, prefer=False): + """Add or update an NTP server entry to the node config + + Args: + name (string): The IP address or FQDN of the NTP server. + prefer (bool): Sets the NTP server entry as preferred if True. + + Returns: + True if the operation succeeds, otherwise False. + """ + if not name or re.match(r'^[\s]+$', name): + raise ValueError('ntp server name must be specified') + if prefer: + name = '%s prefer' % name + cmd = self.command_builder('ntp server', value=name) + return self.configure(cmd) + + def remove_server(self, name): + """Remove an NTP server entry from the node config + + Args: + name (string): The IP address or FQDN of the NTP server. + + Returns: + True if the operation succeeds, otherwise False. + """ + cmd = self.command_builder('no ntp server', value=name) + return self.configure(cmd) + + def remove_all_servers(self): + """Remove all NTP server entries from the node config + + Returns: + True if the operation succeeds, otherwise False. + """ + # 'no ntp' removes all server entries. + # For command_builder, disable command 'ntp' gives the desired command + cmd = self.command_builder('ntp', disable=True) + return self.configure(cmd) + + +def instance(node): + """Returns an instance of Ntp + + Args: + node (Node): The node argument passes an instance of Node to the + resource + + Returns: + object: An instance of Ntp + """ + return Ntp(node) diff --git a/test/fixtures/running_config.text b/test/fixtures/running_config.text index 0a6666e..d0ea092 100644 --- a/test/fixtures/running_config.text +++ b/test/fixtures/running_config.text @@ -241,6 +241,12 @@ ip host puppet 192.168.1.130 ! no ntp trusted-key no ntp authenticate +ntp source Loopback1 +ntp server 1.2.3.4 prefer +ntp server 10.20.30.40 +ntp server 11.22.33.44 +ntp server 123.33.22.11 prefer +ntp server 123.44.55.66 ntp server 192.168.1.32 iburst no ntp serve all ! diff --git a/test/system/test_api_ntp.py b/test/system/test_api_ntp.py new file mode 100644 index 0000000..c33060f --- /dev/null +++ b/test/system/test_api_ntp.py @@ -0,0 +1,157 @@ +# +# Copyright (c) 2015, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +import os +import unittest +import itertools + +import sys +sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) + +from systestlib import DutSystemTest + + +class TestApiNtp(DutSystemTest): + + def test_get(self): + for dut in self.duts: + dut.config(['ntp source Ethernet1', 'ntp server 99.99.1.1']) + response = dut.api('ntp').get() + self.assertIsNotNone(response) + + def test_create(self): + intf = 'Ethernet1' + for dut in self.duts: + dut.config(['no ntp source']) + response = dut.api('ntp').create(intf) + self.assertTrue(response) + response = dut.api('ntp').get() + self.assertEqual(response['source_interface'], intf) + + def test_delete(self): + for dut in self.duts: + dut.config(['ntp source Ethernet1']) + response = dut.api('ntp').delete() + self.assertTrue(response) + response = dut.api('ntp').get() + self.assertIsNone(response['source_interface']) + + def test_default(self): + for dut in self.duts: + dut.config(['ntp source Ethernet1']) + response = dut.api('ntp').default() + self.assertTrue(response) + response = dut.api('ntp').get() + self.assertIsNone(response['source_interface']) + + def test_set_source_interface(self): + intf = 'Ethernet1' + for dut in self.duts: + dut.config(['ntp source Loopback0']) + response = dut.api('ntp').set_source_interface(intf) + self.assertTrue(response) + response = dut.api('ntp').get() + self.assertEqual(response['source_interface'], intf) + + def test_add_server_single(self): + server = '10.10.10.35' + for dut in self.duts: + dut.config(['ntp source Ethernet1', 'no ntp']) + response = dut.api('ntp').add_server(server) + self.assertTrue(response) + response = dut.api('ntp').get() + keys = [x.keys() for x in response['servers']] + keys = list(itertools.chain.from_iterable(keys)) + self.assertListEqual(keys, [server]) + + def test_add_server_multiple(self): + servers = ['10.10.10.37', '10.10.10.36', '10.10.10.34'] + for dut in self.duts: + dut.config(['ntp source Ethernet1', 'no ntp']) + for server in servers: + response = dut.api('ntp').add_server(server) + self.assertTrue(response) + response = dut.api('ntp').get() + keys = [x.keys() for x in response['servers']] + keys = list(itertools.chain.from_iterable(keys)) + self.assertListEqual(sorted(keys), sorted(servers)) + + def test_add_server_prefer(self): + server = '10.10.10.35' + for dut in self.duts: + dut.config(['ntp source Ethernet1', 'no ntp']) + response = dut.api('ntp').add_server(server, prefer=False) + self.assertTrue(response) + response = dut.api('ntp').get() + self.assertIsNone(response['servers'][0][server]) + + response = dut.api('ntp').add_server(server, prefer=True) + self.assertTrue(response) + response = dut.api('ntp').get() + self.assertEqual(response['servers'][0][server], 'prefer') + + def test_add_server_invalid(self): + for dut in self.duts: + dut.config(['ntp source Ethernet1', 'no ntp']) + with self.assertRaises(ValueError): + dut.api('ntp').add_server(None) + dut.api('ntp').add_server('') + dut.api('ntp').add_server(' ') + + def test_remove_server(self): + server = '10.10.10.35' + servers = ['10.10.10.37', '10.10.10.36', '10.10.10.34'] + for dut in self.duts: + dut.config(['ntp source Ethernet1', 'no ntp', + 'ntp server %s' % server]) + for addserver in servers: + dut.config(['ntp server %s' % addserver]) + response = dut.api('ntp').remove_server(server) + self.assertTrue(response) + response = dut.api('ntp').get() + keys = [x.keys() for x in response['servers']] + keys = list(itertools.chain.from_iterable(keys)) + self.assertListEqual(sorted(keys), sorted(servers)) + + def test_remove_all_servers(self): + servers = ['10.10.10.37', '10.10.10.36', '10.10.10.34'] + for dut in self.duts: + dut.config(['ntp source Ethernet1', 'no ntp']) + for addserver in servers: + dut.config(['ntp server %s' % addserver]) + response = dut.api('ntp').remove_all_servers() + self.assertTrue(response) + response = dut.api('ntp').get() + self.assertEqual(response['servers'], []) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/unit/test_api_ntp.py b/test/unit/test_api_ntp.py new file mode 100644 index 0000000..b5b0226 --- /dev/null +++ b/test/unit/test_api_ntp.py @@ -0,0 +1,118 @@ +# +# Copyright (c) 2015, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +import sys +import os +import unittest + +sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) + +from testlib import get_fixture, function +from testlib import EapiConfigUnitTest + +import pyeapi.api.ntp + + +class TestApiNtp(EapiConfigUnitTest): + + def __init__(self, *args, **kwargs): + super(TestApiNtp, self).__init__(*args, **kwargs) + self.instance = pyeapi.api.ntp.Ntp(None) + self.config = open(get_fixture('running_config.text')).read() + + def test_instance(self): + result = pyeapi.api.ntp.instance(None) + self.assertIsInstance(result, pyeapi.api.ntp.Ntp) + + def test_get(self): + result = self.instance.get() + ntp = {'servers': [{'1.2.3.4': 'prefer'}, + {'10.20.30.40': None}, + {'11.22.33.44': None}, + {'123.33.22.11': 'prefer'}, + {'123.44.55.66': None}, + {'joe': None}], + 'source_interface': 'Loopback1'} + keys = ['source_interface', 'servers'] + self.assertEqual(sorted(keys), sorted(result.keys())) + self.assertEqual(ntp['source_interface'], result['source_interface']) + self.assertIsNotNone(result['servers']) + + def test_create(self): + cmd = 'ntp source Ethernet2' + func = function('create', 'Ethernet2') + self.eapi_positive_config_test(func, cmd) + + def test_delete(self): + cmd = 'no ntp source' + func = function('delete') + self.eapi_positive_config_test(func, cmd) + + def test_default(self): + cmd = 'default ntp source' + func = function('default') + self.eapi_positive_config_test(func, cmd) + + def test_set_source_interface(self): + cmd = 'ntp source Vlan50' + func = function('set_source_interface', 'Vlan50') + self.eapi_positive_config_test(func, cmd) + + def test_add_server(self): + cmd = 'ntp server 1.1.1.1' + func = function('add_server', '1.1.1.1') + self.eapi_positive_config_test(func, cmd) + + def test_add_server_prefer(self): + cmd = 'ntp server 1.1.1.1 prefer' + func = function('add_server', '1.1.1.1', prefer=True) + self.eapi_positive_config_test(func, cmd) + + def test_add_server_invalid(self): + func = function('add_server', '', prefer=True) + self.eapi_exception_config_test(func, ValueError) + + func = function('add_server', ' ', prefer=True) + self.eapi_exception_config_test(func, ValueError) + + def test_remove_server(self): + cmd = 'no ntp server 1.1.1.1' + func = function('remove_server', '1.1.1.1') + self.eapi_positive_config_test(func, cmd) + + def test_remove_all_servers(self): + cmd = 'no ntp' + func = function('remove_all_servers') + self.eapi_positive_config_test(func, cmd) + + +if __name__ == '__main__': + unittest.main() From 9dd5cc6808dd4cb2303331b23c9e3afc6e90a4fe Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Mon, 14 Dec 2015 16:36:55 -0700 Subject: [PATCH 4/5] fix error string --- pyeapi/api/bgp.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyeapi/api/bgp.py b/pyeapi/api/bgp.py index 0bfa62e..54e74f9 100644 --- a/pyeapi/api/bgp.py +++ b/pyeapi/api/bgp.py @@ -162,7 +162,8 @@ def set_shutdown(self, default=False, disable=True): def add_network(self, prefix, length, route_map=None): if prefix == '' or length == '': - raise ValueError('network prefix/length values may not be null') + raise ValueError('network prefix and length values ' + 'may not be empty') cmd = 'network {}/{}'.format(prefix, length) if route_map: cmd += ' route-map {}'.format(route_map) @@ -170,7 +171,8 @@ def add_network(self, prefix, length, route_map=None): def remove_network(self, prefix, masklen, route_map=None): if prefix == '' or masklen == '': - raise ValueError('network prefix/masklen values may not be null') + raise ValueError('network prefix and length values ' + 'may not be empty') cmd = 'no network {}/{}'.format(prefix, masklen) if route_map: cmd += ' route-map {}'.format(route_map) From ce506dc5e4b63f4f328b134901a8f60a77a86198 Mon Sep 17 00:00:00 2001 From: Gary Rybak Date: Mon, 14 Dec 2015 16:46:27 -0700 Subject: [PATCH 5/5] fix docstring --- pyeapi/api/ntp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyeapi/api/ntp.py b/pyeapi/api/ntp.py index d7160d4..d4a4ae4 100644 --- a/pyeapi/api/ntp.py +++ b/pyeapi/api/ntp.py @@ -53,7 +53,7 @@ def __init__(self, *args, **kwargs): def get(self): """Returns the current NTP configuration - The Ntp resourc returns the following: + The Ntp resource returns the following: * source_interface (str): The interface port that specifies NTP server @@ -65,7 +65,7 @@ def get(self): Returns: A Python dictionary object of key/value pairs that represents - the current NTP configuration of the node. + the current NTP configuration of the node:: { "source_interface": 'Loopback0',