Browse Source

Add a netmiko device driver for Juniper switches

The driver overrides the save_configuration method to support the Junos
transactional configuration model's commit operation.

The private configuration mode is used in order to provide a level of
isolation between sessions, and to ensure that uncommitted changes are
not left on the switch following a failure to commit the configuration.

Configuration errors are handled by ensuring that the commit operation
is successful.

A retry mechanism is used to handle temporary failures due to multiple
sessions attempting to lock the JunOS configuration database
concurrently. The retry mechanism is configured via the configuration
options 'ngs_commit_interval' and 'ngs_commit_timeout'.

Change-Id: I6e82c43887721517b543c54cf8b958b5a918bd1b
Closes-Bug: #1740587
tags/1.2.0
Mark Goddard 1 year ago
parent
commit
7b6d35f1d8

+ 10
- 0
doc/source/configuration.rst View File

@@ -129,6 +129,16 @@ for the HPE 5900 Series device::
129 129
     password = password
130 130
     ip = <switch mgmt ip address>
131 131
 
132
+for the Juniper device::
133
+
134
+    [genericswitch:hostname-for-juniper]
135
+    device_type = netmiko_juniper
136
+    ip = <switch mgmt ip address>
137
+    username = admin
138
+    password = password
139
+    ngs_commit_timeout = <optional commit timeout (seconds)>
140
+    ngs_commit_interval = <optional commit interval (seconds)>
141
+
132 142
 Additionally the ``GenericSwitch`` mechanism driver needs to be enabled from
133 143
 the ml2 config file ``/etc/neutron/plugins/ml2/ml2_conf.ini``::
134 144
 

+ 1
- 0
doc/source/supported-devices.rst View File

@@ -14,6 +14,7 @@ The following devices are supported by this plugin:
14 14
 * Brocade ICX (FastIron)
15 15
 * Ruijie switches
16 16
 * HPE 5900 Series switches
17
+* Juniper Junos
17 18
 
18 19
 This Mechanism Driver architecture allows easily to add more devices
19 20
 of any type.

+ 153
- 0
networking_generic_switch/devices/netmiko_devices/juniper.py View File

