Add method to cleanup autocreated image objects
This shouldn't really be needed, as the objects should get cleaned up automatically. BUT - if things leak, this method can be used to delete any objects shade has uploaded on behalf of the user for deleting images. While in there, clean up test_image to use a few more good practices. Change-Id: Ifb697944856e1922517074d84a7c00a4af75b1e6
This commit is contained in:
parent
371e0b667e
commit
5996a0313b
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Added new method, delete_autocreated_image_objects
|
||||||
|
that can be used to delete any leaked objects shade
|
||||||
|
may have created on behalf of the user.
|
@ -50,6 +50,8 @@ from shade import _utils
|
|||||||
|
|
||||||
OBJECT_MD5_KEY = 'x-object-meta-x-shade-md5'
|
OBJECT_MD5_KEY = 'x-object-meta-x-shade-md5'
|
||||||
OBJECT_SHA256_KEY = 'x-object-meta-x-shade-sha256'
|
OBJECT_SHA256_KEY = 'x-object-meta-x-shade-sha256'
|
||||||
|
OBJECT_AUTOCREATE_KEY = 'x-object-meta-x-shade-autocreated'
|
||||||
|
OBJECT_AUTOCREATE_CONTAINER = 'images'
|
||||||
IMAGE_MD5_KEY = 'owner_specified.shade.md5'
|
IMAGE_MD5_KEY = 'owner_specified.shade.md5'
|
||||||
IMAGE_SHA256_KEY = 'owner_specified.shade.sha256'
|
IMAGE_SHA256_KEY = 'owner_specified.shade.sha256'
|
||||||
IMAGE_OBJECT_KEY = 'owner_specified.shade.object'
|
IMAGE_OBJECT_KEY = 'owner_specified.shade.object'
|
||||||
@ -4526,7 +4528,7 @@ class OpenStackCloud(
|
|||||||
return up_to_date
|
return up_to_date
|
||||||
|
|
||||||
def create_image(
|
def create_image(
|
||||||
self, name, filename=None, container='images',
|
self, name, filename=None, container=OBJECT_AUTOCREATE_CONTAINER,
|
||||||
md5=None, sha256=None,
|
md5=None, sha256=None,
|
||||||
disk_format=None, container_format=None,
|
disk_format=None, container_format=None,
|
||||||
disable_vendor_agent=True,
|
disable_vendor_agent=True,
|
||||||
@ -4823,6 +4825,7 @@ class OpenStackCloud(
|
|||||||
self.create_object(
|
self.create_object(
|
||||||
container, name, filename,
|
container, name, filename,
|
||||||
md5=md5, sha256=sha256,
|
md5=md5, sha256=sha256,
|
||||||
|
metadata={OBJECT_AUTOCREATE_KEY: 'true'},
|
||||||
**{'content-type': 'application/octet-stream'})
|
**{'content-type': 'application/octet-stream'})
|
||||||
if not current_image:
|
if not current_image:
|
||||||
current_image = self.get_image(name)
|
current_image = self.get_image(name)
|
||||||
@ -7561,11 +7564,13 @@ class OpenStackCloud(
|
|||||||
return self._object_store_client.get(
|
return self._object_store_client.get(
|
||||||
container, params=dict(format='json'))
|
container, params=dict(format='json'))
|
||||||
|
|
||||||
def delete_object(self, container, name):
|
def delete_object(self, container, name, meta=None):
|
||||||
"""Delete an object from a container.
|
"""Delete an object from a container.
|
||||||
|
|
||||||
:param string container: Name of the container holding the object.
|
:param string container: Name of the container holding the object.
|
||||||
:param string name: Name of the object to delete.
|
:param string name: Name of the object to delete.
|
||||||
|
:param dict meta: Metadata for the object in question. (optional, will
|
||||||
|
be fetched if not provided)
|
||||||
|
|
||||||
:returns: True if delete succeeded, False if the object was not found.
|
:returns: True if delete succeeded, False if the object was not found.
|
||||||
|
|
||||||
@ -7580,6 +7585,7 @@ class OpenStackCloud(
|
|||||||
# Errors:
|
# Errors:
|
||||||
# We should ultimately do something with that
|
# We should ultimately do something with that
|
||||||
try:
|
try:
|
||||||
|
if not meta:
|
||||||
meta = self.get_object_metadata(container, name)
|
meta = self.get_object_metadata(container, name)
|
||||||
if not meta:
|
if not meta:
|
||||||
return False
|
return False
|
||||||
@ -7594,6 +7600,28 @@ class OpenStackCloud(
|
|||||||
except OpenStackCloudHTTPError:
|
except OpenStackCloudHTTPError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def delete_autocreated_image_objects(
|
||||||
|
self, container=OBJECT_AUTOCREATE_CONTAINER):
|
||||||
|
"""Delete all objects autocreated for image uploads.
|
||||||
|
|
||||||
|
This method should generally not be needed, as shade should clean up
|
||||||
|
the objects it uses for object-based image creation. If something
|
||||||
|
goes wrong and it is found that there are leaked objects, this method
|
||||||
|
can be used to delete any objects that shade has created on the user's
|
||||||
|
behalf in service of image uploads.
|
||||||
|
"""
|
||||||
|
# This method only makes sense on clouds that use tasks
|
||||||
|
if not self.image_api_use_tasks:
|
||||||
|
return False
|
||||||
|
|
||||||
|
deleted = False
|
||||||
|
for obj in self.list_objects(container):
|
||||||
|
meta = self.get_object_metadata(container, obj['name'])
|
||||||
|
if meta.get(OBJECT_AUTOCREATE_KEY) == 'true':
|
||||||
|
if self.delete_object(container, obj['name'], meta):
|
||||||
|
deleted = True
|
||||||
|
return deleted
|
||||||
|
|
||||||
def get_object_metadata(self, container, name):
|
def get_object_metadata(self, container, name):
|
||||||
try:
|
try:
|
||||||
return self._object_store_client.head(
|
return self._object_store_client.head(
|
||||||
|
@ -408,6 +408,10 @@ class RequestsMockTestCase(BaseTestCase):
|
|||||||
return _RoleData(role_id, role_name, {'role': response},
|
return _RoleData(role_id, role_name, {'role': response},
|
||||||
{'role': request})
|
{'role': request})
|
||||||
|
|
||||||
|
def use_nothing(self):
|
||||||
|
self.calls = []
|
||||||
|
self._uri_registry.clear()
|
||||||
|
|
||||||
def use_keystone_v3(self, catalog='catalog-v3.json'):
|
def use_keystone_v3(self, catalog='catalog-v3.json'):
|
||||||
self.adapter = self.useFixture(rm_fixture.Fixture())
|
self.adapter = self.useFixture(rm_fixture.Fixture())
|
||||||
self.calls = []
|
self.calls = []
|
||||||
|
@ -24,6 +24,7 @@ import munch
|
|||||||
import six
|
import six
|
||||||
|
|
||||||
import shade
|
import shade
|
||||||
|
import shade.openstackcloud
|
||||||
from shade import exc
|
from shade import exc
|
||||||
from shade import meta
|
from shade import meta
|
||||||
from shade.tests import fakes
|
from shade.tests import fakes
|
||||||
@ -44,6 +45,8 @@ class BaseTestImage(base.RequestsMockTestCase):
|
|||||||
self.fake_image_dict = fakes.make_fake_image(image_id=self.image_id)
|
self.fake_image_dict = fakes.make_fake_image(image_id=self.image_id)
|
||||||
self.fake_search_return = {'images': [self.fake_image_dict]}
|
self.fake_search_return = {'images': [self.fake_image_dict]}
|
||||||
self.output = uuid.uuid4().bytes
|
self.output = uuid.uuid4().bytes
|
||||||
|
self.image_name = self.getUniqueString('image')
|
||||||
|
self.container_name = self.getUniqueString('container')
|
||||||
|
|
||||||
|
|
||||||
class TestImage(BaseTestImage):
|
class TestImage(BaseTestImage):
|
||||||
@ -258,8 +261,6 @@ class TestImage(BaseTestImage):
|
|||||||
|
|
||||||
def test_create_image_task(self):
|
def test_create_image_task(self):
|
||||||
self.cloud.image_api_use_tasks = True
|
self.cloud.image_api_use_tasks = True
|
||||||
image_name = 'name-99'
|
|
||||||
container_name = 'image_upload_v2_test_container'
|
|
||||||
endpoint = self.cloud._object_store_client.get_endpoint()
|
endpoint = self.cloud._object_store_client.get_endpoint()
|
||||||
|
|
||||||
task_id = str(uuid.uuid4())
|
task_id = str(uuid.uuid4())
|
||||||
@ -286,18 +287,18 @@ class TestImage(BaseTestImage):
|
|||||||
slo={'min_segment_size': 500})),
|
slo={'min_segment_size': 500})),
|
||||||
dict(method='HEAD',
|
dict(method='HEAD',
|
||||||
uri='{endpoint}/{container}'.format(
|
uri='{endpoint}/{container}'.format(
|
||||||
endpoint=endpoint, container=container_name),
|
endpoint=endpoint, container=self.container_name),
|
||||||
status_code=404),
|
status_code=404),
|
||||||
dict(method='PUT',
|
dict(method='PUT',
|
||||||
uri='{endpoint}/{container}'.format(
|
uri='{endpoint}/{container}'.format(
|
||||||
endpoint=endpoint, container=container_name),
|
endpoint=endpoint, container=self.container_name),
|
||||||
status_code=201,
|
status_code=201,
|
||||||
headers={'Date': 'Fri, 16 Dec 2016 18:21:20 GMT',
|
headers={'Date': 'Fri, 16 Dec 2016 18:21:20 GMT',
|
||||||
'Content-Length': '0',
|
'Content-Length': '0',
|
||||||
'Content-Type': 'text/html; charset=UTF-8'}),
|
'Content-Type': 'text/html; charset=UTF-8'}),
|
||||||
dict(method='HEAD',
|
dict(method='HEAD',
|
||||||
uri='{endpoint}/{container}'.format(
|
uri='{endpoint}/{container}'.format(
|
||||||
endpoint=endpoint, container=container_name),
|
endpoint=endpoint, container=self.container_name),
|
||||||
headers={'Content-Length': '0',
|
headers={'Content-Length': '0',
|
||||||
'X-Container-Object-Count': '0',
|
'X-Container-Object-Count': '0',
|
||||||
'Accept-Ranges': 'bytes',
|
'Accept-Ranges': 'bytes',
|
||||||
@ -309,13 +310,13 @@ class TestImage(BaseTestImage):
|
|||||||
'Content-Type': 'text/plain; charset=utf-8'}),
|
'Content-Type': 'text/plain; charset=utf-8'}),
|
||||||
dict(method='HEAD',
|
dict(method='HEAD',
|
||||||
uri='{endpoint}/{container}/{object}'.format(
|
uri='{endpoint}/{container}/{object}'.format(
|
||||||
endpoint=endpoint, container=container_name,
|
endpoint=endpoint, container=self.container_name,
|
||||||
object=image_name),
|
object=self.image_name),
|
||||||
status_code=404),
|
status_code=404),
|
||||||
dict(method='PUT',
|
dict(method='PUT',
|
||||||
uri='{endpoint}/{container}/{object}'.format(
|
uri='{endpoint}/{container}/{object}'.format(
|
||||||
endpoint=endpoint, container=container_name,
|
endpoint=endpoint, container=self.container_name,
|
||||||
object=image_name),
|
object=self.image_name),
|
||||||
status_code=201,
|
status_code=201,
|
||||||
validate=dict(
|
validate=dict(
|
||||||
headers={'x-object-meta-x-shade-md5': fakes.NO_MD5,
|
headers={'x-object-meta-x-shade-md5': fakes.NO_MD5,
|
||||||
@ -329,8 +330,9 @@ class TestImage(BaseTestImage):
|
|||||||
json=dict(
|
json=dict(
|
||||||
type='import', input={
|
type='import', input={
|
||||||
'import_from': '{container}/{object}'.format(
|
'import_from': '{container}/{object}'.format(
|
||||||
container=container_name, object=image_name),
|
container=self.container_name,
|
||||||
'image_properties': {'name': image_name}}))
|
object=self.image_name),
|
||||||
|
'image_properties': {'name': self.image_name}}))
|
||||||
),
|
),
|
||||||
dict(method='GET',
|
dict(method='GET',
|
||||||
uri='https://image.example.com/v2/tasks/{id}'.format(
|
uri='https://image.example.com/v2/tasks/{id}'.format(
|
||||||
@ -348,8 +350,8 @@ class TestImage(BaseTestImage):
|
|||||||
validate=dict(
|
validate=dict(
|
||||||
json=sorted([{u'op': u'add',
|
json=sorted([{u'op': u'add',
|
||||||
u'value': '{container}/{object}'.format(
|
u'value': '{container}/{object}'.format(
|
||||||
container=container_name,
|
container=self.container_name,
|
||||||
object=image_name),
|
object=self.image_name),
|
||||||
u'path': u'/owner_specified.shade.object'},
|
u'path': u'/owner_specified.shade.object'},
|
||||||
{u'op': u'add', u'value': fakes.NO_MD5,
|
{u'op': u'add', u'value': fakes.NO_MD5,
|
||||||
u'path': u'/owner_specified.shade.md5'},
|
u'path': u'/owner_specified.shade.md5'},
|
||||||
@ -362,8 +364,8 @@ class TestImage(BaseTestImage):
|
|||||||
),
|
),
|
||||||
dict(method='HEAD',
|
dict(method='HEAD',
|
||||||
uri='{endpoint}/{container}/{object}'.format(
|
uri='{endpoint}/{container}/{object}'.format(
|
||||||
endpoint=endpoint, container=container_name,
|
endpoint=endpoint, container=self.container_name,
|
||||||
object=image_name),
|
object=self.image_name),
|
||||||
headers={
|
headers={
|
||||||
'X-Timestamp': '1429036140.50253',
|
'X-Timestamp': '1429036140.50253',
|
||||||
'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1',
|
'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1',
|
||||||
@ -377,15 +379,94 @@ class TestImage(BaseTestImage):
|
|||||||
'Etag': fakes.NO_MD5}),
|
'Etag': fakes.NO_MD5}),
|
||||||
dict(method='DELETE',
|
dict(method='DELETE',
|
||||||
uri='{endpoint}/{container}/{object}'.format(
|
uri='{endpoint}/{container}/{object}'.format(
|
||||||
endpoint=endpoint, container=container_name,
|
endpoint=endpoint, container=self.container_name,
|
||||||
object=image_name)),
|
object=self.image_name)),
|
||||||
dict(method='GET', uri='https://image.example.com/v2/images',
|
dict(method='GET', uri='https://image.example.com/v2/images',
|
||||||
json=self.fake_search_return)
|
json=self.fake_search_return)
|
||||||
])
|
])
|
||||||
|
|
||||||
self.cloud.create_image(
|
self.cloud.create_image(
|
||||||
image_name, self.imagefile.name, wait=True, timeout=1,
|
self.image_name, self.imagefile.name, wait=True, timeout=1,
|
||||||
is_public=False, container=container_name)
|
is_public=False, container=self.container_name)
|
||||||
|
|
||||||
|
self.assert_calls()
|
||||||
|
|
||||||
|
def test_delete_autocreated_no_tasks(self):
|
||||||
|
self.use_nothing()
|
||||||
|
self.cloud.image_api_use_tasks = False
|
||||||
|
deleted = self.cloud.delete_autocreated_image_objects(
|
||||||
|
container=self.container_name)
|
||||||
|
self.assertFalse(deleted)
|
||||||
|
self.assert_calls()
|
||||||
|
|
||||||
|
def test_delete_autocreated_image_objects(self):
|
||||||
|
self.use_keystone_v3()
|
||||||
|
self.cloud.image_api_use_tasks = True
|
||||||
|
endpoint = self.cloud._object_store_client.get_endpoint()
|
||||||
|
other_image = self.getUniqueString('no-delete')
|
||||||
|
|
||||||
|
self.register_uris([
|
||||||
|
dict(method='GET',
|
||||||
|
uri=self.get_mock_url(
|
||||||
|
service_type='object-store',
|
||||||
|
resource=self.container_name,
|
||||||
|
qs_elements=['format=json']),
|
||||||
|
json=[{
|
||||||
|
'content_type': 'application/octet-stream',
|
||||||
|
'bytes': 1437258240,
|
||||||
|
'hash': '249219347276c331b87bf1ac2152d9af',
|
||||||
|
'last_modified': '2015-02-16T17:50:05.289600',
|
||||||
|
'name': other_image,
|
||||||
|
}, {
|
||||||
|
'content_type': 'application/octet-stream',
|
||||||
|
'bytes': 1290170880,
|
||||||
|
'hash': fakes.NO_MD5,
|
||||||
|
'last_modified': '2015-04-14T18:29:00.502530',
|
||||||
|
'name': self.image_name,
|
||||||
|
}]),
|
||||||
|
dict(method='HEAD',
|
||||||
|
uri=self.get_mock_url(
|
||||||
|
service_type='object-store',
|
||||||
|
resource=self.container_name,
|
||||||
|
append=[other_image]),
|
||||||
|
headers={
|
||||||
|
'X-Timestamp': '1429036140.50253',
|
||||||
|
'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1',
|
||||||
|
'Content-Length': '1290170880',
|
||||||
|
'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT',
|
||||||
|
'X-Object-Meta-X-Shade-Sha256': 'does not matter',
|
||||||
|
'X-Object-Meta-X-Shade-Md5': 'does not matter',
|
||||||
|
'Date': 'Thu, 16 Nov 2017 15:24:30 GMT',
|
||||||
|
'Accept-Ranges': 'bytes',
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'Etag': '249219347276c331b87bf1ac2152d9af',
|
||||||
|
}),
|
||||||
|
dict(method='HEAD',
|
||||||
|
uri=self.get_mock_url(
|
||||||
|
service_type='object-store',
|
||||||
|
resource=self.container_name,
|
||||||
|
append=[self.image_name]),
|
||||||
|
headers={
|
||||||
|
'X-Timestamp': '1429036140.50253',
|
||||||
|
'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1',
|
||||||
|
'Content-Length': '1290170880',
|
||||||
|
'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT',
|
||||||
|
'X-Object-Meta-X-Shade-Sha256': fakes.NO_SHA256,
|
||||||
|
'X-Object-Meta-X-Shade-Md5': fakes.NO_MD5,
|
||||||
|
'Date': 'Thu, 16 Nov 2017 15:24:30 GMT',
|
||||||
|
'Accept-Ranges': 'bytes',
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
shade.openstackcloud.OBJECT_AUTOCREATE_KEY: 'true',
|
||||||
|
'Etag': fakes.NO_MD5}),
|
||||||
|
dict(method='DELETE',
|
||||||
|
uri='{endpoint}/{container}/{object}'.format(
|
||||||
|
endpoint=endpoint, container=self.container_name,
|
||||||
|
object=self.image_name)),
|
||||||
|
])
|
||||||
|
|
||||||
|
deleted = self.cloud.delete_autocreated_image_objects(
|
||||||
|
container=self.container_name)
|
||||||
|
self.assertTrue(deleted)
|
||||||
|
|
||||||
self.assert_calls()
|
self.assert_calls()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user