blob: 79f835eec180b38d3bfdfa257dd79c1e1752d6c7 [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
Rodolfo Alonso Hernandez4dea8062020-01-16 16:32:59 +000018from neutron_lib.utils import test
Federico Ressia2aad942018-04-09 12:01:48 +020019from oslo_log import log
20from tempest.lib.common.utils import data_utils
21from tempest.lib import decorators
22
23from 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
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000114def get_unregistered_script(group, result_file):
115 return """#!/bin/bash
116export LC_ALL=en_US.UTF-8
117tcpdump -i any -s0 -vv host %(group)s -vvneA -s0 -l &> %(result_file)s &
118 """ % {'group': group,
119 'result_file': result_file}
120
121
Federico Ressia2aad942018-04-09 12:01:48 +0200122class BaseMulticastTest(object):
123
124 credentials = ['primary']
125 force_tenant_isolation = False
126
127 # Import configuration options
128 available_type_drivers = (
129 CONF.neutron_plugin_options.available_type_drivers)
130
131 hello_message = "I am waiting..."
132 multicast_port = 5007
133 multicast_message = "Big Bang"
134 receiver_output_file = "/tmp/receiver_mcast_out"
135 sender_output_file = "/tmp/sender_mcast_out"
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000136 unregistered_output_file = "/tmp/unregistered_mcast_out"
Federico Ressia2aad942018-04-09 12:01:48 +0200137
138 @classmethod
139 def skip_checks(cls):
140 super(BaseMulticastTest, cls).skip_checks()
141 advanced_image_available = (
142 CONF.neutron_plugin_options.advanced_image_ref or
143 CONF.neutron_plugin_options.default_image_is_advanced)
144 if not advanced_image_available:
145 skip_reason = "This test require advanced tools for this test"
146 raise cls.skipException(skip_reason)
147
148 @classmethod
149 def resource_setup(cls):
150 super(BaseMulticastTest, cls).resource_setup()
151
152 if CONF.neutron_plugin_options.default_image_is_advanced:
153 cls.flavor_ref = CONF.compute.flavor_ref
154 cls.image_ref = CONF.compute.image_ref
155 cls.username = CONF.validation.image_ssh_user
156 else:
157 cls.flavor_ref = (
158 CONF.neutron_plugin_options.advanced_image_flavor_ref)
159 cls.image_ref = CONF.neutron_plugin_options.advanced_image_ref
160 cls.username = CONF.neutron_plugin_options.advanced_image_ssh_user
161
162 # setup basic topology for servers we can log into it
163 cls.network = cls.create_network()
164 cls.subnet = cls.create_subnet(cls.network)
165 cls.router = cls.create_router_by_client()
166 cls.create_router_interface(cls.router['id'], cls.subnet['id'])
167
168 cls.keypair = cls.create_keypair()
169
170 cls.secgroup = cls.os_primary.network_client.create_security_group(
171 name='secgroup_mtu')
172 cls.security_groups.append(cls.secgroup['security_group'])
173 cls.create_loginable_secgroup_rule(
174 secgroup_id=cls.secgroup['security_group']['id'])
175 cls.create_pingable_secgroup_rule(
176 secgroup_id=cls.secgroup['security_group']['id'])
177 # Create security group rule for UDP (multicast traffic)
178 cls.create_secgroup_rules(
179 rule_list=[dict(protocol=constants.PROTO_NAME_UDP,
180 direction=constants.INGRESS_DIRECTION,
181 remote_ip_prefix=cls.any_addresses,
182 ethertype=cls.ethertype)],
183 secgroup_id=cls.secgroup['security_group']['id'])
184
185 # Multicast IP range to be used for multicast group IP asignement
186 if '-' in cls.multicast_group_range:
187 multicast_group_range = netaddr.IPRange(
188 *cls.multicast_group_range.split('-'))
189 else:
190 multicast_group_range = netaddr.IPNetwork(
191 cls.multicast_group_range)
192 cls.multicast_group_iter = iter(multicast_group_range)
193
194 def _create_server(self):
195 name = data_utils.rand_name("multicast-server")
196 server = self.create_server(
197 flavor_ref=self.flavor_ref,
198 image_ref=self.image_ref,
199 key_name=self.keypair['name'], name=name,
200 networks=[{'uuid': self.network['id']}],
201 security_groups=[{'name': self.secgroup['security_group']['name']}]
202 )['server']
203 self.wait_for_server_active(server)
Slawek Kaplonski2211eab2020-10-20 16:43:53 +0200204 self.wait_for_guest_os_ready(server)
Federico Ressia2aad942018-04-09 12:01:48 +0200205 port = self.client.list_ports(
206 network_id=self.network['id'], device_id=server['id'])['ports'][0]
207 server['fip'] = self.create_floatingip(port=port)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200208 server['ssh_client'] = ssh.Client(server['fip']['floating_ip_address'],
209 self.username,
210 pkey=self.keypair['private_key'])
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000211 self._check_cmd_installed_on_server(server['ssh_client'],
212 server['id'], PYTHON3_BIN)
Federico Ressia2aad942018-04-09 12:01:48 +0200213 return server
214
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000215 def _check_cmd_installed_on_server(self, ssh_client, server_id, cmd):
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200216 try:
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000217 ssh_client.execute_script('which %s' % cmd)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200218 except exceptions.SSHScriptFailed:
219 raise self.skipException(
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000220 "%s is not available on server %s" % (cmd, server_id))
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200221
Federico Ressia2aad942018-04-09 12:01:48 +0200222 def _prepare_sender(self, server, mcast_address):
223 check_script = get_sender_script(
224 group=mcast_address, port=self.multicast_port,
225 message=self.multicast_message,
226 result_file=self.sender_output_file)
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_sender.py' % check_script)
Federico Ressia2aad942018-04-09 12:01:48 +0200229
230 def _prepare_receiver(self, server, mcast_address):
231 check_script = get_receiver_script(
232 group=mcast_address, port=self.multicast_port,
233 hello_message=self.hello_message, ack_message=server['id'],
234 result_file=self.receiver_output_file)
235 ssh_client = ssh.Client(
236 server['fip']['floating_ip_address'],
237 self.username,
238 pkey=self.keypair['private_key'])
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000239 self._check_cmd_installed_on_server(ssh_client, server['id'],
240 PYTHON3_BIN)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200241 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100242 'echo "%s" > /tmp/multicast_traffic_receiver.py' % check_script)
Federico Ressia2aad942018-04-09 12:01:48 +0200243
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000244 def _prepare_unregistered(self, server, mcast_address):
245 check_script = get_unregistered_script(
246 group=mcast_address, result_file=self.unregistered_output_file)
247 ssh_client = ssh.Client(
248 server['fip']['floating_ip_address'],
249 self.username,
250 pkey=self.keypair['private_key'])
251 self._check_cmd_installed_on_server(ssh_client, server['id'],
252 'tcpdump')
253 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100254 'echo "%s" > /tmp/unregistered_traffic_receiver.sh' % check_script)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000255
Rodolfo Alonso Hernandez4dea8062020-01-16 16:32:59 +0000256 @test.unstable_test("bug 1850288")
Federico Ressia2aad942018-04-09 12:01:48 +0200257 @decorators.idempotent_id('113486fc-24c9-4be4-8361-03b1c9892867')
258 def test_multicast_between_vms_on_same_network(self):
259 """Test multicast messaging between two servers on the same network
260
261 [Sender server] -> (Multicast network) -> [Receiver server]
262 """
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100263 LOG.debug("IGMP snooping enabled: %s",
264 CONF.neutron_plugin_options.is_igmp_snooping_enabled)
Federico Ressia2aad942018-04-09 12:01:48 +0200265 sender = self._create_server()
266 receivers = [self._create_server() for _ in range(1)]
267 # Sender can be also receiver of multicast traffic
268 receivers.append(sender)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000269 unregistered = self._create_server()
270 self._check_multicast_conectivity(sender=sender, receivers=receivers,
271 unregistered=unregistered)
Federico Ressia2aad942018-04-09 12:01:48 +0200272
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000273 def _is_multicast_traffic_expected(self, mcast_address):
274 """Checks if multicast traffic is expected to arrive.
275
276 Checks if multicast traffic is expected to arrive to the
277 unregistered VM.
278
279 If IGMP snooping is enabled, multicast traffic should not be
280 flooded unless the destination IP is in the range of 224.0.0.X
281 [0].
282
283 [0] https://tools.ietf.org/html/rfc4541 (See section 2.1.2)
284 """
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100285 return (str(mcast_address).startswith('224.0.0') or not
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000286 CONF.neutron_plugin_options.is_igmp_snooping_enabled)
287
288 def _check_multicast_conectivity(self, sender, receivers, unregistered):
Federico Ressia2aad942018-04-09 12:01:48 +0200289 """Test multi-cast messaging between two servers
290
291 [Sender server] -> ... some network topology ... -> [Receiver server]
292 """
293 mcast_address = next(self.multicast_group_iter)
294 LOG.debug("Multicast group address: %s", mcast_address)
295
296 def _message_received(client, msg, file_path):
297 result = client.execute_script(
298 "cat {path} || echo '{path} not exists yet'".format(
299 path=file_path))
300 return msg in result
301
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000302 self._prepare_unregistered(unregistered, mcast_address)
303
304 # Run the unregistered node script
305 unregistered['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100306 "bash /tmp/unregistered_traffic_receiver.sh", become_root=True)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000307
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200308 self._prepare_sender(sender, mcast_address)
Federico Ressia2aad942018-04-09 12:01:48 +0200309 receiver_ids = []
310 for receiver in receivers:
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200311 self._prepare_receiver(receiver, mcast_address)
312 receiver['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100313 "%s /tmp/multicast_traffic_receiver.py &" % PYTHON3_BIN,
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200314 shell="bash")
Federico Ressia2aad942018-04-09 12:01:48 +0200315 utils.wait_until_true(
316 lambda: _message_received(
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200317 receiver['ssh_client'], self.hello_message,
Federico Ressia2aad942018-04-09 12:01:48 +0200318 self.receiver_output_file),
319 exception=RuntimeError(
320 "Receiver script didn't start properly on server "
321 "{!r}.".format(receiver['id'])))
322
Federico Ressia2aad942018-04-09 12:01:48 +0200323 receiver_ids.append(receiver['id'])
324
325 # Now lets run scripts on sender
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200326 sender['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100327 "%s /tmp/multicast_traffic_sender.py" % PYTHON3_BIN)
Federico Ressia2aad942018-04-09 12:01:48 +0200328
329 # And check if message was received
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200330 for receiver in receivers:
Federico Ressia2aad942018-04-09 12:01:48 +0200331 utils.wait_until_true(
332 lambda: _message_received(
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200333 receiver['ssh_client'], self.multicast_message,
Federico Ressia2aad942018-04-09 12:01:48 +0200334 self.receiver_output_file),
335 exception=RuntimeError(
336 "Receiver {!r} didn't get multicast message".format(
337 receiver['id'])))
338
339 # TODO(slaweq): add validation of answears on sended server
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200340 replies_result = sender['ssh_client'].execute_script(
Federico Ressia2aad942018-04-09 12:01:48 +0200341 "cat {path} || echo '{path} not exists yet'".format(
342 path=self.sender_output_file))
343 for receiver_id in receiver_ids:
344 self.assertIn(receiver_id, replies_result)
345
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000346 # Kill the tcpdump command running on the unregistered node so
347 # tcpdump flushes its output to the output file
348 unregistered['ssh_client'].execute_script(
349 "killall tcpdump && sleep 2", become_root=True)
350
351 unregistered_result = unregistered['ssh_client'].execute_script(
352 "cat {path} || echo '{path} not exists yet'".format(
353 path=self.unregistered_output_file))
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100354 LOG.debug("Unregistered VM result: %s", unregistered_result)
355 expected_result = '0 packets captured'
356 if self._is_multicast_traffic_expected(mcast_address):
357 expected_result = '1 packet captured'
358 self.assertIn(expected_result, unregistered_result)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000359
Federico Ressia2aad942018-04-09 12:01:48 +0200360
361class MulticastTestIPv4(BaseMulticastTest, base.BaseTempestTestCase):
362
363 # Import configuration options
364 multicast_group_range = CONF.neutron_plugin_options.multicast_group_range
365
366 # IP version specific parameters
367 _ip_version = constants.IP_VERSION_4
368 any_addresses = constants.IPv4_ANY