summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Goddard <mark@stackhpc.com>2018-07-28 10:16:46 +0100
committerMark Goddard <mark@stackhpc.com>2018-08-08 17:02:06 +0100
commitd5f7ab31215dbae0d8ef262df502658922db2e9a (patch)
tree0481df041a14ee02c5b8b66995b623f4d5f0d5ef
parent451953c9dc3bf140dda10062da15ef0ec14974fb (diff)
Detection of config errors for netmiko drivers
TODO: Do we need to support marking errors as retryable, and adding a retry mechanism for this case? NGS currently provides very little support for verifying that the required configuration has been applied to a device. Commands are formatted and sent to a device, but no validation is performed on the response from the device. There are times when applying configuration can fail, such as story 1737017, but this error is silently ignored. This changes adds a simple error detection framework to the NGS netmiko-based drivers. Command line error signatures are inherently device-specific, but the framework introduced here is common. A check_output method is added to the base Netmiko driver, which compares the response from a device with a list of error message regular expressions in self.ERROR_MSG_PATTERNS. Device drivers should populate the patterns. On detection of an error, a GenericSwitchNetmikoConfigError exception is raised. Change-Id: Id7e3c1ed0c4f78d35c9662c9c52e12519905dfb5 Story: 2003148 Task: 23283
Notes
Notes (review): Code-Review+1: Kaifeng Wang <kaifeng.w@gmail.com> Code-Review+2: Julia Kreger <juliaashleykreger@gmail.com> Code-Review+2: Dmitry Tantsur <divius.inside@gmail.com> Workflow+1: Dmitry Tantsur <divius.inside@gmail.com> Verified+2: Zuul Submitted-by: Zuul Submitted-at: Tue, 14 Aug 2018 19:12:05 +0000 Reviewed-on: https://review.openstack.org/589960 Project: openstack/networking-generic-switch Branch: refs/heads/master
-rw-r--r--networking_generic_switch/devices/netmiko_devices/__init__.py44
-rw-r--r--networking_generic_switch/devices/netmiko_devices/brocade.py6
-rw-r--r--networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py80
3 files changed, 106 insertions, 24 deletions
diff --git a/networking_generic_switch/devices/netmiko_devices/__init__.py b/networking_generic_switch/devices/netmiko_devices/__init__.py
index 1d069b7..6279878 100644
--- a/networking_generic_switch/devices/netmiko_devices/__init__.py
+++ b/networking_generic_switch/devices/netmiko_devices/__init__.py
@@ -48,6 +48,13 @@ class NetmikoSwitch(devices.GenericSwitchDevice):
48 48
49 SAVE_CONFIGURATION = None 49 SAVE_CONFIGURATION = None
50 50
51 ERROR_MSG_PATTERNS = ()
52 """Sequence of error message patterns.
53
54 Sequence of re.RegexObject objects representing patterns to check for in
55 device output that indicate a failure to apply configuration.
56 """
57
51 def __init__(self, device_cfg): 58 def __init__(self, device_cfg):
52 super(NetmikoSwitch, self).__init__(device_cfg) 59 super(NetmikoSwitch, self).__init__(device_cfg)
53 device_type = self.config.get('device_type', '') 60 device_type = self.config.get('device_type', '')
@@ -162,7 +169,8 @@ class NetmikoSwitch(devices.GenericSwitchDevice):
162 cmds += self._format_commands(self.ADD_NETWORK_TO_TRUNK, 169 cmds += self._format_commands(self.ADD_NETWORK_TO_TRUNK,
163 port=port, 170 port=port,
164 segmentation_id=segmentation_id) 171 segmentation_id=segmentation_id)
165 self.send_commands_to_device(cmds) 172 output = self.send_commands_to_device(cmds)
173 self.check_output(output, 'add network')
166 174
167 def del_network(self, segmentation_id, network_id): 175 def del_network(self, segmentation_id, network_id):
168 # NOTE(zhenguo): Remove dashes from uuid as on most devices 32 chars 176 # NOTE(zhenguo): Remove dashes from uuid as on most devices 32 chars
@@ -176,7 +184,8 @@ class NetmikoSwitch(devices.GenericSwitchDevice):
176 cmds += self._format_commands(self.DELETE_NETWORK, 184 cmds += self._format_commands(self.DELETE_NETWORK,
177 segmentation_id=segmentation_id, 185 segmentation_id=segmentation_id,
178 network_id=network_id) 186 network_id=network_id)
179 self.send_commands_to_device(cmds) 187 output = self.send_commands_to_device(cmds)
188 self.check_output(output, 'delete network')
180 189
181 def plug_port_to_network(self, port, segmentation_id): 190 def plug_port_to_network(self, port, segmentation_id):
182 cmds = [] 191 cmds = []
@@ -190,7 +199,8 @@ class NetmikoSwitch(devices.GenericSwitchDevice):
190 self.PLUG_PORT_TO_NETWORK, 199 self.PLUG_PORT_TO_NETWORK,
191 port=port, 200 port=port,
192 segmentation_id=segmentation_id) 201 segmentation_id=segmentation_id)
193 self.send_commands_to_device(cmds) 202 output = self.send_commands_to_device(cmds)
203 self.check_output(output, 'plug port')
194 204
195 def delete_port(self, port, segmentation_id): 205 def delete_port(self, port, segmentation_id):
196 cmds = self._format_commands(self.DELETE_PORT, 206 cmds = self._format_commands(self.DELETE_PORT,
@@ -206,7 +216,8 @@ class NetmikoSwitch(devices.GenericSwitchDevice):
206 self.PLUG_PORT_TO_NETWORK, 216 self.PLUG_PORT_TO_NETWORK,
207 port=port, 217 port=port,
208 segmentation_id=ngs_port_default_vlan) 218 segmentation_id=ngs_port_default_vlan)
209 self.send_commands_to_device(cmds) 219 output = self.send_commands_to_device(cmds)
220 self.check_output(output, 'unplug port')
210 221
211 def send_config_set(self, net_connect, cmd_set): 222 def send_config_set(self, net_connect, cmd_set):
212 """Send a set of configuration lines to the device. 223 """Send a set of configuration lines to the device.
@@ -233,3 +244,28 @@ class NetmikoSwitch(devices.GenericSwitchDevice):
233 LOG.warning("Saving config is not supported for %s," 244 LOG.warning("Saving config is not supported for %s,"
234 " all changes will be lost after switch" 245 " all changes will be lost after switch"
235 " reboot", self.config['device_type']) 246 " reboot", self.config['device_type'])
247
248 def check_output(self, output, operation):
249 """Check the output from the device following an operation.
250
251 Drivers should implement this method to handle output from devices and
252 perform any checks necessary to validate that the configuration was
253 applied successfully.
254
255 :param output: Output from the device.
256 :param operation: Operation being attempted. One of 'add network',
257 'delete network', 'plug port', 'unplug port'.
258 :raises: GenericSwitchNetmikoConfigError if the driver detects that an
259 error has occurred.
260 """
261 if not output:
262 return
263
264 for pattern in self.ERROR_MSG_PATTERNS:
265 if pattern.search(output):
266 msg = ("Found invalid configuration in device response. "
267 "Operation: %(operation)s. Output: %(output)s" %
268 {'operation': operation, 'output': output})
269 raise exc.GenericSwitchNetmikoConfigError(
270 config=device_utils.sanitise_config(self.config),
271 error=msg)
diff --git a/networking_generic_switch/devices/netmiko_devices/brocade.py b/networking_generic_switch/devices/netmiko_devices/brocade.py
index 69dbb4b..24baf0c 100644
--- a/networking_generic_switch/devices/netmiko_devices/brocade.py
+++ b/networking_generic_switch/devices/netmiko_devices/brocade.py
@@ -76,7 +76,5 @@ class BrocadeFastIron(netmiko_devices.NetmikoSwitch):
76 76
77 def plug_port_to_network(self, port, segmentation_id): 77 def plug_port_to_network(self, port, segmentation_id):
78 self.clean_port_vlan_if_necessary(port) 78 self.clean_port_vlan_if_necessary(port)
79 self.send_commands_to_device( 79 super(BrocadeFastIron, self).plug_port_to_network(port,
80 self._format_commands(self.PLUG_PORT_TO_NETWORK, 80 segmentation_id)
81 port=port,
82 segmentation_id=segmentation_id))
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 adb47ab..b1fb934 100644
--- a/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py
+++ b/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py
@@ -12,6 +12,8 @@
12# License for the specific language governing permissions and limitations 12# License for the specific language governing permissions and limitations
13# under the License. 13# under the License.
14 14
15import re
16
15import fixtures 17import fixtures
16import mock 18import mock
17import netmiko 19import netmiko
@@ -45,56 +47,88 @@ class NetmikoSwitchTestBase(fixtures.TestWithFixtures):
45class TestNetmikoSwitch(NetmikoSwitchTestBase): 47class TestNetmikoSwitch(NetmikoSwitchTestBase):
46 48
47 @mock.patch('networking_generic_switch.devices.netmiko_devices.' 49 @mock.patch('networking_generic_switch.devices.netmiko_devices.'
48 'NetmikoSwitch.send_commands_to_device') 50 'NetmikoSwitch.send_commands_to_device',
49 def test_add_network(self, m_sctd): 51 return_value='fake output')
52 @mock.patch('networking_generic_switch.devices.netmiko_devices.'
53 'NetmikoSwitch.check_output')
54 def test_add_network(self, m_check, m_sctd):
50 self.switch.add_network(22, '0ae071f5-5be9-43e4-80ea-e41fefe85b21') 55 self.switch.add_network(22, '0ae071f5-5be9-43e4-80ea-e41fefe85b21')
51 m_sctd.assert_called_with([]) 56 m_sctd.assert_called_with([])
57 m_check.assert_called_once_with('fake output', 'add network')
52 58
53 @mock.patch('networking_generic_switch.devices.netmiko_devices.' 59 @mock.patch('networking_generic_switch.devices.netmiko_devices.'
54 'NetmikoSwitch.send_commands_to_device') 60 'NetmikoSwitch.send_commands_to_device',
55 def test_add_network_with_trunk_ports(self, m_sctd): 61 return_value='fake output')
62 @mock.patch('networking_generic_switch.devices.netmiko_devices.'
63 'NetmikoSwitch.check_output')
64 def test_add_network_with_trunk_ports(self, m_check, m_sctd):
56 switch = self._make_switch_device({'ngs_trunk_ports': 'port1,port2'}) 65 switch = self._make_switch_device({'ngs_trunk_ports': 'port1,port2'})
57 switch.add_network(22, '0ae071f5-5be9-43e4-80ea-e41fefe85b21') 66 switch.add_network(22, '0ae071f5-5be9-43e4-80ea-e41fefe85b21')
58 m_sctd.assert_called_with([]) 67 m_sctd.assert_called_with([])
68 m_check.assert_called_once_with('fake output', 'add network')
59 69
60 @mock.patch('networking_generic_switch.devices.netmiko_devices.' 70 @mock.patch('networking_generic_switch.devices.netmiko_devices.'
61 'NetmikoSwitch.send_commands_to_device') 71 'NetmikoSwitch.send_commands_to_device',
62 def test_del_network(self, m_sctd): 72 return_value='fake output')
73 @mock.patch('networking_generic_switch.devices.netmiko_devices.'
74 'NetmikoSwitch.check_output')
75 def test_del_network(self, m_check, m_sctd):
63 self.switch.del_network(22, '0ae071f5-5be9-43e4-80ea-e41fefe85b21') 76 self.switch.del_network(22, '0ae071f5-5be9-43e4-80ea-e41fefe85b21')
64 m_sctd.assert_called_with([]) 77 m_sctd.assert_called_with([])
78 m_check.assert_called_once_with('fake output', 'delete network')
65 79
66 @mock.patch('networking_generic_switch.devices.netmiko_devices.' 80 @mock.patch('networking_generic_switch.devices.netmiko_devices.'
67 'NetmikoSwitch.send_commands_to_device') 81 'NetmikoSwitch.send_commands_to_device',
68 def test_del_network_with_trunk_ports(self, m_sctd): 82 return_value='fake output')
83 @mock.patch('networking_generic_switch.devices.netmiko_devices.'
84 'NetmikoSwitch.check_output')
85 def test_del_network_with_trunk_ports(self, m_check, m_sctd):
69 switch = self._make_switch_device({'ngs_trunk_ports': 'port1,port2'}) 86 switch = self._make_switch_device({'ngs_trunk_ports': 'port1,port2'})
70 switch.del_network(22, '0ae071f5-5be9-43e4-80ea-e41fefe85b21') 87 switch.del_network(22, '0ae071f5-5be9-43e4-80ea-e41fefe85b21')
71 m_sctd.assert_called_with([]) 88 m_sctd.assert_called_with([])
89 m_check.assert_called_once_with('fake output', 'delete network')
72 90
73 @mock.patch('networking_generic_switch.devices.netmiko_devices.' 91 @mock.patch('networking_generic_switch.devices.netmiko_devices.'
74 'NetmikoSwitch.send_commands_to_device') 92 'NetmikoSwitch.send_commands_to_device',
75 def test_plug_port_to_network(self, m_sctd): 93 return_value='fake output')
94 @mock.patch('networking_generic_switch.devices.netmiko_devices.'
95 'NetmikoSwitch.check_output')
96 def test_plug_port_to_network(self, m_check, m_sctd):
76 self.switch.plug_port_to_network(2222, 22) 97 self.switch.plug_port_to_network(2222, 22)
77 m_sctd.assert_called_with([]) 98 m_sctd.assert_called_with([])
99 m_check.assert_called_once_with('fake output', 'plug port')
78 100
79 @mock.patch('networking_generic_switch.devices.netmiko_devices.' 101 @mock.patch('networking_generic_switch.devices.netmiko_devices.'
80 'NetmikoSwitch.send_commands_to_device') 102 'NetmikoSwitch.send_commands_to_device',
81 def test_plug_port_has_default_vlan(self, m_sctd): 103 return_value='fake output')
104 @mock.patch('networking_generic_switch.devices.netmiko_devices.'
105 'NetmikoSwitch.check_output')
106 def test_plug_port_has_default_vlan(self, m_check, m_sctd):
82 switch = self._make_switch_device({'ngs_port_default_vlan': '20'}) 107 switch = self._make_switch_device({'ngs_port_default_vlan': '20'})
83 switch.plug_port_to_network(2222, 22) 108 switch.plug_port_to_network(2222, 22)
84 m_sctd.assert_called_with([]) 109 m_sctd.assert_called_with([])
110 m_check.assert_called_once_with('fake output', 'plug port')
85 111
86 @mock.patch('networking_generic_switch.devices.netmiko_devices.' 112 @mock.patch('networking_generic_switch.devices.netmiko_devices.'
87 'NetmikoSwitch.send_commands_to_device') 113 'NetmikoSwitch.send_commands_to_device',
88 def test_delete_port(self, m_sctd): 114 return_value='fake output')
115 @mock.patch('networking_generic_switch.devices.netmiko_devices.'
116 'NetmikoSwitch.check_output')
117 def test_delete_port(self, m_check, m_sctd):
89 self.switch.delete_port(2222, 22) 118 self.switch.delete_port(2222, 22)
90 m_sctd.assert_called_with([]) 119 m_sctd.assert_called_with([])
120 m_check.assert_called_once_with('fake output', 'unplug port')
91 121
92 @mock.patch('networking_generic_switch.devices.netmiko_devices.' 122 @mock.patch('networking_generic_switch.devices.netmiko_devices.'
93 'NetmikoSwitch.send_commands_to_device') 123 'NetmikoSwitch.send_commands_to_device',
94 def test_delete_port_has_default_vlan(self, m_sctd): 124 return_value='fake output')
125 @mock.patch('networking_generic_switch.devices.netmiko_devices.'
126 'NetmikoSwitch.check_output')
127 def test_delete_port_has_default_vlan(self, m_check, m_sctd):
95 switch = self._make_switch_device({'ngs_port_default_vlan': '20'}) 128 switch = self._make_switch_device({'ngs_port_default_vlan': '20'})
96 switch.delete_port(2222, 22) 129 switch.delete_port(2222, 22)
97 m_sctd.assert_called_with([]) 130 m_sctd.assert_called_with([])
131 m_check.assert_called_once_with('fake output', 'unplug port')
98 132
99 def test__format_commands(self): 133 def test__format_commands(self):
100 self.switch._format_commands( 134 self.switch._format_commands(
@@ -290,3 +324,17 @@ class TestNetmikoSwitch(NetmikoSwitchTestBase):
290 timeout=120) 324 timeout=120)
291 lock_mock.return_value.__exit__.assert_called_once() 325 lock_mock.return_value.__exit__.assert_called_once()
292 lock_mock.return_value.__enter__.assert_called_once() 326 lock_mock.return_value.__enter__.assert_called_once()
327
328 def test_check_output(self):
329 self.switch.check_output('fake output', 'fake op')
330
331 def test_check_output_error(self):
332 self.switch.ERROR_MSG_PATTERNS = (re.compile('fake error message'),)
333 output = """
334fake switch command
335fake error message
336"""
337 msg = ("Found invalid configuration in device response. Operation: "
338 "fake op. Output: %s" % output)
339 self.assertRaisesRegexp(exc.GenericSwitchNetmikoConfigError, msg,
340 self.switch.check_output, output, 'fake op')