storage: use direct object references, not only identifiers
Reference objects, not their IDs - this way when object is modified, it is visible everywhere where it is used. Main changes: - volume.pool - Pool object - volume.source - Volume object Since volume have Pool object reference now, move volume related functions into Volume class (from Pool class). This avoids horrible `storage.get_pool(volume).something(volume)` construct. One issue here is since volume.source reference a Volume object from a different VM - VM's template, now VM load order is important. Since we don't have control over it, initialize vm.storage when needed - possibly while initializing storage of different VM. Since we don't have cycles in AppVM-TemplateVM dependencies, it is safe. Also, since this commit, volume.source (if defined) always points at volume of the same name from VM's template. Using volumes with something else as a source is no longer supported. QubesOS/qubes-issues#2256
This commit is contained in:
parent
80b459b6d5
commit
0f12870803
@ -59,6 +59,7 @@ else:
|
|||||||
import qubes
|
import qubes
|
||||||
import qubes.ext
|
import qubes.ext
|
||||||
import qubes.utils
|
import qubes.utils
|
||||||
|
import qubes.storage
|
||||||
import qubes.vm
|
import qubes.vm
|
||||||
import qubes.vm.adminvm
|
import qubes.vm.adminvm
|
||||||
import qubes.vm.qubesvm
|
import qubes.vm.qubesvm
|
||||||
@ -1019,12 +1020,14 @@ class Qubes(qubes.PropertyHolder):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
def get_pool(self, name):
|
def get_pool(self, pool):
|
||||||
''' Returns a :py:class:`qubes.storage.Pool` instance '''
|
''' Returns a :py:class:`qubes.storage.Pool` instance '''
|
||||||
|
if isinstance(pool, qubes.storage.Pool):
|
||||||
|
return pool
|
||||||
try:
|
try:
|
||||||
return self.pools[name]
|
return self.pools[pool]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise qubes.exc.QubesException('Unknown storage pool ' + name)
|
raise qubes.exc.QubesException('Unknown storage pool ' + pool)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_pool(**kwargs):
|
def _get_pool(**kwargs):
|
||||||
|
@ -86,7 +86,7 @@ class Volume(object):
|
|||||||
''' Initialize a volume.
|
''' Initialize a volume.
|
||||||
|
|
||||||
:param str name: The domain name
|
:param str name: The domain name
|
||||||
:param str pool: The pool name
|
:param Pool pool: The pool object
|
||||||
:param str vid: Volume identifier needs to be unique in pool
|
:param str vid: Volume identifier needs to be unique in pool
|
||||||
:param bool internal: If `True` volume is hidden when qvm-block ls
|
:param bool internal: If `True` volume is hidden when qvm-block ls
|
||||||
is used
|
is used
|
||||||
@ -96,15 +96,18 @@ class Volume(object):
|
|||||||
:param bool rw: If true volume will be mounted read-write
|
:param bool rw: If true volume will be mounted read-write
|
||||||
:param bool snap_on_start: Create a snapshot from source on start
|
:param bool snap_on_start: Create a snapshot from source on start
|
||||||
:param bool save_on_stop: Write changes to disk in vm.stop()
|
:param bool save_on_stop: Write changes to disk in vm.stop()
|
||||||
:param str source: Vid of other volume in same pool
|
:param Volume source: other volume in same pool, or None
|
||||||
:param str/int size: Size of the volume
|
:param str/int size: Size of the volume
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
super(Volume, self).__init__(**kwargs)
|
super(Volume, self).__init__(**kwargs)
|
||||||
|
assert isinstance(pool, Pool)
|
||||||
|
assert source is None or (isinstance(source, Volume)
|
||||||
|
and source.pool == pool)
|
||||||
|
|
||||||
self.name = str(name)
|
self.name = str(name)
|
||||||
self.pool = str(pool)
|
self.pool = pool
|
||||||
self.internal = internal
|
self.internal = internal
|
||||||
self.removable = removable
|
self.removable = removable
|
||||||
self.revisions_to_keep = int(revisions_to_keep)
|
self.revisions_to_keep = int(revisions_to_keep)
|
||||||
@ -116,7 +119,9 @@ class Volume(object):
|
|||||||
self.vid = vid
|
self.vid = vid
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return other.pool == self.pool and other.vid == self.vid
|
if isinstance(other, Volume):
|
||||||
|
return other.pool == self.pool and other.vid == self.vid
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash('%s:%s' % (self.pool, self.vid))
|
return hash('%s:%s' % (self.pool, self.vid))
|
||||||
@ -125,7 +130,7 @@ class Volume(object):
|
|||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '{!r}'.format(self.pool + ':' + self.vid)
|
return '{!r}'.format(str(self.pool) + ':' + self.vid)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.vid)
|
return str(self.vid)
|
||||||
@ -134,6 +139,93 @@ class Volume(object):
|
|||||||
config = _sanitize_config(self.config)
|
config = _sanitize_config(self.config)
|
||||||
return lxml.etree.Element('volume', **config)
|
return lxml.etree.Element('volume', **config)
|
||||||
|
|
||||||
|
def create(self):
|
||||||
|
''' Create the given volume on disk.
|
||||||
|
|
||||||
|
This can be implemented as a coroutine.
|
||||||
|
'''
|
||||||
|
raise self._not_implemented("create")
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
''' Write the snapshot to disk
|
||||||
|
|
||||||
|
This can be implemented as a coroutine.'''
|
||||||
|
raise self._not_implemented("commit")
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
''' Returns an object that can be `open()`. '''
|
||||||
|
raise self._not_implemented("export")
|
||||||
|
|
||||||
|
def import_data(self):
|
||||||
|
''' Returns an object that can be `open()`. '''
|
||||||
|
raise self._not_implemented("import")
|
||||||
|
|
||||||
|
def import_data_end(self, success):
|
||||||
|
''' End data import operation. This may be used by pool
|
||||||
|
implementation to commit changes, cleanup temporary files etc.
|
||||||
|
|
||||||
|
:param success: True if data import was successful, otherwise False
|
||||||
|
'''
|
||||||
|
# by default do nothing
|
||||||
|
pass
|
||||||
|
|
||||||
|
def import_volume(self, src_volume):
|
||||||
|
''' Imports data from a different volume (possibly in a different
|
||||||
|
pool '''
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
raise self._not_implemented("import_volume")
|
||||||
|
|
||||||
|
def is_dirty(self):
|
||||||
|
''' Return `True` if volume was not properly shutdown and commited '''
|
||||||
|
raise self._not_implemented("is_dirty")
|
||||||
|
|
||||||
|
def is_outdated(self):
|
||||||
|
''' Returns `True` if the currently used `volume.source` of a snapshot
|
||||||
|
volume is outdated.
|
||||||
|
'''
|
||||||
|
raise self._not_implemented("is_outdated")
|
||||||
|
|
||||||
|
def recover(self):
|
||||||
|
''' Try to recover a :py:class:`Volume` or :py:class:`SnapVolume` '''
|
||||||
|
raise self._not_implemented("recover")
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
''' Drop and recreate volume without copying it's content from source.
|
||||||
|
'''
|
||||||
|
raise self._not_implemented("reset")
|
||||||
|
|
||||||
|
def resize(self, size):
|
||||||
|
''' Expands volume, throws
|
||||||
|
:py:class:`qubes.storage.StoragePoolException` if
|
||||||
|
given size is less than current_size
|
||||||
|
|
||||||
|
This can be implemented as a coroutine.
|
||||||
|
'''
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
raise self._not_implemented("resize")
|
||||||
|
|
||||||
|
def revert(self, revision=None):
|
||||||
|
''' Revert volume to previous revision '''
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
raise self._not_implemented("revert")
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
''' Do what ever is needed on start
|
||||||
|
|
||||||
|
This can be implemented as a coroutine.'''
|
||||||
|
raise self._not_implemented("start")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
''' Do what ever is needed on stop
|
||||||
|
|
||||||
|
This can be implemented as a coroutine.'''
|
||||||
|
|
||||||
|
def verify(self):
|
||||||
|
''' Verifies the volume.
|
||||||
|
|
||||||
|
This can be implemented as a coroutine.'''
|
||||||
|
raise self._not_implemented("verify")
|
||||||
|
|
||||||
def block_device(self):
|
def block_device(self):
|
||||||
''' Return :py:class:`BlockDevice` for serialization in
|
''' Return :py:class:`BlockDevice` for serialization in
|
||||||
the libvirt XML template as <disk>.
|
the libvirt XML template as <disk>.
|
||||||
@ -160,7 +252,7 @@ class Volume(object):
|
|||||||
@property
|
@property
|
||||||
def config(self):
|
def config(self):
|
||||||
''' return config data for serialization to qubes.xml '''
|
''' return config data for serialization to qubes.xml '''
|
||||||
result = {'name': self.name, 'pool': self.pool, 'vid': self.vid, }
|
result = {'name': self.name, 'pool': str(self.pool), 'vid': self.vid, }
|
||||||
|
|
||||||
if self.internal:
|
if self.internal:
|
||||||
result['internal'] = self.internal
|
result['internal'] = self.internal
|
||||||
@ -184,10 +276,15 @@ class Volume(object):
|
|||||||
result['snap_on_start'] = self.snap_on_start
|
result['snap_on_start'] = self.snap_on_start
|
||||||
|
|
||||||
if self.source:
|
if self.source:
|
||||||
result['source'] = self.source
|
result['source'] = str(self.source)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def _not_implemented(self, method_name):
|
||||||
|
''' Helper for emitting helpful `NotImplementedError` exceptions '''
|
||||||
|
msg = "Volume {!s} has {!s}() not implemented"
|
||||||
|
msg = msg.format(str(self.__class__.__name__), method_name)
|
||||||
|
return NotImplementedError(msg)
|
||||||
|
|
||||||
class Storage(object):
|
class Storage(object):
|
||||||
''' Class for handling VM virtual disks.
|
''' Class for handling VM virtual disks.
|
||||||
@ -204,12 +301,20 @@ class Storage(object):
|
|||||||
self.log = self.vm.log
|
self.log = self.vm.log
|
||||||
#: Additional drive (currently used only by HVM)
|
#: Additional drive (currently used only by HVM)
|
||||||
self.drive = None
|
self.drive = None
|
||||||
self.pools = {}
|
|
||||||
|
|
||||||
if hasattr(vm, 'volume_config'):
|
if hasattr(vm, 'volume_config'):
|
||||||
for name, conf in self.vm.volume_config.items():
|
for name, conf in self.vm.volume_config.items():
|
||||||
if 'volume_type' in conf:
|
if 'source' in conf:
|
||||||
conf = self._migrate_config(conf)
|
template = getattr(vm, 'template', None)
|
||||||
|
if template:
|
||||||
|
# we have no control over VM load order,
|
||||||
|
# so initialize storage recursively if needed
|
||||||
|
if template.storage is None:
|
||||||
|
template.storage = Storage(template)
|
||||||
|
# FIXME: this effectively ignore 'source' value;
|
||||||
|
# maybe we don't need it at all if it's always from
|
||||||
|
# VM's template?
|
||||||
|
conf['source'] = template.volumes[name]
|
||||||
|
|
||||||
self.init_volume(name, conf)
|
self.init_volume(name, conf)
|
||||||
|
|
||||||
@ -223,50 +328,8 @@ class Storage(object):
|
|||||||
pool = self.vm.app.get_pool(volume_config['pool'])
|
pool = self.vm.app.get_pool(volume_config['pool'])
|
||||||
volume = pool.init_volume(self.vm, volume_config)
|
volume = pool.init_volume(self.vm, volume_config)
|
||||||
self.vm.volumes[name] = volume
|
self.vm.volumes[name] = volume
|
||||||
self.pools[name] = pool
|
|
||||||
return volume
|
return volume
|
||||||
|
|
||||||
def _migrate_config(self, conf):
|
|
||||||
''' Migrates from the old config style to new
|
|
||||||
''' # FIXME: Remove this compatibility hack
|
|
||||||
assert 'volume_type' in conf
|
|
||||||
_type = conf['volume_type']
|
|
||||||
old_volume_types = [
|
|
||||||
'read-write', 'read-only', 'origin', 'snapshot', 'volatile'
|
|
||||||
]
|
|
||||||
msg = "Volume {!s} has unknown type {!s}".format(conf['name'], _type)
|
|
||||||
assert conf['volume_type'] in old_volume_types, msg
|
|
||||||
if _type == 'origin':
|
|
||||||
conf['rw'] = True
|
|
||||||
conf['source'] = None
|
|
||||||
conf['save_on_stop'] = True
|
|
||||||
conf['revisions_to_keep'] = 1
|
|
||||||
elif _type == 'snapshot':
|
|
||||||
conf['rw'] = False
|
|
||||||
if conf['pool'] == 'default':
|
|
||||||
template_vid = os.path.join('vm-templates',
|
|
||||||
self.vm.template.name, conf['name'])
|
|
||||||
elif conf['pool'] == 'qubes_dom0':
|
|
||||||
template_vid = os.path.join(
|
|
||||||
'qubes_dom0', self.vm.template.name + '-' + conf['name'])
|
|
||||||
conf['source'] = template_vid
|
|
||||||
conf['snap_on_start'] = True
|
|
||||||
elif _type == 'read-write':
|
|
||||||
conf['rw'] = True
|
|
||||||
conf['save_on_stop'] = True
|
|
||||||
conf['revisions_to_keep'] = 0
|
|
||||||
elif _type == 'read-only':
|
|
||||||
conf['rw'] = False
|
|
||||||
conf['snap_on_start'] = True
|
|
||||||
conf['save_on_stop'] = False
|
|
||||||
conf['revisions_to_keep'] = 0
|
|
||||||
elif _type == 'volatile':
|
|
||||||
conf['snap_on_start'] = False
|
|
||||||
conf['save_on_stop'] = False
|
|
||||||
conf['revisions_to_keep'] = 0
|
|
||||||
del conf['volume_type']
|
|
||||||
return conf
|
|
||||||
|
|
||||||
def attach(self, volume, rw=False):
|
def attach(self, volume, rw=False):
|
||||||
''' Attach a volume to the domain '''
|
''' Attach a volume to the domain '''
|
||||||
assert self.vm.is_running()
|
assert self.vm.is_running()
|
||||||
@ -342,7 +405,7 @@ class Storage(object):
|
|||||||
''' Resizes volume a read-writable volume '''
|
''' Resizes volume a read-writable volume '''
|
||||||
if isinstance(volume, str):
|
if isinstance(volume, str):
|
||||||
volume = self.vm.volumes[volume]
|
volume = self.vm.volumes[volume]
|
||||||
ret = self.get_pool(volume).resize(volume, size)
|
ret = volume.resize(size)
|
||||||
if asyncio.iscoroutine(ret):
|
if asyncio.iscoroutine(ret):
|
||||||
yield from ret
|
yield from ret
|
||||||
if self.vm.is_running():
|
if self.vm.is_running():
|
||||||
@ -359,7 +422,7 @@ class Storage(object):
|
|||||||
for volume in self.vm.volumes.values():
|
for volume in self.vm.volumes.values():
|
||||||
# launch the operation, if it's asynchronous, then append to wait
|
# launch the operation, if it's asynchronous, then append to wait
|
||||||
# for them at the end
|
# for them at the end
|
||||||
ret = self.get_pool(volume).create(volume)
|
ret = volume.create()
|
||||||
if asyncio.iscoroutine(ret):
|
if asyncio.iscoroutine(ret):
|
||||||
coros.append(ret)
|
coros.append(ret)
|
||||||
if coros:
|
if coros:
|
||||||
@ -378,10 +441,10 @@ class Storage(object):
|
|||||||
self.vm.volumes = {}
|
self.vm.volumes = {}
|
||||||
with VmCreationManager(self.vm):
|
with VmCreationManager(self.vm):
|
||||||
for name, config in self.vm.volume_config.items():
|
for name, config in self.vm.volume_config.items():
|
||||||
dst_pool = self.get_pool(config['pool'])
|
dst_pool = self.vm.app.get_pool(config['pool'])
|
||||||
dst = dst_pool.init_volume(self.vm, config)
|
dst = dst_pool.init_volume(self.vm, config)
|
||||||
src_volume = src_vm.volumes[name]
|
src_volume = src_vm.volumes[name]
|
||||||
src_pool = self.vm.app.get_pool(src_volume.pool)
|
src_pool = src_volume.pool
|
||||||
if dst_pool == src_pool:
|
if dst_pool == src_pool:
|
||||||
msg = "Cloning volume {!s} from vm {!s}"
|
msg = "Cloning volume {!s} from vm {!s}"
|
||||||
self.vm.log.info(msg.format(src_volume.name, src_vm.name))
|
self.vm.log.info(msg.format(src_volume.name, src_vm.name))
|
||||||
@ -404,7 +467,7 @@ class Storage(object):
|
|||||||
volume = clone_op_ret
|
volume = clone_op_ret
|
||||||
|
|
||||||
assert volume, "%s.clone() returned '%s'" % (
|
assert volume, "%s.clone() returned '%s'" % (
|
||||||
self.get_pool(self.vm.volume_config[name]['pool']).
|
self.vm.app.get_pool(self.vm.volume_config[name]['pool']).
|
||||||
__class__.__name__, volume)
|
__class__.__name__, volume)
|
||||||
|
|
||||||
self.vm.volumes[name] = volume
|
self.vm.volumes[name] = volume
|
||||||
@ -418,8 +481,7 @@ class Storage(object):
|
|||||||
|
|
||||||
volumes = self.vm.volumes
|
volumes = self.vm.volumes
|
||||||
for volume in volumes.values():
|
for volume in volumes.values():
|
||||||
pool = self.get_pool(volume)
|
if volume.is_outdated():
|
||||||
if pool.is_outdated(volume):
|
|
||||||
result += [volume]
|
result += [volume]
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@ -428,7 +490,7 @@ class Storage(object):
|
|||||||
''' Notify the pools that the domain was renamed '''
|
''' Notify the pools that the domain was renamed '''
|
||||||
volumes = self.vm.volumes
|
volumes = self.vm.volumes
|
||||||
for name, volume in volumes.items():
|
for name, volume in volumes.items():
|
||||||
pool = self.get_pool(volume)
|
pool = volume.pool
|
||||||
volumes[name] = pool.rename(volume, old_name, new_name)
|
volumes[name] = pool.rename(volume, old_name, new_name)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
@ -443,7 +505,7 @@ class Storage(object):
|
|||||||
'VM directory does not exist: {}'.format(self.vm.dir_path))
|
'VM directory does not exist: {}'.format(self.vm.dir_path))
|
||||||
futures = []
|
futures = []
|
||||||
for volume in self.vm.volumes.values():
|
for volume in self.vm.volumes.values():
|
||||||
ret = self.get_pool(volume).verify(volume)
|
ret = volume.verify()
|
||||||
if asyncio.iscoroutine(ret):
|
if asyncio.iscoroutine(ret):
|
||||||
futures.append(ret)
|
futures.append(ret)
|
||||||
if futures:
|
if futures:
|
||||||
@ -461,7 +523,7 @@ class Storage(object):
|
|||||||
for name, volume in self.vm.volumes.items():
|
for name, volume in self.vm.volumes.items():
|
||||||
self.log.info('Removing volume %s: %s' % (name, volume.vid))
|
self.log.info('Removing volume %s: %s' % (name, volume.vid))
|
||||||
try:
|
try:
|
||||||
ret = self.get_pool(volume).remove(volume)
|
ret = volume.pool.remove(volume)
|
||||||
if asyncio.iscoroutine(ret):
|
if asyncio.iscoroutine(ret):
|
||||||
futures.append(ret)
|
futures.append(ret)
|
||||||
except (IOError, OSError) as e:
|
except (IOError, OSError) as e:
|
||||||
@ -478,8 +540,7 @@ class Storage(object):
|
|||||||
''' Execute the start method on each pool '''
|
''' Execute the start method on each pool '''
|
||||||
futures = []
|
futures = []
|
||||||
for volume in self.vm.volumes.values():
|
for volume in self.vm.volumes.values():
|
||||||
pool = self.get_pool(volume)
|
ret = volume.start()
|
||||||
ret = pool.start(volume)
|
|
||||||
if asyncio.iscoroutine(ret):
|
if asyncio.iscoroutine(ret):
|
||||||
futures.append(ret)
|
futures.append(ret)
|
||||||
|
|
||||||
@ -491,29 +552,20 @@ class Storage(object):
|
|||||||
''' Execute the start method on each pool '''
|
''' Execute the start method on each pool '''
|
||||||
futures = []
|
futures = []
|
||||||
for volume in self.vm.volumes.values():
|
for volume in self.vm.volumes.values():
|
||||||
ret = self.get_pool(volume).stop(volume)
|
ret = volume.stop()
|
||||||
if asyncio.iscoroutine(ret):
|
if asyncio.iscoroutine(ret):
|
||||||
futures.append(ret)
|
futures.append(ret)
|
||||||
|
|
||||||
if futures:
|
if futures:
|
||||||
yield from asyncio.wait(futures)
|
yield from asyncio.wait(futures)
|
||||||
|
|
||||||
def get_pool(self, volume):
|
|
||||||
''' Helper function '''
|
|
||||||
assert isinstance(volume, (Volume, str)), \
|
|
||||||
"You need to pass a Volume or pool name as str"
|
|
||||||
if isinstance(volume, Volume):
|
|
||||||
return self.pools[volume.name]
|
|
||||||
|
|
||||||
return self.vm.app.pools[volume]
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def commit(self):
|
def commit(self):
|
||||||
''' Makes changes to an 'origin' volume persistent '''
|
''' Makes changes to an 'origin' volume persistent '''
|
||||||
futures = []
|
futures = []
|
||||||
for volume in self.vm.volumes.values():
|
for volume in self.vm.volumes.values():
|
||||||
if volume.save_on_stop:
|
if volume.save_on_stop:
|
||||||
ret = self.get_pool(volume).commit(volume)
|
ret = volume.commit()
|
||||||
if asyncio.iscoroutine(ret):
|
if asyncio.iscoroutine(ret):
|
||||||
futures.append(ret)
|
futures.append(ret)
|
||||||
|
|
||||||
@ -540,18 +592,18 @@ class Storage(object):
|
|||||||
assert isinstance(volume, (Volume, str)), \
|
assert isinstance(volume, (Volume, str)), \
|
||||||
"You need to pass a Volume or pool name as str"
|
"You need to pass a Volume or pool name as str"
|
||||||
if isinstance(volume, Volume):
|
if isinstance(volume, Volume):
|
||||||
return self.pools[volume.name].export(volume)
|
return volume.export()
|
||||||
|
|
||||||
return self.pools[volume].export(self.vm.volumes[volume])
|
return self.vm.volumes[volume].export()
|
||||||
|
|
||||||
def import_data(self, volume):
|
def import_data(self, volume):
|
||||||
''' Helper function to import volume data (pool.import_data(volume))'''
|
''' Helper function to import volume data (pool.import_data(volume))'''
|
||||||
assert isinstance(volume, (Volume, str)), \
|
assert isinstance(volume, (Volume, str)), \
|
||||||
"You need to pass a Volume or pool name as str"
|
"You need to pass a Volume or pool name as str"
|
||||||
if isinstance(volume, Volume):
|
if isinstance(volume, Volume):
|
||||||
return self.pools[volume.name].import_data(volume)
|
return volume.import_data()
|
||||||
|
|
||||||
return self.pools[volume].import_data(self.vm.volumes[volume])
|
return self.vm.volumes[volume].import_data()
|
||||||
|
|
||||||
def import_data_end(self, volume, success):
|
def import_data_end(self, volume, success):
|
||||||
''' Helper function to finish/cleanup data import
|
''' Helper function to finish/cleanup data import
|
||||||
@ -559,11 +611,10 @@ class Storage(object):
|
|||||||
assert isinstance(volume, (Volume, str)), \
|
assert isinstance(volume, (Volume, str)), \
|
||||||
"You need to pass a Volume or pool name as str"
|
"You need to pass a Volume or pool name as str"
|
||||||
if isinstance(volume, Volume):
|
if isinstance(volume, Volume):
|
||||||
return self.pools[volume.name].import_data_end(volume,
|
return volume.import_data_end(volume,
|
||||||
success=success)
|
success=success)
|
||||||
|
|
||||||
return self.pools[volume].import_data_end(self.vm.volumes[volume],
|
return self.vm.volumes[volume].import_data_end(success=success)
|
||||||
success=success)
|
|
||||||
|
|
||||||
|
|
||||||
class Pool(object):
|
class Pool(object):
|
||||||
@ -583,7 +634,11 @@ class Pool(object):
|
|||||||
kwargs['name'] = self.name
|
kwargs['name'] = self.name
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.name == other.name
|
if isinstance(other, Pool):
|
||||||
|
return self.name == other.name
|
||||||
|
elif isinstance(other, str):
|
||||||
|
return self.name == other
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
def __neq__(self, other):
|
def __neq__(self, other):
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
@ -598,79 +653,22 @@ class Pool(object):
|
|||||||
config = _sanitize_config(self.config)
|
config = _sanitize_config(self.config)
|
||||||
return lxml.etree.Element('pool', **config)
|
return lxml.etree.Element('pool', **config)
|
||||||
|
|
||||||
def create(self, volume):
|
|
||||||
''' Create the given volume on disk or copy from provided
|
|
||||||
`source_volume`.
|
|
||||||
|
|
||||||
This can be implemented as a coroutine.
|
|
||||||
'''
|
|
||||||
raise self._not_implemented("create")
|
|
||||||
|
|
||||||
def commit(self, volume): # pylint: disable=no-self-use
|
|
||||||
''' Write the snapshot to disk
|
|
||||||
|
|
||||||
This can be implemented as a coroutine.'''
|
|
||||||
msg = "Got volume_type {!s} when expected 'snap'"
|
|
||||||
msg = msg.format(volume.volume_type)
|
|
||||||
assert volume.volume_type == 'snap', msg
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self):
|
def config(self):
|
||||||
''' Returns the pool config to be written to qubes.xml '''
|
''' Returns the pool config to be written to qubes.xml '''
|
||||||
raise self._not_implemented("config")
|
raise self._not_implemented("config")
|
||||||
|
|
||||||
def clone(self, source, target):
|
|
||||||
''' Clone volume.
|
|
||||||
|
|
||||||
This can be implemented as a coroutine. '''
|
|
||||||
raise self._not_implemented("clone")
|
|
||||||
|
|
||||||
def destroy(self):
|
def destroy(self):
|
||||||
''' Called when removing the pool. Use this for implementation specific
|
''' Called when removing the pool. Use this for implementation specific
|
||||||
clean up.
|
clean up.
|
||||||
'''
|
'''
|
||||||
raise self._not_implemented("destroy")
|
raise self._not_implemented("destroy")
|
||||||
|
|
||||||
def export(self, volume):
|
|
||||||
''' Returns an object that can be `open()`. '''
|
|
||||||
raise self._not_implemented("export")
|
|
||||||
|
|
||||||
def import_data(self, volume):
|
|
||||||
''' Returns an object that can be `open()`. '''
|
|
||||||
raise self._not_implemented("import")
|
|
||||||
|
|
||||||
def import_data_end(self, volume, success):
|
|
||||||
''' End data import operation. This may be used by pool
|
|
||||||
implementation to commit changes, cleanup temporary files etc.
|
|
||||||
|
|
||||||
:param success: True if data import was successful, otherwise False
|
|
||||||
'''
|
|
||||||
# by default do nothing
|
|
||||||
pass
|
|
||||||
|
|
||||||
def import_volume(self, dst_pool, dst_volume, src_pool, src_volume):
|
|
||||||
''' Imports data to a volume in this pool '''
|
|
||||||
raise self._not_implemented("import_volume")
|
|
||||||
|
|
||||||
def init_volume(self, vm, volume_config):
|
def init_volume(self, vm, volume_config):
|
||||||
''' Initialize a :py:class:`qubes.storage.Volume` from `volume_config`.
|
''' Initialize a :py:class:`qubes.storage.Volume` from `volume_config`.
|
||||||
'''
|
'''
|
||||||
raise self._not_implemented("init_volume")
|
raise self._not_implemented("init_volume")
|
||||||
|
|
||||||
def is_dirty(self, volume):
|
|
||||||
''' Return `True` if volume was not properly shutdown and commited '''
|
|
||||||
raise self._not_implemented("is_dirty")
|
|
||||||
|
|
||||||
def is_outdated(self, volume):
|
|
||||||
''' Returns `True` if the currently used `volume.source` of a snapshot
|
|
||||||
volume is outdated.
|
|
||||||
'''
|
|
||||||
raise self._not_implemented("is_outdated")
|
|
||||||
|
|
||||||
def recover(self, volume):
|
|
||||||
''' Try to recover a :py:class:`Volume` or :py:class:`SnapVolume` '''
|
|
||||||
raise self._not_implemented("recover")
|
|
||||||
|
|
||||||
def remove(self, volume):
|
def remove(self, volume):
|
||||||
''' Remove volume.
|
''' Remove volume.
|
||||||
|
|
||||||
@ -681,47 +679,12 @@ class Pool(object):
|
|||||||
''' Called when the domain changes its name '''
|
''' Called when the domain changes its name '''
|
||||||
raise self._not_implemented("rename")
|
raise self._not_implemented("rename")
|
||||||
|
|
||||||
def reset(self, volume):
|
|
||||||
''' Drop and recreate volume without copying it's content from source.
|
|
||||||
'''
|
|
||||||
raise self._not_implemented("reset")
|
|
||||||
|
|
||||||
def resize(self, volume, size):
|
|
||||||
''' Expands volume, throws
|
|
||||||
:py:class:`qubes.storage.StoragePoolException` if
|
|
||||||
given size is less than current_size
|
|
||||||
|
|
||||||
This can be implemented as a coroutine.
|
|
||||||
'''
|
|
||||||
raise self._not_implemented("resize")
|
|
||||||
|
|
||||||
def revert(self, volume, revision=None):
|
|
||||||
''' Revert volume to previous revision '''
|
|
||||||
raise self._not_implemented("revert")
|
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
''' Called when adding a pool to the system. Use this for implementation
|
''' Called when adding a pool to the system. Use this for implementation
|
||||||
specific set up.
|
specific set up.
|
||||||
'''
|
'''
|
||||||
raise self._not_implemented("setup")
|
raise self._not_implemented("setup")
|
||||||
|
|
||||||
def start(self, volume): # pylint: disable=no-self-use
|
|
||||||
''' Do what ever is needed on start
|
|
||||||
|
|
||||||
This can be implemented as a coroutine.'''
|
|
||||||
raise self._not_implemented("start")
|
|
||||||
|
|
||||||
def stop(self, volume): # pylint: disable=no-self-use
|
|
||||||
''' Do what ever is needed on stop
|
|
||||||
|
|
||||||
This can be implemented as a coroutine.'''
|
|
||||||
|
|
||||||
def verify(self, volume):
|
|
||||||
''' Verifies the volume.
|
|
||||||
|
|
||||||
This can be implemented as a coroutine.'''
|
|
||||||
raise self._not_implemented("verify")
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def volumes(self):
|
def volumes(self):
|
||||||
''' Return a list of volumes managed by this pool '''
|
''' Return a list of volumes managed by this pool '''
|
||||||
@ -780,7 +743,7 @@ class VmCreationManager(object):
|
|||||||
if type is not None and value is not None and tb is not None:
|
if type is not None and value is not None and tb is not None:
|
||||||
for volume in self.vm.volumes.values():
|
for volume in self.vm.volumes.values():
|
||||||
try:
|
try:
|
||||||
pool = self.vm.storage.get_pool(volume)
|
pool = volume.pool
|
||||||
pool.remove(volume)
|
pool.remove(volume)
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
pass
|
pass
|
||||||
|
@ -56,30 +56,6 @@ class FilePool(qubes.storage.Pool):
|
|||||||
'revisions_to_keep': self.revisions_to_keep
|
'revisions_to_keep': self.revisions_to_keep
|
||||||
}
|
}
|
||||||
|
|
||||||
def clone(self, source, target):
|
|
||||||
new_dir = os.path.dirname(target.path)
|
|
||||||
if target._is_origin or target._is_volume:
|
|
||||||
if not os.path.exists:
|
|
||||||
os.makedirs(new_dir)
|
|
||||||
copy_file(source.path, target.path)
|
|
||||||
return target
|
|
||||||
|
|
||||||
def create(self, volume):
|
|
||||||
assert isinstance(volume.size, int) and volume.size > 0, \
|
|
||||||
'Volatile volume size must be > 0'
|
|
||||||
if volume._is_origin:
|
|
||||||
create_sparse_file(volume.path, volume.size)
|
|
||||||
create_sparse_file(volume.path_cow, volume.size)
|
|
||||||
elif not volume._is_snapshot:
|
|
||||||
if volume.source is not None:
|
|
||||||
source_path = os.path.join(self.dir_path,
|
|
||||||
volume.source + '.img')
|
|
||||||
copy_file(source_path, volume.path)
|
|
||||||
elif volume._is_volatile:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
create_sparse_file(volume.path, volume.size)
|
|
||||||
|
|
||||||
def init_volume(self, vm, volume_config):
|
def init_volume(self, vm, volume_config):
|
||||||
volume_config['dir_path'] = self.dir_path
|
volume_config['dir_path'] = self.dir_path
|
||||||
if os.path.join(self.dir_path, self._vid_prefix(vm)) == vm.dir_path:
|
if os.path.join(self.dir_path, self._vid_prefix(vm)) == vm.dir_path:
|
||||||
@ -98,45 +74,11 @@ class FilePool(qubes.storage.Pool):
|
|||||||
if 'revisions_to_keep' not in volume_config:
|
if 'revisions_to_keep' not in volume_config:
|
||||||
volume_config['revisions_to_keep'] = self.revisions_to_keep
|
volume_config['revisions_to_keep'] = self.revisions_to_keep
|
||||||
|
|
||||||
|
volume_config['pool'] = self
|
||||||
volume = FileVolume(**volume_config)
|
volume = FileVolume(**volume_config)
|
||||||
self._volumes += [volume]
|
self._volumes += [volume]
|
||||||
return volume
|
return volume
|
||||||
|
|
||||||
def is_dirty(self, volume):
|
|
||||||
return False # TODO: How to implement this?
|
|
||||||
|
|
||||||
def resize(self, volume, size):
|
|
||||||
''' Expands volume, throws
|
|
||||||
:py:class:`qubst.storage.qubes.storage.StoragePoolException` if
|
|
||||||
given size is less than current_size
|
|
||||||
''' # pylint: disable=no-self-use
|
|
||||||
if not volume.rw:
|
|
||||||
msg = 'Can not resize reađonly volume {!s}'.format(volume)
|
|
||||||
raise qubes.storage.StoragePoolException(msg)
|
|
||||||
|
|
||||||
if size <= volume.size:
|
|
||||||
raise qubes.storage.StoragePoolException(
|
|
||||||
'For your own safety, shrinking of %s is'
|
|
||||||
' disabled. If you really know what you'
|
|
||||||
' are doing, use `truncate` on %s manually.' %
|
|
||||||
(volume.name, volume.vid))
|
|
||||||
|
|
||||||
with open(volume.path, 'a+b') as fd:
|
|
||||||
fd.truncate(size)
|
|
||||||
|
|
||||||
p = subprocess.Popen(['sudo', 'losetup', '--associated', volume.path],
|
|
||||||
stdout=subprocess.PIPE)
|
|
||||||
result = p.communicate()
|
|
||||||
|
|
||||||
m = re.match(r'^(/dev/loop\d+):\s', result[0].decode())
|
|
||||||
if m is not None:
|
|
||||||
loop_dev = m.group(1)
|
|
||||||
|
|
||||||
# resize loop device
|
|
||||||
subprocess.check_call(['sudo', 'losetup', '--set-capacity',
|
|
||||||
loop_dev])
|
|
||||||
volume.size = size
|
|
||||||
|
|
||||||
def remove(self, volume):
|
def remove(self, volume):
|
||||||
if not volume.internal:
|
if not volume.internal:
|
||||||
return # do not remove random attached file volumes
|
return # do not remove random attached file volumes
|
||||||
@ -167,68 +109,9 @@ class FilePool(qubes.storage.Pool):
|
|||||||
|
|
||||||
return volume
|
return volume
|
||||||
|
|
||||||
def import_volume(self, dst_pool, dst_volume, src_pool, src_volume):
|
|
||||||
msg = "Can not import snapshot volume {!s} in to pool {!s} "
|
|
||||||
msg = msg.format(src_volume, self)
|
|
||||||
assert not src_volume.snap_on_start, msg
|
|
||||||
if dst_volume.save_on_stop:
|
|
||||||
copy_file(src_pool.export(src_volume), dst_volume.path)
|
|
||||||
return dst_volume
|
|
||||||
|
|
||||||
def commit(self, volume):
|
|
||||||
msg = 'Tried to commit a non commitable volume {!r}'.format(volume)
|
|
||||||
assert (volume._is_origin or volume._is_volume) and volume.rw, msg
|
|
||||||
|
|
||||||
if volume._is_volume:
|
|
||||||
return volume
|
|
||||||
|
|
||||||
if os.path.exists(volume.path_cow):
|
|
||||||
old_path = volume.path_cow + '.old'
|
|
||||||
os.rename(volume.path_cow, old_path)
|
|
||||||
|
|
||||||
old_umask = os.umask(0o002)
|
|
||||||
with open(volume.path_cow, 'w') as f_cow:
|
|
||||||
f_cow.truncate(volume.size)
|
|
||||||
os.umask(old_umask)
|
|
||||||
return volume
|
|
||||||
|
|
||||||
def destroy(self):
|
def destroy(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def export(self, volume):
|
|
||||||
return volume.path
|
|
||||||
|
|
||||||
def import_data(self, volume):
|
|
||||||
return volume.path
|
|
||||||
|
|
||||||
def reset(self, volume):
|
|
||||||
''' Remove and recreate a volatile volume '''
|
|
||||||
assert volume._is_volatile, "Not a volatile volume"
|
|
||||||
assert isinstance(volume.size, int) and volume.size > 0, \
|
|
||||||
'Volatile volume size must be > 0'
|
|
||||||
|
|
||||||
_remove_if_exists(volume.path)
|
|
||||||
|
|
||||||
with open(volume.path, "w") as f_volatile:
|
|
||||||
f_volatile.truncate(volume.size)
|
|
||||||
return volume
|
|
||||||
|
|
||||||
def revert(self, volume, revision=None):
|
|
||||||
if revision is not None:
|
|
||||||
try:
|
|
||||||
return volume.revisions[revision]
|
|
||||||
except KeyError:
|
|
||||||
msg = "Volume {!r} does not have revision {!s}"
|
|
||||||
msg = msg.format(volume, revision)
|
|
||||||
raise qubes.storage.StoragePoolException(msg)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
old_path = volume.revisions.values().pop()
|
|
||||||
os.rename(old_path, volume.path_cow)
|
|
||||||
except IndexError:
|
|
||||||
msg = "Volume {!r} does not have old revisions".format(volume)
|
|
||||||
raise qubes.storage.StoragePoolException(msg)
|
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
create_dir_if_not_exists(self.dir_path)
|
create_dir_if_not_exists(self.dir_path)
|
||||||
appvms_path = os.path.join(self.dir_path, 'appvms')
|
appvms_path = os.path.join(self.dir_path, 'appvms')
|
||||||
@ -236,38 +119,6 @@ class FilePool(qubes.storage.Pool):
|
|||||||
vm_templates_path = os.path.join(self.dir_path, 'vm-templates')
|
vm_templates_path = os.path.join(self.dir_path, 'vm-templates')
|
||||||
create_dir_if_not_exists(vm_templates_path)
|
create_dir_if_not_exists(vm_templates_path)
|
||||||
|
|
||||||
def start(self, volume):
|
|
||||||
if volume._is_volatile:
|
|
||||||
self.reset(volume)
|
|
||||||
else:
|
|
||||||
_check_path(volume.path)
|
|
||||||
if volume.snap_on_start:
|
|
||||||
if not volume.save_on_stop:
|
|
||||||
# make sure previous snapshot is removed - even if VM
|
|
||||||
# shutdown routing wasn't called (power interrupt or so)
|
|
||||||
_remove_if_exists(volume.path_cow)
|
|
||||||
try:
|
|
||||||
_check_path(volume.path_cow)
|
|
||||||
except qubes.storage.StoragePoolException:
|
|
||||||
create_sparse_file(volume.path_cow, volume.size)
|
|
||||||
_check_path(volume.path_cow)
|
|
||||||
if hasattr(volume, 'path_source_cow'):
|
|
||||||
try:
|
|
||||||
_check_path(volume.path_source_cow)
|
|
||||||
except qubes.storage.StoragePoolException:
|
|
||||||
create_sparse_file(volume.path_source_cow, volume.size)
|
|
||||||
_check_path(volume.path_source_cow)
|
|
||||||
return volume
|
|
||||||
|
|
||||||
def stop(self, volume):
|
|
||||||
if volume.save_on_stop:
|
|
||||||
self.commit(volume)
|
|
||||||
elif volume.snap_on_start:
|
|
||||||
_remove_if_exists(volume.path_cow)
|
|
||||||
else:
|
|
||||||
_remove_if_exists(volume.path)
|
|
||||||
return volume
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _vid_prefix(vm):
|
def _vid_prefix(vm):
|
||||||
''' Helper to create a prefix for the vid for volume
|
''' Helper to create a prefix for the vid for volume
|
||||||
@ -301,9 +152,6 @@ class FilePool(qubes.storage.Pool):
|
|||||||
|
|
||||||
return os.path.join(self.dir_path, self._vid_prefix(vm))
|
return os.path.join(self.dir_path, self._vid_prefix(vm))
|
||||||
|
|
||||||
def verify(self, volume):
|
|
||||||
return volume.verify()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def volumes(self):
|
def volumes(self):
|
||||||
return self._volumes
|
return self._volumes
|
||||||
@ -329,7 +177,7 @@ class FileVolume(qubes.storage.Volume):
|
|||||||
raise qubes.storage.StoragePoolException(msg)
|
raise qubes.storage.StoragePoolException(msg)
|
||||||
|
|
||||||
if self._is_snapshot:
|
if self._is_snapshot:
|
||||||
img_name = self.source + '-cow.img'
|
img_name = self.source.vid + '-cow.img'
|
||||||
self.path_source_cow = os.path.join(self.dir_path, img_name)
|
self.path_source_cow = os.path.join(self.dir_path, img_name)
|
||||||
elif self._is_volume or self._is_volatile:
|
elif self._is_volume or self._is_volatile:
|
||||||
pass
|
pass
|
||||||
@ -338,10 +186,153 @@ class FileVolume(qubes.storage.Volume):
|
|||||||
else:
|
else:
|
||||||
assert False, 'This should not happen'
|
assert False, 'This should not happen'
|
||||||
|
|
||||||
|
def create(self):
|
||||||
|
assert isinstance(self.size, int) and self.size > 0, \
|
||||||
|
'Volatile volume size must be > 0'
|
||||||
|
if self._is_origin:
|
||||||
|
create_sparse_file(self.path, self.size)
|
||||||
|
create_sparse_file(self.path_cow, self.size)
|
||||||
|
elif not self._is_snapshot:
|
||||||
|
if self.source is not None:
|
||||||
|
source_path = os.path.join(self.dir_path,
|
||||||
|
self.source.vid + '.img')
|
||||||
|
copy_file(source_path, self.path)
|
||||||
|
elif self._is_volatile:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
create_sparse_file(self.path, self.size)
|
||||||
|
|
||||||
|
def is_dirty(self):
|
||||||
|
return False # TODO: How to implement this?
|
||||||
|
|
||||||
|
def resize(self, size):
|
||||||
|
''' Expands volume, throws
|
||||||
|
:py:class:`qubst.storage.qubes.storage.StoragePoolException` if
|
||||||
|
given size is less than current_size
|
||||||
|
''' # pylint: disable=no-self-use
|
||||||
|
if not self.rw:
|
||||||
|
msg = 'Can not resize reađonly volume {!s}'.format(self)
|
||||||
|
raise qubes.storage.StoragePoolException(msg)
|
||||||
|
|
||||||
|
if size <= self.size:
|
||||||
|
raise qubes.storage.StoragePoolException(
|
||||||
|
'For your own safety, shrinking of %s is'
|
||||||
|
' disabled. If you really know what you'
|
||||||
|
' are doing, use `truncate` on %s manually.' %
|
||||||
|
(self.name, self.vid))
|
||||||
|
|
||||||
|
with open(self.path, 'a+b') as fd:
|
||||||
|
fd.truncate(size)
|
||||||
|
|
||||||
|
p = subprocess.Popen(['sudo', 'losetup', '--associated', self.path],
|
||||||
|
stdout=subprocess.PIPE)
|
||||||
|
result = p.communicate()
|
||||||
|
|
||||||
|
m = re.match(r'^(/dev/loop\d+):\s', result[0].decode())
|
||||||
|
if m is not None:
|
||||||
|
loop_dev = m.group(1)
|
||||||
|
|
||||||
|
# resize loop device
|
||||||
|
subprocess.check_call(['sudo', 'losetup', '--set-capacity',
|
||||||
|
loop_dev])
|
||||||
|
self.size = size
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
msg = 'Tried to commit a non commitable volume {!r}'.format(self)
|
||||||
|
assert (self._is_origin or self._is_volume) and self.rw, msg
|
||||||
|
|
||||||
|
if self._is_volume:
|
||||||
|
return self
|
||||||
|
|
||||||
|
if os.path.exists(self.path_cow):
|
||||||
|
old_path = self.path_cow + '.old'
|
||||||
|
os.rename(self.path_cow, old_path)
|
||||||
|
|
||||||
|
old_umask = os.umask(0o002)
|
||||||
|
with open(self.path_cow, 'w') as f_cow:
|
||||||
|
f_cow.truncate(self.size)
|
||||||
|
os.umask(old_umask)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
return self.path
|
||||||
|
|
||||||
|
def import_volume(self, src_volume):
|
||||||
|
msg = "Can not import snapshot volume {!s} in to pool {!s} "
|
||||||
|
msg = msg.format(src_volume, self)
|
||||||
|
assert not src_volume.snap_on_start, msg
|
||||||
|
if self.save_on_stop:
|
||||||
|
copy_file(src_volume.export(), self.path)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def import_data(self):
|
||||||
|
return self.path
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
''' Remove and recreate a volatile volume '''
|
||||||
|
assert self._is_volatile, "Not a volatile volume"
|
||||||
|
assert isinstance(self.size, int) and self.size > 0, \
|
||||||
|
'Volatile volume size must be > 0'
|
||||||
|
|
||||||
|
_remove_if_exists(self.path)
|
||||||
|
|
||||||
|
with open(self.path, "w") as f_volatile:
|
||||||
|
f_volatile.truncate(self.size)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def revert(self, revision=None):
|
||||||
|
if revision is not None:
|
||||||
|
try:
|
||||||
|
return self.revisions[revision]
|
||||||
|
except KeyError:
|
||||||
|
msg = "Volume {!r} does not have revision {!s}"
|
||||||
|
msg = msg.format(self, revision)
|
||||||
|
raise qubes.storage.StoragePoolException(msg)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
old_path = self.revisions.values().pop()
|
||||||
|
os.rename(old_path, self.path_cow)
|
||||||
|
except IndexError:
|
||||||
|
msg = "Volume {!r} does not have old revisions".format(self)
|
||||||
|
raise qubes.storage.StoragePoolException(msg)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if self._is_volatile:
|
||||||
|
self.reset()
|
||||||
|
else:
|
||||||
|
_check_path(self.path)
|
||||||
|
if self.snap_on_start:
|
||||||
|
if not self.save_on_stop:
|
||||||
|
# make sure previous snapshot is removed - even if VM
|
||||||
|
# shutdown routing wasn't called (power interrupt or so)
|
||||||
|
_remove_if_exists(self.path_cow)
|
||||||
|
try:
|
||||||
|
_check_path(self.path_cow)
|
||||||
|
except qubes.storage.StoragePoolException:
|
||||||
|
create_sparse_file(self.path_cow, self.size)
|
||||||
|
_check_path(self.path_cow)
|
||||||
|
if hasattr(self, 'path_source_cow'):
|
||||||
|
try:
|
||||||
|
_check_path(self.path_source_cow)
|
||||||
|
except qubes.storage.StoragePoolException:
|
||||||
|
create_sparse_file(self.path_source_cow, self.size)
|
||||||
|
_check_path(self.path_source_cow)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self.save_on_stop:
|
||||||
|
self.commit()
|
||||||
|
elif self.snap_on_start:
|
||||||
|
_remove_if_exists(self.path_cow)
|
||||||
|
else:
|
||||||
|
_remove_if_exists(self.path)
|
||||||
|
return self
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path(self):
|
def path(self):
|
||||||
if self._is_snapshot:
|
if self._is_snapshot:
|
||||||
return os.path.join(self.dir_path, self.source + '.img')
|
return os.path.join(self.dir_path, self.source.vid + '.img')
|
||||||
return os.path.join(self.dir_path, self.vid + '.img')
|
return os.path.join(self.dir_path, self.vid + '.img')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -33,7 +33,6 @@ class LinuxModules(Volume):
|
|||||||
|
|
||||||
def __init__(self, target_dir, kernel_version, **kwargs):
|
def __init__(self, target_dir, kernel_version, **kwargs):
|
||||||
kwargs['vid'] = ''
|
kwargs['vid'] = ''
|
||||||
kwargs['source'] = self
|
|
||||||
super(LinuxModules, self).__init__(**kwargs)
|
super(LinuxModules, self).__init__(**kwargs)
|
||||||
self._kernel_version = kernel_version
|
self._kernel_version = kernel_version
|
||||||
self.target_dir = target_dir
|
self.target_dir = target_dir
|
||||||
@ -81,6 +80,44 @@ class LinuxModules(Volume):
|
|||||||
def revisions(self):
|
def revisions(self):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
def is_dirty(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def clone(self, source):
|
||||||
|
if isinstance(source, LinuxModules):
|
||||||
|
# do nothing
|
||||||
|
return self
|
||||||
|
raise StoragePoolException('clone of LinuxModules volume from '
|
||||||
|
'different volume type is not supported')
|
||||||
|
|
||||||
|
def create(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
return self.path
|
||||||
|
|
||||||
|
def is_outdated(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
path = self.path
|
||||||
|
if path and not os.path.exists(path):
|
||||||
|
raise StoragePoolException('Missing kernel modules: %s' % path)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def verify(self):
|
||||||
|
if self.vid:
|
||||||
|
_check_path(self.path)
|
||||||
|
_check_path(self.vmlinuz)
|
||||||
|
_check_path(self.initramfs)
|
||||||
|
|
||||||
def block_device(self):
|
def block_device(self):
|
||||||
if self.vid:
|
if self.vid:
|
||||||
return super().block_device()
|
return super().block_device()
|
||||||
@ -98,22 +135,11 @@ class LinuxKernel(Pool):
|
|||||||
def init_volume(self, vm, volume_config):
|
def init_volume(self, vm, volume_config):
|
||||||
assert not volume_config['rw']
|
assert not volume_config['rw']
|
||||||
|
|
||||||
|
volume_config['pool'] = self
|
||||||
volume = LinuxModules(self.dir_path, lambda: vm.kernel, **volume_config)
|
volume = LinuxModules(self.dir_path, lambda: vm.kernel, **volume_config)
|
||||||
|
|
||||||
return volume
|
return volume
|
||||||
|
|
||||||
def is_dirty(self, volume):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def clone(self, source, target):
|
|
||||||
return target
|
|
||||||
|
|
||||||
def create(self, volume):
|
|
||||||
return volume
|
|
||||||
|
|
||||||
def commit(self, volume):
|
|
||||||
return volume
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self):
|
def config(self):
|
||||||
return {
|
return {
|
||||||
@ -125,15 +151,9 @@ class LinuxKernel(Pool):
|
|||||||
def destroy(self):
|
def destroy(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def export(self, volume):
|
|
||||||
return volume.path
|
|
||||||
|
|
||||||
def import_volume(self, dst_pool, dst_volume, src_pool, src_volume):
|
def import_volume(self, dst_pool, dst_volume, src_pool, src_volume):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def is_outdated(self, volume):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def remove(self, volume):
|
def remove(self, volume):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -143,28 +163,12 @@ class LinuxKernel(Pool):
|
|||||||
def setup(self):
|
def setup(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def start(self, volume):
|
|
||||||
path = volume.path
|
|
||||||
if path and not os.path.exists(path):
|
|
||||||
raise StoragePoolException('Missing kernel modules: %s' % path)
|
|
||||||
|
|
||||||
return volume
|
|
||||||
|
|
||||||
def stop(self, volume):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def verify(self, volume):
|
|
||||||
if volume.vid:
|
|
||||||
_check_path(volume.path)
|
|
||||||
_check_path(volume.vmlinuz)
|
|
||||||
_check_path(volume.initramfs)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def volumes(self):
|
def volumes(self):
|
||||||
''' Return all known kernel volumes '''
|
''' Return all known kernel volumes '''
|
||||||
return [LinuxModules(self.dir_path,
|
return [LinuxModules(self.dir_path,
|
||||||
kernel_version,
|
kernel_version,
|
||||||
pool=self.name,
|
pool=self,
|
||||||
name=kernel_version,
|
name=kernel_version,
|
||||||
internal=True,
|
internal=True,
|
||||||
rw=False
|
rw=False
|
||||||
|
@ -25,6 +25,8 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import qubes
|
import qubes
|
||||||
|
import qubes.storage
|
||||||
|
import qubes.utils
|
||||||
|
|
||||||
|
|
||||||
def check_lvm_version():
|
def check_lvm_version():
|
||||||
@ -54,34 +56,6 @@ class ThinPool(qubes.storage.Pool):
|
|||||||
self._pool_id = "{!s}/{!s}".format(volume_group, thin_pool)
|
self._pool_id = "{!s}/{!s}".format(volume_group, thin_pool)
|
||||||
self.log = logging.getLogger('qube.storage.lvm.%s' % self._pool_id)
|
self.log = logging.getLogger('qube.storage.lvm.%s' % self._pool_id)
|
||||||
|
|
||||||
def clone(self, source, target):
|
|
||||||
cmd = ['clone', str(source), str(target)]
|
|
||||||
qubes_lvm(cmd, self.log)
|
|
||||||
return target
|
|
||||||
|
|
||||||
def _commit(self, volume):
|
|
||||||
msg = "Trying to commit {!s}, but it has save_on_stop == False"
|
|
||||||
msg = msg.format(volume)
|
|
||||||
assert volume.save_on_stop, msg
|
|
||||||
|
|
||||||
msg = "Trying to commit {!s}, but it has rw == False"
|
|
||||||
msg = msg.format(volume)
|
|
||||||
assert volume.rw, msg
|
|
||||||
assert hasattr(volume, '_vid_snap')
|
|
||||||
|
|
||||||
try:
|
|
||||||
cmd = ['remove', volume.vid + "-back"]
|
|
||||||
qubes_lvm(cmd, self.log)
|
|
||||||
except qubes.storage.StoragePoolException:
|
|
||||||
pass
|
|
||||||
cmd = ['clone', volume.vid, volume.vid + "-back"]
|
|
||||||
qubes_lvm(cmd, self.log)
|
|
||||||
|
|
||||||
cmd = ['remove', volume.vid]
|
|
||||||
qubes_lvm(cmd, self.log)
|
|
||||||
cmd = ['clone', volume._vid_snap, volume.vid]
|
|
||||||
qubes_lvm(cmd, self.log)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self):
|
def config(self):
|
||||||
return {
|
return {
|
||||||
@ -91,36 +65,9 @@ class ThinPool(qubes.storage.Pool):
|
|||||||
'driver': ThinPool.driver
|
'driver': ThinPool.driver
|
||||||
}
|
}
|
||||||
|
|
||||||
def create(self, volume):
|
|
||||||
assert volume.vid
|
|
||||||
assert volume.size
|
|
||||||
if volume.save_on_stop:
|
|
||||||
if volume.source:
|
|
||||||
cmd = ['clone', str(volume.source), volume.vid]
|
|
||||||
else:
|
|
||||||
cmd = [
|
|
||||||
'create',
|
|
||||||
self._pool_id,
|
|
||||||
volume.vid.split('/', 1)[1],
|
|
||||||
str(volume.size)
|
|
||||||
]
|
|
||||||
qubes_lvm(cmd, self.log)
|
|
||||||
reset_cache()
|
|
||||||
return volume
|
|
||||||
|
|
||||||
def destroy(self):
|
def destroy(self):
|
||||||
pass # TODO Should we remove an existing pool?
|
pass # TODO Should we remove an existing pool?
|
||||||
|
|
||||||
def export(self, volume):
|
|
||||||
''' Returns an object that can be `open()`. '''
|
|
||||||
devpath = '/dev/' + volume.vid
|
|
||||||
return devpath
|
|
||||||
|
|
||||||
def import_data(self, volume):
|
|
||||||
''' Returns an object that can be `open()`. '''
|
|
||||||
devpath = '/dev/' + volume.vid
|
|
||||||
return devpath
|
|
||||||
|
|
||||||
def init_volume(self, vm, volume_config):
|
def init_volume(self, vm, volume_config):
|
||||||
''' Initialize a :py:class:`qubes.storage.Volume` from `volume_config`.
|
''' Initialize a :py:class:`qubes.storage.Volume` from `volume_config`.
|
||||||
'''
|
'''
|
||||||
@ -138,37 +85,12 @@ class ThinPool(qubes.storage.Pool):
|
|||||||
self.volume_group, vm_name, volume_config['name'])
|
self.volume_group, vm_name, volume_config['name'])
|
||||||
|
|
||||||
volume_config['volume_group'] = self.volume_group
|
volume_config['volume_group'] = self.volume_group
|
||||||
|
volume_config['pool'] = self
|
||||||
return ThinVolume(**volume_config)
|
return ThinVolume(**volume_config)
|
||||||
|
|
||||||
def import_volume(self, dst_pool, dst_volume, src_pool, src_volume):
|
|
||||||
if not src_volume.save_on_stop:
|
|
||||||
return dst_volume
|
|
||||||
|
|
||||||
src_path = src_pool.export(src_volume)
|
|
||||||
|
|
||||||
# HACK: neat trick to speed up testing if you have same physical thin
|
|
||||||
# pool assigned to two qubes-pools i.e: qubes_dom0 and test-lvm
|
|
||||||
# pylint: disable=line-too-long
|
|
||||||
if isinstance(src_pool, ThinPool) and src_pool.thin_pool == dst_pool.thin_pool: # NOQA
|
|
||||||
return self.clone(src_volume, dst_volume)
|
|
||||||
else:
|
|
||||||
dst_volume = self.create(dst_volume)
|
|
||||||
|
|
||||||
cmd = ['sudo', 'dd', 'if=' + src_path, 'of=/dev/' + dst_volume.vid,
|
|
||||||
'conv=sparse']
|
|
||||||
subprocess.check_call(cmd)
|
|
||||||
reset_cache()
|
|
||||||
return dst_volume
|
|
||||||
|
|
||||||
def is_dirty(self, volume):
|
|
||||||
if volume.save_on_stop:
|
|
||||||
return os.path.exists(volume.path + '-snap')
|
|
||||||
return False
|
|
||||||
|
|
||||||
def remove(self, volume):
|
def remove(self, volume):
|
||||||
assert volume.vid
|
assert volume.vid
|
||||||
if self.is_dirty(volume):
|
if volume.is_dirty():
|
||||||
cmd = ['remove', volume._vid_snap]
|
cmd = ['remove', volume._vid_snap]
|
||||||
qubes_lvm(cmd, self.log)
|
qubes_lvm(cmd, self.log)
|
||||||
|
|
||||||
@ -195,85 +117,9 @@ class ThinPool(qubes.storage.Pool):
|
|||||||
reset_cache()
|
reset_cache()
|
||||||
return volume
|
return volume
|
||||||
|
|
||||||
def revert(self, volume, revision=None):
|
|
||||||
old_path = volume.path + '-back'
|
|
||||||
if not os.path.exists(old_path):
|
|
||||||
msg = "Volume {!s} has no {!s}".format(volume, old_path)
|
|
||||||
raise qubes.storage.StoragePoolException(msg)
|
|
||||||
|
|
||||||
cmd = ['remove', volume.vid]
|
|
||||||
qubes_lvm(cmd, self.log)
|
|
||||||
cmd = ['clone', volume.vid + '-back', volume.vid]
|
|
||||||
qubes_lvm(cmd, self.log)
|
|
||||||
reset_cache()
|
|
||||||
return volume
|
|
||||||
|
|
||||||
def resize(self, volume, size):
|
|
||||||
''' Expands volume, throws
|
|
||||||
:py:class:`qubst.storage.qubes.storage.StoragePoolException` if
|
|
||||||
given size is less than current_size
|
|
||||||
'''
|
|
||||||
if not volume.rw:
|
|
||||||
msg = 'Can not resize reađonly volume {!s}'.format(volume)
|
|
||||||
raise qubes.storage.StoragePoolException(msg)
|
|
||||||
|
|
||||||
if size <= volume.size:
|
|
||||||
raise qubes.storage.StoragePoolException(
|
|
||||||
'For your own safety, shrinking of %s is'
|
|
||||||
' disabled. If you really know what you'
|
|
||||||
' are doing, use `lvresize` on %s manually.' %
|
|
||||||
(volume.name, volume.vid))
|
|
||||||
|
|
||||||
cmd = ['extend', volume.vid, str(size)]
|
|
||||||
qubes_lvm(cmd, self.log)
|
|
||||||
reset_cache()
|
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
pass # TODO Should we create a non existing pool?
|
pass # TODO Should we create a non existing pool?
|
||||||
|
|
||||||
def start(self, volume):
|
|
||||||
if volume.snap_on_start:
|
|
||||||
if not volume.save_on_stop or not self.is_dirty(volume):
|
|
||||||
self._snapshot(volume)
|
|
||||||
elif not volume.save_on_stop:
|
|
||||||
self._reset_volume(volume)
|
|
||||||
|
|
||||||
reset_cache()
|
|
||||||
return volume
|
|
||||||
|
|
||||||
def stop(self, volume):
|
|
||||||
if volume.save_on_stop and volume.snap_on_start:
|
|
||||||
self._commit(volume)
|
|
||||||
if volume.snap_on_start:
|
|
||||||
cmd = ['remove', volume._vid_snap]
|
|
||||||
qubes_lvm(cmd, self.log)
|
|
||||||
elif not volume.save_on_stop:
|
|
||||||
cmd = ['remove', volume.vid]
|
|
||||||
qubes_lvm(cmd, self.log)
|
|
||||||
reset_cache()
|
|
||||||
return volume
|
|
||||||
|
|
||||||
def _snapshot(self, volume):
|
|
||||||
try:
|
|
||||||
cmd = ['remove', volume._vid_snap]
|
|
||||||
qubes_lvm(cmd, self.log)
|
|
||||||
except: # pylint: disable=bare-except
|
|
||||||
pass
|
|
||||||
|
|
||||||
if volume.source is None:
|
|
||||||
cmd = ['clone', volume.vid, volume._vid_snap]
|
|
||||||
else:
|
|
||||||
cmd = ['clone', str(volume.source), volume._vid_snap]
|
|
||||||
qubes_lvm(cmd, self.log)
|
|
||||||
|
|
||||||
def verify(self, volume):
|
|
||||||
''' Verifies the volume. '''
|
|
||||||
try:
|
|
||||||
vol_info = size_cache[volume.vid]
|
|
||||||
return vol_info['attr'][4] == 'a'
|
|
||||||
except KeyError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def volumes(self):
|
def volumes(self):
|
||||||
''' Return a list of volumes managed by this pool '''
|
''' Return a list of volumes managed by this pool '''
|
||||||
@ -296,20 +142,6 @@ class ThinPool(qubes.storage.Pool):
|
|||||||
volumes += [ThinVolume(**config)]
|
volumes += [ThinVolume(**config)]
|
||||||
return volumes
|
return volumes
|
||||||
|
|
||||||
def _reset_volume(self, volume):
|
|
||||||
''' Resets a volatile volume '''
|
|
||||||
assert volume._is_volatile, \
|
|
||||||
'Expected a volatile volume, but got {!r}'.format(volume)
|
|
||||||
self.log.debug('Resetting volatile ' + volume.vid)
|
|
||||||
try:
|
|
||||||
cmd = ['remove', volume.vid]
|
|
||||||
qubes_lvm(cmd, self.log)
|
|
||||||
except qubes.storage.StoragePoolException:
|
|
||||||
pass
|
|
||||||
cmd = ['create', self._pool_id, volume.vid.split('/')[1],
|
|
||||||
str(volume.size)]
|
|
||||||
qubes_lvm(cmd, self.log)
|
|
||||||
|
|
||||||
|
|
||||||
def init_cache(log=logging.getLogger('qube.storage.lvm')):
|
def init_cache(log=logging.getLogger('qube.storage.lvm')):
|
||||||
cmd = ['lvs', '--noheadings', '-o',
|
cmd = ['lvs', '--noheadings', '-o',
|
||||||
@ -352,6 +184,7 @@ class ThinVolume(qubes.storage.Volume):
|
|||||||
def __init__(self, volume_group, size=0, **kwargs):
|
def __init__(self, volume_group, size=0, **kwargs):
|
||||||
self.volume_group = volume_group
|
self.volume_group = volume_group
|
||||||
super(ThinVolume, self).__init__(size=size, **kwargs)
|
super(ThinVolume, self).__init__(size=size, **kwargs)
|
||||||
|
self.log = logging.getLogger('qube.storage.lvm.%s' % str(self.pool))
|
||||||
|
|
||||||
if self.snap_on_start and self.source is None:
|
if self.snap_on_start and self.source is None:
|
||||||
msg = "snap_on_start specified on {!r} but no volume source set"
|
msg = "snap_on_start specified on {!r} but no volume source set"
|
||||||
@ -408,6 +241,178 @@ class ThinVolume(qubes.storage.Volume):
|
|||||||
raise qubes.storage.StoragePoolException(
|
raise qubes.storage.StoragePoolException(
|
||||||
"You shouldn't use lvm size setter")
|
"You shouldn't use lvm size setter")
|
||||||
|
|
||||||
|
def _reset(self):
|
||||||
|
''' Resets a volatile volume '''
|
||||||
|
assert self._is_volatile, \
|
||||||
|
'Expected a volatile volume, but got {!r}'.format(self)
|
||||||
|
self.log.debug('Resetting volatile ' + self.vid)
|
||||||
|
try:
|
||||||
|
cmd = ['remove', self.vid]
|
||||||
|
qubes_lvm(cmd, self.log)
|
||||||
|
except qubes.storage.StoragePoolException:
|
||||||
|
pass
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
cmd = ['create', self.pool._pool_id, self.vid.split('/')[1],
|
||||||
|
str(self.size)]
|
||||||
|
qubes_lvm(cmd, self.log)
|
||||||
|
|
||||||
|
def _commit(self):
|
||||||
|
msg = "Trying to commit {!s}, but it has save_on_stop == False"
|
||||||
|
msg = msg.format(self)
|
||||||
|
assert self.save_on_stop, msg
|
||||||
|
|
||||||
|
msg = "Trying to commit {!s}, but it has rw == False"
|
||||||
|
msg = msg.format(self)
|
||||||
|
assert self.rw, msg
|
||||||
|
assert hasattr(self, '_vid_snap')
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd = ['remove', self.vid + "-back"]
|
||||||
|
qubes_lvm(cmd, self.log)
|
||||||
|
except qubes.storage.StoragePoolException:
|
||||||
|
pass
|
||||||
|
cmd = ['clone', self.vid, self.vid + "-back"]
|
||||||
|
qubes_lvm(cmd, self.log)
|
||||||
|
|
||||||
|
cmd = ['remove', self.vid]
|
||||||
|
qubes_lvm(cmd, self.log)
|
||||||
|
cmd = ['clone', self._vid_snap, self.vid]
|
||||||
|
qubes_lvm(cmd, self.log)
|
||||||
|
|
||||||
|
|
||||||
|
def create(self):
|
||||||
|
assert self.vid
|
||||||
|
assert self.size
|
||||||
|
if self.save_on_stop:
|
||||||
|
if self.source:
|
||||||
|
cmd = ['clone', str(self.source), self.vid]
|
||||||
|
else:
|
||||||
|
cmd = [
|
||||||
|
'create',
|
||||||
|
self.pool._pool_id, # pylint: disable=protected-access
|
||||||
|
self.vid.split('/', 1)[1],
|
||||||
|
str(self.size)
|
||||||
|
]
|
||||||
|
qubes_lvm(cmd, self.log)
|
||||||
|
reset_cache()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
''' Returns an object that can be `open()`. '''
|
||||||
|
devpath = '/dev/' + self.vid
|
||||||
|
return devpath
|
||||||
|
|
||||||
|
def import_volume(self, src_volume):
|
||||||
|
if not src_volume.save_on_stop:
|
||||||
|
return self
|
||||||
|
|
||||||
|
src_path = src_volume.export()
|
||||||
|
|
||||||
|
# HACK: neat trick to speed up testing if you have same physical thin
|
||||||
|
# pool assigned to two qubes-pools i.e: qubes_dom0 and test-lvm
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
if isinstance(src_volume.pool, ThinPool) and \
|
||||||
|
src_volume.pool.thin_pool == self.pool.thin_pool: # NOQA
|
||||||
|
cmd = ['clone', str(src_volume), str(self)]
|
||||||
|
qubes_lvm(cmd, self.log)
|
||||||
|
else:
|
||||||
|
self.create()
|
||||||
|
|
||||||
|
cmd = ['sudo', 'dd', 'if=' + src_path, 'of=/dev/' + self.vid,
|
||||||
|
'conv=sparse']
|
||||||
|
subprocess.check_call(cmd)
|
||||||
|
reset_cache()
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def import_data(self):
|
||||||
|
''' Returns an object that can be `open()`. '''
|
||||||
|
devpath = '/dev/' + self.vid
|
||||||
|
return devpath
|
||||||
|
|
||||||
|
def is_dirty(self):
|
||||||
|
if self.save_on_stop:
|
||||||
|
return os.path.exists(self.path + '-snap')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def revert(self, revision=None):
|
||||||
|
old_path = self.path + '-back'
|
||||||
|
if not os.path.exists(old_path):
|
||||||
|
msg = "Volume {!s} has no {!s}".format(self, old_path)
|
||||||
|
raise qubes.storage.StoragePoolException(msg)
|
||||||
|
|
||||||
|
cmd = ['remove', self.vid]
|
||||||
|
qubes_lvm(cmd, self.log)
|
||||||
|
cmd = ['clone', self.vid + '-back', self.vid]
|
||||||
|
qubes_lvm(cmd, self.log)
|
||||||
|
reset_cache()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def resize(self, size):
|
||||||
|
''' Expands volume, throws
|
||||||
|
:py:class:`qubst.storage.qubes.storage.StoragePoolException` if
|
||||||
|
given size is less than current_size
|
||||||
|
'''
|
||||||
|
if not self.rw:
|
||||||
|
msg = 'Can not resize reađonly volume {!s}'.format(self)
|
||||||
|
raise qubes.storage.StoragePoolException(msg)
|
||||||
|
|
||||||
|
if size <= self.size:
|
||||||
|
raise qubes.storage.StoragePoolException(
|
||||||
|
'For your own safety, shrinking of %s is'
|
||||||
|
' disabled. If you really know what you'
|
||||||
|
' are doing, use `lvresize` on %s manually.' %
|
||||||
|
(self.name, self.vid))
|
||||||
|
|
||||||
|
cmd = ['extend', self.vid, str(size)]
|
||||||
|
qubes_lvm(cmd, self.log)
|
||||||
|
reset_cache()
|
||||||
|
|
||||||
|
def _snapshot(self):
|
||||||
|
try:
|
||||||
|
cmd = ['remove', self._vid_snap]
|
||||||
|
qubes_lvm(cmd, self.log)
|
||||||
|
except: # pylint: disable=bare-except
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.source is None:
|
||||||
|
cmd = ['clone', self.vid, self._vid_snap]
|
||||||
|
else:
|
||||||
|
cmd = ['clone', str(self.source), self._vid_snap]
|
||||||
|
qubes_lvm(cmd, self.log)
|
||||||
|
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if self.snap_on_start:
|
||||||
|
if not self.save_on_stop or not self.is_dirty():
|
||||||
|
self._snapshot()
|
||||||
|
elif not self.save_on_stop:
|
||||||
|
self._reset()
|
||||||
|
|
||||||
|
reset_cache()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self.save_on_stop and self.snap_on_start:
|
||||||
|
self._commit()
|
||||||
|
if self.snap_on_start:
|
||||||
|
cmd = ['remove', self._vid_snap]
|
||||||
|
qubes_lvm(cmd, self.log)
|
||||||
|
elif not self.save_on_stop:
|
||||||
|
cmd = ['remove', self.vid]
|
||||||
|
qubes_lvm(cmd, self.log)
|
||||||
|
reset_cache()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def verify(self):
|
||||||
|
''' Verifies the volume. '''
|
||||||
|
try:
|
||||||
|
vol_info = size_cache[self.vid]
|
||||||
|
return vol_info['attr'][4] == 'a'
|
||||||
|
except KeyError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def block_device(self):
|
def block_device(self):
|
||||||
''' Return :py:class:`qubes.storage.BlockDevice` for serialization in
|
''' Return :py:class:`qubes.storage.BlockDevice` for serialization in
|
||||||
the libvirt XML template as <disk>.
|
the libvirt XML template as <disk>.
|
||||||
@ -437,7 +442,7 @@ def pool_exists(pool_id):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def qubes_lvm(cmd, log=logging.getLogger('qube.storage.lvm')):
|
def qubes_lvm(cmd, log=logging.getLogger('qubes.storage.lvm')):
|
||||||
''' Call :program:`lvm` to execute an LVM operation '''
|
''' Call :program:`lvm` to execute an LVM operation '''
|
||||||
action = cmd[0]
|
action = cmd[0]
|
||||||
if action == 'remove':
|
if action == 'remove':
|
||||||
|
@ -138,22 +138,31 @@ class TC_01_FileVolumes(qubes.tests.QubesTestCase):
|
|||||||
self.assertTrue(volume.rw)
|
self.assertTrue(volume.rw)
|
||||||
|
|
||||||
def test_001_snapshot_volume(self):
|
def test_001_snapshot_volume(self):
|
||||||
source = 'vm-templates/fedora-23/root'
|
template_vm = self.app.default_template
|
||||||
|
vm = qubes.tests.storage.TestVM(self, template=template_vm)
|
||||||
|
|
||||||
original_size = qubes.config.defaults['root_img_size']
|
original_size = qubes.config.defaults['root_img_size']
|
||||||
|
source_config = {
|
||||||
|
'name': 'root',
|
||||||
|
'pool': self.POOL_NAME,
|
||||||
|
'save_on_stop': True,
|
||||||
|
'rw': False,
|
||||||
|
'size': original_size,
|
||||||
|
}
|
||||||
|
source = self.app.get_pool(self.POOL_NAME).init_volume(template_vm,
|
||||||
|
source_config)
|
||||||
config = {
|
config = {
|
||||||
'name': 'root',
|
'name': 'root',
|
||||||
'pool': 'default',
|
'pool': self.POOL_NAME,
|
||||||
'snap_on_start': True,
|
'snap_on_start': True,
|
||||||
'rw': False,
|
'rw': False,
|
||||||
'source': source,
|
'source': source,
|
||||||
'size': original_size,
|
'size': original_size,
|
||||||
}
|
}
|
||||||
|
|
||||||
template_vm = self.app.default_template
|
|
||||||
vm = qubes.tests.storage.TestVM(self, template=template_vm)
|
|
||||||
volume = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
|
volume = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
|
||||||
self.assertEqual(volume.name, 'root')
|
self.assertEqual(volume.name, 'root')
|
||||||
self.assertEqual(volume.pool, 'default')
|
self.assertEqual(volume.pool, self.POOL_NAME)
|
||||||
self.assertEqual(volume.size, original_size)
|
self.assertEqual(volume.size, original_size)
|
||||||
self.assertTrue(volume.snap_on_start)
|
self.assertTrue(volume.snap_on_start)
|
||||||
self.assertTrue(volume.snap_on_start)
|
self.assertTrue(volume.snap_on_start)
|
||||||
@ -181,12 +190,17 @@ class TC_01_FileVolumes(qubes.tests.QubesTestCase):
|
|||||||
def test_003_read_only_volume(self):
|
def test_003_read_only_volume(self):
|
||||||
template = self.app.default_template
|
template = self.app.default_template
|
||||||
vid = template.volumes['root'].vid
|
vid = template.volumes['root'].vid
|
||||||
config = {'name': 'root', 'pool': 'default', 'rw': False, 'vid': vid}
|
config = {
|
||||||
|
'name': 'root',
|
||||||
|
'pool': self.POOL_NAME,
|
||||||
|
'rw': False,
|
||||||
|
'vid': vid,
|
||||||
|
}
|
||||||
vm = qubes.tests.storage.TestVM(self, template=template)
|
vm = qubes.tests.storage.TestVM(self, template=template)
|
||||||
|
|
||||||
volume = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
|
volume = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
|
||||||
self.assertEqual(volume.name, 'root')
|
self.assertEqual(volume.name, 'root')
|
||||||
self.assertEqual(volume.pool, 'default')
|
self.assertEqual(volume.pool, self.POOL_NAME)
|
||||||
|
|
||||||
# original_size = qubes.config.defaults['root_img_size']
|
# original_size = qubes.config.defaults['root_img_size']
|
||||||
# FIXME: self.assertEqual(volume.size, original_size)
|
# FIXME: self.assertEqual(volume.size, original_size)
|
||||||
@ -322,10 +336,9 @@ class TC_03_FilePool(qubes.tests.QubesTestCase):
|
|||||||
self.assertEqual(vm.volumes['private'].path, expected_private_path)
|
self.assertEqual(vm.volumes['private'].path, expected_private_path)
|
||||||
|
|
||||||
expected_volatile_path = os.path.join(expected_vmdir, 'volatile.img')
|
expected_volatile_path = os.path.join(expected_vmdir, 'volatile.img')
|
||||||
vm.storage.get_pool(vm.volumes['volatile'])\
|
vm.volumes['volatile'].reset()
|
||||||
.reset(vm.volumes['volatile'])
|
|
||||||
self.assertEqualAndExists(vm.volumes['volatile'].path,
|
self.assertEqualAndExists(vm.volumes['volatile'].path,
|
||||||
expected_volatile_path)
|
expected_volatile_path)
|
||||||
|
|
||||||
def test_013_template_file_images(self):
|
def test_013_template_file_images(self):
|
||||||
""" Check if root.img, private.img, volatile.img and root-cow.img are
|
""" Check if root.img, private.img, volatile.img and root-cow.img are
|
||||||
|
@ -134,7 +134,7 @@ class TC_00_ThinPool(ThinPoolBase):
|
|||||||
self.assertEqual(volume.name, 'root')
|
self.assertEqual(volume.name, 'root')
|
||||||
self.assertEqual(volume.pool, self.pool.name)
|
self.assertEqual(volume.pool, self.pool.name)
|
||||||
self.assertEqual(volume.size, qubes.config.defaults['root_img_size'])
|
self.assertEqual(volume.size, qubes.config.defaults['root_img_size'])
|
||||||
self.pool.create(volume)
|
volume.create()
|
||||||
path = "/dev/%s" % volume.vid
|
path = "/dev/%s" % volume.vid
|
||||||
self.assertTrue(os.path.exists(path))
|
self.assertTrue(os.path.exists(path))
|
||||||
self.pool.remove(volume)
|
self.pool.remove(volume)
|
||||||
@ -154,7 +154,7 @@ class TC_00_ThinPool(ThinPoolBase):
|
|||||||
self.assertEqual(volume.name, 'root')
|
self.assertEqual(volume.name, 'root')
|
||||||
self.assertEqual(volume.pool, self.pool.name)
|
self.assertEqual(volume.pool, self.pool.name)
|
||||||
self.assertEqual(volume.size, qubes.config.defaults['root_img_size'])
|
self.assertEqual(volume.size, qubes.config.defaults['root_img_size'])
|
||||||
self.pool.create(volume)
|
volume.create()
|
||||||
path = "/dev/%s" % volume.vid
|
path = "/dev/%s" % volume.vid
|
||||||
self.assertTrue(os.path.exists(path))
|
self.assertTrue(os.path.exists(path))
|
||||||
self.pool.remove(volume)
|
self.pool.remove(volume)
|
||||||
|
@ -79,7 +79,7 @@ class TC_90_AppVM(qubes.tests.vm.qubesvm.QubesVMTestsMixin,
|
|||||||
self.assertFalse(vm.volume_config['root']['save_on_stop'])
|
self.assertFalse(vm.volume_config['root']['save_on_stop'])
|
||||||
self.assertTrue(vm.volume_config['root']['snap_on_start'])
|
self.assertTrue(vm.volume_config['root']['snap_on_start'])
|
||||||
self.assertEqual(vm.volume_config['root'].get('source', None),
|
self.assertEqual(vm.volume_config['root'].get('source', None),
|
||||||
self.template.volumes['root'].source)
|
self.template.volumes['root'])
|
||||||
|
|
||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
vm.volume_config['volatile'].get('save_on_stop', False))
|
vm.volume_config['volatile'].get('save_on_stop', False))
|
||||||
|
@ -98,9 +98,6 @@ class AppVM(qubes.vm.qubesvm.QubesVM):
|
|||||||
del self.volume_config[name]['vid']
|
del self.volume_config[name]['vid']
|
||||||
|
|
||||||
super(AppVM, self).__init__(app, xml, **kwargs)
|
super(AppVM, self).__init__(app, xml, **kwargs)
|
||||||
if 'source' not in self.volume_config['root']:
|
|
||||||
msg = 'missing source for root volume'
|
|
||||||
raise qubes.exc.QubesException(msg)
|
|
||||||
|
|
||||||
@qubes.events.handler('domain-load')
|
@qubes.events.handler('domain-load')
|
||||||
def on_domain_loaded(self, event):
|
def on_domain_loaded(self, event):
|
||||||
|
@ -734,8 +734,10 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
|
|||||||
if not hasattr(self, 'uuid'):
|
if not hasattr(self, 'uuid'):
|
||||||
self.uuid = uuid.uuid4()
|
self.uuid = uuid.uuid4()
|
||||||
|
|
||||||
# Initialize VM image storage class
|
# Initialize VM image storage class;
|
||||||
self.storage = qubes.storage.Storage(self)
|
# it might be already initialized by a recursive call from a child VM
|
||||||
|
if self.storage is None:
|
||||||
|
self.storage = qubes.storage.Storage(self)
|
||||||
|
|
||||||
@qubes.events.handler('property-set:label')
|
@qubes.events.handler('property-set:label')
|
||||||
def on_property_set_label(self, event, name, newvalue, oldvalue=None):
|
def on_property_set_label(self, event, name, newvalue, oldvalue=None):
|
||||||
@ -1761,15 +1763,15 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
|
|||||||
volume_config['size'] = source.size
|
volume_config['size'] = source.size
|
||||||
volume_config['pool'] = source.pool
|
volume_config['pool'] = source.pool
|
||||||
|
|
||||||
has_source = (
|
needs_source = (
|
||||||
'source' in volume_config and volume_config['source'] is not None)
|
'source' in volume_config)
|
||||||
is_snapshot = 'snap_on_start' in volume_config and volume_config[
|
is_snapshot = 'snap_on_start' in volume_config and volume_config[
|
||||||
'snap_on_start']
|
'snap_on_start']
|
||||||
if is_snapshot and not has_source:
|
if is_snapshot and needs_source:
|
||||||
if source.source is not None:
|
if source.source is not None:
|
||||||
volume_config['source'] = source.source
|
volume_config['source'] = source.source
|
||||||
else:
|
else:
|
||||||
volume_config['source'] = source.vid
|
volume_config['source'] = source
|
||||||
return volume_config
|
return volume_config
|
||||||
|
|
||||||
def relative_path(self, path):
|
def relative_path(self, path):
|
||||||
|
Loading…
Reference in New Issue
Block a user