Merge "Add port-range remote_group security group testcase"
diff --git a/.zuul.yaml b/.zuul.yaml
index e51e074..fa0e861 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -506,9 +506,7 @@
       tempest_test_regex: ^neutron_tempest_plugin\.scenario
       devstack_localrc:
         PHYSICAL_NETWORK: default
-        DOWNLOAD_DEFAULT_IMAGES: false
-        IMAGE_URLS: "http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-i386-disk.img,https://cloud-images.ubuntu.com/releases/xenial/release/ubuntu-16.04-server-cloudimg-amd64-disk1.img"
-        DEFAULT_IMAGE_NAME: cirros-0.3.4-i386-disk
+        IMAGE_URLS: https://cloud-images.ubuntu.com/releases/xenial/release/ubuntu-16.04-server-cloudimg-amd64-disk1.img
         ADVANCED_IMAGE_NAME: ubuntu-16.04-server-cloudimg-amd64-disk1
         ADVANCED_INSTANCE_TYPE: ds512M
         ADVANCED_INSTANCE_USER: ubuntu
@@ -848,9 +846,7 @@
         USE_PYTHON3: true
         NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_dvr) | join(',') }}"
         PHYSICAL_NETWORK: default
-        DOWNLOAD_DEFAULT_IMAGES: false
-        IMAGE_URLS: "http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-i386-disk.img,https://cloud-images.ubuntu.com/releases/xenial/release/ubuntu-16.04-server-cloudimg-amd64-disk1.img"
-        DEFAULT_IMAGE_NAME: cirros-0.3.4-i386-disk
+        IMAGE_URLS: https://cloud-images.ubuntu.com/releases/xenial/release/ubuntu-16.04-server-cloudimg-amd64-disk1.img
         ADVANCED_IMAGE_NAME: ubuntu-16.04-server-cloudimg-amd64-disk1
         ADVANCED_INSTANCE_TYPE: ds512M
         ADVANCED_INSTANCE_USER: ubuntu
@@ -1044,8 +1040,6 @@
     vars:
       devstack_localrc:
         DESIGNATE_BACKEND_DRIVER: bind9
-        DOWNLOAD_DEFAULT_IMAGES: false
-        IMAGE_URLS: http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-i386-disk.img,
         Q_AGENT: openvswitch
         # In this job advanced image is not needed, so it's name should be
         # empty
@@ -1085,6 +1079,8 @@
       - openstack/neutron
       - name: openstack/neutron-tempest-plugin
         override-checkout: 0.3.0
+      - name: openstack/designate-tempest-plugin
+        override-checkout: 0.7.0
       - openstack/tempest
     vars:
       branch_override: stable/queens
@@ -1104,6 +1100,13 @@
       This job run on py2 for stable/rocky gate.
     nodeset: openstack-single-node-xenial
     override-checkout: stable/rocky
+    required-projects:
+      - openstack/devstack-gate
+      - openstack/neutron
+      - openstack/neutron-tempest-plugin
+      - name: openstack/designate-tempest-plugin
+        override-checkout: 0.7.0
+      - openstack/tempest
     vars: &designate_scenario_vars_rocky
       branch_override: stable/rocky
       network_api_extensions_common: *api_extensions_rocky
@@ -1119,7 +1122,7 @@
     nodeset: openstack-single-node-xenial
     description: |
       This job run on py3 for other than stable/rocky gate
-      which is nothing but neutron-tempest-pluign master gate.
+      which is nothing but neutron-tempest-plugin master gate.
     override-checkout: stable/rocky
     vars:
       <<: *designate_scenario_vars_rocky
@@ -1131,6 +1134,13 @@
     name: neutron-tempest-plugin-designate-scenario-stein
     parent: neutron-tempest-plugin-designate-scenario
     override-checkout: stable/stein
+    required-projects:
+      - openstack/devstack-gate
+      - openstack/neutron
+      - openstack/neutron-tempest-plugin
+      - name: openstack/designate-tempest-plugin
+        override-checkout: 0.7.0
+      - openstack/tempest
     vars:
       branch_override: stable/stein
       network_api_extensions_common: *api_extensions_stein
@@ -1381,7 +1391,10 @@
       jobs:
         - neutron-tempest-plugin-sfc
         - neutron-tempest-plugin-sfc-train
-        - neutron-tempest-plugin-bgpvpn-bagpipe
+        - neutron-tempest-plugin-bgpvpn-bagpipe:
+            # TODO(bcafarel): switch back to voting when
+            # https://review.opendev.org/708648 is merged
+            voting: false
         - neutron-tempest-plugin-bgpvpn-bagpipe-train
         - neutron-tempest-plugin-fwaas:
             # TODO(slaweq): switch it to be voting when bug
@@ -1397,7 +1410,9 @@
     gate:
       jobs:
         - neutron-tempest-plugin-sfc
