blob: acfb75cc5b70b8318283ac84670baedf2254cb40 [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
yatinkarelc4597e62021-11-26 14:09:18 +053019from paramiko import ssh_exception as ssh_exc
Federico Ressia2aad942018-04-09 12:01:48 +020020from tempest.lib.common.utils import data_utils
21from tempest.lib import decorators
yatinkarelc4597e62021-11-26 14:09:18 +053022from tempest.lib import exceptions as lib_exc
Federico Ressia2aad942018-04-09 12:01:48 +020023
Slawek Kaplonski6b659672021-02-02 22:15:33 +000024from neutron_tempest_plugin.common import ip
Federico Ressia2aad942018-04-09 12:01:48 +020025from neutron_tempest_plugin.common import ssh
26from neutron_tempest_plugin.common import utils
27from neutron_tempest_plugin import config
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +020028from neutron_tempest_plugin import exceptions
Federico Ressia2aad942018-04-09 12:01:48 +020029from neutron_tempest_plugin.scenario import base
30
31
32CONF = config.CONF
33LOG = log.getLogger(__name__)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +020034PYTHON3_BIN = "python3"
Federico Ressia2aad942018-04-09 12:01:48 +020035
36
37def get_receiver_script(group, port, hello_message, ack_message, result_file):
38
39 return """
40import socket
41import struct
42import sys
43
44multicast_group = '%(group)s'
45server_address = ('', %(port)s)
46
47# Create the socket
48sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
49
50# Bind to the server address
51sock.bind(server_address)
52
53# Tell the operating system to add the socket to the multicast group
54# on all interfaces.
55group = socket.inet_aton(multicast_group)
56mreq = struct.pack('4sL', group, socket.INADDR_ANY)
57sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
58
59# Receive/respond loop
60with open('%(result_file)s', 'w') as f:
61 f.write('%(hello_message)s')
62 f.flush()
63 data, address = sock.recvfrom(1024)
64 f.write('received ' + str(len(data)) + ' bytes from ' + str(address))
65 f.write(str(data))
66sock.sendto(b'%(ack_message)s', address)
67 """ % {'group': group,
68 'port': port,
69 'hello_message': hello_message,
70 'ack_message': ack_message,
71 'result_file': result_file}
72
73
74def get_sender_script(group, port, message, result_file):
75
76 return """
77import socket
78import sys
79
80message = b'%(message)s'
81multicast_group = ('%(group)s', %(port)s)
82
83# Create the datagram socket
84sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
85# Set the time-to-live for messages to 1 so they do not go past the
86# local network segment.
87sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1)
88
89# Set a timeout so the socket does not block indefinitely when trying
90# to receive data.
91sock.settimeout(1)
92
93with open('%(result_file)s', 'w') as f:
94 try:
95 # Send data to the multicast group
96 sent = sock.sendto(message, multicast_group)
97
98 # Look for responses from all recipients
99 while True:
100 try:
101 data, server = sock.recvfrom(1024)
102 except socket.timeout:
103 f.write('timed out, no more responses')
104 break
105 else:
106 f.write('received reply ' + str(data) + ' from ' + str(server))
107 finally:
108 sys.stdout.write('closing socket')
109 sock.close()
110 """ % {'group': group,
111 'port': port,
112 'message': message,
113 'result_file': result_file}
114
115
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000116def get_unregistered_script(interface, group, result_file):
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000117 return """#!/bin/bash
118export LC_ALL=en_US.UTF-8
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000119tcpdump -i %(interface)s host %(group)s -vvneA -s0 -l -c1 &> %(result_file)s &
120 """ % {'interface': interface,
121 'group': group,
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000122 'result_file': result_file}
123
124
Federico Ressia2aad942018-04-09 12:01:48 +0200125class BaseMulticastTest(object):
126
Slawek Kaplonskiedf3cba2021-04-21 10:34:02 +0200127 credentials = ['primary', 'admin']
Federico Ressia2aad942018-04-09 12:01:48 +0200128 force_tenant_isolation = False
129
130 # Import configuration options
131 available_type_drivers = (
132 CONF.neutron_plugin_options.available_type_drivers)
133
134 hello_message = "I am waiting..."
135 multicast_port = 5007
136 multicast_message = "Big Bang"
137 receiver_output_file = "/tmp/receiver_mcast_out"
138 sender_output_file = "/tmp/sender_mcast_out"
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000139 unregistered_output_file = "/tmp/unregistered_mcast_out"
Federico Ressia2aad942018-04-09 12:01:48 +0200140
141 @classmethod
142 def skip_checks(cls):
143 super(BaseMulticastTest, cls).skip_checks()
144 advanced_image_available = (
145 CONF.neutron_plugin_options.advanced_image_ref or
146 CONF.neutron_plugin_options.default_image_is_advanced)
147 if not advanced_image_available:
148 skip_reason = "This test require advanced tools for this test"
149 raise cls.skipException(skip_reason)
150
151 @classmethod
152 def resource_setup(cls):
153 super(BaseMulticastTest, cls).resource_setup()
154
155 if CONF.neutron_plugin_options.default_image_is_advanced:
156 cls.flavor_ref = CONF.compute.flavor_ref
157 cls.image_ref = CONF.compute.image_ref
158 cls.username = CONF.validation.image_ssh_user
159 else:
160 cls.flavor_ref = (
161 CONF.neutron_plugin_options.advanced_image_flavor_ref)
162 cls.image_ref = CONF.neutron_plugin_options.advanced_image_ref
163 cls.username = CONF.neutron_plugin_options.advanced_image_ssh_user
164
165 # setup basic topology for servers we can log into it
166 cls.network = cls.create_network()
167 cls.subnet = cls.create_subnet(cls.network)
168 cls.router = cls.create_router_by_client()
169 cls.create_router_interface(cls.router['id'], cls.subnet['id'])
170
171 cls.keypair = cls.create_keypair()
172
173 cls.secgroup = cls.os_primary.network_client.create_security_group(
174 name='secgroup_mtu')
175 cls.security_groups.append(cls.secgroup['security_group'])
176 cls.create_loginable_secgroup_rule(
177 secgroup_id=cls.secgroup['security_group']['id'])
178 cls.create_pingable_secgroup_rule(
179 secgroup_id=cls.secgroup['security_group']['id'])
180 # Create security group rule for UDP (multicast traffic)
181 cls.create_secgroup_rules(
182 rule_list=[dict(protocol=constants.PROTO_NAME_UDP,
183 direction=constants.INGRESS_DIRECTION,
184 remote_ip_prefix=cls.any_addresses,
185 ethertype=cls.ethertype)],
186 secgroup_id=cls.secgroup['security_group']['id'])
187
188 # Multicast IP range to be used for multicast group IP asignement
189 if '-' in cls.multicast_group_range:
190 multicast_group_range = netaddr.IPRange(
191 *cls.multicast_group_range.split('-'))
192 else:
193 multicast_group_range = netaddr.IPNetwork(
194 cls.multicast_group_range)
195 cls.multicast_group_iter = iter(multicast_group_range)
196
197 def _create_server(self):
198 name = data_utils.rand_name("multicast-server")
199 server = self.create_server(
200 flavor_ref=self.flavor_ref,
201 image_ref=self.image_ref,
202 key_name=self.keypair['name'], name=name,
203 networks=[{'uuid': self.network['id']}],
204 security_groups=[{'name': self.secgroup['security_group']['name']}]
205 )['server']
206 self.wait_for_server_active(server)
Slawek Kaplonski2211eab2020-10-20 16:43:53 +0200207 self.wait_for_guest_os_ready(server)
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000208 server['port'] = self.client.list_ports(
Federico Ressia2aad942018-04-09 12:01:48 +0200209 network_id=self.network['id'], device_id=server['id'])['ports'][0]
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000210 server['fip'] = self.create_floatingip(port=server['port'])
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200211 server['ssh_client'] = ssh.Client(server['fip']['floating_ip_address'],
212 self.username,
213 pkey=self.keypair['private_key'])
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000214 self._check_cmd_installed_on_server(server['ssh_client'],
yatinkarelc4597e62021-11-26 14:09:18 +0530215 server, PYTHON3_BIN)
Federico Ressia2aad942018-04-09 12:01:48 +0200216 return server
217
yatinkarelc4597e62021-11-26 14:09:18 +0530218 def _check_cmd_installed_on_server(self, ssh_client, server, cmd):
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200219 try:
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000220 ssh_client.execute_script('which %s' % cmd)
yatinkarelc4597e62021-11-26 14:09:18 +0530221 except (lib_exc.SSHTimeout, ssh_exc.AuthenticationException) as ssh_e:
222 LOG.debug(ssh_e)
223 self._log_console_output([server])
224 self._log_local_network_status()
225 raise
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200226 except exceptions.SSHScriptFailed:
227 raise self.skipException(
yatinkarelc4597e62021-11-26 14:09:18 +0530228 "%s is not available on server %s" % (cmd, server['id']))
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200229
Federico Ressia2aad942018-04-09 12:01:48 +0200230 def _prepare_sender(self, server, mcast_address):
231 check_script = get_sender_script(
232 group=mcast_address, port=self.multicast_port,
233 message=self.multicast_message,
234 result_file=self.sender_output_file)
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_sender.py' % check_script)
Federico Ressia2aad942018-04-09 12:01:48 +0200237
238 def _prepare_receiver(self, server, mcast_address):
239 check_script = get_receiver_script(
240 group=mcast_address, port=self.multicast_port,
241 hello_message=self.hello_message, ack_message=server['id'],
242 result_file=self.receiver_output_file)
243 ssh_client = ssh.Client(
244 server['fip']['floating_ip_address'],
245 self.username,
246 pkey=self.keypair['private_key'])
yatinkarelc4597e62021-11-26 14:09:18 +0530247 self._check_cmd_installed_on_server(ssh_client, server,
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000248 PYTHON3_BIN)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200249 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100250 'echo "%s" > /tmp/multicast_traffic_receiver.py' % check_script)
Federico Ressia2aad942018-04-09 12:01:48 +0200251
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000252 def _prepare_unregistered(self, server, mcast_address):
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000253 ssh_client = ssh.Client(
254 server['fip']['floating_ip_address'],
255 self.username,
256 pkey=self.keypair['private_key'])
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000257 ip_command = ip.IPCommand(ssh_client=ssh_client)
258 addresses = ip_command.list_addresses(port=server['port'])
259 port_iface = ip.get_port_device_name(addresses, server['port'])
260 check_script = get_unregistered_script(
261 interface=port_iface, group=mcast_address,
262 result_file=self.unregistered_output_file)
yatinkarelc4597e62021-11-26 14:09:18 +0530263 self._check_cmd_installed_on_server(ssh_client, server,
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000264 'tcpdump')
265 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100266 'echo "%s" > /tmp/unregistered_traffic_receiver.sh' % check_script)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000267
Federico Ressia2aad942018-04-09 12:01:48 +0200268 @decorators.idempotent_id('113486fc-24c9-4be4-8361-03b1c9892867')
269 def test_multicast_between_vms_on_same_network(self):
270 """Test multicast messaging between two servers on the same network
271
272 [Sender server] -> (Multicast network) -> [Receiver server]
273 """
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100274 LOG.debug("IGMP snooping enabled: %s",
275 CONF.neutron_plugin_options.is_igmp_snooping_enabled)
Federico Ressia2aad942018-04-09 12:01:48 +0200276 sender = self._create_server()
277 receivers = [self._create_server() for _ in range(1)]
278 # Sender can be also receiver of multicast traffic
279 receivers.append(sender)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000280 unregistered = self._create_server()
281 self._check_multicast_conectivity(sender=sender, receivers=receivers,
282 unregistered=unregistered)
Federico Ressia2aad942018-04-09 12:01:48 +0200283
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000284 def _is_multicast_traffic_expected(self, mcast_address):
285 """Checks if multicast traffic is expected to arrive.
286
287 Checks if multicast traffic is expected to arrive to the
288 unregistered VM.
289
290 If IGMP snooping is enabled, multicast traffic should not be
291 flooded unless the destination IP is in the range of 224.0.0.X
292 [0].
293
294 [0] https://tools.ietf.org/html/rfc4541 (See section 2.1.2)
295 """
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100296 return (str(mcast_address).startswith('224.0.0') or not
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000297 CONF.neutron_plugin_options.is_igmp_snooping_enabled)
298
299 def _check_multicast_conectivity(self, sender, receivers, unregistered):
Federico Ressia2aad942018-04-09 12:01:48 +0200300 """Test multi-cast messaging between two servers
301
302 [Sender server] -> ... some network topology ... -> [Receiver server]
303 """
304 mcast_address = next(self.multicast_group_iter)
305 LOG.debug("Multicast group address: %s", mcast_address)
306
307 def _message_received(client, msg, file_path):
308 result = client.execute_script(
309 "cat {path} || echo '{path} not exists yet'".format(
310 path=file_path))
311 return msg in result
312
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000313 self._prepare_unregistered(unregistered, mcast_address)
314
315 # Run the unregistered node script
316 unregistered['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100317 "bash /tmp/unregistered_traffic_receiver.sh", become_root=True)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000318
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200319 self._prepare_sender(sender, mcast_address)
Federico Ressia2aad942018-04-09 12:01:48 +0200320 receiver_ids = []
321 for receiver in receivers:
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200322 self._prepare_receiver(receiver, mcast_address)
323 receiver['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100324 "%s /tmp/multicast_traffic_receiver.py &" % PYTHON3_BIN,
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200325 shell="bash")
Federico Ressia2aad942018-04-09 12:01:48 +0200326 utils.wait_until_true(
327 lambda: _message_received(
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200328 receiver['ssh_client'], self.hello_message,
Federico Ressia2aad942018-04-09 12:01:48 +0200329 self.receiver_output_file),
330 exception=RuntimeError(
331 "Receiver script didn't start properly on server "
332 "{!r}.".format(receiver['id'])))
333
Federico Ressia2aad942018-04-09 12:01:48 +0200334 receiver_ids.append(receiver['id'])
335
336 # Now lets run scripts on sender
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200337 sender['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100338 "%s /tmp/multicast_traffic_sender.py" % PYTHON3_BIN)
Federico Ressia2aad942018-04-09 12:01:48 +0200339
340 # And check if message was received
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200341 for receiver in receivers:
Federico Ressia2aad942018-04-09 12:01:48 +0200342 utils.wait_until_true(
343 lambda: _message_received(
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200344 receiver['ssh_client'], self.multicast_message,
Federico Ressia2aad942018-04-09 12:01:48 +0200345 self.receiver_output_file),
346 exception=RuntimeError(
347 "Receiver {!r} didn't get multicast message".format(
348 receiver['id'])))
349
350 # TODO(slaweq): add validation of answears on sended server
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200351 replies_result = sender['ssh_client'].execute_script(
Federico Ressia2aad942018-04-09 12:01:48 +0200352 "cat {path} || echo '{path} not exists yet'".format(
353 path=self.sender_output_file))
354 for receiver_id in receiver_ids:
355 self.assertIn(receiver_id, replies_result)
356
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000357 def check_unregistered_host():
358 unregistered_result = unregistered['ssh_client'].execute_script(
359 "cat {path} || echo '{path} not exists yet'".format(
360 path=self.unregistered_output_file))
361 LOG.debug("Unregistered VM result: %s", unregistered_result)
362 return expected_result in unregistered_result
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000363
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000364 expected_result = '1 packet captured'
365 unregistered_error_message = (
366 'Unregistered server did not received expected packet.')
367 if not self._is_multicast_traffic_expected(mcast_address):
368 # Kill the tcpdump command runs on the unregistered node with "-c"
369 # option so it will be stopped automatically if it will receive
370 # packet matching filters,
371 # We don't expect any packets to be captured really in this case
372 # so let's kill tcpdump so it flushes its output to the output
373 # file.
Slawek Kaplonski8de8b992021-05-19 22:48:33 +0200374 expected_result = ('0 packets captured')
Slawek Kaplonski6b659672021-02-02 22:15:33 +0000375 unregistered_error_message = (
376 'Unregistered server received unexpected packet(s).')
377 try:
378 unregistered['ssh_client'].execute_script(
379 "killall tcpdump && sleep 2", become_root=True)
380 except exceptions.SSHScriptFailed:
381 # Probably some packet was captured by tcpdump and due to that
382 # it is already stopped
383 self.assertTrue(check_unregistered_host(),
384 unregistered_error_message)
385 return
386
387 utils.wait_until_true(
388 check_unregistered_host,
389 exception=RuntimeError(unregistered_error_message))
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000390
Federico Ressia2aad942018-04-09 12:01:48 +0200391
392class MulticastTestIPv4(BaseMulticastTest, base.BaseTempestTestCase):
393
394 # Import configuration options
395 multicast_group_range = CONF.neutron_plugin_options.multicast_group_range
396
397 # IP version specific parameters
398 _ip_version = constants.IP_VERSION_4
399 any_addresses = constants.IPv4_ANY