Add app.pools collection and related functions
Collection to access storage pools and methods to create/remove pools.
This commit is contained in:
parent
64d2f13212
commit
e7d89bcc0d
@ -31,6 +31,7 @@ import qubesmgmt.vm
|
|||||||
import qubesmgmt.label
|
import qubesmgmt.label
|
||||||
import qubesmgmt.exc
|
import qubesmgmt.exc
|
||||||
import qubesmgmt.utils
|
import qubesmgmt.utils
|
||||||
|
import qubesmgmt.storage
|
||||||
|
|
||||||
QUBESD_SOCK = '/var/run/qubesd.sock'
|
QUBESD_SOCK = '/var/run/qubesd.sock'
|
||||||
BUF_SIZE = 4096
|
BUF_SIZE = 4096
|
||||||
@ -109,12 +110,67 @@ class QubesBase(qubesmgmt.base.PropertyHolder):
|
|||||||
domains = None
|
domains = None
|
||||||
#: labels collection
|
#: labels collection
|
||||||
labels = None
|
labels = None
|
||||||
|
#: storage pools
|
||||||
|
pools = None
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(QubesBase, self).__init__(self, 'mgmt.property.', 'dom0')
|
super(QubesBase, self).__init__(self, 'mgmt.property.', 'dom0')
|
||||||
self.domains = VMCollection(self)
|
self.domains = VMCollection(self)
|
||||||
self.labels = qubesmgmt.base.WrapperObjectsCollection(
|
self.labels = qubesmgmt.base.WrapperObjectsCollection(
|
||||||
self, 'mgmt.label.List', qubesmgmt.label.Label)
|
self, 'mgmt.label.List', qubesmgmt.label.Label)
|
||||||
|
self.pools = qubesmgmt.base.WrapperObjectsCollection(
|
||||||
|
self, 'mgmt.pool.List', qubesmgmt.storage.Pool)
|
||||||
|
#: cache for available storage pool drivers and options to create them
|
||||||
|
self._pool_drivers = None
|
||||||
|
|
||||||
|
def _refresh_pool_drivers(self):
|
||||||
|
'''
|
||||||
|
Refresh cached storage pool drivers and their parameters.
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
'''
|
||||||
|
if self._pool_drivers is None:
|
||||||
|
pool_drivers_data = self.qubesd_call(
|
||||||
|
'dom0', 'mgmt.pool.ListDrivers', None, None)
|
||||||
|
assert pool_drivers_data.endswith(b'\n')
|
||||||
|
pool_drivers = {}
|
||||||
|
for driver_line in pool_drivers_data.decode('ascii').splitlines():
|
||||||
|
if not driver_line:
|
||||||
|
continue
|
||||||
|
driver_name, driver_options = driver_line.split(' ', 1)
|
||||||
|
pool_drivers[driver_name] = driver_options.split(' ')
|
||||||
|
self._pool_drivers = pool_drivers
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pool_drivers(self):
|
||||||
|
''' Available storage pool drivers '''
|
||||||
|
self._refresh_pool_drivers()
|
||||||
|
return self._pool_drivers.keys()
|
||||||
|
|
||||||
|
def pool_driver_parameters(self, driver):
|
||||||
|
''' Parameters to initialize storage pool using given driver '''
|
||||||
|
self._refresh_pool_drivers()
|
||||||
|
return self._pool_drivers[driver]
|
||||||
|
|
||||||
|
def add_pool(self, name, driver, **kwargs):
|
||||||
|
''' Add a storage pool to config
|
||||||
|
|
||||||
|
:param name: name of storage pool to create
|
||||||
|
:param driver: driver to use, see :py:meth:`pool_drivers` for
|
||||||
|
available drivers
|
||||||
|
:param kwargs: configuration parameters for storage pool,
|
||||||
|
see :py:meth:`pool_driver_parameters` for a list
|
||||||
|
'''
|
||||||
|
# sort parameters only to ease testing, not required by API
|
||||||
|
payload = 'name={}\n'.format(name) + \
|
||||||
|
''.join('{}={}\n'.format(key, value)
|
||||||
|
for key, value in sorted(kwargs.items()))
|
||||||
|
self.qubesd_call('dom0', 'mgmt.pool.Add', driver,
|
||||||
|
payload.encode('utf-8'))
|
||||||
|
|
||||||
|
def remove_pool(self, name):
|
||||||
|
''' Remove a storage pool '''
|
||||||
|
self.qubesd_call('dom0', 'mgmt.pool.Remove', name, None)
|
||||||
|
|
||||||
|
|
||||||
class QubesLocal(QubesBase):
|
class QubesLocal(QubesBase):
|
||||||
|
@ -80,6 +80,11 @@ class Volume(object):
|
|||||||
info = info.decode('ascii')
|
info = info.decode('ascii')
|
||||||
self._info = dict([line.split('=', 1) for line in info.splitlines()])
|
self._info = dict([line.split('=', 1) for line in info.splitlines()])
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, Volume):
|
||||||
|
return self.pool == other.pool and self.vid == other.vid
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pool(self):
|
def pool(self):
|
||||||
'''Storage volume pool name.'''
|
'''Storage volume pool name.'''
|
||||||
@ -175,3 +180,59 @@ class Volume(object):
|
|||||||
if not isinstance(revision, str):
|
if not isinstance(revision, str):
|
||||||
raise TypeError('revision must be a str')
|
raise TypeError('revision must be a str')
|
||||||
self._qubesd_call('Revert', revision.encode('ascii'))
|
self._qubesd_call('Revert', revision.encode('ascii'))
|
||||||
|
|
||||||
|
|
||||||
|
class Pool(object):
|
||||||
|
''' A Pool is used to manage different kind of volumes (File
|
||||||
|
based/LVM/Btrfs/...).
|
||||||
|
'''
|
||||||
|
def __init__(self, app, name=None):
|
||||||
|
''' Initialize storage pool wrapper
|
||||||
|
|
||||||
|
:param app: Qubes() object
|
||||||
|
:param name: name of the pool
|
||||||
|
'''
|
||||||
|
self.app = app
|
||||||
|
self.name = name
|
||||||
|
self._config = None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, Pool):
|
||||||
|
return self.name == other.name
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
if isinstance(other, Pool):
|
||||||
|
return self.name < other.name
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self):
|
||||||
|
''' Storage pool config '''
|
||||||
|
if self._config is None:
|
||||||
|
pool_info_data = self.app.qubesd_call(
|
||||||
|
'dom0', 'mgmt.pool.Info', self.name, None)
|
||||||
|
pool_info_data = pool_info_data.decode('utf-8')
|
||||||
|
assert pool_info_data.endswith('\n')
|
||||||
|
pool_info_data = pool_info_data[:-1]
|
||||||
|
self._config = dict(
|
||||||
|
l.split('=', 1) for l in pool_info_data.splitlines())
|
||||||
|
return self._config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def driver(self):
|
||||||
|
''' Storage pool driver '''
|
||||||
|
return self.config['driver']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def volumes(self):
|
||||||
|
''' Volumes managed by this pool '''
|
||||||
|
volumes_data = self.app.qubesd_call(
|
||||||
|
'dom0', 'mgmt.pool.volume.List', self.name, None)
|
||||||
|
assert volumes_data.endswith(b'\n')
|
||||||
|
volumes_data = volumes_data[:-1].decode('ascii')
|
||||||
|
for vid in volumes_data.splitlines():
|
||||||
|
yield Volume(self.app, self.name, vid)
|
||||||
|
@ -232,3 +232,78 @@ class TestPoolVolume(TestVMVolume):
|
|||||||
b'some-id snapid1')] = b'0\x00'
|
b'some-id snapid1')] = b'0\x00'
|
||||||
self.vol.revert('snapid1')
|
self.vol.revert('snapid1')
|
||||||
self.assertAllCalled()
|
self.assertAllCalled()
|
||||||
|
|
||||||
|
|
||||||
|
class TestPool(qubesmgmt.tests.QubesTestCase):
|
||||||
|
def test_000_list(self):
|
||||||
|
self.app.expected_calls[('dom0', 'mgmt.pool.List', None, None)] = \
|
||||||
|
b'0\x00file\nlvm\n'
|
||||||
|
seen = set()
|
||||||
|
for pool in self.app.pools:
|
||||||
|
self.assertIsInstance(pool, qubesmgmt.storage.Pool)
|
||||||
|
self.assertIn(pool.name, ('file', 'lvm'))
|
||||||
|
self.assertNotIn(pool.name, seen)
|
||||||
|
seen.add(pool.name)
|
||||||
|
|
||||||
|
self.assertEqual(seen, set(['file', 'lvm']))
|
||||||
|
self.assertAllCalled()
|
||||||
|
|
||||||
|
def test_010_config(self):
|
||||||
|
self.app.expected_calls[('dom0', 'mgmt.pool.List', None, None)] = \
|
||||||
|
b'0\x00file\nlvm\n'
|
||||||
|
self.app.expected_calls[('dom0', 'mgmt.pool.Info', 'file', None)] = \
|
||||||
|
b'0\x00driver=file\n' \
|
||||||
|
b'dir_path=/var/lib/qubes\n' \
|
||||||
|
b'name=file\n' \
|
||||||
|
b'revisions_to_keep=3\n'
|
||||||
|
pool = self.app.pools['file']
|
||||||
|
self.assertEqual(pool.config, {
|
||||||
|
'driver': 'file',
|
||||||
|
'dir_path': '/var/lib/qubes',
|
||||||
|
'name': 'file',
|
||||||
|
'revisions_to_keep': '3',
|
||||||
|
})
|
||||||
|
self.assertAllCalled()
|
||||||
|
|
||||||
|
def test_020_volumes(self):
|
||||||
|
self.app.expected_calls[('dom0', 'mgmt.pool.List', None, None)] = \
|
||||||
|
b'0\x00file\nlvm\n'
|
||||||
|
self.app.expected_calls[
|
||||||
|
('dom0', 'mgmt.pool.volume.List', 'file', None)] = \
|
||||||
|
b'0\x00vol1\n' \
|
||||||
|
b'vol2\n'
|
||||||
|
pool = self.app.pools['file']
|
||||||
|
seen = set()
|
||||||
|
for volume in pool.volumes:
|
||||||
|
self.assertIsInstance(volume, qubesmgmt.storage.Volume)
|
||||||
|
self.assertIn(volume.vid, ('vol1', 'vol2'))
|
||||||
|
self.assertEqual(volume.pool, 'file')
|
||||||
|
self.assertNotIn(volume.vid, seen)
|
||||||
|
seen.add(volume.vid)
|
||||||
|
|
||||||
|
self.assertEqual(seen, set(['vol1', 'vol2']))
|
||||||
|
self.assertAllCalled()
|
||||||
|
|
||||||
|
def test_030_pool_drivers(self):
|
||||||
|
self.app.expected_calls[
|
||||||
|
('dom0', 'mgmt.pool.ListDrivers', None, None)] = \
|
||||||
|
b'0\x00file dir_path revisions_to_keep\n' \
|
||||||
|
b'lvm volume_group thin_pool revisions_to_keep\n'
|
||||||
|
self.assertEqual(set(self.app.pool_drivers), set(['file', 'lvm']))
|
||||||
|
self.assertEqual(set(self.app.pool_driver_parameters('file')),
|
||||||
|
set(['dir_path', 'revisions_to_keep']))
|
||||||
|
self.assertAllCalled()
|
||||||
|
|
||||||
|
def test_040_add(self):
|
||||||
|
self.app.expected_calls[
|
||||||
|
('dom0', 'mgmt.pool.Add', 'some-driver',
|
||||||
|
b'name=test-pool\nparam1=value1\nparam2=123\n')] = b'0\x00'
|
||||||
|
self.app.add_pool('test-pool', driver='some-driver',
|
||||||
|
param1='value1', param2=123)
|
||||||
|
self.assertAllCalled()
|
||||||
|
|
||||||
|
def test_050_remove(self):
|
||||||
|
self.app.expected_calls[
|
||||||
|
('dom0', 'mgmt.pool.Remove', 'test-pool', None)] = b'0\x00'
|
||||||
|
self.app.remove_pool('test-pool')
|
||||||
|
self.assertAllCalled()
|
||||||
|
Loading…
Reference in New Issue
Block a user