blob: 390e0f08f421fd104d4af3fb539386b3283368a7 [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
216 def _prepare_sender(self, server, mcast_address):
217 check_script = get_sender_script(
218 group=mcast_address, port=self.multicast_port,
219 message=self.multicast_message,
220 result_file=self.sender_output_file)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200221 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100222 'echo "%s" > /tmp/multicast_traffic_sender.py' % check_script)
Federico Ressia2aad942018-04-09 12:01:48 +0200223
224 def _prepare_receiver(self, server, mcast_address):
225 check_script = get_receiver_script(
226 group=mcast_address, port=self.multicast_port,
227 hello_message=self.hello_message, ack_message=server['id'],
228 result_file=self.receiver_output_file)
229 ssh_client = ssh.Client(
230 server['fip']['floating_ip_address'],
231 self.username,
232 pkey=self.keypair['private_key'])
yatinkarelc4597e62021-11-26 14:09:18 +0530233 self._check_cmd_installed_on_server(ssh_client, server,
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000234 PYTHON3_BIN)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200235 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100236 'echo "%s" > /tmp/multicast_traffic_receiver.py' % check_script)
Federico Ressia2aad942018-04-09 12:01:48 +0200237
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000238 def _prepare_unregistered(self, server, mcast_address):
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000239 ssh_client = ssh.Client(
240 server['fip']['floating_ip_address'],
241 self.username,
242 pkey=self.keypair['private_key'])
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000243 ip_command = ip.IPCommand(ssh_client=ssh_client)
244 addresses = ip_command.list_addresses(port=server['port'])
245 port_iface = ip.get_port_device_name(addresses, server['port'])
246 check_script = get_unregistered_script(
247 interface=port_iface, group=mcast_address,
248 result_file=self.unregistered_output_file)
yatinkarelc4597e62021-11-26 14:09:18 +0530249 self._check_cmd_installed_on_server(ssh_client, server,
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000250 'tcpdump')
251 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100252 'echo "%s" > /tmp/unregistered_traffic_receiver.sh' % check_script)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000253
Federico Ressia2aad942018-04-09 12:01:48 +0200254 @decorators.idempotent_id('113486fc-24c9-4be4-8361-03b1c9892867')
255 def test_multicast_between_vms_on_same_network(self):
256 """Test multicast messaging between two servers on the same network
257
258 [Sender server] -> (Multicast network) -> [Receiver server]
259 """
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100260 LOG.debug("IGMP snooping enabled: %s",
261 CONF.neutron_plugin_options.is_igmp_snooping_enabled)
Federico Ressia2aad942018-04-09 12:01:48 +0200262 sender = self._create_server()
263 receivers = [self._create_server() for _ in range(1)]
264 # Sender can be also receiver of multicast traffic
265 receivers.append(sender)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000266 unregistered = self._create_server()
267 self._check_multicast_conectivity(sender=sender, receivers=receivers,
268 unregistered=unregistered)
Federico Ressia2aad942018-04-09 12:01:48 +0200269
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000270 def _is_multicast_traffic_expected(self, mcast_address):
271 """Checks if multicast traffic is expected to arrive.
272
273 Checks if multicast traffic is expected to arrive to the
274 unregistered VM.
275
276 If IGMP snooping is enabled, multicast traffic should not be
277 flooded unless the destination IP is in the range of 224.0.0.X
278 [0].
279
280 [0] https://tools.ietf.org/html/rfc4541 (See section 2.1.2)
281 """
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100282 return (str(mcast_address).startswith('224.0.0') or not
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000283 CONF.neutron_plugin_options.is_igmp_snooping_enabled)
284
285 def _check_multicast_conectivity(self, sender, receivers, unregistered):
Federico Ressia2aad942018-04-09 12:01:48 +0200286 """Test multi-cast messaging between two servers
287
288 [Sender server] -> ... some network topology ... -> [Receiver server]
289 """
290 mcast_address = next(self.multicast_group_iter)
291 LOG.debug("Multicast group address: %s", mcast_address)
292
293 def _message_received(client, msg, file_path):
294 result = client.execute_script(
295 "cat {path} || echo '{path} not exists yet'".format(
296 path=file_path))
297 return msg in result
298
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000299 self._prepare_unregistered(unregistered, mcast_address)
300
301 # Run the unregistered node script
302 unregistered['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100303 "bash /tmp/unregistered_traffic_receiver.sh", become_root=True)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000304
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200305 self._prepare_sender(sender, mcast_address)
Federico Ressia2aad942018-04-09 12:01:48 +0200306 receiver_ids = []
307 for receiver in receivers:
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200308 self._prepare_receiver(receiver, mcast_address)
309 receiver['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100310 "%s /tmp/multicast_traffic_receiver.py &" % PYTHON3_BIN,
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200311 shell="bash")
Federico Ressia2aad942018-04-09 12:01:48 +0200312 utils.wait_until_true(
313 lambda: _message_received(
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200314 receiver['ssh_client'], self.hello_message,
Federico Ressia2aad942018-04-09 12:01:48 +0200315 self.receiver_output_file),
316 exception=RuntimeError(
317 "Receiver script didn't start properly on server "
318 "{!r}.".format(receiver['id'])))
319
Federico Ressia2aad942018-04-09 12:01:48 +0200320 receiver_ids.append(receiver['id'])
321
322 # Now lets run scripts on sender
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200323 sender['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100324 "%s /tmp/multicast_traffic_sender.py" % PYTHON3_BIN)
Federico Ressia2aad942018-04-09 12:01:48 +0200325
326 # And check if message was received
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200327 for receiver in receivers:
Federico Ressia2aad942018-04-09 12:01:48 +0200328 utils.wait_until_true(
329 lambda: _message_received(
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200330 receiver['ssh_client'], self.multicast_message,
Federico Ressia2aad942018-04-09 12:01:48 +0200331 self.receiver_output_file),
332 exception=RuntimeError(
333 "Receiver {!r} didn't get multicast message".format(
334 receiver['id'])))
335
336 # TODO(slaweq): add validation of answears on sended server
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200337 replies_result = sender['ssh_client'].execute_script(
Federico Ressia2aad942018-04-09 12:01:48 +0200338 "cat {path} || echo '{path} not exists yet'".format(
339 path=self.sender_output_file))
340 for receiver_id in receiver_ids:
341 self.assertIn(receiver_id, replies_result)
342
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