Merge "Revert image state to queued if conversion fails"
This commit is contained in:
commit
695fcb67c9
@ -63,13 +63,16 @@ class _ConvertImage(task.Task):
|
|||||||
|
|
||||||
default_provides = 'file_path'
|
default_provides = 'file_path'
|
||||||
|
|
||||||
def __init__(self, context, task_id, task_type, action_wrapper):
|
def __init__(self, context, task_id, task_type, action_wrapper,
|
||||||
|
stores):
|
||||||
self.context = context
|
self.context = context
|
||||||
self.task_id = task_id
|
self.task_id = task_id
|
||||||
self.task_type = task_type
|
self.task_type = task_type
|
||||||
self.action_wrapper = action_wrapper
|
self.action_wrapper = action_wrapper
|
||||||
|
self.stores = stores
|
||||||
self.image_id = action_wrapper.image_id
|
self.image_id = action_wrapper.image_id
|
||||||
self.dest_path = ""
|
self.dest_path = ""
|
||||||
|
self.src_path = ""
|
||||||
self.python = CONF.wsgi.python_interpreter
|
self.python = CONF.wsgi.python_interpreter
|
||||||
super(_ConvertImage, self).__init__(
|
super(_ConvertImage, self).__init__(
|
||||||
name='%s-Convert_Image-%s' % (task_type, task_id))
|
name='%s-Convert_Image-%s' % (task_type, task_id))
|
||||||
@ -83,8 +86,8 @@ class _ConvertImage(task.Task):
|
|||||||
target_format = CONF.image_conversion.output_format
|
target_format = CONF.image_conversion.output_format
|
||||||
# TODO(jokke): Once we support other schemas we need to take them into
|
# TODO(jokke): Once we support other schemas we need to take them into
|
||||||
# account and handle the paths here.
|
# account and handle the paths here.
|
||||||
src_path = file_path.split('file://')[-1]
|
self.src_path = file_path.split('file://')[-1]
|
||||||
dest_path = "%(path)s.%(target)s" % {'path': src_path,
|
dest_path = "%(path)s.%(target)s" % {'path': self.src_path,
|
||||||
'target': target_format}
|
'target': target_format}
|
||||||
self.dest_path = dest_path
|
self.dest_path = dest_path
|
||||||
|
|
||||||
@ -104,7 +107,7 @@ class _ConvertImage(task.Task):
|
|||||||
# qemu-img on it.
|
# qemu-img on it.
|
||||||
# See https://bugs.launchpad.net/nova/+bug/2059809 for details.
|
# See https://bugs.launchpad.net/nova/+bug/2059809 for details.
|
||||||
try:
|
try:
|
||||||
inspector = inspector_cls.from_file(src_path)
|
inspector = inspector_cls.from_file(self.src_path)
|
||||||
if not inspector.safety_check():
|
if not inspector.safety_check():
|
||||||
LOG.error('Image failed %s safety check; aborting conversion',
|
LOG.error('Image failed %s safety check; aborting conversion',
|
||||||
source_format)
|
source_format)
|
||||||
@ -123,7 +126,7 @@ class _ConvertImage(task.Task):
|
|||||||
stdout, stderr = putils.trycmd("qemu-img", "info",
|
stdout, stderr = putils.trycmd("qemu-img", "info",
|
||||||
"-f", source_format,
|
"-f", source_format,
|
||||||
"--output=json",
|
"--output=json",
|
||||||
src_path,
|
self.src_path,
|
||||||
prlimit=utils.QEMU_IMG_PROC_LIMITS,
|
prlimit=utils.QEMU_IMG_PROC_LIMITS,
|
||||||
python_exec=self.python,
|
python_exec=self.python,
|
||||||
log_errors=putils.LOG_ALL_ERRORS,)
|
log_errors=putils.LOG_ALL_ERRORS,)
|
||||||
@ -186,7 +189,7 @@ class _ConvertImage(task.Task):
|
|||||||
stdout, stderr = putils.trycmd('qemu-img', 'convert',
|
stdout, stderr = putils.trycmd('qemu-img', 'convert',
|
||||||
'-f', source_format,
|
'-f', source_format,
|
||||||
'-O', target_format,
|
'-O', target_format,
|
||||||
src_path, dest_path,
|
self.src_path, dest_path,
|
||||||
log_errors=putils.LOG_ALL_ERRORS)
|
log_errors=putils.LOG_ALL_ERRORS)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
@ -204,7 +207,7 @@ class _ConvertImage(task.Task):
|
|||||||
LOG.info(_LI('Updated image %s size=%i disk_format=%s'),
|
LOG.info(_LI('Updated image %s size=%i disk_format=%s'),
|
||||||
self.image_id, new_size, target_format)
|
self.image_id, new_size, target_format)
|
||||||
|
|
||||||
os.remove(src_path)
|
os.remove(self.src_path)
|
||||||
|
|
||||||
return "file://%s" % dest_path
|
return "file://%s" % dest_path
|
||||||
|
|
||||||
@ -217,6 +220,23 @@ class _ConvertImage(task.Task):
|
|||||||
if os.path.exists(self.dest_path):
|
if os.path.exists(self.dest_path):
|
||||||
os.remove(self.dest_path)
|
os.remove(self.dest_path)
|
||||||
|
|
||||||
|
# NOTE(abhishekk): If we failed to convert the image, then none
|
||||||
|
# of the _ImportToStore() tasks could have run, so we need
|
||||||
|
# to move all stores out of "importing" to "failed".
|
||||||
|
with self.action_wrapper as action:
|
||||||
|
action.set_image_attribute(status='queued')
|
||||||
|
if self.stores:
|
||||||
|
action.remove_importing_stores(self.stores)
|
||||||
|
action.add_failed_stores(self.stores)
|
||||||
|
|
||||||
|
if self.src_path:
|
||||||
|
try:
|
||||||
|
os.remove(self.src_path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
# NOTE(abhishekk): We must have raced with something
|
||||||
|
# else, so this is not a problem
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_flow(**kwargs):
|
def get_flow(**kwargs):
|
||||||
"""Return task flow for no-op.
|
"""Return task flow for no-op.
|
||||||
@ -232,7 +252,8 @@ def get_flow(**kwargs):
|
|||||||
task_id = kwargs.get('task_id')
|
task_id = kwargs.get('task_id')
|
||||||
task_type = kwargs.get('task_type')
|
task_type = kwargs.get('task_type')
|
||||||
action_wrapper = kwargs.get('action_wrapper')
|
action_wrapper = kwargs.get('action_wrapper')
|
||||||
|
stores = kwargs.get('backend', [])
|
||||||
|
|
||||||
return lf.Flow(task_type).add(
|
return lf.Flow(task_type).add(
|
||||||
_ConvertImage(context, task_id, task_type, action_wrapper)
|
_ConvertImage(context, task_id, task_type, action_wrapper, stores)
|
||||||
)
|
)
|
||||||
|
@ -59,6 +59,7 @@ class TestConvertImageTask(test_utils.BaseTestCase):
|
|||||||
self.context = mock.MagicMock()
|
self.context = mock.MagicMock()
|
||||||
self.img_repo = mock.MagicMock()
|
self.img_repo = mock.MagicMock()
|
||||||
self.task_repo = mock.MagicMock()
|
self.task_repo = mock.MagicMock()
|
||||||
|
self.stores = mock.MagicMock()
|
||||||
self.image_id = UUID1
|
self.image_id = UUID1
|
||||||
|
|
||||||
self.gateway = gateway.Gateway()
|
self.gateway = gateway.Gateway()
|
||||||
@ -87,7 +88,10 @@ class TestConvertImageTask(test_utils.BaseTestCase):
|
|||||||
task_input=task_input)
|
task_input=task_input)
|
||||||
|
|
||||||
self.image.extra_properties = {
|
self.image.extra_properties = {
|
||||||
'os_glance_import_task': self.task.task_id}
|
'os_glance_import_task': self.task.task_id,
|
||||||
|
'os_glance_importing_to_stores': mock.MagicMock(),
|
||||||
|
'os_glance_failed_import': ""
|
||||||
|
}
|
||||||
self.wrapper = import_flow.ImportActionWrapper(self.img_repo,
|
self.wrapper = import_flow.ImportActionWrapper(self.img_repo,
|
||||||
self.image_id,
|
self.image_id,
|
||||||
self.task.task_id)
|
self.task.task_id)
|
||||||
@ -105,7 +109,8 @@ class TestConvertImageTask(test_utils.BaseTestCase):
|
|||||||
image_convert = image_conversion._ConvertImage(self.context,
|
image_convert = image_conversion._ConvertImage(self.context,
|
||||||
self.task.task_id,
|
self.task.task_id,
|
||||||
self.task_type,
|
self.task_type,
|
||||||
self.wrapper)
|
self.wrapper,
|
||||||
|
self.stores)
|
||||||
|
|
||||||
self.task_repo.get.return_value = self.task
|
self.task_repo.get.return_value = self.task
|
||||||
image = mock.MagicMock(image_id=self.image_id, virtual_size=None,
|
image = mock.MagicMock(image_id=self.image_id, virtual_size=None,
|
||||||
@ -137,7 +142,8 @@ class TestConvertImageTask(test_utils.BaseTestCase):
|
|||||||
image_convert = image_conversion._ConvertImage(self.context,
|
image_convert = image_conversion._ConvertImage(self.context,
|
||||||
self.task.task_id,
|
self.task.task_id,
|
||||||
self.task_type,
|
self.task_type,
|
||||||
self.wrapper)
|
self.wrapper,
|
||||||
|
self.stores)
|
||||||
|
|
||||||
self.task_repo.get.return_value = self.task
|
self.task_repo.get.return_value = self.task
|
||||||
image = mock.MagicMock(image_id=self.image_id, virtual_size=None,
|
image = mock.MagicMock(image_id=self.image_id, virtual_size=None,
|
||||||
@ -354,15 +360,23 @@ class TestConvertImageTask(test_utils.BaseTestCase):
|
|||||||
image = self.img_repo.get.return_value
|
image = self.img_repo.get.return_value
|
||||||
self.assertEqual(123, image.virtual_size)
|
self.assertEqual(123, image.virtual_size)
|
||||||
|
|
||||||
@mock.patch.object(os, 'remove')
|
def _set_image_conversion(self, mock_os_remove, stores=[]):
|
||||||
def test_image_convert_revert_success(self, mock_os_remove):
|
|
||||||
mock_os_remove.return_value = None
|
mock_os_remove.return_value = None
|
||||||
|
wrapper = mock.MagicMock()
|
||||||
image_convert = image_conversion._ConvertImage(self.context,
|
image_convert = image_conversion._ConvertImage(self.context,
|
||||||
self.task.task_id,
|
self.task.task_id,
|
||||||
self.task_type,
|
self.task_type,
|
||||||
self.wrapper)
|
wrapper,
|
||||||
|
stores)
|
||||||
|
action = wrapper.__enter__.return_value
|
||||||
self.task_repo.get.return_value = self.task
|
self.task_repo.get.return_value = self.task
|
||||||
|
return action, image_convert
|
||||||
|
|
||||||
|
@mock.patch.object(os, 'remove')
|
||||||
|
def test_image_convert_revert_success_multiple_stores(
|
||||||
|
self, mock_os_remove):
|
||||||
|
action, image_convert = self._set_image_conversion(
|
||||||
|
mock_os_remove, stores=self.stores)
|
||||||
|
|
||||||
with mock.patch.object(processutils, 'execute') as exc_mock:
|
with mock.patch.object(processutils, 'execute') as exc_mock:
|
||||||
exc_mock.return_value = ("", None)
|
exc_mock.return_value = ("", None)
|
||||||
@ -370,6 +384,48 @@ class TestConvertImageTask(test_utils.BaseTestCase):
|
|||||||
os_exists_mock.return_value = True
|
os_exists_mock.return_value = True
|
||||||
image_convert.revert(result=mock.MagicMock())
|
image_convert.revert(result=mock.MagicMock())
|
||||||
self.assertEqual(1, mock_os_remove.call_count)
|
self.assertEqual(1, mock_os_remove.call_count)
|
||||||
|
action.set_image_attribute.assert_called_once_with(
|
||||||
|
status='queued')
|
||||||
|
action.remove_importing_stores.assert_called_once_with(
|
||||||
|
self.stores)
|
||||||
|
action.add_failed_stores.assert_called_once_with(
|
||||||
|
self.stores)
|
||||||
|
|
||||||
|
@mock.patch.object(os, 'remove')
|
||||||
|
def test_image_convert_revert_success_single_store(
|
||||||
|
self, mock_os_remove):
|
||||||
|
action, image_convert = self._set_image_conversion(mock_os_remove)
|
||||||
|
|
||||||
|
with mock.patch.object(processutils, 'execute') as exc_mock:
|
||||||
|
exc_mock.return_value = ("", None)
|
||||||
|
with mock.patch.object(os.path, 'exists') as os_exists_mock:
|
||||||
|
os_exists_mock.return_value = True
|
||||||
|
image_convert.revert(result=mock.MagicMock())
|
||||||
|
self.assertEqual(1, mock_os_remove.call_count)
|
||||||
|
self.assertEqual(0, action.remove_importing_stores.call_count)
|
||||||
|
self.assertEqual(0, action.add_failed_store.call_count)
|
||||||
|
action.set_image_attribute.assert_called_once_with(
|
||||||
|
status='queued')
|
||||||
|
|
||||||
|
@mock.patch.object(os, 'remove')
|
||||||
|
def test_image_convert_revert_success_src_file_exists(
|
||||||
|
self, mock_os_remove):
|
||||||
|
action, image_convert = self._set_image_conversion(
|
||||||
|
mock_os_remove, stores=self.stores)
|
||||||
|
image_convert.src_path = mock.MagicMock()
|
||||||
|
|
||||||
|
with mock.patch.object(processutils, 'execute') as exc_mock:
|
||||||
|
exc_mock.return_value = ("", None)
|
||||||
|
with mock.patch.object(os.path, 'exists') as os_exists_mock:
|
||||||
|
os_exists_mock.return_value = True
|
||||||
|
image_convert.revert(result=mock.MagicMock())
|
||||||
|
action.set_image_attribute.assert_called_once_with(
|
||||||
|
status='queued')
|
||||||
|
action.remove_importing_stores.assert_called_once_with(
|
||||||
|
self.stores)
|
||||||
|
action.add_failed_stores.assert_called_once_with(
|
||||||
|
self.stores)
|
||||||
|
self.assertEqual(2, mock_os_remove.call_count)
|
||||||
|
|
||||||
def test_image_convert_interpreter_configured(self):
|
def test_image_convert_interpreter_configured(self):
|
||||||
# By default, wsgi.python_interpreter is None; if it is
|
# By default, wsgi.python_interpreter is None; if it is
|
||||||
@ -380,5 +436,6 @@ class TestConvertImageTask(test_utils.BaseTestCase):
|
|||||||
convert = image_conversion._ConvertImage(self.context,
|
convert = image_conversion._ConvertImage(self.context,
|
||||||
self.task.task_id,
|
self.task.task_id,
|
||||||
self.task_type,
|
self.task_type,
|
||||||
self.wrapper)
|
self.wrapper,
|
||||||
|
self.stores)
|
||||||
self.assertEqual(fake_interpreter, convert.python)
|
self.assertEqual(fake_interpreter, convert.python)
|
||||||
|
Loading…
Reference in New Issue
Block a user