blob: 4d79f859c377be891f8f765c85f8563b3740fdc5 [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
Slawek Kaplonskiedf3cba2021-04-21 10:34:02 +0200125 credentials = ['primary', 'admin']
Federico Ressia2aad942018-04-09 12:01:48 +0200126 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
Renjing Xiaoe6e4b892025-12-23 12:46:10 +0000153 cls.setup_advanced_image()
Federico Ressia2aad942018-04-09 12:01:48 +0200154
155 # setup basic topology for servers we can log into it
156 cls.network = cls.create_network()
157 cls.subnet = cls.create_subnet(cls.network)
158 cls.router = cls.create_router_by_client()
159 cls.create_router_interface(cls.router['id'], cls.subnet['id'])
160
161 cls.keypair = cls.create_keypair()
162
163 cls.secgroup = cls.os_primary.network_client.create_security_group(
164 name='secgroup_mtu')
165 cls.security_groups.append(cls.secgroup['security_group'])
166 cls.create_loginable_secgroup_rule(
167 secgroup_id=cls.secgroup['security_group']['id'])
168 cls.create_pingable_secgroup_rule(
169 secgroup_id=cls.secgroup['security_group']['id'])
170 # Create security group rule for UDP (multicast traffic)
171 cls.create_secgroup_rules(
172 rule_list=[dict(protocol=constants.PROTO_NAME_UDP,
173 direction=constants.INGRESS_DIRECTION,
174 remote_ip_prefix=cls.any_addresses,
175 ethertype=cls.ethertype)],
176 secgroup_id=cls.secgroup['security_group']['id'])
177
178 # Multicast IP range to be used for multicast group IP asignement
179 if '-' in cls.multicast_group_range:
180 multicast_group_range = netaddr.IPRange(
181 *cls.multicast_group_range.split('-'))
182 else:
183 multicast_group_range = netaddr.IPNetwork(
184 cls.multicast_group_range)
185 cls.multicast_group_iter = iter(multicast_group_range)
186
187 def _create_server(self):
188 name = data_utils.rand_name("multicast-server")
189 server = self.create_server(
190 flavor_ref=self.flavor_ref,
191 image_ref=self.image_ref,
192 key_name=self.keypair['name'], name=name,
193 networks=[{'uuid': self.network['id']}],
194 security_groups=[{'name': self.secgroup['security_group']['name']}]
195 )['server']
196 self.wait_for_server_active(server)
Slawek Kaplonski2211eab2020-10-20 16:43:53 +0200197 self.wait_for_guest_os_ready(server)
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000198 server['port'] = self.client.list_ports(
Federico Ressia2aad942018-04-09 12:01:48 +0200199 network_id=self.network['id'], device_id=server['id'])['ports'][0]
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000200 server['fip'] = self.create_floatingip(port=server['port'])
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200201 server['ssh_client'] = ssh.Client(server['fip']['floating_ip_address'],
202 self.username,
203 pkey=self.keypair['private_key'])
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000204 self._check_cmd_installed_on_server(server['ssh_client'],
yatinkarelc4597e62021-11-26 14:09:18 +0530205 server, PYTHON3_BIN)
Federico Ressia2aad942018-04-09 12:01:48 +0200206 return server
207
208 def _prepare_sender(self, server, mcast_address):
209 check_script = get_sender_script(
210 group=mcast_address, port=self.multicast_port,
211 message=self.multicast_message,
212 result_file=self.sender_output_file)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200213 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100214 'echo "%s" > /tmp/multicast_traffic_sender.py' % check_script)
Federico Ressia2aad942018-04-09 12:01:48 +0200215
216 def _prepare_receiver(self, server, mcast_address):
217 check_script = get_receiver_script(
218 group=mcast_address, port=self.multicast_port,
219 hello_message=self.hello_message, ack_message=server['id'],
220 result_file=self.receiver_output_file)
221 ssh_client = ssh.Client(
222 server['fip']['floating_ip_address'],
223 self.username,
224 pkey=self.keypair['private_key'])
yatinkarelc4597e62021-11-26 14:09:18 +0530225 self._check_cmd_installed_on_server(ssh_client, server,
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000226 PYTHON3_BIN)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200227 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100228 'echo "%s" > /tmp/multicast_traffic_receiver.py' % check_script)
Federico Ressia2aad942018-04-09 12:01:48 +0200229
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000230 def _prepare_unregistered(self, server, mcast_address):
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000231 ssh_client = ssh.Client(
232 server['fip']['floating_ip_address'],
233 self.username,
234 pkey=self.keypair['private_key'])
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000235 ip_command = ip.IPCommand(ssh_client=ssh_client)
236 addresses = ip_command.list_addresses(port=server['port'])
237 port_iface = ip.get_port_device_name(addresses, server['port'])
238 check_script = get_unregistered_script(
239 interface=port_iface, group=mcast_address,
240 result_file=self.unregistered_output_file)
yatinkarelc4597e62021-11-26 14:09:18 +0530241 self._check_cmd_installed_on_server(ssh_client, server,
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000242 'tcpdump')
243 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100244 'echo "%s" > /tmp/unregistered_traffic_receiver.sh' % check_script)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000245
Federico Ressia2aad942018-04-09 12:01:48 +0200246 @decorators.idempotent_id('113486fc-24c9-4be4-8361-03b1c9892867')
247 def test_multicast_between_vms_on_same_network(self):
248 """Test multicast messaging between two servers on the same network
249
250 [Sender server] -> (Multicast network) -> [Receiver server]
251 """
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100252 LOG.debug("IGMP snooping enabled: %s",
253 CONF.neutron_plugin_options.is_igmp_snooping_enabled)
Federico Ressia2aad942018-04-09 12:01:48 +0200254 sender = self._create_server()
255 receivers = [self._create_server() for _ in range(1)]
256 # Sender can be also receiver of multicast traffic
257 receivers.append(sender)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000258 unregistered = self._create_server()
259 self._check_multicast_conectivity(sender=sender, receivers=receivers,
260 unregistered=unregistered)
Federico Ressia2aad942018-04-09 12:01:48 +0200261
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000262 def _is_multicast_traffic_expected(self, mcast_address):
263 """Checks if multicast traffic is expected to arrive.
264
265 Checks if multicast traffic is expected to arrive to the
266 unregistered VM.
267
268 If IGMP snooping is enabled, multicast traffic should not be
269 flooded unless the destination IP is in the range of 224.0.0.X
270 [0].
271
272 [0] https://tools.ietf.org/html/rfc4541 (See section 2.1.2)
273 """
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100274 return (str(mcast_address).startswith('224.0.0') or not
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000275 CONF.neutron_plugin_options.is_igmp_snooping_enabled)
276
277 def _check_multicast_conectivity(self, sender, receivers, unregistered):
Federico Ressia2aad942018-04-09 12:01:48 +0200278 """Test multi-cast messaging between two servers
279
280 [Sender server] -> ... some network topology ... -> [Receiver server]
281 """
282 mcast_address = next(self.multicast_group_iter)
283 LOG.debug("Multicast group address: %s", mcast_address)
284
285 def _message_received(client, msg, file_path):
286 result = client.execute_script(
287 "cat {path} || echo '{path} not exists yet'".format(
288 path=file_path))
289 return msg in result
290
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000291 self._prepare_unregistered(unregistered, mcast_address)
292
293 # Run the unregistered node script
294 unregistered['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100295 "bash /tmp/unregistered_traffic_receiver.sh", become_root=True)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000296
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200297 self._prepare_sender(sender, mcast_address)
Federico Ressia2aad942018-04-09 12:01:48 +0200298 receiver_ids = []
299 for receiver in receivers:
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200300 self._prepare_receiver(receiver, mcast_address)
301 receiver['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100302 "%s /tmp/multicast_traffic_receiver.py &" % PYTHON3_BIN,
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200303 shell="bash")
Federico Ressia2aad942018-04-09 12:01:48 +0200304 utils.wait_until_true(
305 lambda: _message_received(
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200306 receiver['ssh_client'], self.hello_message,
Federico Ressia2aad942018-04-09 12:01:48 +0200307 self.receiver_output_file),
308 exception=RuntimeError(
309 "Receiver script didn't start properly on server "
310 "{!r}.".format(receiver['id'])))
311
Federico Ressia2aad942018-04-09 12:01:48 +0200312 receiver_ids.append(receiver['id'])
313
314 # Now lets run scripts on sender
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200315 sender['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100316 "%s /tmp/multicast_traffic_sender.py" % PYTHON3_BIN)
Federico Ressia2aad942018-04-09 12:01:48 +0200317
318 # And check if message was received
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200319 for receiver in receivers:
Federico Ressia2aad942018-04-09 12:01:48 +0200320 utils.wait_until_true(
321 lambda: _message_received(
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200322 receiver['ssh_client'], self.multicast_message,
Federico Ressia2aad942018-04-09 12:01:48 +0200323 self.receiver_output_file),
324 exception=RuntimeError(
325 "Receiver {!r} didn't get multicast message".format(
326 receiver['id'])))
327
Renjing Xiao98dacff2025-09-01 16:24:10 +0100328 def _sender_completed():
329 replies_result = sender['ssh_client'].execute_script(
330 "cat {path} 2>/dev/null || echo ''".format(
331 path=self.sender_output_file))
332 for receiver_id in receiver_ids:
333 expected_pattern = "received reply b'{}' from".format(
334 receiver_id)
335 if expected_pattern not in replies_result:
336 return False
337 return replies_result.count('received reply') == len(receiver_ids)
338
339 utils.wait_until_true(
340 _sender_completed,
341 exception=RuntimeError("Sender didn't complete properly"))
Federico Ressia2aad942018-04-09 12:01:48 +0200342
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000343 def check_unregistered_host():
344 unregistered_result = unregistered['ssh_client'].execute_script(
345 "cat {path} || echo '{path} not exists yet'".format(
346 path=self.unregistered_output_file))
347 LOG.debug("Unregistered VM result: %s", unregistered_result)
348 return expected_result in unregistered_result
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000349
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000350 expected_result = '1 packet captured'
351 unregistered_error_message = (
352 'Unregistered server did not received expected packet.')
353 if not self._is_multicast_traffic_expected(mcast_address):
354 # Kill the tcpdump command runs on the unregistered node with "-c"
355 # option so it will be stopped automatically if it will receive
356 # packet matching filters,
357 # We don't expect any packets to be captured really in this case
358 # so let's kill tcpdump so it flushes its output to the output
359 # file.
Slawek Kaplonski8de8b992021-05-19 22:48:33 +0200360 expected_result = ('0 packets captured')
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000361 unregistered_error_message = (
362 'Unregistered server received unexpected packet(s).')
363 try:
364 unregistered['ssh_client'].execute_script(
365 "killall tcpdump && sleep 2", become_root=True)
366 except exceptions.SSHScriptFailed:
367 # Probably some packet was captured by tcpdump and due to that
368 # it is already stopped
369 self.assertTrue(check_unregistered_host(),
370 unregistered_error_message)
371 return
372
373 utils.wait_until_true(
374 check_unregistered_host,
375 exception=RuntimeError(unregistered_error_message))
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000376
Federico Ressia2aad942018-04-09 12:01:48 +0200377
378class MulticastTestIPv4(BaseMulticastTest, base.BaseTempestTestCase):
379
380 # Import configuration options
381 multicast_group_range = CONF.neutron_plugin_options.multicast_group_range
382
383 # IP version specific parameters
384 _ip_version = constants.IP_VERSION_4
385 any_addresses = constants.IPv4_ANY