-        - neutron-tempest-plugin-bgpvpn-bagpipe
+        # TODO(bcafarel): bring back to gate queue when
+        # https://review.opendev.org/708648 is merged
+        # - neutron-tempest-plugin-bgpvpn-bagpipe
         # TODO(slaweq): bring it back to gate queue
         # https://bugs.launchpad.net/neutron/+bug/1858645 will be fixed
         # - neutron-tempest-plugin-fwaas
diff --git a/neutron_tempest_plugin/common/shell.py b/neutron_tempest_plugin/common/shell.py
index bd4a7a3..eebb07d 100644
--- a/neutron_tempest_plugin/common/shell.py
+++ b/neutron_tempest_plugin/common/shell.py
@@ -46,7 +46,7 @@
 
     :param timeout: command execution timeout in seconds
 
-    :param check: when False it doesn't raises ShellCommandError when
+    :param check: when False it doesn't raises ShellCommandFailed when
     exit status is not zero. True by default
 
     :returns: STDOUT text when command execution terminates with zero exit
@@ -57,7 +57,7 @@
     try to read STDOUT and STDERR buffers (not fully implemented) before
     raising the exception.
 
-    :raises ShellCommandError: when command execution terminates with non-zero
+    :raises ShellCommandFailed: when command execution terminates with non-zero
     exit status.
     """
     ssh_client = ssh_client or SSH_PROXY_CLIENT
@@ -110,7 +110,7 @@
 
     except lib_exc.SSHExecCommandFailed as ex:
         # Please note class SSHExecCommandFailed has been re-based on
-        # top of ShellCommandError
+        # top of ShellCommandFailed
         stdout = ex.stdout
         stderr = ex.stderr
         exit_status = ex.exit_status
@@ -174,7 +174,7 @@
                                                  stdout=self.stdout)
 
         elif self.exit_status != 0:
-            raise exceptions.ShellCommandError(command=self.command,
-                                               exit_status=self.exit_status,
-                                               stderr=self.stderr,
-                                               stdout=self.stdout)
+            raise exceptions.ShellCommandFailed(command=self.command,
+                                                exit_status=self.exit_status,
+                                                stderr=self.stderr,
+                                                stdout=self.stdout)
diff --git a/neutron_tempest_plugin/config.py b/neutron_tempest_plugin/config.py
index 2f2f913..28d6b76 100644
--- a/neutron_tempest_plugin/config.py
+++ b/neutron_tempest_plugin/config.py
@@ -123,6 +123,12 @@
                 default=False,
                 help='Allow creation of shared resources.'
                      'The default value is false.'),
+    cfg.BoolOpt('is_igmp_snooping_enabled',
+                default=False,
+                help='Indicates whether IGMP snooping is enabled or not. '
+                     'If True, multicast test(s) will assert that multicast '
+                     'traffic is not being flooded to all ports. Defaults '
+                     'to False.'),
 ]
 
 # TODO(amuller): Redo configuration options registration as part of the planned
diff --git a/neutron_tempest_plugin/exceptions.py b/neutron_tempest_plugin/exceptions.py
index 895cb40..398bc1c 100644
--- a/neutron_tempest_plugin/exceptions.py
+++ b/neutron_tempest_plugin/exceptions.py
@@ -96,5 +96,5 @@
     exceptions.SSHExecCommandFailed, ShellCommandFailed)
 
 # Above code created a new SSHExecCommandFailed class based on top
-# of ShellCommandError
+# of ShellCommandFailed
 assert issubclass(exceptions.SSHExecCommandFailed, ShellCommandFailed)
diff --git a/neutron_tempest_plugin/scenario/test_multicast.py b/neutron_tempest_plugin/scenario/test_multicast.py
index a39a0c3..566ac95 100644
--- a/neutron_tempest_plugin/scenario/test_multicast.py
+++ b/neutron_tempest_plugin/scenario/test_multicast.py
@@ -111,6 +111,14 @@
            'result_file': result_file}
 
 
+def get_unregistered_script(group, result_file):
+    return """#!/bin/bash
+export LC_ALL=en_US.UTF-8
+tcpdump -i any -s0 -vv host %(group)s -vvneA -s0 -l &> %(result_file)s &
+    """ % {'group': group,
+           'result_file': result_file}
+
+
 class BaseMulticastTest(object):
 
     credentials = ['primary']
@@ -125,6 +133,7 @@
     multicast_message = "Big Bang"
     receiver_output_file = "/tmp/receiver_mcast_out"
     sender_output_file = "/tmp/sender_mcast_out"
+    unregistered_output_file = "/tmp/unregistered_mcast_out"
 
     @classmethod
     def skip_checks(cls):
@@ -198,16 +207,16 @@
         server['ssh_client'] = ssh.Client(server['fip']['floating_ip_address'],
                                           self.username,
                                           pkey=self.keypair['private_key'])
-        self._check_python_installed_on_server(server['ssh_client'],
-                                               server['id'])
+        self._check_cmd_installed_on_server(server['ssh_client'],
+                                            server['id'], PYTHON3_BIN)
         return server
 
-    def _check_python_installed_on_server(self, ssh_client, server_id):
+    def _check_cmd_installed_on_server(self, ssh_client, server_id, cmd):
         try:
-            ssh_client.execute_script('which %s' % PYTHON3_BIN)
+            ssh_client.execute_script('which %s' % cmd)
         except exceptions.SSHScriptFailed:
             raise self.skipException(
-                "%s is not available on server %s" % (PYTHON3_BIN, server_id))
+                "%s is not available on server %s" % (cmd, server_id))
 
     def _prepare_sender(self, server, mcast_address):
         check_script = get_sender_script(
@@ -226,10 +235,23 @@
             server['fip']['floating_ip_address'],
             self.username,
             pkey=self.keypair['private_key'])
-        self._check_python_installed_on_server(ssh_client, server['id'])
+        self._check_cmd_installed_on_server(ssh_client, server['id'],
+                                            PYTHON3_BIN)
         server['ssh_client'].execute_script(
             'echo "%s" > ~/multicast_traffic_receiver.py' % check_script)
 
+    def _prepare_unregistered(self, server, mcast_address):
+        check_script = get_unregistered_script(
+            group=mcast_address, result_file=self.unregistered_output_file)
+        ssh_client = ssh.Client(
+            server['fip']['floating_ip_address'],
+            self.username,
+            pkey=self.keypair['private_key'])
+        self._check_cmd_installed_on_server(ssh_client, server['id'],
+                                            'tcpdump')
+        server['ssh_client'].execute_script(
+            'echo "%s" > ~/unregistered_traffic_receiver.sh' % check_script)
+
     @test.unstable_test("bug 1850288")
     @decorators.idempotent_id('113486fc-24c9-4be4-8361-03b1c9892867')
     def test_multicast_between_vms_on_same_network(self):
@@ -241,9 +263,26 @@
         receivers = [self._create_server() for _ in range(1)]
         # Sender can be also receiver of multicast traffic
         receivers.append(sender)
-        self._check_multicast_conectivity(sender=sender, receivers=receivers)
+        unregistered = self._create_server()
+        self._check_multicast_conectivity(sender=sender, receivers=receivers,
+                                          unregistered=unregistered)
 
-    def _check_multicast_conectivity(self, sender, receivers):
+    def _is_multicast_traffic_expected(self, mcast_address):
+        """Checks if multicast traffic is expected to arrive.
+
+        Checks if multicast traffic is expected to arrive to the
+        unregistered VM.
+
+        If IGMP snooping is enabled, multicast traffic should not be
+        flooded unless the destination IP is in the range of 224.0.0.X
+        [0].
+
+        [0] https://tools.ietf.org/html/rfc4541 (See section 2.1.2)
+        """
+        return (mcast_address.startswith('224.0.0') or not
+                CONF.neutron_plugin_options.is_igmp_snooping_enabled)
+
+    def _check_multicast_conectivity(self, sender, receivers, unregistered):
         """Test multi-cast messaging between two servers
 
         [Sender server] -> ... some network topology ... -> [Receiver server]
