diff --git a/glance/async_/flows/api_image_import.py b/glance/async_/flows/api_image_import.py index a7ca79d4bf..ea79f99ebd 100644 --- a/glance/async_/flows/api_image_import.py +++ b/glance/async_/flows/api_image_import.py @@ -29,6 +29,7 @@ from taskflow.patterns import linear_flow as lf from taskflow import retry from taskflow import task +from glance.api import common as api_common import glance.async_.flows._internal_plugins as internal_plugins import glance.async_.flows.plugins as import_plugins from glance.common import exception @@ -301,6 +302,24 @@ class _ImportActions(object): raise AttributeError('Setting %s is not allowed' % attr) setattr(self._image, attr, value) + def set_image_extra_properties(self, properties): + """Merge values into image extra_properties. + + This allows a plugin to set additional properties on the image, + as long as those are outside the reserved namespace. Any keys + in the internal namespace will be dropped (and logged). + + :param properties: A dict of properties to be merged in + """ + for key, value in properties.items(): + if key.startswith(api_common.GLANCE_RESERVED_NS): + LOG.warning(('Dropping %(key)s=%(val)s during metadata ' + 'injection for %(image)s'), + {'key': key, 'val': value, + 'image': self.image_id}) + else: + self._image.extra_properties[key] = value + def remove_location_for_store(self, backend): """Remove a location from an image given a backend store. diff --git a/glance/tests/unit/async_/flows/test_api_image_import.py b/glance/tests/unit/async_/flows/test_api_image_import.py index 48ed12a17c..027e680d5f 100644 --- a/glance/tests/unit/async_/flows/test_api_image_import.py +++ b/glance/tests/unit/async_/flows/test_api_image_import.py @@ -689,6 +689,47 @@ class TestImportActionWrapper(test_utils.BaseTestCase): self.assertRaises(AttributeError, action.set_image_attribute, id='foo') + @mock.patch.object(import_flow, 'LOG') + def test_set_image_extra_properties(self, mock_log): + mock_repo = mock.MagicMock() + mock_image = mock_repo.get.return_value + mock_image.image_id = IMAGE_ID1 + mock_image.extra_properties = {'os_glance_import_task': TASK_ID1} + mock_image.status = 'bar' + wrapper = import_flow.ImportActionWrapper(mock_repo, IMAGE_ID1, + TASK_ID1) + # One banned property + with wrapper as action: + action.set_image_extra_properties({'os_glance_foo': 'bar'}) + self.assertEqual({'os_glance_import_task': TASK_ID1}, + mock_image.extra_properties) + + mock_log.warning.assert_called() + mock_log.warning.reset_mock() + + # Two banned properties + with wrapper as action: + action.set_image_extra_properties({'os_glance_foo': 'bar', + 'os_glance_baz': 'bat'}) + self.assertEqual({'os_glance_import_task': TASK_ID1}, + mock_image.extra_properties) + + mock_log.warning.assert_called() + mock_log.warning.reset_mock() + + # One banned and one allowed property + with wrapper as action: + action.set_image_extra_properties({'foo': 'bar', + 'os_glance_foo': 'baz'}) + self.assertEqual({'foo': 'bar', + 'os_glance_import_task': TASK_ID1}, + mock_image.extra_properties) + + mock_log.warning.assert_called_once_with( + 'Dropping %(key)s=%(val)s during metadata injection for %(image)s', + {'key': 'os_glance_foo', 'val': 'baz', + 'image': IMAGE_ID1}) + def test_drop_lock_for_task(self): mock_repo = mock.MagicMock() mock_repo.get.return_value.extra_properties = {