diff --git a/cinder/image/image_utils.py b/cinder/image/image_utils.py index b467380845a..3f8f28ccdad 100644 --- a/cinder/image/image_utils.py +++ b/cinder/image/image_utils.py @@ -51,6 +51,7 @@ from cinder.i18n import _ from cinder.image import accelerator from cinder.image import glance import cinder.privsep.format_inspector +import cinder.privsep.path from cinder import utils from cinder.volume import throttling from cinder.volume import volume_utils @@ -376,6 +377,16 @@ def check_qemu_img_version(minimum_version: str) -> None: raise exception.VolumeBackendAPIException(data=_msg) +def _ensure_exists(dest_path: str) -> None: + # Ensure that "dest" exists if it is in /dev/ + # This prevents qemu-img from creating a file in /dev/ + # if the block device has disappeared. + if dest_path.startswith('/dev/'): + if not cinder.privsep.path.exists(dest_path): + raise exception.CinderException( + "qemu-img convert destination %s does not exist" % dest_path) + + def _convert_image( prefix: tuple, source: str, @@ -443,6 +454,8 @@ def _convert_image( src_passphrase_file=src_passphrase_file, disable_sparse=disable_sparse) + _ensure_exists(dest) + start_time = timeutils.utcnow() # If there is not enough space on the conversion partition, include diff --git a/cinder/privsep/path.py b/cinder/privsep/path.py index 6fd7405fdab..3c53a55b2c6 100644 --- a/cinder/privsep/path.py +++ b/cinder/privsep/path.py @@ -37,3 +37,8 @@ def symlink(src, dest): if not os.path.exists(src): raise exception.FileNotFound(file_path=src) os.symlink(src, dest) + + +@cinder.privsep.sys_admin_pctxt.entrypoint +def exists(path: str | os.PathLike) -> bool: + return os.path.exists(path) diff --git a/cinder/tests/unit/test_image_utils.py b/cinder/tests/unit/test_image_utils.py index 5a1239112e2..e181d44e751 100644 --- a/cinder/tests/unit/test_image_utils.py +++ b/cinder/tests/unit/test_image_utils.py @@ -264,6 +264,8 @@ class TestQemuImgInfo(test.TestCase): @ddt.ddt class TestConvertImage(test.TestCase): + @mock.patch('cinder.image.image_utils._ensure_exists', + new=mock.MagicMock()) @mock.patch('cinder.image.image_utils.qemu_img_info') @mock.patch('cinder.utils.execute') @mock.patch('cinder.utils.is_blk_device', return_value=True) @@ -296,6 +298,8 @@ class TestConvertImage(test.TestCase): '-O', out_format, source, dest, run_as_root=True) + @mock.patch('cinder.image.image_utils._ensure_exists', + new=mock.MagicMock()) @mock.patch('cinder.image.image_utils.qemu_img_info') @mock.patch('cinder.utils.execute') @mock.patch('cinder.utils.is_blk_device', return_value=True) @@ -333,6 +337,8 @@ class TestConvertImage(test.TestCase): '-O', out_format, source, dest, run_as_root=True) + @mock.patch('cinder.image.image_utils._ensure_exists', + new=mock.MagicMock()) @mock.patch('cinder.volume.volume_utils.check_for_odirect_support', return_value=True) @mock.patch('cinder.image.image_utils.qemu_img_info') @@ -358,6 +364,8 @@ class TestConvertImage(test.TestCase): source, dest, run_as_root=True) + @mock.patch('cinder.image.image_utils._ensure_exists', + new=mock.MagicMock()) @mock.patch('cinder.volume.volume_utils.check_for_odirect_support', return_value=True) @mock.patch('cinder.image.image_utils.qemu_img_info') @@ -383,6 +391,8 @@ class TestConvertImage(test.TestCase): source, dest, run_as_root=True) + @mock.patch('cinder.image.image_utils._ensure_exists', + new=mock.MagicMock()) @mock.patch('cinder.image.image_utils.qemu_img_info') @mock.patch('cinder.utils.execute') @mock.patch('cinder.utils.is_blk_device', return_value=True) @@ -403,6 +413,8 @@ class TestConvertImage(test.TestCase): '-O', out_format, '-t', 'none', source, dest, run_as_root=True) + @mock.patch('cinder.image.image_utils._ensure_exists', + new=mock.MagicMock()) @mock.patch('cinder.image.image_utils.qemu_img_info') @mock.patch('cinder.utils.execute') @mock.patch('cinder.utils.is_blk_device', return_value=False) @@ -422,6 +434,8 @@ class TestConvertImage(test.TestCase): source, dest, run_as_root=True) @ddt.data(True, False) + @mock.patch('cinder.image.image_utils._ensure_exists', + new=mock.MagicMock()) @mock.patch('cinder.image.image_utils.qemu_img_info') @mock.patch('cinder.utils.execute') @mock.patch('cinder.utils.is_blk_device', return_value=False) @@ -446,6 +460,8 @@ class TestConvertImage(test.TestCase): mock_exec.assert_called_once_with(*exec_args, run_as_root=True) + @mock.patch('cinder.image.image_utils._ensure_exists', + new=mock.MagicMock()) @mock.patch('cinder.image.image_utils.qemu_img_info') @mock.patch('cinder.utils.execute') @mock.patch('cinder.utils.is_blk_device', return_value=False) @@ -464,6 +480,8 @@ class TestConvertImage(test.TestCase): '-O', out_format, '-S', '0', source, dest, run_as_root=True) + @mock.patch('cinder.image.image_utils._ensure_exists', + new=mock.MagicMock()) @mock.patch('cinder.volume.volume_utils.check_for_odirect_support', return_value=True) @mock.patch('cinder.image.image_utils.qemu_img_info') diff --git a/releasenotes/notes/bug-2132083-detect-missing-dev-during-img-conversion-ae80feda55ca7550.yaml b/releasenotes/notes/bug-2132083-detect-missing-dev-during-img-conversion-ae80feda55ca7550.yaml new file mode 100644 index 00000000000..d2b9e0b0dec --- /dev/null +++ b/releasenotes/notes/bug-2132083-detect-missing-dev-during-img-conversion-ae80feda55ca7550.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + `Bug #2132083 `_: When + creating a volume from an image, a storage connectivity failure or + similar could result in the local block device node disappearing. In + this situation, avoid writing the image to a file in /dev/ when calling + qemu-img.