From c4bef14fc1975f2e115a6ec8560e674e8aa5b1bf Mon Sep 17 00:00:00 2001 From: Tim Burke Date: Wed, 9 Oct 2019 16:59:23 -0700 Subject: [PATCH] v1auth: support endpoint_data_for() api ...so we can be used with openstacksdk. Also, add a few functests that use openstacksdk. Change-Id: Ie6987f5de48914ec8932254cde79a973a0264877 --- lower-constraints.txt | 1 + swiftclient/authv1.py | 12 +++- test-requirements.txt | 1 + test/functional/__init__.py | 93 ++++++++++++++++++++++++++++ test/functional/test_openstacksdk.py | 92 +++++++++++++++++++++++++++ test/functional/test_swiftclient.py | 85 +++++++------------------ 6 files changed, 221 insertions(+), 63 deletions(-) create mode 100644 test/functional/test_openstacksdk.py diff --git a/lower-constraints.txt b/lower-constraints.txt index 88d28650..ead02791 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -21,6 +21,7 @@ mccabe==0.2.1 mock==1.2.0 netaddr==0.7.10 openstackdocstheme==1.20.0 +openstacksdk==0.11.0 oslo.config==1.2.0 pbr==2.0.0 pep8==1.5.7 diff --git a/swiftclient/authv1.py b/swiftclient/authv1.py index 55469acf..d70acac3 100644 --- a/swiftclient/authv1.py +++ b/swiftclient/authv1.py @@ -45,6 +45,7 @@ from six.moves.urllib.parse import urljoin # Note that while we import keystoneauth1 here, we *don't* need to add it to # requirements.txt -- this entire module only makes sense (and should only be # loaded) if keystoneauth is already installed. +from keystoneauth1 import discover from keystoneauth1 import plugin from keystoneauth1 import exceptions from keystoneauth1 import loading @@ -110,11 +111,20 @@ class ServiceCatalogV1(object): ] def url_for(self, **kwargs): + return self.endpoint_data_for(**kwargs).url + + def endpoint_data_for(self, **kwargs): kwargs.setdefault('interface', 'public') kwargs.setdefault('service_type', None) if kwargs['service_type'] == 'object-store': - return self.storage_url + return discover.EndpointData( + service_type='object-store', + service_name='swift', + interface=kwargs['interface'], + region_name='default', + catalog_url=self.storage_url, + ) # Although our "catalog" includes an identity entry, nothing that uses # url_for() (including `openstack endpoint list`) will know what to do diff --git a/test-requirements.txt b/test-requirements.txt index b3ca5f89..13732533 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,3 +4,4 @@ coverage!=4.4,>=4.0 # Apache-2.0 keystoneauth1>=3.4.0 # Apache-2.0 mock>=1.2.0 # BSD stestr>=2.0.0 # Apache-2.0 +openstacksdk>=0.11.0 # Apache-2.0 diff --git a/test/functional/__init__.py b/test/functional/__init__.py index e69de29b..248875a6 100644 --- a/test/functional/__init__.py +++ b/test/functional/__init__.py @@ -0,0 +1,93 @@ +# Copyright (c) 2014 Christian Schwede +# +# 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 os +from six.moves import configparser + +TEST_CONFIG = None + + +def _load_config(force_reload=False): + global TEST_CONFIG + if not force_reload and TEST_CONFIG is not None: + return TEST_CONFIG + + config_file = os.environ.get('SWIFT_TEST_CONFIG_FILE', + '/etc/swift/test.conf') + parser = configparser.ConfigParser({'auth_version': '1'}) + parser.read(config_file) + conf = {} + if parser.has_section('func_test'): + if parser.has_option('func_test', 'auth_uri'): + conf['auth_url'] = parser.get('func_test', 'auth_uri') + try: + conf['auth_version'] = parser.get('func_test', 'auth_version') + except configparser.NoOptionError: + last_piece = conf['auth_url'].rstrip('/').rsplit('/', 1)[1] + if last_piece.endswith('.0'): + last_piece = last_piece[:-2] + if last_piece in ('1', '2', '3'): + conf['auth_version'] = last_piece + else: + raise + else: + auth_host = parser.get('func_test', 'auth_host') + auth_port = parser.getint('func_test', 'auth_port') + auth_ssl = parser.getboolean('func_test', 'auth_ssl') + auth_prefix = parser.get('func_test', 'auth_prefix') + conf['auth_version'] = parser.get('func_test', 'auth_version') + if auth_ssl: + auth_url = "https://" + else: + auth_url = "http://" + auth_url += "%s:%s%s" % (auth_host, auth_port, auth_prefix) + if conf['auth_version'] == "1": + auth_url += 'v1.0' + conf['auth_url'] = auth_url + + try: + conf['account_username'] = parser.get('func_test', + 'account_username') + except configparser.NoOptionError: + conf['account'] = parser.get('func_test', 'account') + conf['username'] = parser.get('func_test', 'username') + conf['account_username'] = "%s:%s" % (conf['account'], + conf['username']) + else: + # Still try to get separate account/usernames for keystone tests + try: + conf['account'] = parser.get('func_test', 'account') + conf['username'] = parser.get('func_test', 'username') + except configparser.NoOptionError: + pass + + conf['password'] = parser.get('func_test', 'password') + + # For keystone v3 + try: + conf['account4'] = parser.get('func_test', 'account4') + conf['username4'] = parser.get('func_test', 'username4') + conf['domain4'] = parser.get('func_test', 'domain4') + conf['password4'] = parser.get('func_test', 'password4') + except configparser.NoOptionError: + pass + + TEST_CONFIG = conf + + +try: + _load_config() +except configparser.NoOptionError: + TEST_CONFIG = None # sentinel used in test setup diff --git a/test/functional/test_openstacksdk.py b/test/functional/test_openstacksdk.py new file mode 100644 index 00000000..cee7f4e9 --- /dev/null +++ b/test/functional/test_openstacksdk.py @@ -0,0 +1,92 @@ +# Copyright (c) 2019 Tim Burke +# +# 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 unittest +import uuid + +import openstack + +from . import TEST_CONFIG + +PREFIX = 'test-swiftclient-' + + +class TestOpenStackSDK(unittest.TestCase): + @classmethod + def setUpClass(cls): + # NB: Only runs for v1 auth, to exercise our keystoneauth plugin + cls.skip_tests = (TEST_CONFIG is None or + TEST_CONFIG['auth_version'] != '1') + if not cls.skip_tests: + cls.conn = openstack.connect( + auth_type='v1password', + auth_url=TEST_CONFIG['auth_url'], + username=TEST_CONFIG['account_username'], + password=TEST_CONFIG['password'], + ) + cls.object_store = cls.conn.object_store + + def setUp(self): + if self.skip_tests: + raise unittest.SkipTest('SKIPPING V1-AUTH TESTS') + + def tearDown(self): + if self.skip_tests: + return + for c in self.object_store.containers(): + if c.name.startswith(PREFIX): + for o in self.object_store.objects(c.name): + self.object_store.delete_object( + o.name, container=c.name) + self.object_store.delete_container(c.name) + + def test_containers(self): + meta = self.object_store.get_account_metadata() + count_before = meta.account_container_count + containers = sorted(PREFIX + str(uuid.uuid4()) + for _ in range(10)) + for c in containers: + self.object_store.create_container(c) + self.assertEqual([ + c.name for c in self.object_store.containers() + if c.name.startswith(PREFIX) + ], containers) + meta = self.object_store.get_account_metadata() + self.assertEqual(count_before + len(containers), + meta.account_container_count) + + def test_objects(self): + container = PREFIX + str(uuid.uuid4()) + self.object_store.create_container(container) + objects = sorted(str(uuid.uuid4()) for _ in range(10)) + for o in objects: + self.object_store.create_object(container, o, data=b'x') + self.assertEqual([ + o.name for o in self.object_store.objects(container) + ], objects) + meta = self.object_store.get_container_metadata(container) + self.assertEqual(len(objects), meta.object_count) + + def test_object_metadata(self): + container = PREFIX + str(uuid.uuid4()) + self.object_store.create_container(container) + obj = str(uuid.uuid4()) + obj_meta = {str(uuid.uuid4()): str(uuid.uuid4()) for _ in range(10)} + # NB: as of 0.36.0, create_object() doesn't play well with passing + # both data and metadata, so we do a PUT then POST + self.object_store.create_object(container, obj, data=b'x') + self.object_store.set_object_metadata(obj, container, **obj_meta) + meta = self.object_store.get_object_metadata(obj, container) + self.assertEqual(obj_meta, meta.metadata) diff --git a/test/functional/test_swiftclient.py b/test/functional/test_swiftclient.py index 9a74c63f..54c514de 100644 --- a/test/functional/test_swiftclient.py +++ b/test/functional/test_swiftclient.py @@ -13,23 +13,23 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os import unittest import time from io import BytesIO import six -from six.moves import configparser import swiftclient +from . import TEST_CONFIG class TestFunctional(unittest.TestCase): def __init__(self, *args, **kwargs): super(TestFunctional, self).__init__(*args, **kwargs) - self.skip_tests = False - self._get_config() + self.skip_tests = (TEST_CONFIG is None) + if not self.skip_tests: + self._get_config() self.test_data = b'42' * 10 self.etag = '2704306ec982238d85d4b235c925d58e' @@ -41,50 +41,10 @@ class TestFunctional(unittest.TestCase): self.objectname_2 = self.objectname + '_second' def _get_config(self): - config_file = os.environ.get('SWIFT_TEST_CONFIG_FILE', - '/etc/swift/test.conf') - config = configparser.ConfigParser({'auth_version': '1'}) - config.read(config_file) - self.config = config - if config.has_section('func_test'): - if config.has_option('func_test', 'auth_uri'): - self.auth_url = config.get('func_test', 'auth_uri') - try: - self.auth_version = config.get('func_test', 'auth_version') - except configparser.NoOptionError: - last_piece = self.auth_url.rstrip('/').rsplit('/', 1)[1] - if last_piece.endswith('.0'): - last_piece = last_piece[:-2] - if last_piece in ('1', '2', '3'): - self.auth_version = last_piece - else: - raise - else: - auth_host = config.get('func_test', 'auth_host') - auth_port = config.getint('func_test', 'auth_port') - auth_ssl = config.getboolean('func_test', 'auth_ssl') - auth_prefix = config.get('func_test', 'auth_prefix') - self.auth_version = config.get('func_test', 'auth_version') - self.auth_url = "" - if auth_ssl: - self.auth_url += "https://" - else: - self.auth_url += "http://" - self.auth_url += "%s:%s%s" % ( - auth_host, auth_port, auth_prefix) - if self.auth_version == "1": - self.auth_url += 'v1.0' - - try: - self.account_username = config.get('func_test', - 'account_username') - except configparser.NoOptionError: - account = config.get('func_test', 'account') - username = config.get('func_test', 'username') - self.account_username = "%s:%s" % (account, username) - self.password = config.get('func_test', 'password') - else: - self.skip_tests = True + self.auth_url = TEST_CONFIG['auth_url'] + self.auth_version = TEST_CONFIG['auth_version'] + self.account_username = TEST_CONFIG['account_username'] + self.password = TEST_CONFIG['password'] def _get_connection(self): """ @@ -514,20 +474,20 @@ class TestUsingKeystone(TestFunctional): """ def _get_connection(self): - account = username = password = None + account = username = None if self.auth_version not in ('2', '3'): self.skipTest('SKIPPING KEYSTONE-SPECIFIC FUNCTIONAL TESTS') try: - account = self.config.get('func_test', 'account') - username = self.config.get('func_test', 'username') - password = self.config.get('func_test', 'password') - except Exception: + account = TEST_CONFIG['account'] + username = TEST_CONFIG['username'] + except KeyError: self.skipTest('SKIPPING KEYSTONE-SPECIFIC FUNCTIONAL TESTS' + ' - NO CONFIG') - os_options = {'tenant_name': account} + return swiftclient.Connection( - self.auth_url, username, password, auth_version=self.auth_version, - os_options=os_options) + self.auth_url, username, self.password, + auth_version=self.auth_version, + os_options={'tenant_name': account}) class TestUsingKeystoneV3(TestFunctional): @@ -539,13 +499,14 @@ class TestUsingKeystoneV3(TestFunctional): account = username = password = project_domain = user_domain = None if self.auth_version != '3': self.skipTest('SKIPPING KEYSTONE-V3-SPECIFIC FUNCTIONAL TESTS') + try: - account = self.config.get('func_test', 'account4') - username = self.config.get('func_test', 'username4') - user_domain = self.config.get('func_test', 'domain4') - project_domain = self.config.get('func_test', 'domain4') - password = self.config.get('func_test', 'password4') - except Exception: + account = TEST_CONFIG['account4'] + username = TEST_CONFIG['username4'] + user_domain = TEST_CONFIG['domain4'] + project_domain = TEST_CONFIG['domain4'] + password = TEST_CONFIG['password4'] + except KeyError: self.skipTest('SKIPPING KEYSTONE-V3-SPECIFIC FUNCTIONAL TESTS' + ' - NO CONFIG')