Skip to content

Commit f7b6d14

Browse files
committed
Add network trunk subport commands
The ``network subport list`` command requires the trunk to be passed as a ``--trunk`` option, which reads backwards for a resource that cannot exist without a parent trunk. Likewise, managing subports means reaching for ``network trunk set --subport`` and ``network trunk unset --subport``, which bury the operation in a generic set/unset command and require a nested key=value blob to name the port. Add three cmds that take the trunk as a positional argument instead: openstack network trunk subport list <trunk> openstack network trunk subport add <trunk> <port> [--segmentation-type <type>] [--segmentation-id <id>] openstack network trunk subport remove <trunk> <port> [<port> ...] ``add`` uses discrete ``--segmentation-type`` and ``--segmentation-id`` options rather than the nested key=value form, and ``--segmentation-id`` is parsed as an integer so argparse reports bad input directly. ``remove`` accepts multiple ports, mirroring ``network trunk delete``. This change is purely additive. The existing ``network subport list``, ``network trunk set`` and ``network trunk unset`` commands are unchanged, so nothing breaks for current users. The subport listing body is extracted into a ``_list_trunk_subports()`` helper shared by both list commands. No documentation change is needed as the existing ``network trunk *`` autoprogram-cliff glob already picks up the new commands. Assisted-By: Claude Opus 5 Change-Id: I85e07e963be41196452760b844a40e7f3223d379 Signed-off-by: Doug Goldstein <cardoe@cardoe.com>
1 parent 01668c8 commit f7b6d14

5 files changed

Lines changed: 488 additions & 19 deletions

File tree

openstackclient/network/v2/network_trunk.py

