diff --git a/doc/source/code/connection.py b/doc/source/code/connection.py new file mode 100644 index 00000000..e4d6f4dd --- /dev/null +++ b/doc/source/code/connection.py @@ -0,0 +1,6 @@ +from openstack import connection +conn = connection.Connection(auth_url="http://openstack:5000/v3", + project_name="big_project", + user_name="SDK_user", + password="Super5ecretPassw0rd") + diff --git a/doc/source/highlevel/object_store.rst b/doc/source/highlevel/object_store.rst new file mode 100644 index 00000000..8f46216c --- /dev/null +++ b/doc/source/highlevel/object_store.rst @@ -0,0 +1,15 @@ +Object Store API +================ + +For details on how to use this API, see :doc:`/userguides/object_store` + +.. automodule:: openstack.object_store.v1._proxy + +The Object Store Class +---------------------- + +The Object Store high-level interface is exposed as the ``object_store`` +object on :class:`~openstack.connection.Connection` objects. + +.. autoclass:: openstack.object_store.v1._proxy.Proxy + :members: diff --git a/doc/source/index.rst b/doc/source/index.rst index 9ec7fe2b..c2b97e2c 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -11,6 +11,22 @@ Welcome! contributing glossary +User Guides +----------- + +.. toctree:: + :maxdepth: 1 + + userguides/object_store + +High-Level Interface +-------------------- + +.. toctree:: + :maxdepth: 1 + + highlevel/object_store + Resource Level Classes ---------------------- diff --git a/doc/source/userguides/object_store.rst b/doc/source/userguides/object_store.rst new file mode 100644 index 00000000..6c510ae9 --- /dev/null +++ b/doc/source/userguides/object_store.rst @@ -0,0 +1,224 @@ +Using the OpenStack Object Store API +==================================== + +The Object Store API operates on two things: containers and objects. + +Before working with the ``object_store`` API, you'll need to obtain a +:class:`~openstack.connection.Connection` object like so. + +.. literalinclude:: /code/connection.py + +Working with Containers +----------------------- + +Listing Containers +****************** + +To list existing containers, use the +:meth:`~openstack.object_store.v1._proxy.Proxy.containers` method. :: + + >>> for cont in conn.object_store.containers(): + ... print cont + ... + Container: {u'count': 5, u'bytes': 500, u'name': u'my container'} + Container: {u'count': 0, u'bytes': 0, u'name': u'empty container'} + Container: {u'count': 100, u'bytes': 1000000, u'name': u'another container'} + +The ``containers`` method returns a generator which yields +:class:`~openstack.object_store.v1.container.Container` objects. It handles +pagination for you, which can be adjusted via the ``limit`` argument. +By default, the ``containers`` method will yield as many containers as the +service will return, and it will continue requesting until it receives +no more. :: + + >>> for cont in conn.object_store.containers(limit=500): + ... print(cont) + ... + <500 Containers> + ... another request transparently made to the Object Store service + <500 more Containers> + ... + +Creating Containers +******************* + +To create a container, use the +:meth:`~openstack.object_store.v1._proxy.Proxy.create_container` method. :: + + >>> cont = conn.object_store.create_container("new container".decode("utf8")) + >>> cont + Container: {'name': u'new container'} + +You can also create containers by passing in a +:class:`~openstack.object_store.v1.container.Container` resource. This is +helpful if you wanted to create another container which uses the same metadata +settings that another container has. :: + + >>> from copy import copy + >>> print cont.name, cont.read_ACL + MyContainer .r:mysite.com + >>> new_cont = copy(cont) + >>> new_cont.name = "copied container" + >>> conn.object_store.create_container(new_cont) + Container: {u'name': 'copied container', 'x-container-read': '.r:mysite.com'} + +Working with Container Metadata +******************************* + +To get the metadata for a container, use the +:meth:`~openstack.object_store.v1._proxy.Proxy.get_container_metadata` method. +This method either takes the name of a container, or a +:class:`~openstack.object_store.v1.container.Container` object, and it returns +a `Container` object with all of its metadata attributes set. :: + + >>> cont = conn.object_store.get_container_metadata("new container".decode("utf8")) + Container: {'content-length': '0', 'x-container-object-count': '0', + 'name': u'new container', 'accept-ranges': 'bytes', + 'x-trans-id': 'tx22c5de63466e4c05bb104-0054740c39', + 'date': 'Tue, 25 Nov 2014 04:57:29 GMT', + 'x-timestamp': '1416889793.23520', 'x-container-read': '.r:mysite.com', + 'x-container-bytes-used': '0', 'content-type': 'text/plain; charset=utf-8'} + +To set the metadata for a container, use the +:meth:`~openstack.object_store.v1._proxy.Proxy.set_container_metadata` method. +This method takes a :class:`~openstack.object_store.v1.container.Container` +object. For example, to grant another user write access to this container, +you can set the +:attr:`~openstack.object_store.v1.container.Container.write_ACL` on a +resource and pass it to `set_container_metadata`. :: + + >>> cont.write_ACL = "big_project:another_user" + >>> conn.object_store.set_container_metadata(cont) + Container: {'content-length': '0', 'x-container-object-count': '0', + 'name': u'my new container', 'accept-ranges': 'bytes', + 'x-trans-id': 'txc3ee751f971d41de9e9f4-0054740ec1', + 'date': 'Tue, 25 Nov 2014 05:08:17 GMT', + 'x-timestamp': '1416889793.23520', 'x-container-read': '.r:mysite.com', + 'x-container-bytes-used': '0', 'content-type': 'text/plain; charset=utf-8', + 'x-container-write': 'big_project:another_user'} + +Working with Objects +-------------------- + +Objects are held in containers. From an API standpoint, you work with +them using similarly named methods, typically with an additional argument +to specify their container. + +Listing Objects +*************** + +To list the objects that exist in a container, use the +:meth:`~openstack.object_store.v1._proxy.Proxy.objects` method. + +If you have a :class:`~openstack.object_store.v1.container.Container` +object, you can pass it to ``objects``. :: + + >>> print cont.name + pictures + >>> for obj in conn.object_store.objects(cont): + ... print obj + ... + Object: {u'hash': u'0522d4ccdf9956badcb15c4087a0c4cb', + u'name': u'pictures/selfie.jpg', u'bytes': 15744, + 'last-modified': u'2014-10-31T06:33:36.618640', + u'last_modified': u'2014-10-31T06:33:36.618640', + u'content_type': u'image/jpeg', 'container': u'pictures', + 'content-type': u'image/jpeg'} + ... + +Similar to the :meth:`~openstack.object_store.v1._proxy.Proxy.containers` +method, ``objects`` returns a generator which yields +:class:`~openstack.object_store.v1.obj.Object` objects stored in the +container. It also handles pagination for you, which you can adjust +with the ``limit`` parameter, otherwise making each request for the maximum +that your Object Store will return. + +If you have the name of a container instead of an object, you can also +pass that to the ``objects`` method. :: + + >>> for obj in conn.object_store.objects("pictures".decode("utf8"), + limit=100): + ... print obj + ... + <100 Objects> + ... another request transparently made to the Object Store service + <100 more Objects> + +Getting Object Data +******************* + +Once you have an :class:`~openstack.object_store.v1.obj.Object`, you get +the data stored inside of it with the +:meth:`~openstack.object_store.v1._proxy.Proxy.get_object_data` method. :: + + >>> print ob.name + message.txt + >>> data = conn.object_store.get_object_data(ob) + >>> print data + Hello, world! + +Additionally, if you want to save the object to disk, the +:meth:`~openstack.object_store.v1._proxy.Proxy.save_object` convenience +method takes an :class:`~openstack.object_store.v1.obj.Object` and a +``path`` to write the contents to. :: + + >>> conn.object_store.save_object(ob, "the_message.txt") + +Creating Objects +**************** + +Once you have data you'd like to store in the Object Store service, you use +the :meth:`~openstack.object_store.v1._proxy.Proxy.create_object` method. +This method takes the ``data`` to be stored, along with an ``obj`` and +``container``. The ``obj`` can either be the name of an object or an +:class:`~openstack.object_store.v1.obj.Object` instance, and ``container`` +can either be the name of a container or an +:class:`~openstack.object_store.v1.container.Container` instance. :: + + >>> hello = conn.object_store.create_object("Hello, world!", + "helloworld.txt".decode("utf8"), + "My Container".decode("utf8")) + >>> print hello + Object: {'content-length': '0', 'container': u'My Container', + 'name': u'helloworld.txt', + 'last-modified': 'Tue, 25 Nov 2014 17:39:29 GMT', + 'etag': '5eb63bbbe01eeed093cb22bb8f5acdc3', + 'x-trans-id': 'tx3035d41b03334aeaaf3dd-005474bed0', + 'date': 'Tue, 25 Nov 2014 17:39:28 GMT', + 'content-type': 'text/html; charset=UTF-8'} + +If you have an existing object and want to update its data, you can easily +do that by passing new ``data`` along with existing +:class:`~openstack.object_store.v1.obj.Object` and +:class:`~openstack.object_store.v1.container.Container` instances. :: + + >>> conn.object_store.create_object("Hola, mundo!", hello, cont) + +Working with Object Metadata +**************************** + +Working with metadata on objects is identical to how it's done with +containers. You use the +:meth:`~openstack.object_store.v1._proxy.Proxy.get_object_metadata` and +:meth:`~openstack.object_store.v1._proxy.Proxy.set_object_metadata` methods. + +The metadata attributes to be set can be found on the +:class:`~openstack.object_store.v1.obj.Object` object. :: + + >>> secret.delete_after = 300 + >>> secret = conn.object_store.set_object_metadata(secret) + +We set the :attr:`~openstack.object_store.obj.Object.delete_after` +value to 500 seconds, causing the object to be deleted in 300 seconds, +or five minutes. That attribute corresponds to the ``X-Delete-After`` +header value, which you can see is returned when we retreive the updated +metadata. :: + + >>> conn.object_store.get_object_metadata(ob) + Object: {'content-length': '11', 'container': u'Secret Container', + 'name': u'selfdestruct.txt', 'x-delete-after': 300, + 'accept-ranges': 'bytes', 'last-modified': 'Tue, 25 Nov 2014 17:50:45 GMT', + 'etag': '5eb63bbbe01eeed093cb22bb8f5acdc3', + 'x-timestamp': '1416937844.36805', + 'x-trans-id': 'tx5c3fd94adf7c4e1b8f334-005474c17b', + 'date': 'Tue, 25 Nov 2014 17:50:51 GMT', 'content-type': 'text/plain'} diff --git a/examples/object_store.py b/examples/object_store.py new file mode 100644 index 00000000..d46fb81f --- /dev/null +++ b/examples/object_store.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. + +from __future__ import print_function + +import glob +import os +import sys + +from examples import common +from openstack import connection + +CONTAINER_HEADER = ("Name{0}| Bytes Used{1}| " + "Num Objects".format(13 * " ", 1 * " ")) +CONTAINER_FORMAT = ("{0.name: <16} | {0.bytes: <10} | {0.count}") +OBJECT_HEADER = ("Name{0}| Bytes {1}| " + "Content-Type".format(27 * " ", 2 * " ")) +OBJECT_FORMAT = ("{0.name: <30} | {0.bytes: <7} | {0.content_type}") + + +def list_containers(conn): + print(CONTAINER_HEADER) + print("=" * len(CONTAINER_HEADER)) + for container in conn.object_store.containers(): + print(CONTAINER_FORMAT.format(container)) + + +def list_objects(conn, container): + print(OBJECT_HEADER) + print("=" * len(OBJECT_HEADER)) + for obj in conn.object_store.objects(container.decode("utf8")): + print(OBJECT_FORMAT.format(obj)) + + +def upload_directory(conn, directory, pattern): + """Upload a directory to object storage. + + Given an OpenStack connection, a directory, and a file glob pattern, + upload all files matching the pattern from that directory into a + container named after the directory containing the files. + """ + container_name = os.path.basename(os.path.realpath(directory)) + + container = conn.object_store.create_container( + container_name.decode("utf8")) + + for root, dirs, files in os.walk(directory): + for file in glob.iglob(os.path.join(root, pattern)): + with open(file, "rb") as f: + ob = conn.object_store.create_object(data=f.read(), + obj=file.decode("utf8"), + container=container) + print("Uploaded {0.name}".format(ob)) + + +def main(): + # Add on to the common parser with a few options of our own. + parser = common.option_parser() + + parser.add_argument("--list-containers", dest="list_containers", + action="store_true") + parser.add_argument("--list-objects", dest="container") + parser.add_argument("--upload-directory", dest="directory") + parser.add_argument("--pattern", dest="pattern") + + opts = parser.parse_args() + + args = { + 'auth_plugin': opts.auth_plugin, + 'auth_url': opts.auth_url, + 'project_name': opts.project_name, + 'domain_name': opts.domain_name, + 'project_domain_name': opts.project_domain_name, + 'user_domain_name': opts.user_domain_name, + 'user_name': opts.user_name, + 'password': opts.password, + 'verify': opts.verify, + 'token': opts.token, + } + conn = connection.Connection(**args) + + if opts.list_containers: + return list_containers(conn) + elif opts.container: + return list_objects(conn, opts.container) + elif opts.directory and opts.pattern: + return upload_directory(conn, opts.directory.decode("utf8"), + opts.pattern) + else: + print(parser.print_help()) + + return -1 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 025230cf..efadbe85 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -10,8 +10,176 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.object_store.v1 import container as _container +from openstack.object_store.v1 import obj as _obj + class Proxy(object): def __init__(self, session): self.session = session + + def get_account_metadata(self, container=None): + """Get metatdata for this account. + + :param container: The container to retreive metadata for. + :type container: + :class:`~openstack.object_store.v1.container.Container` + """ + # TODO(briancurtin): should just use Container.head directly? + if container is None: + container = _container.Container() + container.head(self.session) + return container + + def set_account_metadata(self, container): + """Set metatdata for this account. + + :param container: The container to set metadata for. + :type container: + :class:`~openstack.object_store.v1.container.Container` + """ + container.update(self.session) + return container + + def containers(self, limit=None, marker=None, **kwargs): + """Return a generator that yields the account's Container objects. + + :param int limit: Set the limit of how many containers to retrieve. + :param str marker: The name of the container to begin iterating from. + """ + return _container.Container.list(self.session, limit=limit, + marker=marker, **kwargs) + + def get_container_metadata(self, container): + """Get metatdata for a container. + + :param container: The container to retreive metadata for. + :type container: + :class:`~openstack.object_store.v1.container.Container` + """ + container = _container.Container.from_id(container) + # TODO(briancurtin): may want to check if the container has a + # name at this point. If it doesn't, this call will work but it's + # actually getting *account* metadata. + container.head(self.session) + return container + + def set_container_metadata(self, container): + """Set metatdata for a container. + + :param container: The container to set metadata for. + :type container: + :class:`~openstack.object_store.v1.container.Container` + """ + container.create(self.session) + return container + + def create_container(self, container): + """Create a container, + + :param container: A container name or object. + :type container: + :class:`~openstack.object_store.v1.container.Container` + """ + container = _container.Container.from_id(container) + container.create(self.session) + return container + + def delete_container(self, container): + """Delete a container. + + :param container: A container name or object. + :type container: + :class:`~openstack.object_store.v1.container.Container` + """ + container = _container.Container.from_id(container) + container.delete(self.session) + + def objects(self, container, limit=None, marker=None, **kwargs): + """Return a generator that yields the Container's objects. + + :param container: A container name or object. + :type container: + :class:`~openstack.object_store.v1.container.Container` + """ + container = _container.Container.from_id(container) + + objs = _obj.Object.list(self.session, limit=limit, marker=marker, + path_args={"container": container.name}, + **kwargs) + # TODO(briancurtin): Objects have to know their container at this + # point, otherwise further operations like getting their metadata + # or downloading them is a hassle because the end-user would have + # to maintain both the container and the object separately. + for ob in objs: + ob.container = container.name + yield ob + + def get_object_data(self, obj): + """Retreive the data contained inside an object. + + :param obj: The object to retreive. + :type obj: :class:`~openstack.object_store.v1.obj.Object` + """ + return obj.get(self.session) + + def save_object(self, obj, path): + """Save the data contained inside an object to disk. + + :param obj: The object to save to disk. + :type obj: :class:`~openstack.object_store.v1.obj.Object` + :param path str: Location to write the object contents. + """ + with open(path, "w") as out: + out.write(self.get_object_data(obj)) + + def create_object(self, data, obj, container=None, **kwargs): + """Create an object within the object store. + + :param data: The data to store. + :param obj: The name of the object to create, or an obj.Object + :type obj: :class:`~openstack.object_store.v1.obj.Object` + """ + obj = _obj.Object.from_id(obj) + + # If we were given an Object complete with an underlying Container, + # this attribute access will succeed. Otherwise we'll need to set + # a container value on `obj` out of the `container` value. + name = getattr(obj, "container") + if not name: + cnt = _container.Container.from_id(container) + obj.container = cnt.name + + obj.create(self.session, data) + return obj + + def copy_object(self): + """Copy an object.""" + raise NotImplementedError + + def delete_object(self, obj): + """Delete an object. + + :param obj: The object to delete. + :type obj: :class:`~openstack.object_store.v1.obj.Object` + """ + obj.delete(self.session) + + def get_object_metadata(self, obj): + """Get metatdata for an object. + + :param obj: The object to retreive metadata from. + :type obj: :class:`~openstack.object_store.v1.obj.Object` + """ + obj.head(self.session) + return obj + + def set_object_metadata(self, obj): + """Set metatdata for an object. + + :param obj: The object to set metadata for. + :type obj: :class:`~openstack.object_store.v1.obj.Object` + """ + obj.create(self.session) + return obj diff --git a/openstack/object_store/v1/container.py b/openstack/object_store/v1/container.py index faa44aa2..4962d1c1 100644 --- a/openstack/object_store/v1/container.py +++ b/openstack/object_store/v1/container.py @@ -96,7 +96,7 @@ class Container(resource.Resource): #: Content-Type header, if present. detect_content_type = resource.prop("x-detect-content-type", type=bool) #: In combination with Expect: 100-Continue, specify an - #: "If-None-Match: *" header to query whether the server already + #: "If-None-Match: \*" header to query whether the server already #: has a copy of the object before any data is sent. if_none_match = resource.prop("if-none-match") diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index 0b11fc21..2d1417e7 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -53,7 +53,7 @@ class Object(resource.Resource): #: See http://www.ietf.org/rfc/rfc2616.txt. if_match = resource.prop("if-match", type=dict) #: In combination with Expect: 100-Continue, specify an - #: "If-None-Match: *" header to query whether the server already + #: "If-None-Match: \*" header to query whether the server already #: has a copy of the object before any data is sent. if_none_match = resource.prop("if-none-match", type=dict) #: See http://www.ietf.org/rfc/rfc2616.txt. @@ -167,3 +167,19 @@ class Object(resource.Resource): headers=headers).content return resp + + def create(self, session, data=None): + """Create a remote resource from this instance.""" + if not self.allow_create: + raise exceptions.MethodNotSupported('create') + + url = utils.urljoin("", self.base_path % self, self.id) + + if data is not None: + resp = session.put(url, service=self.service, data=data, + accept="bytes").headers + else: + resp = session.post(url, service=self.service, data=None, + accept=None).headers + + self._attrs.update(resp) diff --git a/openstack/tests/object_store/v1/test_proxy.py b/openstack/tests/object_store/v1/test_proxy.py new file mode 100644 index 00000000..19c80d65 --- /dev/null +++ b/openstack/tests/object_store/v1/test_proxy.py @@ -0,0 +1,442 @@ +# 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 json + +import httpretty +import mock +import six + +from openstack.object_store.v1 import _proxy +from openstack.object_store.v1 import container +from openstack.object_store.v1 import obj +from openstack import session +from openstack.tests import base +from openstack.tests import fakes +from openstack.tests import test_proxy_base +from openstack import transport + + +class TestObjectStoreProxy(test_proxy_base.TestProxyBase): + + def setUp(self): + super(TestObjectStoreProxy, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + +class Test_account_metadata(TestObjectStoreProxy): + + def _test_container_object(self, method, verb): + container = mock.MagicMock() + + result = method(container) + + self.assertIs(result, container) + + getattr(container, verb).assert_called_once_with(self.session) + + def test_get_account_metadata(self): + self._test_container_object(self.proxy.get_account_metadata, "head") + + def test_set_account_metadata(self): + self._test_container_object(self.proxy.set_account_metadata, "update") + + @mock.patch("openstack.object_store.v1._proxy._container.Container") + def test_get_account_metadata_no_arg(self, mock_container): + created_container = mock.MagicMock() + mock_container.return_value = created_container + + self.proxy.get_account_metadata() + + mock_container.assert_called_once_with() + created_container.head.assert_called_once_with(self.session) + + +class Test_containers(TestObjectStoreProxy, base.TestTransportBase): + + TEST_URL = fakes.FakeAuthenticator.ENDPOINT + + def setUp(self): + super(Test_containers, self).setUp() + self.transport = transport.Transport(accept=transport.JSON) + self.auth = fakes.FakeAuthenticator() + self.session = session.Session(self.transport, self.auth) + + self.proxy = _proxy.Proxy(self.session) + + self.containers_body = [] + for i in range(3): + self.containers_body.append({six.text_type("name"): + six.text_type("container%d" % i)}) + + @httpretty.activate + def test_all_containers(self): + self.stub_url(httpretty.GET, + path=[container.Container.base_path], + responses=[httpretty.Response( + body=json.dumps(self.containers_body), + status=200, content_type="application/json"), + httpretty.Response(body=json.dumps([]), + status=200, content_type="application/json")]) + + count = 0 + for actual, expected in zip(self.proxy.containers(), + self.containers_body): + self.assertEqual(actual, expected) + count += 1 + self.assertEqual(count, len(self.containers_body)) + + @httpretty.activate + def test_containers_limited(self): + limit = len(self.containers_body) + 1 + limit_param = "?limit=%d" % limit + + self.stub_url(httpretty.GET, + path=[container.Container.base_path + limit_param], + json=self.containers_body) + + count = 0 + for actual, expected in zip(self.proxy.containers(limit=limit), + self.containers_body): + self.assertEqual(actual, expected) + count += 1 + + self.assertEqual(count, len(self.containers_body)) + # Since we've chosen a limit larger than the body, only one request + # should be made, so it should be the last one. + self.assertIn(limit_param, httpretty.last_request().path) + + @httpretty.activate + def test_containers_with_marker(self): + marker = six.text_type("container2") + marker_param = "?marker=%s" % marker + + self.stub_url(httpretty.GET, + path=[container.Container.base_path + marker_param], + json=self.containers_body) + + count = 0 + for actual, expected in zip(self.proxy.containers(marker=marker), + self.containers_body): + # Make sure the marker made it into the actual request. + self.assertIn(marker_param, httpretty.last_request().path) + self.assertEqual(actual, expected) + count += 1 + + self.assertEqual(count, len(self.containers_body)) + + # Since we have to make one request beyond the end, because no + # limit was provided, make sure the last container appears as + # the marker in this last request. + self.assertIn(self.containers_body[-1]["name"], + httpretty.last_request().path) + + +class Test_container_metadata(TestObjectStoreProxy): + + @mock.patch("openstack.resource.Resource.from_id") + def test_get_container_metadata_object(self, mock_fi): + container = mock.MagicMock() + mock_fi.return_value = container + + result = self.proxy.get_container_metadata(container) + + self.assertIs(result, container) + container.head.assert_called_once_with(self.session) + + @mock.patch("openstack.resource.Resource.from_id") + def test_get_container_metadata_name(self, mock_fi): + name = six.text_type("my_container") + created_container = mock.MagicMock() + created_container.name = name + mock_fi.return_value = created_container + + result = self.proxy.get_container_metadata(name) + + self.assertEqual(result.name, name) + created_container.head.assert_called_once_with(self.session) + + def test_set_container_metadata_object(self): + container = mock.MagicMock() + + result = self.proxy.set_container_metadata(container) + + self.assertIs(result, container) + container.create.assert_called_once_with(self.session) + + +class Test_create_container(TestObjectStoreProxy): + + @mock.patch("openstack.resource.Resource.from_id") + def test_container_object(self, mock_fi): + container = mock.MagicMock() + mock_fi.return_value = container + + result = self.proxy.create_container(container) + + self.assertIs(result, container) + container.create.assert_called_once_with(self.session) + + @mock.patch("openstack.resource.Resource.from_id") + def test_container_name(self, mock_fi): + name = six.text_type("my_container") + created_container = mock.MagicMock() + created_container.name = name + mock_fi.return_value = created_container + + result = self.proxy.create_container(name) + + self.assertEqual(result.name, name) + created_container.create.assert_called_once_with(self.session) + + +class Test_delete_container(TestObjectStoreProxy): + + @mock.patch("openstack.resource.Resource.from_id") + def test_container_object(self, mock_fi): + container = mock.MagicMock() + mock_fi.return_value = container + + result = self.proxy.delete_container(container) + + self.assertIsNone(result) + container.delete.assert_called_once_with(self.session) + + @mock.patch("openstack.resource.Resource.from_id") + def test_container_name(self, mock_fi): + name = six.text_type("my_container") + created_container = mock.MagicMock() + created_container.name = name + mock_fi.return_value = created_container + + result = self.proxy.delete_container(name) + + self.assertIsNone(result) + created_container.delete.assert_called_once_with(self.session) + + +class Test_objects(TestObjectStoreProxy, base.TestTransportBase): + + TEST_URL = fakes.FakeAuthenticator.ENDPOINT + + def setUp(self): + super(Test_objects, self).setUp() + self.transport = transport.Transport(accept=transport.JSON) + self.auth = fakes.FakeAuthenticator() + self.session = session.Session(self.transport, self.auth) + + self.proxy = _proxy.Proxy(self.session) + + self.container_name = six.text_type("my_container") + + self.objects_body = [] + for i in range(3): + self.objects_body.append({six.text_type("name"): + six.text_type("object%d" % i)}) + + # Returned object bodies have their container inserted. + self.returned_objects = [] + for ob in self.objects_body: + ob[six.text_type("container")] = self.container_name + self.returned_objects.append(ob) + self.assertEqual(len(self.objects_body), len(self.returned_objects)) + + @httpretty.activate + def test_all_objects(self): + self.stub_url(httpretty.GET, + path=[obj.Object.base_path % + {"container": self.container_name}], + responses=[httpretty.Response( + body=json.dumps(self.objects_body), + status=200, content_type="application/json"), + httpretty.Response(body=json.dumps([]), + status=200, content_type="application/json")]) + + count = 0 + for actual, expected in zip(self.proxy.objects(self.container_name), + self.returned_objects): + self.assertEqual(actual, expected) + count += 1 + self.assertEqual(count, len(self.returned_objects)) + + @httpretty.activate + def test_objects_limited(self): + limit = len(self.objects_body) + 1 + limit_param = "?limit=%d" % limit + + self.stub_url(httpretty.GET, + path=[obj.Object.base_path % + {"container": self.container_name} + limit_param], + json=self.objects_body) + + count = 0 + for actual, expected in zip(self.proxy.objects(self.container_name, + limit=limit), + self.returned_objects): + self.assertEqual(actual, expected) + count += 1 + + self.assertEqual(count, len(self.returned_objects)) + # Since we've chosen a limit larger than the body, only one request + # should be made, so it should be the last one. + self.assertIn(limit_param, httpretty.last_request().path) + + @httpretty.activate + def test_objects_with_marker(self): + marker = six.text_type("object2") + marker_param = "?marker=%s" % marker + + self.stub_url(httpretty.GET, + path=[obj.Object.base_path % + {"container": self.container_name} + marker_param], + json=self.objects_body) + + count = 0 + for actual, expected in zip(self.proxy.objects(self.container_name, + marker=marker), + self.returned_objects): + # Make sure the marker made it into the actual request. + self.assertIn(marker_param, httpretty.last_request().path) + self.assertEqual(actual, expected) + count += 1 + + self.assertEqual(count, len(self.returned_objects)) + + # Since we have to make one request beyond the end, because no + # limit was provided, make sure the last container appears as + # the marker in this last request. + self.assertIn(self.returned_objects[-1]["name"], + httpretty.last_request().path) + + +class Test_get_object_data(TestObjectStoreProxy): + + def test_get(self): + the_data = "here's some data" + ob = mock.MagicMock() + ob.get.return_value = the_data + + result = self.proxy.get_object_data(ob) + + self.assertEqual(result, the_data) + ob.get.assert_called_once_with(self.session) + + +class Test_save_object(TestObjectStoreProxy): + + @mock.patch("openstack.object_store.v1._proxy.Proxy.get_object_data") + def test_save(self, mock_get): + the_data = "here's some data" + mock_get.return_value = the_data + ob = mock.MagicMock() + + fake_open = mock.mock_open() + file_path = "blarga/somefile" + with mock.patch("openstack.object_store.v1._proxy.open", + fake_open, create=True): + self.proxy.save_object(ob, file_path) + + fake_open.assert_called_once_with(file_path, "w") + fake_handle = fake_open() + fake_handle.write.assert_called_once_with(the_data) + + +class Test_create_object(TestObjectStoreProxy): + + def setUp(self): + super(Test_create_object, self).setUp() + self.the_data = six.b("here's some data") + self.container_name = six.text_type("my_container") + self.object_name = six.text_type("my_object") + + @mock.patch("openstack.object_store.v1.obj.Object.from_id") + def test_create_with_obj_name_real_container(self, mock_fi): + created_object = mock.MagicMock() + created_object.name = self.object_name + # Since we're using a MagicMock, we have to explicitly set this to + # None otherwise when it gets accessed it'll have a value which + # is not what we want to happen. + created_object.container = None + mock_fi.return_value = created_object + + cont = container.Container.new(name=self.container_name) + + result = self.proxy.create_object(self.the_data, self.object_name, + cont) + + self.assertIs(result, created_object) + self.assertEqual(result.name, self.object_name) + self.assertEqual(result.container, self.container_name) + created_object.create.assert_called_once_with(self.session, + self.the_data) + + def test_create_with_real_obj_real_container(self): + ob = obj.Object.new(name=self.object_name) + ob.create = mock.MagicMock() + cont = container.Container.new(name=self.container_name) + + result = self.proxy.create_object(self.the_data, ob, cont) + + self.assertIs(result, ob) + self.assertEqual(result.name, self.object_name) + self.assertEqual(result.container, self.container_name) + ob.create.assert_called_once_with(self.session, self.the_data) + + def test_create_with_full_obj_no_container_arg(self): + ob = obj.Object.new(name=self.object_name, + container=self.container_name) + ob.create = mock.MagicMock() + + result = self.proxy.create_object(self.the_data, ob) + + self.assertIs(result, ob) + self.assertEqual(result.name, self.object_name) + self.assertEqual(result.container, self.container_name) + ob.create.assert_called_once_with(self.session, self.the_data) + + +class Test_object_metadata(TestObjectStoreProxy): + + @mock.patch("openstack.resource.Resource.from_id") + def test_get_object_metadata(self, mock_fi): + ob = mock.MagicMock() + mock_fi.return_value = ob + + result = self.proxy.get_object_metadata(ob) + + self.assertIs(result, ob) + ob.head.assert_called_once_with(self.session) + + def test_set_object_metadata(self): + ob = mock.MagicMock() + + result = self.proxy.set_object_metadata(ob) + + self.assertIs(result, ob) + ob.create.assert_called_once_with(self.session) + + +class Test_delete_object(TestObjectStoreProxy): + + def test_delete_object(self): + ob = mock.MagicMock() + + result = self.proxy.delete_object(ob) + + self.assertIsNone(result) + ob.delete.assert_called_once_with(self.session) + + +class Test_copy_object(TestObjectStoreProxy): + + def test_copy_object(self): + self.assertRaises(NotImplementedError, self.proxy.copy_object)