@@ -257,6 +296,12 @@
                     path=file_path))
             return msg in result
 
+        self._prepare_unregistered(unregistered, mcast_address)
+
+        # Run the unregistered node script
+        unregistered['ssh_client'].execute_script(
+            "bash ~/unregistered_traffic_receiver.sh", become_root=True)
+
         self._prepare_sender(sender, mcast_address)
         receiver_ids = []
         for receiver in receivers:
@@ -295,6 +340,18 @@
         for receiver_id in receiver_ids:
             self.assertIn(receiver_id, replies_result)
 
+        # Kill the tcpdump command running on the unregistered node so
+        # tcpdump flushes its output to the output file
+        unregistered['ssh_client'].execute_script(
+            "killall tcpdump && sleep 2", become_root=True)
+
+        unregistered_result = unregistered['ssh_client'].execute_script(
+            "cat {path} || echo '{path} not exists yet'".format(
+                path=self.unregistered_output_file))
+        num_of_pckt = (1 if self._is_multicast_traffic_expected(mcast_address)
+                       else 0)
+        self.assertIn('%d packets captured' % num_of_pckt, unregistered_result)
+
 
 class MulticastTestIPv4(BaseMulticastTest, base.BaseTempestTestCase):
 
diff --git a/releasenotes/notes/igmp-snooping-8d6d85608df8880a.yaml b/releasenotes/notes/igmp-snooping-8d6d85608df8880a.yaml
new file mode 100644
index 0000000..032be1f
--- /dev/null
+++ b/releasenotes/notes/igmp-snooping-8d6d85608df8880a.yaml
@@ -0,0 +1,11 @@
+---
+features:
+  - |
+    Enhanced the ``test_multicast_between_vms_on_same_network`` adding
+    IGMP test coverage to it. A new VM running tcpdump is spawned as
+    part of the test to verify whether the traffic is reaching it or not.
+upgrade:
+  - |
+    Add a new configuration option called ``is_igmp_snooping_enabled``
+    to enable/disable IGMP testing as part of the
+    ``test_multicast_between_vms_on_same_network`` test case.