diff --git a/Makefile b/Makefile index 1fbe3699..e5538477 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,7 @@ ADMIN_API_METHODS_SIMPLE = \ admin.label.Remove \ admin.pool.Add \ admin.pool.Info \ + admin.pool.UsageDetails \ admin.pool.List \ admin.pool.ListDrivers \ admin.pool.Remove \ diff --git a/qubes/api/admin.py b/qubes/api/admin.py index e997cd4a..f9a9c35c 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -622,6 +622,7 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI): self.fire_event_for_permission(pool=pool) other_info = '' + # Deprecated: remove this when all tools using this call are updated pool_size = pool.size if pool_size is not None: other_info += 'size={}\n'.format(pool_size) @@ -630,11 +631,6 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI): if pool_usage is not None: other_info += 'usage={}\n'.format(pool_usage) - pool_details = pool.usage_details - for name in pool_details: - if name not in ['data_size', 'data_usage']: - other_info += '{}={}\n'.format(name, pool_details[name]) - try: included_in = pool.included_in(self.app) if included_in: @@ -646,6 +642,26 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI): for prop, val in sorted(pool.config.items())) + \ other_info + @qubes.api.method('admin.pool.UsageDetails', no_payload=True, + scope='global', read=True) + @asyncio.coroutine + def pool_usage(self): + self.enforce(self.dest.name == 'dom0') + self.enforce(self.arg in self.app.pools.keys()) + + pool = self.app.pools[self.arg] + + self.fire_event_for_permission(pool=pool) + + usage = '' + + pool_details = pool.usage_details + + for name in sorted(pool_details): + usage += '{}={}\n'.format(name, pool_details[name]) + + return usage + @qubes.api.method('admin.pool.Add', scope='global', write=True) @asyncio.coroutine diff --git a/qubes/storage/lvm.py b/qubes/storage/lvm.py index 1c502d5e..b0a3b52d 100644 --- a/qubes/storage/lvm.py +++ b/qubes/storage/lvm.py @@ -207,8 +207,8 @@ class ThinPool(qubes.storage.Pool): metadata_usage = qubes.storage.lvm.size_cache[ self.volume_group + '/' + self.thin_pool]['metadata_usage'] except KeyError: - metadata_size = None - metadata_usage = None + metadata_size = 0 + metadata_usage = 0 result['metadata_size'] = metadata_size result['metadata_usage'] = metadata_usage diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index 5db6225b..47153ce6 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -582,14 +582,13 @@ class TC_00_VMs(AdminAPITestCase): 'pool1': unittest.mock.Mock(config={ 'param1': 'value1', 'param2': 'value2'}, usage=102400, - size=204800, - usage_details={'metadata_size': 500}) + size=204800) } self.app.pools['pool1'].included_in.return_value = None value = self.call_mgmt_func(b'admin.pool.Info', b'dom0', b'pool1') self.assertEqual(value, - 'param1=value1\nparam2=value2\nsize=204800\nusage=102400\nmetadata_size=500\n') + 'param1=value1\nparam2=value2\nsize=204800\nusage=102400\n') self.assertFalse(self.app.save.called) def test_151_pool_info_unsupported_size(self): @@ -623,6 +622,23 @@ class TC_00_VMs(AdminAPITestCase): '\nincluded_in=pool1\n') self.assertFalse(self.app.save.called) + def test_153_pool_usage(self): + self.app.pools = { + 'pool1': unittest.mock.Mock(config={ + 'param1': 'value1', 'param2': 'value2'}, + usage_details={ + 'data_usage': 102400, + 'data_size': 204800, + 'metadata_size': 1024, + 'metadata_usage': 50}) + } + self.app.pools['pool1'].included_in.return_value = None + value = self.call_mgmt_func(b'admin.pool.UsageDetails', b'dom0', b'pool1') + + self.assertEqual(value, + 'data_size=204800\ndata_usage=102400\nmetadata_size=1024\nmetadata_usage=50\n') + self.assertFalse(self.app.save.called) + @unittest.mock.patch('qubes.storage.pool_drivers') @unittest.mock.patch('qubes.storage.driver_parameters') def test_160_pool_add(self, mock_parameters, mock_drivers): diff --git a/qubes/tests/storage_lvm.py b/qubes/tests/storage_lvm.py index 5837395c..563fdf9f 100644 --- a/qubes/tests/storage_lvm.py +++ b/qubes/tests/storage_lvm.py @@ -997,6 +997,38 @@ class TC_00_ThinPool(ThinPoolBase): self.assertNotIn(volume1, list(self.pool.volumes)) self.assertNotIn(volume1, list(self.pool.volumes)) + def test_110_metadata_size(self): + with self.assertNotRaises(NotImplementedError): + usage = self.pool.usage_details + + metadata_size = usage['metadata_size'] + environ = os.environ.copy() + environ['LC_ALL'] = 'C.utf8' + pool_size = subprocess.check_output(['sudo', 'lvs', '--noheadings', + '-o', 'lv_metadata_size', + '--units', 'b', + self.pool.volume_group + '/' + self.pool.thin_pool], + env=environ) + self.assertEqual(metadata_size, int(pool_size.strip()[:-1])) + + def test_111_metadata_usage(self): + with self.assertNotRaises(NotImplementedError): + usage = self.pool.usage_details + + metadata_usage = usage['metadata_usage'] + environ = os.environ.copy() + environ['LC_ALL'] = 'C.utf8' + + pool_info = subprocess.check_output(['sudo', 'lvs', '--noheadings', + '-o', 'lv_metadata_size,metadata_percent', + '--units', 'b', self.pool.volume_group + '/' + self.pool.thin_pool], + env=environ) + pool_size, pool_usage = pool_info.strip().split() + pool_size = int(pool_size[:-1]) + pool_usage = float(pool_usage) + self.assertEqual(metadata_usage, int(pool_size * pool_usage / 100)) + + @skipUnlessLvmPoolExists class TC_01_ThinPool(ThinPoolBase, qubes.tests.SystemTestCase): ''' Sanity tests for :py:class:`qubes.storage.lvm.ThinPool` '''