@@ -0,0 +1,153 @@
1
+# Copyright (c) 2018 StackHPC Ltd.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+from oslo_log import log as logging
16
+import tenacity
17
+
18
+from networking_generic_switch.devices import netmiko_devices
19
+from networking_generic_switch.devices import utils as device_utils
20
+from networking_generic_switch import exceptions as exc
21
+
22
+LOG = logging.getLogger(__name__)
23
+
24
+# Internal ngs options will not be passed to driver.
25
+JUNIPER_INTERNAL_OPTS = [
26
+    # Timeout (seconds) for committing configuration changes.
27
+    {'name': 'ngs_commit_timeout', 'default': 60},
28
+    # Interval (seconds) between attempts to commit configuration changes.
29
+    {'name': 'ngs_commit_interval', 'default': 5},
30
+]
31
+
32
+
33
+class Juniper(netmiko_devices.NetmikoSwitch):
34
+
35
+    ADD_NETWORK = (
36
+        'set vlans {network_id} vlan-id {segmentation_id}',
37
+    )
38
+
39
+    DELETE_NETWORK = (
40
+        'delete vlans {network_id}',
41
+    )
42
+
43
+    PLUG_PORT_TO_NETWORK = (
44
+        # Delete any existing VLAN associations - only one VLAN may be
45
+        # associated with an access mode port.
46
+        'delete interface {port} unit 0 family ethernet-switching '
47
+        'vlan members',
48
+        'set interface {port} unit 0 family ethernet-switching '
49
+        'vlan members {segmentation_id}',
50
+    )
51
+
52
+    DELETE_PORT = (
53
+        'delete interface {port} unit 0 family ethernet-switching '
54
+        'vlan members',
55
+    )
56
+
57
+    ADD_NETWORK_TO_TRUNK = (
58
+        'set interface {port} unit 0 family ethernet-switching '
59
+        'vlan members {segmentation_id}',
60
+    )
61
+
62
+    REMOVE_NETWORK_FROM_TRUNK = (
63
+        'delete interface {port} unit 0 family ethernet-switching '
64
+        'vlan members {segmentation_id}',
65
+    )
66
+
67
+    def __init__(self, device_cfg):
68
+        super(Juniper, self).__init__(device_cfg)
69
+
70
+        # Do not expose Juniper internal options to device config.
71
+        for opt in JUNIPER_INTERNAL_OPTS:
72
+            opt_name = opt['name']
73
+            if opt_name in self.config:
74
+                self.ngs_config[opt_name] = self.config.pop(opt_name)
75
+            elif 'default' in opt:
76
+                self.ngs_config[opt_name] = opt['default']
77
+
78
+    def send_config_set(self, net_connect, cmd_set):
79
+        """Send a set of configuration lines to the device.
80
+
81
+        :param net_connect: a netmiko connection object.
82
+        :param cmd_set: a list of configuration lines to send.
83
+        :returns: The output of the configuration commands.
84
+        """
85
+        # We use the private configuration mode, which hides the configuration
86
+        # changes of concurrent sessions from us, and discards uncommitted
87
+        # changes on termination of the session. See
88
+        # https://www.juniper.net/documentation/en_US/junos/topics/concept/junos-cli-multiple-users-usage-overview.html.
89
+        net_connect.config_mode(config_command='configure private')
90
+
91
+        # Don't exit configuration mode, as we still need to commit the changes
92
+        # in save_configuration().
93
+        return net_connect.send_config_set(config_commands=cmd_set,
94
+                                           exit_config_mode=False)
95
+
96
+    def save_configuration(self, net_connect):
97
+        """Save the device's configuration.
98
+
99
+        :param net_connect: a netmiko connection object.
100
+        :raises GenericSwitchNetmikoConfigError if saving the configuration
101
+            fails.
102
+        """
103
+        # Junos configuration is transactional, and requires an explicit commit
104
+        # of changes in order for them to be applied. Since committing requires
105
+        # an exclusive lock on the configuration database, it can fail if
106
+        # another session has a lock. We use a retry mechanism to work around
107
+        # this.
108
+
109
+        class DBLocked(Exception):
110
+            """Switch configuration DB is locked by another user."""
111
+
112
+            def __init__(self, err):
113
+                self.err = err
114
+
115
+        @tenacity.retry(
116
+            # Log a message after each failed attempt.
117
+            after=tenacity.after_log(LOG, logging.DEBUG),
118
+            # Reraise exceptions if our final attempt fails.
119
+            reraise=True,
120
+            # Retry on failure to commit the configuration due to the DB
121
+            # being locked by another session.
122
+            retry=(tenacity.retry_if_exception_type(DBLocked)),
123
+            # Stop after the configured timeout.
124
+            stop=tenacity.stop_after_delay(
125
+                int(self.ngs_config['ngs_commit_timeout'])),
126
+            # Wait for the configured interval between attempts.
127
+            wait=tenacity.wait_fixed(
128
+                int(self.ngs_config['ngs_commit_interval'])),
129
+        )
130
+        def commit():
131
+            try:
132
+                net_connect.commit()
133
+            except ValueError as e:
134
+                # Netmiko raises ValueError on commit failure, and appends the
135
+                # CLI output to the exception message. Raise a more specific
136
+                # exception for a locked DB, on which tenacity will retry.
137
+                if "error: configuration database locked" in str(e):
138
+                    raise DBLocked(e)
139
+                raise
140
+
141
+        try:
142
+            commit()
143
+        except DBLocked as e:
144
+            msg = ("Reached timeout waiting for switch configuration DB lock: "
145
+                   "%s" % e.err)
146
+            LOG.error(msg)
147
+            raise exc.GenericSwitchNetmikoConfigError(
148
+                config=device_utils.sanitise_config(self.config), error=msg)
149
+        except ValueError as e:
150
+            msg = "Failed to commit configuration: %s" % e
151
+            LOG.error(msg)
152
+            raise exc.GenericSwitchNetmikoConfigError(
153
+                config=device_utils.sanitise_config(self.config), error=msg)

+ 4
- 0
networking_generic_switch/exceptions.py View File

