# 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_tempest_plugin.common import ip from neutron_tempest_plugin.common import ssh 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 from whitebox_neutron_tempest_plugin.tests.scenario import base CONF = config.CONF WB_CONF = CONF.whitebox_neutron_plugin_options LOG = log.getLogger(__name__) def get_capture_script(result_file, interface): return """#!/bin/bash export LC_ALL=en_US.UTF-8 tcpdump -Qin -i %(interface)s -vvneA -s0 -l icmp &> %(result_file)s & """ % {'result_file': result_file, 'interface': interface} # TODO(eolivare): create a parent class for BaseBroadcastTest and # BaseMulticastTest with common code class BaseBroadcastTest(object): vlan_transparent = False servers = [] # Import configuration options receivers_count = WB_CONF.broadcast_receivers_count capture_output_file = "/tmp/capture_broadcast_out" # the following values can be used to send ping messages including once the # text "Hi here" broadcast_message = "Hi here" broadcast_message_pattern = "486920686572652e" ping_size = 23 @classmethod def skip_checks(cls): super(BaseBroadcastTest, 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(BaseBroadcastTest, 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 try: if cls.vlan_transparent: cls.network = cls.create_network( vlan_transparent=cls.vlan_transparent) else: cls.network = cls.create_network() except exceptions.ServerFault as exc: msg = 'Backend does not support VLAN Transparency.' if exc.resp_body['message'] == msg: raise cls.skipException(msg) else: raise exc 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_bcast') 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']) def _create_server(self, server_type, vlan_tag=None, scheduler_hints=None): network_id = self.network['id'] if self.vlan_transparent: server_index = len(self.servers) vlan_ipmask = self.vlan_ipmast_template.format( ip_last_byte=server_index + 10) allowed_address_pairs = [{'ip_address': vlan_ipmask}] port = self.create_port( network={'id': network_id}, security_groups=[self.secgroup['security_group']['id']], allowed_address_pairs=allowed_address_pairs) networks = [{'port': port['id']}] else: networks = [{'uuid': network_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(server_type) } if not (CONF.compute.min_compute_nodes > 1): LOG.debug('number of compute nodes is not higher than 1 - ' 'scheduler_hints will not be used') elif scheduler_hints: params['scheduler_hints'] = scheduler_hints server = self.create_server(**params)['server'] self.wait_for_server_active(server) port = self.client.list_ports(device_id=server['id'])['ports'][0] 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']) # enable icmp broadcast responses server['ssh_client'].execute_script( "sysctl net.ipv4.icmp_echo_ignore_broadcasts=0", become_root=True) if self.vlan_transparent: # configure transparent vlan on server vlan_tag = vlan_tag or self.vlan_tag server['vlan_device'] = self._configure_vlan_transparent( port, server['ssh_client'], vlan_ipmask, vlan_tag) self.servers.append(server) return server def _prepare_capture_script(self, server): interface = server['vlan_device'] if self.vlan_transparent else 'any' capture_script = get_capture_script( result_file=self.capture_output_file, interface=interface) server['ssh_client'].execute_script( 'echo "%s" > /tmp/capture_script.sh' % capture_script) def _check_broadcast_conectivity(self, sender, receivers, nonreceivers=[], num_pings=1): 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, file_path, expected_count): """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 = %s', result) count = len( re.findall(self.broadcast_message, result)) self.assertEqual( count, expected_count, 'Received number of messages ({}) differs ' 'from the expected ({})'.format( count, num_pings)) # tcpdump started both on receivers and nonreceivers for server in receivers + nonreceivers: self._prepare_capture_script(server) server['ssh_client'].execute_script( "bash /tmp/capture_script.sh", become_root=True) if not self.vlan_transparent: cidr = self.subnet['cidr'] else: cidr = self.vlan_ipmast_template.format(ip_last_byte=0) broadcast_ip = netaddr.IPNetwork(cidr).broadcast # waiting until tcpdump capturing on receivers time.sleep(2) sender['ssh_client'].execute_script( ("ping -b %(broadcast_ip)s -s %(ping_size)d " "-c %(num_pings)d -p %(ping_pattern)s") % { 'broadcast_ip': broadcast_ip, 'ping_size': self.ping_size, 'num_pings': num_pings, 'ping_pattern': self.broadcast_message_pattern}) # waiting until packets reached receivers time.sleep(2) # num_ping packets expected on each receiver server for server in receivers: server['ssh_client'].execute_script( "killall tcpdump && sleep 2", become_root=True) LOG.debug('Validating number of messages on ' 'receiver %s', server['id']) _validate_number_of_messages( server['ssh_client'], self.capture_output_file, num_pings) # No packets expected on each nonreceiver server for server in nonreceivers: server['ssh_client'].execute_script( "killall tcpdump && sleep 2", become_root=True) LOG.debug('Validating number of messages on ' 'receiver %s', server['id']) _validate_number_of_messages( server['ssh_client'], self.capture_output_file, 0) class BroadcastTestIPv4(BaseBroadcastTest, base.TrafficFlowTest): # IP version specific parameters _ip_version = constants.IP_VERSION_4 any_addresses = constants.IPv4_ANY class BroadcastTestIPv4Common(BroadcastTestIPv4): @decorators.idempotent_id('7f33370a-5f46-4452-8b2f-166acda1720f') def test_broadcast_same_network(self): """Test broadcast messaging between servers on the same network [Sender server] -> (Broadcast network) -> [Receiver server] Scenario: 1. Create VMs for sender, receiver(s) on a common internal network and send broadcast ping messages 2. Verify that all broadcast packets reach the other hosts In case of vlan_transparent, broadcast packets will be captured on VLAN interfaces """ sender = self._create_server('broadcast-sender') receivers = [] for i in range(self.receivers_count): # even VMs scheduled on a different compute from the sender # odd VMs scheduled on the same compute than the sender if i % 2 == 0: scheduler_hints = {'different_host': sender['id']} else: scheduler_hints = {'same_host': sender['id']} receivers.append(self._create_server( 'broadcast-receiver', scheduler_hints=scheduler_hints)) self._check_broadcast_conectivity(sender=sender, receivers=receivers, num_pings=2) class BroadcastTestVlanTransparency(BroadcastTestIPv4): required_extensions = ['vlan-transparent', 'allowed-address-pairs'] vlan_transparent = True vlan_tag = 123 vlan_ipmast_template = '192.168.111.{ip_last_byte}/24' def _configure_vlan_transparent(self, port, ssh_client, vlan_ip, vlan_tag): 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=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.") return subport_iface @decorators.idempotent_id('7ea12762-31af-4bf2-9219-c54212171010') def test_broadcast_vlan_transparency(self): """Test broadcast messaging between servers on the same network, but using different VLANs [Sender server] -> (Broadcast network) -> [Receiver server] Scenario: 1. Create VMs for sender, receiver(s) on a common internal network and send broadcast ping messages 2. Verify that all broadcast packets reach the other hosts within a common VLAN and that receivers with different VLANs do not receive those messages """ vlan_groups = (self.vlan_tag, self.vlan_tag + 1) senders = {} receivers = {} for vlan_group in vlan_groups: senders[vlan_group] = self._create_server( 'broadcast-sender-%d' % vlan_group, vlan_tag=vlan_group) receivers[vlan_group] = [] for i in range(self.receivers_count): # even VMs scheduled on a different compute from the sender # odd VMs scheduled on the same compute than the sender if i % 2 == 0: scheduler_hints = { 'different_host': senders[vlan_group]['id']} else: scheduler_hints = {'same_host': senders[vlan_group]['id']} receivers[vlan_group].append( self._create_server('broadcast-receiver-%d' % vlan_group, vlan_tag=vlan_group, scheduler_hints=scheduler_hints)) self._check_broadcast_conectivity( sender=senders[vlan_groups[0]], receivers=receivers[vlan_groups[0]], nonreceivers=receivers[vlan_groups[1]], num_pings=2) self._check_broadcast_conectivity( sender=senders[vlan_groups[1]], receivers=receivers[vlan_groups[1]], nonreceivers=receivers[vlan_groups[0]], num_pings=2)