Merge "Readd jobs for neutron-fwaas master branch"
diff --git a/neutron_tempest_plugin/api/admin/test_ports.py b/neutron_tempest_plugin/api/admin/test_ports.py
index a374b81..e9a1bdb 100644
--- a/neutron_tempest_plugin/api/admin/test_ports.py
+++ b/neutron_tempest_plugin/api/admin/test_ports.py
@@ -74,6 +74,7 @@
 
     EGRESS_KBPS = 1000
     INGRESS_KBPS = 2000
+    ANY_KPPS = 500
 
     @classmethod
     def skip_checks(cls):
@@ -101,9 +102,11 @@
             cls.os_admin.qos_minimum_bandwidth_rules_client
         cls.qos_bw_limit_rule_client = \
             cls.os_admin.qos_limit_bandwidth_rules_client
+        cls.qos_minimum_packet_rate_rules_client = \
+            cls.os_admin.qos_minimum_packet_rate_rules_client
 
     def _create_qos_policy_and_port(self, network, vnic_type,
-                                    network_policy=False):
+                                    network_policy=False, min_kpps=False):
         qos_policy = self.create_qos_policy(
             name=data_utils.rand_name('test_policy'), shared=True)
         self.qos_minimum_bandwidth_rules_client.create_minimum_bandwidth_rule(
@@ -116,6 +119,13 @@
             **{'direction': const.INGRESS_DIRECTION,
                'min_kbps': self.INGRESS_KBPS})
 
+        if min_kpps:
+            self.qos_minimum_packet_rate_rules_client.\
+                create_minimum_packet_rate_rule(
+                    qos_policy_id=qos_policy['id'],
+                    **{'direction': const.ANY_DIRECTION,
+                    'min_kpps': min_kpps})
+
         port_policy_id = qos_policy['id'] if not network_policy else None
         port_kwargs = {
             'qos_policy_id': port_policy_id,
@@ -129,13 +139,14 @@
         port_id = self.create_port(network, **port_kwargs)['id']
         return self.admin_client.show_port(port_id)['port']
 
-    def _assert_resource_request(self, port, vnic_type):
+    def _assert_resource_request(self, port, vnic_type, min_kpps=None):
         self.assertIn('resource_request', port)
         vnic_trait = 'CUSTOM_VNIC_TYPE_%s' % vnic_type.upper()
         physnet_trait = 'CUSTOM_PHYSNET_%s' % self.physnet_name.upper()
         if utils.is_extension_enabled('port-resource-request-groups',
                                       'network'):
             min_bw_group_found = False
+            min_pps_group_found = False if min_kpps else True
             for rg in port['resource_request']['request_groups']:
                 self.assertIn(rg['id'],
                               port['resource_request']['same_subtree'])
@@ -151,11 +162,21 @@
                         rg['resources']
                     )
                     min_bw_group_found = True
+                elif (('NET_PACKET_RATE_KILOPACKET_PER_SEC' in
+                        rg['resources'] and min_kpps) and
+                        not min_pps_group_found):
+                    self.assertCountEqual([vnic_trait], rg['required'])
+
+                    self.assertEqual(
+                        {'NET_PACKET_RATE_KILOPACKET_PER_SEC': min_kpps},
+                        rg['resources']
+                    )
+                    min_pps_group_found = True
                 else:
                     self.fail('"resource_request" contains unexpected request '
                               'group: %s', rg)
 
-            if not min_bw_group_found:
+            if not min_bw_group_found or not min_pps_group_found:
                 self.fail('Did not find expected request groups in '
                           '"resource_request": %s',
                           port['resource_request']['request_groups'])
@@ -186,6 +207,27 @@
         port = self.admin_client.show_port(port_id)['port']
         self.assertIsNone(port['resource_request'])
 
