Skip to content

Commit f0655a9

Browse files
jayofdoomJohn GarbuttAndyEllarddougszumski
authored andcommitted
Add VLAN and Port allowlist
Adds support for a VLAN and port allowlist for high security environments to be able to restrict Ironic activities only to ports which are attached to nodes (or any other restriction desired and configured). Co-authored-by: John Garbutt <John.Garbutt@gresearch.co.uk> Co-authored-by: Andrew Ellard <andrew.ellard@gresearch.co.uk> Co-authored-by: Doug Szumski <doug@stackhpc.com> Change-Id: Ic0ef1d3f917f3b1e666e5f1b87ae13457663330c
1 parent 4ef0a04 commit f0655a9

6 files changed

Lines changed: 314 additions & 40 deletions

File tree

doc/source/configuration.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Switch configuration format::
1616
password = <credential password>
1717
key_file = <ssh key file>
1818
secret = <enable secret>
19+
ngs_allowed_vlans = <comma-separated list of allowed vlans for switch>
20+
ngs_allowed_ports = <comma-separated list of allowed ports for switch>
1921

2022
# If set ngs_port_default_vlan to default_vlan, switch's
2123
# interface will restore the default_vlan.
@@ -24,6 +26,10 @@ Switch configuration format::
2426
The ``device_type`` entry is mandatory. Most other configuration entries
2527
are optional, see below.
2628

29+
The two new optional configuration parameters ``ngs_allowed_vlans`` and
30+
``ngs_allowed_ports`` have been introduced to manage allowed VLANs and ports
31+
on switches. If not set, all ports or VLANS are allowed.
32+
2733
.. note::
2834

2935
Switch will be selected by local_link_connection/switch_info

networking_generic_switch/devices/__init__.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@
5454
{'name': 'ngs_fake_sleep_min_s'},
5555
{'name': 'ngs_fake_sleep_max_s'},
5656
{'name': 'ngs_fake_failure_prob'},
57+
# Allow list for VLANs and ports for this switch
58+
# default open, but setting empty string blocks all ports
59+
{'name': 'ngs_allowed_vlans'},
60+
{'name': 'ngs_allowed_ports'},
5761
]
5862

5963

@@ -179,6 +183,37 @@ def _batch_requests(self):
179183
return strutils.bool_from_string(
180184
self.ngs_config['ngs_batch_requests'])
181185

186+
def _get_allowed_vlans(self):
187+
allowed_vlans = self.ngs_config.get('ngs_allowed_vlans')
188+
if allowed_vlans is None:
189+
return None
190+
return allowed_vlans.split(',')
191+
192+
def _get_allowed_ports(self):
193+
allowed_ports = self.ngs_config.get('ngs_allowed_ports')
194+
if allowed_ports is None:
195+
return None
196+
return allowed_ports.split(',')
197+
198+
def is_allowed(self, port_id, segmentation_id):
199+
is_port_id_allowed = True
200+
allowed_ports = self._get_allowed_ports()
201+
if allowed_ports is not None:
202+
is_port_id_allowed = port_id in allowed_ports
203+
LOG.debug("Port %(port_id) allowed: %(is_port_id_allowed",
204+
{"port_id": port_id,
205+
"is_port_id_allowed": is_port_id_allowed})
206+
207+
is_vlan_allowed = True
208+
allowed_vlans = self._get_allowed_vlans()
209+
if allowed_vlans is not None:
210+
is_vlan_allowed = str(segmentation_id) in allowed_vlans
211+
LOG.debug("VLAN %(vlan) allowed: %(is_allowed",
212+
{"vlan": segmentation_id,
213+
"is_allowed": is_vlan_allowed})
214+
215+
return is_port_id_allowed and is_vlan_allowed
216+
182217
@abc.abstractmethod
183218
def add_network(self, segmentation_id, network_id):
184219
pass

networking_generic_switch/generic_switch_mech.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,9 +477,47 @@ def bind_port(self, context):
477477
if not self._is_link_valid(port, network):
478478
return
479479

480-
segments = context.segments_to_bind
480+
is_802_3ad = self._is_802_3ad(port)
481+
for link in local_link_information:
482+
port_id = link.get('port_id')
483+
switch_info = link.get('switch_info')
484+
switch_id = link.get('switch_id')
485+
switch = device_utils.get_switch_device(
486+
self.switches, switch_info=switch_info,
487+
ngs_mac_address=switch_id)
488+
489+
segments = context.segments_to_bind
490+
# If segmentation ID is None, set vlan 1
491+
segmentation_id = segments[0].get('segmentation_id') or 1
492+
493+
# Fail if port or vlan not in allow list
494+
if not switch.is_allowed(port_id, segmentation_id):
495+
LOG.warn("Skipped binding port %(port_id)s, "
496+
"port %(port)s in segment "
497+
"%(segment_id)s on device %(device)s, as either "
498+
"the port or vlan is not on the allow list",
499+
{'port_id': port['id'], 'port': port_id,
500+
'device': switch_info,
501+
'segment_id': segmentation_id})
502+
return
503+
504+
LOG.debug("Putting port %(port_id)s on %(switch_info)s "
505+
"to vlan: %(segmentation_id)s",
506+
{'port_id': port_id, 'switch_info': switch_info,
507+
'segmentation_id': segmentation_id})
508+
# Move port to network
509+
if is_802_3ad and hasattr(switch, 'plug_bond_to_network'):
510+
switch.plug_bond_to_network(port_id, segmentation_id)
511+
else:
512+
switch.plug_port_to_network(port_id, segmentation_id)
513+
LOG.info("Successfully bound port %(port_id)s in segment "
514+
"%(segment_id)s on device %(device)s",
515+
{'port_id': port['id'], 'device': switch_info,
516+
'segment_id': segmentation_id})
517+
481518
context.set_binding(segments[0][api.ID],
482519
portbindings.VIF_TYPE_OTHER, {})
520+
483521
provisioning_blocks.add_provisioning_component(
484522
context._plugin_context, port['id'], resources.PORT,
485523
GENERIC_SWITCH_ENTITY)

