diff --git a/tests/functional/queues/v1/test_flavor.py b/tests/functional/queues/v1/test_flavor.py new file mode 100644 index 00000000..c1f43244 --- /dev/null +++ b/tests/functional/queues/v1/test_flavor.py @@ -0,0 +1,27 @@ +# Copyright (c) 2014 Red Hat, Inc. +# +# 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 zaqarclient.tests.queues import flavor +from zaqarclient.transport import http + + +class QueuesV1_1FlavorHttpFunctionalTest( + flavor.QueuesV1_1FlavorFunctionalTest): + + is_functional = True + transport_cls = http.HttpTransport + url = 'http://127.0.0.1:8888/v1.1' + version = 1.1 diff --git a/tests/unit/queues/v1/test_flavor.py b/tests/unit/queues/v1/test_flavor.py new file mode 100644 index 00000000..385554d4 --- /dev/null +++ b/tests/unit/queues/v1/test_flavor.py @@ -0,0 +1,25 @@ +# Copyright (c) 2014 Red Hat, Inc. +# +# 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 zaqarclient.tests.queues import flavor +from zaqarclient.transport import http + + +class QueuesV1_1FlavorHttpUnitTest(flavor.QueuesV1_1FlavorUnitTest): + + transport_cls = http.HttpTransport + url = 'http://127.0.0.1:8888/v1.1' + version = 1.1 diff --git a/zaqarclient/common/decorators.py b/zaqarclient/common/decorators.py new file mode 100644 index 00000000..7b90a813 --- /dev/null +++ b/zaqarclient/common/decorators.py @@ -0,0 +1,67 @@ +# Copyright (c) 2013 Red Hat, Inc. +# +# 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 functools + +from zaqarclient import errors + + +def version(min_version, max_version=None): + min_version = float(min_version) + max_version = max_version and float(max_version) + + error_msg = ('Method %(name)s is supported from version %(min)s ' + 'to %(max)s') + + def method(meth): + @functools.wraps(meth) + def wrapper(self, *args, **kwargs): + if (self.api_version < min_version or + (max_version and self.api_version > max_version)): + msg = error_msg % dict(name=str(meth), + min=min_version, + max=max_version or 'latest') + raise errors.UnsupportedVersion(msg) + return meth(self, *args, **kwargs) + return wrapper + return method + + +def lazy_property(write=False, delete=True): + """Creates a lazy property. + + :param write: Whether this property is "writable" + :param delete: Whether this property can be deleted. + """ + + def wrapper(fn): + attr_name = '_lazy_' + fn.__name__ + + def getter(self): + if not hasattr(self, attr_name): + setattr(self, attr_name, fn(self)) + return getattr(self, attr_name) + + def setter(self, value): + setattr(self, attr_name, value) + + def deleter(self): + delattr(self, attr_name) + + return property(fget=getter, + fset=write and setter, + fdel=delete and deleter, + doc=fn.__doc__) + return wrapper diff --git a/zaqarclient/errors.py b/zaqarclient/errors.py index 251cc8fe..7b12a180 100644 --- a/zaqarclient/errors.py +++ b/zaqarclient/errors.py @@ -31,3 +31,7 @@ class DriverLoadFailure(ZaqarError): class InvalidOperation(ZaqarError): """Raised when attempted a non existent operation.""" + + +class UnsupportedVersion(ZaqarError): + """Raised if there is no endpoint which supports the requested version.""" diff --git a/zaqarclient/queues/v1/api.py b/zaqarclient/queues/v1/api.py index fa30f40c..314aedf5 100644 --- a/zaqarclient/queues/v1/api.py +++ b/zaqarclient/queues/v1/api.py @@ -172,6 +172,24 @@ class V1(api.Api): } }, + 'flavor_create': { + 'ref': 'flavors/{flavor_name}', + 'method': 'PUT', + 'required': ['flavor_name'], + 'properties': { + 'flavor_name': {'type': 'string'}, + } + }, + + 'flavor_delete': { + 'ref': 'flavors/{flavor_name}', + 'method': 'DELETE', + 'required': ['flavor_name'], + 'properties': { + 'flavor_name': {'type': 'string'}, + } + }, + 'claim_create': { 'ref': 'queues/{queue_name}/claims', 'method': 'POST', diff --git a/zaqarclient/queues/v1/client.py b/zaqarclient/queues/v1/client.py index a5c86c3c..7f630aa4 100644 --- a/zaqarclient/queues/v1/client.py +++ b/zaqarclient/queues/v1/client.py @@ -16,7 +16,9 @@ import uuid import warnings +from zaqarclient.common import decorators from zaqarclient.queues.v1 import core +from zaqarclient.queues.v1 import flavor from zaqarclient.queues.v1 import iterator from zaqarclient.queues.v1 import pool from zaqarclient.queues.v1 import queues @@ -136,6 +138,18 @@ class Client(object): """ return pool.Pool(self, ref, **kwargs) + @decorators.version(min_version=1.1) + def flavor(self, ref, **kwargs): + """Returns a flavor instance + + :param ref: Flavor's reference name. + :type ref: `six.text_type` + + :returns: A flavor instance + :rtype: `flavor.Flavor` + """ + return flavor.Flavor(self, ref, **kwargs) + def health(self): """Gets the health status of Zaqar server.""" req, trans = self._request_and_transport() diff --git a/zaqarclient/queues/v1/core.py b/zaqarclient/queues/v1/core.py index 7061233b..ab25ea85 100644 --- a/zaqarclient/queues/v1/core.py +++ b/zaqarclient/queues/v1/core.py @@ -433,6 +433,41 @@ def pool_delete(transport, request, pool_name): transport.send(request) +def flavor_create(transport, request, name, flavor_data): + """Creates a flavor called `name` + + :param transport: Transport instance to use + :type transport: `transport.base.Transport` + :param request: Request instance ready to be sent. + :type request: `transport.request.Request` + :param name: Flavor reference name. + :type name: `six.text_type` + :param flavor_data: Flavor's properties, i.e: pool, capabilities. + :type flavor_data: `dict` + """ + + request.operation = 'flavor_create' + request.params['flavor_name'] = name + request.content = json.dumps(flavor_data) + transport.send(request) + + +def flavor_delete(transport, request, name): + """Deletes the flavor `name` + + :param transport: Transport instance to use + :type transport: `transport.base.Transport` + :param request: Request instance ready to be sent. + :type request: `transport.request.Request` + :param name: Flavor reference name. + :type name: `six.text_type` + """ + + request.operation = 'flavor_delete' + request.params['flavor_name'] = name + transport.send(request) + + def health(transport, request, callback=None): """Check the health of web head for load balancing diff --git a/zaqarclient/queues/v1/flavor.py b/zaqarclient/queues/v1/flavor.py new file mode 100644 index 00000000..c7a8ad61 --- /dev/null +++ b/zaqarclient/queues/v1/flavor.py @@ -0,0 +1,48 @@ +# Copyright (c) 2014 Red Hat, Inc. +# +# 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 zaqarclient.queues.v1 import core + + +class Flavor(object): + + def __init__(self, client, name, + pool, auto_create=True, **capabilities): + self.client = client + + self.name = name + self.pool = pool + self.capabilities = capabilities + + if auto_create: + self.ensure_exists() + + def ensure_exists(self): + """Ensures pool exists + + This method is not race safe, + the pool could've been deleted + right after it was called. + """ + req, trans = self.client._request_and_transport() + + data = {'pool': self.pool, + 'capabilities': self.capabilities} + + core.flavor_create(trans, req, self.name, data) + + def delete(self): + req, trans = self.client._request_and_transport() + core.flavor_delete(trans, req, self.name) diff --git a/zaqarclient/tests/queues/flavor.py b/zaqarclient/tests/queues/flavor.py new file mode 100644 index 00000000..18a42015 --- /dev/null +++ b/zaqarclient/tests/queues/flavor.py @@ -0,0 +1,85 @@ +# Copyright (c) 2014 Red Hat, Inc. +# +# 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 + +from zaqarclient.tests.queues import base +from zaqarclient.transport import response + + +class QueuesV1_1FlavorUnitTest(base.QueuesTestBase): + + url = 'http://127.0.0.1:8888/v1.1' + version = 1.1 + + def test_flavor_create(self): + flavor_data = {'pool': 'stomach'} + + with mock.patch.object(self.transport, 'send', + autospec=True) as send_method: + + resp = response.Response(None, None) + send_method.return_value = resp + + # NOTE(flaper87): This will call + # ensure exists in the client instance + # since auto_create's default is True + flavor = self.client.flavor('tasty', **flavor_data) + self.assertEqual(flavor.name, 'tasty') + self.assertEqual(flavor.pool, 'stomach') + + def test_flavor_delete(self): + flavor_data = {'pool': 'stomach'} + + with mock.patch.object(self.transport, 'send', + autospec=True) as send_method: + + resp = response.Response(None, None) + send_method.return_value = resp + + # NOTE(flaper87): This will call + # ensure exists in the client instance + # since auto_create's default is True + flavor = self.client.flavor('tasty', **flavor_data) + flavor.delete() + + # NOTE(flaper87): Nothing to assert here, + # just checking our way down to the transport + # doesn't crash. + + +class QueuesV1_1FlavorFunctionalTest(base.QueuesTestBase): + + url = 'http://127.0.0.1:8888/v1.1' + version = 1.1 + + def test_flavor_create(self): + pool_data = {'uri': 'sqlite://', + 'weight': 10} + self.client.pool('stomach', **pool_data) + + flavor_data = {'pool': 'stomach'} + flavor = self.client.flavor('tasty', **flavor_data) + self.assertEqual(flavor.name, 'tasty') + self.assertEqual(flavor.pool, 'stomach') + + def test_flavor_delete(self): + pool_data = {'uri': 'sqlite://', + 'weight': 10} + self.client.pool('stomach', **pool_data) + + flavor_data = {'pool': 'stomach'} + flavor = self.client.flavor('tasty', **flavor_data) + flavor.delete()