summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.openstack.org>2018-07-27 11:51:06 +0000
committerGerrit Code Review <review@openstack.org>2018-07-27 11:51:06 +0000
commitbdf571d48f0c5dc7858a8db79710983d9db48e03 (patch)
tree614d5c5024d40ebb256988cb80eec612faa27178
parent73f9b4b6a5a74d0cff009d08ea702158cdc7f20e (diff)
parent7b6d35f1d83dca4b45ca18458c55f032e02615bd (diff)
Merge "Add a netmiko device driver for Juniper switches"
-rw-r--r--doc/source/configuration.rst10
-rw-r--r--doc/source/supported-devices.rst1
-rw-r--r--networking_generic_switch/devices/netmiko_devices/juniper.py153
-rw-r--r--networking_generic_switch/exceptions.py4
-rw-r--r--networking_generic_switch/tests/unit/netmiko/test_juniper.py212
-rw-r--r--releasenotes/notes/juniper-92d75d3086cf78a2.yaml16
-rw-r--r--setup.cfg1
7 files changed, 397 insertions, 0 deletions
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst
index 56b5637..580e237 100644
--- a/doc/source/configuration.rst
+++ b/doc/source/configuration.rst
@@ -129,6 +129,16 @@ for the HPE 5900 Series device::
129 password = password 129 password = password
130 ip = <switch mgmt ip address> 130 ip = <switch mgmt ip address>
131 131
132for 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
132Additionally the ``GenericSwitch`` mechanism driver needs to be enabled from 142Additionally the ``GenericSwitch`` mechanism driver needs to be enabled from
133the ml2 config file ``/etc/neutron/plugins/ml2/ml2_conf.ini``:: 143the ml2 config file ``/etc/neutron/plugins/ml2/ml2_conf.ini``::
134 144
diff --git a/doc/source/supported-devices.rst b/doc/source/supported-devices.rst
index f2f46e0..a7e342a 100644
--- a/doc/source/supported-devices.rst
+++ b/doc/source/supported-devices.rst
@@ -14,6 +14,7 @@ The following devices are supported by this plugin:
14* Brocade ICX (FastIron) 14* Brocade ICX (FastIron)
15* Ruijie switches 15* Ruijie switches
16* HPE 5900 Series switches 16* HPE 5900 Series switches
17* Juniper Junos
17 18
18This Mechanism Driver architecture allows easily to add more devices 19This Mechanism Driver architecture allows easily to add more devices
19of any type. 20of any type.
diff --git a/networking_generic_switch/devices/netmiko_devices/juniper.py b/networking_generic_switch/devices/netmiko_devices/juniper.py
new file mode 100644
index 0000000..5f3b096
--- /dev/null
+++ b/networking_generic_switch/devices/netmiko_devices/juniper.py
@@ -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
15from oslo_log import log as logging
16import tenacity
17
18from networking_generic_switch.devices import netmiko_devices
19from networking_generic_switch.devices import utils as device_utils
20from networking_generic_switch import exceptions as exc
21
22LOG = logging.getLogger(__name__)
23
24# Internal ngs options will not be passed to driver.
25JUNIPER_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
33class 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)
diff --git a/networking_generic_switch/exceptions.py b/networking_generic_switch/exceptions.py
index e8a2d7b..29fc074 100644
--- a/networking_generic_switch/exceptions.py
+++ b/networking_generic_switch/exceptions.py
@@ -35,3 +35,7 @@ class GenericSwitchNetmikoNotSupported(GenericSwitchException):
35 35
36class GenericSwitchNetmikoConnectError(GenericSwitchException): 36class GenericSwitchNetmikoConnectError(GenericSwitchException):
37 message = _("Netmiko connection error: %(config)s, error: %(error)s") 37 message = _("Netmiko connection error: %(config)s, error: %(error)s")
38
39
40class GenericSwitchNetmikoConfigError(GenericSwitchException):
41 message = _("Netmiko configuration error: %(config)s, error: %(error)s")
diff --git a/networking_generic_switch/tests/unit/netmiko/test_juniper.py b/networking_generic_switch/tests/unit/netmiko/test_juniper.py
new file mode 100644
index 0000000..b532513
--- /dev/null
+++ b/networking_generic_switch/tests/unit/netmiko/test_juniper.py
@@ -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
15import mock
16import netmiko
17import tenacity
18
19from networking_generic_switch.devices import netmiko_devices
20from networking_generic_switch.devices.netmiko_devices import juniper
21from networking_generic_switch import exceptions as exc
22from networking_generic_switch.tests.unit.netmiko import test_netmiko_base
23
24
25class 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 = """
112error: 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
136error: 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'])
diff --git a/releasenotes/notes/juniper-92d75d3086cf78a2.yaml b/releasenotes/notes/juniper-92d75d3086cf78a2.yaml
new file mode 100644
index 0000000..d57b2dd
--- /dev/null
+++ b/releasenotes/notes/juniper-92d75d3086cf78a2.yaml
@@ -0,0 +1,16 @@
1---
2features:
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``.
diff --git a/setup.cfg b/setup.cfg
index 94959e4..3afdc6e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -29,6 +29,7 @@ generic_switch.devices =
29 netmiko_brocade_fastiron = networking_generic_switch.devices.netmiko_devices.brocade:BrocadeFastIron 29 netmiko_brocade_fastiron = networking_generic_switch.devices.netmiko_devices.brocade:BrocadeFastIron
30 netmiko_ruijie = networking_generic_switch.devices.netmiko_devices.ruijie:Ruijie 30 netmiko_ruijie = networking_generic_switch.devices.netmiko_devices.ruijie:Ruijie
31 netmiko_hpe_comware = networking_generic_switch.devices.netmiko_devices.hpe:HpeComware 31 netmiko_hpe_comware = networking_generic_switch.devices.netmiko_devices.hpe:HpeComware
32 netmiko_juniper = networking_generic_switch.devices.netmiko_devices.juniper:Juniper
32tempest.test_plugins = 33tempest.test_plugins =
33 ngs_tests = tempest_plugin.plugin:NGSTempestPlugin 34 ngs_tests = tempest_plugin.plugin:NGSTempestPlugin
34 35