diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 2290814fc..5a406b680 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -7429,7 +7429,7 @@ class OpenStackCloud(_normalize.Normalizer): self, container, name, filename=None, md5=None, sha256=None, segment_size=None, use_slo=True, metadata=None, - generate_checksums=True, + generate_checksums=None, data=None, **headers): """Create a file object. @@ -7439,7 +7439,9 @@ class OpenStackCloud(_normalize.Normalizer): This container will be created if it does not exist already. :param name: Name for the object within the container. :param filename: The path to the local file whose contents will be - uploaded. + uploaded. Mutually exclusive with data. + :param data: The content to upload to the object. Mutually exclusive + with filename. :param md5: A hexadecimal md5 of the file. (Optional), if it is known and can be passed here, it will save repeating the expensive md5 process. It is assumed to be accurate. @@ -7462,10 +7464,25 @@ class OpenStackCloud(_normalize.Normalizer): :raises: ``OpenStackCloudException`` on operation error. """ + if data is not None and filename: + raise ValueError( + "Both filename and data given. Please choose one.") + if data is not None and not name: + raise ValueError( + "name is a required parameter when data is given") + if data is not None and generate_checksums: + raise ValueError( + "checksums cannot be generated with data parameter") + if generate_checksums is None: + if data is not None: + generate_checksums = False + else: + generate_checksums = True + if not metadata: metadata = {} - if not filename: + if not filename and data is None: filename = name # segment_size gets used as a step value in a range call, so needs @@ -7473,7 +7490,10 @@ class OpenStackCloud(_normalize.Normalizer): if segment_size: segment_size = int(segment_size) segment_size = self.get_object_segment_size(segment_size) - file_size = os.path.getsize(filename) + if filename: + file_size = os.path.getsize(filename) + else: + file_size = len(data) if generate_checksums and (md5 is None or sha256 is None): (md5, sha256) = self._get_file_hashes(filename) @@ -7487,10 +7507,17 @@ class OpenStackCloud(_normalize.Normalizer): # On some clouds this is not necessary. On others it is. I'm confused. self.create_container(container) + endpoint = '{container}/{name}'.format(container=container, name=name) + + if data is not None: + self.log.debug( + "swift uploading data to %(endpoint)s", + {'endpoint': endpoint}) + + return self._upload_object_data(endpoint, data, headers) + if self.is_object_stale(container, name, filename, md5, sha256): - endpoint = '{container}/{name}'.format( - container=container, name=name) self.log.debug( "swift uploading %(filename)s to %(endpoint)s", {'filename': filename, 'endpoint': endpoint}) @@ -7502,6 +7529,10 @@ class OpenStackCloud(_normalize.Normalizer): endpoint, filename, headers, file_size, segment_size, use_slo) + def _upload_object_data(self, endpoint, data, headers): + return self._object_store_client.put( + endpoint, headers=headers, data=data) + def _upload_object(self, endpoint, filename, headers): return self._object_store_client.put( endpoint, headers=headers, data=open(filename, 'r')) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index f4e3e3ee4..2b115a1ce 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -594,7 +594,7 @@ class TestCase(base.TestCase): key = '{method}|{uri}|{params}'.format( method=method, uri=uri, params=kw_params) validate = to_mock.pop('validate', {}) - valid_keys = set(['json', 'headers', 'params']) + valid_keys = set(['json', 'headers', 'params', 'data']) invalid_keys = set(validate.keys()) - valid_keys if invalid_keys: raise TypeError( diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index b13d81446..6e08d52b6 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -950,3 +950,55 @@ class TestObjectUploads(BaseTestObject): generate_checksums=False) self.assert_calls() + + def test_create_object_data(self): + + self.register_uris([ + dict(method='GET', + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': 1000}, + slo={'min_segment_size': 500})), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + status_code=404), + dict(method='PUT', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, container=self.container), + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, container=self.container), + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=201, + validate=dict( + headers={}, + data=self.content, + )), + ]) + + self.cloud.create_object( + container=self.container, name=self.object, + data=self.content) + + self.assert_calls() diff --git a/releasenotes/notes/create-object-data-870cb543543aa983.yaml b/releasenotes/notes/create-object-data-870cb543543aa983.yaml new file mode 100644 index 000000000..4e0255050 --- /dev/null +++ b/releasenotes/notes/create-object-data-870cb543543aa983.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add a data parameter to ``openstack.connection.Connection.create_object`` + so that data can be passed in directly instead of through a file.