Merge "neutron-lib: Skip tempest job for changes in unit tests code"
diff --git a/devstack/functions.sh b/devstack/functions.sh
index 8d8a4bf..f758ff6 100644
--- a/devstack/functions.sh
+++ b/devstack/functions.sh
@@ -85,3 +85,15 @@
     fi
     iniset $TEMPEST_CONFIG neutron_plugin_options advanced_image_flavor_ref $flavor_ref
 }
+
+
+function create_flavor_for_advance_image {
+    local name=$1
+    local ram=$2
+    local disk=$3
+    local vcpus=$4
+
+    if [[ -z $(openstack flavor list | grep $name) ]]; then
+        openstack flavor create --ram $ram --disk $disk --vcpus $vcpus $name
+    fi
+}
diff --git a/devstack/plugin.sh b/devstack/plugin.sh
index 7a46014..2e3ac20 100644
--- a/devstack/plugin.sh
+++ b/devstack/plugin.sh
@@ -20,6 +20,7 @@
         test-config)
             echo_summary "Configuring neutron-tempest-plugin tempest options"
             configure_advanced_image
+            create_flavor_for_advance_image ntp_image_384M 384 4 1
             configure_flavor_for_advanced_image
     esac
 fi
diff --git a/neutron_tempest_plugin/api/test_address_groups.py b/neutron_tempest_plugin/api/test_address_groups.py
index 478d3b2..69f22d0 100644
--- a/neutron_tempest_plugin/api/test_address_groups.py
+++ b/neutron_tempest_plugin/api/test_address_groups.py
@@ -28,6 +28,119 @@
 ADDRESS_GROUP_NAME = 'test-address-group'
 
 
+class AddressGroupTest(base.BaseAdminNetworkTest):
+
+    credentials = ['primary', 'admin']
+    required_extensions = ['address-group']
+
+    @decorators.idempotent_id('496fef1b-22ce-483b-ab93-d28bf46954b0')
+    def test_address_group_lifecycle(self):
+        ag_description = "Test AG description"
+        ag_name = data_utils.rand_name(ADDRESS_GROUP_NAME)
+        addresses = ['10.10.10.3/32', '192.168.0.10/24', '2001:db8::f00/64']
+        expected_addresses = [
+            '10.10.10.3/32', '192.168.0.0/24', '2001:db8::/64']
+        ag = self.create_address_group(
+            description=ag_description,
+            name=ag_name,
+            addresses=addresses)
+        self.assertEqual(ag_description, ag['description'])
+        self.assertEqual(ag_name, ag['name'])
+        self.assertListEqual(
+            sorted(expected_addresses), sorted(ag['addresses']))
+
+        new_description = 'New AG description'
+        updated_ag = self.client.update_address_group(
+            ag['id'], description=new_description)['address_group']
+        self.assertEqual(new_description, updated_ag['description'])
+        self.assertEqual(ag_name, updated_ag['name'])
+        self.assertListEqual(
+            sorted(expected_addresses), sorted(updated_ag['addresses']))
+
+        self.client.delete_address_group(ag['id'])
+        with testtools.ExpectedException(exceptions.NotFound):
+            self.client.show_address_group(ag['id'])
+
+    @decorators.idempotent_id('8a42029a-40eb-4b44-a7cf-38500046f9b8')
+    def test_address_group_create_with_wrong_address(self):
+        with testtools.ExpectedException(exceptions.BadRequest):
+            self.create_address_group(
+                name=data_utils.rand_name(ADDRESS_GROUP_NAME),
+                addresses=['10.20.30.40'])
+
+        with testtools.ExpectedException(exceptions.BadRequest):
+            self.create_address_group(
+                name=data_utils.rand_name(ADDRESS_GROUP_NAME),
+                addresses=['this is bad IP address'])
+
+    @decorators.idempotent_id('27c03921-bb12-4b9a-b32e-7083bc90ff1f')
+    def test_edit_addresses_in_address_group(self):
+        addresses = ['10.10.10.3/32', '192.168.0.10/24', '2001:db8::f00/64']
+        expected_addresses = [
+            '10.10.10.3/32', '192.168.0.0/24', '2001:db8::/64']
+        ag = self.create_address_group(
+            name=data_utils.rand_name(ADDRESS_GROUP_NAME),
+            addresses=addresses)
+        self.assertListEqual(
+            sorted(expected_addresses), sorted(ag['addresses']))
+
+        added_addresses = ['10.20.30.40/32']
+        self.client.add_addresses_to_address_group(
+            ag['id'], addresses=added_addresses)
+        updated_ag = self.client.show_address_group(ag['id'])['address_group']
+        expected_addresses += added_addresses
+        self.assertListEqual(
+            sorted(expected_addresses), sorted(updated_ag['addresses']))
+
+        removed_addresses = [expected_addresses.pop(0)]
+        self.client.remove_addresses_from_address_group(
+            ag['id'], addresses=removed_addresses)
+        updated_ag = self.client.show_address_group(ag['id'])['address_group']
+        self.assertListEqual(
+            sorted(expected_addresses), sorted(updated_ag['addresses']))
+
+    @decorators.idempotent_id('feec6747-b4b8-49e3-8cff-817d3f097f2c')
+    def test_add_wrong_address_to_address_group(self):
+        addresses = ['10.10.10.3/32', '192.168.0.10/24', '2001:db8::f00/64']
+        expected_addresses = [
+            '10.10.10.3/32', '192.168.0.0/24', '2001:db8::/64']
+        ag = self.create_address_group(
+            name=data_utils.rand_name(ADDRESS_GROUP_NAME),
+            addresses=addresses)
+        self.assertListEqual(
+            sorted(expected_addresses), sorted(ag['addresses']))
+        with testtools.ExpectedException(exceptions.BadRequest):
+            self.client.add_addresses_to_address_group(
+                ag['id'], addresses=['this is bad IP address'])
+        updated_ag = self.client.show_address_group(ag['id'])['address_group']
+        self.assertListEqual(
+            sorted(expected_addresses), sorted(updated_ag['addresses']))
+
+    @decorators.idempotent_id('74f6fd4c-257b-4725-887b-470e96960e24')
+    def test_remove_wrong_address_from_address_group(self):
+        addresses = ['10.10.10.3/32', '192.168.0.10/24', '2001:db8::f00/64']
+        expected_addresses = [
+            '10.10.10.3/32', '192.168.0.0/24', '2001:db8::/64']
+        ag = self.create_address_group(
+            name=data_utils.rand_name(ADDRESS_GROUP_NAME),
+            addresses=addresses)
+        self.assertListEqual(
+            sorted(expected_addresses), sorted(ag['addresses']))
+        with testtools.ExpectedException(exceptions.NotFound):
+            self.client.remove_addresses_from_address_group(
+                ag['id'], addresses=['10.200.200.200'])
+        updated_ag = self.client.show_address_group(ag['id'])['address_group']
+        self.assertListEqual(
+            sorted(expected_addresses), sorted(updated_ag['addresses']))
+
+        with testtools.ExpectedException(exceptions.BadRequest):
+            self.client.remove_addresses_from_address_group(
+                ag['id'], addresses=['this is bad IP address'])
+        updated_ag = self.client.show_address_group(ag['id'])['address_group']
+        self.assertListEqual(
+            sorted(expected_addresses), sorted(updated_ag['addresses']))
+
+
 class RbacSharedAddressGroupTest(base.BaseAdminNetworkTest):
 
     force_tenant_isolation = True
