diff --git a/octavia_tempest_plugin/clients.py b/octavia_tempest_plugin/clients.py index 14381f2d..1a0a8947 100644 --- a/octavia_tempest_plugin/clients.py +++ b/octavia_tempest_plugin/clients.py @@ -15,6 +15,8 @@ from tempest import clients from tempest import config +from octavia_tempest_plugin.services.load_balancer.v2 import ( + amphora_client) from octavia_tempest_plugin.services.load_balancer.v2 import ( healthmonitor_client) from octavia_tempest_plugin.services.load_balancer.v2 import ( @@ -53,3 +55,5 @@ class ManagerV2(clients.Manager): self.auth_provider, SERVICE_TYPE, CONF.identity.region) self.l7rule_client = l7rule_client.L7RuleClient( self.auth_provider, SERVICE_TYPE, CONF.identity.region) + self.amphora_client = amphora_client.AmphoraClient( + self.auth_provider, SERVICE_TYPE, CONF.identity.region) diff --git a/octavia_tempest_plugin/common/constants.py b/octavia_tempest_plugin/common/constants.py index 31a970b0..871f9309 100644 --- a/octavia_tempest_plugin/common/constants.py +++ b/octavia_tempest_plugin/common/constants.py @@ -149,6 +149,42 @@ ADVANCED = 'advanced' OWNERADMIN = 'owner_or_admin' NONE = 'none' +# Amphora fields +COMPUTE_ID = 'compute_id' +LB_NETWORK_IP = 'lb_network_ip' +VRRP_IP = 'vrrp_ip' +HA_IP = 'ha_ip' +VRRP_PORT_ID = 'vrrp_port_id' +HA_PORT_ID = 'ha_port_id' +CERT_EXPIRATION = 'cert_expiration' +CERT_BUSY = 'cert_busy' +ROLE = 'role' +STATUS = 'status' +VRRP_INTERFACE = 'vrrp_interface' +VRRP_ID = 'vrrp_id' +VRRP_PRIORITY = 'vrrp_priority' +CACHED_ZONE = 'cached_zone' +IMAGE_ID = 'image_id' + +# Amphora roles +ROLE_STANDALONE = 'STANDALONE' +ROLE_MASTER = 'MASTER' +ROLE_BACKUP = 'BACKUP' +AMPHORA_ROLES = (ROLE_STANDALONE, ROLE_MASTER, ROLE_BACKUP) + +# Amphora statuses +STATUS_BOOTING = 'BOOTING' +STATUS_ALLOCATED = 'ALLOCATED' +STATUS_READY = 'READY' +STATUS_PENDING_CREATE = 'PENDING_CREATE' +STATUS_PENDING_DELETE = 'PENDING_DELETE' +STATUS_DELETED = 'DELETED' +STATUS_ERROR = 'ERROR' +AMPHORA_STATUSES = ( + STATUS_BOOTING, STATUS_ALLOCATED, STATUS_READY, STATUS_PENDING_CREATE, + STATUS_PENDING_DELETE, STATUS_DELETED, STATUS_ERROR +) + # API valid fields SHOW_LOAD_BALANCER_RESPONSE_FIELDS = ( ADMIN_STATE_UP, CREATED_AT, DESCRIPTION, FLAVOR_ID, ID, LISTENERS, NAME, @@ -192,3 +228,10 @@ SHOW_L7RULE_RESPONSE_FIELDS = ( ID, ADMIN_STATE_UP, CREATED_AT, UPDATED_AT, TYPE, VALUE, COMPARE_TYPE, KEY, INVERT ) + +SHOW_AMPHORA_RESPONSE_FIELDS = ( + ID, LOADBALANCER_ID, COMPUTE_ID, LB_NETWORK_IP, VRRP_IP, HA_IP, + VRRP_PORT_ID, HA_PORT_ID, CERT_EXPIRATION, CERT_BUSY, ROLE, STATUS, + VRRP_INTERFACE, VRRP_ID, VRRP_PRIORITY, CACHED_ZONE, IMAGE_ID, + CREATED_AT, UPDATED_AT +) diff --git a/octavia_tempest_plugin/services/load_balancer/v2/amphora_client.py b/octavia_tempest_plugin/services/load_balancer/v2/amphora_client.py new file mode 100644 index 00000000..65a8077b --- /dev/null +++ b/octavia_tempest_plugin/services/load_balancer/v2/amphora_client.py @@ -0,0 +1,105 @@ +# Copyright 2018 GoDaddy +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest import config + +from octavia_tempest_plugin.services.load_balancer.v2 import base_client + +CONF = config.CONF + + +class AmphoraClient(base_client.BaseLBaaSClient): + + root_tag = 'amphora' + list_root_tag = 'amphorae' + base_uri = '/v2.0/octavia/{object}' + + def show_amphora(self, amphora_id, query_params=None, + return_object_only=True): + """Get amphora details. + + :param amphora_id: The amphora ID to query. + :param query_params: The optional query parameters to append to the + request. Ex. fields=id&fields=name + :param return_object_only: If True, the response returns the object + inside the root tag. False returns the full + response from the API. + :raises AssertionError: if the expected_code isn't a valid http success + response code + :raises BadRequest: If a 400 response code is received + :raises Conflict: If a 409 response code is received + :raises Forbidden: If a 403 response code is received + :raises Gone: If a 410 response code is received + :raises InvalidContentType: If a 415 response code is received + :raises InvalidHTTPResponseBody: The response body wasn't valid JSON + :raises InvalidHttpSuccessCode: if the read code isn't an expected + http success code + :raises NotFound: If a 404 response code is received + :raises NotImplemented: If a 501 response code is received + :raises OverLimit: If a 413 response code is received and over_limit is + not in the response body + :raises RateLimitExceeded: If a 413 response code is received and + over_limit is in the response body + :raises ServerFault: If a 500 response code is received + :raises Unauthorized: If a 401 response code is received + :raises UnexpectedContentType: If the content-type of the response + isn't an expect type + :raises UnexpectedResponseCode: If a response code above 400 is + received and it doesn't fall into any + of the handled checks + :raises UnprocessableEntity: If a 422 response code is received and + couldn't be parsed + :returns: An amphora object. + """ + return self._show_object(obj_id=amphora_id, + query_params=query_params, + return_object_only=return_object_only) + + def list_amphorae(self, query_params=None, return_object_only=True): + """Get a list of amphora objects. + + :param query_params: The optional query parameters to append to the + request. Ex. fields=id&fields=name + :param return_object_only: If True, the response returns the object + inside the root tag. False returns the full + response from the API. + :raises AssertionError: if the expected_code isn't a valid http success + response code + :raises BadRequest: If a 400 response code is received + :raises Conflict: If a 409 response code is received + :raises Forbidden: If a 403 response code is received + :raises Gone: If a 410 response code is received + :raises InvalidContentType: If a 415 response code is received + :raises InvalidHTTPResponseBody: The response body wasn't valid JSON + :raises InvalidHttpSuccessCode: if the read code isn't an expected + http success code + :raises NotFound: If a 404 response code is received + :raises NotImplemented: If a 501 response code is received + :raises OverLimit: If a 413 response code is received and over_limit is + not in the response body + :raises RateLimitExceeded: If a 413 response code is received and + over_limit is in the response body + :raises ServerFault: If a 500 response code is received + :raises Unauthorized: If a 401 response code is received + :raises UnexpectedContentType: If the content-type of the response + isn't an expect type + :raises UnexpectedResponseCode: If a response code above 400 is + received and it doesn't fall into any + of the handled checks + :raises UnprocessableEntity: If a 422 response code is received and + couldn't be parsed + :returns: A list of amphora objects. + """ + return self._list_objects(query_params=query_params, + return_object_only=return_object_only) diff --git a/octavia_tempest_plugin/tests/scenario/v2/test_amphora.py b/octavia_tempest_plugin/tests/scenario/v2/test_amphora.py new file mode 100644 index 00000000..b5ebaf57 --- /dev/null +++ b/octavia_tempest_plugin/tests/scenario/v2/test_amphora.py @@ -0,0 +1,159 @@ +# Copyright 2018 GoDaddy +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from uuid import UUID + +from dateutil import parser +from tempest import config +from tempest.lib.common.utils import data_utils +from tempest.lib import decorators +from tempest.lib import exceptions + +from octavia_tempest_plugin.common import constants as const +from octavia_tempest_plugin.tests import test_base +from octavia_tempest_plugin.tests import waiters + +CONF = config.CONF + + +class AmphoraAPITest(test_base.LoadBalancerBaseTest): + """Test the amphora object API.""" + + @classmethod + def resource_setup(cls): + """Setup resources needed by the tests.""" + super(AmphoraAPITest, cls).resource_setup() + + lb_name = data_utils.rand_name("lb_member_lb1_amphora") + lb_kwargs = {const.PROVIDER: CONF.load_balancer.provider, + const.NAME: lb_name} + + cls._setup_lb_network_kwargs(lb_kwargs) + + lb = cls.mem_lb_client.create_loadbalancer(**lb_kwargs) + cls.lb_id = lb[const.ID] + cls.addClassResourceCleanup( + cls.mem_lb_client.cleanup_loadbalancer, + cls.lb_id) + + waiters.wait_for_status(cls.mem_lb_client.show_loadbalancer, + cls.lb_id, const.PROVISIONING_STATUS, + const.ACTIVE, + CONF.load_balancer.lb_build_interval, + CONF.load_balancer.lb_build_timeout) + + @decorators.idempotent_id('a0e9ff99-2c4f-45d5-81c9-78d3107c236f') + def test_amphora_list_and_show(self): + """Tests amphora show API. + + * Show amphora details. + * Validate the show reflects the requested values. + * Validates that other accounts cannot see the amphora. + """ + lb_name = data_utils.rand_name("lb_member_lb2_amphora-list") + lb = self.mem_lb_client.create_loadbalancer( + name=lb_name, + vip_network_id=self.lb_member_vip_net[const.ID]) + lb_id = lb[const.ID] + self.addCleanup( + self.mem_lb_client.cleanup_loadbalancer, + lb_id) + + waiters.wait_for_status(self.mem_lb_client.show_loadbalancer, + lb_id, + const.PROVISIONING_STATUS, + const.ACTIVE, + CONF.load_balancer.lb_build_interval, + CONF.load_balancer.lb_build_timeout) + + # Test that a user with lb_admin role can list the amphora + if CONF.load_balancer.RBAC_test_type == const.ADVANCED: + amphora_client = self.os_roles_lb_admin.amphora_client + amphora_adm = amphora_client.list_amphorae() + self.assertTrue(len(amphora_adm) >= 2) + + # Test that a different user, with load balancer member role, cannot + # see this amphora + if not CONF.load_balancer.RBAC_test_type == const.NONE: + member2_client = self.os_roles_lb_member2.amphora_client + self.assertRaises(exceptions.Forbidden, + member2_client.list_amphorae) + + # Test that a user, without the load balancer member role, cannot + # list amphorae + if CONF.load_balancer.RBAC_test_type == const.ADVANCED: + self.assertRaises( + exceptions.Forbidden, + self.os_primary.amphora_client.list_amphorae) + + # Test that a user with cloud admin role can list the amphorae + if not CONF.load_balancer.RBAC_test_type == const.NONE: + adm = self.os_admin.amphora_client.list_amphorae() + self.assertTrue(len(adm) >= 2) + + # Get an actual list of the amphorae + amphorae = self.os_admin.amphora_client.list_amphorae() + + # There should be AT LEAST 2, there may be more depending on the + # configured topology, or if there are other LBs created besides ours + self.assertTrue(len(amphorae) >= 2) + + # Make sure all of the fields exist on the amp list records + for field in const.SHOW_AMPHORA_RESPONSE_FIELDS: + self.assertIn(field, amphorae[0]) + + amp1_id = amphorae[0][const.ID] + amp1 = self.os_admin.amphora_client.show_amphora(amphora_id=amp1_id) + + # Make sure all of the fields exist on the amp show record + for field in const.SHOW_AMPHORA_RESPONSE_FIELDS: + self.assertIn(field, amp1) + + # Verify a few of the fields are the right type + parser.parse(amp1[const.CREATED_AT]) + parser.parse(amp1[const.UPDATED_AT]) + UUID(amp1[const.ID]) + UUID(amp1[const.COMPUTE_ID]) + UUID(amp1[const.VRRP_PORT_ID]) + self.assertIn(amp1[const.ROLE], const.AMPHORA_ROLES) + self.assertIn(amp1[const.STATUS], const.AMPHORA_STATUSES) + # We might have gotten unassigned/spare amps? + if amp1[const.STATUS] == const.STATUS_ALLOCATED: + UUID(amp1[const.HA_PORT_ID]) + UUID(amp1[const.LOADBALANCER_ID]) + + # Test that all of the fields from the amp list match those from a show + for field in const.SHOW_AMPHORA_RESPONSE_FIELDS: + self.assertEqual(amphorae[0][field], amp1[field]) + + amp2_id = amphorae[1][const.ID] + amp2 = self.os_admin.amphora_client.show_amphora(amphora_id=amp2_id) + + # Test that all of the fields from the amp list match those from a show + # (on another amphora) + for field in const.SHOW_AMPHORA_RESPONSE_FIELDS: + self.assertEqual(amphorae[1][field], amp2[field]) + + # Test filtering by loadbalancer_id + amphorae = self.os_admin.amphora_client.list_amphorae( + query_params='{loadbalancer_id}={lb_id}'.format( + loadbalancer_id=const.LOADBALANCER_ID, lb_id=self.lb_id)) + self.assertEqual(1, len(amphorae)) + self.assertEqual(self.lb_id, amphorae[0][const.LOADBALANCER_ID]) + + amphorae = self.os_admin.amphora_client.list_amphorae( + query_params='{loadbalancer_id}={lb_id}'.format( + loadbalancer_id=const.LOADBALANCER_ID, lb_id=lb_id)) + self.assertEqual(1, len(amphorae)) + self.assertEqual(lb_id, amphorae[0][const.LOADBALANCER_ID]) diff --git a/octavia_tempest_plugin/tests/test_base.py b/octavia_tempest_plugin/tests/test_base.py index cf068799..0e6eba43 100644 --- a/octavia_tempest_plugin/tests/test_base.py +++ b/octavia_tempest_plugin/tests/test_base.py @@ -114,6 +114,7 @@ class LoadBalancerBaseTest(test.BaseTestCase): cls.os_roles_lb_member.healthmonitor_client) cls.mem_l7policy_client = cls.os_roles_lb_member.l7policy_client cls.mem_l7rule_client = cls.os_roles_lb_member.l7rule_client + cls.mem_amphora_client = cls.os_roles_lb_member.amphora_client @classmethod def resource_setup(cls):