# Copyright 2018 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 netaddr from neutron_lib import constants from oslo_log import log from tempest.lib.common.utils import data_utils from tempest.lib import decorators 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 config from neutron_tempest_plugin import exceptions from neutron_tempest_plugin.scenario import base CONF = config.CONF LOG = log.getLogger(__name__) PYTHON3_BIN = "python3" def get_receiver_script(group, port, hello_message, ack_message, result_file): 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 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} def get_sender_script(group, port, message, result_file): 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. sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1) # 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} def get_unregistered_script(interface, group, result_file): return """#!/bin/bash export LC_ALL=en_US.UTF-8 tcpdump -i %(interface)s host %(group)s -vvneA -s0 -l -c1 &> %(result_file)s & """ % {'interface': interface, 'group': group, 'result_file': result_file} class BaseMulticastTest(object): credentials = ['primary', 'admin'] force_tenant_isolation = False # Import configuration options available_type_drivers = ( CONF.neutron_plugin_options.available_type_drivers) hello_message = "I am waiting..." multicast_port = 5007 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): 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 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_mtu') 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']) # 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) def _create_server(self): name = data_utils.rand_name("multicast-server") server = self.create_server( flavor_ref=self.flavor_ref, image_ref=self.image_ref, key_name=self.keypair['name'], name=name, networks=[{'uuid': self.network['id']}], security_groups=[{'name': self.secgroup['security_group']['name']}] )['server'] self.wait_for_server_active(server) self.wait_for_guest_os_ready(server) server['port'] = self.client.list_ports( network_id=self.network['id'], device_id=server['id'])['ports'][0] server['fip'] = self.create_floatingip(port=server['port']) server['ssh_client'] = ssh.Client(server['fip']['floating_ip_address'], self.username, pkey=self.keypair['private_key']) self._check_cmd_installed_on_server(server['ssh_client'], server, PYTHON3_BIN) return server def _prepare_sender(self, server, mcast_address): check_script = get_sender_script( group=mcast_address, port=self.multicast_port, message=self.multicast_message, result_file=self.sender_output_file) server['ssh_client'].execute_script( 'echo "%s" > /tmp/multicast_traffic_sender.py' % check_script) def _prepare_receiver(self, server, mcast_address): 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) 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, PYTHON3_BIN) server['ssh_client'].execute_script( 'echo "%s" > /tmp/multicast_traffic_receiver.py' % check_script) def _prepare_unregistered(self, server, mcast_address): ssh_client = ssh.Client( server['fip']['floating_ip_address'], self.username, pkey=self.keypair['private_key']) ip_command = ip.IPCommand(ssh_client=ssh_client) addresses = ip_command.list_addresses(port=server['port']) port_iface = ip.get_port_device_name(addresses, server['port']) check_script = get_unregistered_script( interface=port_iface, group=mcast_address, result_file=self.unregistered_output_file) self._check_cmd_installed_on_server(ssh_client, server, 'tcpdump') server['ssh_client'].execute_script( 'echo "%s" > /tmp/unregistered_traffic_receiver.sh' % check_script) @decorators.idempotent_id('113486fc-24c9-4be4-8361-03b1c9892867') def test_multicast_between_vms_on_same_network(self): """Test multicast messaging between two servers on the same network [Sender server] -> (Multicast network) -> [Receiver server] """ LOG.debug("IGMP snooping enabled: %s", CONF.neutron_plugin_options.is_igmp_snooping_enabled) sender = self._create_server() receivers = [self._create_server() for _ in range(1)] # Sender can be also receiver of multicast traffic receivers.append(sender) unregistered = self._create_server() self._check_multicast_conectivity(sender=sender, receivers=receivers, unregistered=unregistered) 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, sender, receivers, unregistered): """Test multi-cast messaging between two servers [Sender server] -> ... some network topology ... -> [Receiver server] """ mcast_address = next(self.multicast_group_iter) LOG.debug("Multicast group address: %s", mcast_address) 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 self._prepare_unregistered(unregistered, mcast_address) # Run the unregistered node script unregistered['ssh_client'].execute_script( "bash /tmp/unregistered_traffic_receiver.sh", become_root=True) self._prepare_sender(sender, mcast_address) receiver_ids = [] for receiver in receivers: self._prepare_receiver(receiver, mcast_address) receiver['ssh_client'].execute_script( "%s /tmp/multicast_traffic_receiver.py &" % PYTHON3_BIN, shell="bash") 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.append(receiver['id']) # Now lets run scripts on sender sender['ssh_client'].execute_script( "%s /tmp/multicast_traffic_sender.py" % PYTHON3_BIN) # And check if message was received for receiver in receivers: utils.wait_until_true( lambda: _message_received( receiver['ssh_client'], self.multicast_message, self.receiver_output_file), exception=RuntimeError( "Receiver {!r} didn't get multicast message".format( receiver['id']))) # TODO(slaweq): add validation of answears on sended server replies_result = sender['ssh_client'].execute_script( "cat {path} || echo '{path} not exists yet'".format( path=self.sender_output_file)) for receiver_id in receiver_ids: self.assertIn(receiver_id, replies_result) def check_unregistered_host(): unregistered_result = unregistered['ssh_client'].execute_script( "cat {path} || echo '{path} not exists yet'".format( path=self.unregistered_output_file)) LOG.debug("Unregistered VM result: %s", unregistered_result) return expected_result in unregistered_result expected_result = '1 packet captured' unregistered_error_message = ( 'Unregistered server did not received expected packet.') if not self._is_multicast_traffic_expected(mcast_address): # Kill the tcpdump command runs on the unregistered node with "-c" # option so it will be stopped automatically if it will receive # packet matching filters, # We don't expect any packets to be captured really in this case # so let's kill tcpdump so it flushes its output to the output # file. expected_result = ('0 packets captured') unregistered_error_message = ( 'Unregistered server received unexpected packet(s).') try: unregistered['ssh_client'].execute_script( "killall tcpdump && sleep 2", become_root=True) except exceptions.SSHScriptFailed: # Probably some packet was captured by tcpdump and due to that # it is already stopped self.assertTrue(check_unregistered_host(), unregistered_error_message) return utils.wait_until_true( check_unregistered_host, exception=RuntimeError(unregistered_error_message)) class MulticastTestIPv4(BaseMulticastTest, base.BaseTempestTestCase): # 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