@@ -35,3 +35,7 @@ class GenericSwitchNetmikoNotSupported(GenericSwitchException):
35 35
 
36 36
 class GenericSwitchNetmikoConnectError(GenericSwitchException):
37 37
     message = _("Netmiko connection error: %(config)s, error: %(error)s")
38
+
39
+
40
+class GenericSwitchNetmikoConfigError(GenericSwitchException):
41
+    message = _("Netmiko configuration error: %(config)s, error: %(error)s")

+ 212
- 0
networking_generic_switch/tests/unit/netmiko/test_juniper.py View File

@@ -0,0 +1,212 @@
1
+# Copyright (c) 2018 StackHPC Ltd.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+import mock
16
+import netmiko
17
+import tenacity
18
+
19
+from networking_generic_switch.devices import netmiko_devices
20
+from networking_generic_switch.devices.netmiko_devices import juniper
21
+from networking_generic_switch import exceptions as exc
22
+from networking_generic_switch.tests.unit.netmiko import test_netmiko_base
23
+
24
+
25
+class TestNetmikoJuniper(test_netmiko_base.NetmikoSwitchTestBase):
26
+
27
+    def _make_switch_device(self, extra_cfg={}):
28
+        device_cfg = {'device_type': 'netmiko_juniper'}
29
+        device_cfg.update(extra_cfg)
30
+        return juniper.Juniper(device_cfg)
31
+
32
+    def test_constants(self):
33
+        self.assertIsNone(self.switch.SAVE_CONFIGURATION)
34
+
35
+    @mock.patch('networking_generic_switch.devices.netmiko_devices.'
36
+                'NetmikoSwitch.send_commands_to_device')
37
+    def test_add_network(self, m_exec):
38
+        self.switch.add_network(33, '0ae071f5-5be9-43e4-80ea-e41fefe85b21')
39
+        m_exec.assert_called_with(
40
+            ['set vlans 0ae071f55be943e480eae41fefe85b21 vlan-id 33'])
41
+
42
+    @mock.patch('networking_generic_switch.devices.netmiko_devices.'
43
+                'NetmikoSwitch.send_commands_to_device')
44
+    def test_add_network_with_trunk_ports(self, mock_exec):
45
+        switch = self._make_switch_device({'ngs_trunk_ports': 'port1,port2'})
46
+        switch.add_network(33, '0ae071f5-5be9-43e4-80ea-e41fefe85b21')
47
+        mock_exec.assert_called_with(
48
+            ['set vlans 0ae071f55be943e480eae41fefe85b21 vlan-id 33',
49
+             'set interface port1 unit 0 family ethernet-switching '
50
+             'vlan members 33',
51
+             'set interface port2 unit 0 family ethernet-switching '
52
+             'vlan members 33'])
53
+
54
+    @mock.patch('networking_generic_switch.devices.netmiko_devices.'
55
+                'NetmikoSwitch.send_commands_to_device')
56
+    def test_del_network(self, mock_exec):
57
+        self.switch.del_network(33, '0ae071f55be943e480eae41fefe85b21')
58
+        mock_exec.assert_called_with(
59
+            ['delete vlans 0ae071f55be943e480eae41fefe85b21'])
60
+
61
+    @mock.patch('networking_generic_switch.devices.netmiko_devices.'
62
+                'NetmikoSwitch.send_commands_to_device')
63
+    def test_del_network_with_trunk_ports(self, mock_exec):
64
+        switch = self._make_switch_device({'ngs_trunk_ports': 'port1,port2'})
65
+        switch.del_network(33, '0ae071f55be943e480eae41fefe85b21')
66
+        mock_exec.assert_called_with(
67
+            ['delete interface port1 unit 0 family ethernet-switching '
68
+             'vlan members 33',
69
+             'delete interface port2 unit 0 family ethernet-switching '
70
+             'vlan members 33',
71
+             'delete vlans 0ae071f55be943e480eae41fefe85b21'])
72
+
73
+    @mock.patch('networking_generic_switch.devices.netmiko_devices.'
74
+                'NetmikoSwitch.send_commands_to_device')
75
+    def test_plug_port_to_network(self, mock_exec):
76
+        self.switch.plug_port_to_network(3333, 33)
77
+        mock_exec.assert_called_with(
78
+            ['delete interface 3333 unit 0 family ethernet-switching '
79
+             'vlan members',
80
+             'set interface 3333 unit 0 family ethernet-switching '
81
+             'vlan members 33'])
82
+
83
+    @mock.patch('networking_generic_switch.devices.netmiko_devices.'
84
+                'NetmikoSwitch.send_commands_to_device')
85
+    def test_delete_port(self, mock_exec):
86
+        self.switch.delete_port(3333, 33)
87
+        mock_exec.assert_called_with(
88
+            ['delete interface 3333 unit 0 family ethernet-switching '
89
+             'vlan members'])
90
+
91
+    def test_send_config_set(self):
92
+        connect_mock = mock.MagicMock(netmiko.base_connection.BaseConnection)
93
+        connect_mock.send_config_set.return_value = 'fake output'
94
+        result = self.switch.send_config_set(connect_mock, ['spam ham aaaa'])
95
+        self.assertFalse(connect_mock.enable.called)
96
+        connect_mock.send_config_set.assert_called_once_with(
97
+            config_commands=['spam ham aaaa'], exit_config_mode=False)
98
+        self.assertEqual('fake output', result)
99
+
100
+    def test_save_configuration(self):
101
+        mock_connection = mock.Mock()
102
+        self.switch.save_configuration(mock_connection)
103
+        mock_connection.commit.assert_called_once_with()
104
+
105
+    @mock.patch.object(netmiko_devices.tenacity, 'wait_fixed',
106
+                       return_value=tenacity.wait_fixed(0.01))
107
+    @mock.patch.object(netmiko_devices.tenacity, 'stop_after_delay',
108
+                       return_value=tenacity.stop_after_delay(0.1))
109
+    def test_save_configuration_timeout(self, m_stop, m_wait):
110
+        mock_connection = mock.Mock()
111
+        output = """
112
+error: configuration database locked by:
113
+  user terminal p0 (pid 1234) on since 2017-1-1 00:00:00 UTC
114
+      exclusive private [edit]
115
+
116
+{master:0}[edit]"""
117
+        mock_connection.commit.side_effect = ValueError(
118
+            "Commit failed with the following errors:\n\n{0}".format(output))
119
+
120
+        self.assertRaisesRegexp(exc.GenericSwitchNetmikoConfigError,
121
+                                "Reached timeout waiting for",
122
+                                self.switch.save_configuration,
123
+                                mock_connection)
124
+        self.assertGreater(mock_connection.commit.call_count, 1)
125
+        m_stop.assert_called_once_with(60)
126
+        m_wait.assert_called_once_with(5)
127
+
128
+    def test_save_configuration_error(self):
129
+        mock_connection = mock.Mock()
130
+        output = """
131
+[edit vlans]
132
+  'duplicate-vlan'
133
+    l2ald: Duplicate vlan-id exists for vlan duplicate-vlan
134
+[edit vlans]
135
+  Failed to parse vlan hierarchy completely
136
+error: configuration check-out failed
137
+
138
+{master:0}[edit]"""
139
+        mock_connection.commit.side_effect = ValueError(
140
+            "Commit failed with the following errors:\n\n{0}".format(output))
141
+
142
+        self.assertRaisesRegexp(exc.GenericSwitchNetmikoConfigError,
143
+                                "Failed to commit configuration",
144
+                                self.switch.save_configuration,
145
+                                mock_connection)
146
+        mock_connection.commit.assert_called_once_with()
147
+
148
+    @mock.patch.object(netmiko_devices.tenacity, 'wait_fixed',
149
+                       return_value=tenacity.wait_fixed(0.01))
150
+    @mock.patch.object(netmiko_devices.tenacity, 'stop_after_delay',
151
+                       return_value=tenacity.stop_after_delay(0.1))
152
+    def test_save_configuration_non_default_timing(self, m_stop, m_wait):
153
+        self.switch = self._make_switch_device({'ngs_commit_timeout': 42,
154
+                                                'ngs_commit_interval': 43})
155
+        mock_connection = mock.MagicMock(
156
+            netmiko.base_connection.BaseConnection)
157
+        self.switch.save_configuration(mock_connection)
158
+        mock_connection.commit.assert_called_once_with()
159
+        m_stop.assert_called_once_with(42)
160
+        m_wait.assert_called_once_with(43)
161
+
162
+    def test__format_commands(self):
163
+        cmd_set = self.switch._format_commands(
164
+            juniper.Juniper.ADD_NETWORK,
165
+            segmentation_id=22,
166
+            network_id=22)
167
+        self.assertEqual(cmd_set, ['set vlans 22 vlan-id 22'])
168
+
169
+        cmd_set = self.switch._format_commands(
170
+            juniper.Juniper.DELETE_NETWORK,
171
+            segmentation_id=22,
172
+            network_id=22)
173
+        self.assertEqual(cmd_set, ['delete vlans 22'])
174
+
175
+        cmd_set = self.switch._format_commands(
176
+            juniper.Juniper.PLUG_PORT_TO_NETWORK,
177
+            port=3333,
178
+            segmentation_id=33)
179
+        self.assertEqual(cmd_set,
180
+                         ['delete interface 3333 unit 0 '
181
+                          'family ethernet-switching '
182
+                          'vlan members',
183
+                          'set interface 3333 unit 0 '
184
+                          'family ethernet-switching '
185
+                          'vlan members 33'])
186
+
187
+        cmd_set = self.switch._format_commands(
188
+            juniper.Juniper.DELETE_PORT,
189
+            port=3333,
190
+            segmentation_id=33)
191
+        self.assertEqual(cmd_set,
192
+                         ['delete interface 3333 unit 0 '
193
+                          'family ethernet-switching '
194
+                          'vlan members'])
195
+
196
+        cmd_set = self.switch._format_commands(
197
+            juniper.Juniper.ADD_NETWORK_TO_TRUNK,
198
+            port=3333,
199
+            segmentation_id=33)
200
+        self.assertEqual(cmd_set,
201
+                         ['set interface 3333 unit 0 '
202
+                          'family ethernet-switching '
203
+                          'vlan members 33'])
204
+
205
+        cmd_set = self.switch._format_commands(
206
+            juniper.Juniper.REMOVE_NETWORK_FROM_TRUNK,
207
+            port=3333,
208
+            segmentation_id=33)
209
+        self.assertEqual(cmd_set,
210
+                         ['delete interface 3333 unit 0 '
211
+                          'family ethernet-switching '
212
+                          'vlan members 33'])

