Add scenario test for trunk E2E flow

Add Scenario test "test_parent_port_connectivity_after_trunk_deleted"
to verify the E2E flow of fix proposed for Bug: #1794424
"Enable delete bound trunk for linux bridge agent"

Co-Authored-By: Allain Legacy <Allain.legacy@windriver.com>

Depends-On: https://review.openstack.org/#/c/605589/
Change-Id: Ic2e02f4b5dc8d7930e251340d8be194733b0a4f7
Related-Bug: #1794424
Story: 2003889
diff --git a/.zuul.yaml b/.zuul.yaml
index bd8619f..da63618 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -163,6 +163,7 @@
           $TEMPEST_CONFIG:
             neutron_plugin_options:
               available_type_drivers: flat,vlan,local,vxlan
+              q_agent: linuxbridge
 
 - job:
     name: neutron-tempest-plugin-scenario-linuxbridge-queens
@@ -172,6 +173,14 @@
       branch_override: stable/queens
       devstack_localrc:
         NETWORK_API_EXTENSIONS: address-scope,agent,allowed-address-pairs,auto-allocated-topology,availability_zone,binding,default-subnetpools,dhcp_agent_scheduler,dns-integration,ext-gw-mode,external-net,extra_dhcp_opt,extraroute,flavors,ip-substring-filtering,l3-flavors,l3-ha,l3_agent_scheduler,logging,metering,multi-provider,net-mtu,net-mtu-writable,network-ip-availability,network_availability_zone,pagination,port-security,project-id,provider,qos,qos-fip,quotas,quota_details,rbac-policies,router,router_availability_zone,security-group,port-security-groups-filtering,segment,service-type,sorting,standard-attr-description,standard-attr-revisions,standard-attr-timestamp,standard-attr-tag,subnet_allocation,tag,tag-ext,trunk,trunk-details
+      devstack_local_conf:
+        test-config:
+          # NOTE: ignores linux bridge's trunk delete on bound port test
+          # for queens branch (as https://review.openstack.org/#/c/605589/
+          # fix will not apply for queens branch)
+          $TEMPEST_CONFIG:
+            neutron_plugin_options:
+              q_agent: None
 
 - job:
     name: neutron-tempest-plugin-scenario-linuxbridge-rocky
@@ -181,6 +190,14 @@
       branch_override: stable/rocky
       devstack_localrc:
         NETWORK_API_EXTENSIONS: address-scope,agent,allowed-address-pairs,auto-allocated-topology,availability_zone,binding,default-subnetpools,dhcp_agent_scheduler,dns-domain-ports,dns-integration,ext-gw-mode,external-net,extra_dhcp_opt,extraroute,fip-port-details,flavors,ip-substring-filtering,l3-flavors,l3-ha,l3_agent_scheduler,logging,metering,multi-provider,net-mtu,net-mtu-writable,network-ip-availability,network_availability_zone,pagination,port-security,project-id,provider,qos,qos-fip,quotas,quota_details,rbac-policies,router,router_availability_zone,security-group,port-security-groups-filtering,segment,service-type,sorting,standard-attr-description,standard-attr-revisions,standard-attr-timestamp,standard-attr-tag,subnet_allocation,tag,tag-ext,trunk,trunk-details
+      devstack_local_conf:
+        test-config:
+          # NOTE: ignores linux bridge's trunk delete on bound port test
+          # for rocky branch (as https://review.openstack.org/#/c/605589/
+          # fix will not apply for rocky branch)
+          $TEMPEST_CONFIG:
+            neutron_plugin_options:
+              q_agent: None
 
 - job:
     name: neutron-tempest-plugin-dvr-multinode-scenario
diff --git a/neutron_tempest_plugin/api/base.py b/neutron_tempest_plugin/api/base.py
index 028d901..e1eafcd 100644
--- a/neutron_tempest_plugin/api/base.py
+++ b/neutron_tempest_plugin/api/base.py
@@ -764,7 +764,7 @@
         return trunk
 
     @classmethod
