blob: b0da235b0cea1fdcee3b6078da25bc0c744ae29d [file] [log] [blame]
Federico Ressia2aad942018-04-09 12:01:48 +02001# Copyright 2018 Red Hat, Inc.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16import netaddr
17from neutron_lib import constants
18from oslo_log import log
19from tempest.lib.common.utils import data_utils
20from tempest.lib import decorators
21
Slawek Kaplonski6b659672021-02-02 22:15:33 +000022from neutron_tempest_plugin.common import ip
Federico Ressia2aad942018-04-09 12:01:48 +020023from neutron_tempest_plugin.common import ssh
24from neutron_tempest_plugin.common import utils
25from neutron_tempest_plugin import config
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +020026from neutron_tempest_plugin import exceptions
Federico Ressia2aad942018-04-09 12:01:48 +020027from neutron_tempest_plugin.scenario import base
28
29
30CONF = config.CONF
31LOG = log.getLogger(__name__)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +020032PYTHON3_BIN = "python3"
Federico Ressia2aad942018-04-09 12:01:48 +020033
34
35def get_receiver_script(group, port, hello_message, ack_message, result_file):
36
37 return """
38import socket
39import struct
40import sys
41
42multicast_group = '%(group)s'
43server_address = ('', %(port)s)
44
45# Create the socket
46sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
47
48# Bind to the server address
49sock.bind(server_address)
50
51# Tell the operating system to add the socket to the multicast group
52# on all interfaces.
53group = socket.inet_aton(multicast_group)
54mreq = struct.pack('4sL', group, socket.INADDR_ANY)
55sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
56
57# Receive/respond loop
58with open('%(result_file)s', 'w') as f:
59 f.write('%(hello_message)s')
60 f.flush()
61 data, address = sock.recvfrom(1024)
62 f.write('received ' + str(len(data)) + ' bytes from ' + str(address))
63 f.write(str(data))
64sock.sendto(b'%(ack_message)s', address)
65 """ % {'group': group,
66 'port': port,
67 'hello_message': hello_message,
68 'ack_message': ack_message,
69 'result_file': result_file}
70
71
72def get_sender_script(group, port, message, result_file):
73
74 return """
75import socket
76import sys
77
78message = b'%(message)s'
79multicast_group = ('%(group)s', %(port)s)
80
81# Create the datagram socket
82sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
83# Set the time-to-live for messages to 1 so they do not go past the
84# local network segment.
85sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1)
86
87# Set a timeout so the socket does not block indefinitely when trying
88# to receive data.
89sock.settimeout(1)
90
91with open('%(result_file)s', 'w') as f:
92 try:
93 # Send data to the multicast group
94 sent = sock.sendto(message, multicast_group)
95
96 # Look for responses from all recipients
97 while True:
98 try:
99 data, server = sock.recvfrom(1024)
100 except socket.timeout:
101 f.write('timed out, no more responses')
102 break
103 else:
104 f.write('received reply ' + str(data) + ' from ' + str(server))
105 finally:
106 sys.stdout.write('closing socket')
107 sock.close()
108 """ % {'group': group,
109 'port': port,
110 'message': message,
111 'result_file': result_file}
112
113
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000114def get_unregistered_script(interface, group, result_file):
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000115 return """#!/bin/bash
116export LC_ALL=en_US.UTF-8
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000117tcpdump -i %(interface)s host %(group)s -vvneA -s0 -l -c1 &> %(result_file)s &
118 """ % {'interface': interface,
119 'group': group,
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000120 'result_file': result_file}
121
122
Federico Ressia2aad942018-04-09 12:01:48 +0200123class BaseMulticastTest(object):
124
125 credentials = ['primary']
126 force_tenant_isolation = False
127
128 # Import configuration options
129 available_type_drivers = (
130 CONF.neutron_plugin_options.available_type_drivers)
131
132 hello_message = "I am waiting..."
133 multicast_port = 5007
134 multicast_message = "Big Bang"
135 receiver_output_file = "/tmp/receiver_mcast_out"
136 sender_output_file = "/tmp/sender_mcast_out"
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000137 unregistered_output_file = "/tmp/unregistered_mcast_out"
Federico Ressia2aad942018-04-09 12:01:48 +0200138
139 @classmethod
140 def skip_checks(cls):
141 super(BaseMulticastTest, cls).skip_checks()
142 advanced_image_available = (
143 CONF.neutron_plugin_options.advanced_image_ref or
144 CONF.neutron_plugin_options.default_image_is_advanced)
145 if not advanced_image_available:
146 skip_reason = "This test require advanced tools for this test"
147 raise cls.skipException(skip_reason)
148
149 @classmethod
150 def resource_setup(cls):
151 super(BaseMulticastTest, cls).resource_setup()
152
153 if CONF.neutron_plugin_options.default_image_is_advanced:
154 cls.flavor_ref = CONF.compute.flavor_ref
155 cls.image_ref = CONF.compute.image_ref
156 cls.username = CONF.validation.image_ssh_user
157 else:
158 cls.flavor_ref = (
159 CONF.neutron_plugin_options.advanced_image_flavor_ref)
160 cls.image_ref = CONF.neutron_plugin_options.advanced_image_ref
161 cls.username = CONF.neutron_plugin_options.advanced_image_ssh_user
162
163 # setup basic topology for servers we can log into it
164 cls.network = cls.create_network()
165 cls.subnet = cls.create_subnet(cls.network)
166 cls.router = cls.create_router_by_client()
167 cls.create_router_interface(cls.router['id'], cls.subnet['id'])
168
169 cls.keypair = cls.create_keypair()
170
171 cls.secgroup = cls.os_primary.network_client.create_security_group(
172 name='secgroup_mtu')
173 cls.security_groups.append(cls.secgroup['security_group'])
174 cls.create_loginable_secgroup_rule(
175 secgroup_id=cls.secgroup['security_group']['id'])
176 cls.create_pingable_secgroup_rule(
177 secgroup_id=cls.secgroup['security_group']['id'])
178 # Create security group rule for UDP (multicast traffic)
179 cls.create_secgroup_rules(
180 rule_list=[dict(protocol=constants.PROTO_NAME_UDP,
181 direction=constants.INGRESS_DIRECTION,
182 remote_ip_prefix=cls.any_addresses,
183 ethertype=cls.ethertype)],
184 secgroup_id=cls.secgroup['security_group']['id'])
185
186 # Multicast IP range to be used for multicast group IP asignement
187 if '-' in cls.multicast_group_range:
188 multicast_group_range = netaddr.IPRange(
189 *cls.multicast_group_range.split('-'))
190 else:
191 multicast_group_range = netaddr.IPNetwork(
192 cls.multicast_group_range)
193 cls.multicast_group_iter = iter(multicast_group_range)
194
195 def _create_server(self):
196 name = data_utils.rand_name("multicast-server")
197 server = self.create_server(
198 flavor_ref=self.flavor_ref,
199 image_ref=self.image_ref,
200 key_name=self.keypair['name'], name=name,
201 networks=[{'uuid': self.network['id']}],
202 security_groups=[{'name': self.secgroup['security_group']['name']}]
203 )['server']
204 self.wait_for_server_active(server)
Slawek Kaplonski2211eab2020-10-20 16:43:53 +0200205 self.wait_for_guest_os_ready(server)
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000206 server['port'] = self.client.list_ports(
Federico Ressia2aad942018-04-09 12:01:48 +0200207 network_id=self.network['id'], device_id=server['id'])['ports'][0]
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000208 server['fip'] = self.create_floatingip(port=server['port'])
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200209 server['ssh_client'] = ssh.Client(server['fip']['floating_ip_address'],
210 self.username,
211 pkey=self.keypair['private_key'])
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000212 self._check_cmd_installed_on_server(server['ssh_client'],
213 server['id'], PYTHON3_BIN)
Federico Ressia2aad942018-04-09 12:01:48 +0200214 return server
215
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000216 def _check_cmd_installed_on_server(self, ssh_client, server_id, cmd):
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200217 try:
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000218 ssh_client.execute_script('which %s' % cmd)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200219 except exceptions.SSHScriptFailed:
220 raise self.skipException(
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000221 "%s is not available on server %s" % (cmd, server_id))
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200222
Federico Ressia2aad942018-04-09 12:01:48 +0200223 def _prepare_sender(self, server, mcast_address):
224 check_script = get_sender_script(
225 group=mcast_address, port=self.multicast_port,
226 message=self.multicast_message,
227 result_file=self.sender_output_file)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200228 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100229 'echo "%s" > /tmp/multicast_traffic_sender.py' % check_script)
Federico Ressia2aad942018-04-09 12:01:48 +0200230
231 def _prepare_receiver(self, server, mcast_address):
232 check_script = get_receiver_script(
233 group=mcast_address, port=self.multicast_port,
234 hello_message=self.hello_message, ack_message=server['id'],
235 result_file=self.receiver_output_file)
236 ssh_client = ssh.Client(
237 server['fip']['floating_ip_address'],
238 self.username,
239 pkey=self.keypair['private_key'])
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000240 self._check_cmd_installed_on_server(ssh_client, server['id'],
241 PYTHON3_BIN)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200242 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100243 'echo "%s" > /tmp/multicast_traffic_receiver.py' % check_script)
Federico Ressia2aad942018-04-09 12:01:48 +0200244
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000245 def _prepare_unregistered(self, server, mcast_address):
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000246 ssh_client = ssh.Client(
247 server['fip']['floating_ip_address'],
248 self.username,
249 pkey=self.keypair['private_key'])
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000250 ip_command = ip.IPCommand(ssh_client=ssh_client)
251 addresses = ip_command.list_addresses(port=server['port'])
252 port_iface = ip.get_port_device_name(addresses, server['port'])
253 check_script = get_unregistered_script(
254 interface=port_iface, group=mcast_address,
255 result_file=self.unregistered_output_file)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000256 self._check_cmd_installed_on_server(ssh_client, server['id'],
257 'tcpdump')
258 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100259 'echo "%s" > /tmp/unregistered_traffic_receiver.sh' % check_script)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000260
Federico Ressia2aad942018-04-09 12:01:48 +0200261 @decorators.idempotent_id('113486fc-24c9-4be4-8361-03b1c9892867')
262 def test_multicast_between_vms_on_same_network(self):
263 """Test multicast messaging between two servers on the same network
264
265 [Sender server] -> (Multicast network) -> [Receiver server]
266 """
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100267 LOG.debug("IGMP snooping enabled: %s",
268 CONF.neutron_plugin_options.is_igmp_snooping_enabled)
Federico Ressia2aad942018-04-09 12:01:48 +0200269 sender = self._create_server()
270 receivers = [self._create_server() for _ in range(1)]
271 # Sender can be also receiver of multicast traffic
272 receivers.append(sender)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000273 unregistered = self._create_server()
274 self._check_multicast_conectivity(sender=sender, receivers=receivers,
275 unregistered=unregistered)
Federico Ressia2aad942018-04-09 12:01:48 +0200276
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000277 def _is_multicast_traffic_expected(self, mcast_address):
278 """Checks if multicast traffic is expected to arrive.
279
280 Checks if multicast traffic is expected to arrive to the
281 unregistered VM.
282
283 If IGMP snooping is enabled, multicast traffic should not be
284 flooded unless the destination IP is in the range of 224.0.0.X
285 [0].
286
287 [0] https://tools.ietf.org/html/rfc4541 (See section 2.1.2)
288 """
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100289 return (str(mcast_address).startswith('224.0.0') or not
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000290 CONF.neutron_plugin_options.is_igmp_snooping_enabled)
291
292 def _check_multicast_conectivity(self, sender, receivers, unregistered):
Federico Ressia2aad942018-04-09 12:01:48 +0200293 """Test multi-cast messaging between two servers
294
295 [Sender server] -> ... some network topology ... -> [Receiver server]
296 """
297 mcast_address = next(self.multicast_group_iter)
298 LOG.debug("Multicast group address: %s", mcast_address)
299
300 def _message_received(client, msg, file_path):
301 result = client.execute_script(
302 "cat {path} || echo '{path} not exists yet'".format(
303 path=file_path))
304 return msg in result
305
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000306 self._prepare_unregistered(unregistered, mcast_address)
307
308 # Run the unregistered node script
309 unregistered['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100310 "bash /tmp/unregistered_traffic_receiver.sh", become_root=True)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000311
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200312 self._prepare_sender(sender, mcast_address)
Federico Ressia2aad942018-04-09 12:01:48 +0200313 receiver_ids = []
314 for receiver in receivers:
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200315 self._prepare_receiver(receiver, mcast_address)
316 receiver['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100317 "%s /tmp/multicast_traffic_receiver.py &" % PYTHON3_BIN,
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200318 shell="bash")
Federico Ressia2aad942018-04-09 12:01:48 +0200319 utils.wait_until_true(
320 lambda: _message_received(
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200321 receiver['ssh_client'], self.hello_message,
Federico Ressia2aad942018-04-09 12:01:48 +0200322 self.receiver_output_file),
323 exception=RuntimeError(
324 "Receiver script didn't start properly on server "
325 "{!r}.".format(receiver['id'])))
326
Federico Ressia2aad942018-04-09 12:01:48 +0200327 receiver_ids.append(receiver['id'])
328
329 # Now lets run scripts on sender
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200330 sender['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100331 "%s /tmp/multicast_traffic_sender.py" % PYTHON3_BIN)
Federico Ressia2aad942018-04-09 12:01:48 +0200332
333 # And check if message was received
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200334 for receiver in receivers:
Federico Ressia2aad942018-04-09 12:01:48 +0200335 utils.wait_until_true(
336 lambda: _message_received(
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200337 receiver['ssh_client'], self.multicast_message,
Federico Ressia2aad942018-04-09 12:01:48 +0200338 self.receiver_output_file),
339 exception=RuntimeError(
340 "Receiver {!r} didn't get multicast message".format(
341 receiver['id'])))
342
343 # TODO(slaweq): add validation of answears on sended server
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200344 replies_result = sender['ssh_client'].execute_script(
Federico Ressia2aad942018-04-09 12:01:48 +0200345 "cat {path} || echo '{path} not exists yet'".format(
346 path=self.sender_output_file))
347 for receiver_id in receiver_ids:
348 self.assertIn(receiver_id, replies_result)
349
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000350 def check_unregistered_host():
351 unregistered_result = unregistered['ssh_client'].execute_script(
352 "cat {path} || echo '{path} not exists yet'".format(
353 path=self.unregistered_output_file))
354 LOG.debug("Unregistered VM result: %s", unregistered_result)
355 return expected_result in unregistered_result
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000356
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000357 expected_result = '1 packet captured'
358 unregistered_error_message = (
359 'Unregistered server did not received expected packet.')
360 if not self._is_multicast_traffic_expected(mcast_address):
361 # Kill the tcpdump command runs on the unregistered node with "-c"
362 # option so it will be stopped automatically if it will receive
363 # packet matching filters,
364 # We don't expect any packets to be captured really in this case
365 # so let's kill tcpdump so it flushes its output to the output
366 # file.
367 expected_result = (
368 '0 packets captured\n0 packets received by filter')
369 unregistered_error_message = (
370 'Unregistered server received unexpected packet(s).')
371 try:
372 unregistered['ssh_client'].execute_script(
373 "killall tcpdump && sleep 2", become_root=True)
374 except exceptions.SSHScriptFailed:
375 # Probably some packet was captured by tcpdump and due to that
376 # it is already stopped
377 self.assertTrue(check_unregistered_host(),
378 unregistered_error_message)
379 return
380
381 utils.wait_until_true(
382 check_unregistered_host,
383 exception=RuntimeError(unregistered_error_message))
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000384
Federico Ressia2aad942018-04-09 12:01:48 +0200385
386class MulticastTestIPv4(BaseMulticastTest, base.BaseTempestTestCase):
387
388 # Import configuration options
389 multicast_group_range = CONF.neutron_plugin_options.multicast_group_range
390
391 # IP version specific parameters
392 _ip_version = constants.IP_VERSION_4
393 any_addresses = constants.IPv4_ANY