Implement the rest of Swift containers and objects

This change adds support for Swift objects and updates some of the
container implementation. At this point, full CRUD support for Swift is
basically implemented save for adding user-defined headers.

Change-Id: Icbaffb5d76bfd1977ab7213a9b83bc28cf0bc551
This commit is contained in:
Brian Curtin 2014-08-14 14:39:39 -05:00
parent 3ee1effdb9
commit c9ade4e294
6 changed files with 308 additions and 24 deletions

View File

@ -11,13 +11,16 @@
# License for the specific language governing permissions and limitations
# under the License.
from openstack import exceptions
from openstack.object_store import object_store_service
from openstack import resource
from openstack import utils
class Container(resource.Resource):
base_path = "/"
service = object_store_service.ObjectStoreService()
id_attribute = "name"
allow_create = True
allow_retrieve = True
@ -26,7 +29,7 @@ class Container(resource.Resource):
allow_list = True
allow_head = True
# Account data (from headers when id=None)
# Account data (when id=None)
timestamp = resource.prop("x-timestamp")
account_bytes_used = resource.prop("x-account-bytes-used")
account_container_count = resource.prop("x-account-container-count")
@ -34,7 +37,7 @@ class Container(resource.Resource):
meta_temp_url_key = resource.prop("x-account-meta-temp-url-key")
meta_temp_url_key_2 = resource.prop("x-account-meta-temp-url-key-2")
# Container data (from list when id=None)
# Container body data (when id=None)
name = resource.prop("name")
count = resource.prop("count")
bytes = resource.prop("bytes")
@ -43,17 +46,41 @@ class Container(resource.Resource):
object_count = resource.prop("x-container-object-count")
bytes_used = resource.prop("x-container-bytes-used")
# Optional Container metadata (from head when id=name)
can_read = resource.prop("x-container-read")
can_write = resource.prop("x-container-write")
# Request headers (when id=None)
newest = resource.prop("x-newest", type=bool)
# Request headers (when id=name)
read_ACL = resource.prop("x-container-read")
write_ACL = resource.prop("x-container-write")
sync_to = resource.prop("x-container-sync-to")
sync_key = resource.prop("x-container-sync-key")
versions_location = resource.prop("x-versions-location")
remove_versions_location = resource.prop("x-remove-versions-location")
content_type = resource.prop("content-type")
detect_content_type = resource.prop("x-detect-content-type", type=bool)
if_none_match = resource.prop("if-none-match")
@property
def id(self):
try:
val = self.name
except AttributeError:
val = None
return val
def _do_create_update(self, session, method):
url = utils.urljoin(self.base_path, self.id)
# Only send actual headers, not potentially set body values.
headers = self._attrs.copy()
for val in ("name", "count", "bytes"):
headers.pop(val, None)
data = method(url, service=self.service, accept=None,
headers=headers).headers
self._reset_dirty()
return data
def create(self, session):
if not self.allow_create:
raise exceptions.MethodNotSupported('create')
return self._do_create_update(session, session.put)
def update(self, session):
if not self.allow_update:
raise exceptions.MethodNotSupported('update')
return self._do_create_update(session, session.post)

View File

@ -0,0 +1,93 @@
# 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 openstack import exceptions
from openstack.object_store import object_store_service
from openstack import resource
from openstack import utils
class Object(resource.Resource):
base_path = "/%(container)s"
service = object_store_service.ObjectStoreService()
id_attribute = "name"
allow_create = True
allow_retrieve = True
allow_update = True
allow_delete = True
allow_list = True
allow_head = True
# URL parameters
container = resource.prop("container")
name = resource.prop("name")
# Object details
hash = resource.prop("hash")
bytes = resource.prop("bytes")
# Headers for HEAD and GET requests
auth_token = resource.prop("x-auth-token")
newest = resource.prop("x-newest", type=bool)
range = resource.prop("range", type=dict)
if_match = resource.prop("if-match", type=dict)
if_none_match = resource.prop("if-none-match", type=dict)
if_modified_since = resource.prop("if-modified-since", type=dict)
if_unmodified_since = resource.prop("if-unmodified-since", type=dict)
# Query parameters
signature = resource.prop("signature")
expires = resource.prop("expires")
multipart_manifest = resource.prop("multipart-manifest")
# Response headers from HEAD and GET
content_length = resource.prop("content-length")
content_type = resource.prop("content-type")
accept_ranges = resource.prop("accept-ranges")
last_modified = resource.prop("last-modified")
etag = resource.prop("etag")
is_static_large_object = resource.prop("x-static-large-object")
date = resource.prop("date")
content_encoding = resource.prop("content-encoding")
content_disposition = resource.prop("content-disposition")
delete_at = resource.prop("x-delete-at", type=int)
object_manifest = resource.prop("x-object-manifest")
timestamp = resource.prop("x-timestamp")
# Headers for PUT and POST requests
transfer_encoding = resource.prop("transfer-encoding")
detect_content_type = resource.prop("x-detect-content-type", type=bool)
copy_from = resource.prop("x-copy-from")
delete_after = resource.prop("x-delete-after", type=int)
def get(self, session):
if not self.allow_retrieve:
raise exceptions.MethodNotSupported('retrieve')
# When joining the base_path part and the id part, base_path's
# leading slash gets dropped off here. Putting an empty leading value
# in front of it causes it to get joined and replaced.
url = utils.urljoin("", self.base_path % self, self.id)
# Only send actual headers, not potentially set body values and
# query parameters.
headers = self._attrs.copy()
for val in ("container", "name", "hash", "bytes", "signature",
"expires", "multipart_manifest"):
headers.pop(val, None)
resp = session.get(url, service=self.service, accept="bytes",
headers=headers).content
return resp

