blob: 4fd41cf72c669ddfc9011c90aabd99cda9999bab [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
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'],
yatinkarelc4597e62021-11-26 14:09:18 +0530213 server, PYTHON3_BIN)
Federico Ressia2aad942018-04-09 12:01:48 +0200214 return server
215
yatinkarelc4597e62021-11-26 14:09:18 +0530216 def _check_cmd_installed_on_server(self, ssh_client, server, 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)
Eduardo Olivares46fa4242022-04-18 12:47:43 +0200219 except base.SSH_EXC_TUPLE as ssh_e:
yatinkarelc4597e62021-11-26 14:09:18 +0530220 LOG.debug(ssh_e)
221 self._log_console_output([server])
222 self._log_local_network_status()
223 raise
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200224 except exceptions.SSHScriptFailed:
225 raise self.skipException(
yatinkarelc4597e62021-11-26 14:09:18 +0530226 "%s is not available on server %s" % (cmd, server['id']))
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200227
Federico Ressia2aad942018-04-09 12:01:48 +0200228 def _prepare_sender(self, server, mcast_address):
229 check_script = get_sender_script(
230 group=mcast_address, port=self.multicast_port,
231 message=self.multicast_message,
232 result_file=self.sender_output_file)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200233 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100234 'echo "%s" > /tmp/multicast_traffic_sender.py' % check_script)
Federico Ressia2aad942018-04-09 12:01:48 +0200235
236 def _prepare_receiver(self, server, mcast_address):
237 check_script = get_receiver_script(
238 group=mcast_address, port=self.multicast_port,
239 hello_message=self.hello_message, ack_message=server['id'],
240 result_file=self.receiver_output_file)
241 ssh_client = ssh.Client(
242 server['fip']['floating_ip_address'],
243 self.username,
244 pkey=self.keypair['private_key'])
yatinkarelc4597e62021-11-26 14:09:18 +0530245 self._check_cmd_installed_on_server(ssh_client, server,
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000246 PYTHON3_BIN)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200247 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100248 'echo "%s" > /tmp/multicast_traffic_receiver.py' % check_script)
Federico Ressia2aad942018-04-09 12:01:48 +0200249
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000250 def _prepare_unregistered(self, server, mcast_address):
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000251 ssh_client = ssh.Client(
252 server['fip']['floating_ip_address'],
253 self.username,
254 pkey=self.keypair['private_key'])
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000255 ip_command = ip.IPCommand(ssh_client=ssh_client)
256 addresses = ip_command.list_addresses(port=server['port'])
257 port_iface = ip.get_port_device_name(addresses, server['port'])
258 check_script = get_unregistered_script(
259 interface=port_iface, group=mcast_address,
260 result_file=self.unregistered_output_file)
yatinkarelc4597e62021-11-26 14:09:18 +0530261 self._check_cmd_installed_on_server(ssh_client, server,
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000262 'tcpdump')
263 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100264 'echo "%s" > /tmp/unregistered_traffic_receiver.sh' % check_script)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000265
Federico Ressia2aad942018-04-09 12:01:48 +0200266 @decorators.idempotent_id('113486fc-24c9-4be4-8361-03b1c9892867')
267 def test_multicast_between_vms_on_same_network(self):
268 """Test multicast messaging between two servers on the same network
269
270 [Sender server] -> (Multicast network) -> [Receiver server]
271 """
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100272 LOG.debug("IGMP snooping enabled: %s",
273 CONF.neutron_plugin_options.is_igmp_snooping_enabled)
Federico Ressia2aad942018-04-09 12:01:48 +0200274 sender = self._create_server()
275 receivers = [self._create_server() for _ in range(1)]
276 # Sender can be also receiver of multicast traffic
277 receivers.append(sender)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000278 unregistered = self._create_server()
279 self._check_multicast_conectivity(sender=sender, receivers=receivers,
280 unregistered=unregistered)
Federico Ressia2aad942018-04-09 12:01:48 +0200281
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000282 def _is_multicast_traffic_expected(self, mcast_address):
283 """Checks if multicast traffic is expected to arrive.
284
285 Checks if multicast traffic is expected to arrive to the
286 unregistered VM.
287
288 If IGMP snooping is enabled, multicast traffic should not be
289 flooded unless the destination IP is in the range of 224.0.0.X
290 [0].
291
292 [0] https://tools.ietf.org/html/rfc4541 (See section 2.1.2)
293 """
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100294 return (str(mcast_address).startswith('224.0.0') or not
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000295 CONF.neutron_plugin_options.is_igmp_snooping_enabled)
296
297 def _check_multicast_conectivity(self, sender, receivers, unregistered):
Federico Ressia2aad942018-04-09 12:01:48 +0200298 """Test multi-cast messaging between two servers
299
300 [Sender server] -> ... some network topology ... -> [Receiver server]
301 """
302 mcast_address = next(self.multicast_group_iter)
303 LOG.debug("Multicast group address: %s", mcast_address)
304
305 def _message_received(client, msg, file_path):
306 result = client.execute_script(
307 "cat {path} || echo '{path} not exists yet'".format(
308 path=file_path))
309 return msg in result
310
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000311 self._prepare_unregistered(unregistered, mcast_address)
312
313 # Run the unregistered node script
314 unregistered['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100315 "bash /tmp/unregistered_traffic_receiver.sh", become_root=True)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000316
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200317 self._prepare_sender(sender, mcast_address)
Federico Ressia2aad942018-04-09 12:01:48 +0200318 receiver_ids = []
319 for receiver in receivers:
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200320 self._prepare_receiver(receiver, mcast_address)
321 receiver['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100322 "%s /tmp/multicast_traffic_receiver.py &" % PYTHON3_BIN,
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200323 shell="bash")
Federico Ressia2aad942018-04-09 12:01:48 +0200324 utils.wait_until_true(
325 lambda: _message_received(
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200326 receiver['ssh_client'], self.hello_message,
Federico Ressia2aad942018-04-09 12:01:48 +0200327 self.receiver_output_file),
328 exception=RuntimeError(
329 "Receiver script didn't start properly on server "
330 "{!r}.".format(receiver['id'])))
331
Federico Ressia2aad942018-04-09 12:01:48 +0200332 receiver_ids.append(receiver['id'])
333
334 # Now lets run scripts on sender
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200335 sender['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100336 "%s /tmp/multicast_traffic_sender.py" % PYTHON3_BIN)
Federico Ressia2aad942018-04-09 12:01:48 +0200337
338 # And check if message was received
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200339 for receiver in receivers:
Federico Ressia2aad942018-04-09 12:01:48 +0200340 utils.wait_until_true(
341 lambda: _message_received(
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200342 receiver['ssh_client'], self.multicast_message,
Federico Ressia2aad942018-04-09 12:01:48 +0200343 self.receiver_output_file),
344 exception=RuntimeError(
345 "Receiver {!r} didn't get multicast message".format(
346 receiver['id'])))
347
348 # TODO(slaweq): add validation of answears on sended server
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200349 replies_result = sender['ssh_client'].execute_script(
Federico Ressia2aad942018-04-09 12:01:48 +0200350 "cat {path} || echo '{path} not exists yet'".format(
351 path=self.sender_output_file))
352 for receiver_id in receiver_ids:
353 self.assertIn(receiver_id, replies_result)
354
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000355 def check_unregistered_host():
356 unregistered_result = unregistered['ssh_client'].execute_script(
357 "cat {path} || echo '{path} not exists yet'".format(
358 path=self.unregistered_output_file))
359 LOG.debug("Unregistered VM result: %s", unregistered_result)
360 return expected_result in unregistered_result
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000361
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000362 expected_result = '1 packet captured'
363 unregistered_error_message = (
364 'Unregistered server did not received expected packet.')
365 if not self._is_multicast_traffic_expected(mcast_address):
366 # Kill the tcpdump command runs on the unregistered node with "-c"
367 # option so it will be stopped automatically if it will receive
368 # packet matching filters,
369 # We don't expect any packets to be captured really in this case
370 # so let's kill tcpdump so it flushes its output to the output
371 # file.
Slawek Kaplonski8de8b992021-05-19 22:48:33 +0200372 expected_result = ('0 packets captured')
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000373 unregistered_error_message = (
374 'Unregistered server received unexpected packet(s).')
375 try:
376 unregistered['ssh_client'].execute_script(
377 "killall tcpdump && sleep 2", become_root=True)
378 except exceptions.SSHScriptFailed:
379 # Probably some packet was captured by tcpdump and due to that
380 # it is already stopped
381 self.assertTrue(check_unregistered_host(),
382 unregistered_error_message)
383 return
384
385 utils.wait_until_true(
386 check_unregistered_host,
387 exception=RuntimeError(unregistered_error_message))
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000388
Federico Ressia2aad942018-04-09 12:01:48 +0200389
390class MulticastTestIPv4(BaseMulticastTest, base.BaseTempestTestCase):
391
392 # Import configuration options
393 multicast_group_range = CONF.neutron_plugin_options.multicast_group_range
394
395 # IP version specific parameters
396 _ip_version = constants.IP_VERSION_4
397 any_addresses = constants.IPv4_ANY