diff --git a/full_quobyte_patch/Ocata/Cinder/full_quobyte_ocata_cinder.patch b/full_quobyte_patch/Ocata/full_quobyte_ocata_cinder.patch similarity index 62% rename from full_quobyte_patch/Ocata/Cinder/full_quobyte_ocata_cinder.patch rename to full_quobyte_patch/Ocata/full_quobyte_ocata_cinder.patch index 64fd683..03622b8 100644 --- a/full_quobyte_patch/Ocata/Cinder/full_quobyte_ocata_cinder.patch +++ b/full_quobyte_patch/Ocata/full_quobyte_ocata_cinder.patch @@ -1,8 +1,75 @@ diff --git cinder/tests/unit/volume/drivers/test_quobyte.py cinder/tests/unit/volume/drivers/test_quobyte.py -index de5169c..7856679 100644 +index de5169c..20a8973 100644 --- cinder/tests/unit/volume/drivers/test_quobyte.py +++ cinder/tests/unit/volume/drivers/test_quobyte.py -@@ -604,13 +604,11 @@ class QuobyteDriverTestCase(test.TestCase): +@@ -17,6 +17,7 @@ + + import errno + import os ++import psutil + import six + import traceback + +@@ -100,6 +101,12 @@ class QuobyteDriverTestCase(test.TestCase): + if not caught: + self.fail('Expected raised exception but nothing caught.') + ++ def get_mock_partitions(self): ++ mypart = mock.Mock() ++ mypart.device = "quobyte@" ++ mypart.mountpoint = self.TEST_MNT_POINT ++ return [mypart] ++ + def test_local_path(self): + """local_path common use case.""" + drv = self._driver +@@ -113,7 +120,9 @@ class QuobyteDriverTestCase(test.TestCase): + def test_mount_quobyte_should_mount_correctly(self): + with mock.patch.object(self._driver, '_execute') as mock_execute, \ + mock.patch('cinder.volume.drivers.quobyte.QuobyteDriver' +- '.read_proc_mount') as mock_open: ++ '.read_proc_mount') as mock_open, \ ++ mock.patch('cinder.volume.drivers.quobyte.QuobyteDriver' ++ '._validate_volume') as mock_validate: + # Content of /proc/mount (not mounted yet). + mock_open.return_value = six.StringIO( + "/dev/sda5 / ext4 rw,relatime,data=ordered 0 0") +@@ -127,17 +136,15 @@ class QuobyteDriverTestCase(test.TestCase): + 'mount.quobyte', self.TEST_QUOBYTE_VOLUME, + self.TEST_MNT_POINT, run_as_root=False) + +- getfattr_call = mock.call( +- 'getfattr', '-n', 'quobyte.info', self.TEST_MNT_POINT, +- run_as_root=False) +- + mock_execute.assert_has_calls( +- [mkdir_call, mount_call, getfattr_call], any_order=False) ++ [mkdir_call, mount_call], any_order=False) ++ mock_validate.called_once_with(self.TEST_MNT_POINT) + + def test_mount_quobyte_already_mounted_detected_seen_in_proc_mount(self): +- with mock.patch.object(self._driver, '_execute') as mock_execute, \ ++ with mock.patch('cinder.volume.drivers.quobyte.QuobyteDriver' ++ '.read_proc_mount') as mock_open, \ + mock.patch('cinder.volume.drivers.quobyte.QuobyteDriver' +- '.read_proc_mount') as mock_open: ++ '._validate_volume') as mock_validate: + # Content of /proc/mount (already mounted). + mock_open.return_value = six.StringIO( + "quobyte@%s %s fuse rw,nosuid,nodev,noatime,user_id=1000" +@@ -146,10 +153,7 @@ class QuobyteDriverTestCase(test.TestCase): + + self._driver._mount_quobyte(self.TEST_QUOBYTE_VOLUME, + self.TEST_MNT_POINT) +- +- mock_execute.assert_called_once_with( +- 'getfattr', '-n', 'quobyte.info', self.TEST_MNT_POINT, +- run_as_root=False) ++ mock_validate.assert_called_once_with(self.TEST_MNT_POINT) + + def test_mount_quobyte_should_suppress_and_log_already_mounted_error(self): + """test_mount_quobyte_should_suppress_and_log_already_mounted_error +@@ -604,13 +608,11 @@ class QuobyteDriverTestCase(test.TestCase): img_info = imageutils.QemuImgInfo(qemu_img_info_output) @@ -16,6 +83,91 @@ index de5169c..7856679 100644 image_utils.qemu_img_info.assert_called_once_with(volume_path, run_as_root=False) image_utils.resize_image.assert_called_once_with(volume_path, 3) +@@ -928,3 +930,84 @@ class QuobyteDriverTestCase(test.TestCase): + self.assertEqual("true", + drv.configuration.nas_secure_file_permissions) + self.assertFalse(drv._execute_as_root) ++ ++ @mock.patch.object(psutil, "disk_partitions") ++ @mock.patch.object(os, "stat") ++ def test_validate_volume_all_good(self, stat_mock, part_mock): ++ part_mock.return_value = self.get_mock_partitions() ++ drv = self._driver ++ ++ def statMockCall(*args): ++ if args[0] == self.TEST_MNT_POINT: ++ stat_result = mock.Mock() ++ stat_result.st_size = 0 ++ return stat_result ++ return os.stat(args) ++ ++ stat_mock.side_effect = statMockCall ++ ++ drv._validate_volume(self.TEST_MNT_POINT) ++ ++ stat_mock.assert_called_once_with(self.TEST_MNT_POINT) ++ part_mock.assert_called_once_with(all=True) ++ ++ @mock.patch.object(psutil, "disk_partitions") ++ @mock.patch.object(os, "stat") ++ def test_validate_volume_mount_not_working(self, stat_mock, part_mock): ++ part_mock.return_value = self.get_mock_partitions() ++ drv = self._driver ++ ++ def statMockCall(*args): ++ if args[0] == self.TEST_MNT_POINT: ++ raise exception.VolumeDriverException() ++ ++ stat_mock.side_effect = [statMockCall, os.stat] ++ ++ self.assertRaises( ++ exception.VolumeDriverException, ++ drv._validate_volume, ++ self.TEST_MNT_POINT) ++ stat_mock.assert_called_once_with(self.TEST_MNT_POINT) ++ part_mock.assert_called_once_with(all=True) ++ ++ def test_validate_volume_no_mtab_entry(self): ++ msg = ("Volume driver reported an error: " ++ "No matching Quobyte mount entry for %(mpt)s" ++ " could be found for validation in partition list." ++ % {'mpt': self.TEST_MNT_POINT}) ++ ++ self.assertRaisesAndMessageMatches( ++ exception.VolumeDriverException, ++ msg, ++ self._driver._validate_volume, ++ self.TEST_MNT_POINT) ++ ++ @mock.patch.object(psutil, "disk_partitions") ++ def test_validate_volume_wrong_mount_type(self, part_mock): ++ mypart = mock.Mock() ++ mypart.device = "not-quobyte" ++ mypart.mountpoint = self.TEST_MNT_POINT ++ part_mock.return_value = [mypart] ++ msg = ("Volume driver reported an error: " ++ "The mount %(mpt)s is not a valid" ++ " Quobyte volume according to partition list." ++ % {'mpt': self.TEST_MNT_POINT}) ++ drv = self._driver ++ ++ self.assertRaisesAndMessageMatches( ++ exception.VolumeDriverException, ++ msg, ++ drv._validate_volume, ++ self.TEST_MNT_POINT) ++ part_mock.assert_called_once_with(all=True) ++ ++ @mock.patch.object(psutil, "disk_partitions") ++ def test_validate_volume_stale_mount(self, part_mock): ++ part_mock.return_value = self.get_mock_partitions() ++ drv = self._driver ++ ++ # As this uses a local fs dir size is >0, raising an exception ++ self.assertRaises( ++ exception.VolumeDriverException, ++ drv._validate_volume, ++ self.TEST_MNT_POINT) diff --git cinder/tests/unit/volume/drivers/test_remotefs.py cinder/tests/unit/volume/drivers/test_remotefs.py index 0cd2ad8..72d70f4 100644 --- cinder/tests/unit/volume/drivers/test_remotefs.py @@ -216,10 +368,18 @@ index 0cd2ad8..72d70f4 100644 def test_create_regular_file(self): self._driver._create_regular_file('/path', 1) diff --git cinder/volume/drivers/quobyte.py cinder/volume/drivers/quobyte.py -index 8d77e65..9d4086a 100644 +index 8d77e65..32d4021 100644 --- cinder/volume/drivers/quobyte.py +++ cinder/volume/drivers/quobyte.py -@@ -30,14 +30,14 @@ from cinder import interface +@@ -16,6 +16,7 @@ + + import errno + import os ++import psutil + + from oslo_concurrency import processutils + from oslo_config import cfg +@@ -30,14 +31,14 @@ from cinder import interface from cinder import utils from cinder.volume.drivers import remotefs as remotefs_drv @@ -238,17 +398,18 @@ index 8d77e65..9d4086a 100644 cfg.StrOpt('quobyte_client_cfg', help=('Path to a Quobyte Client configuration file.')), cfg.BoolOpt('quobyte_sparsed_volumes', -@@ -79,6 +79,9 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed): +@@ -79,6 +80,10 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed): Version history: 1.0 - Initial driver. 1.1 - Adds optional insecure NAS settings ++ 1.1.1 - Removes getfattr calls from driver + 1.1.2 - Fixes a bug in the creation of cloned volumes + 1.1.4 - Fixes capability to configure redundancy in quobyte_volume_url + 1.1.5 - Enables extension of volumes with snapshots """ driver_volume_type = 'quobyte' -@@ -172,7 +175,7 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed): +@@ -172,7 +177,7 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed): @utils.synchronized('quobyte', external=False) def create_cloned_volume(self, volume, src_vref): """Creates a clone of the specified volume.""" @@ -257,7 +418,7 @@ index 8d77e65..9d4086a 100644 @utils.synchronized('quobyte', external=False) def create_volume(self, volume): -@@ -295,14 +298,6 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed): +@@ -295,14 +300,6 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed): @utils.synchronized('quobyte', external=False) def extend_volume(self, volume, size_gb): volume_path = self.local_path(volume) @@ -272,7 +433,7 @@ index 8d77e65..9d4086a 100644 info = self._qemu_img_info(volume_path, volume.name) backing_fmt = info.file_format -@@ -312,7 +307,10 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed): +@@ -312,7 +309,10 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed): raise exception.InvalidVolume(msg % backing_fmt) # qemu-img can resize both raw and qcow2 files @@ -284,6 +445,60 @@ index 8d77e65..9d4086a 100644 def _do_create_volume(self, volume): """Create a volume on given Quobyte volume. +@@ -465,16 +465,40 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed): + self._validate_volume(mount_path) + + def _validate_volume(self, mount_path): +- """Wraps execute calls for checking validity of a Quobyte volume""" +- command = ['getfattr', "-n", "quobyte.info", mount_path] +- try: +- self._execute(*command, run_as_root=self._execute_as_root) +- except processutils.ProcessExecutionError as exc: +- msg = (_("The mount %(mount_path)s is not a valid" +- " Quobyte USP volume. Error: %(exc)s") +- % {'mount_path': mount_path, 'exc': exc}) +- raise exception.VolumeDriverException(msg) +- +- if not os.access(mount_path, os.W_OK | os.X_OK): +- LOG.warning(_LW("Volume is not writable. Please broaden the file" +- " permissions. Mount: %s"), mount_path) ++ """Runs a number of tests on the expect Quobyte mount""" ++ partitions = psutil.disk_partitions(all=True) ++ for p in partitions: ++ if mount_path == p.mountpoint: ++ if p.device.startswith("quobyte@"): ++ try: ++ statresult = os.stat(mount_path) ++ if statresult.st_size == 0: ++ # client looks healthy ++ if not os.access(mount_path, ++ os.W_OK | os.X_OK): ++ LOG.warning(_LW("Volume is not writable. " ++ "Please broaden the file" ++ " permissions." ++ " Mount: %s"), ++ mount_path) ++ return # we're happy here ++ else: ++ msg = (_("The mount %(mount_path)s is not a " ++ "valid Quobyte volume. Stale mount?") ++ % {'mount_path': mount_path}) ++ raise exception.VolumeDriverException(msg) ++ except Exception as exc: ++ msg = (_("The mount %(mount_path)s is not a valid" ++ " Quobyte volume. Error: %(exc)s . " ++ " Possibly a Quobyte client crash?") ++ % {'mount_path': mount_path, 'exc': exc}) ++ raise exception.VolumeDriverException(msg) ++ else: ++ msg = (_("The mount %(mount_path)s is not a valid" ++ " Quobyte volume according to partition list.") ++ % {'mount_path': mount_path}) ++ raise exception.VolumeDriverException(msg) ++ msg = (_("No matching Quobyte mount entry for %(mount_path)s" ++ " could be found for validation in partition list.") ++ % {'mount_path': mount_path}) ++ raise exception.VolumeDriverException(msg) diff --git cinder/volume/drivers/remotefs.py cinder/volume/drivers/remotefs.py index 6f07cb9..e76c945 100644 --- cinder/volume/drivers/remotefs.py diff --git a/full_quobyte_patch/Ocata/full_quobyte_ocata_nova.patch b/full_quobyte_patch/Ocata/full_quobyte_ocata_nova.patch new file mode 100644 index 0000000..2f387a9 --- /dev/null +++ b/full_quobyte_patch/Ocata/full_quobyte_ocata_nova.patch @@ -0,0 +1,434 @@ +diff --git nova/tests/unit/virt/libvirt/test_imagebackend.py nova/tests/unit/virt/libvirt/test_imagebackend.py +index b3cc277..2c6dff9 100644 +--- nova/tests/unit/virt/libvirt/test_imagebackend.py ++++ nova/tests/unit/virt/libvirt/test_imagebackend.py +@@ -120,9 +120,8 @@ class _ImageTestCase(object): + image.cache(fake_fetch, self.TEMPLATE_PATH, self.SIZE) + + self.assertEqual(fake_processutils.fake_execute_get_log(), +- ['fallocate -l 1 %s.fallocate_test' % self.PATH, +- 'fallocate -n -l %s %s' % (self.SIZE, self.PATH), +- 'fallocate -n -l %s %s' % (self.SIZE, self.PATH)]) ++ ['truncate -s %s %s' % (self.SIZE, self.PATH), ++ 'truncate -s %s %s' % (self.SIZE, self.PATH)]) + + def test_prealloc_image_without_write_access(self): + CONF.set_override('preallocate_images', 'space') +diff --git nova/tests/unit/virt/libvirt/volume/test_quobyte.py nova/tests/unit/virt/libvirt/volume/test_quobyte.py +index a43a79a..7f867fa 100644 +--- nova/tests/unit/virt/libvirt/volume/test_quobyte.py ++++ nova/tests/unit/virt/libvirt/volume/test_quobyte.py +@@ -15,12 +15,15 @@ + """Unit tests for the Quobyte volume driver module.""" + + import os ++import traceback + + import mock + from oslo_concurrency import processutils + from oslo_utils import fileutils ++import psutil ++import six + +-from nova import exception ++from nova import exception as nova_exception + from nova import test + from nova.tests.unit.virt.libvirt.volume import test_volume + from nova import utils +@@ -31,9 +34,37 @@ from nova.virt.libvirt.volume import quobyte + class QuobyteTestCase(test.NoDBTestCase): + """Tests the nova.virt.libvirt.volume.quobyte module utilities.""" + ++ TEST_MNT_POINT = mock.sentinel.TEST_MNT_POINT ++ ++ def assertRaisesAndMessageMatches( ++ self, excClass, msg, callableObj, *args, **kwargs): ++ """Ensure that the specified exception was raised. """ ++ ++ caught = False ++ try: ++ callableObj(*args, **kwargs) ++ except Exception as exc: ++ caught = True ++ self.assertIsInstance(exc, excClass, ++ 'Wrong exception caught: %s Stacktrace: %s' % ++ (exc, traceback.format_exc())) ++ self.assertIn(msg, six.text_type(exc)) ++ ++ if not caught: ++ self.fail('Expected raised exception but nothing caught.') ++ ++ def get_mock_partitions(self): ++ mypart = mock.Mock() ++ mypart.device = "quobyte@" ++ mypart.mountpoint = self.TEST_MNT_POINT ++ return [mypart] ++ ++ @mock.patch.object(os.path, "exists", return_value=False) + @mock.patch.object(fileutils, "ensure_tree") + @mock.patch.object(utils, "execute") +- def test_quobyte_mount_volume(self, mock_execute, mock_ensure_tree): ++ def test_quobyte_mount_volume_not_systemd(self, mock_execute, ++ mock_ensure_tree, ++ mock_exists): + mnt_base = '/mnt' + quobyte_volume = '192.168.1.1/volume-00001' + export_mnt_base = os.path.join(mnt_base, +@@ -43,17 +74,45 @@ class QuobyteTestCase(test.NoDBTestCase): + + mock_ensure_tree.assert_called_once_with(export_mnt_base) + expected_commands = [mock.call('mount.quobyte', ++ '--disable-xattrs', + quobyte_volume, +- export_mnt_base, +- check_exit_code=[0, 4]) ++ export_mnt_base) ++ ] ++ mock_execute.assert_has_calls(expected_commands) ++ mock_exists.assert_called_once_with(" /run/systemd/system") ++ ++ @mock.patch.object(os.path, "exists", return_value=True) ++ @mock.patch.object(fileutils, "ensure_tree") ++ @mock.patch.object(utils, "execute") ++ def test_quobyte_mount_volume_systemd(self, mock_execute, ++ mock_ensure_tree, ++ mock_exists): ++ mnt_base = '/mnt' ++ quobyte_volume = '192.168.1.1/volume-00001' ++ export_mnt_base = os.path.join(mnt_base, ++ utils.get_hash_str(quobyte_volume)) ++ ++ quobyte.mount_volume(quobyte_volume, export_mnt_base) ++ ++ mock_ensure_tree.assert_called_once_with(export_mnt_base) ++ expected_commands = [mock.call('systemd-run', ++ '--scope', ++ '--user', ++ 'mount.quobyte', ++ '--disable-xattrs', ++ quobyte_volume, ++ export_mnt_base) + ] + mock_execute.assert_has_calls(expected_commands) ++ mock_exists.assert_called_once_with(" /run/systemd/system") + ++ @mock.patch.object(os.path, "exists", return_value=False) + @mock.patch.object(fileutils, "ensure_tree") + @mock.patch.object(utils, "execute") + def test_quobyte_mount_volume_with_config(self, + mock_execute, +- mock_ensure_tree): ++ mock_ensure_tree, ++ mock_exists): + mnt_base = '/mnt' + quobyte_volume = '192.168.1.1/volume-00001' + export_mnt_base = os.path.join(mnt_base, +@@ -66,13 +125,14 @@ class QuobyteTestCase(test.NoDBTestCase): + + mock_ensure_tree.assert_called_once_with(export_mnt_base) + expected_commands = [mock.call('mount.quobyte', ++ '--disable-xattrs', + quobyte_volume, + export_mnt_base, + '-c', +- config_file_dummy, +- check_exit_code=[0, 4]) ++ config_file_dummy) + ] + mock_execute.assert_has_calls(expected_commands) ++ mock_exists.assert_called_once_with(" /run/systemd/system") + + @mock.patch.object(fileutils, "ensure_tree") + @mock.patch.object(utils, "execute", +@@ -141,49 +201,91 @@ class QuobyteTestCase(test.NoDBTestCase): + "the Quobyte Volume at %s", + export_mnt_base)) + +- @mock.patch.object(os, "access", return_value=True) +- @mock.patch.object(utils, "execute") +- def test_quobyte_is_valid_volume(self, mock_execute, mock_access): +- mnt_base = '/mnt' +- quobyte_volume = '192.168.1.1/volume-00001' +- export_mnt_base = os.path.join(mnt_base, +- utils.get_hash_str(quobyte_volume)) +- +- quobyte.validate_volume(export_mnt_base) +- +- mock_execute.assert_called_once_with('getfattr', +- '-n', +- 'quobyte.info', +- export_mnt_base) +- +- @mock.patch.object(utils, "execute", +- side_effect=(processutils. +- ProcessExecutionError)) +- def test_quobyte_is_valid_volume_vol_not_valid_volume(self, mock_execute): +- mnt_base = '/mnt' +- quobyte_volume = '192.168.1.1/volume-00001' +- export_mnt_base = os.path.join(mnt_base, +- utils.get_hash_str(quobyte_volume)) +- +- self.assertRaises(exception.NovaException, +- quobyte.validate_volume, +- export_mnt_base) +- +- @mock.patch.object(os, "access", return_value=False) +- @mock.patch.object(utils, "execute", +- side_effect=(processutils. +- ProcessExecutionError)) +- def test_quobyte_is_valid_volume_vol_no_valid_access(self, +- mock_execute, +- mock_access): +- mnt_base = '/mnt' +- quobyte_volume = '192.168.1.1/volume-00001' +- export_mnt_base = os.path.join(mnt_base, +- utils.get_hash_str(quobyte_volume)) +- +- self.assertRaises(exception.NovaException, +- quobyte.validate_volume, +- export_mnt_base) ++ @mock.patch.object(psutil, "disk_partitions") ++ @mock.patch.object(os, "stat") ++ def test_validate_volume_all_good(self, stat_mock, part_mock): ++ part_mock.return_value = self.get_mock_partitions() ++ drv = quobyte ++ ++ def statMockCall(*args): ++ if args[0] == self.TEST_MNT_POINT: ++ stat_result = mock.Mock() ++ stat_result.st_size = 0 ++ return stat_result ++ return os.stat(args) ++ stat_mock.side_effect = statMockCall ++ ++ drv.validate_volume(self.TEST_MNT_POINT) ++ ++ stat_mock.assert_called_once_with(self.TEST_MNT_POINT) ++ part_mock.assert_called_once_with(all=True) ++ ++ @mock.patch.object(psutil, "disk_partitions") ++ @mock.patch.object(os, "stat") ++ def test_validate_volume_mount_not_working(self, stat_mock, part_mock): ++ part_mock.return_value = self.get_mock_partitions() ++ drv = quobyte ++ ++ def statMockCall(*args): ++ print (args) ++ if args[0] == self.TEST_MNT_POINT: ++ raise nova_exception.InvalidVolume() ++ stat_mock.side_effect = [os.stat, statMockCall] ++ ++ self.assertRaises( ++ excClass=nova_exception.InvalidVolume, ++ callableObj=drv.validate_volume, ++ mount_path=self.TEST_MNT_POINT) ++ stat_mock.assert_called_with(self.TEST_MNT_POINT) ++ part_mock.assert_called_once_with(all=True) ++ ++ def test_validate_volume_no_mtab_entry(self): ++ msg = ("No matching Quobyte mount entry for %(mpt)s" ++ " could be found for validation in partition list." ++ % {'mpt': self.TEST_MNT_POINT}) ++ ++ self.assertRaisesAndMessageMatches( ++ nova_exception.InvalidVolume, ++ msg, ++ quobyte.validate_volume, ++ self.TEST_MNT_POINT) ++ ++ @mock.patch.object(psutil, "disk_partitions") ++ def test_validate_volume_wrong_mount_type(self, part_mock): ++ mypart = mock.Mock() ++ mypart.device = "not-quobyte" ++ mypart.mountpoint = self.TEST_MNT_POINT ++ part_mock.return_value = [mypart] ++ msg = ("The mount %(mpt)s is not a valid" ++ " Quobyte volume according to partition list." ++ % {'mpt': self.TEST_MNT_POINT}) ++ ++ self.assertRaisesAndMessageMatches( ++ nova_exception.InvalidVolume, ++ msg, ++ quobyte.validate_volume, ++ self.TEST_MNT_POINT) ++ part_mock.assert_called_once_with(all=True) ++ ++ @mock.patch.object(os, "stat") ++ @mock.patch.object(psutil, "disk_partitions") ++ def test_validate_volume_stale_mount(self, part_mock, stat_mock): ++ part_mock.return_value = self.get_mock_partitions() ++ ++ def statMockCall(*args): ++ if args[0] == self.TEST_MNT_POINT: ++ stat_result = mock.Mock() ++ stat_result.st_size = 1 ++ return stat_result ++ return os.stat(args) ++ stat_mock.side_effect = statMockCall ++ ++ # As this uses a dir size >0, it raises an exception ++ self.assertRaises( ++ nova_exception.InvalidVolume, ++ quobyte.validate_volume, ++ self.TEST_MNT_POINT) ++ part_mock.assert_called_once_with(all=True) + + + class LibvirtQuobyteVolumeDriverTestCase( +@@ -332,12 +434,12 @@ class LibvirtQuobyteVolumeDriverTestCase( + + def exe_side_effect(*cmd, **kwargs): + if cmd == mock.ANY: +- raise exception.NovaException() ++ raise nova_exception.NovaException() + + with mock.patch.object(quobyte, + 'validate_volume') as mock_execute: + mock_execute.side_effect = exe_side_effect +- self.assertRaises(exception.NovaException, ++ self.assertRaises(nova_exception.NovaException, + libvirt_driver.connect_volume, + connection_info, + self.disk_info) +diff --git nova/virt/libvirt/imagebackend.py nova/virt/libvirt/imagebackend.py +index 8dd227e..eaf5c81 100644 +--- nova/virt/libvirt/imagebackend.py ++++ nova/virt/libvirt/imagebackend.py +@@ -232,9 +232,11 @@ class Image(object): + if os.path.exists(base) and size > self.get_disk_size(base): + self.resize_image(size) + +- if (self.preallocate and self._can_fallocate() and +- os.access(self.path, os.W_OK)): +- utils.execute('fallocate', '-n', '-l', size, self.path) ++ if (self.preallocate and os.access(self.path, os.W_OK)): ++ LOG.debug('Truncating new image at %(path)s to virtual size ' ++ '(patched for Quobyte).', ++ {'path': self.path}) ++ utils.execute('truncate', '-s', size, self.path) + + def _can_fallocate(self): + """Check once per class, whether fallocate(1) is available, +diff --git nova/virt/libvirt/volume/quobyte.py nova/virt/libvirt/volume/quobyte.py +index 284a084..f36aacb 100644 +--- nova/virt/libvirt/volume/quobyte.py ++++ nova/virt/libvirt/volume/quobyte.py +@@ -19,6 +19,7 @@ import os + from oslo_concurrency import processutils + from oslo_log import log as logging + from oslo_utils import fileutils ++import psutil + import six + + import nova.conf +@@ -44,15 +45,22 @@ def mount_volume(volume, mnt_base, configfile=None): + """Wraps execute calls for mounting a Quobyte volume""" + fileutils.ensure_tree(mnt_base) + +- command = ['mount.quobyte', volume, mnt_base] ++ # NOTE(kaisers): disable xattrs to speed up io as this omits ++ # additional metadata requests in the backend. xattrs can be ++ # enabled without issues but will reduce performance. ++ command = ['mount.quobyte', '--disable-xattrs', volume, mnt_base] ++ if os.path.exists(" /run/systemd/system"): ++ # Note(kaisers): with systemd this requires a separate CGROUP to ++ # prevent Nova service stop/restarts from killing the mount. ++ command = ['systemd-run', '--scope', '--user', 'mount.quobyte', ++ '--disable-xattrs', volume, mnt_base] + if configfile: + command.extend(['-c', configfile]) + + LOG.debug('Mounting volume %s at mount point %s ...', + volume, + mnt_base) +- # Run mount command but do not fail on already mounted exit code +- utils.execute(*command, check_exit_code=[0, 4]) ++ utils.execute(*command) + LOG.info(_LI('Mounted volume: %s'), volume) + + +@@ -69,21 +77,32 @@ def umount_volume(mnt_base): + mnt_base) + + +-def validate_volume(mnt_base): +- """Wraps execute calls for checking validity of a Quobyte volume""" +- command = ['getfattr', "-n", "quobyte.info", mnt_base] +- try: +- utils.execute(*command) +- except processutils.ProcessExecutionError as exc: +- msg = (_("The mount %(mount_path)s is not a valid" +- " Quobyte volume. Error: %(exc)s") +- % {'mount_path': mnt_base, 'exc': exc}) +- raise nova_exception.InternalError(msg) +- +- if not os.access(mnt_base, os.W_OK | os.X_OK): +- msg = (_LE("Volume is not writable. Please broaden the file" +- " permissions. Mount: %s") % mnt_base) +- raise nova_exception.InternalError(msg) ++def validate_volume(mount_path): ++ """Runs a number of tests to be sure this is a (working) Quobyte mount""" ++ partitions = psutil.disk_partitions(all=True) ++ for p in partitions: ++ if mount_path != p.mountpoint: ++ continue ++ if p.device.startswith("quobyte@"): ++ statresult = os.stat(mount_path) ++ # Note(kaisers): Quobyte always shows mount points with size 0 ++ if statresult.st_size == 0: ++ # client looks healthy ++ return # we're happy here ++ else: ++ msg = (_("The mount %(mount_path)s is not a " ++ "valid Quobyte volume. Stale mount?") ++ % {'mount_path': mount_path}) ++ raise nova_exception.InvalidVolume(msg) ++ else: ++ msg = (_("The mount %(mount_path)s is not a valid" ++ " Quobyte volume according to partition list.") ++ % {'mount_path': mount_path}) ++ raise nova_exception.InvalidVolume(msg) ++ msg = (_("No matching Quobyte mount entry for %(mount_path)s" ++ " could be found for validation in partition list.") ++ % {'mount_path': mount_path}) ++ raise nova_exception.InvalidVolume(msg) + + + class LibvirtQuobyteVolumeDriver(fs.LibvirtBaseFileSystemVolumeDriver): +@@ -106,7 +125,7 @@ class LibvirtQuobyteVolumeDriver(fs.LibvirtBaseFileSystemVolumeDriver): + + return conf + +- @utils.synchronized('connect_volume') ++ @utils.synchronized('connect_qb_volume') + def connect_volume(self, connection_info, disk_info): + """Connect the volume.""" + data = connection_info['data'] +@@ -127,12 +146,12 @@ class LibvirtQuobyteVolumeDriver(fs.LibvirtBaseFileSystemVolumeDriver): + + if not mounted: + mount_volume(quobyte_volume, +- mount_path, +- CONF.libvirt.quobyte_client_cfg) ++ mount_path, ++ CONF.libvirt.quobyte_client_cfg) + + validate_volume(mount_path) + +- @utils.synchronized('connect_volume') ++ @utils.synchronized('connect_qb_volume') + def disconnect_volume(self, connection_info, disk_dev): + """Disconnect the volume.""" + +diff --git releasenotes/notes/bug_1659328-73686be497f5f85a.yaml releasenotes/notes/bug_1659328-73686be497f5f85a.yaml +new file mode 100644 +index 0000000..e55248e +--- /dev/null ++++ releasenotes/notes/bug_1659328-73686be497f5f85a.yaml +@@ -0,0 +1,6 @@ ++--- ++ ++fixes: ++ - | ++ The i/o performance for Quobyte volumes has been increased significantly ++ by disabling xattrs. diff --git a/full_quobyte_patch/README.md b/full_quobyte_patch/README.md index d7d05b9..112584c 100644 --- a/full_quobyte_patch/README.md +++ b/full_quobyte_patch/README.md @@ -9,6 +9,13 @@ When complete these patches contain all relevant fixes and changes for the given - Fixes bug [1674611](https://bugs.launchpad.net/cinder/+bug/1674611) with patch [447958](https://review.openstack.org/#/c/447958/), thus allowing e.g. cloned backups of Quobyte Cinder volumes. - Fixes bug [1675710](https://bugs.launchpad.net/cinder/+bug/1675710) with patch [449553](https://review.openstack.org/#/c/449553/) in order to allow specifying multiple Quobyte registries in the quobyte_volume_url Cinder configuration option. - Fixes bug [1687048](https://bugs.launchpad.net/cinder/+bug/1687048) with patch [461471](https://review.openstack.org/#/c/461471/) + - Removes the requirement to support extended attributes from Cinder mounts with patch [427833](https://review.openstack.org/#/c/427833/) for improved performance + +- Nova + - Fixes bug [1530860](https://bugs.launchpad.net/nova/+bug/1530860) with patch [432344](https://review.openstack.org/#/c/432344/) in order to prevent systemd service restarts from unmounting Nova mounts + - Fixes bug [1679976](https://bugs.launchpad.net/nova/+bug/1679976) with patch [453537](https://review.openstack.org/#/c/453537/) disallowing a specific and potentially bad exit code during volume mounts + - Removes the requirement to support extended attributes from Nova mounts with patch [428646](https://review.openstack.org/#/c/428646/) for improved performance + - Exchanges Nova fallocate on qcow2 ephemeral images with a truncate command for improved performance with Quobyte ### Usage