networking_generic_switch/tests/unit/test_devices.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@ def test_driver_ngs_config(self):
118118
"ngs_physical_networks": "physnet1,physnet2",
119119
"ngs_port_default_vlan": "20",
120120
"ngs_disable_inactive_ports": "true",
121-
"ngs_network_name_format": "net-{network_id}"}
121+
"ngs_network_name_format": "net-{network_id}",
122+
"ngs_allowed_vlans": "123,124",
123+
"ngs_allowed_ports": "Ethernet1/1,Ethernet1/2"}
122124
device = devices.device_manager(device_cfg)
123125
self.assertIsInstance(device, devices.GenericSwitchDevice)
124126
self.assertNotIn('ngs_mac_address', device.config)
@@ -127,6 +129,8 @@ def test_driver_ngs_config(self):
127129
self.assertNotIn('ngs_trunk_ports', device.config)
128130
self.assertNotIn('ngs_physical_networks', device.config)
129131
self.assertNotIn('ngs_port_default_vlan', device.config)
132+
self.assertNotIn('ngs_allowed_vlans', device.config)
133+
self.assertNotIn('ngs_allowed_ports', device.config)
130134
self.assertEqual('aa:bb:cc:dd:ee:ff',
131135
device.ngs_config['ngs_mac_address'])
132136
self.assertEqual('120', device.ngs_config['ngs_ssh_connect_timeout'])
@@ -139,6 +143,49 @@ def test_driver_ngs_config(self):
139143
device.ngs_config['ngs_disable_inactive_ports'])
140144
self.assertEqual('net-{network_id}',
141145
device.ngs_config['ngs_network_name_format'])
146+
self.assertEqual("123,124",
147+
device.ngs_config['ngs_allowed_vlans'])
148+
self.assertEqual(["123", "124"],
149+
device._get_allowed_vlans())
150+
self.assertEqual("Ethernet1/1,Ethernet1/2",
151+
device.ngs_config['ngs_allowed_ports'])
152+
self.assertEqual(["Ethernet1/1", "Ethernet1/2"],
153+
device._get_allowed_ports())
154+
155+
def test_driver_ngs_is_allowed(self):
156+
device_cfg = {"device_type": 'netmiko_ovs_linux',
157+
"ngs_allowed_vlans": "123,124",
158+
"ngs_allowed_ports": "Ethernet1/1,Ethernet1/2"}
159+
device = devices.device_manager(device_cfg)
160+
161+
# test all allowed
162+
self.assertTrue(device.is_allowed("Ethernet1/1", 123))
163+
self.assertTrue(device.is_allowed("Ethernet1/2", 124))
164+
# fail on vlan
165+
self.assertFalse(device.is_allowed("Ethernet1/2", 125))
166+
# fail on port
167+
self.assertFalse(device.is_allowed("Ethernet1/3", 124))
168+
# fail on both
169+
self.assertFalse(device.is_allowed("Ethernet2/2", 1))
170+
171+
def test_driver_ngs_is_allowed_fails_on_empty_ports(self):
172+
device_cfg = {"device_type": 'netmiko_ovs_linux',
173+
"ngs_allowed_vlans": "123",
174+
"ngs_allowed_ports": ""}
175+
device = devices.device_manager(device_cfg)
176+
self.assertFalse(device.is_allowed("Ethernet1/1", 123))
177+
178+
def test_driver_ngs_is_allowed_fails_on_empty_vlans(self):
179+
device_cfg = {"device_type": 'netmiko_ovs_linux',
180+
"ngs_allowed_vlans": "",
181+
"ngs_allowed_ports": "Ethernet1/1"}
182+
device = devices.device_manager(device_cfg)
183+
self.assertFalse(device.is_allowed("Ethernet1/1", 123))
184+
185+
def test_driver_ngs_is_allowed_default(self):
186+
device_cfg = {"device_type": 'netmiko_ovs_linux'}
187+
device = devices.device_manager(device_cfg)
188+
self.assertTrue(device.is_allowed("Ethernet1/1", 123))
142189

143190
def test_driver_ngs_config_defaults(self):
144191
device_cfg = {"device_type": 'netmiko_ovs_linux'}
@@ -153,6 +200,8 @@ def test_driver_ngs_config_defaults(self):
153200
self.assertFalse(device.ngs_config['ngs_disable_inactive_ports'])
154201
self.assertEqual('{network_id}',
155202
device.ngs_config['ngs_network_name_format'])
203+
self.assertNotIn('ngs_allowed_vlans', device.ngs_config)
204+
self.assertNotIn('ngs_allowed_ports', device.ngs_config)
156205

157206
def test__get_trunk_ports(self):
158207
device_cfg = {"ngs_trunk_ports": 'port1, Po 1/30,port42'}

0 commit comments

Comments
 (0)