Skip to content

Commit b80121e

Browse files
committed
Add PVLAN commands for Neutron
Following the merge of the spec for PVLAN [1] support, this patch introduces the parameter --pvlan in networks, as well as --pvlan-type and --pvlan-community in ports. It handles the validation on the client side of the different parameters: For Network: - PVLAN can only be enabled on if port security is enabled. For Port: - PVLAN attributes can only be set if associated network if PVLAN is enabled and port security is enabled. - pvlan-community can only be set if pvlan-type is community There are now unit tests for these too. [1] https://specs.openstack.org/openstack/neutron-specs/specs/2026.1/pvlan-semantics-for-provider-networks.html Change-Id: I10033f59b52edea37350ca148dd2328bf2322cdb Signed-off-by: Elvira Garcia <egarciar@redhat.com>
1 parent e19bdd8 commit b80121e

6 files changed

Lines changed: 337 additions & 1 deletion

File tree

openstackclient/network/v2/network.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,13 @@ def _get_attrs_network(
146146
attrs['qos_policy_id'] = _qos_policy.id
147147
if 'no_qos_policy' in parsed_args and parsed_args.no_qos_policy:
148148
attrs['qos_policy_id'] = None
149+
150+
# Set pvlan
151+
if parsed_args.pvlan:
152+
attrs['pvlan'] = True
153+
if parsed_args.no_pvlan:
154+
attrs['pvlan'] = False
155+
149156
# Update DNS network options
150157
if parsed_args.dns_domain is not None:
151158
attrs['dns_domain'] = parsed_args.dns_domain
@@ -348,6 +355,24 @@ def get_parser(self, prog_name: str) -> argparse.ArgumentParser:
348355
help=_("Disable VLAN QinQ (S-Tag ethtype 0x8a88) for the network"),
349356
)
350357

