# Copyright 2020 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 re import time import netaddr from neutron_lib import constants from neutron_lib.utils import test from neutron_tempest_plugin.common import ip from neutron_tempest_plugin.common import ssh from neutron_tempest_plugin.common import utils from neutron_tempest_plugin import exceptions from oslo_log import log from tempest import config from tempest.lib.common.utils import data_utils from tempest.lib import decorators from tempest.lib import exceptions as temp_exc from whitebox_neutron_tempest_plugin.tests.scenario import base CONF = config.CONF WB_CONF = CONF.whitebox_neutron_plugin_options LOG = log.getLogger(__name__) PYTHON3_BIN = "python3" INSTANCE_INTERFACE = WB_CONF.default_instance_interface def get_receiver_script( group, port, hello_message, ack_message, result_file, interface=None): if interface: bind_to_dev_cmd = """ sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, str('%(interface)s').encode('utf-8'))""" % { 'interface': interface} else: bind_to_dev_cmd = '' return """ import socket import struct import sys multicast_group = '%(group)s' server_address = ('', %(port)s) # Create the socket sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) %(bind_to_dev_cmd)s # Bind to the server address sock.bind(server_address) # Tell the operating system to add the socket to the multicast group # on all interfaces. group = socket.inet_aton(multicast_group) mreq = struct.pack('4sL', group, socket.INADDR_ANY) sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) # Receive/respond loop with open('%(result_file)s', 'w') as f: f.write('%(hello_message)s') f.flush() data, address = sock.recvfrom(1024) f.write('received ' + str(len(data)) + ' bytes from ' + str(address)) f.write(str(data)) sock.sendto(b'%(ack_message)s', address) """ % {'group': group, 'port': port, 'hello_message': hello_message, 'ack_message': ack_message, 'result_file': result_file, 'bind_to_dev_cmd': bind_to_dev_cmd} def get_sender_script(group, port, message, result_file, ttl): return """ import socket import sys message = b'%(message)s' multicast_group = ('%(group)s', %(port)s) # Create the datagram socket sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) # Set the time-to-live for messages to 1 so they do not go past the # local network segment. For east-west or north-south tests recommended # TTL is 2. sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, %(ttl)s) # Set a timeout so the socket does not block indefinitely when trying # to receive data. sock.settimeout(1) with open('%(result_file)s', 'w') as f: try: # Send data to the multicast group sent = sock.sendto(message, multicast_group) # Look for responses from all recipients while True: try: data, server = sock.recvfrom(1024) except socket.timeout: f.write('timed out, no more responses') break else: f.write('received reply ' + str(data) + ' from ' + str(server)) finally: sys.stdout.write('closing socket') sock.close() """ % {'group': group, 'port': port, 'message': message, 'result_file': result_file, 'ttl': ttl} def get_capture_script(result_file, mcast_port, interface): return """#!/bin/bash export LC_ALL=en_US.UTF-8 tcpdump -Qin -i %(interface)s -vvneA -s0 -l port %(mcast_port)s \ &> %(result_file)s & """ % {'interface': interface, 'mcast_port': mcast_port, 'result_file': result_file} class BaseMulticastTest(object): credentials = ['primary', 'admin'] force_tenant_isolation = False vlan_transparent = False # Import configuration options receivers_count = WB_CONF.mcast_receivers_count mcast_groups_count = WB_CONF.mcast_groups_count external_querier_period = WB_CONF.external_igmp_querier_period hello_message = "I am waiting..." multicast_port = 5007 multicast_message = "group %s" receiver_output_file = "/tmp/receiver_mcast_out" sender_output_file = "/tmp/sender_mcast_out_%s" capture_output_file = "/tmp/capture_mcast_out" @classmethod def skip_checks(cls): super(BaseMulticastTest, cls).skip_checks() advanced_image_available = ( CONF.neutron_plugin_options.advanced_image_ref or CONF.neutron_plugin_options.default_image_is_advanced) if not advanced_image_available: skip_reason = "This test require advanced tools for this test" raise cls.skipException(skip_reason) @classmethod def resource_setup(cls): super(BaseMulticastTest, cls).resource_setup() if CONF.neutron_plugin_options.default_image_is_advanced: cls.flavor_ref = CONF.compute.flavor_ref cls.image_ref = CONF.compute.image_ref cls.username = CONF.validation.image_ssh_user else: cls.flavor_ref = ( CONF.neutron_plugin_options.advanced_image_flavor_ref) cls.image_ref = CONF.neutron_plugin_options.advanced_image_ref cls.username = CONF.neutron_plugin_options.advanced_image_ssh_user # setup basic topology for servers we can log into it if cls.has_ovn_support: try: cls.network = cls.create_network( vlan_transparent=cls.vlan_transparent) except temp_exc.ServerFault as exc: msg = 'Backend does not support VLAN Transparency.' if exc.resp_body['message'] == msg: raise cls.skipException(msg) else: raise exc else: cls.network = cls.create_network() cls.subnet = cls.create_subnet(cls.network) cls.router = cls.create_router_by_client() cls.create_router_interface(cls.router['id'], cls.subnet['id']) cls.keypair = cls.create_keypair() cls.secgroup = cls.os_primary.network_client.create_security_group( name='secgroup_mcast') cls.security_groups.append(cls.secgroup['security_group']) cls.create_loginable_secgroup_rule( secgroup_id=cls.secgroup['security_group']['id']) cls.create_pingable_secgroup_rule( secgroup_id=cls.secgroup['security_group']['id']) # Allow receiving of IGMP queries by VMs cls.create_security_group_rule( security_group_id=cls.secgroup['security_group']['id'], protocol=constants.PROTO_NAME_IGMP, direction=constants.INGRESS_DIRECTION) # Create security group rule for UDP (multicast traffic) cls.create_secgroup_rules( rule_list=[dict(protocol=constants.PROTO_NAME_UDP, direction=constants.INGRESS_DIRECTION, remote_ip_prefix=cls.any_addresses, ethertype=cls.ethertype)], secgroup_id=cls.secgroup['security_group']['id']) # Multicast IP range to be used for multicast group IP asignement if '-' in cls.multicast_group_range: multicast_group_range = netaddr.IPRange( *cls.multicast_group_range.split('-')) else: multicast_group_range = netaddr.IPNetwork( cls.multicast_group_range) cls.multicast_group_iter = iter(multicast_group_range) # For external or north-south topologies external network # need to be defined cls.external_network = cls.client.show_network( CONF.network.public_network_id)['network'] def _check_cmd_installed_on_server(self, ssh_client, server_id, cmd): try: ssh_client.execute_script('which %s' % cmd) except exceptions.SSHScriptFailed: raise self.skipException( "%s is not available on server %s" % (cmd, server_id)) def _prepare_sender(self, server, mcast_groups, ttl): for mcast_group in mcast_groups: output_file = self.sender_output_file % mcast_group check_script = get_sender_script( group=mcast_group, port=self.multicast_port, message=self.multicast_message % mcast_group, result_file=output_file, ttl=ttl) server['ssh_client'].execute_script( 'echo "%s" > /tmp/multicast_traffic_sender_%s.py' % (check_script, mcast_group)) def _prepare_capture_script(self, server): interface = (server['vlan_device'] if self.vlan_transparent else INSTANCE_INTERFACE) capture_script = get_capture_script( result_file=self.capture_output_file, mcast_port=self.multicast_port, interface=interface) self._check_cmd_installed_on_server( server['ssh_client'], server['id'], 'tcpdump') server['ssh_client'].execute_script( 'echo "%s" > /tmp/capture_script.sh' % capture_script) def _prepare_receiver(self, server, mcast_address): interface = server['vlan_device'] if self.vlan_transparent else None check_script = get_receiver_script( group=mcast_address, port=self.multicast_port, hello_message=self.hello_message, ack_message=server['id'], result_file=self.receiver_output_file, interface=interface) server['ssh_client'].execute_script( 'echo "%s" > /tmp/multicast_traffic_receiver.py' % check_script) self._prepare_capture_script(server) def _prepare_unregistered(self, server): # We need to kill multicast receiver script if receiver becomes # unsubscribed and the server moves to the pool of unregistered VMs server['ssh_client'].exec_command( "pids=$(ps ax | grep multicast | grep -v grep | awk '{print $1}')" ";kill -9 $pids && sleep 2 || true") self._prepare_capture_script(server) def _prepare_igmp_snooping_test( self, mcast_groups, receivers_count=1, topology='internal', port_type=None): """Function for creating desired topology for the test Available topologies: * internal(default): sender and receivers are on tenant network * external: sender and receivers are on external(public) network * east-west: sender and receivers are on different tenant networks * north-south: sender is on external and receivers on tenant network :param mcast_groups(list): list of multicast groups to use. Note, for each group at least one receiver will be created. :param receivers_count(int): Number of receivers to create for each multicast group. Note, first receiver is always created on a different compute node than sender is running. :param topology(str): one of 4 available topologies to use (see list above) :param port_type(str): type of port to use. If omitted, default port type will be used. Can be set to 'direct' or 'direct-physical' for SR-IOV environments. :returns: List of the following objects, sender, list of receivers, list of unregistered, id of destination network """ # Note, for now we support port_type other than 'normal' only # when using SR-IOV environement and therefore only external network if topology == 'external' or topology == 'north-south': self.ensure_external_network_is_shared() sender = self._create_server_for_topology( network_id=self.external_network['id'], port_type=port_type) elif topology == 'east-west': network2 = self.create_network() subnet2 = self.create_subnet(network2, cidr="10.100.1.0/24") self.create_router_interface(self.router['id'], subnet2['id']) sender = self._create_server_for_topology( network_id=network2['id'], port_type=port_type) else: sender = self._create_server_for_topology(port_type=port_type) if topology == 'external': dst_network_id = self.external_network['id'] else: dst_network_id = self.network['id'] receivers = [] for group_id in range(len(mcast_groups)): if receivers_count > 0: # At least one receiver in the group we need to be launched # on different host receivers.append( [self._create_server_for_topology( different_host=sender, network_id=dst_network_id, port_type=port_type)]) for _ in range(receivers_count - 1): receivers[group_id].append( self._create_server_for_topology( network_id=dst_network_id, port_type=port_type)) else: # For some tests we do not create receivers receivers.append([]) servers = [item for sublist in receivers for item in sublist] servers.append(sender) unregistered = self._create_server_for_topology( network_id=dst_network_id, port_type=port_type) servers.append(unregistered) # For some tests we need to support more than one unregistered server unregistered = [unregistered] for server in servers: self._check_cmd_installed_on_server(server['ssh_client'], server['id'], PYTHON3_BIN) return [sender, receivers, unregistered, dst_network_id] def restart_openvswitch_on_compute_nodes(self): for node in self.nodes: if node['is_compute'] is True and node['is_controller'] is False: self.reset_node_service('ovs vswitchd', node['client']) 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 (str(mcast_address).startswith('224.0.0') or not CONF.neutron_plugin_options.is_igmp_snooping_enabled) def _check_multicast_conectivity( self, mcast_groups, sender, receivers, unregistered, test_unsubscribe=False, start_delay=None, pre_action=None, ttl=1): """Test multicast messaging between servers [Sender server] -> ... some network topology ... -> [Receiver server] :param mcast_groups(list): list of multicast groups to use. Note: for each group will be created a sender script and at least one receiver. :param sender(server): server that will send multicast traffic :param receivers(list): list of lists of servers. There is ia list of receivers per group. :param unregistered(list): list of servers that are not subscribed to receive multicast traffic. :param test_unsubscribe(boolean): whether to test unsubsribe step or not. Default is False. :param start_delay(int): whether to include delay waiting step before sending packets and how long (in seconds). Default is no delay. :param pre_action(callable): run additional arbitrary action before sending multicast packets. :param ttl(int): time-to-live for multicast packages. Default is 1 fits well when all VMs are on the same network. For testing multicast between 2 different network TTL should be set to 2. Scenario is the following: 1. Install required scripts on all VMs according their roles. 2. Send multicast packets from sender to each multicast group. Note: by default a single group is used and single receiver, but this can be adjusted with config options. 3. Verify that multicast traffic reached each receiver and does not reach unregistgered host. Number of received packets is the same as sent number. 4. Unsubscribe one receiver in each group and repeat steps 2 and 3. Note: in case more than one receiver configured the step will repeat until no subscribed hosts left. """ def _message_received(client, msg, file_path): result = client.execute_script( "cat {path} || echo '{path} not exists yet'".format( path=file_path)) return msg in result def _validate_number_of_messages( client, mcast_groups, file_path, allowed_group=None): """This function validates number of packets that reached a VM The function compares actual received number of packets per group with the expected number. """ result = client.execute_script( "cat {path} || echo '{path} not exists yet'".format( path=file_path)) # We need to make sure that exactly the expected count # of messages reached receiver, no more and no less LOG.debug('result = {}'. format(result)) for mcast_group in mcast_groups: LOG.debug('Validating group %s', mcast_group) if allowed_group: LOG.debug('Allowed group %s', allowed_group) if ((allowed_group and mcast_group == allowed_group) or self._is_multicast_traffic_expected(mcast_group)): expected_count = 1 else: expected_count = 0 count = len( re.findall(self.multicast_message % mcast_group, result)) self.assertEqual( count, expected_count, 'Received number of messages ({}) to group {} differs ' 'from the expected ({})'.format( count, mcast_group, expected_count)) def _test_traffic_between_servers(): for server in unregistered: self._prepare_unregistered(server) server['ssh_client'].execute_script( "bash /tmp/capture_script.sh", become_root=True) receiver_ids = [] # receivers is a list of separate lists for each mcast group group_ids = range(len(mcast_groups)) for group_id in group_ids: receiver_ids.append([]) for receiver in receivers[group_id]: self._prepare_receiver( receiver, mcast_groups[group_id]) # We run the capture script on each receiver as well # in order to check that there is no traffic from any # group that the receiver did not subscribe to receiver['ssh_client'].execute_script( "bash /tmp/capture_script.sh", become_root=True) # receiver script needs to be executed as root user for # vlan_transparency in order to bind the vlan interface receiver['ssh_client'].execute_script( "%s /tmp/multicast_traffic_receiver.py &" % PYTHON3_BIN, shell="bash", become_root=self.vlan_transparent) utils.wait_until_true( lambda: _message_received( receiver['ssh_client'], self.hello_message, self.receiver_output_file), exception=RuntimeError( "Receiver script didn't start properly on server " "{!r}.".format(receiver['id']))) receiver_ids[group_id].append(receiver['id']) if pre_action: pre_action() if start_delay and start_delay > 5: LOG.debug( "Waiting %s seconds for start delay to expire", start_delay) time.sleep(start_delay) else: # (rsafrono) Note, this delay is needed to make sure all # receivers are ready. In some cases (e.g. SR-IOV) test result # is unstable if we start to send traffic without this delay. time.sleep(10) for group in mcast_groups: LOG.debug("Starting script for group %s on " "sender", group) sender['ssh_client'].execute_script( "%s /tmp/multicast_traffic_sender_%s.py" % ( PYTHON3_BIN, group)) for group_id in group_ids: for receiver in receivers[group_id]: utils.wait_until_true( lambda: _message_received( receiver['ssh_client'], self.multicast_message % mcast_groups[group_id], self.receiver_output_file), exception=RuntimeError( "Receiver {!r} didn't get multicast " "message".format(receiver['id']))) receiver['ssh_client'].execute_script( "killall tcpdump && sleep 2", become_root=True) LOG.debug('Validating number of messages on ' 'receiver %s', receiver['id']) _validate_number_of_messages( receiver['ssh_client'], mcast_groups, self.capture_output_file, mcast_groups[group_id]) for group_id in group_ids: sender_output = ( self.sender_output_file % mcast_groups[group_id]) replies_result = sender['ssh_client'].execute_script( "cat {path} || echo '{path} not exists yet'".format( path=sender_output)) for receiver_id in receiver_ids[group_id]: self.assertIn(receiver_id, replies_result) for server in unregistered: server['ssh_client'].execute_script( "killall tcpdump && sleep 2", become_root=True) LOG.debug('Validating number of messages on ' 'unregistered %s', server['id']) _validate_number_of_messages( server['ssh_client'], mcast_groups, self.capture_output_file) def _unsubscribe_receiver_from_group(group_id): if len(receivers[group_id]) > 0: unregistered.append(receivers[group_id][-1]) del receivers[group_id][-1] self._prepare_sender(sender, mcast_groups, ttl) _test_traffic_between_servers() # If test_unsubscribe option is enabled then we unsubscribe # one receiver from each group and retest traffic. # Repeat this step until all receivers are unsubscribed. if test_unsubscribe: while len(receivers[0]) > 0: for group_id in range(len(mcast_groups)): _unsubscribe_receiver_from_group(group_id) _test_traffic_between_servers() class MulticastTestIPv4(BaseMulticastTest, base.TrafficFlowTest): # Import configuration options multicast_group_range = CONF.neutron_plugin_options.multicast_group_range # IP version specific parameters _ip_version = constants.IP_VERSION_4 any_addresses = constants.IPv4_ANY class MulticastTestIPv4Common(MulticastTestIPv4): @decorators.idempotent_id('615a35d3-642e-4440-a19e-351f29d0b3ff') def test_igmp_snooping_same_network_and_unsubscribe(self): """Test multicast messaging between servers on the same network [Sender server] -> (Multicast network) -> [Receiver server] Scenario: 1. Create VMs for sender, receiver(s), unregistered host. Note: default(internal, same network) topology is used. mcast_groups contain only groups where no flooding to all ports expected. 2. Verify that multicast packets reach subscribed hosts and do not reach unsubscribed. For more details see _check_multicast_connectivity document text and code. """ mcast_groups = [next(self.multicast_group_iter) for _ in range(self.mcast_groups_count)] sender, receivers, unregistered, _ = self._prepare_igmp_snooping_test( mcast_groups=mcast_groups, receivers_count=self.receivers_count) self._check_multicast_conectivity( mcast_groups=mcast_groups, sender=sender, receivers=receivers, unregistered=unregistered, test_unsubscribe=True) @decorators.idempotent_id('fcd50103-eeba-475f-803b-dfc9e8c342a5') def test_igmp_snooping_ext_network_and_unsubscribe(self): """Test multicast between VMs on external network [Sender server] -> (Multicast network) -> [Receiver server] Scenario: 1. Create VMs for sender, receiver(s), unregistered host. Note: External topology is used. mcast_groups contain only groups where no flooding to all ports expected. 2. Verify that multicast packets reach subscribed hosts and do not reach unsubscribed. For more details see _check_multicast_connectivity document text and code. """ mcast_groups = [next(self.multicast_group_iter) for _ in range(self.mcast_groups_count)] sender, receivers, unregistered, _ = self._prepare_igmp_snooping_test( mcast_groups=mcast_groups, topology='external', receivers_count=self.receivers_count) self._check_multicast_conectivity( mcast_groups=mcast_groups, sender=sender, receivers=receivers, unregistered=unregistered, test_unsubscribe=True) @decorators.idempotent_id('35523bd5-4b35-4e6b-a25a-20826808d85d') def test_flooding_when_special_groups(self): """Test that multicast traffic of groups 224.0.0.X is flooded Scenario: 1. Create VMs for sender, receiver(s), unregistered host. Note: default(internal, same network) topology is used. mcast_groups contains only groups from the range 224.0.0.X 2. Verify that multicast packets reach all VMs, even unregistered ones. """ mcast_groups = ['224.0.0.100'] sender, receivers, unregistered, _ = self._prepare_igmp_snooping_test( mcast_groups=mcast_groups) self._check_multicast_conectivity( mcast_groups=mcast_groups, sender=sender, receivers=receivers, unregistered=unregistered) @decorators.idempotent_id('42bfc8e1-0efb-4573-bb4a-290060a40980') def test_igmp_snooping_after_openvswitch_restart(self): """Test IGMP snooping after openvswitch restart [Sender VM] -> (Multicast network) -> [Receiver VM] Note: this test should not be run with other tests in parallel because it can affect other tests results. Scenario: 1. Create VMs for sender, receiver(s), unregistered host. mcast_groups contain only groups where no flooding to all ports expected. 2. Subscribe receiver VMs to a multicast group but do not send multicast packets immediately. 3. Restart Openvswitch on compute nodes. 4. Send multicast traffic and verify that multicast packets reach subscribed hosts and do not reach unsubscribed. For more details see _check_multicast_connectivity document text and code. """ if not hasattr(self, 'nodes'): raise self.skipException( "Nodes info not available. Test won't be able to restart " "openvswitch service on a node.") if len(self.nodes) == 1: raise self.skipException( "This test is not supported on a single-node environment") mcast_groups = [next(self.multicast_group_iter) for _ in range(self.mcast_groups_count)] sender, receivers, unregistered, _ = self._prepare_igmp_snooping_test( mcast_groups=mcast_groups, receivers_count=self.receivers_count) self._check_multicast_conectivity( mcast_groups=mcast_groups, sender=sender, receivers=receivers, unregistered=unregistered, pre_action=self.restart_openvswitch_on_compute_nodes) class MulticastTestIPv4Sriov(MulticastTestIPv4): @classmethod def resource_setup(cls): super(MulticastTestIPv4Sriov, cls).resource_setup() if not cls.has_sriov_support: skip_reason = "Environment does not support SR-IOV" raise cls.skipException(skip_reason) def _validate_pfs_availability(self): required_sriov_ports = ( self.mcast_groups_count * self.receivers_count + 2) available_sriov_ports = ( WB_CONF.sriov_pfs_per_host * len([node for node in self.nodes if node['is_compute'] is True])) if (available_sriov_ports < required_sriov_ports): self.skipTest( 'Not enough SR-IOV ports ({}), while required {}'.format( available_sriov_ports, required_sriov_ports)) class MulticastTestIPv4SriovCommon(MulticastTestIPv4Sriov): @decorators.idempotent_id('4210438f-080b-4254-a7d1-166144f04572') def test_igmp_snooping_ext_network_with_vf_ports(self): """Test multicast between VMs with SR-IOV VF ports on external network [Sender with VF port] -> (External network) -> [Receiver with VF port] Note: the test runs only on SR-IOV environment and requires shared external network. Scenario: 1. Create VMs for sender, receiver(s), unregistered host. Note: External topology is used. External network must be shared. mcast_groups contain only groups where no flooding to all ports expected. 2. Verify that multicast packets reach subscribed hosts and do not reach unsubscribed. For more details see _check_multicast_connectivity document text and code. """ mcast_groups = [next(self.multicast_group_iter) for _ in range(self.mcast_groups_count)] sender, receivers, unregistered, _ = self._prepare_igmp_snooping_test( mcast_groups=mcast_groups, topology='external', port_type='direct', receivers_count=self.receivers_count) self._check_multicast_conectivity( mcast_groups=mcast_groups, sender=sender, receivers=receivers, unregistered=unregistered, test_unsubscribe=True) @decorators.idempotent_id('86fb3e4b-5e01-4953-99a9-4f85c561f57e') def test_igmp_snooping_ext_network_with_pf_ports(self): """Test multicast between VMs with SR-IOV PF ports on external network [Sender with PF port] -> (External network) -> [Receiver with PF port] Note: the test runs only on SR-IOV environment and requires shared external network. Scenario: 1. Create VMs for sender, receiver(s), unregistered host. Note: External topology is used. External network must be shared. mcast_groups contain only groups where no flooding to all ports expected. 2. Verify that multicast packets reach subscribed hosts and do not reach unsubscribed. For more details see _check_multicast_connectivity document text and code. """ mcast_groups = [next(self.multicast_group_iter) for _ in range(self.mcast_groups_count)] self._validate_pfs_availability() sender, receivers, unregistered, _ = self._prepare_igmp_snooping_test( mcast_groups=mcast_groups, topology='external', port_type='direct-physical', receivers_count=self.receivers_count) self._check_multicast_conectivity( mcast_groups=mcast_groups, sender=sender, receivers=receivers, unregistered=unregistered, test_unsubscribe=True) class MulticastTestIPv4OvnBase(MulticastTestIPv4, base.BaseTempestTestCaseOvn): mcast_idle_timeout = 60 # We will use this function in order to decrease the idle timeout from the # default value in order to save some time def set_mcast_idle_timeout(self, network_id): uuid = self.get_item_uuid( db='nb', item='logical_switch', search_string='name=neutron-' + network_id) cmd = self.nbctl + " set logical_switch " + uuid + " other_config:" self.run_on_master_controller( cmd + "mcast_idle_timeout=" + str(self.mcast_idle_timeout)) def set_querier_on_network(self, network_id): port = self.client.list_ports( network_id=network_id, device_owner=constants.DEVICE_OWNER_DHCP)['ports'] eth_src = port[0]['mac_address'] ip4_src = port[0]['fixed_ips'][0]['ip_address'] uuid = self.get_item_uuid( db='nb', item='logical_switch', search_string='name=neutron-' + network_id) cmd = self.nbctl + " set logical_switch " + uuid + " other_config:" self.run_on_master_controller( cmd + "mcast_querier='true';" + cmd + "mcast_eth_src=" + eth_src + ";" + cmd + "mcast_ip4_src=" + ip4_src) def set_mcast_relay_on_router(self, router_id, state='true'): uuid = self.get_item_uuid( db='nb', item='logical_router', search_string='name=neutron-' + router_id) self.run_on_master_controller( self.nbctl + " set logical_router " + uuid + " options:mcast_relay=" + state) def restart_ovn_controller_on_compute_nodes(self): for node in self.nodes: if node['is_compute'] is True and node['is_controller'] is False: self.reset_node_service('ovn controller', node['client']) class MulticastTestIPv4Ovn(MulticastTestIPv4OvnBase): @test.unstable_test("querier not available by default, see RHBZ 1791815") @decorators.idempotent_id('fa082cf9-37fc-4e7f-bfdb-fbd8e6860634') def test_multicast_after_idle_timeout(self): """Test multicast messaging after idle timeout [Sender VM] -> (Multicast network) -> [Receiver VM] Scenario: 1. Create VMs for sender, receiver(s), unregistered host. mcast_groups contain only groups where no flooding to all ports expected. 2. Subscribe receiver VMs to a multicast group but do not send multicast packets immediately. Wait first until idle_timeout expires and send packets afterwards. 3. Verify that multicast packets reach subscribed hosts and do not reach unsubscribed. For more details see _check_multicast_connectivity document text and code. """ mcast_groups = [next(self.multicast_group_iter) for _ in range(self.mcast_groups_count)] sender, receivers, unregistered, dst_network_id = ( self._prepare_igmp_snooping_test( mcast_groups=mcast_groups, receivers_count=self.receivers_count)) self.set_mcast_idle_timeout(dst_network_id) self._check_multicast_conectivity( mcast_groups=mcast_groups, sender=sender, receivers=receivers, unregistered=unregistered, start_delay=self.mcast_idle_timeout) # (rsafrono) Note, this test is temporary. It includes enabling querier # as a workaround because currently the querier is not enabled by default, # see RHBZ 1791815 @test.unstable_test("Multicast querier not yet supported officially") @decorators.idempotent_id('be5e153d-1ce7-4d19-9efd-2aae0ec74749') def test_idle_timeout_with_querier_enabled(self): """Test multicast messaging after idle timeout when querier is enabled [Sender VM] -> (Multicast network) -> [Receiver VM] Scenario: 1. Create VMs for sender, receiver(s), unregistered host. mcast_groups contain only groups where no flooding to all ports expected. 2. Enable querier on the network. Subscribe receiver VMs to a multicast group but do not send multicast packets immediately. Wait first until idle_timeout expires and send afterwards. 3. Verify that multicast packets reach subscribed hosts and do not reach unsubscribed. For more details see _check_multicast_connectivity document text and code. """ mcast_groups = [next(self.multicast_group_iter) for _ in range(self.mcast_groups_count)] sender, receivers, unregistered, dst_network_id = ( self._prepare_igmp_snooping_test( mcast_groups=mcast_groups, receivers_count=self.receivers_count)) self.set_querier_on_network(dst_network_id) self.set_mcast_idle_timeout(dst_network_id) self._check_multicast_conectivity( mcast_groups=mcast_groups, sender=sender, receivers=receivers, unregistered=unregistered, start_delay=self.mcast_idle_timeout) # (rsafrono) Note, this test includes enabling mcast_relay as # a workaround. Currently mcast_relay is not enabled on router by default. @test.unstable_test("mcast relay not yet supported officially") @decorators.idempotent_id('3a906cd8-e27a-40a7-a369-829a7ec91af6') def test_multicast_east_west(self): """Test multicast between servers on different tenant networks [Sender VM] [Receiver VM] | | [Internal network A] -> [Router] -> [Internal network B] Scenario: 1. Create VMs for sender, receiver(s), unregistered host. Note: East-west topology is used (2 internal networks) mcast_groups contain only groups where no flooding to all ports expected. 2. Verify that multicast packets reach subscribed hosts and do not reach unsubscribed. For more details see _check_multicast_connectivity document text and code. """ self.set_mcast_relay_on_router(self.router['id']) mcast_groups = [next(self.multicast_group_iter) for _ in range(self.mcast_groups_count)] sender, receivers, unregistered, _ = ( self._prepare_igmp_snooping_test( mcast_groups=mcast_groups, topology='east-west', receivers_count=self.receivers_count)) self._check_multicast_conectivity( mcast_groups=mcast_groups, sender=sender, receivers=receivers, unregistered=unregistered, ttl=2) # (rsafrono) Note, this test includes enabling mcast_relay as # a workaround. Currently mcast_relay is not enabled on router by default. @test.unstable_test("Core OVN bug, see RHBZ 1902075") @decorators.idempotent_id('71abfc10-3f6c-4096-a1d3-8fd934b5e3ba') def test_multicast_north_south(self): """Test multicast between VMs on external and internal networks [Sender VM] -> [External network] | [Router] | [Receiver VM] <- [Internal network] Note: the test requires shared external network. Scenario: 1. Create VMs for sender, receiver(s), unregistered host. Note: North-south topology is used where sender is on the external network and receivers on the internal one. mcast_groups contain only groups where no flooding to all ports expected. 2. Verify that multicast packets reach subscribed hosts and do not reach unsubscribed. For more details see _check_multicast_connectivity document text and code. """ self.set_mcast_relay_on_router(self.router['id']) mcast_groups = [next(self.multicast_group_iter) for _ in range(self.mcast_groups_count)] sender, receivers, unregistered, _ = ( self._prepare_igmp_snooping_test( mcast_groups=mcast_groups, topology='north-south', receivers_count=self.receivers_count)) self._check_multicast_conectivity( mcast_groups=mcast_groups, sender=sender, receivers=receivers, unregistered=unregistered, ttl=2) class MulticastTestIPv4SriovOvn( MulticastTestIPv4Sriov, MulticastTestIPv4OvnBase): # (rsafrono) External IGMP querier required for this test. # For now the only reliable option for this is to use SR-IOV environment. @decorators.idempotent_id('fb56ac55-7863-44f9-b0e4-3383093838a2') def test_after_ovn_controller_restart_with_external_querier(self): """Test IGMP snooping after ovn controller restart [Sender VM] -> (Multicast network) -> [Receiver VM] Notes: 1. External IGMP querier required for this test to run properly. 2. This test should not be run with other tests in parallel because it can affect other tests results. Scenario: 1. Create VMs for sender, receiver(s), unregistered host. mcast_groups contain only groups where no flooding to all ports expected. 2. Subscribe receiver VMs to a multicast group but do not send multicast packets immediately. 3. Restart OVN controller on compute nodes. This will cause that OVN controller unlearns the multicast groups. 4. Wait until external querier send new queries, receivers VMs will respond to queries and OVN controller will re-learn the multicast groups. 5. Send multicast traffic and verify that multicast packets reach subscribed hosts and do not reach unsubscribed. For more details see _check_multicast_connectivity document text and code. """ mcast_groups = [next(self.multicast_group_iter) for _ in range(self.mcast_groups_count)] sender, receivers, unregistered, dst_network_id = ( self._prepare_igmp_snooping_test( port_type='direct', mcast_groups=mcast_groups, topology='external', receivers_count=self.receivers_count)) self._check_multicast_conectivity( mcast_groups=mcast_groups, sender=sender, receivers=receivers, unregistered=unregistered, start_delay=self.external_querier_period, pre_action=self.restart_ovn_controller_on_compute_nodes) class MulticastTestVlanTransparency(MulticastTestIPv4): required_extensions = ['vlan-transparent', 'allowed-address-pairs'] vlan_transparent = True vlan_tag = 123 vlan_ipmast_template = '192.168.123.{ip_last_byte}/24' # initialize server index: this index will be used for allowed_address_pair # values server_index = 0 def _prepare_igmp_snooping_test_vlan_transparency( self, mcast_groups, receivers_count=1, topology='internal', port_type=None): sender = self._create_multicast_server_vlan_transparency(mcast_groups) receivers = [] for group_id in range(len(mcast_groups)): if receivers_count > 0: # At least one receiver in the group we need to be launched # on different host receivers.append( [self._create_multicast_server_vlan_transparency( mcast_groups, different_host=sender)]) for _ in range(receivers_count - 1): receivers[group_id].append( self._create_multicast_server_vlan_transparency( mcast_groups)) else: # For some tests we do not create receivers receivers.append([]) servers = [item for sublist in receivers for item in sublist] servers.append(sender) unregistered = self._create_multicast_server_vlan_transparency( mcast_groups) servers.append(unregistered) # For some tests we need to support more than one unregistered server unregistered = [unregistered] for server in servers: self._check_cmd_installed_on_server(server['ssh_client'], server['id'], PYTHON3_BIN) return [sender, receivers, unregistered] def _create_multicast_server_vlan_transparency( self, mcast_groups, different_host=None): vlan_ipmask = self.vlan_ipmast_template.format( ip_last_byte=self.server_index + 10) self.server_index += 1 allowed_address_pairs = [{'ip_address': vlan_ipmask}] port = self.create_port( network={'id': self.network['id']}, security_groups=[self.secgroup['security_group']['id']], allowed_address_pairs=allowed_address_pairs) networks = [{'port': port['id']}] params = { 'flavor_ref': self.flavor_ref, 'image_ref': self.image_ref, 'key_name': self.keypair['name'], 'networks': networks, 'security_groups': [ {'name': self.secgroup['security_group']['name']}], 'name': data_utils.rand_name('multicast-server-vlan-transparent') } if (different_host and CONF.compute.min_compute_nodes > 1): params['scheduler_hints'] = { 'different_host': different_host['id']} server = self.create_server(**params)['server'] # create fip access_ip_address = self.create_floatingip( port=port)['floating_ip_address'] server['ssh_client'] = ssh.Client(access_ip_address, self.username, pkey=self.keypair['private_key']) # configure transparent vlan on server server['vlan_device'] = self._configure_vlan_transparent( port, server['ssh_client'], vlan_ipmask, mcast_groups) return server def _configure_vlan_transparent( self, port, ssh_client, vlan_ip, mcast_groups): ip_command = ip.IPCommand(ssh_client=ssh_client) for address in ip_command.list_addresses(port=port): port_iface = address.device.name break else: self.fail("Parent port fixed IP not found on server.") subport_iface = ip_command.configure_vlan_transparent( port=port, vlan_tag=self.vlan_tag, ip_addresses=[vlan_ip]) for address in ip_command.list_addresses(ip_addresses=vlan_ip): self.assertEqual(subport_iface, address.device.name) self.assertEqual(port_iface, address.device.parent) break else: self.fail("Sub-port fixed IP not found on server.") for mcast_group in mcast_groups: ip_command.add_route(mcast_group, subport_iface) return subport_iface @decorators.idempotent_id('c480cec8-3ca4-4781-baad-2e1190079467') def test_igmp_snooping_vlan_transparency(self): """Test multicast messaging between servers on the same network [Sender server] -> (Multicast network) -> [Receiver server] Scenario: 1. Create VMs for sender, receiver(s), unregistered host. Note: default(internal, same network) topology is used. mcast_groups contain only groups where no flooding to all ports expected. 2. Verify that multicast packets reach subscribed hosts and do not reach unsubscribed. For more details see _check_multicast_connectivity document text and code. """ mcast_groups = [next(self.multicast_group_iter) for _ in range(self.mcast_groups_count)] sender, receivers, unregistered = ( self._prepare_igmp_snooping_test_vlan_transparency( mcast_groups=mcast_groups, receivers_count=self.receivers_count)) self._check_multicast_conectivity( mcast_groups=mcast_groups, sender=sender, receivers=receivers, unregistered=unregistered, test_unsubscribe=False)