diff --git a/neutron_tempest_plugin/api/test_extra_dhcp_options.py b/neutron_tempest_plugin/api/test_extra_dhcp_options.py
index 844666a..91c270d 100644
--- a/neutron_tempest_plugin/api/test_extra_dhcp_options.py
+++ b/neutron_tempest_plugin/api/test_extra_dhcp_options.py
@@ -13,6 +13,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from neutron_lib import constants
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
 
@@ -48,9 +49,14 @@
         cls.ip_server = ('123.123.123.45' if cls._ip_version == 4
                          else '2015::badd')
         cls.extra_dhcp_opts = [
-            {'opt_value': 'pxelinux.0', 'opt_name': 'bootfile-name'},
-            {'opt_value': cls.ip_tftp, 'opt_name': 'tftp-server'},
-            {'opt_value': cls.ip_server, 'opt_name': 'server-ip-address'}
+            {'opt_value': 'pxelinux.0',
+             'opt_name': 'bootfile-name'},  # default ip_version is 4
+            {'opt_value': cls.ip_tftp,
+             'opt_name': 'tftp-server',
+             'ip_version': cls._ip_version},
+            {'opt_value': cls.ip_server,
+             'opt_name': 'server-ip-address',
+             'ip_version': cls._ip_version}
         ]
 
     @decorators.idempotent_id('d2c17063-3767-4a24-be4f-a23dbfa133c9')
@@ -85,8 +91,11 @@
         self.assertEqual(len(retrieved), len(extra_dhcp_opts))
         for retrieved_option in retrieved:
             for option in extra_dhcp_opts:
