From 49b390c044d2790de5987f5a47027d82bd07e948 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 20 Jul 2021 10:50:03 +0200 Subject: [PATCH] Initialize functional testing (#30) Initialize functional testing Reviewed-by: None Reviewed-by: kucerakk Reviewed-by: Artem Goncharov Reviewed-by: OpenTelekomCloud Bot --- .zuul.yaml | 8 + doc/source/contributor/index.rst | 3 +- etc/octavia_proxy.conf | 2 +- etc/policy.yaml | 23 --- octavia_proxy/api/common/hooks.py | 5 +- octavia_proxy/api/v2/controllers/__init__.py | 3 +- octavia_proxy/api/v2/controllers/base.py | 6 + octavia_proxy/common/context.py | 9 -- octavia_proxy/common/policy.py | 28 ---- octavia_proxy/common/service.py | 7 +- octavia_proxy/policies/base.py | 10 +- octavia_proxy/tests/functional/__init__.py | 0 .../tests/functional/api/__init__.py | 0 .../functional/api/test_root_controller.py | 37 +++++ .../tests/functional/api/v2/__init__.py | 0 octavia_proxy/tests/functional/api/v2/base.py | 147 ++++++++++++++++++ .../functional/api/v2/test_load_balancer.py | 26 ++++ .../tests/functional/api/v2/test_provider.py | 40 +++++ octavia_proxy/tests/functional/base.py | 33 ++++ .../add-func-tests-3c166f118aa68555.yaml | 4 + test-requirements.txt | 5 + tox.ini | 4 + 22 files changed, 325 insertions(+), 75 deletions(-) delete mode 100644 etc/policy.yaml create mode 100644 octavia_proxy/tests/functional/__init__.py create mode 100644 octavia_proxy/tests/functional/api/__init__.py create mode 100644 octavia_proxy/tests/functional/api/test_root_controller.py create mode 100644 octavia_proxy/tests/functional/api/v2/__init__.py create mode 100644 octavia_proxy/tests/functional/api/v2/base.py create mode 100644 octavia_proxy/tests/functional/api/v2/test_load_balancer.py create mode 100644 octavia_proxy/tests/functional/api/v2/test_provider.py create mode 100644 octavia_proxy/tests/functional/base.py create mode 100644 releasenotes/notes/add-func-tests-3c166f118aa68555.yaml diff --git a/.zuul.yaml b/.zuul.yaml index 6313f9a5..07ac070a 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -41,8 +41,16 @@ - otc-tox-pep8 - otc-tox-py39-tips - octavia-proxy-build-image + - tox-functional: + required-projects: + - name: opentelekomcloud/python-otcextensions + override-checkout: elb gate: jobs: - otc-tox-pep8 - otc-tox-py39-tips - octavia-proxy-upload-image + - tox-functional: + required-projects: + - name: opentelekomcloud/python-otcextensions + override-checkout: elb diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index 671f0d3e..4fe84031 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -15,8 +15,9 @@ Contributor Guidelines Developer Setup --------------- -- tox create venv for octavia-proxy +- tox `tox -e py39 --notest` - source into it +- `rm -rf .tox/py39/lib/python3.9/site-packages/otcextensions*` - with the venv python go to otcextensions elb branch and do `python setup.py develop` - add into the clouds.yaml diff --git a/etc/octavia_proxy.conf b/etc/octavia_proxy.conf index dde4a827..0a5b1520 100644 --- a/etc/octavia_proxy.conf +++ b/etc/octavia_proxy.conf @@ -48,7 +48,7 @@ auth_strategy = validatetoken # Dictionary of enabled provider driver names and descriptions # A comma separated list of dictionaries of the enabled provider driver names # and descriptions. -enabled_provider_drivers = elbv3:The Open Telekom Cloud AZ aware LD driver. +enabled_provider_drivers = elbv3:The Open Telekom Cloud AZ aware LB driver. #enabled_provider_drivers = elbv2:The Open Telekom Cloud Enhanced LB driver.,elbv3:The Open Telekom Cloud AZ aware LD driver. # diff --git a/etc/policy.yaml b/etc/policy.yaml deleted file mode 100644 index 65c2e852..00000000 --- a/etc/policy.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# This policy.yaml will revert the Octavia API to follow the legacy -# admin-or-owner RBAC policies. -# It provides a similar policy to legacy OpenStack policies where any -# user or admin has access to load-balancer resources that they own. -# Users with the admin role has access to all load-balancer resources, -# whether they own them or not. - -# Role Rules -#"context_is_admin": "role:admin or role:load-balancer_admin" -#"admin_or_owner": "is_admin:True or project_id:%(project_id)s" -"load-balancer:member_and_owner": "(role:te_admin and rule:load-balancer:owner)" - #"load-balancer:read": "(rule:load-balancer:observer_and_owner or rule:load-balancer:global_observer or rule:load-balancer:member_and_owner or rule:load-balancer:admin)", - -# Rules -#"load-balancer:read": "rule:admin_or_owner" -#"load-balancer:read-global": "is_admin:True" -#"load-balancer:write": "rule:admin_or_owner" -#"load-balancer:read-quota": "rule:admin_or_owner" -#"load-balancer:read-quota-global": "is_admin:True" -#"load-balancer:write-quota": "is_admin:True" - - #"os_load-balancer_api:provider:get_all": "rule:load-balancer:read" - #"os_load-balancer_api:loadbalancer:get_all": "rule:admin_or_owner" diff --git a/octavia_proxy/api/common/hooks.py b/octavia_proxy/api/common/hooks.py index 98efbbe6..48853972 100644 --- a/octavia_proxy/api/common/hooks.py +++ b/octavia_proxy/api/common/hooks.py @@ -18,8 +18,9 @@ class ContextHook(hooks.PecanHook): def on_route(self, state): context_obj = context.Context.from_environ(state.request.environ) - token_info = state.request.environ['keystone.token_info'] - context_obj.set_token_info(token_info) + token_info = state.request.environ.get('keystone.token_info') + if token_info: + context_obj.set_token_info(token_info) state.request.context['octavia_context'] = context_obj diff --git a/octavia_proxy/api/v2/controllers/__init__.py b/octavia_proxy/api/v2/controllers/__init__.py index 95e597f8..195d53d1 100644 --- a/octavia_proxy/api/v2/controllers/__init__.py +++ b/octavia_proxy/api/v2/controllers/__init__.py @@ -5,6 +5,7 @@ from octavia_proxy.api.v2.controllers import flavors from octavia_proxy.api.v2.controllers import load_balancer from octavia_proxy.api.v2.controllers import listener +from octavia_proxy.api.v2.controllers import pool from octavia_proxy.api.v2.controllers import provider @@ -20,7 +21,7 @@ def __init__(self): super().__init__() self.loadbalancers = load_balancer.LoadBalancersController() self.listeners = listener.ListenersController() -# self.pools = pool.PoolsController() + self.pools = pool.PoolsController() # self.l7policies = l7policy.L7PolicyController() # self.healthmonitors = health_monitor.HealthMonitorController() # self.quotas = quotas.QuotasController() diff --git a/octavia_proxy/api/v2/controllers/base.py b/octavia_proxy/api/v2/controllers/base.py index 6bdf5241..e6aacb67 100644 --- a/octavia_proxy/api/v2/controllers/base.py +++ b/octavia_proxy/api/v2/controllers/base.py @@ -127,3 +127,9 @@ def _filter_fields(self, object_list, fields): if member not in fields: setattr(obj, member, wtypes.Unset) return object_list + + @staticmethod + def _get_attrs(obj): + attrs = [attr for attr in dir(obj) if not callable( + getattr(obj, attr)) and not attr.startswith("_")] + return attrs diff --git a/octavia_proxy/common/context.py b/octavia_proxy/common/context.py index fcf090d9..8e9a5dd6 100644 --- a/octavia_proxy/common/context.py +++ b/octavia_proxy/common/context.py @@ -18,13 +18,8 @@ class Context(common_context.RequestContext): _session = None def __init__(self, user_id=None, project_id=None, **kwargs): - if project_id: - kwargs['tenant'] = project_id - super().__init__(**kwargs) - self.is_admin = False - def set_token_info(self, token_info): """Set token into to be able to recreate session @@ -51,7 +46,3 @@ def session(self): vendor_hook='otcextensions.sdk:load') self._session = sdk return self._session - - @property - def project_id(self): - return self.tenant diff --git a/octavia_proxy/common/policy.py b/octavia_proxy/common/policy.py index 64473a60..dc254d6a 100644 --- a/octavia_proxy/common/policy.py +++ b/octavia_proxy/common/policy.py @@ -98,10 +98,6 @@ def authorize(self, action, target, context, do_raise=True, exc=None): do_raise is False. """ credentials = context.to_policy_values() - # Inject is_admin into the credentials to allow override via - # config auth_strategy = constants.NOAUTH - credentials['is_admin'] = ( - credentials.get('is_admin') or context.is_admin) if not exc: exc = exceptions.PolicyForbidden @@ -120,34 +116,10 @@ def authorize(self, action, target, context, do_raise=True, exc=None): {'action': action, 'credentials': credentials}) return None - def check_is_admin(self, context): - """Does roles contains 'admin' role according to policy setting. - - """ - credentials = context.to_dict() - return self.enforce('context_is_admin', credentials, credentials) - def get_rules(self): return self.rules -@oslo_policy.register('is_admin') -class IsAdminCheck(oslo_policy.Check): - """An explicit check for is_admin.""" - - def __init__(self, kind, match): - """Initialize the check.""" - - self.expected = match.lower() == 'true' - - super().__init__(kind, str(self.expected)) - - def __call__(self, target, creds, enforcer): - """Determine whether is_admin matches the requested value.""" - - return creds['is_admin'] == self.expected - - # This is used for the oslopolicy-policy-generator tool def get_no_context_enforcer(): return Policy() diff --git a/octavia_proxy/common/service.py b/octavia_proxy/common/service.py index fca1ef7b..eb79fa3b 100644 --- a/octavia_proxy/common/service.py +++ b/octavia_proxy/common/service.py @@ -11,6 +11,7 @@ # 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 pathlib import Path from oslo_config import cfg @@ -20,8 +21,12 @@ def prepare_service(argv=None): """Sets global config from config file and sets up logging.""" argv = argv or [] + config_file = '/etc/octavia_proxy/octavia_proxy.conf' + kwargs = dict() + if Path(config_file).is_file(): + kwargs['default_config_files'] = [config_file] config.init( argv[1:], - default_config_files=['/etc/octavia_proxy/octavia_proxy.conf'] + **kwargs ) config.setup_logging(cfg.CONF) diff --git a/octavia_proxy/policies/base.py b/octavia_proxy/policies/base.py index 82bf719f..c5aeaa55 100644 --- a/octavia_proxy/policies/base.py +++ b/octavia_proxy/policies/base.py @@ -30,18 +30,10 @@ # role:admin # User is admin to all APIs - policy.RuleDefault('context_is_admin', - 'role:admin or role:load-balancer_admin'), - - # Note: 'is_admin:True' is a policy rule that takes into account the - # auth_strategy == noauth configuration setting. - # It is equivalent to 'rule:context_is_admin or {auth_strategy == noauth}' - policy.RuleDefault('load-balancer:owner', 'project_id:%(project_id)s'), # API access roles - policy.RuleDefault('load-balancer:admin', 'is_admin:True or ' - 'role:admin or ' + policy.RuleDefault('load-balancer:admin', 'role:te_admin or ' 'role:load-balancer_admin'), policy.RuleDefault('load-balancer:observer_and_owner', diff --git a/octavia_proxy/tests/functional/__init__.py b/octavia_proxy/tests/functional/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/octavia_proxy/tests/functional/api/__init__.py b/octavia_proxy/tests/functional/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/octavia_proxy/tests/functional/api/test_root_controller.py b/octavia_proxy/tests/functional/api/test_root_controller.py new file mode 100644 index 00000000..107d024e --- /dev/null +++ b/octavia_proxy/tests/functional/api/test_root_controller.py @@ -0,0 +1,37 @@ +# 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 octavia_proxy.tests.functional import base +import pecan.testing + +from octavia_proxy.api import config as pconfig + + +class TestRootController(base.TestCase): + def get(self, app, path, params=None, headers=None, status=200, + expect_errors=False): + response = app.get( + path, params=params, headers=headers, status=status, + expect_errors=expect_errors) + return response + + def _get_versions_with_config(self): + # Note: we need to set argv=() to stop the wsgi setup_app from + # pulling in the testing tool sys.argv + app = pecan.testing.load_test_app({'app': pconfig.app, + 'wsme': pconfig.wsme}, argv=()) + return self.get(app=app, path='/').json.get('versions', None) + + def test_api_versions(self): + versions = self._get_versions_with_config() + version_ids = tuple(v.get('id') for v in versions) + self.assertIn('v2.0', version_ids) diff --git a/octavia_proxy/tests/functional/api/v2/__init__.py b/octavia_proxy/tests/functional/api/v2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/octavia_proxy/tests/functional/api/v2/base.py b/octavia_proxy/tests/functional/api/v2/base.py new file mode 100644 index 00000000..0eca16c0 --- /dev/null +++ b/octavia_proxy/tests/functional/api/v2/base.py @@ -0,0 +1,147 @@ +# 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. + +import openstack + +from octavia_proxy.common import constants +from octavia_proxy.tests.functional import base +import pecan.testing + +from octavia_proxy.api import config as pconfig + + +class BaseAPITest(base.TestCase): + + BASE_PATH = '/v2' + BASE_PATH_v2_0 = '/v2.0' + + # /lbaas/flavors + FLAVORS_PATH = '/flavors' + FLAVOR_PATH = FLAVORS_PATH + '/{flavor_id}' + + # /lbaas/flavorprofiles + FPS_PATH = '/flavorprofiles' + FP_PATH = FPS_PATH + '/{fp_id}' + + # /lbaas/availabilityzones + AZS_PATH = '/availabilityzones' + AZ_PATH = AZS_PATH + '/{az_name}' + + # /lbaas/availabilityzoneprofiles + AZPS_PATH = '/availabilityzoneprofiles' + AZP_PATH = AZPS_PATH + '/{azp_id}' + + # /lbaas/loadbalancers + LBS_PATH = '/lbaas/loadbalancers' + LB_PATH = LBS_PATH + '/{lb_id}' + LB_STATUS_PATH = LB_PATH + '/statuses' + LB_STATS_PATH = LB_PATH + '/stats' + + # /lbaas/listeners/ + LISTENERS_PATH = '/lbaas/listeners' + LISTENER_PATH = LISTENERS_PATH + '/{listener_id}' + LISTENER_STATS_PATH = LISTENER_PATH + '/stats' + + # /lbaas/pools + POOLS_PATH = '/lbaas/pools' + POOL_PATH = POOLS_PATH + '/{pool_id}' + + # /lbaas/pools/{pool_id}/members + MEMBERS_PATH = POOL_PATH + '/members' + MEMBER_PATH = MEMBERS_PATH + '/{member_id}' + + # /lbaas/healthmonitors + HMS_PATH = '/lbaas/healthmonitors' + HM_PATH = HMS_PATH + '/{healthmonitor_id}' + + # /lbaas/l7policies + L7POLICIES_PATH = '/lbaas/l7policies' + L7POLICY_PATH = L7POLICIES_PATH + '/{l7policy_id}' + L7RULES_PATH = L7POLICY_PATH + '/rules' + L7RULE_PATH = L7RULES_PATH + '/{l7rule_id}' + + QUOTAS_PATH = '/lbaas/quotas' + QUOTA_PATH = QUOTAS_PATH + '/{project_id}' + QUOTA_DEFAULT_PATH = QUOTAS_PATH + '/{project_id}/default' + + PROVIDERS_PATH = '/lbaas/providers' + FLAVOR_CAPABILITIES_PATH = ( + PROVIDERS_PATH + '/{provider}/flavor_capabilities') + AVAILABILITY_ZONE_CAPABILITIES_PATH = ( + PROVIDERS_PATH + '/{provider}/availability_zone_capabilities') + + NOT_AUTHORIZED_BODY = { + 'debuginfo': None, 'faultcode': 'Client', + 'faultstring': 'Policy does not allow this request to be performed.'} + + def setUp(self): + super().setUp() + self._token = None + self._sdk_connection = None + self.project_id = None + self.conf.config( + group='api_settings', + auth_strategy=constants.KEYSTONE_EXT) + self.app = self._make_app() + + def reset_pecan(): + pecan.set_config({}, overwrite=True) + + self.addCleanup(reset_pecan) + + def tearDown(self): + if self._sdk_connection: + self._sdk_connection.close() + super().tearDown() + + def _make_app(self): + # Note: we need to set argv=() to stop the wsgi setup_app from + # pulling in the testing tool sys.argv + return pecan.testing.load_test_app( + { + 'app': pconfig.app, + 'wsme': pconfig.wsme, + 'debug': True, + }, argv=()) + + def _get_full_path(self, path): + return ''.join([self.BASE_PATH, path]) + + def _get_full_path_v2_0(self, path): + return ''.join([self.BASE_PATH_v2_0, path]) + + def _build_body(self, json): + return {self.root_tag: json} + + def _get_token(self): + if not self._sdk_connection: + self._sdk_connection = openstack.connect() + if not self._token: + self._token = self._sdk_connection.auth_token + self.project_id = self._sdk_connection.current_project_id + return self._token + + def get(self, path, params=None, headers=None, status=200, + expect_errors=False, authorized=True): + full_path = self._get_full_path(path) + if authorized: + if not headers: + headers = dict() + headers['X-Auth-Token'] = self._get_token() + response = self.app.get( + full_path, + params=params, + headers=headers, + status=status, + expect_errors=expect_errors + ) + return response diff --git a/octavia_proxy/tests/functional/api/v2/test_load_balancer.py b/octavia_proxy/tests/functional/api/v2/test_load_balancer.py new file mode 100644 index 00000000..c2926069 --- /dev/null +++ b/octavia_proxy/tests/functional/api/v2/test_load_balancer.py @@ -0,0 +1,26 @@ +# Copyright 2014 Rackspace +# +# 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 octavia_proxy.tests.functional.api.v2 import base + + +class TestLoadBalancer(base.BaseAPITest): + root_tag = 'loadbalancer' + root_tag_list = 'loadbalancers' + root_tag_links = 'loadbalancers_links' + + def test_empty_list(self): + response = self.get(self.LBS_PATH) + api_list = response.json.get(self.root_tag_list) + self.assertEqual([], api_list) diff --git a/octavia_proxy/tests/functional/api/v2/test_provider.py b/octavia_proxy/tests/functional/api/v2/test_provider.py new file mode 100644 index 00000000..cb6c9e9d --- /dev/null +++ b/octavia_proxy/tests/functional/api/v2/test_provider.py @@ -0,0 +1,40 @@ +# 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 octavia_proxy.tests.functional.api.v2 import base + + +class TestProvider(base.BaseAPITest): + + root_tag_list = 'providers' + + def setUp(self): + super().setUp() + + def test_get_all_providers(self): + elbv2_dict = {u'description': u'The ELBv2 driver.', + u'name': u'elbv2'} + elbv3_dict = {u'description': u'The ELBv3 driver.', + u'name': u'elbv3'} + providers = self.get(self.PROVIDERS_PATH).json.get(self.root_tag_list) + self.assertEqual(2, len(providers)) + self.assertIn(elbv2_dict, providers) + self.assertIn(elbv3_dict, providers) + + def test_get_all_providers_fields(self): + elbv2_dict = {u'name': u'elbv2'} + elbv3_dict = {u'name': u'elbv3'} + providers = self.get(self.PROVIDERS_PATH, params={'fields': ['name']}) + providers_list = providers.json.get(self.root_tag_list) + self.assertEqual(2, len(providers_list)) + self.assertIn(elbv2_dict, providers_list) + self.assertIn(elbv3_dict, providers_list) diff --git a/octavia_proxy/tests/functional/base.py b/octavia_proxy/tests/functional/base.py new file mode 100644 index 00000000..73c9cc5e --- /dev/null +++ b/octavia_proxy/tests/functional/base.py @@ -0,0 +1,33 @@ +# 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. + +import keyring +from keyrings.alt import file + +from oslotest import base + +from oslo_config import fixture as oslo_fixture + +from octavia_proxy.common import config + + +class TestCase(base.BaseTestCase): + + def setUp(self): + super().setUp() + cfg = config.cfg.CONF + # debug can be enabled as: + # cfg.debug = True + self.conf = self.useFixture(oslo_fixture.Config(cfg)) + keyring.set_keyring( + file.PlaintextKeyring() + ) diff --git a/releasenotes/notes/add-func-tests-3c166f118aa68555.yaml b/releasenotes/notes/add-func-tests-3c166f118aa68555.yaml new file mode 100644 index 00000000..cdedbeac --- /dev/null +++ b/releasenotes/notes/add-func-tests-3c166f118aa68555.yaml @@ -0,0 +1,4 @@ +--- +other: + - | + Initialize functional testing. diff --git a/test-requirements.txt b/test-requirements.txt index 177a9fb4..ff2cc072 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,8 @@ doc8>=0.8.0 # Apache-2.0 stestr>=1.0.0 # Apache-2.0 flake8 +oslotest>=3.2.0 # Apache-2.0 + +# Caching token in func tests +keyring # MIT +keyrings.alt # MIT diff --git a/tox.ini b/tox.ini index b4831729..73a5dace 100644 --- a/tox.ini +++ b/tox.ini @@ -61,3 +61,7 @@ deps = -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W --keep-going -b html releasenotes/source releasenotes/build/html + +[testenv:functional] +# This will use whatever 'basepython' is set to, so the name is ambiguous. +setenv = OS_TEST_PATH={toxinidir}/octavia_proxy/tests/functional