-    def delete_trunk(cls, trunk, client=None):
+    def delete_trunk(cls, trunk, client=None, detach_parent_port=True):
         """Delete network trunk
 
         :param trunk: dictionary containing trunk ID (trunk['id'])
@@ -790,7 +790,7 @@
             parent_port.update(client.show_port(parent_port['id'])['port'])
             return not parent_port['device_id']
 
-        if not is_parent_port_detached():
+        if detach_parent_port and not is_parent_port_detached():
             # this could probably happen when trunk is deleted and parent port
             # has been assigned to a VM that is still running. Here we are
             # assuming that device_id points to such VM.
diff --git a/neutron_tempest_plugin/config.py b/neutron_tempest_plugin/config.py
index 030a126..1bc9617 100644
--- a/neutron_tempest_plugin/config.py
+++ b/neutron_tempest_plugin/config.py
@@ -53,6 +53,10 @@
                     '"mtu":<MTU> - integer '
                     '"cidr"<SUBNET/MASK> - string '
                     '"provider:segmentation_id":<VLAN_ID> - integer'),
+    cfg.StrOpt('q_agent',
+               default=None,
+               choices=['None', 'linuxbridge', 'ovs', 'sriov'],
+               help='Agent used for devstack@q-agt.service'),
 
     # Option for feature to connect via SSH to VMs using an intermediate SSH
     # server
diff --git a/neutron_tempest_plugin/scenario/test_trunk.py b/neutron_tempest_plugin/scenario/test_trunk.py
index 1903180..85b16cb 100644
--- a/neutron_tempest_plugin/scenario/test_trunk.py
+++ b/neutron_tempest_plugin/scenario/test_trunk.py
@@ -47,8 +47,8 @@
         # setup basic topology for servers we can log into
         cls.network = cls.create_network()
         cls.subnet = cls.create_subnet(cls.network)
-        router = cls.create_router_by_client()
-        cls.create_router_interface(router['id'], cls.subnet['id'])
+        cls.router = cls.create_router_by_client()
+        cls.create_router_interface(cls.router['id'], cls.subnet['id'])
         cls.keypair = cls.create_keypair()
         cls.secgroup = cls.os_primary.network_client.create_security_group(
             name=data_utils.rand_name('secgroup'))
@@ -95,6 +95,27 @@
         t = self.client.show_trunk(trunk_id)['trunk']
         return t['status'] == 'ACTIVE'
 
+    def _create_server_with_network(self, network, use_advanced_image=False):
+        port = self.create_port(network, security_groups=[
+            self.secgroup['security_group']['id']])
+        server, fip = self._create_server_with_fip(
+            port['id'], use_advanced_image=use_advanced_image)
+        ssh_user = CONF.validation.image_ssh_user
+        if use_advanced_image:
+            ssh_user = CONF.neutron_plugin_options.advanced_image_ssh_user
+
+        server_ssh_client = ssh.Client(
+            fip['floating_ip_address'],
+            ssh_user,
+            pkey=self.keypair['private_key'])
+
+        return {
+            'server': server,
+            'fip': fip,
+            'ssh_client': server_ssh_client,
+            'port': port,
+        }
+
     def _create_server_with_port_and_subport(self, vlan_network, vlan_tag,
                                              use_advanced_image=False):
         parent_port = self.create_port(self.network, security_groups=[
@@ -107,7 +128,7 @@
             'port_id': port_for_subport['id'],
             'segmentation_type': 'vlan',
             'segmentation_id': vlan_tag}
-        self.create_trunk(parent_port, [subport])
+        trunk = self.create_trunk(parent_port, [subport])
 
         server, fip = self._create_server_with_fip(
             parent_port['id'], use_advanced_image=use_advanced_image)
@@ -126,6 +147,8 @@
             'fip': fip,
             'ssh_client': server_ssh_client,
             'subport': port_for_subport,
+            'parentport': parent_port,
+            'trunk': trunk,
         }
 
     def _wait_for_server(self, server, advanced_image=False):
@@ -260,3 +283,78 @@
             servers[1]['subport']['fixed_ips'][0]['ip_address'],
             should_succeed=True
         )
+
+    @testtools.skipUnless(
+          CONF.neutron_plugin_options.advanced_image_ref,
+          "Advanced image is required to run this test.")
+    @testtools.skipUnless(
+          CONF.neutron_plugin_options.q_agent == "linuxbridge",
+          "Linux bridge agent is required to run this test.")
+    @decorators.idempotent_id('d61cbdf6-1896-491c-b4b4-871caf7fbffe')
+    def test_parent_port_connectivity_after_trunk_deleted_lb(self):
+        vlan_tag = 10
+
+        vlan_network = self.create_network()
+        vlan_subnet = self.create_subnet(vlan_network)
+        self.create_router_interface(self.router['id'], vlan_subnet['id'])
+
+        trunk_network_server = self._create_server_with_port_and_subport(
+            vlan_network, vlan_tag, use_advanced_image=True)
+        normal_network_server = self._create_server_with_network(self.network)
+        vlan_network_server = self._create_server_with_network(vlan_network)
+
+        self._wait_for_server(trunk_network_server, advanced_image=True)
+        # Configure VLAN interfaces on server
+        command = CONFIGURE_VLAN_INTERFACE_COMMANDS % {'tag': vlan_tag}
+        trunk_network_server['ssh_client'].exec_command(command)
+        out = trunk_network_server['ssh_client'].exec_command(
+            'PATH=$PATH:/usr/sbin;ip addr list')
+        LOG.debug("Interfaces on server %s: %s", trunk_network_server, out)
+
+        self._wait_for_server(normal_network_server)
+        self._wait_for_server(vlan_network_server)
+
+        # allow intra-securitygroup traffic
+        rule = self.client.create_security_group_rule(
+            security_group_id=self.secgroup['security_group']['id'],
+            direction='ingress', ethertype='IPv4', protocol='icmp',
+            remote_group_id=self.secgroup['security_group']['id'])
+        self.addCleanup(self.client.delete_security_group_rule,
+                        rule['security_group_rule']['id'])
+
+        # Ping from trunk_network_server to normal_network_server
+        # via parent port
+        self.check_remote_connectivity(
+            trunk_network_server['ssh_client'],
+            normal_network_server['port']['fixed_ips'][0]['ip_address'],
+            should_succeed=True
+        )
+
+        # Ping from trunk_network_server to vlan_network_server via VLAN
+        # interface should success
+        self.check_remote_connectivity(
+            trunk_network_server['ssh_client'],
+            vlan_network_server['port']['fixed_ips'][0]['ip_address'],
+            should_succeed=True
+        )
+
+        # Delete the trunk
+        self.delete_trunk(trunk_network_server['trunk'],
+            detach_parent_port=False)
+        LOG.debug("Trunk %s is deleted.", trunk_network_server['trunk']['id'])
+
+        # Ping from trunk_network_server to normal_network_server
+        # via parent port success after trunk deleted
+        self.check_remote_connectivity(
+            trunk_network_server['ssh_client'],
+            normal_network_server['port']['fixed_ips'][0]['ip_address'],
+            should_succeed=True
+        )
+
+        # Ping from trunk_network_server to vlan_network_server via VLAN
+        # interface should fail after trunk deleted
+        self.check_remote_connectivity(
+            trunk_network_server['ssh_client'],
+            vlan_network_server['port']['fixed_ips'][0]['ip_address'],
+            should_succeed=False
+        )
diff --git a/neutron_tempest_plugin/services/network/json/network_client.py b/neutron_tempest_plugin/services/network/json/network_client.py
index 3b07e24..db6c152 100644
--- a/neutron_tempest_plugin/services/network/json/network_client.py
+++ b/neutron_tempest_plugin/services/network/json/network_client.py
@@ -871,6 +871,13 @@
         body = jsonutils.loads(body)
         return service_client.ResponseBody(resp, body)
 
+    def delete_security_group_rule(self, security_group_rule_id):
+        uri = '%s/security-group-rules/%s' % (self.uri_prefix,
+                                              security_group_rule_id)
+        resp, body = self.delete(uri)
+        self.expected_success(204, resp.status)
+        return service_client.ResponseBody(resp, body)
+
     def list_security_groups(self, **kwargs):
         post_body = {'security_groups': kwargs}
         body = jsonutils.dumps(post_body)