diff --git a/nova/tests/unit/virt/lxd/test_driver.py b/nova/tests/unit/virt/lxd/test_driver.py index aee7212d..10fa5b88 100644 --- a/nova/tests/unit/virt/lxd/test_driver.py +++ b/nova/tests/unit/virt/lxd/test_driver.py @@ -1176,6 +1176,34 @@ class LXDDriverTest(test.NoDBTestCase): self.assertEqual(expected, value) + @mock.patch.object(driver.utils, 'execute') + def test__get_zpool_info(self, execute): + # first test with a zpool; should make 3 calls to execute + execute.side_effect = [ + ('1\n', None), + ('2\n', None), + ('3\n', None) + ] + expected = { + 'total': 1, + 'used': 2, + 'available': 3, + } + self.assertEqual(expected, driver._get_zpool_info('lxd')) + + # then test with a zfs dataset; should just be 2 calls + execute.reset_mock() + execute.side_effect = [ + ('10\n', None), + ('20\n', None), + ] + expected = { + 'total': 30, + 'used': 10, + 'available': 20, + } + self.assertEqual(expected, driver._get_zpool_info('lxd/dataset')) + @mock.patch('socket.gethostname', mock.Mock(return_value='fake_hostname')) @mock.patch('nova.virt.lxd.driver.open') @mock.patch.object(driver.utils, 'execute') @@ -1210,9 +1238,9 @@ class LXDDriverTest(test.NoDBTestCase): 'Core(s) per socket: 5\n' 'Thread(s) per core: 4\n\n', None), - ('2.17T\n', None), - ('200.4G\n', None), - ('1.8T\n', None) + ('2385940232273\n', None), # 2.17T + ('215177861529\n', None), # 200.4G + ('1979120929996\n', None) # 1.8T ] meminfo = mock.MagicMock() diff --git a/nova/virt/lxd/driver.py b/nova/virt/lxd/driver.py index 7cd6c20a..38ff0857 100644 --- a/nova/virt/lxd/driver.py +++ b/nova/virt/lxd/driver.py @@ -66,7 +66,6 @@ import psutil from oslo_concurrency import lockutils from nova.compute import task_states from oslo_utils import excutils -from oslo_utils import strutils from nova.virt import firewall _ = i18n._ @@ -192,23 +191,43 @@ def _get_fs_info(path): 'used': used} -def _get_zpool_info(pool): - """Get free/used/total disk space in a zfs pool.""" - def _get_zpool_attribute(attribute): - value, err = utils.execute('zpool', 'list', +def _get_zpool_info(pool_or_dataset): + """Get the free/used/total diskspace in a zfs pool or dataset. + A dataset is distinguished by having a '/' in the string. + + :param pool_or_dataset: The string name of the pool or dataset + :type pool_or_dataset: str + :returns: dictionary with keys 'total', 'available', 'used' + :rtype: Dict[str, int] + :raises: :class:`exception.NovaException` + :raises: :class:`oslo.concurrency.PorcessExecutionError` + :raises: :class:`OSError` + """ + def _get_zfs_attribute(cmd, attribute): + value, err = utils.execute(cmd, 'list', '-o', attribute, - '-H', pool, + '-H', + '-p', + pool_or_dataset, run_as_root=True) if err: - msg = _('Unable to parse zpool output.') + msg = _('Unable to parse zfs output.') raise exception.NovaException(msg) - value = strutils.string_to_bytes('{}B'.format(value.strip()), - return_int=True) + value = int(value.strip()) return value - total = _get_zpool_attribute('size') - used = _get_zpool_attribute('alloc') - available = _get_zpool_attribute('free') + if '/' in pool_or_dataset: + # it's a dataset: + # for zfs datasets we only have 'available' and 'used' and so need to + # construct the total from available and used. + used = _get_zfs_attribute('zfs', 'used') + available = _get_zfs_attribute('zfs', 'available') + total = available + used + else: + # otherwise it's a zpool + total = _get_zfs_attribute('zpool', 'size') + used = _get_zfs_attribute('zpool', 'alloc') + available = _get_zfs_attribute('zpool', 'free') return {'total': total, 'available': available, 'used': used}