+                # default ip_version is 4
+                ip_version = option.get('ip_version', constants.IP_VERSION_4)
                 if (retrieved_option['opt_value'] == option['opt_value'] and
-                    retrieved_option['opt_name'] == option['opt_name']):
+                    retrieved_option['opt_name'] == option['opt_name'] and
+                    retrieved_option['ip_version'] == ip_version):
                     break
             else:
                 self.fail('Extra DHCP option not found in port %s' %
diff --git a/neutron_tempest_plugin/scenario/test_ipv6.py b/neutron_tempest_plugin/scenario/test_ipv6.py
index 4237d4f..32fb581 100644
--- a/neutron_tempest_plugin/scenario/test_ipv6.py
+++ b/neutron_tempest_plugin/scenario/test_ipv6.py
@@ -43,35 +43,41 @@
     """
     ip_command = ip.IPCommand(ssh)
     nic = ip_command.get_nic_name_by_mac(ipv6_port['mac_address'])
+    ip_command.set_link(nic, "up")
 
-    # NOTE(slaweq): on RHEL based OS ifcfg file for new interface is
-    # needed to make IPv6 working on it, so if
-    # /etc/sysconfig/network-scripts directory exists ifcfg-%(nic)s file
-    # should be added in it
-    if sysconfig_network_scripts_dir_exists(ssh):
+
+def configure_eth_connection_profile_NM(ssh):
+    """Prepare a Network manager profile for ipv6 port
+
+    By default the NetworkManager uses IPv6 privacy
+    format it isn't supported by neutron then we create
+    a ether profile with eui64 supported format
+
+    @param ssh: RemoteClient ssh instance to server
+    """
+    # NOTE(ccamposr): on RHEL based OS we need a ether profile with
+    # eui64 format
+    if nmcli_command_exists(ssh):
         try:
-            ssh.execute_script(
-                'echo -e "DEVICE=%(nic)s\\nNAME=%(nic)s\\nIPV6INIT=yes" | '
-                'tee /etc/sysconfig/network-scripts/ifcfg-%(nic)s; '
-                'nmcli connection reload' % {'nic': nic},
-                become_root=True)
-            ssh.execute_script('nmcli connection up %s' % nic,
+            ssh.execute_script('nmcli connection add type ethernet con-name '
+                               'ether ifname "*"', become_root=True)
+            ssh.execute_script('nmcli con mod ether ipv6.addr-gen-mode eui64',
                                become_root=True)
+
         except lib_exc.SSHExecCommandFailed as e:
             # NOTE(slaweq): Sometimes it can happen that this SSH command
             # will fail because of some error from network manager in
             # guest os.
             # But even then doing ip link set up below is fine and
             # IP address should be configured properly.
-            LOG.debug("Error during restarting %(nic)s interface on "
-                      "instance. Error message: %(error)s",
-                      {'nic': nic, 'error': e})
-    ip_command.set_link(nic, "up")
+            LOG.debug("Error creating NetworkManager profile. "
+                      "Error message: %(error)s",
+                      {'error': e})
 
 
-def sysconfig_network_scripts_dir_exists(ssh):
+def nmcli_command_exists(ssh):
     return "False" not in ssh.execute_script(
-        'test -d /etc/sysconfig/network-scripts/ || echo "False"')
+        'if ! type nmcli > /dev/null ; then echo "False"; fi')
 
 
 class IPv6Test(base.BaseTempestTestCase):
@@ -165,6 +171,8 @@
 
         # And plug VM to the second IPv6 network
         ipv6_port = self.create_port(ipv6_networks[1])
+        # Add NetworkManager profile with ipv6 eui64 format to guest OS
+        configure_eth_connection_profile_NM(ssh_client)
         self.create_interface(vm['id'], ipv6_port['id'])
         ip.wait_for_interface_status(
             self.os_primary.interfaces_client, vm['id'],
diff --git a/neutron_tempest_plugin/services/network/json/network_client.py b/neutron_tempest_plugin/services/network/json/network_client.py
index 2678d73..d4eff84 100644
--- a/neutron_tempest_plugin/services/network/json/network_client.py
+++ b/neutron_tempest_plugin/services/network/json/network_client.py
@@ -1126,3 +1126,21 @@
             self.uri_prefix, resource_type, resource_id, tag)
         resp, body = self.delete(uri)
         self.expected_success(204, resp.status)
+
+    def add_addresses_to_address_group(self, address_group_id, addresses):
+        uri = '%s/address-groups/%s/add_addresses' % (
+            self.uri_prefix, address_group_id)
+        request_body = {'addresses': addresses}
+        resp, response_body = self.put(uri, jsonutils.dumps(request_body))
+        self.expected_success(200, resp.status)
+        return service_client.ResponseBody(
+            resp, jsonutils.loads(response_body))
+
+    def remove_addresses_from_address_group(self, address_group_id, addresses):
+        uri = '%s/address-groups/%s/remove_addresses' % (
+            self.uri_prefix, address_group_id)
+        request_body = {'addresses': addresses}
+        resp, response_body = self.put(uri, jsonutils.dumps(request_body))
+        self.expected_success(200, resp.status)
+        return service_client.ResponseBody(
+            resp, jsonutils.loads(response_body))
diff --git a/zuul.d/base.yaml b/zuul.d/base.yaml
index 04fe323..880e64f 100644
--- a/zuul.d/base.yaml
+++ b/zuul.d/base.yaml
@@ -112,7 +112,7 @@
         CIRROS_VERSION: 0.5.1
         IMAGE_URLS: https://cloud-images.ubuntu.com/releases/bionic/release/ubuntu-18.04-server-cloudimg-amd64.img
         ADVANCED_IMAGE_NAME: ubuntu-18.04-server-cloudimg-amd64
-        ADVANCED_INSTANCE_TYPE: ds512M
+        ADVANCED_INSTANCE_TYPE: ntp_image_384M
         ADVANCED_INSTANCE_USER: ubuntu
         BUILD_TIMEOUT: 784
       tempest_concurrency: 3  # out of 4
diff --git a/zuul.d/master_jobs.yaml b/zuul.d/master_jobs.yaml
index 18bc638..f9fdd05 100644
--- a/zuul.d/master_jobs.yaml
+++ b/zuul.d/master_jobs.yaml
@@ -267,6 +267,34 @@
       - ^neutron/plugins/ml2/drivers/ovn/.*$
 
 - job:
+    name: neutron-tempest-plugin-scenario-openvswitch-distributed-dhcp
+    parent: neutron-tempest-plugin-scenario-openvswitch
+    timeout: 10000
+    vars:
+      # NOTE: DHCP extra options and dns services aren't supported with
+      # distributed DHCP L2 agent extension
+      tempest_exclude_regex: "\
+          (^neutron_tempest_plugin.scenario.test_dhcp.DHCPTest.test_extra_dhcp_opts)|\
+          (^neutron_tempest_plugin.scenario.test_internal_dns.InternalDNSTest.test_dns_domain_and_name)"
+      devstack_services:
+        q-dhcp: false
+        q-distributed-dhcp: true
+
+- job:
+    name: neutron-tempest-plugin-scenario-openvswitch-iptables_hybrid-distributed-dhcp
+    parent: neutron-tempest-plugin-scenario-openvswitch-iptables_hybrid
+    timeout: 10000
+    vars:
+      # NOTE: DHCP extra options and dns services aren't supported with
+      # distributed DHCP L2 agent extension
+      tempest_exclude_regex: "\
+          (^neutron_tempest_plugin.scenario.test_dhcp.DHCPTest.test_extra_dhcp_opts)|\
+          (^neutron_tempest_plugin.scenario.test_internal_dns.InternalDNSTest.test_dns_domain_and_name)"
+      devstack_services:
+        q-dhcp: false
+        q-distributed-dhcp: true
+
+- job:
     name: neutron-tempest-plugin-scenario-linuxbridge
     parent: neutron-tempest-plugin-scenario
     timeout: 10000
@@ -492,7 +520,7 @@
         CIRROS_VERSION: 0.5.1
         IMAGE_URLS: https://cloud-images.ubuntu.com/releases/bionic/release/ubuntu-18.04-server-cloudimg-amd64.img
         ADVANCED_IMAGE_NAME: ubuntu-18.04-server-cloudimg-amd64
-        ADVANCED_INSTANCE_TYPE: ds512M
+        ADVANCED_INSTANCE_TYPE: ntp_image_384M
         ADVANCED_INSTANCE_USER: ubuntu
         BUILD_TIMEOUT: 784
         Q_AGENT: openvswitch
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index 969f80a..08fb58c 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -20,6 +20,8 @@
     experimental:
       jobs:
         - neutron-tempest-plugin-dvr-multinode-scenario
+        - neutron-tempest-plugin-scenario-openvswitch-distributed-dhcp
+        - neutron-tempest-plugin-scenario-openvswitch-iptables_hybrid-distributed-dhcp
 
 
 - project-template:
diff --git a/zuul.d/rocky_jobs.yaml b/zuul.d/rocky_jobs.yaml
index 11e4c9a..47f88f2 100644
--- a/zuul.d/rocky_jobs.yaml
+++ b/zuul.d/rocky_jobs.yaml
@@ -435,7 +435,7 @@
         CIRROS_VERSION: 0.5.1
         IMAGE_URLS: https://cloud-images.ubuntu.com/releases/bionic/release/ubuntu-18.04-server-cloudimg-amd64.img
         ADVANCED_IMAGE_NAME: ubuntu-18.04-server-cloudimg-amd64
-        ADVANCED_INSTANCE_TYPE: ds512M
+        ADVANCED_INSTANCE_TYPE: ntp_image_384M
         ADVANCED_INSTANCE_USER: ubuntu
         BUILD_TIMEOUT: 784
         TEMPEST_PLUGINS: /opt/stack/neutron-tempest-plugin