Rework glare hooks

This patch adds pre and post hooks for all artifact modifying operations:
create, update, activate, deactivate, reactivate, publish, upload,
add_location, download, delete.
Example: 'pre_create_hook' and 'post_create_hook'.

Previous hook calls with 'validate_' prefix are removed except
'validate_upload' which is marked as depricated.

Change-Id: I746ceaacd357e332790c84ffe50e712cfa6cecda
This commit is contained in:
Mike Fedosin 2017-08-25 01:04:08 +03:00
parent 408e77dc5c
commit b93aea2e3a
5 changed files with 386 additions and 283 deletions

View File

@ -14,7 +14,6 @@
# under the License.
from copy import deepcopy
import os
import jsonpatch
from oslo_config import cfg
@ -145,8 +144,10 @@ class Engine(object):
def _apply_patch(self, context, af, patch):
# This function is a collection of hacks and workarounds to make
# json patch apply changes to oslo_vo object.
action_names = {'artifact:update'}
action_names = ['update']
af_dict = af.to_dict()
policy.authorize('artifact:update', af_dict, context)
af.pre_update_hook(context, af)
try:
for operation in patch._ops:
# apply the change to make sure that it's correct
@ -174,19 +175,29 @@ class Engine(object):
to_visibility=af_dict['visibility']
)
if af_dict['visibility'] == 'public':
af.validate_publish(context, af)
action_names.add('artifact:publish')
policy.authorize(
'artifact:publish', af_dict, context)
af.pre_publish_hook(context, af)
action_names.append('publish')
elif field_name == 'status':
utils.validate_status_transition(
af, from_status=af.status, to_status=af_dict['status'])
if af_dict['status'] == 'deactivated':
action_names.add('artifact:deactivate')
policy.authorize(
'artifact:deactivate', af_dict, context)
af.pre_deactivate_hook(context, af)
action_names.append('deactivate')
elif af_dict['status'] == 'active':
if af.status == 'deactivated':
action_names.add('artifact:reactivate')
policy.authorize(
'artifact:reactivate', af_dict, context)
af.pre_reactivate_hook(context, af)
action_names.append('reactivate')
else:
af.validate_activate(context, af)
action_names.add('artifact:activate')
policy.authorize(
'artifact:activate', af_dict, context)
af.pre_activate_hook(context, af)
action_names.append('activate')
else:
utils.validate_change_allowed(af, field_name)
@ -235,7 +246,9 @@ class Engine(object):
raise exception.BadRequest(msg)
utils.validate_change_allowed(af, field_name)
setattr(af, field_name, value)
artifact_type.pre_create_hook(context, af)
af = af.create(context)
artifact_type.post_create_hook(context, af)
# notify about new artifact
Notifier.notify(context, action_name, af)
# return artifact to the user
@ -266,9 +279,6 @@ class Engine(object):
if not updates:
return af.to_dict()
for action_name in action_names:
policy.authorize(action_name, af.to_dict(), context)
if any(i in updates for i in ('name', 'version', 'visibility')):
# to change an artifact scope it's required to set a lock first
with self._create_scoped_lock(
@ -279,8 +289,14 @@ class Engine(object):
else:
modified_af = af.save(context)
# call post hooks for all operations when data is written in db and
# send broadcast notifications
for action_name in action_names:
Notifier.notify(context, action_name, modified_af)
getattr(modified_af, 'post_%s_hook' % action_name)(
context, modified_af)
Notifier.notify(
context, 'artifact:' + action_name, modified_af)
return modified_af.to_dict()
def show(self, context, type_name, artifact_id):
@ -353,7 +369,7 @@ class Engine(object):
af = self._show_artifact(context, type_name, artifact_id)
action_name = 'artifact:delete'
policy.authorize(action_name, af.to_dict(), context)
af.validate_delete(context, af)
af.pre_delete_hook(context, af)
blobs = af.delete(context, af)
delayed_delete = getattr(
@ -369,6 +385,7 @@ class Engine(object):
LOG.info("Blobs successfully deleted for artifact %s", af.id)
# delete artifact itself
af.db_api.delete(context, af.id)
af.post_delete_hook(context, af)
Notifier.notify(context, action_name, af)
@staticmethod
@ -450,6 +467,8 @@ class Engine(object):
"%(af)s") % {'blob': field_name, 'af': af.id}
raise exception.Conflict(message=msg)
utils.validate_change_allowed(af, field_name)
af.pre_add_location_hook(
context, af, field_name, location, blob_key)
modified_af = self._save_blob_info(
context, af, field_name, blob_key, blob)
@ -458,6 +477,8 @@ class Engine(object):
{'location': location, 'artifact': af.id,
'blob': blob_name})
modified_af.post_add_location_hook(
context, modified_af, field_name, blob_key)
Notifier.notify(context, action_name, modified_af)
return modified_af.to_dict()
@ -548,11 +569,16 @@ class Engine(object):
{'artifact': af.id, 'blob_name': blob_name})
# try to perform blob uploading to storage
path = None
try:
try:
# call upload hook first
fd, path = af.validate_upload(context, af, field_name, fd)
if hasattr(af, 'validate_upload'):
LOG.warning("Method 'validate_upload' was deprecated. "
"Please use 'pre_upload_hook' instead.")
fd, path = af.validate_upload(context, af, field_name, fd)
else:
fd = af.pre_upload_hook(
context, af, field_name, blob_key, fd)
except exception.GlareException:
raise
except Exception as e:
@ -576,9 +602,6 @@ class Engine(object):
blob_dict_attr = getattr(modified_af, field_name)
del blob_dict_attr[blob_key]
af.update_blob(context, af.id, field_name, blob_dict_attr)
finally:
if path:
os.remove(path)
LOG.info("Successfully finished blob uploading for artifact "
"%(artifact)s blob field %(blob)s.",
@ -595,6 +618,9 @@ class Engine(object):
modified_af = self._save_blob_info(
context, af, field_name, blob_key, blob)
modified_af.post_upload_hook(
context, modified_af, field_name, blob_key)
Notifier.notify(context, action_name, modified_af)
return modified_af.to_dict()
@ -628,6 +654,8 @@ class Engine(object):
msg = _("%s is not ready for download") % blob_name
raise exception.Conflict(message=msg)
af.pre_download_hook(context, af, field_name, blob_key)
meta = {'md5': blob.get('md5'),
'sha1': blob.get('sha1'),
'sha256': blob.get('sha256'),
@ -639,18 +667,14 @@ class Engine(object):
meta['size'] = blob.get('size')
meta['content_type'] = blob.get('content_type')
path = None
try:
# call download hook in the end
data, path = af.validate_download(
context, af, field_name, data)
data = af.post_download_hook(
context, af, field_name, blob_key, data)
except exception.GlareException:
raise
except Exception as e:
raise exception.BadRequest(message=str(e))
finally:
if path:
os.remove(path)
return data, meta

View File

@ -478,29 +478,87 @@ Possible values:
af_upd = cls.db_api.update_blob(context, af_id, {field_name: values})
return cls.init_artifact(context, af_upd)
# Next comes a collection of hooks for various operations
@classmethod
def validate_activate(cls, context, af):
"""Validation hook for activation."""
def pre_create_hook(cls, context, af):
pass
@classmethod
def validate_upload(cls, context, af, field_name, fd):
"""Validation hook for uploading."""
return fd, None
@classmethod
def validate_download(cls, context, af, field_name, fd):
"""Validation hook for downloading."""
return fd, None
@classmethod
def validate_publish(cls, context, af):
"""Validation hook for publishing."""
def post_create_hook(cls, context, af):
pass
@classmethod
def validate_delete(cls, context, af):
"""Validation hook for deletion."""
def pre_update_hook(cls, context, af):
pass
@classmethod
def post_update_hook(cls, context, af):
pass
@classmethod
def pre_activate_hook(cls, context, af):
pass
@classmethod
def post_activate_hook(cls, context, af):
pass
@classmethod
def pre_publish_hook(cls, context, af):
pass
@classmethod
def post_publish_hook(cls, context, af):
pass
@classmethod
def pre_deactivate_hook(cls, context, af):
pass
@classmethod
def post_deactivate_hook(cls, context, af):
pass
@classmethod
def pre_reactivate_hook(cls, context, af):
pass
@classmethod
def post_reactivate_hook(cls, context, af):
pass
@classmethod
def pre_upload_hook(cls, context, af, field_name, blob_key, fd):
return fd
@classmethod
def post_upload_hook(cls, context, af, field_name, blob_key):
pass
@classmethod
def pre_add_location_hook(
cls, context, af, field_name, blob_key, location):
pass
@classmethod
def post_add_location_hook(cls, context, af, field_name, blob_key):
pass
@classmethod
def pre_download_hook(cls, context, af, field_name, blob_key):
pass
@classmethod
def post_download_hook(cls, context, af, field_name, blob_key, fd):
return fd
@classmethod
def pre_delete_hook(cls, context, af):
pass
@classmethod
def post_delete_hook(cls, context, af):
pass
def to_notification(self):

View File

@ -12,16 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import shutil
import tempfile
from oslo_config import cfg
from oslo_log import log as logging
from oslo_versionedobjects import fields
from glare.common import exception
from glare.objects import base
from glare.objects.meta import file_utils
from glare.objects.meta import wrappers
Field = wrappers.Field.init
@ -36,21 +33,41 @@ LOG = logging.getLogger(__name__)
class HookChecker(base.BaseArtifact):
fields = {
'zip': Blob(description="Original zipped data.",
required_on_activate=False),
'content': Folder(system=True, required_on_activate=False),
'forbid_activate': Field(fields.FlexibleBooleanField,
default=False),
'forbid_publish': Field(fields.FlexibleBooleanField,
default=False, mutable=True),
'forbid_download_zip': Field(fields.FlexibleBooleanField,
default=False),
'forbid_delete': Field(fields.FlexibleBooleanField,
default=False, mutable=True),
'temp_dir': Field(
fields.StringField,
required_on_activate=False,
mutable=True),
'temp_file_path_create': Field(
fields.StringField,
required_on_activate=False,
mutable=True),
'temp_file_path_update': Field(
fields.StringField,
required_on_activate=False,
mutable=True),
'temp_file_path_activate': Field(
fields.StringField,
required_on_activate=False,
mutable=True),
'temp_file_path_reactivate': Field(
fields.StringField,
required_on_activate=False,
mutable=True),
'temp_file_path_deactivate': Field(
fields.StringField,
required_on_activate=False,
mutable=True),
'temp_file_path_publish': Field(
fields.StringField,
required_on_activate=False,
mutable=True),
'blob': Blob(
required_on_activate=False,
mutable=True)
}
artifact_type_opts = [
cfg.BoolOpt('in_memory_processing')
cfg.StrOpt('temp_file_path')
]
@classmethod
@ -58,61 +75,142 @@ class HookChecker(base.BaseArtifact):
return "hooks_artifact"
@classmethod
def _validate_upload_harddrive(cls, context, af, field_name, fd):
path = None
tdir = None
try:
tfile, path = file_utils.create_temporary_file(fd, '.zip')
tdir = file_utils.extract_zip_to_temporary_folder(tfile)
# upload all files to 'content' folder
for subdir, dirs, files in os.walk(tdir):
for file_name in files:
path_to_file = os.path.join(subdir, file_name)
with open(path_to_file, "rb") as f:
file_utils.upload_content_file(
context, af, f, 'content',
path_to_file[len(tdir) + 1:])
except Exception as e:
if path is not None and os.path.exists(path):
# remove temporary file if something went wrong
os.remove(path)
raise e
finally:
# remove temporary folder
if tdir is not None:
shutil.rmtree(tdir)
tfile.flush()
tfile.seek(0)
return tfile, path
def pre_create_hook(cls, context, af):
# create a temporary file and set the path to artifact field
__, af.temp_file_path_create = tempfile.mkstemp(dir=af.temp_dir)
with open(af.temp_file_path_create, 'w') as f:
f.write('pre_create_hook was called\n')
@classmethod
def validate_upload(cls, context, af, field_name, fd):
if getattr(CONF, 'artifact_type:hooks_artifact').in_memory_processing:
return file_utils.unpack_zip_archive_in_memory(
context, af, 'content', fd), None
else:
return cls._validate_upload_harddrive(
context, af, field_name, fd)
def post_create_hook(cls, context, af):
with open(af.temp_file_path_create, 'a') as f:
f.write('post_create_hook was called\n')
@classmethod
def validate_download(cls, context, af, field_name, fd):
if af.forbid_download_zip and field_name == 'zip':
raise exception.BadRequest
return fd, None
def pre_update_hook(cls, context, af):
# create a temporary file and set the path to artifact field
__, af.temp_file_path_update = tempfile.mkstemp(dir=af.temp_dir)
with open(af.temp_file_path_update, 'w') as f:
f.write('pre_update_hook was called\n')
@classmethod
def validate_activate(cls, context, af):
if af.forbid_activate:
raise exception.BadRequest
def post_update_hook(cls, context, af):
with open(af.temp_file_path_update, 'a') as f:
f.write('post_update_hook was called\n')
@classmethod
def validate_publish(cls, context, af):
if af.forbid_publish:
raise exception.BadRequest
def pre_activate_hook(cls, context, af):
# create a temporary file and set the path to artifact field
__, af.temp_file_path_activate = tempfile.mkstemp(dir=af.temp_dir)
with open(af.temp_file_path_activate, 'w') as f:
f.write('pre_activate_hook was called\n')
@classmethod
def validate_delete(cls, context, af):
if af.forbid_delete:
raise exception.BadRequest
def post_activate_hook(cls, context, af):
with open(af.temp_file_path_activate, 'a') as f:
f.write('post_activate_hook was called\n')
@classmethod
def pre_publish_hook(cls, context, af):
# create a temporary file and set the path to artifact field
__, af.temp_file_path_publish = tempfile.mkstemp(dir=af.temp_dir)
with open(af.temp_file_path_publish, 'w') as f:
f.write('pre_publish_hook was called\n')
@classmethod
def post_publish_hook(cls, context, af):
with open(af.temp_file_path_publish, 'a') as f:
f.write('post_publish_hook was called\n')
@classmethod
def pre_deactivate_hook(cls, context, af):
# create a temporary file and set the path to artifact field
__, af.temp_file_path_deactivate = tempfile.mkstemp(dir=af.temp_dir)
with open(af.temp_file_path_deactivate, 'w') as f:
f.write('pre_deactivate_hook was called\n')
@classmethod
def post_deactivate_hook(cls, context, af):
with open(af.temp_file_path_deactivate, 'a') as f:
f.write('post_deactivate_hook was called\n')
@classmethod
def pre_reactivate_hook(cls, context, af):
# create a temporary file and set the path to artifact field
__, af.temp_file_path_reactivate = tempfile.mkstemp(dir=af.temp_dir)
with open(af.temp_file_path_reactivate, 'w') as f:
f.write('pre_reactivate_hook was called\n')
@classmethod
def post_reactivate_hook(cls, context, af):
with open(af.temp_file_path_reactivate, 'a') as f:
f.write('post_reactivate_hook was called\n')
@classmethod
def pre_upload_hook(cls, context, af, field_name, blob_key, fd):
# create a temporary file and set the path to artifact field
file_path = getattr(
CONF, 'artifact_type:hooks_artifact').temp_file_path
if file_path:
with open(file_path, 'w') as f:
f.write('pre_upload_hook was called\n')
return fd
@classmethod
def post_upload_hook(cls, context, af, field_name, blob_key):
file_path = getattr(
CONF, 'artifact_type:hooks_artifact').temp_file_path
if file_path:
with open(file_path, 'a') as f:
f.write('post_upload_hook was called\n')
@classmethod
def pre_add_location_hook(
cls, context, af, field_name, blob_key, location):
# create a temporary file and set the path to artifact field
file_path = getattr(
CONF, 'artifact_type:hooks_artifact').temp_file_path
if file_path:
with open(file_path, 'w') as f:
f.write('pre_add_location_hook was called\n')
@classmethod
def post_add_location_hook(cls, context, af, field_name, blob_key):
file_path = getattr(
CONF, 'artifact_type:hooks_artifact').temp_file_path
if file_path:
with open(file_path, 'a') as f:
f.write('post_add_location_hook was called\n')
@classmethod
def pre_download_hook(cls, context, af, field_name, blob_key):
file_path = getattr(
CONF, 'artifact_type:hooks_artifact').temp_file_path
if file_path:
with open(file_path, 'a') as f:
f.write('pre_download_hook was called\n')
@classmethod
def post_download_hook(cls, context, af, field_name, blob_key, fd):
file_path = getattr(
CONF, 'artifact_type:hooks_artifact').temp_file_path
if file_path:
with open(file_path, 'a') as f:
f.write('post_download_hook was called\n')
return fd
@classmethod
def pre_delete_hook(cls, context, af):
file_path = getattr(
CONF, 'artifact_type:hooks_artifact').temp_file_path
if file_path:
with open(file_path, 'w') as f:
f.write('pre_delete_hook was called\n')
@classmethod
def post_delete_hook(cls, context, af):
file_path = getattr(
CONF, 'artifact_type:hooks_artifact').temp_file_path
if file_path:
with open(file_path, 'a') as f:
f.write('post_delete_hook was called\n')

View File

@ -252,25 +252,21 @@ class TestArtifactUpload(base.BaseTestArtifactAPI):
self.sample_artifact['id'])
self.assertNotIn('blb', artifact['dict_of_blobs'])
@mock.patch('os.remove')
def test_upload_with_hook(self, mocked_os_remove):
def test_upload_with_hook(self):
with mock.patch.object(
sample_artifact.SampleArtifact, 'validate_upload',
return_value=(BytesIO(b'aaa'), 'temporary_path')):
sample_artifact.SampleArtifact, 'pre_upload_hook',
return_value=BytesIO(b'ffff')):
self.controller.upload_blob(
self.req, 'sample_artifact', self.sample_artifact['id'],
'blob', BytesIO(b'aaa'), 'application/octet-stream')
artifact = self.controller.show(self.req, 'sample_artifact',
self.sample_artifact['id'])
self.assertEqual(3, artifact['blob']['size'])
self.assertEqual(4, artifact['blob']['size'])
self.assertEqual('active', artifact['blob']['status'])
# If temporary folder has been created it must be removed
mocked_os_remove.assert_called_once_with('temporary_path')
@mock.patch('os.remove')
def test_upload_with_hook_error(self, mocked_os_remove):
def test_upload_with_hook_error(self):
with mock.patch.object(
sample_artifact.SampleArtifact, 'validate_upload',
sample_artifact.SampleArtifact, 'pre_upload_hook',
side_effect=Exception):
self.assertRaises(
exc.BadRequest, self.controller.upload_blob,
@ -280,7 +276,6 @@ class TestArtifactUpload(base.BaseTestArtifactAPI):
art = self.controller.show(self.req, 'sample_artifact',
self.sample_artifact['id'])
self.assertEqual({}, art['dict_of_blobs'])
self.assertEqual(0, mocked_os_remove.call_count)
def test_upload_nonexistent_field(self):
self.assertRaises(

View File

@ -12,187 +12,115 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import mock
import os
import tempfile
from six import BytesIO
from glare.common import exception as exc
from glare.tests.unit import base
class TestArtifactHooks(base.BaseTestArtifactAPI):
def setUp(self):
super(TestArtifactHooks, self).setUp()
values = {'name': 'ttt', 'version': '1.0'}
self.hooks_artifact = self.controller.create(
self.req, 'hooks_artifact', values)
def test_create_hook(self):
values = {'name': 'ttt', 'version': '1.0', 'temp_dir': self.test_dir}
art = self.controller.create(self.req, 'hooks_artifact', values)
self.assertEqual(self.test_dir, art['temp_dir'])
self.assertIsNotNone(art['temp_file_path_create'])
with open(art['temp_file_path_create']) as f:
self.assertEqual('pre_create_hook was called\n', f.readline())
self.assertEqual('post_create_hook was called\n', f.readline())
def test_upload_hook(self):
var_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
'../', 'var'))
data_path = os.path.join(var_dir, 'hooks.zip')
with open(data_path, "rb") as data:
self.controller.upload_blob(
self.req, 'hooks_artifact', self.hooks_artifact['id'], 'zip',
data, 'application/octet-stream')
artifact = self.controller.show(self.req, 'hooks_artifact',
self.hooks_artifact['id'])
self.assertEqual(818, artifact['zip']['size'])
self.assertEqual('active', artifact['zip']['status'])
self.assertEqual(11, artifact['content']['aaa.txt']['size'])
self.assertEqual(11, artifact['content']['folder1/bbb.txt']['size'])
self.assertEqual(
11, artifact['content']['folder1/folder2/ccc.txt']['size'])
def test_upload_hook_inmemory(self):
# enable in-memory processing
self.config(in_memory_processing=True,
group='artifact_type:hooks_artifact')
# First check uploading with smaller limit fails
with mock.patch('glare.objects.meta.file_utils.'
'INMEMORY_OBJECT_SIZE_LIMIT', 817):
self.assertRaises(exc.RequestEntityTooLarge, self.test_upload_hook)
# Now try with standard limit
self.test_upload_hook()
def test_download_hook(self):
# upload data
var_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
'../', 'var'))
data_path = os.path.join(var_dir, 'hooks.zip')
with open(data_path, "rb") as data:
self.controller.upload_blob(
self.req, 'hooks_artifact', self.hooks_artifact['id'], 'zip',
data, 'application/octet-stream')
# download main 'zip'
data = self.controller.download_blob(
self.req, 'hooks_artifact', self.hooks_artifact['id'],
'zip')['data']
bytes_read = 0
for chunk in data:
bytes_read += len(chunk)
self.assertEqual(818, bytes_read)
# download a file from 'content'
data = self.controller.download_blob(
self.req, 'hooks_artifact', self.hooks_artifact['id'],
'content/folder1/bbb.txt')['data']
bytes_read = 0
for chunk in data:
bytes_read += len(chunk)
self.assertEqual(11, bytes_read)
# now forbid to download zip
changes = [{'op': 'replace', 'path': '/forbid_download_zip',
'value': 'yes'}]
self.update_with_values(changes, art_type='hooks_artifact',
art_id=self.hooks_artifact['id'])
artifact = self.controller.show(self.req, 'hooks_artifact',
self.hooks_artifact['id'])
self.assertEqual(True, artifact['forbid_download_zip'])
# download from 'zip' fails now
self.assertRaises(
exc.BadRequest, self.controller.download_blob,
self.req, 'hooks_artifact', self.hooks_artifact['id'], 'zip')
# download a 'content' file still works
data = self.controller.download_blob(
self.req, 'hooks_artifact', self.hooks_artifact['id'],
'content/folder1/folder2/ccc.txt')['data']
bytes_read = 0
for chunk in data:
bytes_read += len(chunk)
self.assertEqual(11, bytes_read)
def test_activation_hook(self):
# forbid to activate artifact
changes = [{'op': 'replace', 'path': '/forbid_activate',
'value': 'yes'}]
self.update_with_values(changes, art_type='hooks_artifact',
art_id=self.hooks_artifact['id'])
# activation fails now
changes = [{'op': 'replace', 'path': '/status',
'value': 'active'}]
self.assertRaises(
exc.BadRequest, self.update_with_values, changes,
art_type='hooks_artifact', art_id=self.hooks_artifact['id'])
# unblock artifact activation
changes = [{'op': 'replace', 'path': '/forbid_activate',
'value': 'no'}]
self.update_with_values(changes, art_type='hooks_artifact',
art_id=self.hooks_artifact['id'])
# now activation works
changes = [{'op': 'replace', 'path': '/status',
'value': 'active'}]
art = self.update_with_values(changes, art_type='hooks_artifact',
art_id=self.hooks_artifact['id'])
self.assertEqual('active', art['status'])
def test_publishing_hook(self):
def test_update_ops_hook(self):
self.req = self.get_fake_request(user=self.users['admin'])
values = {'name': 'ttt', 'version': '1.0', 'temp_dir': self.test_dir}
art = self.controller.create(self.req, 'hooks_artifact', values)
self.assertEqual(self.test_dir, art['temp_dir'])
# activate artifact to begin
changes = [{'op': 'replace', 'path': '/status',
'value': 'active'}]
changes = [{'op': 'replace', 'path': '/description',
'value': 'some_string'},
{'op': 'replace', 'path': '/status',
'value': 'active'},
{'op': 'replace', 'path': '/status',
'value': 'deactivated'},
{'op': 'replace', 'path': '/status',
'value': 'active'},
{'op': 'replace', 'path': '/visibility',
'value': 'public'}]
art = self.update_with_values(changes, art_type='hooks_artifact',
art_id=self.hooks_artifact['id'])
art_id=art['id'])
self.assertEqual('active', art['status'])
# forbid to publish artifact
changes = [{'op': 'replace', 'path': '/forbid_publish',
'value': 'yes'}]
self.update_with_values(changes, art_type='hooks_artifact',
art_id=self.hooks_artifact['id'])
# publication fails now
changes = [{'op': 'replace', 'path': '/visibility',
'value': 'public'}]
self.assertRaises(
exc.BadRequest, self.update_with_values, changes,
art_type='hooks_artifact', art_id=self.hooks_artifact['id'])
# unblock artifact publication
changes = [{'op': 'replace', 'path': '/forbid_publish',
'value': 'no'}]
self.update_with_values(changes, art_type='hooks_artifact',
art_id=self.hooks_artifact['id'])
# now publication works
changes = [{'op': 'replace', 'path': '/visibility',
'value': 'public'}]
art = self.update_with_values(changes, art_type='hooks_artifact',
art_id=self.hooks_artifact['id'])
self.assertEqual('some_string', art['description'])
self.assertEqual('public', art['visibility'])
def test_deletion_hook(self):
# forbid to activate artifact
changes = [{'op': 'replace', 'path': '/forbid_delete',
'value': 'yes'}]
self.update_with_values(changes, art_type='hooks_artifact',
art_id=self.hooks_artifact['id'])
actions = ['update', 'activate', 'deactivate', 'reactivate', 'publish']
for action in actions:
with open(art['temp_file_path_%s' % action]) as f:
self.assertEqual('pre_%s_hook was called\n' % action,
f.readline())
self.assertEqual('post_%s_hook was called\n' % action,
f.readline())
# deletion fails now
self.assertRaises(
exc.BadRequest, self.controller.delete, self.req,
'hooks_artifact', self.hooks_artifact['id'])
def test_upload_download_hooks(self):
temp_file_path = tempfile.mktemp(dir=self.test_dir)
self.config(temp_file_path=temp_file_path,
group='artifact_type:hooks_artifact')
# unblock artifact deletion
changes = [{'op': 'replace', 'path': '/forbid_delete',
'value': 'no'}]
self.update_with_values(changes, art_type='hooks_artifact',
art_id=self.hooks_artifact['id'])
values = {'name': 'ttt', 'version': '1.0', 'temp_dir': self.test_dir}
art = self.controller.create(self.req, 'hooks_artifact', values)
# now deletion works
self.controller.delete(self.req, 'hooks_artifact',
self.hooks_artifact['id'])
self.assertRaises(exc.NotFound, self.controller.show, self.req,
'hooks_artifact', self.hooks_artifact['id'])
art = self.controller.upload_blob(
self.req, 'hooks_artifact', art['id'], 'blob',
BytesIO(b'aaa'), 'application/octet-stream')
self.assertEqual(3, art['blob']['size'])
self.assertEqual('active', art['blob']['status'])
self.controller.download_blob(
self.req, 'hooks_artifact', art['id'], 'blob')
with open(temp_file_path) as f:
self.assertEqual('pre_upload_hook was called\n', f.readline())
self.assertEqual('post_upload_hook was called\n', f.readline())
self.assertEqual('pre_download_hook was called\n', f.readline())
self.assertEqual('post_download_hook was called\n', f.readline())
def test_add_location_hook(self):
temp_file_path = tempfile.mktemp(dir=self.test_dir)
self.config(temp_file_path=temp_file_path,
group='artifact_type:hooks_artifact')
values = {'name': 'ttt', 'version': '1.0', 'temp_dir': self.test_dir}
art = self.controller.create(self.req, 'hooks_artifact', values)
ct = 'application/vnd+openstack.glare-custom-location+json'
body = {'url': 'https://FAKE_LOCATION.com',
'md5': "fake", 'sha1': "fake_sha", "sha256": "fake_sha256"}
art = self.controller.upload_blob(
self.req, 'hooks_artifact', art['id'], 'blob', body, ct)
self.assertIsNone(art['blob']['size'])
self.assertEqual('active', art['blob']['status'])
# hook isn't called if we download external location
self.controller.download_blob(
self.req, 'hooks_artifact', art['id'], 'blob')
with open(temp_file_path) as f:
self.assertEqual(
'pre_add_location_hook was called\n', f.readline())
self.assertEqual(
'post_add_location_hook was called\n', f.readline())
def test_delete_hook(self):
temp_file_path = tempfile.mktemp(dir=self.test_dir)
self.config(temp_file_path=temp_file_path,
group='artifact_type:hooks_artifact')
values = {'name': 'ttt', 'version': '1.0', 'temp_dir': self.test_dir}
art = self.controller.create(self.req, 'hooks_artifact', values)
self.controller.delete(self.req, 'hooks_artifact', art['id'])
with open(temp_file_path) as f:
self.assertEqual('pre_delete_hook was called\n', f.readline())
self.assertEqual('post_delete_hook was called\n', f.readline())