358+
pvlan_grp = parser.add_mutually_exclusive_group()
359+
pvlan_grp.add_argument(
360+
'--pvlan',
361+
action='store_true',
362+
help=_(
363+
"Enable Private VLAN for the network "
364+
"(PVLAN extension required)"
365+
),
366+
)
367+
pvlan_grp.add_argument(
368+
'--no-pvlan',
369+
action='store_true',
370+
help=_(
371+
"Disable Private VLAN for the network "
372+
"(PVLAN extension required)"
373+
),
374+
)
375+
351376
_add_additional_network_options(parser)
352377
_tag.add_tag_option_to_parser_for_create(parser, _('network'))
353378
return parser
@@ -367,13 +392,26 @@ def take_action(
367392
if parsed_args.no_qinq_vlan:
368393
attrs['vlan_qinq'] = False
369394

395+
if parsed_args.pvlan:
396+
attrs['pvlan'] = True
397+
if parsed_args.no_pvlan:
398+
attrs['pvlan'] = False
399+
370400
if attrs.get('vlan_transparent') and attrs.get('vlan_qinq'):
371401
msg = _(
372402
"--transparent-vlan and --qinq-vlan can not be both enabled "
373403
"for the network."
374404
)
375405
raise exceptions.CommandError(msg)
376406

407+
if (
408+
attrs.get('port_security_enabled') is False
409+
and attrs.get('pvlan') is True
410+
):
411+
msg = _(
412+
"--disable-port-security and --pvlan can not be used together."
413+
)
414+
raise exceptions.CommandError(msg)
377415
if (
378416
parsed_args.segmentation_id
379417
and not parsed_args.provider_network_type
@@ -770,6 +808,23 @@ def get_parser(self, prog_name: str) -> argparse.ArgumentParser:
770808
action='store_true',
771809
help=_("Remove the QoS policy attached to this network"),
772810
)
811+
pvlan_grp = parser.add_mutually_exclusive_group()
812+
pvlan_grp.add_argument(
813+
'--pvlan',
814+
action='store_true',
815+
help=_(
816+
"Enable Private VLAN for the network. PVLAN extension "
817+
"required."
818+
),
819+
)
820+
pvlan_grp.add_argument(
821+
'--no-pvlan',
822+
action='store_true',
823+
help=_(
824+
"Disable Private VLAN for the network (Default). "
825+
"PVLAN extension required."
826+
),
827+
)
773828
_tag.add_tag_option_to_parser_for_set(parser, _('network'))
774829
_add_additional_network_options(parser)
775830
return parser
@@ -782,6 +837,14 @@ def take_action(self, parsed_args: argparse.Namespace) -> None:
782837
attrs.update(
783838
self._parse_extra_properties(parsed_args.extra_properties)
784839
)
840+
if (
841+
attrs.get('port_security_enabled') is False
842+
and attrs.get('pvlan') is True
843+
):
844+
msg = _(
845+
"--disable-port-security and --pvlan can not be used together."
846+
)
847+
raise exceptions.CommandError(msg)
785848
if attrs:
786849
with common.check_missing_extension_if_error(
787850
self.app.client_manager.network, attrs

openstackclient/network/v2/port.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ def _get_columns(item: Any) -> tuple[tuple[str, ...], tuple[str, ...]]:
105105
'port_security_enabled': 'is_port_security_enabled',
106106
'project_id': 'project_id',
107107
'propagate_uplink_status': 'propagate_uplink_status',
108+
'pvlan_type': 'pvlan_type',
109+
'pvlan_community': 'pvlan_community',
108110
'resource_request': 'resource_request',
109111
'revision_number': 'revision_number',
110112
'qos_network_policy_id': 'qos_network_policy_id',
@@ -256,9 +258,48 @@ def _get_attrs(
256258
if parsed_args.trusted:
257259
attrs['trusted'] = True
258260

261+
if 'pvlan_type' in parsed_args and parsed_args.pvlan_type is not None:
262+
attrs['pvlan_type'] = parsed_args.pvlan_type
263+
if (
264+
'pvlan_community' in parsed_args
265+
and parsed_args.pvlan_community is not None
266+
):
267+
attrs['pvlan_community'] = parsed_args.pvlan_community
268+
269+
_validate_pvlan_port(attrs)
270+
259271
return attrs
260272

261273

274+
def _validate_pvlan_port(attrs: dict[str, Any]) -> None:
275+
if (attrs.get('pvlan_type') or attrs.get('pvlan_community')) and attrs.get(
276+
'port_security_enabled'
277+
) is False:
278+
msg = _(
279+
"PVLAN attributes cannot be set when port security is disabled."
280+
)
281+
raise exceptions.CommandError(msg)
282+
283+
if attrs.get('pvlan_type') == 'community' and not attrs.get(
284+
'pvlan_community'
285+
):
286+
msg = _(
287+
"--pvlan-community is required when --pvlan-type is 'community'."
288+
)
289+
raise exceptions.CommandError(msg)
290+
291+
292+
def _validate_pvlan_network_port(attrs: dict[str, Any], network: Any) -> None:
293+
if not (attrs.get('pvlan_type') or attrs.get('pvlan_community')):
294+
return
295+
if not network.pvlan:
296+
msg = _(
297+
"PVLAN attributes cannot be set on a port whose "
298+
"network does not have PVLAN enabled."
299+
)
300+
raise exceptions.CommandError(msg)
301+
302+
262303
def _prepare_fixed_ips(
263304
client_manager: Any, parsed_args: argparse.Namespace
264305
) -> None:
@@ -445,6 +486,26 @@ def _add_updatable_args(
445486
"which expect it in this dictionary (for example, Nova)."
446487
),
447488
)
489+
parser.add_argument(
490+
'--pvlan-type',
491+
metavar='<type>',
492+
choices=['promiscuous', 'isolated', 'community'],
493+
dest='pvlan_type',
494+
help=_(
495+
"Set Private VLAN type for this port. Requires PVLAN service "
496+
"plugin. Default: promiscuous."
497+
),
498+
)
499+
parser.add_argument(
500+
'--pvlan-community',
501+
metavar='<community>',
502+
dest='pvlan_community',
503+
help=_(
504+
"Set PVLAN community name for this port. "
505+
"Only applies when pvlan-type is 'community'. "
506+
"Requires PVLAN service plugin. Default: None."
507+
),
508+
)
448509

449510

450511
# TODO(abhiraut): Use the SDK resource mapped attribute names once the
@@ -732,6 +793,8 @@ def take_action(
732793
self._parse_extra_properties(parsed_args.extra_properties)
733794
)
734795

796+
_validate_pvlan_network_port(attrs, network)
797+
735798
with common.check_missing_extension_if_error(network_client, attrs):
736799
obj = network_client.create_port(**attrs)
737800

@@ -1232,6 +1295,10 @@ def take_action(self, parsed_args: argparse.Namespace) -> None:
12321295
self._parse_extra_properties(parsed_args.extra_properties)
12331296
)
12341297

1298+
if attrs.get('pvlan_type') or attrs.get('pvlan_community'):
1299+
network = client.find_network(obj.network_id, ignore_missing=False)
1300+
_validate_pvlan_network_port(attrs, network)
1301+
12351302
if attrs:
12361303
with common.check_missing_extension_if_error(
12371304
self.app.client_manager.network, attrs
@@ -1355,6 +1422,13 @@ def get_parser(self, prog_name: str) -> argparse.ArgumentParser:
13551422
default=False,
13561423
help=_("Clear device owner for the port."),
13571424
)
1425+
parser.add_argument(
1426+
'--pvlan-community',
1427+
action='store_true',
1428+
default=False,
1429+
dest='pvlan_community',
1430+
help=_("Clear PVLAN community name for the port."),
1431+
)
13581432
_tag.add_tag_option_to_parser_for_unset(parser, _('port'))
13591433
parser.add_argument(
13601434
'port',
@@ -1425,6 +1499,8 @@ def take_action(self, parsed_args: argparse.Namespace) -> None:
14251499
attrs['device_id'] = ''
14261500
if parsed_args.device_owner:
14271501
attrs['device_owner'] = ''
1502+
if parsed_args.pvlan_community:
1503+
attrs['pvlan_community'] = None
14281504

14291505
attrs.update(
14301506
self._parse_extra_properties(parsed_args.extra_properties)

openstackclient/tests/unit/network/v2/fakes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
from openstackclient.tests.unit import utils
5959

6060

61+
PVLAN_TYPE_COMMUNITY = 'community'
62+
PVLAN_COMMUNITY_NAME = 'community_1'
6163
RULE_TYPE_BANDWIDTH_LIMIT = 'bandwidth-limit'
6264
RULE_TYPE_DSCP_MARKING = 'dscp-marking'
6365
RULE_TYPE_MINIMUM_BANDWIDTH = 'minimum-bandwidth'
@@ -951,6 +953,7 @@ def create_one_network(attrs=None):
951953
'provider:network_type': 'vlan',
952954
'provider:physical_network': 'physnet1',
953955
'provider:segmentation_id': "400",
956+
'pvlan': "False",
954957
'router:external': True,
955958
'availability_zones': [],
956959
'availability_zone_hints': [],
@@ -1211,6 +1214,8 @@ def create_one_port(attrs=None):
12111214
'security_group_ids': [],
12121215
'status': 'ACTIVE',
12131216
'project_id': 'project-id-' + uuid.uuid4().hex,
1217+
'pvlan_type': PVLAN_TYPE_COMMUNITY,
1218+
'pvlan_community': PVLAN_COMMUNITY_NAME,
12141219
'qos_network_policy_id': 'qos-policy-id-' + uuid.uuid4().hex,
12151220
'qos_policy_id': 'qos-policy-id-' + uuid.uuid4().hex,
12161221
'tags': [],

openstackclient/tests/unit/network/v2/test_network.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class TestCreateNetworkIdentityV3(TestNetwork):
7070
'provider:network_type',
7171
'provider:physical_network',
7272
'provider:segmentation_id',
73+
'pvlan',
7374
'qos_policy_id',
7475
'router:external',
7576
'shared',
@@ -99,6 +100,7 @@ class TestCreateNetworkIdentityV3(TestNetwork):
99100
_network.provider_network_type,
100101
_network.provider_physical_network,
101102
_network.provider_segmentation_id,
103+
_network.pvlan,
102104
_network.qos_policy_id,
103105
network.RouterExternalColumn(_network.is_router_external),
104106
_network.is_shared,
@@ -189,6 +191,7 @@ def test_create_all_options(self):
189191
self.qos_policy.id,
190192
"--transparent-vlan",
191193
"--no-qinq-vlan",
194+
"--no-pvlan",
192195
"--enable-port-security",
193196
"--dns-domain",
194197
"example.org.",
@@ -210,6 +213,7 @@ def test_create_all_options(self):
210213
('qos_policy', self.qos_policy.id),
211214
('transparent_vlan', True),
212215
('qinq_vlan', False),
216+
('pvlan', False),
213217
('enable_port_security', True),
214218
('name', self._network.name),
215219
('dns_domain', 'example.org.'),
@@ -235,6 +239,7 @@ def test_create_all_options(self):
235239
'qos_policy_id': self.qos_policy.id,
236240
'vlan_transparent': True,
237241
'vlan_qinq': False,
242+
'pvlan': False,
238243
'port_security_enabled': True,
239244
'dns_domain': 'example.org.',
240245
}
@@ -326,6 +331,19 @@ def test_create_with_vlan_qinq_and_transparency_enabled(self):
326331
exceptions.CommandError, self.cmd.take_action, parsed_args
327332
)
328333

334+
def test_create_with_pvlan_and_port_security_disabled(self):
335+
arglist = [
336+
"--disable-port-security",
337+
"--pvlan",
338+
self._network.name,
339+
]
340+
verifylist = [('disable_port_security', True), ('pvlan', True)]
341+
342+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
343+
self.assertRaises(
344+
exceptions.CommandError, self.cmd.take_action, parsed_args
345+
)
346+
329347
def test_create_with_provider_segment_without_provider_type(self):
330348
arglist = [
331349
"--provider-segment",
@@ -371,6 +389,7 @@ class TestCreateNetworkIdentityV2(
371389
'name',
372390
'port_security_enabled',
373391
'project_id',
392+
'pvlan',
374393
'provider:network_type',
375394
'provider:physical_network',
376395
'provider:segmentation_id',
@@ -400,6 +419,7 @@ class TestCreateNetworkIdentityV2(
400419
_network.name,
401420
_network.is_port_security_enabled,
402421
_network.project_id,
422+
_network.pvlan,
403423
_network.provider_network_type,
404424
_network.provider_physical_network,
405425
_network.provider_segmentation_id,
@@ -996,6 +1016,23 @@ def setUp(self):
9961016
# Get the command object to test
9971017
self.cmd = network.SetNetwork(self.app, None)
9981018

1019+
def test_set_with_pvlan_and_port_security_disabled(self):
1020+
arglist = [
1021+
self._network.name,
1022+
'--disable-port-security',
1023+
'--pvlan',
1024+
]
1025+
verifylist = [
1026+
('network', self._network.name),
1027+
('disable_port_security', True),
1028+
('pvlan', True),
1029+
]
1030+
1031+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
1032+
self.assertRaises(
1033+
exceptions.CommandError, self.cmd.take_action, parsed_args
1034+
)
1035+
9991036
def test_set_this(self):
10001037
arglist = [
10011038
self._network.name,
@@ -1189,6 +1226,7 @@ class TestShowNetwork(TestNetwork):
11891226
'provider:network_type',
11901227
'provider:physical_network',
11911228
'provider:segmentation_id',
1229+
'pvlan',
11921230
'qos_policy_id',
11931231
'router:external',
11941232
'shared',
@@ -1218,6 +1256,7 @@ class TestShowNetwork(TestNetwork):
12181256
_network.provider_network_type,
12191257
_network.provider_physical_network,
12201258
_network.provider_segmentation_id,
1259+
_network.pvlan,
12211260
_network.qos_policy_id,
12221261
network.RouterExternalColumn(_network.is_router_external),
12231262
_network.is_shared,
@@ -1265,7 +1304,6 @@ def test_show_all_options(self):
12651304
self.network_client.find_network.assert_called_once_with(
12661305
self._network.name, ignore_missing=False
12671306
)
1268-
12691307
self.assertEqual(set(self.columns), set(columns))
12701308
self.assertCountEqual(self.data, data)
12711309

0 commit comments

Comments
 (0)