Browse Source

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
Mark Goddard 8 months ago
parent
commit
d5f7ab3121

+ 40
- 4
networking_generic_switch/devices/netmiko_devices/__init__.py View File

@@ -48,6 +48,13 @@ class NetmikoSwitch(devices.GenericSwitchDevice):
48 48
 
49 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 58
     def __init__(self, device_cfg):
52 59
         super(NetmikoSwitch, self).__init__(device_cfg)
53 60
         device_type = self.config.get('device_type', '')
@@ -162,7 +169,8 @@ class NetmikoSwitch(devices.GenericSwitchDevice):
162 169
             cmds += self._format_commands(self.ADD_NETWORK_TO_TRUNK,
163 170
                                           port=port,
164 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 175
     def del_network(self, segmentation_id, network_id):
168 176
         # NOTE(zhenguo): Remove dashes from uuid as on most devices 32 chars
@@ -176,7 +184,8 @@ class NetmikoSwitch(devices.GenericSwitchDevice):
176 184
         cmds += self._format_commands(self.DELETE_NETWORK,
177 185
                                       segmentation_id=segmentation_id,
178 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 190
     def plug_port_to_network(self, port, segmentation_id):
182 191
         cmds = []
@@ -190,7 +199,8 @@ class NetmikoSwitch(devices.GenericSwitchDevice):
190 199
             self.PLUG_PORT_TO_NETWORK,
191 200
             port=port,
192 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 205
     def delete_port(self, port, segmentation_id):
196 206
         cmds = self._format_commands(self.DELETE_PORT,
@@ -206,7 +216,8 @@ class NetmikoSwitch(devices.GenericSwitchDevice):
206 216
                 self.PLUG_PORT_TO_NETWORK,
207 217
                 port=port,
208 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 222
     def send_config_set(self, net_connect, cmd_set):
212 223
         """Send a set of configuration lines to the device.
@@ -233,3 +244,28 @@ class NetmikoSwitch(devices.GenericSwitchDevice):
233 244
                 LOG.warning("Saving config is not supported for %s,"
234 245
                             " all changes will be lost after switch"
235 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)

+ 2
- 4
networking_generic_switch/devices/netmiko_devices/brocade.py View File

@@ -76,7 +76,5 @@ class BrocadeFastIron(netmiko_devices.NetmikoSwitch):
76 76
 
77 77
     def plug_port_to_network(self, port, segmentation_id):
78 78
         self.clean_port_vlan_if_necessary(port)
79
-        self.send_commands_to_device(
80
-            self._format_commands(self.PLUG_PORT_TO_NETWORK,
81
-                                  port=port,
82
-                                  segmentation_id=segmentation_id))
79
+        super(BrocadeFastIron, self).plug_port_to_network(port,
80
+                                                          segmentation_id)

+ 64
- 16
networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py View File

@@ -12,6 +12,8 @@
12 12
 #    License for the specific language governing permissions and limitations
13 13
 #    under the License.
14 14
 
15
+import re
16
+
15 17
 import fixtures
16 18
 import mock
17 19
 import netmiko
@@ -45,56 +47,88 @@ class NetmikoSwitchTestBase(fixtures.TestWithFixtures):
45 47
 class TestNetmikoSwitch(NetmikoSwitchTestBase):
46 48
 
47 49
     @mock.patch('networking_generic_switch.devices.netmiko_devices.'
48
-                'NetmikoSwitch.send_commands_to_device')
49
-    def test_add_network(self, m_sctd):
50
+                'NetmikoSwitch.send_commands_to_device',
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 55
         self.switch.add_network(22, '0ae071f5-5be9-43e4-80ea-e41fefe85b21')
51 56
         m_sctd.assert_called_with([])
57
+        m_check.assert_called_once_with('fake output', 'add network')
52 58
 
53 59
     @mock.patch('networking_generic_switch.devices.netmiko_devices.'
54
-                'NetmikoSwitch.send_commands_to_device')
55
-    def test_add_network_with_trunk_ports(self, m_sctd):
60
+                'NetmikoSwitch.send_commands_to_device',
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 65
         switch = self._make_switch_device({'ngs_trunk_ports': 'port1,port2'})
57 66
         switch.add_network(22, '0ae071f5-5be9-43e4-80ea-e41fefe85b21')
58 67
         m_sctd.assert_called_with([])
68
+        m_check.assert_called_once_with('fake output', 'add network')
59 69
 
60 70
     @mock.patch('networking_generic_switch.devices.netmiko_devices.'
61
-                'NetmikoSwitch.send_commands_to_device')
62
-    def test_del_network(self, m_sctd):
71
+                'NetmikoSwitch.send_commands_to_device',
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 76
         self.switch.del_network(22, '0ae071f5-5be9-43e4-80ea-e41fefe85b21')
64 77
         m_sctd.assert_called_with([])
78
+        m_check.assert_called_once_with('fake output', 'delete network')
65 79
 
66 80
     @mock.patch('networking_generic_switch.devices.netmiko_devices.'
67
-                'NetmikoSwitch.send_commands_to_device')
68
-    def test_del_network_with_trunk_ports(self, m_sctd):
81
+                'NetmikoSwitch.send_commands_to_device',
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 86
         switch = self._make_switch_device({'ngs_trunk_ports': 'port1,port2'})
70 87
         switch.del_network(22, '0ae071f5-5be9-43e4-80ea-e41fefe85b21')
71 88
         m_sctd.assert_called_with([])
89
+        m_check.assert_called_once_with('fake output', 'delete network')
72 90
 
73 91
     @mock.patch('networking_generic_switch.devices.netmiko_devices.'
74
-                'NetmikoSwitch.send_commands_to_device')
75
-    def test_plug_port_to_network(self, m_sctd):
92
+                'NetmikoSwitch.send_commands_to_device',
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 97
         self.switch.plug_port_to_network(2222, 22)
77 98
         m_sctd.assert_called_with([])
99
+        m_check.assert_called_once_with('fake output', 'plug port')
78 100
 
79 101
     @mock.patch('networking_generic_switch.devices.netmiko_devices.'
80
-                'NetmikoSwitch.send_commands_to_device')
81
-    def test_plug_port_has_default_vlan(self, m_sctd):
102
+                'NetmikoSwitch.send_commands_to_device',
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 107
         switch = self._make_switch_device({'ngs_port_default_vlan': '20'})
83 108
         switch.plug_port_to_network(2222, 22)
84 109
         m_sctd.assert_called_with([])
110
+        m_check.assert_called_once_with('fake output', 'plug port')
85 111
 
86 112
     @mock.patch('networking_generic_switch.devices.netmiko_devices.'
87
-                'NetmikoSwitch.send_commands_to_device')
88
-    def test_delete_port(self, m_sctd):
113
+                'NetmikoSwitch.send_commands_to_device',
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 118
         self.switch.delete_port(2222, 22)
90 119
         m_sctd.assert_called_with([])
120
+        m_check.assert_called_once_with('fake output', 'unplug port')
91 121
 
92 122
     @mock.patch('networking_generic_switch.devices.netmiko_devices.'
93
-                'NetmikoSwitch.send_commands_to_device')
94
-    def test_delete_port_has_default_vlan(self, m_sctd):
123
+                'NetmikoSwitch.send_commands_to_device',
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 128
         switch = self._make_switch_device({'ngs_port_default_vlan': '20'})
96 129
         switch.delete_port(2222, 22)
97 130
         m_sctd.assert_called_with([])
131
+        m_check.assert_called_once_with('fake output', 'unplug port')
98 132
 
99 133
     def test__format_commands(self):
100 134
         self.switch._format_commands(
@@ -290,3 +324,17 @@ class TestNetmikoSwitch(NetmikoSwitchTestBase):
290 324
                                           timeout=120)
291 325
         lock_mock.return_value.__exit__.assert_called_once()
292 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 = """
334
+fake switch command
335
+fake 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')

Loading…
Cancel
Save