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:
parent
3ee1effdb9
commit
c9ade4e294
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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'])
|
||||
|
|
Loading…
Reference in New Issue