From 4f7980840717c2b5d87219dfdb43ad3327d1c288 Mon Sep 17 00:00:00 2001 From: smile-luobin <luobin_smile@163.com> Date: Sun, 14 Feb 2016 07:13:03 +0000 Subject: [PATCH] Implement swift bank plugin A basic implement swift bank plugin Change-Id: I7c809aed46b225e3de5d298d7c1bb650fab7a139 Closes-Bug: #1545384 --- requirements.txt | 1 + smaug/common/config.py | 10 + smaug/exception.py | 28 ++ smaug/services/protection/bank_plugin.py | 4 +- .../protection/bank_plugins/__init__.py | 0 .../bank_plugins/swift_bank_plugin.py | 271 ++++++++++++++++++ .../unit/protection/fake_swift_client.py | 99 +++++++ .../unit/protection/test_swift_bank_plugin.py | 104 +++++++ test-requirements.txt | 1 + 9 files changed, 516 insertions(+), 2 deletions(-) create mode 100644 smaug/services/protection/bank_plugins/__init__.py create mode 100644 smaug/services/protection/bank_plugins/swift_bank_plugin.py create mode 100644 smaug/tests/unit/protection/fake_swift_client.py create mode 100644 smaug/tests/unit/protection/test_swift_bank_plugin.py diff --git a/requirements.txt b/requirements.txt index 5120674d..9151b94d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,3 +29,4 @@ sqlalchemy-migrate>=0.9.6 stevedore>=1.5.0 # Apache-2.0 WebOb>=1.2.3 oslo.i18n>=1.5.0 # Apache-2.0 +python-swiftclient>=2.2.0 # Apache-2.0 diff --git a/smaug/common/config.py b/smaug/common/config.py index f17ab9d4..01e5876d 100644 --- a/smaug/common/config.py +++ b/smaug/common/config.py @@ -70,6 +70,16 @@ global_opts = [ choices=['noauth', 'keystone'], help='The strategy to use for auth. Supports noauth or ' 'keystone.'), + cfg.IntOpt('lease_renew_window', + default=120, + help='period for bank lease, in seconds, ' + 'between bank lease client renew the lease'), + cfg.IntOpt('lease_expire_window', + default=600, + help='expired_window for bank lease, in seconds'), + cfg.IntOpt('lease_validity_window', + default=100, + help='validity_window for bank lease, in seconds'), ] CONF.register_opts(global_opts) diff --git a/smaug/exception.py b/smaug/exception.py index c8065906..b3c894aa 100644 --- a/smaug/exception.py +++ b/smaug/exception.py @@ -233,3 +233,31 @@ class ProviderNotFound(NotFound): class CheckpointNotFound(NotFound): message = _("Checkpoint %(checkpoint_id)s could" " not be found.") + + +class BankCreateObjectFailed(SmaugException): + message = _("Create Object in Bank Failed: %(reason)s") + + +class BankUpdateObjectFailed(SmaugException): + message = _("Update Object %(key)s in Bank Failed: %(reason)s") + + +class BankDeleteObjectFailed(SmaugException): + message = _("Delete Object %(key)s in Bank Failed: %(reason)s") + + +class BankGetObjectFailed(SmaugException): + message = _("Get Object %(key)s in Bank Failed: %(reason)s") + + +class BankListObjectsFailed(SmaugException): + message = _("Get Object in Bank Failed: %(reason)s") + + +class AcquireLeaseFailed(SmaugException): + message = _("Acquire Lease in Failed: %(reason)s") + + +class CreateContainerFailed(SmaugException): + message = _("Create Container in Bank Failed: %(reason)s") diff --git a/smaug/services/protection/bank_plugin.py b/smaug/services/protection/bank_plugin.py index 66b40866..e4b6be7b 100644 --- a/smaug/services/protection/bank_plugin.py +++ b/smaug/services/protection/bank_plugin.py @@ -28,12 +28,12 @@ LOG = logging.getLogger(__name__) @six.add_metaclass(abc.ABCMeta) class LeasePlugin(object): @abc.abstractmethod - def acquire_lease(self, owner_id): + def acquire_lease(self): # TODO(wangliuan) pass @abc.abstractmethod - def renew_lease(self, owner_id): + def renew_lease(self): # TODO(wangliuan) pass diff --git a/smaug/services/protection/bank_plugins/__init__.py b/smaug/services/protection/bank_plugins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/smaug/services/protection/bank_plugins/swift_bank_plugin.py b/smaug/services/protection/bank_plugins/swift_bank_plugin.py new file mode 100644 index 00000000..3b7e5aac --- /dev/null +++ b/smaug/services/protection/bank_plugins/swift_bank_plugin.py @@ -0,0 +1,271 @@ +# 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 oslo_config import cfg +from oslo_log import log as logging +from oslo_service import loopingcall +from smaug import exception +from smaug.i18n import _, _LE +from smaug.services.protection.bank_plugin import BankPlugin +from smaug.services.protection.bank_plugin import LeasePlugin +from swiftclient import client as swift +from swiftclient import ClientException +import time +import uuid + +swift_client_opts = [ + cfg.StrOpt('bank_swift_url', + help='The URL of the Swift endpoint'), + cfg.StrOpt('bank_swift_auth_url', + help='The URL of the Keystone endpoint'), + cfg.StrOpt('bank_swift_auth', + default='single_user', + help='Swift authentication mechanism'), + cfg.StrOpt('bank_swift_auth_version', + default='1', + help='Swift authentication version. ' + 'Specify "1" for auth 1.0, or "2" for auth 2.0'), + cfg.StrOpt('bank_swift_tenant_name', + help='Swift tenant/account name. ' + 'Required when connecting to an auth 2.0 system'), + cfg.StrOpt('bank_swift_user', + help='Swift user name'), + cfg.StrOpt('bank_swift_key', + help='Swift key for authentication'), + cfg.IntOpt('bank_swift_retry_attempts', + default=3, + help='The number of retries to make for ' + 'Swift operations'), + cfg.IntOpt('bank_swift_retry_backoff', + default=2, + help='The backoff time in seconds ' + 'between Swift retries'), + cfg.StrOpt('bank_swift_ca_cert_file', + help='Location of the CA certificate file ' + 'to use for swift client requests.'), + cfg.BoolOpt('bank_swift_auth_insecure', + default=False, + help='Bypass verification of server certificate when ' + 'making SSL connection to Swift.'), +] + +CONF = cfg.CONF +CONF.register_opts(swift_client_opts, "swift_client") +LOG = logging.getLogger(__name__) + + +class SwiftConnectionFailed(exception.SmaugException): + message = _("Connection to swift failed: %(reason)s") + + +class SwiftBankPlugin(BankPlugin, LeasePlugin): + def __init__(self, context, object_container): + super(BankPlugin, self).__init__() + self.context = context + self.swift_retry_attempts = CONF.swift_client.bank_swift_retry_attempts + self.swift_retry_backoff = CONF.swift_client.bank_swift_retry_backoff + self.swift_auth_insecure = CONF.swift_client.bank_swift_auth_insecure + self.swift_ca_cert_file = CONF.swift_client.bank_swift_ca_cert_file + self.lease_expire_window = CONF.lease_expire_window + self.lease_renew_window = CONF.lease_renew_window + # TODO(luobin): + # init lease_validity_window + # according to lease_renew_window if not configured + self.lease_validity_window = CONF.lease_validity_window + + # TODO(luobin): create a uuid of this bank_plugin + self.owner_id = str(uuid.uuid4()) + self.lease_expire_time = 0 + self.bank_leases_container = "leases" + self.bank_object_container = object_container + self.connection = self._setup_connection() + + # create container + try: + self._put_container(self.bank_object_container) + self._put_container(self.bank_leases_container) + except SwiftConnectionFailed as err: + LOG.error(_LE("bank plugin create container failed.")) + raise exception.CreateContainerFailed(reason=err) + + # acquire lease + try: + self.acquire_lease() + except exception.AcquireLeaseFailed as err: + LOG.error(_LE("bank plugin acquire lease failed.")) + raise err + + # start renew lease + renew_lease_loop = loopingcall.FixedIntervalLoopingCall( + self.renew_lease) + renew_lease_loop.start(interval=self.lease_renew_window, + initial_delay=self.lease_renew_window) + + def _setup_connection(self): + if CONF.swift_client.bank_swift_auth == "single_user": + connection = swift.Connection( + authurl=CONF.swift_client.bank_swift_auth_url, + auth_version=CONF.swift_client.bank_swift_auth_version, + tenant_name=CONF.swift_client.bank_swift_tenant_name, + user=CONF.swift_client.bank_swift_user, + key=CONF.swift_client.bank_swift_key, + retries=self.swift_retry_attempts, + starting_backoff=self.swift_retry_backoff, + insecure=self.swift_auth_insecure, + cacert=self.swift_ca_cert_file) + else: + connection = swift.Connection( + preauthurl=CONF.swift_client.bank_swift_url, + preauthtoken=self.context.auth_token, + retries=self.swift_retry_attempts, + starting_backoff=self.swift_retry_backoff, + insecure=self.swift_auth_insecure, + cacert=self.swift_ca_cert_file) + return connection + + def create_object(self, key, value): + try: + self._put_object(container=self.bank_object_container, + obj=key, + contents=value) + except SwiftConnectionFailed as err: + LOG.error(_LE("create object failed, err: %s."), err) + raise exception.BankCreateObjectFailed(reasone=err, + key=key) + + def update_object(self, key, value): + try: + self._put_object(container=self.bank_object_container, + obj=key, + contents=value) + except SwiftConnectionFailed as err: + LOG.error(_LE("update object failed, err: %s."), err) + raise exception.BankUpdateObjectFailed(reasone=err, + key=key) + + def delete_object(self, key): + try: + self._delete_object(container=self.bank_object_container, + obj=key) + except SwiftConnectionFailed as err: + LOG.error(_LE("delete object failed, err: %s."), err) + raise exception.BankDeleteObjectFailed(reasone=err, + key=key) + + def get_object(self, key): + try: + return self._get_object(container=self.bank_object_container, + obj=key) + except SwiftConnectionFailed as err: + LOG.error(_LE("get object failed, err: %s."), err) + raise exception.BankGetObjectFailed(reasone=err, + key=key) + + def list_objects(self, prefix=None, limit=None, marker=None): + object_names = [] + try: + body = self._get_container(container=self.bank_object_container, + prefix=prefix, + limit=limit, + marker=marker) + except SwiftConnectionFailed as err: + LOG.error(_LE("list objects failed, err: %s."), err) + raise exception.BankListObjectsFailed(reasone=err) + for obj in body: + if obj.get("name"): + object_names.append(obj.get("name")) + return object_names + + def acquire_lease(self): + container = self.bank_leases_container + obj = self.owner_id + contents = self.owner_id + headers = {'X-Delete-After': self.lease_expire_window} + try: + self._put_object(container=container, + obj=obj, + contents=contents, + headers=headers) + self.lease_expire_time = long( + time.time()) + self.lease_expire_window + except SwiftConnectionFailed as err: + LOG.error(_LE("acquire lease failed, err:%s."), err) + raise exception.AcquireLeaseFailed(reason=err) + + def renew_lease(self): + container = self.bank_leases_container + obj = self.owner_id + headers = {'X-Delete-After': self.lease_expire_window} + try: + self._post_object(container=container, + obj=obj, + headers=headers) + self.lease_expire_time = long( + time.time()) + self.lease_expire_window + except SwiftConnectionFailed as err: + LOG.error(_LE("acquire lease failed, err:%s."), err) + + def check_lease_validity(self): + if (self.lease_expire_time - long(time.time()) >= + self.lease_validity_window): + return True + else: + return False + + def _put_object(self, container, obj, contents, headers=None): + try: + self.connection.put_object(container=container, + obj=obj, + contents=contents, + headers=headers) + except ClientException as err: + raise SwiftConnectionFailed(reason=err) + + def _get_object(self, container, obj): + try: + (_resp, body) = self.connection.get_object(container=container, + obj=obj) + return body + except ClientException as err: + raise SwiftConnectionFailed(reason=err) + + def _post_object(self, container, obj, headers): + try: + self.connection.post_object(container=container, + obj=obj, + headers=headers) + except ClientException as err: + raise SwiftConnectionFailed(reason=err) + + def _delete_object(self, container, obj): + try: + self.connection.delete_object(container=container, + obj=obj) + except ClientException as err: + raise SwiftConnectionFailed(reason=err) + + def _put_container(self, container): + try: + self.connection.put_container(container=container) + except ClientException as err: + raise SwiftConnectionFailed(reason=err) + + def _get_container(self, container, prefix=None, limit=None, marker=None): + try: + (_resp, body) = self.connection.get_container( + container=container, + prefix=prefix, + limit=limit, + marker=marker) + return body + except ClientException as err: + raise SwiftConnectionFailed(reason=err) diff --git a/smaug/tests/unit/protection/fake_swift_client.py b/smaug/tests/unit/protection/fake_swift_client.py new file mode 100644 index 00000000..8cc332bb --- /dev/null +++ b/smaug/tests/unit/protection/fake_swift_client.py @@ -0,0 +1,99 @@ +# 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 swiftclient import ClientException +import tempfile + + +class FakeSwiftClient(object): + def __init__(self, *args, **kwargs): + pass + + @classmethod + def connection(cls, *args, **kargs): + return FakeSwiftConnection() + + +class FakeSwiftConnection(object): + def __init__(self, *args, **kwargs): + self.swiftdir = tempfile.mkdtemp() + + def put_container(self, container): + container_dir = self.swiftdir + "/" + container + if os.path.exists(container_dir) is True: + return + else: + os.makedirs(container_dir) + + def get_container(self, container, prefix, limit, marker): + container_dir = self.swiftdir + "/" + container + body = [] + if prefix: + objects_dir = container_dir + "/" + prefix + else: + objects_dir = container_dir + for f in os.listdir(objects_dir): + if os.path.isfile(objects_dir + "/" + f): + body.append({"name": f}) + else: + body.append({"subdir": f}) + return None, body + + def put_object(self, container, obj, contents, headers=None): + container_dir = self.swiftdir + "/" + container + obj_file = container_dir + "/" + obj + obj_dir = obj_file[0:obj_file.rfind("/")] + if os.path.exists(container_dir) is True: + if os.path.exists(obj_dir) is False: + os.makedirs(obj_dir) + with open(obj_file, "w") as f: + f.write(contents) + return + else: + raise ClientException("error_container") + + def get_object(self, container, obj): + container_dir = self.swiftdir + "/" + container + obj_file = container_dir + "/" + obj + if os.path.exists(container_dir) is True: + if os.path.exists(obj_file) is True: + with open(obj_file, "r") as f: + return None, f.read() + else: + raise ClientException("error_obj") + else: + raise ClientException("error_container") + + def delete_object(self, container, obj): + container_dir = self.swiftdir + "/" + container + obj_file = container_dir + "/" + obj + if os.path.exists(container_dir) is True: + if os.path.exists(obj_file) is True: + os.remove(obj_file) + else: + raise ClientException("error_obj") + else: + raise ClientException("error_container") + + def update_object(self, container, obj, contents): + container_dir = self.swiftdir + "/" + container + obj_file = container_dir + "/" + obj + if os.path.exists(container_dir) is True: + if os.path.exists(obj_file) is True: + with open(obj_file, "w") as f: + f.write(contents) + return + else: + raise ClientException("error_obj") + else: + raise ClientException("error_container") diff --git a/smaug/tests/unit/protection/test_swift_bank_plugin.py b/smaug/tests/unit/protection/test_swift_bank_plugin.py new file mode 100644 index 00000000..6e7a0c98 --- /dev/null +++ b/smaug/tests/unit/protection/test_swift_bank_plugin.py @@ -0,0 +1,104 @@ +# 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 mock +import os +from oslo_config import cfg +from oslo_utils import importutils +from smaug.tests import base +from smaug.tests.unit.protection.fake_swift_client import FakeSwiftClient +from swiftclient import client as swift +import time + +CONF = cfg.CONF + + +class FakeConf(object): + def __init__(self): + self.lease_expire_window = 600 + self.lease_renew_window = 120 + self.lease_validity_window = 100 + + +class SwiftBankPluginTest(base.TestCase): + def setUp(self): + super(SwiftBankPluginTest, self).setUp() + self.conf = FakeConf() + self.fake_connection = FakeSwiftClient.connection() + import_str = "smaug.services.protection.bank_plugins." \ + "swift_bank_plugin.SwiftBankPlugin" + self.object_container = "objects" + swift_bank_plugin_cls = importutils.import_class( + import_str=import_str) + swift.Connection = mock.MagicMock() + swift.Connection.return_value = self.fake_connection + self.swift_bank_plugin = swift_bank_plugin_cls(None, + self.object_container) + + def test_acquire_lease(self): + self.swift_bank_plugin.acquire_lease() + expire_time = long(time.time()) + self.conf.lease_expire_window + self.assertEqual(self.swift_bank_plugin.lease_expire_time, expire_time) + + def test_renew_lease(self): + self.swift_bank_plugin.acquire_lease() + expire_time = long(time.time()) + self.conf.lease_expire_window + self.assertEqual(self.swift_bank_plugin.lease_expire_time, expire_time) + time.sleep(5) + self.swift_bank_plugin.acquire_lease() + expire_time = long(time.time()) + self.conf.lease_expire_window + self.assertEqual(self.swift_bank_plugin.lease_expire_time, expire_time) + + def test_check_lease_validity(self): + self.swift_bank_plugin.acquire_lease() + expire_time = long(time.time()) + self.conf.lease_expire_window + self.assertEqual(self.swift_bank_plugin.lease_expire_time, expire_time) + is_valid = self.swift_bank_plugin.check_lease_validity() + self.assertEqual(is_valid, True) + + def test_create_object(self): + self.swift_bank_plugin.create_object("key-1", "value-1") + object_file = os.path.join(self.fake_connection.swiftdir, + self.object_container, + "key-1") + with open(object_file, "r") as f: + contents = f.read() + self.assertEqual(contents, "value-1") + + def test_delete_object(self): + self.swift_bank_plugin.create_object("key", "value") + self.swift_bank_plugin.delete_object("key") + object_file = os.path.join(self.fake_connection.swiftdir, + self.object_container, + "key") + self.assertEqual(os.path.isfile(object_file), False) + + def test_get_object(self): + self.swift_bank_plugin.create_object("key", "value") + value = self.swift_bank_plugin.get_object("key") + self.assertEqual(value, "value") + + def test_list_objects(self): + self.swift_bank_plugin.create_object("key-1", "value-1") + self.swift_bank_plugin.create_object("key-2", "value-2") + objects = self.swift_bank_plugin.list_objects(prefix=None) + self.assertEqual(len(objects), 2) + + def test_update_object(self): + self.swift_bank_plugin.create_object("key-1", "value-1") + self.swift_bank_plugin.update_object("key-1", "value-2") + object_file = os.path.join(self.fake_connection.swiftdir, + self.object_container, + "key-1") + with open(object_file, "r") as f: + contents = f.read() + self.assertEqual(contents, "value-2") diff --git a/test-requirements.txt b/test-requirements.txt index 0ecbfef8..78e691a9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,3 +13,4 @@ oslotest>=1.10.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=1.4.0 +python-swiftclient>=2.2.0 # Apache-2.0