Add Local IP API tests

Tests were verified on OVS environment.
API job definition is not changed, because OVN does not
support Local IP.

Depends-On: https://review.opendev.org/c/openstack/neutron/+/816435
Depends-On: https://review.opendev.org/c/openstack/neutron/+/818228
Change-Id: I4760db4dd9916ec895ef63573c49bde91727d142
diff --git a/neutron_tempest_plugin/api/base.py b/neutron_tempest_plugin/api/base.py
index ecdd00a..53d95ec 100644
--- a/neutron_tempest_plugin/api/base.py
+++ b/neutron_tempest_plugin/api/base.py
@@ -118,6 +118,8 @@
         cls.routers = []
         cls.floating_ips = []
         cls.port_forwardings = []
+        cls.local_ips = []
+        cls.local_ip_associations = []
         cls.metering_labels = []
         cls.service_profiles = []
         cls.flavors = []
@@ -167,6 +169,15 @@
             for floating_ip in cls.floating_ips:
                 cls._try_delete_resource(cls.delete_floatingip, floating_ip)
 
+            # Clean up Local IP Associations
+            for association in cls.local_ip_associations:
+                cls._try_delete_resource(cls.delete_local_ip_association,
+                                         association)
+            # Clean up Local IPs
+            for local_ip in cls.local_ips:
+                cls._try_delete_resource(cls.delete_local_ip,
+                                         local_ip)
+
             # Clean up conntrack helpers
             for cth in cls.conntrack_helpers:
                 cls._try_delete_resource(cls.delete_conntrack_helper, cth)
@@ -732,6 +743,98 @@
         client = client or pf.get('client') or cls.client
         client.delete_port_forwarding(pf['floatingip_id'], pf['id'])
 
+    def create_local_ip(cls, network_id=None,
+                        client=None, **kwargs):
+        """Creates a Local IP.
+
+        Create a Local IP and schedule it for later deletion.
+        If a client is passed, then it is used for deleting the IP too.
+
+        :param network_id: network ID where to create
+        By default this is 'CONF.network.public_network_id'.
+
+        :param client: network client to be used for creating and cleaning up
+        the Local IP.
+
+        :param **kwargs: additional creation parameters to be forwarded to
+        networking server.
+        """
+
+        client = client or cls.client
+        network_id = (network_id or
+                      cls.external_network_id)
+
+        local_ip = client.create_local_ip(network_id,
+                                          **kwargs)['local_ip']
+
+        # save client to be used later in cls.delete_local_ip
+        # for final cleanup
+        local_ip['client'] = client
+        cls.local_ips.append(local_ip)
+        return local_ip
+
+    @classmethod
+    def delete_local_ip(cls, local_ip, client=None):
+        """Delete Local IP
+
+        :param client: Client to be used
+        If client is not given it will use the client used to create
+        the Local IP, or cls.client if unknown.
+        """
+
+        client = client or local_ip.get('client') or cls.client
+        client.delete_local_ip(local_ip['id'])
+
+    @classmethod
+    def create_local_ip_association(cls, local_ip_id, fixed_port_id,
+                                    fixed_ip_address=None, client=None):
+        """Creates a Local IP association.
+
+        Create a Local IP Association and schedule it for later deletion.
+        If a client is passed, then it is used for deleting the association
+        too.
+
+        :param local_ip_id: The ID of the Local IP.
+
+        :param fixed_port_id: The ID of the Neutron port
+        to be associated with the Local IP
+
+        :param fixed_ip_address: The fixed IPv4 address of the Neutron
+        port to be associated with the Local IP
+
+        :param client: network client to be used for creating and cleaning up
+        the Local IP Association.
+        """
+
+        client = client or cls.client
+
+        association = client.create_local_ip_association(
+            local_ip_id, fixed_port_id,
+            fixed_ip_address)['port_association']
+
+        # save ID of Local IP  for final cleanup
+        association['local_ip_id'] = local_ip_id
+
+        # save client to be used later in
+        # cls.delete_local_ip_association for final cleanup
+        association['client'] = client
+        cls.local_ip_associations.append(association)
+        return association
+
+    @classmethod
+    def delete_local_ip_association(cls, association, client=None):
+
+        """Delete Local IP Association
+
+        :param client: Client to be used
+        If client is not given it will use the client used to create
+        the local IP association, or cls.client if unknown.
+        """
+
+        client = client or association.get('client') or cls.client
+        client.delete_local_ip_association(association['local_ip_id'],
+                                           association['fixed_port_id'])
+
     @classmethod
     def create_router_interface(cls, router_id, subnet_id):
         """Wrapper utility that returns a router interface."""