From c82344de5e56a48f39f057b5676b2870a870a124 Mon Sep 17 00:00:00 2001 From: Darren Hoyland Date: Thu, 5 Oct 2017 10:52:38 +0100 Subject: [PATCH 1/5] Workaround for netmiko not allowing the config mode command to be set (tmp until patch approved upstream) --- .gitignore | 1 + .../devices/netmiko_devices/__init__.py | 49 ++++++++++++++++++- .../tests/unit/netmiko/test_netmiko_base.py | 1 + 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 45b35e56..a7c3ba77 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ develop-eggs .venv .coverage cover +.stestr # Others .*.swp diff --git a/networking_generic_switch/devices/netmiko_devices/__init__.py b/networking_generic_switch/devices/netmiko_devices/__init__.py index 5db17fca..61916713 100644 --- a/networking_generic_switch/devices/netmiko_devices/__init__.py +++ b/networking_generic_switch/devices/netmiko_devices/__init__.py @@ -14,6 +14,7 @@ import atexit import contextlib +import time import uuid import netmiko @@ -23,14 +24,53 @@ import tenacity from tooz import coordination +from netmiko.py23_compat import string_types + from networking_generic_switch import devices from networking_generic_switch import exceptions as exc from networking_generic_switch import locking as ngs_lock + LOG = logging.getLogger(__name__) CONF = cfg.CONF +def send_config_set(net_connect, config_commands=None, exit_config_mode=True, + delay_factor=1, max_loops=150, strip_prompt=False, + strip_command=False, config_mode_cmd=''): + """Temporarily overriding the send_config_set method from netmiko. + + Temporarily overriding the send_config_set method from netmiko, until + upstream patch is accepted: + + https://github.com/ktbyers/netmiko/pull/593 + + """ + + delay_factor = net_connect.select_delay_factor(delay_factor) + if config_commands is None: + return '' + elif isinstance(config_commands, string_types): + config_commands = (config_commands,) + + if not hasattr(config_commands, '__iter__'): + raise ValueError("Invalid argument passed into send_config_set") + + # Send config commands + output = net_connect.config_mode(config_mode_cmd) + for cmd in config_commands: + net_connect.write_channel(net_connect.normalize_cmd(cmd)) + time.sleep(delay_factor * .5) + + # Gather output + output += net_connect._read_channel_timing( + delay_factor=delay_factor, max_loops=max_loops) + if exit_config_mode: + output += net_connect.exit_config_mode() + output = net_connect._sanitize_output(output) + return output + + class NetmikoSwitch(devices.GenericSwitchDevice): ADD_NETWORK = None @@ -136,7 +176,14 @@ def send_commands_to_device(self, cmd_set): with ngs_lock.PoolLock(self.locker, **self.lock_kwargs): with self._get_connection() as net_connect: net_connect.enable() - output = net_connect.send_config_set( + + # output = net_connect.send_config_set( + # config_mode_cmd='configure private', + # config_commands=cmd_set) + # FIXME: use the above, commented out version of + # send_config_set once netmiko patch is accepted upstream. + output = send_config_set( + net_connect, config_mode_cmd='configure private', config_commands=cmd_set) # NOTE (vsaienko) always save configuration # when configuration is applied successfully. diff --git a/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py b/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py index fb688439..6a75bd86 100644 --- a/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py +++ b/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py @@ -185,6 +185,7 @@ def test_switch_send_commands_with_coordinator(self, get_coord_mock, connect_mock = mock.MagicMock(SAVE_CONFIGURATION=None) connect_mock.__enter__.return_value = connect_mock nm_mock.return_value = connect_mock + lock_mock.return_value.__enter__.return_value = lock_mock switch.send_commands_to_device(['spam ham']) From 7e3b658ecf1d48e7176d58b26a70082c2d2227bb Mon Sep 17 00:00:00 2001 From: Darren Hoyland Date: Thu, 5 Oct 2017 13:37:09 +0100 Subject: [PATCH 2/5] made juniper specific --- .../devices/netmiko_devices/__init__.py | 49 ++----------------- .../devices/netmiko_devices/juniper.py | 39 +++++++++++++++ 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/networking_generic_switch/devices/netmiko_devices/__init__.py b/networking_generic_switch/devices/netmiko_devices/__init__.py index 61916713..5d8fdcdc 100644 --- a/networking_generic_switch/devices/netmiko_devices/__init__.py +++ b/networking_generic_switch/devices/netmiko_devices/__init__.py @@ -35,42 +35,6 @@ CONF = cfg.CONF -def send_config_set(net_connect, config_commands=None, exit_config_mode=True, - delay_factor=1, max_loops=150, strip_prompt=False, - strip_command=False, config_mode_cmd=''): - """Temporarily overriding the send_config_set method from netmiko. - - Temporarily overriding the send_config_set method from netmiko, until - upstream patch is accepted: - - https://github.com/ktbyers/netmiko/pull/593 - - """ - - delay_factor = net_connect.select_delay_factor(delay_factor) - if config_commands is None: - return '' - elif isinstance(config_commands, string_types): - config_commands = (config_commands,) - - if not hasattr(config_commands, '__iter__'): - raise ValueError("Invalid argument passed into send_config_set") - - # Send config commands - output = net_connect.config_mode(config_mode_cmd) - for cmd in config_commands: - net_connect.write_channel(net_connect.normalize_cmd(cmd)) - time.sleep(delay_factor * .5) - - # Gather output - output += net_connect._read_channel_timing( - delay_factor=delay_factor, max_loops=max_loops) - if exit_config_mode: - output += net_connect.exit_config_mode() - output = net_connect._sanitize_output(output) - return output - - class NetmikoSwitch(devices.GenericSwitchDevice): ADD_NETWORK = None @@ -167,6 +131,9 @@ def _create_connection(): with net_connect: yield net_connect + def send_config_set(self, net_connect, config_commands): + return net_connect.send_config_set(config_commands=config_commands) + def send_commands_to_device(self, cmd_set): if not cmd_set: LOG.debug("Nothing to execute") @@ -177,14 +144,8 @@ def send_commands_to_device(self, cmd_set): with self._get_connection() as net_connect: net_connect.enable() - # output = net_connect.send_config_set( - # config_mode_cmd='configure private', - # config_commands=cmd_set) - # FIXME: use the above, commented out version of - # send_config_set once netmiko patch is accepted upstream. - output = send_config_set( - net_connect, config_mode_cmd='configure private', - config_commands=cmd_set) + output = self.send_config_set( + net_connect, config_commands=cmd_set) # NOTE (vsaienko) always save configuration # when configuration is applied successfully. self.save_configuration(net_connect) diff --git a/networking_generic_switch/devices/netmiko_devices/juniper.py b/networking_generic_switch/devices/netmiko_devices/juniper.py index a64e46ad..378fbdd6 100644 --- a/networking_generic_switch/devices/netmiko_devices/juniper.py +++ b/networking_generic_switch/devices/netmiko_devices/juniper.py @@ -11,6 +11,9 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +import time + +from netmiko.py23_compat import string_types from networking_generic_switch.devices import netmiko_devices @@ -49,6 +52,42 @@ class Juniper(netmiko_devices.NetmikoSwitch): 'vlan members {segmentation_id}', ) + def send_config_set(self, net_connect, config_commands=None, + exit_config_mode=True, delay_factor=1, max_loops=150, + strip_prompt=False, strip_command=False, + config_mode_cmd='configure private'): + """Temporarily overriding the send_config_set method from netmiko. + + Temporarily overriding the send_config_set method from netmiko, until + upstream patch is accepted: + + https://github.com/ktbyers/netmiko/pull/593 + + """ + + delay_factor = net_connect.select_delay_factor(delay_factor) + if config_commands is None: + return '' + elif isinstance(config_commands, string_types): + config_commands = (config_commands,) + + if not hasattr(config_commands, '__iter__'): + raise ValueError("Invalid argument passed into send_config_set") + + # Send config commands + output = net_connect.config_mode(config_mode_cmd) + for cmd in config_commands: + net_connect.write_channel(net_connect.normalize_cmd(cmd)) + time.sleep(delay_factor * .5) + + # Gather output + output += net_connect._read_channel_timing( + delay_factor=delay_factor, max_loops=max_loops) + if exit_config_mode: + output += net_connect.exit_config_mode() + output = net_connect._sanitize_output(output) + return output + def save_configuration(self, net_connect): """Save the device's configuration. From 54f8727380cd68d867e7349d94b00770c8656341 Mon Sep 17 00:00:00 2001 From: Darren Hoyland Date: Thu, 5 Oct 2017 13:47:08 +0100 Subject: [PATCH 3/5] lint and fixes --- .../devices/netmiko_devices/__init__.py | 5 ----- .../tests/unit/netmiko/test_netmiko_base.py | 1 - 2 files changed, 6 deletions(-) diff --git a/networking_generic_switch/devices/netmiko_devices/__init__.py b/networking_generic_switch/devices/netmiko_devices/__init__.py index 5d8fdcdc..e0d16de3 100644 --- a/networking_generic_switch/devices/netmiko_devices/__init__.py +++ b/networking_generic_switch/devices/netmiko_devices/__init__.py @@ -14,7 +14,6 @@ import atexit import contextlib -import time import uuid import netmiko @@ -24,13 +23,10 @@ import tenacity from tooz import coordination -from netmiko.py23_compat import string_types - from networking_generic_switch import devices from networking_generic_switch import exceptions as exc from networking_generic_switch import locking as ngs_lock - LOG = logging.getLogger(__name__) CONF = cfg.CONF @@ -143,7 +139,6 @@ def send_commands_to_device(self, cmd_set): with ngs_lock.PoolLock(self.locker, **self.lock_kwargs): with self._get_connection() as net_connect: net_connect.enable() - output = self.send_config_set( net_connect, config_commands=cmd_set) # NOTE (vsaienko) always save configuration diff --git a/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py b/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py index 6a75bd86..fb688439 100644 --- a/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py +++ b/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py @@ -185,7 +185,6 @@ def test_switch_send_commands_with_coordinator(self, get_coord_mock, connect_mock = mock.MagicMock(SAVE_CONFIGURATION=None) connect_mock.__enter__.return_value = connect_mock nm_mock.return_value = connect_mock - lock_mock.return_value.__enter__.return_value = lock_mock switch.send_commands_to_device(['spam ham']) From e71a93136d083b77074237f00054cff592496b07 Mon Sep 17 00:00:00 2001 From: Darren Hoyland Date: Thu, 5 Oct 2017 14:51:33 +0100 Subject: [PATCH 4/5] lint --- networking_generic_switch/tests/unit/netmiko/test_juniper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/networking_generic_switch/tests/unit/netmiko/test_juniper.py b/networking_generic_switch/tests/unit/netmiko/test_juniper.py index 3426e411..eeda6fa7 100644 --- a/networking_generic_switch/tests/unit/netmiko/test_juniper.py +++ b/networking_generic_switch/tests/unit/netmiko/test_juniper.py @@ -51,7 +51,8 @@ def test_add_network_with_trunk_ports(self, mock_exec): 'NetmikoSwitch.send_commands_to_device') def test_del_network(self, mock_exec): self.switch.del_network(33, '0ae071f55be943e480eae41fefe85b21') - mock_exec.assert_called_with(['delete vlans 0ae071f55be943e480eae41fefe85b21']) + mock_exec.assert_called_with([ + 'delete vlans 0ae071f55be943e480eae41fefe85b21']) @mock.patch('networking_generic_switch.devices.netmiko_devices.' 'NetmikoSwitch.send_commands_to_device') From 0143af639167814dea55f3bcc87926299048f02c Mon Sep 17 00:00:00 2001 From: Darren Hoyland Date: Thu, 5 Oct 2017 16:03:33 +0100 Subject: [PATCH 5/5] lint --- networking_generic_switch/devices/netmiko_devices/__init__.py | 2 +- networking_generic_switch/devices/netmiko_devices/juniper.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/networking_generic_switch/devices/netmiko_devices/__init__.py b/networking_generic_switch/devices/netmiko_devices/__init__.py index e0d16de3..045546b7 100644 --- a/networking_generic_switch/devices/netmiko_devices/__init__.py +++ b/networking_generic_switch/devices/netmiko_devices/__init__.py @@ -140,7 +140,7 @@ def send_commands_to_device(self, cmd_set): with self._get_connection() as net_connect: net_connect.enable() output = self.send_config_set( - net_connect, config_commands=cmd_set) + net_connect, config_commands=cmd_set) # NOTE (vsaienko) always save configuration # when configuration is applied successfully. self.save_configuration(net_connect) diff --git a/networking_generic_switch/devices/netmiko_devices/juniper.py b/networking_generic_switch/devices/netmiko_devices/juniper.py index 378fbdd6..a8827c06 100644 --- a/networking_generic_switch/devices/netmiko_devices/juniper.py +++ b/networking_generic_switch/devices/netmiko_devices/juniper.py @@ -13,6 +13,8 @@ # under the License. import time +import tenacity + from netmiko.py23_compat import string_types from networking_generic_switch.devices import netmiko_devices @@ -52,6 +54,8 @@ class Juniper(netmiko_devices.NetmikoSwitch): 'vlan members {segmentation_id}', ) + @tenacity.retry(reraise=True, stop=tenacity.stop_after_attempt(3), + wait=tenacity.wait_fixed(14)) def send_config_set(self, net_connect, config_commands=None, exit_config_mode=True, delay_factor=1, max_loops=150, strip_prompt=False, strip_command=False,