diff --git a/nova/exception.py b/nova/exception.py index e7c5c873936c..a3eb065a7c78 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -1892,3 +1892,7 @@ class EnumFieldUnset(Invalid): class InvalidImageFormat(Invalid): msg_fmt = _("Invalid image format '%(format)s'") + + +class UnsupportedImageModel(Invalid): + msg_fmt = _("Image model '%(image)s' is not supported") diff --git a/nova/tests/unit/virt/disk/mount/test_loop.py b/nova/tests/unit/virt/disk/mount/test_loop.py index 6375c9386b81..e317d8c39ca2 100644 --- a/nova/tests/unit/virt/disk/mount/test_loop.py +++ b/nova/tests/unit/virt/disk/mount/test_loop.py @@ -18,6 +18,7 @@ import fixtures from nova import test from nova.virt.disk.mount import loop +from nova.virt.image import model as imgmodel def _fake_noop(*args, **kwargs): @@ -33,9 +34,15 @@ def _fake_trycmd_losetup_fails(*args, **kwards): class LoopTestCase(test.NoDBTestCase): + def setUp(self): + super(LoopTestCase, self).setUp() + + self.file = imgmodel.LocalFileImage("/some/file.qcow2", + imgmodel.FORMAT_QCOW2) + def test_get_dev(self): tempdir = self.useFixture(fixtures.TempDir()).path - l = loop.LoopMount(None, tempdir) + l = loop.LoopMount(self.file, tempdir) self.useFixture(fixtures.MonkeyPatch('nova.utils.trycmd', _fake_trycmd_losetup_works)) self.useFixture(fixtures.MonkeyPatch('nova.utils.execute', @@ -55,7 +62,7 @@ class LoopTestCase(test.NoDBTestCase): def test_inner_get_dev_fails(self): tempdir = self.useFixture(fixtures.TempDir()).path - l = loop.LoopMount(None, tempdir) + l = loop.LoopMount(self.file, tempdir) self.useFixture(fixtures.MonkeyPatch('nova.utils.trycmd', _fake_trycmd_losetup_fails)) @@ -72,7 +79,7 @@ class LoopTestCase(test.NoDBTestCase): def test_get_dev_timeout(self): tempdir = self.useFixture(fixtures.TempDir()).path - l = loop.LoopMount(None, tempdir) + l = loop.LoopMount(self.file, tempdir) self.useFixture(fixtures.MonkeyPatch('time.sleep', _fake_noop)) self.useFixture(fixtures.MonkeyPatch('nova.utils.trycmd', _fake_trycmd_losetup_fails)) @@ -89,7 +96,7 @@ class LoopTestCase(test.NoDBTestCase): def test_unget_dev(self): tempdir = self.useFixture(fixtures.TempDir()).path - l = loop.LoopMount(None, tempdir) + l = loop.LoopMount(self.file, tempdir) self.useFixture(fixtures.MonkeyPatch('nova.utils.execute', _fake_noop)) diff --git a/nova/tests/unit/virt/disk/mount/test_nbd.py b/nova/tests/unit/virt/disk/mount/test_nbd.py index d048511d1699..3e8710d77fde 100644 --- a/nova/tests/unit/virt/disk/mount/test_nbd.py +++ b/nova/tests/unit/virt/disk/mount/test_nbd.py @@ -23,6 +23,7 @@ import fixtures from nova import test from nova.virt.disk.mount import nbd +from nova.virt.image import model as imgmodel ORIG_EXISTS = os.path.exists ORIG_LISTDIR = os.listdir @@ -67,24 +68,26 @@ class NbdTestCase(test.NoDBTestCase): _fake_detect_nbd_devices) self.useFixture(fixtures.MonkeyPatch('os.listdir', _fake_listdir_nbd_devices)) + self.file = imgmodel.LocalFileImage("/some/file.qcow2", + imgmodel.FORMAT_QCOW2) def test_nbd_no_devices(self): tempdir = self.useFixture(fixtures.TempDir()).path self.stubs.Set(nbd.NbdMount, '_detect_nbd_devices', _fake_detect_nbd_devices_none) - n = nbd.NbdMount(None, tempdir) + n = nbd.NbdMount(self.file, tempdir) self.assertIsNone(n._allocate_nbd()) def test_nbd_no_free_devices(self): tempdir = self.useFixture(fixtures.TempDir()).path - n = nbd.NbdMount(None, tempdir) + n = nbd.NbdMount(self.file, tempdir) self.useFixture(fixtures.MonkeyPatch('os.path.exists', _fake_exists_all_used)) self.assertIsNone(n._allocate_nbd()) def test_nbd_not_loaded(self): tempdir = self.useFixture(fixtures.TempDir()).path - n = nbd.NbdMount(None, tempdir) + n = nbd.NbdMount(self.file, tempdir) # Fake out os.path.exists def fake_exists(path): @@ -101,7 +104,7 @@ class NbdTestCase(test.NoDBTestCase): def test_nbd_allocation(self): tempdir = self.useFixture(fixtures.TempDir()).path - n = nbd.NbdMount(None, tempdir) + n = nbd.NbdMount(self.file, tempdir) self.useFixture(fixtures.MonkeyPatch('os.path.exists', _fake_exists_no_users)) self.useFixture(fixtures.MonkeyPatch('random.shuffle', _fake_noop)) @@ -111,7 +114,7 @@ class NbdTestCase(test.NoDBTestCase): def test_nbd_allocation_one_in_use(self): tempdir = self.useFixture(fixtures.TempDir()).path - n = nbd.NbdMount(None, tempdir) + n = nbd.NbdMount(self.file, tempdir) self.useFixture(fixtures.MonkeyPatch('random.shuffle', _fake_noop)) # Fake out os.path.exists @@ -135,12 +138,12 @@ class NbdTestCase(test.NoDBTestCase): tempdir = self.useFixture(fixtures.TempDir()).path self.stubs.Set(nbd.NbdMount, '_detect_nbd_devices', _fake_detect_nbd_devices_none) - n = nbd.NbdMount(None, tempdir) + n = nbd.NbdMount(self.file, tempdir) self.assertFalse(n._inner_get_dev()) def test_inner_get_dev_qemu_fails(self): tempdir = self.useFixture(fixtures.TempDir()).path - n = nbd.NbdMount(None, tempdir) + n = nbd.NbdMount(self.file, tempdir) self.useFixture(fixtures.MonkeyPatch('os.path.exists', _fake_exists_no_users)) @@ -155,7 +158,7 @@ class NbdTestCase(test.NoDBTestCase): def test_inner_get_dev_qemu_timeout(self): tempdir = self.useFixture(fixtures.TempDir()).path - n = nbd.NbdMount(None, tempdir) + n = nbd.NbdMount(self.file, tempdir) self.useFixture(fixtures.MonkeyPatch('os.path.exists', _fake_exists_no_users)) @@ -195,7 +198,7 @@ class NbdTestCase(test.NoDBTestCase): def test_inner_get_dev_works(self): tempdir = self.useFixture(fixtures.TempDir()).path - n = nbd.NbdMount(None, tempdir) + n = nbd.NbdMount(self.file, tempdir) self.useFixture(fixtures.MonkeyPatch('random.shuffle', _fake_noop)) self.useFixture(fixtures.MonkeyPatch('os.path.exists', self.fake_exists_one)) @@ -219,13 +222,13 @@ class NbdTestCase(test.NoDBTestCase): # This test is just checking we don't get an exception when we unget # something we don't have tempdir = self.useFixture(fixtures.TempDir()).path - n = nbd.NbdMount(None, tempdir) + n = nbd.NbdMount(self.file, tempdir) self.useFixture(fixtures.MonkeyPatch('nova.utils.execute', _fake_noop)) n.unget_dev() def test_get_dev(self): tempdir = self.useFixture(fixtures.TempDir()).path - n = nbd.NbdMount(None, tempdir) + n = nbd.NbdMount(self.file, tempdir) self.useFixture(fixtures.MonkeyPatch('random.shuffle', _fake_noop)) self.useFixture(fixtures.MonkeyPatch('nova.utils.execute', _fake_noop)) self.useFixture(fixtures.MonkeyPatch('os.path.exists', @@ -252,7 +255,7 @@ class NbdTestCase(test.NoDBTestCase): self.stubs.Set(nbd.NbdMount, '_inner_get_dev', fake_get_dev_fails) tempdir = self.useFixture(fixtures.TempDir()).path - n = nbd.NbdMount(None, tempdir) + n = nbd.NbdMount(self.file, tempdir) self.useFixture(fixtures.MonkeyPatch('random.shuffle', _fake_noop)) self.useFixture(fixtures.MonkeyPatch('time.sleep', _fake_noop)) self.useFixture(fixtures.MonkeyPatch('nova.utils.execute', _fake_noop)) @@ -318,7 +321,7 @@ class NbdTestCase(test.NoDBTestCase): pid_exists)) def get_a_device(): - n = nbd.NbdMount(None, tempdir) + n = nbd.NbdMount(self.file, tempdir) n.get_dev() chosen_devices.append(n.device) diff --git a/nova/tests/unit/virt/disk/test_api.py b/nova/tests/unit/virt/disk/test_api.py index bb3e63fbddce..9175d3c6db6c 100644 --- a/nova/tests/unit/virt/disk/test_api.py +++ b/nova/tests/unit/virt/disk/test_api.py @@ -23,13 +23,14 @@ from nova import test from nova import utils from nova.virt.disk import api from nova.virt.disk.mount import api as mount +from nova.virt.image import model as imgmodel class FakeMount(object): device = None @staticmethod - def instance_for_format(imgfile, mountdir, partition, imgfmt): + def instance_for_format(image, mountdir, partition): return FakeMount() def get_dev(self): @@ -115,10 +116,11 @@ class APITestCase(test.NoDBTestCase): imgsize = 10 device = "/dev/sdh" use_cow = True + image = imgmodel.LocalFileImage(imgfile, imgmodel.FORMAT_QCOW2) self.flags(resize_fs_using_block_device=True) mounter = FakeMount.instance_for_format( - imgfile, None, None, 'qcow2') + image, None, None) mounter.device = device self.mox.StubOutWithMock(api, 'can_resize_image') @@ -133,8 +135,7 @@ class APITestCase(test.NoDBTestCase): api.can_resize_image(imgfile, imgsize).AndReturn(True) utils.execute('qemu-img', 'resize', imgfile, imgsize) api.is_image_extendable(imgfile, use_cow).AndReturn(True) - mount.Mount.instance_for_format( - imgfile, None, None, 'qcow2').AndReturn(mounter) + mount.Mount.instance_for_format(image, None, None).AndReturn(mounter) mounter.get_dev().AndReturn(True) api.resize2fs(mounter.device, run_as_root=True, check_exit_code=[0]) mounter.unget_dev() diff --git a/nova/tests/unit/virt/disk/vfs/test_localfs.py b/nova/tests/unit/virt/disk/vfs/test_localfs.py index b362a9a3e136..21135e6e32aa 100644 --- a/nova/tests/unit/virt/disk/vfs/test_localfs.py +++ b/nova/tests/unit/virt/disk/vfs/test_localfs.py @@ -23,6 +23,7 @@ from nova import test from nova.tests.unit import utils as tests_utils import nova.utils from nova.virt.disk.vfs import localfs as vfsimpl +from nova.virt.image import model as imgmodel CONF = cfg.CONF @@ -432,7 +433,9 @@ class VirtDiskVFSLocalFSTest(test.NoDBTestCase): self.assertTrue(mkdtemp.called) NbdMount.assert_called_once_with( - 'img.qcow2', 'tmp/', None) + imgmodel.LocalFileImage('img.qcow2', + imgmodel.FORMAT_QCOW2), + 'tmp/', None) mounter.do_mount.assert_called_once_with() @mock.patch.object(tempfile, 'mkdtemp') @@ -448,5 +451,7 @@ class VirtDiskVFSLocalFSTest(test.NoDBTestCase): self.assertTrue(mkdtemp.called) NbdMount.assert_called_once_with( - 'img.qcow2', 'tmp/', None) + imgmodel.LocalFileImage('img.qcow2', + imgmodel.FORMAT_QCOW2), + 'tmp/', None) self.assertFalse(mounter.do_mount.called) diff --git a/nova/tests/unit/virt/test_virt.py b/nova/tests/unit/virt/test_virt.py index 0e4b302b9d98..2a35c5a2c0ef 100644 --- a/nova/tests/unit/virt/test_virt.py +++ b/nova/tests/unit/virt/test_virt.py @@ -131,7 +131,7 @@ class TestDiskImage(test.NoDBTestCase): mountdir = '/mnt/fake_rootfs' fakemount = FakeMount(image, mountdir, None) - def fake_instance_for_format(imgfile, mountdir, partition, imgfmt): + def fake_instance_for_format(image, mountdir, partition): return fakemount self.stubs.Set(mount.Mount, 'instance_for_format', @@ -149,7 +149,7 @@ class TestDiskImage(test.NoDBTestCase): mountdir = '/mnt/fake_rootfs' fakemount = FakeMount(image, mountdir, None) - def fake_instance_for_format(imgfile, mountdir, partition, imgfmt): + def fake_instance_for_format(image, mountdir, partition): return fakemount self.stubs.Set(mount.Mount, 'instance_for_format', @@ -169,7 +169,7 @@ class TestDiskImage(test.NoDBTestCase): mountdir = '/mnt/fake_rootfs' fakemount = FakeMount(image, mountdir, None) - def fake_instance_for_format(imgfile, mountdir, partition, imgfmt): + def fake_instance_for_format(image, mountdir, partition): return fakemount self.stubs.Set(mount.Mount, 'instance_for_format', @@ -200,8 +200,8 @@ class TestVirtDisk(test.NoDBTestCase): def proc_mounts(self, mount_point): return None - def fake_instance_for_format(imgfile, mountdir, partition, imgfmt): - return FakeMount(imgfile, mountdir, partition) + def fake_instance_for_format(image, mountdir, partition): + return FakeMount(image, mountdir, partition) self.stubs.Set(os.path, 'exists', lambda _: True) self.stubs.Set(disk_api._DiskImage, '_device_for_path', proc_mounts) diff --git a/nova/virt/disk/api.py b/nova/virt/disk/api.py index ebc20f91d8a9..232744d8cb22 100644 --- a/nova/virt/disk/api.py +++ b/nova/virt/disk/api.py @@ -42,6 +42,7 @@ from nova.i18n import _LW from nova import utils from nova.virt.disk.mount import api as mount from nova.virt.disk.vfs import api as vfs +from nova.virt.image import model as imgmodel from nova.virt import images @@ -173,7 +174,12 @@ def get_disk_size(path): def extend(image, size, use_cow=False): - """Increase image to size.""" + """Increase image to size. + + :param image: path to disk image file + :param size: image size in bytes + :param use_cow: whether the disk is a qcow2 file + """ if not can_resize_image(image, size): return @@ -200,7 +206,9 @@ def extend(image, size, use_cow=False): # in case of non-raw disks we can't just resize the image, but # rather the mounted device instead mounter = mount.Mount.instance_for_format( - image, None, None, 'qcow2') + imgmodel.LocalFileImage(image, + imgmodel.FORMAT_QCOW2), + None, None) if mounter.get_dev(): safe_resize2fs(mounter.device, run_as_root=True, @@ -270,11 +278,24 @@ class _DiskImage(object): tmp_prefix = 'openstack-disk-mount-tmp' def __init__(self, image, partition=None, use_cow=False, mount_dir=None): + """Create a new _DiskImage object instance + + :param image: the path to the disk image file + :param partition: the partition number within the image + :param use_cow: whether the disk is in qcow2 format + :param mount_dir: the directory to mount the image on + """ + # These passed to each mounter - self.image = image self.partition = partition self.mount_dir = mount_dir - self.use_cow = use_cow + + if use_cow: + self.image = imgmodel.LocalFileImage(image, + imgmodel.FORMAT_QCOW2) + else: + self.image = imgmodel.LocalFileImage(image, + imgmodel.FORMAT_RAW) # Internal self._mkdir = False @@ -328,14 +349,10 @@ class _DiskImage(object): self.mount_dir = tempfile.mkdtemp(prefix=self.tmp_prefix) self._mkdir = True - imgfmt = "raw" - if self.use_cow: - imgfmt = "qcow2" - mounter = mount.Mount.instance_for_format(self.image, self.mount_dir, - self.partition, - imgfmt) + self.partition) + if mounter.do_mount(): self._mounter = mounter return self._mounter.device diff --git a/nova/virt/disk/mount/api.py b/nova/virt/disk/mount/api.py index 60a8bbf54415..b952b96e86a3 100644 --- a/nova/virt/disk/mount/api.py +++ b/nova/virt/disk/mount/api.py @@ -19,8 +19,10 @@ import time from oslo_log import log as logging from oslo_utils import importutils +from nova import exception from nova.i18n import _, _LI, _LW from nova import utils +from nova.virt.image import model as imgmodel LOG = logging.getLogger(__name__) @@ -37,42 +39,79 @@ class Mount(object): mode = None # to be overridden in subclasses @staticmethod - def instance_for_format(imgfile, mountdir, partition, imgfmt): - LOG.debug("Instance for format imgfile=%(imgfile)s " - "mountdir=%(mountdir)s partition=%(partition)s " - "imgfmt=%(imgfmt)s", - {'imgfile': imgfile, 'mountdir': mountdir, - 'partition': partition, 'imgfmt': imgfmt}) - if imgfmt == "raw": - LOG.debug("Using LoopMount") - return importutils.import_object( - "nova.virt.disk.mount.loop.LoopMount", - imgfile, mountdir, partition) + def instance_for_format(image, mountdir, partition): + """Get a Mount instance for the image type + + :param image: instance of nova.virt.image.model.Image + :param mountdir: path to mount the image at + :param partition: partition number to mount + """ + LOG.debug("Instance for format image=%(image)s " + "mountdir=%(mountdir)s partition=%(partition)s", + {'image': image, 'mountdir': mountdir, + 'partition': partition}) + + if isinstance(image, imgmodel.LocalFileImage): + if image.format == imgmodel.FORMAT_RAW: + LOG.debug("Using LoopMount") + return importutils.import_object( + "nova.virt.disk.mount.loop.LoopMount", + image, mountdir, partition) + else: + LOG.debug("Using NbdMount") + return importutils.import_object( + "nova.virt.disk.mount.nbd.NbdMount", + image, mountdir, partition) else: - LOG.debug("Using NbdMount") - return importutils.import_object( - "nova.virt.disk.mount.nbd.NbdMount", - imgfile, mountdir, partition) + # TODO(berrange) we could mount images of + # type LocalBlockImage directly without + # involving loop or nbd devices + # + # We could also mount RBDImage directly + # using kernel RBD block dev support. + # + # This is left as an enhancement for future + # motivated developers todo, since raising + # an exception is on par with what this + # code did historically + raise exception.UnsupportedImageModel( + image.__class__.__name__) @staticmethod - def instance_for_device(imgfile, mountdir, partition, device): - LOG.debug("Instance for device imgfile=%(imgfile)s " + def instance_for_device(image, mountdir, partition, device): + """Get a Mount instance for the device type + + :param image: instance of nova.virt.image.model.Image + :param mountdir: path to mount the image at + :param partition: partition number to mount + :param device: mounted device path + """ + + LOG.debug("Instance for device image=%(image)s " "mountdir=%(mountdir)s partition=%(partition)s " "device=%(device)s", - {'imgfile': imgfile, 'mountdir': mountdir, + {'image': image, 'mountdir': mountdir, 'partition': partition, 'device': device}) + if "loop" in device: LOG.debug("Using LoopMount") return importutils.import_object( "nova.virt.disk.mount.loop.LoopMount", - imgfile, mountdir, partition, device) + image, mountdir, partition, device) else: LOG.debug("Using NbdMount") return importutils.import_object( "nova.virt.disk.mount.nbd.NbdMount", - imgfile, mountdir, partition, device) + image, mountdir, partition, device) def __init__(self, image, mount_dir, partition=None, device=None): + """Create a new Mount instance + + :param image: instance of nova.virt.image.model.Image + :param mount_dir: path to mount the image at + :param partition: partition number to mount + :param device: mounted device path + """ # Input self.image = image diff --git a/nova/virt/disk/mount/loop.py b/nova/virt/disk/mount/loop.py index 8c47126ebf0e..abf239df649a 100644 --- a/nova/virt/disk/mount/loop.py +++ b/nova/virt/disk/mount/loop.py @@ -27,7 +27,8 @@ class LoopMount(api.Mount): mode = 'loop' def _inner_get_dev(self): - out, err = utils.trycmd('losetup', '--find', '--show', self.image, + out, err = utils.trycmd('losetup', '--find', '--show', + self.image.path, run_as_root=True) if err: self.error = _('Could not attach image to loopback: %s') % err diff --git a/nova/virt/disk/mount/nbd.py b/nova/virt/disk/mount/nbd.py index 68c91ba91b4d..21b91c9f1591 100644 --- a/nova/virt/disk/mount/nbd.py +++ b/nova/virt/disk/mount/nbd.py @@ -83,8 +83,9 @@ class NbdMount(api.Mount): # NOTE(mikal): qemu-nbd will return an error if the device file is # already in use. LOG.debug('Get nbd device %(dev)s for %(imgfile)s', - {'dev': device, 'imgfile': self.image}) - _out, err = utils.trycmd('qemu-nbd', '-c', device, self.image, + {'dev': device, 'imgfile': self.image.path}) + _out, err = utils.trycmd('qemu-nbd', '-c', device, + self.image.path, run_as_root=True) if err: self.error = _('qemu-nbd error: %s') % err diff --git a/nova/virt/disk/vfs/localfs.py b/nova/virt/disk/vfs/localfs.py index 215bfa3bbe22..f141a930a120 100644 --- a/nova/virt/disk/vfs/localfs.py +++ b/nova/virt/disk/vfs/localfs.py @@ -24,6 +24,7 @@ from nova import utils from nova.virt.disk.mount import loop from nova.virt.disk.mount import nbd from nova.virt.disk.vfs import api as vfs +from nova.virt.image import model as imgmodel LOG = logging.getLogger(__name__) @@ -65,14 +66,18 @@ class VFSLocalFS(vfs.VFS): try: if self.imgfmt == "raw": LOG.debug("Using LoopMount") - mnt = loop.LoopMount(self.imgfile, - self.imgdir, - self.partition) + mnt = loop.LoopMount( + imgmodel.LocalFileImage(self.imgfile, + imgmodel.FORMAT_RAW), + self.imgdir, + self.partition) else: LOG.debug("Using NbdMount") - mnt = nbd.NbdMount(self.imgfile, - self.imgdir, - self.partition) + mnt = nbd.NbdMount( + imgmodel.LocalFileImage(self.imgfile, + imgmodel.FORMAT_QCOW2), + self.imgdir, + self.partition) if mount: if not mnt.do_mount(): raise exception.NovaException(mnt.error)