Lines changed: 134 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -308,31 +308,116 @@ def take_action(
308308
self, parsed_args: argparse.Namespace
309309
) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]:
310310
network_client = self.app.client_manager.network
311-
trunk_id = network_client.find_trunk(
311+
return _list_trunk_subports(network_client, parsed_args.trunk)
312+
313+
314+
class ListNetworkTrunkSubport(command.Lister):
315+
"""List all subports for a given network trunk"""
316+
317+
def get_parser(self, prog_name: str) -> argparse.ArgumentParser:
318+
parser = super().get_parser(prog_name)
319+
parser.add_argument(
320+
'trunk',
321+
metavar="<trunk>",
322+
help=_("List subports belonging to this trunk (name or ID)"),
323+
)
324+
return parser
325+
326+
def take_action(
327+
self, parsed_args: argparse.Namespace
328+
) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]:
329+
network_client = self.app.client_manager.network
330+
return _list_trunk_subports(network_client, parsed_args.trunk)
331+
332+
333+
class AddNetworkTrunkSubport(command.Command):
334+
"""Add a subport to a given network trunk"""
335+
336+
def get_parser(self, prog_name: str) -> argparse.ArgumentParser:
337+
parser = super().get_parser(prog_name)
338+
parser.add_argument(
339+
'trunk',
340+
metavar="<trunk>",
341+
help=_("Trunk to add the subport to (name or ID)"),
342+
)
343+
parser.add_argument(
344+
'port',
345+
metavar="<port>",
346+
help=_("Port to add as a subport (name or ID)"),
347+
)
348+
parser.add_argument(
349+
'--segmentation-type',
350+
metavar="<segmentation-type>",
351+
help=_("Segmentation type of the subport, e.g. 'vlan'"),
352+
)
353+
parser.add_argument(
354+
'--segmentation-id',
355+
metavar="<segmentation-id>",
356+
type=int,
357+
help=_("Segmentation ID of the subport"),
358+
)
359+
return parser
360+
361+
def take_action(self, parsed_args: argparse.Namespace) -> None:
362+
network_client = self.app.client_manager.network
363+
trunk = network_client.find_trunk(
312364
parsed_args.trunk,
313365
ignore_missing=False,
314366
)
315-
data = network_client.get_trunk_subports(trunk_id)
316-
headers: tuple[str, ...] = (
317-
'Port',
318-
'Segmentation Type',
319-
'Segmentation ID',
367+
subport: dict[str, Any] = {
368+
'port_id': network_client.find_port(
369+
parsed_args.port,
370+
ignore_missing=False,
371+
).id
372+
}
373+
if parsed_args.segmentation_type is not None:
374+
subport['segmentation_type'] = parsed_args.segmentation_type
375+
if parsed_args.segmentation_id is not None:
376+
subport['segmentation_id'] = parsed_args.segmentation_id
377+
try:
378+
network_client.add_trunk_subports(trunk, [subport])
379+
except Exception as e:
380+
msg = _("Failed to add subport to trunk '%(t)s': %(e)s") % {
381+
't': parsed_args.trunk,
382+
'e': e,
383+
}
384+
raise exceptions.CommandError(msg)
385+
386+
387+
class RemoveNetworkTrunkSubport(command.Command):
388+
"""Remove subport(s) from a given network trunk"""
389+
390+
def get_parser(self, prog_name: str) -> argparse.ArgumentParser:
391+
parser = super().get_parser(prog_name)
392+
parser.add_argument(
393+
'trunk',
394+
metavar="<trunk>",
395+
help=_("Trunk to remove the subport(s) from (name or ID)"),
320396
)
321-
columns: tuple[str, ...] = (
322-
'port_id',
323-
'segmentation_type',
324-
'segmentation_id',
397+
parser.add_argument(
398+
'port',
399+
metavar="<port>",
400+
nargs="+",
401+
help=_("Port(s) to remove as a subport (name or ID)"),
325402
)
326-
return (
327-
headers,
328-
(
329-
osc_utils.get_dict_properties(
330-
s,
331-
columns,
332-
)
333-
for s in data[SUB_PORTS]
334-
),
403+
return parser
404+
405+
def take_action(self, parsed_args: argparse.Namespace) -> None:
406+
network_client = self.app.client_manager.network
407+
trunk = network_client.find_trunk(
408+
parsed_args.trunk,
409+
ignore_missing=False,
335410
)
411+
attrs = [
412+
{
413+
'port_id': network_client.find_port(
414+
port,
415+
ignore_missing=False,
416+
).id
417+
}
418+
for port in parsed_args.port
419+
]
420+
network_client.delete_trunk_subports(trunk, attrs)
336421

337422

338423
class UnsetNetworkTrunk(command.Command):
@@ -383,6 +468,36 @@ def _get_columns(
383468
)
384469

385470

471+
def _list_trunk_subports(
472+
network_client: network_v2.Proxy, trunk: str
473+
) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]:
474+
trunk_obj = network_client.find_trunk(
475+
trunk,
476+
ignore_missing=False,
477+
)
478+
data = network_client.get_trunk_subports(trunk_obj)
479+
headers: tuple[str, ...] = (
480+
'Port',
481+
'Segmentation Type',
482+
'Segmentation ID',
483+
)
484+
columns: tuple[str, ...] = (
485+
'port_id',
486+
'segmentation_type',
487+
'segmentation_id',
488+
)
489+
return (
490+
headers,
491+
(
492+
osc_utils.get_dict_properties(
493+
s,
494+
columns,
495+
)
496+
for s in data[SUB_PORTS]
497+
),
498+
)
499+
500+
386501
def _get_attrs_for_trunk(
387502
network_client: network_v2.Proxy,
388503
identity_client: identity_v2.Proxy | identity_v3.Proxy,

openstackclient/tests/functional/network/v2/test_network_trunk.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,41 @@ def test_network_trunk_list_subports(self):
146146
],
147147
json_output,
148148
)
149+
150+
def test_network_trunk_subport_add_remove_list(self):
151+
trunk_name = uuid.uuid4().hex
152+
json_output = json.loads(
153+
self.openstack(
154+
f'network trunk create {trunk_name} --parent-port {self.parent_port_name} -f json '
155+
)
156+
)
157+
self.addCleanup(self.openstack, 'network trunk delete ' + trunk_name)
158+
self.assertEqual(trunk_name, json_output['name'])
159+
160+
# Add subport to trunk
161+
self.openstack(
162+
f'network trunk subport add {trunk_name} {self.sub_port_name} '
163+
'--segmentation-type vlan --segmentation-id 42'
164+
)
165+
json_output = json.loads(
166+
self.openstack(f'network trunk subport list {trunk_name} -f json')
167+
)
168+
self.assertEqual(
169+
[
170+
{
171+
'Port': self.sub_port_id,
172+
'Segmentation ID': 42,
173+
'Segmentation Type': 'vlan',
174+
}
175+
],
176+
json_output,
177+
)
178+
179+
# Remove subport from trunk
180+
self.openstack(
181+
f'network trunk subport remove {trunk_name} {self.sub_port_name}'
182+
)
183+
json_output = json.loads(
184+
self.openstack(f'network trunk subport list {trunk_name} -f json')
185+
)
186+
self.assertEqual([], json_output)

0 commit comments

Comments
 (0)