blob: 6eb298642da6f972e7e5229785168b0c328ee63d [file] [log] [blame]
Daniel Mellado3c0aeab2016-01-29 11:30:25 +00001# Copyright 2012 OpenStack Foundation
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
Ihar Hrachyshka59382252016-04-05 15:54:33 +020016import functools
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +020017import math
Ihar Hrachyshka59382252016-04-05 15:54:33 +020018
Daniel Mellado3c0aeab2016-01-29 11:30:25 +000019import netaddr
20from tempest.lib.common.utils import data_utils
21from tempest.lib import exceptions as lib_exc
22from tempest import test
23
Ihar Hrachyshka59382252016-04-05 15:54:33 +020024from neutron.common import constants
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +020025from neutron.common import utils
Daniel Mellado3c0aeab2016-01-29 11:30:25 +000026from neutron.tests.tempest.api import clients
27from neutron.tests.tempest import config
28from neutron.tests.tempest import exceptions
29
30CONF = config.CONF
31
32
33class BaseNetworkTest(test.BaseTestCase):
34
35 """
36 Base class for the Neutron tests that use the Tempest Neutron REST client
37
38 Per the Neutron API Guide, API v1.x was removed from the source code tree
39 (docs.openstack.org/api/openstack-network/2.0/content/Overview-d1e71.html)
40 Therefore, v2.x of the Neutron API is assumed. It is also assumed that the
41 following options are defined in the [network] section of etc/tempest.conf:
42
43 project_network_cidr with a block of cidr's from which smaller blocks
44 can be allocated for tenant networks
45
46 project_network_mask_bits with the mask bits to be used to partition
47 the block defined by tenant-network_cidr
48
49 Finally, it is assumed that the following option is defined in the
50 [service_available] section of etc/tempest.conf
51
52 neutron as True
53 """
54
55 force_tenant_isolation = False
56 credentials = ['primary']
57
58 # Default to ipv4.
59 _ip_version = 4
60
61 @classmethod
62 def get_client_manager(cls, credential_type=None, roles=None,
63 force_new=None):
Genadi Chereshnyacc395c02016-07-25 12:17:37 +030064 manager = super(BaseNetworkTest, cls).get_client_manager(
65 credential_type=credential_type,
66 roles=roles,
67 force_new=force_new
68 )
Daniel Mellado3c0aeab2016-01-29 11:30:25 +000069 # Neutron uses a different clients manager than the one in the Tempest
70 return clients.Manager(manager.credentials)
71
72 @classmethod
73 def skip_checks(cls):
74 super(BaseNetworkTest, cls).skip_checks()
75 if not CONF.service_available.neutron:
76 raise cls.skipException("Neutron support is required")
77 if cls._ip_version == 6 and not CONF.network_feature_enabled.ipv6:
78 raise cls.skipException("IPv6 Tests are disabled.")
Jakub Libosvar1982aa12017-05-30 11:15:33 +000079 for req_ext in getattr(cls, 'required_extensions', []):
80 if not test.is_extension_enabled(req_ext, 'network'):
81 msg = "%s extension not enabled." % req_ext
82 raise cls.skipException(msg)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +000083
84 @classmethod
85 def setup_credentials(cls):
86 # Create no network resources for these test.
87 cls.set_network_resources()
88 super(BaseNetworkTest, cls).setup_credentials()
89
90 @classmethod
91 def setup_clients(cls):
92 super(BaseNetworkTest, cls).setup_clients()
fumihiko kakumaa216fc12017-07-14 10:43:29 +090093 cls.client = cls.os_primary.network_client
Daniel Mellado3c0aeab2016-01-29 11:30:25 +000094
95 @classmethod
96 def resource_setup(cls):
97 super(BaseNetworkTest, cls).resource_setup()
98
99 cls.networks = []
Miguel Lavalle124378b2016-09-21 16:41:47 -0500100 cls.admin_networks = []
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000101 cls.subnets = []
Kevin Bentonba3651c2017-09-01 17:13:01 -0700102 cls.admin_subnets = []
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000103 cls.ports = []
104 cls.routers = []
105 cls.floating_ips = []
106 cls.metering_labels = []
107 cls.service_profiles = []
108 cls.flavors = []
109 cls.metering_label_rules = []
110 cls.qos_rules = []
111 cls.qos_policies = []
112 cls.ethertype = "IPv" + str(cls._ip_version)
113 cls.address_scopes = []
114 cls.admin_address_scopes = []
115 cls.subnetpools = []
116 cls.admin_subnetpools = []
Itzik Brownbac51dc2016-10-31 12:25:04 +0000117 cls.security_groups = []
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000118
119 @classmethod
120 def resource_cleanup(cls):
121 if CONF.service_available.neutron:
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000122 # Clean up floating IPs
123 for floating_ip in cls.floating_ips:
124 cls._try_delete_resource(cls.client.delete_floatingip,
125 floating_ip['id'])
126 # Clean up routers
127 for router in cls.routers:
128 cls._try_delete_resource(cls.delete_router,
129 router)
130 # Clean up metering label rules
131 for metering_label_rule in cls.metering_label_rules:
132 cls._try_delete_resource(
133 cls.admin_client.delete_metering_label_rule,
134 metering_label_rule['id'])
135 # Clean up metering labels
136 for metering_label in cls.metering_labels:
137 cls._try_delete_resource(
138 cls.admin_client.delete_metering_label,
139 metering_label['id'])
140 # Clean up flavors
141 for flavor in cls.flavors:
142 cls._try_delete_resource(
143 cls.admin_client.delete_flavor,
144 flavor['id'])
145 # Clean up service profiles
146 for service_profile in cls.service_profiles:
147 cls._try_delete_resource(
148 cls.admin_client.delete_service_profile,
149 service_profile['id'])
150 # Clean up ports
151 for port in cls.ports:
152 cls._try_delete_resource(cls.client.delete_port,
153 port['id'])
154 # Clean up subnets
155 for subnet in cls.subnets:
156 cls._try_delete_resource(cls.client.delete_subnet,
157 subnet['id'])
Kevin Bentonba3651c2017-09-01 17:13:01 -0700158 # Clean up admin subnets
159 for subnet in cls.admin_subnets:
160 cls._try_delete_resource(cls.admin_client.delete_subnet,
161 subnet['id'])
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000162 # Clean up networks
163 for network in cls.networks:
164 cls._try_delete_resource(cls.client.delete_network,
165 network['id'])
166
Miguel Lavalle124378b2016-09-21 16:41:47 -0500167 # Clean up admin networks
168 for network in cls.admin_networks:
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000169 cls._try_delete_resource(cls.admin_client.delete_network,
170 network['id'])
171
Itzik Brownbac51dc2016-10-31 12:25:04 +0000172 # Clean up security groups
173 for secgroup in cls.security_groups:
174 cls._try_delete_resource(cls.client.delete_security_group,
175 secgroup['id'])
176
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000177 for subnetpool in cls.subnetpools:
178 cls._try_delete_resource(cls.client.delete_subnetpool,
179 subnetpool['id'])
180
181 for subnetpool in cls.admin_subnetpools:
182 cls._try_delete_resource(cls.admin_client.delete_subnetpool,
183 subnetpool['id'])
184
185 for address_scope in cls.address_scopes:
186 cls._try_delete_resource(cls.client.delete_address_scope,
187 address_scope['id'])
188
189 for address_scope in cls.admin_address_scopes:
190 cls._try_delete_resource(
191 cls.admin_client.delete_address_scope,
192 address_scope['id'])
193
Sławek Kapłońskie100c4d2017-08-23 21:18:34 +0000194 # Clean up QoS rules
195 for qos_rule in cls.qos_rules:
196 cls._try_delete_resource(cls.admin_client.delete_qos_rule,
197 qos_rule['id'])
198 # Clean up QoS policies
199 # as all networks and ports are already removed, QoS policies
200 # shouldn't be "in use"
201 for qos_policy in cls.qos_policies:
202 cls._try_delete_resource(cls.admin_client.delete_qos_policy,
203 qos_policy['id'])
204
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000205 super(BaseNetworkTest, cls).resource_cleanup()
206
207 @classmethod
208 def _try_delete_resource(cls, delete_callable, *args, **kwargs):
209 """Cleanup resources in case of test-failure
210
211 Some resources are explicitly deleted by the test.
212 If the test failed to delete a resource, this method will execute
213 the appropriate delete methods. Otherwise, the method ignores NotFound
214 exceptions thrown for resources that were correctly deleted by the
215 test.
216
217 :param delete_callable: delete method
218 :param args: arguments for delete method
219 :param kwargs: keyword arguments for delete method
220 """
221 try:
222 delete_callable(*args, **kwargs)
223 # if resource is not found, this means it was deleted in the test
224 except lib_exc.NotFound:
225 pass
226
227 @classmethod
Sergey Belousa627ed92016-10-07 14:29:07 +0300228 def create_network(cls, network_name=None, client=None, **kwargs):
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000229 """Wrapper utility that returns a test network."""
230 network_name = network_name or data_utils.rand_name('test-network-')
231
Sergey Belousa627ed92016-10-07 14:29:07 +0300232 client = client or cls.client
233 body = client.create_network(name=network_name, **kwargs)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000234 network = body['network']
Sławek Kapłońskia694a5f2017-08-24 19:51:22 +0000235 if client is cls.client:
236 cls.networks.append(network)
237 else:
238 cls.admin_networks.append(network)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000239 return network
240
241 @classmethod
242 def create_shared_network(cls, network_name=None, **post_body):
243 network_name = network_name or data_utils.rand_name('sharednetwork-')
244 post_body.update({'name': network_name, 'shared': True})
245 body = cls.admin_client.create_network(**post_body)
246 network = body['network']
Miguel Lavalle124378b2016-09-21 16:41:47 -0500247 cls.admin_networks.append(network)
248 return network
249
250 @classmethod
251 def create_network_keystone_v3(cls, network_name=None, project_id=None,
252 tenant_id=None, client=None):
253 """Wrapper utility that creates a test network with project_id."""
254 client = client or cls.client
255 network_name = network_name or data_utils.rand_name(
256 'test-network-with-project_id')
257 project_id = cls.client.tenant_id
258 body = client.create_network_keystone_v3(network_name, project_id,
259 tenant_id)
260 network = body['network']
261 if client is cls.client:
262 cls.networks.append(network)
263 else:
264 cls.admin_networks.append(network)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000265 return network
266
267 @classmethod
268 def create_subnet(cls, network, gateway='', cidr=None, mask_bits=None,
269 ip_version=None, client=None, **kwargs):
270 """Wrapper utility that returns a test subnet."""
271
272 # allow tests to use admin client
273 if not client:
274 client = cls.client
275
276 # The cidr and mask_bits depend on the ip version.
277 ip_version = ip_version if ip_version is not None else cls._ip_version
278 gateway_not_set = gateway == ''
279 if ip_version == 4:
280 cidr = cidr or netaddr.IPNetwork(
281 config.safe_get_config_value(
282 'network', 'project_network_cidr'))
283 mask_bits = (
284 mask_bits or config.safe_get_config_value(
285 'network', 'project_network_mask_bits'))
286 elif ip_version == 6:
287 cidr = (
288 cidr or netaddr.IPNetwork(
289 config.safe_get_config_value(
290 'network', 'project_network_v6_cidr')))
291 mask_bits = (
292 mask_bits or config.safe_get_config_value(
293 'network', 'project_network_v6_mask_bits'))
294 # Find a cidr that is not in use yet and create a subnet with it
295 for subnet_cidr in cidr.subnet(mask_bits):
296 if gateway_not_set:
297 gateway_ip = str(netaddr.IPAddress(subnet_cidr) + 1)
298 else:
299 gateway_ip = gateway
300 try:
301 body = client.create_subnet(
302 network_id=network['id'],
303 cidr=str(subnet_cidr),
304 ip_version=ip_version,
305 gateway_ip=gateway_ip,
306 **kwargs)
307 break
308 except lib_exc.BadRequest as e:
309 is_overlapping_cidr = 'overlaps with another subnet' in str(e)
310 if not is_overlapping_cidr:
311 raise
312 else:
313 message = 'Available CIDR for subnet creation could not be found'
314 raise ValueError(message)
315 subnet = body['subnet']
Kevin Bentonba3651c2017-09-01 17:13:01 -0700316 if client is cls.client:
317 cls.subnets.append(subnet)
318 else:
319 cls.admin_subnets.append(subnet)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000320 return subnet
321
322 @classmethod
323 def create_port(cls, network, **kwargs):
324 """Wrapper utility that returns a test port."""
325 body = cls.client.create_port(network_id=network['id'],
326 **kwargs)
327 port = body['port']
328 cls.ports.append(port)
329 return port
330
331 @classmethod
332 def update_port(cls, port, **kwargs):
333 """Wrapper utility that updates a test port."""
334 body = cls.client.update_port(port['id'],
335 **kwargs)
336 return body['port']
337
338 @classmethod
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300339 def _create_router_with_client(
340 cls, client, router_name=None, admin_state_up=False,
341 external_network_id=None, enable_snat=None, **kwargs
342 ):
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000343 ext_gw_info = {}
344 if external_network_id:
345 ext_gw_info['network_id'] = external_network_id
YAMAMOTO Takashi9bd4f972017-06-20 12:49:30 +0900346 if enable_snat is not None:
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000347 ext_gw_info['enable_snat'] = enable_snat
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300348 body = client.create_router(
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000349 router_name, external_gateway_info=ext_gw_info,
350 admin_state_up=admin_state_up, **kwargs)
351 router = body['router']
352 cls.routers.append(router)
353 return router
354
355 @classmethod
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300356 def create_router(cls, *args, **kwargs):
357 return cls._create_router_with_client(cls.client, *args, **kwargs)
358
359 @classmethod
360 def create_admin_router(cls, *args, **kwargs):
rajat294495c042017-06-28 15:37:16 +0530361 return cls._create_router_with_client(cls.os_admin.network_client,
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300362 *args, **kwargs)
363
364 @classmethod
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000365 def create_floatingip(cls, external_network_id):
366 """Wrapper utility that returns a test floating IP."""
367 body = cls.client.create_floatingip(
368 floating_network_id=external_network_id)
369 fip = body['floatingip']
370 cls.floating_ips.append(fip)
371 return fip
372
373 @classmethod
374 def create_router_interface(cls, router_id, subnet_id):
375 """Wrapper utility that returns a router interface."""
376 interface = cls.client.add_router_interface_with_subnet_id(
377 router_id, subnet_id)
378 return interface
379
380 @classmethod
Sławek Kapłońskiff294062016-12-04 15:00:54 +0000381 def get_supported_qos_rule_types(cls):
382 body = cls.client.list_qos_rule_types()
383 return [rule_type['type'] for rule_type in body['rule_types']]
384
385 @classmethod
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200386 def create_qos_policy(cls, name, description=None, shared=False,
Hirofumi Ichihara39a6ee12017-08-23 13:55:12 +0900387 tenant_id=None, is_default=False):
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000388 """Wrapper utility that returns a test QoS policy."""
389 body = cls.admin_client.create_qos_policy(
Hirofumi Ichihara39a6ee12017-08-23 13:55:12 +0900390 name, description, shared, tenant_id, is_default)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000391 qos_policy = body['policy']
392 cls.qos_policies.append(qos_policy)
393 return qos_policy
394
395 @classmethod
Sławek Kapłoński153f3452017-03-24 22:04:53 +0000396 def create_qos_bandwidth_limit_rule(cls, policy_id, max_kbps,
397 max_burst_kbps,
398 direction=constants.EGRESS_DIRECTION):
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000399 """Wrapper utility that returns a test QoS bandwidth limit rule."""
400 body = cls.admin_client.create_bandwidth_limit_rule(
Sławek Kapłoński153f3452017-03-24 22:04:53 +0000401 policy_id, max_kbps, max_burst_kbps, direction)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000402 qos_rule = body['bandwidth_limit_rule']
403 cls.qos_rules.append(qos_rule)
404 return qos_rule
405
406 @classmethod
407 def delete_router(cls, router):
408 body = cls.client.list_router_interfaces(router['id'])
409 interfaces = body['ports']
410 for i in interfaces:
411 try:
412 cls.client.remove_router_interface_with_subnet_id(
413 router['id'], i['fixed_ips'][0]['subnet_id'])
414 except lib_exc.NotFound:
415 pass
416 cls.client.delete_router(router['id'])
417
418 @classmethod
419 def create_address_scope(cls, name, is_admin=False, **kwargs):
420 if is_admin:
421 body = cls.admin_client.create_address_scope(name=name, **kwargs)
422 cls.admin_address_scopes.append(body['address_scope'])
423 else:
424 body = cls.client.create_address_scope(name=name, **kwargs)
425 cls.address_scopes.append(body['address_scope'])
426 return body['address_scope']
427
428 @classmethod
429 def create_subnetpool(cls, name, is_admin=False, **kwargs):
430 if is_admin:
431 body = cls.admin_client.create_subnetpool(name, **kwargs)
432 cls.admin_subnetpools.append(body['subnetpool'])
433 else:
434 body = cls.client.create_subnetpool(name, **kwargs)
435 cls.subnetpools.append(body['subnetpool'])
436 return body['subnetpool']
437
438
439class BaseAdminNetworkTest(BaseNetworkTest):
440
441 credentials = ['primary', 'admin']
442
443 @classmethod
444 def setup_clients(cls):
445 super(BaseAdminNetworkTest, cls).setup_clients()
fumihiko kakumaa216fc12017-07-14 10:43:29 +0900446 cls.admin_client = cls.os_admin.network_client
Jakub Libosvarf5758012017-08-15 13:45:30 +0000447 cls.identity_admin_client = cls.os_admin.projects_client
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000448
449 @classmethod
450 def create_metering_label(cls, name, description):
451 """Wrapper utility that returns a test metering label."""
452 body = cls.admin_client.create_metering_label(
453 description=description,
454 name=data_utils.rand_name("metering-label"))
455 metering_label = body['metering_label']
456 cls.metering_labels.append(metering_label)
457 return metering_label
458
459 @classmethod
460 def create_metering_label_rule(cls, remote_ip_prefix, direction,
461 metering_label_id):
462 """Wrapper utility that returns a test metering label rule."""
463 body = cls.admin_client.create_metering_label_rule(
464 remote_ip_prefix=remote_ip_prefix, direction=direction,
465 metering_label_id=metering_label_id)
466 metering_label_rule = body['metering_label_rule']
467 cls.metering_label_rules.append(metering_label_rule)
468 return metering_label_rule
469
470 @classmethod
471 def create_flavor(cls, name, description, service_type):
472 """Wrapper utility that returns a test flavor."""
473 body = cls.admin_client.create_flavor(
474 description=description, service_type=service_type,
475 name=name)
476 flavor = body['flavor']
477 cls.flavors.append(flavor)
478 return flavor
479
480 @classmethod
481 def create_service_profile(cls, description, metainfo, driver):
482 """Wrapper utility that returns a test service profile."""
483 body = cls.admin_client.create_service_profile(
484 driver=driver, metainfo=metainfo, description=description)
485 service_profile = body['service_profile']
486 cls.service_profiles.append(service_profile)
487 return service_profile
488
489 @classmethod
490 def get_unused_ip(cls, net_id, ip_version=None):
Gary Kotton011345f2016-06-15 08:04:31 -0700491 """Get an unused ip address in a allocation pool of net"""
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000492 body = cls.admin_client.list_ports(network_id=net_id)
493 ports = body['ports']
494 used_ips = []
495 for port in ports:
496 used_ips.extend(
497 [fixed_ip['ip_address'] for fixed_ip in port['fixed_ips']])
498 body = cls.admin_client.list_subnets(network_id=net_id)
499 subnets = body['subnets']
500
501 for subnet in subnets:
502 if ip_version and subnet['ip_version'] != ip_version:
503 continue
504 cidr = subnet['cidr']
505 allocation_pools = subnet['allocation_pools']
506 iterators = []
507 if allocation_pools:
508 for allocation_pool in allocation_pools:
509 iterators.append(netaddr.iter_iprange(
510 allocation_pool['start'], allocation_pool['end']))
511 else:
512 net = netaddr.IPNetwork(cidr)
513
514 def _iterip():
515 for ip in net:
516 if ip not in (net.network, net.broadcast):
517 yield ip
518 iterators.append(iter(_iterip()))
519
520 for iterator in iterators:
521 for ip in iterator:
522 if str(ip) not in used_ips:
523 return str(ip)
524
525 message = (
526 "net(%s) has no usable IP address in allocation pools" % net_id)
527 raise exceptions.InvalidConfiguration(message)
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200528
529
Sławek Kapłońskiff294062016-12-04 15:00:54 +0000530def require_qos_rule_type(rule_type):
531 def decorator(f):
532 @functools.wraps(f)
533 def wrapper(self, *func_args, **func_kwargs):
534 if rule_type not in self.get_supported_qos_rule_types():
535 raise self.skipException(
536 "%s rule type is required." % rule_type)
537 return f(self, *func_args, **func_kwargs)
538 return wrapper
539 return decorator
540
541
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200542def _require_sorting(f):
543 @functools.wraps(f)
544 def inner(self, *args, **kwargs):
Ihar Hrachyshka34feb5b2016-06-14 16:16:06 +0200545 if not test.is_extension_enabled("sorting", "network"):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200546 self.skipTest('Sorting feature is required')
547 return f(self, *args, **kwargs)
548 return inner
549
550
551def _require_pagination(f):
552 @functools.wraps(f)
553 def inner(self, *args, **kwargs):
Ihar Hrachyshka34feb5b2016-06-14 16:16:06 +0200554 if not test.is_extension_enabled("pagination", "network"):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200555 self.skipTest('Pagination feature is required')
556 return f(self, *args, **kwargs)
557 return inner
558
559
560class BaseSearchCriteriaTest(BaseNetworkTest):
561
562 # This should be defined by subclasses to reflect resource name to test
563 resource = None
564
Armando Migliaccio57581c62016-07-01 10:13:19 -0700565 field = 'name'
566
Ihar Hrachyshkaa8fe5a12016-05-24 14:50:58 +0200567 # NOTE(ihrachys): some names, like those starting with an underscore (_)
568 # are sorted differently depending on whether the plugin implements native
569 # sorting support, or not. So we avoid any such cases here, sticking to
570 # alphanumeric. Also test a case when there are multiple resources with the
571 # same name
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200572 resource_names = ('test1', 'abc1', 'test10', '123test') + ('test1',)
573
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200574 force_tenant_isolation = True
575
Ihar Hrachyshkaa8fe5a12016-05-24 14:50:58 +0200576 list_kwargs = {}
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200577
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200578 list_as_admin = False
579
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200580 def assertSameOrder(self, original, actual):
581 # gracefully handle iterators passed
582 original = list(original)
583 actual = list(actual)
584 self.assertEqual(len(original), len(actual))
585 for expected, res in zip(original, actual):
Armando Migliaccio57581c62016-07-01 10:13:19 -0700586 self.assertEqual(expected[self.field], res[self.field])
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200587
588 @utils.classproperty
589 def plural_name(self):
590 return '%ss' % self.resource
591
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200592 @property
593 def list_client(self):
594 return self.admin_client if self.list_as_admin else self.client
595
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200596 def list_method(self, *args, **kwargs):
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200597 method = getattr(self.list_client, 'list_%s' % self.plural_name)
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200598 kwargs.update(self.list_kwargs)
599 return method(*args, **kwargs)
600
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200601 def get_bare_url(self, url):
602 base_url = self.client.base_url
603 self.assertTrue(url.startswith(base_url))
604 return url[len(base_url):]
605
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200606 @classmethod
607 def _extract_resources(cls, body):
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200608 return body[cls.plural_name]
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200609
610 def _test_list_sorts(self, direction):
611 sort_args = {
612 'sort_dir': direction,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700613 'sort_key': self.field
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200614 }
615 body = self.list_method(**sort_args)
616 resources = self._extract_resources(body)
617 self.assertNotEmpty(
618 resources, "%s list returned is empty" % self.resource)
Armando Migliaccio57581c62016-07-01 10:13:19 -0700619 retrieved_names = [res[self.field] for res in resources]
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200620 expected = sorted(retrieved_names)
621 if direction == constants.SORT_DIRECTION_DESC:
622 expected = list(reversed(expected))
623 self.assertEqual(expected, retrieved_names)
624
625 @_require_sorting
626 def _test_list_sorts_asc(self):
627 self._test_list_sorts(constants.SORT_DIRECTION_ASC)
628
629 @_require_sorting
630 def _test_list_sorts_desc(self):
631 self._test_list_sorts(constants.SORT_DIRECTION_DESC)
632
633 @_require_pagination
634 def _test_list_pagination(self):
635 for limit in range(1, len(self.resource_names) + 1):
636 pagination_args = {
637 'limit': limit,
638 }
639 body = self.list_method(**pagination_args)
640 resources = self._extract_resources(body)
641 self.assertEqual(limit, len(resources))
642
643 @_require_pagination
644 def _test_list_no_pagination_limit_0(self):
645 pagination_args = {
646 'limit': 0,
647 }
648 body = self.list_method(**pagination_args)
649 resources = self._extract_resources(body)
Béla Vancsicsf1806182016-08-23 07:36:18 +0200650 self.assertGreaterEqual(len(resources), len(self.resource_names))
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200651
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200652 def _test_list_pagination_iteratively(self, lister):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200653 # first, collect all resources for later comparison
654 sort_args = {
655 'sort_dir': constants.SORT_DIRECTION_ASC,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700656 'sort_key': self.field
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200657 }
658 body = self.list_method(**sort_args)
659 expected_resources = self._extract_resources(body)
660 self.assertNotEmpty(expected_resources)
661
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200662 resources = lister(
663 len(expected_resources), sort_args
664 )
665
666 # finally, compare that the list retrieved in one go is identical to
667 # the one containing pagination results
668 self.assertSameOrder(expected_resources, resources)
669
670 def _list_all_with_marker(self, niterations, sort_args):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200671 # paginate resources one by one, using last fetched resource as a
672 # marker
673 resources = []
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200674 for i in range(niterations):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200675 pagination_args = sort_args.copy()
676 pagination_args['limit'] = 1
677 if resources:
678 pagination_args['marker'] = resources[-1]['id']
679 body = self.list_method(**pagination_args)
680 resources_ = self._extract_resources(body)
681 self.assertEqual(1, len(resources_))
682 resources.extend(resources_)
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200683 return resources
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200684
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200685 @_require_pagination
686 @_require_sorting
687 def _test_list_pagination_with_marker(self):
688 self._test_list_pagination_iteratively(self._list_all_with_marker)
689
690 def _list_all_with_hrefs(self, niterations, sort_args):
691 # paginate resources one by one, using next href links
692 resources = []
693 prev_links = {}
694
695 for i in range(niterations):
696 if prev_links:
697 uri = self.get_bare_url(prev_links['next'])
698 else:
Ihar Hrachyshka7f79fe62016-06-07 21:23:44 +0200699 sort_args.update(self.list_kwargs)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200700 uri = self.list_client.build_uri(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200701 self.plural_name, limit=1, **sort_args)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200702 prev_links, body = self.list_client.get_uri_with_links(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200703 self.plural_name, uri
704 )
705 resources_ = self._extract_resources(body)
706 self.assertEqual(1, len(resources_))
707 resources.extend(resources_)
708
709 # The last element is empty and does not contain 'next' link
710 uri = self.get_bare_url(prev_links['next'])
711 prev_links, body = self.client.get_uri_with_links(
712 self.plural_name, uri
713 )
714 self.assertNotIn('next', prev_links)
715
716 # Now walk backwards and compare results
717 resources2 = []
718 for i in range(niterations):
719 uri = self.get_bare_url(prev_links['previous'])
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200720 prev_links, body = self.list_client.get_uri_with_links(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200721 self.plural_name, uri
722 )
723 resources_ = self._extract_resources(body)
724 self.assertEqual(1, len(resources_))
725 resources2.extend(resources_)
726
727 self.assertSameOrder(resources, reversed(resources2))
728
729 return resources
730
731 @_require_pagination
732 @_require_sorting
733 def _test_list_pagination_with_href_links(self):
734 self._test_list_pagination_iteratively(self._list_all_with_hrefs)
735
736 @_require_pagination
737 @_require_sorting
738 def _test_list_pagination_page_reverse_with_href_links(
739 self, direction=constants.SORT_DIRECTION_ASC):
740 pagination_args = {
741 'sort_dir': direction,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700742 'sort_key': self.field,
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200743 }
744 body = self.list_method(**pagination_args)
745 expected_resources = self._extract_resources(body)
746
747 page_size = 2
748 pagination_args['limit'] = page_size
749
750 prev_links = {}
751 resources = []
752 num_resources = len(expected_resources)
753 niterations = int(math.ceil(float(num_resources) / page_size))
754 for i in range(niterations):
755 if prev_links:
756 uri = self.get_bare_url(prev_links['previous'])
757 else:
Ihar Hrachyshka7f79fe62016-06-07 21:23:44 +0200758 pagination_args.update(self.list_kwargs)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200759 uri = self.list_client.build_uri(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200760 self.plural_name, page_reverse=True, **pagination_args)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200761 prev_links, body = self.list_client.get_uri_with_links(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200762 self.plural_name, uri
763 )
764 resources_ = self._extract_resources(body)
Béla Vancsicsf1806182016-08-23 07:36:18 +0200765 self.assertGreaterEqual(page_size, len(resources_))
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200766 resources.extend(reversed(resources_))
767
768 self.assertSameOrder(expected_resources, reversed(resources))
769
770 @_require_pagination
771 @_require_sorting
772 def _test_list_pagination_page_reverse_asc(self):
773 self._test_list_pagination_page_reverse(
774 direction=constants.SORT_DIRECTION_ASC)
775
776 @_require_pagination
777 @_require_sorting
778 def _test_list_pagination_page_reverse_desc(self):
779 self._test_list_pagination_page_reverse(
780 direction=constants.SORT_DIRECTION_DESC)
781
782 def _test_list_pagination_page_reverse(self, direction):
783 pagination_args = {
784 'sort_dir': direction,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700785 'sort_key': self.field,
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200786 'limit': 3,
787 }
788 body = self.list_method(**pagination_args)
789 expected_resources = self._extract_resources(body)
790
791 pagination_args['limit'] -= 1
792 pagination_args['marker'] = expected_resources[-1]['id']
793 pagination_args['page_reverse'] = True
794 body = self.list_method(**pagination_args)
795
796 self.assertSameOrder(
797 # the last entry is not included in 2nd result when used as a
798 # marker
799 expected_resources[:-1],
800 self._extract_resources(body))
Victor Morales1be97b42016-09-05 08:50:06 -0500801
802 def _test_list_validation_filters(self):
803 validation_args = {
804 'unknown_filter': 'value',
805 }
806 body = self.list_method(**validation_args)
807 resources = self._extract_resources(body)
808 for resource in resources:
809 self.assertIn(resource['name'], self.resource_names)