From d2d72192315949929c32a72f693e76fda123a893 Mon Sep 17 00:00:00 2001
From: Fernando Royo <froyo@redhat.com>
Date: Tue, 21 Jan 2025 14:23:07 +0100
Subject: [PATCH] Get "security_groups" when port list

Neutron API is accepting 'security_groups' field  in
order to return the list of security_groups attached
to a port, but openstackclient is parsing the output
over a Openstack Port object that has security_group_ids
to map. This patch sends to the Neutron API the expected
field value and replace the output key to allow the
mapping just in case '--long' argument is passed.

Closes-Bug: #2095414
Change-Id: I188edc3c620ce29d7b16497ca24fd7d972a06618
---
 openstackclient/network/v2/port.py            | 29 ++++++++++---------
 .../tests/functional/network/v2/test_port.py  | 29 ++++++++++++++++++-
 .../tests/unit/network/v2/test_port.py        | 16 +++++-----
 3 files changed, 51 insertions(+), 23 deletions(-)

diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py
index 478bca840e..cf334bcddb 100644
--- a/openstackclient/network/v2/port.py
+++ b/openstackclient/network/v2/port.py
@@ -866,35 +866,30 @@ class ListPort(command.Lister):
         network_client = self.app.client_manager.network
         identity_client = self.app.client_manager.identity
 
-        columns = (
+        columns = [
             'id',
             'name',
             'mac_address',
             'fixed_ips',
             'status',
-        )
-        column_headers = (
+        ]
+        column_headers = [
             'ID',
             'Name',
             'MAC Address',
             'Fixed IP Addresses',
             'Status',
-        )
+        ]
 
         filters = {}
         if parsed_args.long:
-            columns += (
-                'security_group_ids',
-                'device_owner',
-                'tags',
-                'trunk_details',
+            columns.extend(
+                ['security_groups', 'device_owner', 'tags', 'trunk_details']
             )
-            column_headers += (
-                'Security Groups',
-                'Device Owner',
-                'Tags',
-                'Trunk subports',
+            column_headers.extend(
+                ['Security Groups', 'Device Owner', 'Tags', 'Trunk subports']
             )
+
         if parsed_args.device_owner is not None:
             filters['device_owner'] = parsed_args.device_owner
         if parsed_args.device_id is not None:
@@ -942,6 +937,12 @@ class ListPort(command.Lister):
 
         data = network_client.ports(fields=columns, **filters)
 
+        if parsed_args.long:
+            columns = [
+                'security_group_ids' if item == 'security_groups' else item
+                for item in columns
+            ]
+
         headers, attrs = utils.calculate_header_and_attrs(
             column_headers, columns, parsed_args
         )
diff --git a/openstackclient/tests/functional/network/v2/test_port.py b/openstackclient/tests/functional/network/v2/test_port.py
index 24fbe0ea16..ef46b1f42e 100644
--- a/openstackclient/tests/functional/network/v2/test_port.py
+++ b/openstackclient/tests/functional/network/v2/test_port.py
@@ -81,10 +81,20 @@ class PortTests(common.NetworkTagTests):
         self.addCleanup(self.openstack, f'port delete {id1}')
         self.assertEqual(self.NAME, json_output.get('name'))
 
+        # sg for port2
+        sg_name1 = uuid.uuid4().hex
         json_output = self.openstack(
-            f'port create --network {self.NETWORK_NAME} {self.NAME}x',
+            f'security group create {sg_name1}',
             parse_output=True,
         )
+        sg_id1 = json_output.get('id')
+        self.addCleanup(self.openstack, f'security group delete {sg_id1}')
+        json_output = self.openstack(
+            f'port create --network {self.NETWORK_NAME} '
+            f'--security-group {sg_name1} {self.NAME}x',
+            parse_output=True,
+        )
+
         id2 = json_output.get('id')
         self.assertIsNotNone(id2)
         mac2 = json_output.get('mac_address')
@@ -113,6 +123,12 @@ class PortTests(common.NetworkTagTests):
         id_list = [item.get('ID') for item in json_output]
         self.assertIn(id1, id_list)
         self.assertIn(id2, id_list)
+        item_sg_map = {
+            item.get('ID'): item.get('Security Groups') for item in json_output
+        }
+        self.assertIn(id1, item_sg_map.keys())
+        self.assertIn(id2, item_sg_map.keys())
+        self.assertIn([sg_id1], item_sg_map.values())
 
         # Test list --mac-address
         json_output = self.openstack(
@@ -127,6 +143,17 @@ class PortTests(common.NetworkTagTests):
         self.assertNotIn(mac1, item_map.values())
         self.assertIn(mac2, item_map.values())
 
+        # Test list --security-group
+        json_output = self.openstack(
+            f'port list --security-group {sg_id1}',
+            parse_output=True,
+        )
+        item_map = {
+            item.get('ID'): item.get('Security Groups') for item in json_output
+        }
+        self.assertNotIn(id1, item_map.keys())
+        self.assertIn(id2, item_map.keys())
+
         # Test list with unknown fields
         json_output = self.openstack(
             'port list -c ID -c Name -c device_id',
diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py
index 9cea37dfc0..77561f78c2 100644
--- a/openstackclient/tests/unit/network/v2/test_port.py
+++ b/openstackclient/tests/unit/network/v2/test_port.py
@@ -24,13 +24,13 @@ from openstackclient.tests.unit.network.v2 import fakes as network_fakes
 from openstackclient.tests.unit import utils as test_utils
 
 
-LIST_FIELDS_TO_RETRIEVE = ('id', 'name', 'mac_address', 'fixed_ips', 'status')
-LIST_FIELDS_TO_RETRIEVE_LONG = (
-    'security_group_ids',
+LIST_FIELDS_TO_RETRIEVE = ['id', 'name', 'mac_address', 'fixed_ips', 'status']
+LIST_FIELDS_TO_RETRIEVE_LONG = [
+    'security_groups',
     'device_owner',
     'tags',
     'trunk_details',
-)
+]
 
 
 class TestPort(network_fakes.TestNetworkV2):
@@ -1274,15 +1274,15 @@ class TestListPort(compute_fakes.FakeClientMixin, TestPort):
     )
     _ports = (_pport, _sport1, _sport2)
 
-    columns = (
+    columns = [
         'ID',
         'Name',
         'MAC Address',
         'Fixed IP Addresses',
         'Status',
-    )
+    ]
 
-    columns_long = (
+    columns_long = [
         'ID',
         'Name',
         'MAC Address',
@@ -1292,7 +1292,7 @@ class TestListPort(compute_fakes.FakeClientMixin, TestPort):
         'Device Owner',
         'Tags',
         'Trunk subports',
-    )
+    ]
 
     data = []
     for prt in _ports: