Skip to content

Commit a10e14c

Browse files
authored
Merge pull request #44 from stackhpc/upstream/2023.1-2024-08-19
Synchronise 2023.1 with upstream
2 parents d90f70b + 6a17e4e commit a10e14c

File tree

6 files changed

+139
-26
lines changed

6 files changed

+139
-26
lines changed

glance/api/v2/images.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,7 @@ def _do_add(self, req, image, api_pol, change):
674674
json_schema_version = change.get('json_schema_version', 10)
675675
if path_root == 'locations':
676676
api_pol.update_locations()
677-
self._do_add_locations(image, path[1], value)
677+
self._do_add_locations(image, path[1], value, req.context)
678678
else:
679679
api_pol.update_property(path_root, value)
680680
if ((hasattr(image, path_root) or
@@ -1043,7 +1043,7 @@ def _do_replace_locations(self, image, value):
10431043
raise webob.exc.HTTPBadRequest(
10441044
explanation=encodeutils.exception_to_unicode(ve))
10451045

1046-
def _do_add_locations(self, image, path_pos, value):
1046+
def _do_add_locations(self, image, path_pos, value, context):
10471047
if CONF.show_multiple_locations == False:
10481048
msg = _("It's not allowed to add locations if locations are "
10491049
"invisible.")
@@ -1059,7 +1059,7 @@ def _do_add_locations(self, image, path_pos, value):
10591059
updated_location = value
10601060
if CONF.enabled_backends:
10611061
updated_location = store_utils.get_updated_store_location(
1062-
[value])[0]
1062+
[value], context=context)[0]
10631063

10641064
pos = self._get_locations_op_pos(path_pos,
10651065
len(image.locations), True)

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/common/store_utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,12 @@ def _update_cinder_location_and_store_id(context, loc):
238238
"due to unknown issues."), uri)
239239

240240

241-
def get_updated_store_location(locations):
241+
def get_updated_store_location(locations, context=None):
242242
for loc in locations:
243+
if loc['url'].startswith("cinder://") and context:
244+
_update_cinder_location_and_store_id(context, loc)
245+
continue
246+
243247
store_id = _get_store_id_from_uri(loc['url'])
244248
if store_id:
245249
loc['metadata']['store'] = store_id

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)

glance/tests/unit/common/test_utils.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ class TestCinderStoreUtils(base.MultiStoreClearingUnitTest):
102102
new_callable=mock.PropertyMock)
103103
def _test_update_cinder_store_in_location(self, mock_url_prefix,
104104
mock_associate_store,
105-
is_valid=True):
105+
is_valid=True,
106+
modify_source_url=False):
106107
volume_id = 'db457a25-8f16-4b2c-a644-eae8d17fe224'
107108
store_id = 'fast-cinder'
108109
expected = 'fast-cinder'
@@ -117,7 +118,11 @@ def _test_update_cinder_store_in_location(self, mock_url_prefix,
117118
}]
118119
mock_url_prefix.return_value = 'cinder://%s' % store_id
119120
image.locations = locations
120-
store_utils.update_store_in_locations(context, image, image_repo)
121+
if modify_source_url:
122+
updated_location = store_utils.get_updated_store_location(
123+
locations, context=context)
124+
else:
125+
store_utils.update_store_in_locations(context, image, image_repo)
121126

122127
if is_valid:
123128
# This is the case where we found an image that has an
@@ -129,10 +134,18 @@ def _test_update_cinder_store_in_location(self, mock_url_prefix,
129134
# format i.e. this is the case when store is valid and location
130135
# url, metadata are updated and image_repo.save is called
131136
expected_url = mock_url_prefix.return_value + '/' + volume_id
132-
self.assertEqual(expected_url, image.locations[0].get('url'))
133-
self.assertEqual(expected, image.locations[0]['metadata'].get(
134-
'store'))
135-
self.assertEqual(1, image_repo.save.call_count)
137+
if modify_source_url:
138+
# This is the case where location url is modified to be
139+
# compatible with multistore as below,
140+
# `cinder://store_id/volume_id` to avoid InvalidLocation error
141+
self.assertEqual(expected_url, updated_location[0].get('url'))
142+
self.assertEqual(expected,
143+
updated_location[0]['metadata'].get('store'))
144+
else:
145+
self.assertEqual(expected_url, image.locations[0].get('url'))
146+
self.assertEqual(expected, image.locations[0]['metadata'].get(
147+
'store'))
148+
self.assertEqual(1, image_repo.save.call_count)
136149
else:
137150
# Here, we've got an image backed by a volume which does
138151
# not have a corresponding store specifying the volume_type.
@@ -151,6 +164,15 @@ def test_update_cinder_store_location_valid_type(self):
151164
def test_update_cinder_store_location_invalid_type(self):
152165
self._test_update_cinder_store_in_location(is_valid=False)
153166

167+
def test_get_updated_cinder_store_location(self):
168+
"""
169+
Test if location url is modified to be compatible
170+
with multistore.
171+
"""
172+
173+
self._test_update_cinder_store_in_location(
174+
modify_source_url=True)
175+
154176

155177
class TestUtils(test_utils.BaseTestCase):
156178
"""Test routines in glance.utils"""
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
fixes:
3+
- |
4+
`Bug #2054575 <https://bugs.launchpad.net/glance/+bug/2054575>`_:
5+
Fixed the issue when cinder uploads a volume to glance in the
6+
optimized path and glance rejects the request with invalid location.
7+
Now we convert the old location format sent by cinder into the new
8+
location format supported by multi store, hence allowing volumes to
9+
be uploaded in an optimized way.

0 commit comments

Comments
 (0)