blob: b2ea8ae19f86787ee22483d4d1fcf99b9c154af8 [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)
204 port = self.client.list_ports(
205 network_id=self.network['id'], device_id=server['id'])['ports'][0]
206 server['fip'] = self.create_floatingip(port=port)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200207 server['ssh_client'] = ssh.Client(server['fip']['floating_ip_address'],
208 self.username,
209 pkey=self.keypair['private_key'])
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000210 self._check_cmd_installed_on_server(server['ssh_client'],
211 server['id'], PYTHON3_BIN)
Federico Ressia2aad942018-04-09 12:01:48 +0200212 return server
213
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000214 def _check_cmd_installed_on_server(self, ssh_client, server_id, cmd):
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200215 try:
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000216 ssh_client.execute_script('which %s' % cmd)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200217 except exceptions.SSHScriptFailed:
218 raise self.skipException(
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000219 "%s is not available on server %s" % (cmd, server_id))
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200220
Federico Ressia2aad942018-04-09 12:01:48 +0200221 def _prepare_sender(self, server, mcast_address):
222 check_script = get_sender_script(
223 group=mcast_address, port=self.multicast_port,
224 message=self.multicast_message,
225 result_file=self.sender_output_file)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200226 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100227 'echo "%s" > /tmp/multicast_traffic_sender.py' % check_script)
Federico Ressia2aad942018-04-09 12:01:48 +0200228
229 def _prepare_receiver(self, server, mcast_address):
230 check_script = get_receiver_script(
231 group=mcast_address, port=self.multicast_port,
232 hello_message=self.hello_message, ack_message=server['id'],
233 result_file=self.receiver_output_file)
234 ssh_client = ssh.Client(
235 server['fip']['floating_ip_address'],
236 self.username,
237 pkey=self.keypair['private_key'])
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000238 self._check_cmd_installed_on_server(ssh_client, server['id'],
239 PYTHON3_BIN)
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200240 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100241 'echo "%s" > /tmp/multicast_traffic_receiver.py' % check_script)
Federico Ressia2aad942018-04-09 12:01:48 +0200242
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000243 def _prepare_unregistered(self, server, mcast_address):
244 check_script = get_unregistered_script(
245 group=mcast_address, result_file=self.unregistered_output_file)
246 ssh_client = ssh.Client(
247 server['fip']['floating_ip_address'],
248 self.username,
249 pkey=self.keypair['private_key'])
250 self._check_cmd_installed_on_server(ssh_client, server['id'],
251 'tcpdump')
252 server['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100253 'echo "%s" > /tmp/unregistered_traffic_receiver.sh' % check_script)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000254
Rodolfo Alonso Hernandez4dea8062020-01-16 16:32:59 +0000255 @test.unstable_test("bug 1850288")
Federico Ressia2aad942018-04-09 12:01:48 +0200256 @decorators.idempotent_id('113486fc-24c9-4be4-8361-03b1c9892867')
257 def test_multicast_between_vms_on_same_network(self):
258 """Test multicast messaging between two servers on the same network
259
260 [Sender server] -> (Multicast network) -> [Receiver server]
261 """
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100262 LOG.debug("IGMP snooping enabled: %s",
263 CONF.neutron_plugin_options.is_igmp_snooping_enabled)
Federico Ressia2aad942018-04-09 12:01:48 +0200264 sender = self._create_server()
265 receivers = [self._create_server() for _ in range(1)]
266 # Sender can be also receiver of multicast traffic
267 receivers.append(sender)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000268 unregistered = self._create_server()
269 self._check_multicast_conectivity(sender=sender, receivers=receivers,
270 unregistered=unregistered)
Federico Ressia2aad942018-04-09 12:01:48 +0200271
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000272 def _is_multicast_traffic_expected(self, mcast_address):
273 """Checks if multicast traffic is expected to arrive.
274
275 Checks if multicast traffic is expected to arrive to the
276 unregistered VM.
277
278 If IGMP snooping is enabled, multicast traffic should not be
279 flooded unless the destination IP is in the range of 224.0.0.X
280 [0].
281
282 [0] https://tools.ietf.org/html/rfc4541 (See section 2.1.2)
283 """
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100284 return (str(mcast_address).startswith('224.0.0') or not
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000285 CONF.neutron_plugin_options.is_igmp_snooping_enabled)
286
287 def _check_multicast_conectivity(self, sender, receivers, unregistered):
Federico Ressia2aad942018-04-09 12:01:48 +0200288 """Test multi-cast messaging between two servers
289
290 [Sender server] -> ... some network topology ... -> [Receiver server]
291 """
292 mcast_address = next(self.multicast_group_iter)
293 LOG.debug("Multicast group address: %s", mcast_address)
294
295 def _message_received(client, msg, file_path):
296 result = client.execute_script(
297 "cat {path} || echo '{path} not exists yet'".format(
298 path=file_path))
299 return msg in result
300
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000301 self._prepare_unregistered(unregistered, mcast_address)
302
303 # Run the unregistered node script
304 unregistered['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100305 "bash /tmp/unregistered_traffic_receiver.sh", become_root=True)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000306
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200307 self._prepare_sender(sender, mcast_address)
Federico Ressia2aad942018-04-09 12:01:48 +0200308 receiver_ids = []
309 for receiver in receivers:
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200310 self._prepare_receiver(receiver, mcast_address)
311 receiver['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100312 "%s /tmp/multicast_traffic_receiver.py &" % PYTHON3_BIN,
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200313 shell="bash")
Federico Ressia2aad942018-04-09 12:01:48 +0200314 utils.wait_until_true(
315 lambda: _message_received(
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200316 receiver['ssh_client'], self.hello_message,
Federico Ressia2aad942018-04-09 12:01:48 +0200317 self.receiver_output_file),
318 exception=RuntimeError(
319 "Receiver script didn't start properly on server "
320 "{!r}.".format(receiver['id'])))
321
Federico Ressia2aad942018-04-09 12:01:48 +0200322 receiver_ids.append(receiver['id'])
323
324 # Now lets run scripts on sender
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200325 sender['ssh_client'].execute_script(
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100326 "%s /tmp/multicast_traffic_sender.py" % PYTHON3_BIN)
Federico Ressia2aad942018-04-09 12:01:48 +0200327
328 # And check if message was received
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200329 for receiver in receivers:
Federico Ressia2aad942018-04-09 12:01:48 +0200330 utils.wait_until_true(
331 lambda: _message_received(
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200332 receiver['ssh_client'], self.multicast_message,
Federico Ressia2aad942018-04-09 12:01:48 +0200333 self.receiver_output_file),
334 exception=RuntimeError(
335 "Receiver {!r} didn't get multicast message".format(
336 receiver['id'])))
337
338 # TODO(slaweq): add validation of answears on sended server
Slawek Kaplonski2f467ca2019-09-05 16:28:09 +0200339 replies_result = sender['ssh_client'].execute_script(
Federico Ressia2aad942018-04-09 12:01:48 +0200340 "cat {path} || echo '{path} not exists yet'".format(
341 path=self.sender_output_file))
342 for receiver_id in receiver_ids:
343 self.assertIn(receiver_id, replies_result)
344
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000345 # Kill the tcpdump command running on the unregistered node so
346 # tcpdump flushes its output to the output file
347 unregistered['ssh_client'].execute_script(
348 "killall tcpdump && sleep 2", become_root=True)
349
350 unregistered_result = unregistered['ssh_client'].execute_script(
351 "cat {path} || echo '{path} not exists yet'".format(
352 path=self.unregistered_output_file))
Lucas Alvares Gomes023396f2020-04-20 13:44:48 +0100353 LOG.debug("Unregistered VM result: %s", unregistered_result)
354 expected_result = '0 packets captured'
355 if self._is_multicast_traffic_expected(mcast_address):
356 expected_result = '1 packet captured'
357 self.assertIn(expected_result, unregistered_result)
Lucas Alvares Gomesb5114e02020-02-04 13:36:46 +0000358
Federico Ressia2aad942018-04-09 12:01:48 +0200359
360class MulticastTestIPv4(BaseMulticastTest, base.BaseTempestTestCase):
361
362 # Import configuration options
363 multicast_group_range = CONF.neutron_plugin_options.multicast_group_range
364
365 # IP version specific parameters
366 _ip_version = constants.IP_VERSION_4
367 any_addresses = constants.IPv4_ANY