Add router check, subnet attached gateway IP update or deletion

Added a new test to ``RoutersTest``. If a subnet has a router
interface, the subnet gateway IP cannot be modified or deleted.
Both operations will raise a ``GatewayIpInUse`` exception.

Depends-On: https://review.opendev.org/c/openstack/neutron/+/904713

Related-Bug: #2036423
Change-Id: I46a39c53017589e23e03ceabc45c2f144ca2f3bb
diff --git a/neutron_tempest_plugin/api/base.py b/neutron_tempest_plugin/api/base.py
index 28d556a..7f96168 100644
--- a/neutron_tempest_plugin/api/base.py
+++ b/neutron_tempest_plugin/api/base.py
@@ -416,7 +416,7 @@
     @classmethod
     def create_subnet(cls, network, gateway='', cidr=None, mask_bits=None,
                       ip_version=None, client=None, reserve_cidr=True,
-                      **kwargs):
+                      allocation_pool_size=None, **kwargs):
         """Wrapper utility that returns a test subnet.
 
         Convenient wrapper for client.create_subnet method. It reserves and
@@ -454,10 +454,19 @@
         using the same CIDR for further subnets in the scope of the same
         test case class
 
+        :param allocation_pool_size: if the CIDR is not defined, this method
+        will assign one in ``get_subnet_cidrs``. Once done, the allocation pool
+        will be defined reserving the number of IP addresses requested,
+        starting from the end of the assigned CIDR.
+
         :param **kwargs: optional parameters to be forwarded to wrapped method
 
         [1] http://netaddr.readthedocs.io/en/latest/tutorial_01.html#supernets-and-subnets  # noqa
         """
+        def allocation_pool(cidr, pool_size):
+            start = str(netaddr.IPAddress(cidr.last) - pool_size)
+            end = str(netaddr.IPAddress(cidr.last) - 1)
+            return {'start': start, 'end': end}
 
         # allow tests to use admin client
         if not client:
@@ -480,6 +489,9 @@
                 kwargs['gateway_ip'] = str(gateway or (subnet_cidr.ip + 1))
             else:
                 kwargs['gateway_ip'] = None
+            if allocation_pool_size:
+                kwargs['allocation_pools'] = [
+                    allocation_pool(subnet_cidr, allocation_pool_size)]
             try:
                 body = client.create_subnet(
                     network_id=network['id'],
diff --git a/neutron_tempest_plugin/api/test_routers.py b/neutron_tempest_plugin/api/test_routers.py
index 4179e6d..ff5d391 100644
--- a/neutron_tempest_plugin/api/test_routers.py
+++ b/neutron_tempest_plugin/api/test_routers.py
@@ -293,6 +293,38 @@
         self.assertEqual(port_show1['device_id'], router1['id'])
         self.assertEqual(port_show2['device_id'], router2['id'])
 
+    @decorators.idempotent_id('4f8a2a1e-7fe9-4d99-9bff-5dc0e78b7e06')
+    def test_router_interface_update_and_remove_gateway_ip(self):
+        network = self.create_network()
+        subnet = self.create_subnet(network, allocation_pool_size=5)
+
+        # Update the subnet gateway IP, using the next one. Because the
+        # allocation pool is on the upper part of the CIDR, the lower IP
+        # addresses are free. This operation must be allowed because the subnet
+        # does not have yet a router port.
+        gateway_ip = netaddr.IPAddress(subnet['gateway_ip'])
+        self.client.update_subnet(subnet['id'], gateway_ip=str(gateway_ip + 1))
+
+        router = self._create_router(data_utils.rand_name('router'), True)
+        intf = self.create_router_interface(router['id'], subnet['id'])
+
+        def _status_active():
+            return self.client.show_port(
+                intf['port_id'])['port']['status'] == 'ACTIVE'
+
+        utils.wait_until_true(_status_active, exception=AssertionError)
+
+        # The gateway update must raise a ``GatewayIpInUse`` exception because
+        # there is an allocated router port.
+        gateway_ip = netaddr.IPAddress(subnet['gateway_ip'])
+        self.assertRaises(lib_exc.Conflict, self.client.update_subnet,
+                          subnet['id'], gateway_ip=str(gateway_ip + 2))
+
+        # The gateway deletion returns the same exception.
+        gateway_ip = netaddr.IPAddress(subnet['gateway_ip'])
+        self.assertRaises(lib_exc.Conflict, self.client.update_subnet,
+                          subnet['id'], gateway_ip=None)
+
 
 class RoutersIpV6Test(RoutersTest):
     _ip_version = 6