+ 16
- 0
releasenotes/notes/juniper-92d75d3086cf78a2.yaml View File

@@ -0,0 +1,16 @@
1
+---
2
+features:
3
+  - |
4
+    Adds a new driver, ``netmiko_juniper``, for Juniper JunOS devices.
5
+
6
+    The private configuration mode is used in order to provide a level of
7
+    isolation between sessions, and to ensure that uncommitted changes are not
8
+    left on the switch following a failure to commit the configuration.
9
+
10
+    Configuration errors are handled by ensuring that the commit operation is
11
+    successful.
12
+
13
+    A retry mechanism is used to handle temporary failures due to multiple
14
+    sessions attempting to lock the JunOS configuration database concurrently.
15
+    The retry mechanism is configured via the configuration options
16
+    ``ngs_commit_interval`` and ``ngs_commit_timeout``.

+ 1
- 0
setup.cfg View File

@@ -29,6 +29,7 @@ generic_switch.devices =
29 29
     netmiko_brocade_fastiron = networking_generic_switch.devices.netmiko_devices.brocade:BrocadeFastIron
30 30
     netmiko_ruijie = networking_generic_switch.devices.netmiko_devices.ruijie:Ruijie
31 31
     netmiko_hpe_comware = networking_generic_switch.devices.netmiko_devices.hpe:HpeComware
32
+    netmiko_juniper = networking_generic_switch.devices.netmiko_devices.juniper:Juniper
32 33
 tempest.test_plugins =
33 34
     ngs_tests = tempest_plugin.plugin:NGSTempestPlugin
34 35
 

Loading…
Cancel
Save