View File

@ -282,9 +282,6 @@ class Resource(collections.MutableMapping):
url = utils.urljoin(url, r_id)
data = session.head(url, service=cls.service, accept=None).headers
resp_id = data.pop("X-Trans-Id", None)
if resp_id:
data[cls.id_attribute] = resp_id
return data

View File

@ -13,9 +13,12 @@
import mock
import testtools
from openstack import exceptions
from openstack.object_store.v1 import container
CONTAINER_NAME = "mycontainer"
ACCOUNT_EXAMPLE = {
'content-length': '0',
'accept-ranges': 'bytes',
@ -31,7 +34,7 @@ ACCOUNT_EXAMPLE = {
CONT_EXAMPLE = {
"count": 999,
"bytes": 12345,
"name": "mycontainer"
"name": CONTAINER_NAME
}
HEAD_EXAMPLE = {
@ -67,7 +70,7 @@ LIST_EXAMPLE = [
class TestAccount(testtools.TestCase):
def test_make_it(self):
sot = container.Container(ACCOUNT_EXAMPLE)
sot = container.Container.new(**ACCOUNT_EXAMPLE)
self.assertIsNone(sot.id)
self.assertEqual(ACCOUNT_EXAMPLE['x-timestamp'], sot.timestamp)
self.assertEqual(ACCOUNT_EXAMPLE['x-account-bytes-used'],
@ -80,9 +83,21 @@ class TestAccount(testtools.TestCase):
class TestContainer(testtools.TestCase):
def setUp(self):
super(TestContainer, self).setUp()
self.resp = mock.Mock()
self.resp.body = {}
self.resp.headers = {"X-Trans-Id": "abcdef"}
self.sess = mock.Mock()
self.sess.put = mock.MagicMock()
self.sess.put.return_value = self.resp
self.sess.post = mock.MagicMock()
self.sess.post.return_value = self.resp
def test_basic(self):
sot = container.Container(CONT_EXAMPLE)
sot = container.Container.new(**CONT_EXAMPLE)
self.assertIsNone(sot.resources_key)
self.assertEqual('name', sot.id_attribute)
self.assertEqual('/', sot.base_path)
self.assertEqual('object-store', sot.service.service_type)
self.assertTrue(sot.allow_update)
@ -93,7 +108,7 @@ class TestContainer(testtools.TestCase):
self.assertTrue(sot.allow_head)
def test_make_it(self):
sot = container.Container(CONT_EXAMPLE)
sot = container.Container.new(**CONT_EXAMPLE)
self.assertEqual(CONT_EXAMPLE['name'], sot.id)
self.assertEqual(CONT_EXAMPLE['name'], sot.name)
self.assertEqual(CONT_EXAMPLE['count'], sot.count)
@ -117,9 +132,9 @@ class TestContainer(testtools.TestCase):
self.assertEqual(HEAD_EXAMPLE['x-container-bytes-used'],
sot.bytes_used)
self.assertEqual(HEAD_EXAMPLE['x-container-read'],
sot.can_read)
sot.read_ACL)
self.assertEqual(HEAD_EXAMPLE['x-container-write'],
sot.can_write)
sot.write_ACL)
self.assertEqual(HEAD_EXAMPLE['x-container-sync-to'],
sot.sync_to)
self.assertEqual(HEAD_EXAMPLE['x-container-sync-key'],
@ -141,3 +156,43 @@ class TestContainer(testtools.TestCase):
self.assertEqual(response[item].name, LIST_EXAMPLE[item]["name"])
self.assertEqual(response[item].count, LIST_EXAMPLE[item]["count"])
self.assertEqual(response[item].bytes, LIST_EXAMPLE[item]["bytes"])
def _test_create_update(self, sot, sot_call, sess_method):
sot.read_ACL = "some ACL"
sot.write_ACL = "another ACL"
sot.detect_content_type = True
headers = {
"x-container-read": "some ACL",
"x-container-write": "another ACL",
"x-detect-content-type": True
}
sot_call(self.sess)
url = "/%s" % CONTAINER_NAME
sess_method.assert_called_with(url, service=sot.service, accept=None,
headers=headers)
def test_create(self):
sot = container.Container.new(name=CONTAINER_NAME)
self._test_create_update(sot, sot.create, self.sess.put)
def test_update(self):
sot = container.Container.new(name=CONTAINER_NAME)
self._test_create_update(sot, sot.update, self.sess.post)
def _test_cant(self, sot, call):
sot.allow_create = False
self.assertRaises(exceptions.MethodNotSupported,
call, self.sess)
def test_cant_create(self):
sot = container.Container.new(name=CONTAINER_NAME)
sot.allow_create = False
self.assertRaises(exceptions.MethodNotSupported,
sot.create, self.sess)
def test_cant_update(self):
sot = container.Container.new(name=CONTAINER_NAME)
sot.allow_update = False
self.assertRaises(exceptions.MethodNotSupported,
sot.update, self.sess)

View File

@ -0,0 +1,114 @@
# 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 testtools
from openstack import exceptions
from openstack.object_store.v1 import obj
CONTAINER_NAME = "mycontainer"
OBJECT_NAME = "myobject"
OBJ_EXAMPLE = {
"hash": "243f87b91224d85722564a80fd3cb1f1",
"last-modified": "2014-07-13T18:41:03.319240",
"bytes": 252466,
"name": OBJECT_NAME,
"content-type": "application/octet-stream"
}
HEAD_EXAMPLE = {
'content-length': '252466',
'container': CONTAINER_NAME,
'name': OBJECT_NAME,
'accept-ranges': 'bytes',
'last-modified': 'Sun, 13 Jul 2014 18:41:04 GMT',
'etag': '243f87b91224d85722564a80fd3cb1f1',
'x-timestamp': '1405276863.31924',
'date': 'Thu, 28 Aug 2014 14:41:59 GMT',
'content-type': 'application/octet-stream',
'id': 'tx5fb5ad4f4d0846c6b2bc7-0053ff3fb7'
}
class TestObject(testtools.TestCase):
def setUp(self):
super(TestObject, self).setUp()
self.resp = mock.Mock()
self.resp.content = "lol here's some content"
self.resp.headers = {"X-Trans-Id": "abcdef"}
self.sess = mock.Mock()
self.sess.get = mock.MagicMock()
self.sess.get.return_value = self.resp
def test_basic(self):
sot = obj.Object.new(**OBJ_EXAMPLE)
self.assertIsNone(sot.resources_key)
self.assertEqual("name", sot.id_attribute)
self.assertEqual('/%(container)s', sot.base_path)
self.assertEqual('object-store', sot.service.service_type)
self.assertTrue(sot.allow_update)
self.assertTrue(sot.allow_create)
self.assertTrue(sot.allow_retrieve)
self.assertTrue(sot.allow_delete)
self.assertTrue(sot.allow_list)
self.assertTrue(sot.allow_head)
def test_new(self):
sot = obj.Object.new(container=CONTAINER_NAME, name=OBJECT_NAME)
self.assertEqual(OBJECT_NAME, sot.name)
self.assertEqual(CONTAINER_NAME, sot.container)
def test_head(self):
sot = obj.Object.existing(**OBJ_EXAMPLE)
# Update object with HEAD data
sot._attrs.update(HEAD_EXAMPLE)
# Attributes from creation
self.assertEqual(OBJ_EXAMPLE['name'], sot.name)
self.assertEqual(OBJ_EXAMPLE['hash'], sot.hash)
self.assertEqual(OBJ_EXAMPLE['bytes'], sot.bytes)
# Attributes from header
self.assertEqual(HEAD_EXAMPLE['container'], sot.container)
self.assertEqual(HEAD_EXAMPLE['content-length'], sot.content_length)
self.assertEqual(HEAD_EXAMPLE['accept-ranges'], sot.accept_ranges)
self.assertEqual(HEAD_EXAMPLE['last-modified'], sot.last_modified)
self.assertEqual(HEAD_EXAMPLE['etag'], sot.etag)
self.assertEqual(HEAD_EXAMPLE['x-timestamp'], sot.timestamp)
self.assertEqual(HEAD_EXAMPLE['date'], sot.date)
self.assertEqual(HEAD_EXAMPLE['content-type'], sot.content_type)
def test_get(self):
sot = obj.Object.new(container=CONTAINER_NAME, name=OBJECT_NAME)
sot.newest = True
sot.if_match = {"who": "what"}
headers = {
"x-newest": True,
"if-match": {"who": "what"}
}
rv = sot.get(self.sess)
url = "/%s/%s" % (CONTAINER_NAME, OBJECT_NAME)
self.sess.get.assert_called_with(url, service=sot.service,
accept="bytes", headers=headers)
self.assertEqual(rv, self.resp.content)
def test_cant_get(self):
sot = obj.Object.new(container=CONTAINER_NAME, name=OBJECT_NAME)
sot.allow_retrieve = False
self.assertRaises(exceptions.MethodNotSupported, sot.get, self.sess)

View File

@ -126,12 +126,10 @@ class ResourceTests(base.TestTransportBase):
self.stub_url(httpretty.HEAD, path=[fake_path, fake_id],
name=fake_name,
attr1=fake_attr1,
attr2=fake_attr2,
x_trans_id=fake_id)
attr2=fake_attr2)
obj = FakeResource.head_by_id(self.session, fake_id,
path_args=fake_arguments)
self.assertEqual(fake_id, int(obj.id))
self.assertEqual(fake_name, obj['name'])
self.assertEqual(fake_attr1, obj['attr1'])
self.assertEqual(fake_attr2, obj['attr2'])