Skip to content

Commit 066fa00

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "Revert image state to queued if conversion fails" into stable/2024.1
2 parents a41eb91 + 633e85c commit 066fa00

File tree

2 files changed

+94
-16
lines changed

2 files changed

+94
-16
lines changed

glance/async_/flows/plugins/image_conversion.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,16 @@ class _ConvertImage(task.Task):
6363

6464
default_provides = 'file_path'
6565

66-
def __init__(self, context, task_id, task_type, action_wrapper):
66+
def __init__(self, context, task_id, task_type, action_wrapper,
67+
stores):
6768
self.context = context
6869
self.task_id = task_id
6970
self.task_type = task_type
7071
self.action_wrapper = action_wrapper
72+
self.stores = stores
7173
self.image_id = action_wrapper.image_id
7274
self.dest_path = ""
75+
self.src_path = ""
7376
self.python = CONF.wsgi.python_interpreter
7477
super(_ConvertImage, self).__init__(
7578
name='%s-Convert_Image-%s' % (task_type, task_id))
@@ -83,8 +86,8 @@ def _execute(self, action, file_path, **kwargs):
8386
target_format = CONF.image_conversion.output_format
8487
# TODO(jokke): Once we support other schemas we need to take them into
8588
# account and handle the paths here.
86-
src_path = file_path.split('file://')[-1]
87-
dest_path = "%(path)s.%(target)s" % {'path': src_path,
89+
self.src_path = file_path.split('file://')[-1]
90+
dest_path = "%(path)s.%(target)s" % {'path': self.src_path,
8891
'target': target_format}
8992
self.dest_path = dest_path
9093

@@ -104,7 +107,7 @@ def _execute(self, action, file_path, **kwargs):
104107
# qemu-img on it.
105108
# See https://bugs.launchpad.net/nova/+bug/2059809 for details.
106109
try:
107-
inspector = inspector_cls.from_file(src_path)
110+
inspector = inspector_cls.from_file(self.src_path)
108111
if not inspector.safety_check():
109112
LOG.error('Image failed %s safety check; aborting conversion',
110113
source_format)
@@ -123,7 +126,7 @@ def _execute(self, action, file_path, **kwargs):
123126
stdout, stderr = putils.trycmd("qemu-img", "info",
124127
"-f", source_format,
125128
"--output=json",
126-
src_path,
129+
self.src_path,
127130
prlimit=utils.QEMU_IMG_PROC_LIMITS,
128131
python_exec=self.python,
129132
log_errors=putils.LOG_ALL_ERRORS,)
@@ -186,7 +189,7 @@ def _execute(self, action, file_path, **kwargs):
186189
stdout, stderr = putils.trycmd('qemu-img', 'convert',
187190
'-f', source_format,
188191
'-O', target_format,
189-
src_path, dest_path,
192+
self.src_path, dest_path,
190193
log_errors=putils.LOG_ALL_ERRORS)
191194
except OSError as exc:
192195
with excutils.save_and_reraise_exception():
@@ -204,7 +207,7 @@ def _execute(self, action, file_path, **kwargs):
204207
LOG.info(_LI('Updated image %s size=%i disk_format=%s'),
205208
self.image_id, new_size, target_format)
206209

207-
os.remove(src_path)
210+
os.remove(self.src_path)
208211

209212
return "file://%s" % dest_path
210213

@@ -217,6 +220,23 @@ def revert(self, result=None, **kwargs):
217220
if os.path.exists(self.dest_path):
218221
os.remove(self.dest_path)
219222

223+
# NOTE(abhishekk): If we failed to convert the image, then none
224+
# of the _ImportToStore() tasks could have run, so we need
225+
# to move all stores out of "importing" to "failed".
226+
with self.action_wrapper as action:
227+
action.set_image_attribute(status='queued')
228+
if self.stores:
229+
action.remove_importing_stores(self.stores)
230+
action.add_failed_stores(self.stores)
231+
232+
if self.src_path:
233+
try:
234+
os.remove(self.src_path)
235+
except FileNotFoundError:
236+
# NOTE(abhishekk): We must have raced with something
237+
# else, so this is not a problem
238+
pass
239+
220240

221241
def get_flow(**kwargs):
222242
"""Return task flow for no-op.
@@ -232,7 +252,8 @@ def get_flow(**kwargs):
232252
task_id = kwargs.get('task_id')
233253
task_type = kwargs.get('task_type')
234254
action_wrapper = kwargs.get('action_wrapper')
255+
stores = kwargs.get('backend', [])
235256

236257
return lf.Flow(task_type).add(
237-
_ConvertImage(context, task_id, task_type, action_wrapper)
258+
_ConvertImage(context, task_id, task_type, action_wrapper, stores)
238259
)

glance/tests/unit/async_/flows/plugins/test_image_conversion.py

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def setUp(self):
5959
self.context = mock.MagicMock()
6060
self.img_repo = mock.MagicMock()
6161
self.task_repo = mock.MagicMock()
62+
self.stores = mock.MagicMock()
6263
self.image_id = UUID1
6364

6465
self.gateway = gateway.Gateway()
@@ -87,7 +88,10 @@ def setUp(self):
8788
task_input=task_input)
8889

8990
self.image.extra_properties = {
90-
'os_glance_import_task': self.task.task_id}
91+
'os_glance_import_task': self.task.task_id,
92+
'os_glance_importing_to_stores': mock.MagicMock(),
93+
'os_glance_failed_import': ""
94+
}
9195
self.wrapper = import_flow.ImportActionWrapper(self.img_repo,
9296
self.image_id,
9397
self.task.task_id)
@@ -105,7 +109,8 @@ def test_image_convert_success(self, mock_os_remove, mock_os_stat):
105109
image_convert = image_conversion._ConvertImage(self.context,
106110
self.task.task_id,
107111
self.task_type,
108-
self.wrapper)
112+
self.wrapper,
113+
self.stores)
109114

110115
self.task_repo.get.return_value = self.task
111116
image = mock.MagicMock(image_id=self.image_id, virtual_size=None,
@@ -137,7 +142,8 @@ def _setup_image_convert_info_fail(self, disk_format='qcow2'):
137142
image_convert = image_conversion._ConvertImage(self.context,
138143
self.task.task_id,
139144
self.task_type,
140-
self.wrapper)
145+
self.wrapper,
146+
self.stores)
141147

142148
self.task_repo.get.return_value = self.task
143149
image = mock.MagicMock(image_id=self.image_id, virtual_size=None,
@@ -354,22 +360,72 @@ def test_image_convert_same_format_does_nothing(self):
354360
image = self.img_repo.get.return_value
355361
self.assertEqual(123, image.virtual_size)
356362

357-
@mock.patch.object(os, 'remove')
358-
def test_image_convert_revert_success(self, mock_os_remove):
363+
def _set_image_conversion(self, mock_os_remove, stores=[]):
359364
mock_os_remove.return_value = None
365+
wrapper = mock.MagicMock()
360366
image_convert = image_conversion._ConvertImage(self.context,
361367
self.task.task_id,
362368
self.task_type,
363-
self.wrapper)
364-
369+
wrapper,
370+
stores)
371+
action = wrapper.__enter__.return_value
365372
self.task_repo.get.return_value = self.task
373+
return action, image_convert
374+
375+
@mock.patch.object(os, 'remove')
376+
def test_image_convert_revert_success_multiple_stores(
377+
self, mock_os_remove):
378+
action, image_convert = self._set_image_conversion(
379+
mock_os_remove, stores=self.stores)
380+
381+
with mock.patch.object(processutils, 'execute') as exc_mock:
382+
exc_mock.return_value = ("", None)
383+
with mock.patch.object(os.path, 'exists') as os_exists_mock:
384+
os_exists_mock.return_value = True
385+
image_convert.revert(result=mock.MagicMock())
386+
self.assertEqual(1, mock_os_remove.call_count)
387+
action.set_image_attribute.assert_called_once_with(
388+
status='queued')
389+
action.remove_importing_stores.assert_called_once_with(
390+
self.stores)
391+
action.add_failed_stores.assert_called_once_with(
392+
self.stores)
393+
394+
@mock.patch.object(os, 'remove')
395+
def test_image_convert_revert_success_single_store(
396+
self, mock_os_remove):
397+
action, image_convert = self._set_image_conversion(mock_os_remove)
366398

367399
with mock.patch.object(processutils, 'execute') as exc_mock:
368400
exc_mock.return_value = ("", None)
369401
with mock.patch.object(os.path, 'exists') as os_exists_mock:
370402
os_exists_mock.return_value = True
371403
image_convert.revert(result=mock.MagicMock())
372404
self.assertEqual(1, mock_os_remove.call_count)
405+
self.assertEqual(0, action.remove_importing_stores.call_count)
406+
self.assertEqual(0, action.add_failed_store.call_count)
407+
action.set_image_attribute.assert_called_once_with(
408+
status='queued')
409+
410+
@mock.patch.object(os, 'remove')
411+
def test_image_convert_revert_success_src_file_exists(
412+
self, mock_os_remove):
413+
action, image_convert = self._set_image_conversion(
414+
mock_os_remove, stores=self.stores)
415+
image_convert.src_path = mock.MagicMock()
416+
417+
with mock.patch.object(processutils, 'execute') as exc_mock:
418+
exc_mock.return_value = ("", None)
419+
with mock.patch.object(os.path, 'exists') as os_exists_mock:
420+
os_exists_mock.return_value = True
421+
image_convert.revert(result=mock.MagicMock())
422+
action.set_image_attribute.assert_called_once_with(
423+
status='queued')
424+
action.remove_importing_stores.assert_called_once_with(
425+
self.stores)
426+
action.add_failed_stores.assert_called_once_with(
427+
self.stores)
428+
self.assertEqual(2, mock_os_remove.call_count)
373429

374430
def test_image_convert_interpreter_configured(self):
375431
# By default, wsgi.python_interpreter is None; if it is
@@ -380,5 +436,6 @@ def test_image_convert_interpreter_configured(self):
380436
convert = image_conversion._ConvertImage(self.context,
381437
self.task.task_id,
382438
self.task_type,
383-
self.wrapper)
439+
self.wrapper,
440+
self.stores)
384441
self.assertEqual(fake_interpreter, convert.python)

0 commit comments

Comments
 (0)