+    @decorators.idempotent_id('5ae93aa0-408a-11ec-bbca-17b1a60f3438')
+    @utils.requires_ext(service='network',
+                        extension='port-resource-request-groups')
+    def test_port_resource_request_min_bw_and_min_pps(self):
+        port = self._create_qos_policy_and_port(
+            network=self.prov_network, vnic_type=self.vnic_type,
+            network_policy=False, min_kpps=self.ANY_KPPS)
+        port_id = port['id']
+
+        self._assert_resource_request(port, self.vnic_type,
+                                      min_kpps=self.ANY_KPPS)
+
+        # Note(lajoskatona): port-resource-request is an admin only feature,
+        # so test if non-admin user can't see the new field.
+        port = self.client.show_port(port_id)['port']
+        self.assertNotIn('resource_request', port)
+
+        self.update_port(port, **{'qos_policy_id': None})
+        port = self.admin_client.show_port(port_id)['port']
+        self.assertIsNone(port['resource_request'])
+
     @decorators.idempotent_id('7261391f-64cc-45a6-a1e3-435694c54bf5')
     def test_port_resource_request_no_provider_net_conflict(self):
         conflict = self.assertRaises(
@@ -220,8 +262,12 @@
 
     @decorators.idempotent_id('b6c34ae4-44c8-47f0-86de-7ef9866fa000')
     def test_port_resource_request_inherited_policy(self):
+        base_segm = CONF.neutron_plugin_options.provider_net_base_segm_id
+        prov_network = self.create_provider_network(
+            physnet_name=self.physnet_name,
+            start_segmentation_id=base_segm)
         port = self._create_qos_policy_and_port(
-            network=self.prov_network, vnic_type=self.vnic_type,
+            network=prov_network, vnic_type=self.vnic_type,
             network_policy=True)
 
         self._assert_resource_request(port, self.vnic_type)
diff --git a/neutron_tempest_plugin/scenario/test_dhcp.py b/neutron_tempest_plugin/scenario/test_dhcp.py
index b95eaa2..d0545e2 100644
--- a/neutron_tempest_plugin/scenario/test_dhcp.py
+++ b/neutron_tempest_plugin/scenario/test_dhcp.py
@@ -11,14 +11,18 @@
 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 #    License for the specific language governing permissions and limitations
 #    under the License.
+import netaddr
+from neutron_lib import constants
 from oslo_log import log
 from paramiko import ssh_exception as ssh_exc
 from tempest.common import utils
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
+import testtools
 
 from neutron_tempest_plugin.common import ssh
+from neutron_tempest_plugin.common import utils as neutron_utils
 from neutron_tempest_plugin import config
 from neutron_tempest_plugin.scenario import base
 
@@ -92,3 +96,96 @@
             self._log_console_output([server])
             self._log_local_network_status()
             raise
+
+
+class DHCPPortUpdateTest(base.BaseTempestTestCase):
+
+    credentials = ['primary', 'admin']
+
+    @classmethod
+    def resource_setup(cls):
+        super(DHCPPortUpdateTest, cls).resource_setup()
+        cls.rand_name = data_utils.rand_name(
+            cls.__name__.rsplit('.', 1)[-1])
+        cls.network = cls.create_network(name=cls.rand_name)
+        cls.router = cls.create_router_by_client()
+        cls.keypair = cls.create_keypair(name=cls.rand_name)
+        cls.security_group = cls.create_security_group(name=cls.rand_name)
+        cls.create_loginable_secgroup_rule(cls.security_group['id'])
+        cls.create_pingable_secgroup_rule(cls.security_group['id'])
+
+    @testtools.skipUnless(
+        CONF.neutron_plugin_options.firewall_driver == 'ovn',
+        "OVN driver is required to run this test - "
+        "LP#1942794 solution only applied to OVN")
+    @decorators.idempotent_id('8171cc68-9dbb-46ca-b065-17b5b2e26094')
+    def test_modify_dhcp_port_ip_address(self):
+        """Test Scenario
+
+        1) Create a network and a subnet with DHCP enabled
+        2) Modify the default IP address from the subnet DHCP port
+        3) Create a server in this network and check ssh connectivity
+
+        For the step 3), the server needs to obtain ssh keys from the metadata
+
+        Related bug: LP#1942794
+        """
+        # create subnet (dhcp is enabled by default)
+        subnet = self.create_subnet(network=self.network, name=self.rand_name)
+
+        def _get_dhcp_ports():
+            # in some cases, like ML2/OVS, the subnet port associated to DHCP
+            # is created with device_owner='network:dhcp'
+            dhcp_ports = self.client.list_ports(
+                network_id=self.network['id'],
+                device_owner=constants.DEVICE_OWNER_DHCP)['ports']
+            # in other cases, like ML2/OVN, the subnet port used for metadata
+            # is created with device_owner='network:distributed'
+            distributed_ports = self.client.list_ports(
+                network_id=self.network['id'],
+                device_owner=constants.DEVICE_OWNER_DISTRIBUTED)['ports']
+            self.dhcp_ports = dhcp_ports + distributed_ports
+            self.assertLessEqual(
+                len(self.dhcp_ports), 1, msg='Only one port was expected')
+            return len(self.dhcp_ports) == 1
+
+        # obtain the dhcp port
+        # in some cases this port is not created together with the subnet, but
+        # immediately after it, so some delay may be needed and that is the
+        # reason why a waiter function is used here
+        self.dhcp_ports = []
+        neutron_utils.wait_until_true(
+            lambda: _get_dhcp_ports(),
+            timeout=10)
+        dhcp_port = self.dhcp_ports[0]
+
+        # modify DHCP port IP address
+        old_dhcp_port_ip = netaddr.IPAddress(
+            dhcp_port['fixed_ips'][0]['ip_address'])
+        if str(old_dhcp_port_ip) != subnet['allocation_pools'][0]['end']:
+            new_dhcp_port_ip = str(old_dhcp_port_ip + 1)
+        else:
+            new_dhcp_port_ip = str(old_dhcp_port_ip - 1)
+        self.update_port(port=dhcp_port,
+                         fixed_ips=[{'subnet_id': subnet['id'],
+                                     'ip_address': new_dhcp_port_ip}])
+
+        # create server
+        server = self.create_server(
+            flavor_ref=CONF.compute.flavor_ref,
+            image_ref=CONF.compute.image_ref,
+            key_name=self.keypair['name'],
+            security_groups=[{'name': self.security_group['name']}],
+            networks=[{'uuid': self.network['id']}])
+
+        # attach fip to the server
+        self.create_router_interface(self.router['id'], subnet['id'])
+        server_port = self.client.list_ports(
+            network_id=self.network['id'],
+            device_id=server['server']['id'])['ports'][0]
+        fip = self.create_floatingip(port_id=server_port['id'])
+
+        # check connectivity
+        self.check_connectivity(fip['floating_ip_address'],
+                                CONF.validation.image_ssh_user,
+                                self.keypair['private_key'])
diff --git a/zuul.d/master_jobs.yaml b/zuul.d/master_jobs.yaml
index 0c16384..9e36810 100644
--- a/zuul.d/master_jobs.yaml
+++ b/zuul.d/master_jobs.yaml
@@ -459,13 +459,9 @@
       network_api_extensions: *api_extensions
       network_api_extensions_ovn:
         - vlan-transparent
-      # TODO(slaweq): Remove test_trunk_subport_lifecycle test from the
-      # blacklist when bug https://bugs.launchpad.net/neutron/+bug/1885900 will
-      # be fixed
       # TODO(jlibosva): Remove the NetworkWritableMtuTest test from the list
       # once east/west fragmentation is supported in core OVN
       tempest_exclude_regex: "\
-          (^neutron_tempest_plugin.scenario.test_trunk.TrunkTest.test_trunk_subport_lifecycle)|\
           (^neutron_tempest_plugin.scenario.test_mtu.NetworkWritableMtuTest)"
       devstack_localrc:
         Q_AGENT: ovn