qubes.storage.file use new storage API

This commit is contained in:
Bahtiar `kalkin-` Gadimov 2016-07-12 17:53:31 +02:00
parent 1f735669bc
commit d1c606b952
No known key found for this signature in database
GPG Key ID: 96ED3C3BA19C3DEE
2 changed files with 355 additions and 430 deletions

View File

@ -22,10 +22,8 @@
# with this program; if not, write to the Free Software Foundation, Inc., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
# #
''' This module contains pool implementations backed by file images''' ''' This module contains pool implementations backed by file images'''
from __future__ import absolute_import from __future__ import absolute_import
import os import os
@ -33,87 +31,101 @@ import os.path
import re import re
import subprocess import subprocess
from qubes.storage import Pool, StoragePoolException, Volume import qubes.storage
BLKSIZE = 512 BLKSIZE = 512
class FilePool(Pool): class FilePool(qubes.storage.Pool):
''' File based 'original' disk implementation ''' ''' File based 'original' disk implementation
''' # pylint: disable=protected-access
driver = 'file' driver = 'file'
def __init__(self, name=None, dir_path=None): def __init__(self, revisions_to_keep=1, dir_path=None, **kwargs):
super(FilePool, self).__init__(name=name) super(FilePool, self).__init__(revisions_to_keep=revisions_to_keep,
**kwargs)
assert dir_path, "No pool dir_path specified" assert dir_path, "No pool dir_path specified"
self.dir_path = os.path.normpath(dir_path) self.dir_path = os.path.normpath(dir_path)
self._volumes = [] self._volumes = []
def clone(self, source, target):
''' Clones the volume if the `source.pool` if the source is a
:py:class:`FileVolume`.
'''
if issubclass(FileVolume, source.__class__):
raise StoragePoolException('Volumes %s and %s use different pools'
% (source.__class__, target.__class__))
if source.volume_type not in ['origin', 'read-write']:
return target
copy_file(source.vid, target.vid)
return target
def create(self, volume, source_volume=None):
_type = volume.volume_type
size = volume.size
if _type == 'origin':
create_sparse_file(volume.path_origin, size)
create_sparse_file(volume.path_cow, size)
elif _type in ['read-write'] and source_volume:
copy_file(source_volume.path, volume.path)
elif _type in ['read-write', 'volatile']:
create_sparse_file(volume.path, size)
return volume
@property @property
def config(self): def config(self):
return { return {
'name': self.name, 'name': self.name,
'dir_path': self.dir_path, 'dir_path': self.dir_path,
'driver': FilePool.driver, 'driver': FilePool.driver,
'revisions_to_keep': self.revisions_to_keep
} }
def is_outdated(self, volume): def clone(self, source, target):
# FIX: Implement or remove this at all? new_dir = os.path.dirname(target.path)
raise NotImplementedError 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, long)) 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):
volume_config['dir_path'] = self.dir_path
if os.path.join(self.dir_path, self._vid_prefix(vm)) == vm.dir_path:
volume_config['backward_comp'] = True
if 'vid' not in volume_config:
volume_config['vid'] = os.path.join(
self._vid_prefix(vm), volume_config['name'])
try:
if volume_config['reset_on_start']:
volume_config['revisions_to_keep'] = 0
except KeyError:
pass
finally:
if 'revisions_to_keep' not in volume_config:
volume_config['revisions_to_keep'] = self.revisions_to_keep
volume = FileVolume(**volume_config)
self._volumes += [volume]
return volume
def is_dirty(self, volume):
return False # TODO: How to implement this?
def resize(self, volume, size): def resize(self, volume, size):
''' Expands volume, throws ''' Expands volume, throws
:py:class:`qubst.storage.StoragePoolException` if given size is :py:class:`qubst.storage.qubes.storage.StoragePoolException` if
less than current_size given size is less than current_size
''' # pylint: disable=no-self-use ''' # pylint: disable=no-self-use
_type = volume.volume_type if not volume.rw:
if _type not in ['origin', 'read-write', 'volatile']: msg = 'Can not resize reađonly volume {!s}'.format(volume)
raise StoragePoolException('Can not resize a %s volume %s' % raise qubes.storage.StoragePoolException(msg)
(_type, volume.vid))
if size <= volume.size: if size <= volume.size:
raise StoragePoolException( raise qubes.storage.StoragePoolException(
'For your own safety, shrinking of %s is' 'For your own safety, shrinking of %s is'
' disabled. If you really know what you' ' disabled. If you really know what you'
' are doing, use `truncate` on %s manually.' % ' are doing, use `truncate` on %s manually.' %
(volume.name, volume.vid)) (volume.name, volume.vid))
if _type == 'origin': with open(volume.path, 'a+b') as fd:
path = volume.path_origin
elif _type in ['read-write', 'volatile']:
path = volume.path
with open(path, 'a+b') as fd:
fd.truncate(size) fd.truncate(size)
p = subprocess.Popen( p = subprocess.Popen(['sudo', 'losetup', '--associated', volume.path],
['sudo', 'losetup', '--associated', path],
stdout=subprocess.PIPE) stdout=subprocess.PIPE)
result = p.communicate() result = p.communicate()
@ -122,34 +134,57 @@ class FilePool(Pool):
loop_dev = m.group(1) loop_dev = m.group(1)
# resize loop device # resize loop device
subprocess.check_call(['sudo', 'losetup', '--set-capacity', loop_dev subprocess.check_call(['sudo', 'losetup', '--set-capacity',
]) loop_dev])
def remove(self, volume): def remove(self, volume):
if volume.volume_type in ['read-write', 'volatile']: if not volume.internal:
_remove_if_exists(volume.path) return # do not remove random attached file volumes
elif volume.volume_type == 'origin': elif volume._is_snapshot:
return # no need to remove, because it's just a snapshot
else:
_remove_if_exists(volume.path) _remove_if_exists(volume.path)
if volume._is_origin:
_remove_if_exists(volume.path_cow) _remove_if_exists(volume.path_cow)
def rename(self, volume, old_name, new_name): def rename(self, volume, old_name, new_name):
assert issubclass(volume.__class__, FileVolume) assert issubclass(volume.__class__, FileVolume)
old_dir = os.path.dirname(volume.path) subdir, _, volume_path = volume.vid.split('/', 2)
new_dir = os.path.join(os.path.dirname(old_dir), new_name)
if not os.path.exists(new_dir): if volume._is_origin:
os.makedirs(new_dir) # TODO: Renaming the old revisions
new_path = os.path.join(self.dir_path, subdir, new_name)
if not os.path.exists(new_path):
os.mkdir(new_path, 0755)
new_volume_path = os.path.join(new_path, self.name + '.img')
if not volume.backward_comp:
os.rename(volume.path, new_volume_path)
new_volume_path_cow = os.path.join(new_path, self.name + '-cow.img')
if os.path.exists(new_volume_path_cow) and not volume.backward_comp:
os.rename(volume.path_cow, new_volume_path_cow)
volume.rename_target_dir(old_name, new_name) volume.vid = os.path.join(subdir, new_name, volume_path)
return volume return volume
def commit_template_changes(self, volume): def import_volume(self, dst_pool, dst_volume, src_pool, src_volume):
if volume.volume_type != 'origin': 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 return volume
if os.path.exists(volume.path_cow): if os.path.exists(volume.path_cow):
os.rename(volume.path_cow, volume.path_cow + '.old') old_path = volume.path_cow + '.old'
os.rename(volume.path_cow, old_path)
old_umask = os.umask(002) old_umask = os.umask(002)
with open(volume.path_cow, 'w') as f_cow: with open(volume.path_cow, 'w') as f_cow:
@ -160,6 +195,37 @@ class FilePool(Pool):
def destroy(self): def destroy(self):
pass pass
def export(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, long)) 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')
@ -168,18 +234,39 @@ class FilePool(Pool):
create_dir_if_not_exists(vm_templates_path) create_dir_if_not_exists(vm_templates_path)
def start(self, volume): def start(self, volume):
if volume.volume_type == 'volatile': if volume._is_snapshot or volume._is_origin:
_reset_volume(volume)
if volume.volume_type in ['origin', 'snapshot']:
_check_path(volume.path_origin)
_check_path(volume.path_cow)
else:
_check_path(volume.path) _check_path(volume.path)
try:
_check_path(volume.path_cow)
except qubes.storage.StoragePoolException:
create_sparse_file(volume.path_cow, volume.size)
_check_path(volume.path_cow)
elif volume._is_volatile:
self.reset(volume)
return volume return volume
def stop(self, volume): def stop(self, volume):
pass if volume.save_on_stop:
self.commit(volume)
elif volume._is_volatile:
_remove_if_exists(volume.path)
return volume
@staticmethod
def _vid_prefix(vm):
''' Helper to create a prefix for the vid for volume
''' # FIX Remove this if we drop the file backend
import qubes.vm.templatevm # pylint: disable=redefined-outer-name
import qubes.vm.dispvm # pylint: disable=redefined-outer-name
if isinstance(vm, qubes.vm.templatevm.TemplateVM):
subdir = 'vm-templates'
elif isinstance(vm, qubes.vm.dispvm.DispVM):
subdir = 'appvms'
return os.path.join(subdir, vm.template.name + '-dvm')
else:
subdir = 'appvms'
return os.path.join(subdir, vm.name)
def target_dir(self, vm): def target_dir(self, vm):
""" Returns the path to vmdir depending on the type of the VM. """ Returns the path to vmdir depending on the type of the VM.
@ -198,61 +285,8 @@ class FilePool(Pool):
string (str) absolute path to the directory where the vm files string (str) absolute path to the directory where the vm files
are stored are stored
""" """
# FIX Remove this if we drop the file backend
import qubes.vm.templatevm # nopep8
import qubes.vm.dispvm # nopep8
if isinstance(vm, qubes.vm.templatevm.TemplateVM):
subdir = 'vm-templates'
elif isinstance(vm, qubes.vm.dispvm.DispVM):
subdir = 'appvms'
return os.path.join(self.dir_path, subdir,
vm.template.name + '-dvm')
else:
subdir = 'appvms'
return os.path.join(self.dir_path, subdir, vm.name) return os.path.join(self.dir_path, self._vid_prefix(vm))
def init_volume(self, vm, volume_config):
assert 'volume_type' in volume_config, "Volume type missing " \
+ str(volume_config)
volume_type = volume_config['volume_type']
known_types = {
'read-write': ReadWriteFile,
'read-only': ReadOnlyFile,
'origin': OriginFile,
'snapshot': SnapshotFile,
'volatile': VolatileFile,
}
if volume_type not in known_types:
raise StoragePoolException("Unknown volume type " + volume_type)
if volume_type in ['snapshot', 'read-only']:
name = volume_config['name']
origin_vm = vm.template
while origin_vm.volume_config[name]['volume_type'] == volume_type:
origin_vm = origin_vm.template
expected_origin_type = {
'snapshot': 'origin',
'read-only': 'read-write', # FIXME: really?
}[volume_type]
assert origin_vm.volume_config[name]['volume_type'] == \
expected_origin_type
origin_pool = vm.app.get_pool(origin_vm.volume_config[name]['pool'])
assert isinstance(origin_pool,
FilePool), 'Origin volume not a file volume'
volume_config['target_dir'] = origin_pool.target_dir(origin_vm)
volume_config['size'] = origin_vm.volume_config[name]['size']
else:
volume_config['target_dir'] = self.target_dir(vm)
volume = known_types[volume_type](**volume_config)
self._volumes += [volume]
return volume
def verify(self, volume): def verify(self, volume):
return volume.verify() return volume.verify()
@ -262,33 +296,77 @@ class FilePool(Pool):
return self._volumes return self._volumes
class FileVolume(Volume): class FileVolume(qubes.storage.Volume):
''' Parent class for the xen volumes implementation which expects a ''' Parent class for the xen volumes implementation which expects a
`target_dir` param on initialization. `target_dir` param on initialization. '''
'''
def __init__(self, target_dir, **kwargs): def __init__(self, dir_path, backward_comp=False, **kwargs):
self.target_dir = target_dir self.dir_path = dir_path
assert self.target_dir, "target_dir not specified" self.backward_comp = backward_comp
assert self.dir_path, "dir_path not specified"
super(FileVolume, self).__init__(**kwargs) super(FileVolume, self).__init__(**kwargs)
def _new_dir(self, new_name): if self.snap_on_start and self.source is None:
''' Returns a new directory path based on the new_name. This is a helper msg = "snap_on_start specified on {!r} but no volume source set"
method for moving file images during vm renaming. msg = msg.format(self.name)
raise qubes.storage.StoragePoolException(msg)
elif not self.snap_on_start and self.source is not None:
msg = "source specified on {!r} but no snap_on_start set"
msg = msg.format(self.name)
raise qubes.storage.StoragePoolException(msg)
if self._is_snapshot:
self.path = os.path.join(self.dir_path, self.source + '.img')
img_name = self.source + '-cow.img'
self.path_cow = os.path.join(self.dir_path, img_name)
elif self._is_volume or self._is_volatile:
self.path = os.path.join(self.dir_path, self.vid + '.img')
elif self._is_origin:
self.path = os.path.join(self.dir_path, self.vid + '.img')
img_name = self.vid + '-cow.img'
self.path_cow = os.path.join(self.dir_path, img_name)
else:
assert False, 'This should not happen'
def verify(self):
''' Verifies the volume. '''
if not os.path.exists(self.path) and not self._is_volatile:
msg = 'Missing image file: {!s}.'.format(self.path)
raise qubes.storage.StoragePoolException(msg)
return True
@property
def script(self):
if self._is_volume or self._is_volatile:
return None
elif self._is_origin:
return 'block-origin'
elif self._is_origin_snapshot or self._is_snapshot:
return 'block-snapshot'
def block_device(self):
''' Return :py:class:`qubes.devices.BlockDevice` for serialization in
the libvirt XML template as <disk>.
''' '''
old_dir = os.path.dirname(self.path) path = self.path
return os.path.join(os.path.dirname(old_dir), new_name) if self._is_origin or self._is_snapshot:
path += ":" + self.path_cow
return qubes.devices.BlockDevice(path, self.name, self.script, self.rw,
self.domain, self.devtype)
@property
def revisions(self):
if not hasattr(self, 'path_cow'):
return {}
class SizeMixIn(FileVolume): old_revision = self.path_cow + '.old' # pylint: disable=no-member
''' A mix in which expects a `size` param to be > 0 on initialization and
provides a usage property wrapper.
'''
def __init__(self, size=0, **kwargs): if not os.path.exists(old_revision):
assert size, 'Empty size provided' return {}
assert size > 0, 'Size for volume ' + kwargs['name'] + ' is <=0' else:
super(SizeMixIn, self).__init__(size=int(size), **kwargs) seconds = os.path.getctime(old_revision)
iso_date = qubes.storage.isodate(seconds).split('.', 1)[0]
return {iso_date: old_revision}
@property @property
def usage(self): def usage(self):
@ -296,169 +374,31 @@ class SizeMixIn(FileVolume):
return get_disk_usage(self.vid) return get_disk_usage(self.vid)
@property @property
def config(self): def _is_volatile(self):
''' return config data for serialization to qubes.xml ''' ''' Internal helper. Useful for differentiating volume handling '''
return {'name': self.name, return not self.snap_on_start and not self.save_on_stop
'pool': self.pool,
'size': str(self.size),
'volume_type': self.volume_type}
class ReadWriteFile(SizeMixIn):
''' Represents a readable & writable file image based volume '''
def __init__(self, **kwargs):
super(ReadWriteFile, self).__init__(**kwargs)
self.path = os.path.join(self.target_dir, self.name + '.img')
self.vid = self.path
def rename_target_dir(self, new_name, new_dir):
''' Called by :py:class:`FilePool` when a domain changes it's name '''
# pylint: disable=unused-argument
old_path = self.path
file_name = os.path.basename(self.path)
new_path = os.path.join(new_dir, file_name)
os.rename(old_path, new_path)
self.target_dir = new_dir
self.path = new_path
self.vid = self.path
def verify(self):
''' Verifies the volume. '''
if not os.path.exists(self.path):
raise StoragePoolException('Missing image file: %s' % self.path)
class ReadOnlyFile(FileVolume):
''' Represents a readonly file image based volume '''
usage = 0
def __init__(self, size=0, **kwargs):
super(ReadOnlyFile, self).__init__(size=int(size), **kwargs)
self.path = self.vid
def rename_target_dir(self, old_name, new_name):
""" Called by :py:class:`FilePool` when a domain changes it's name.
Only copies the volume if it belongs to the domain being renamed.
Currently if a volume is in a directory named the same as the domain,
it's ”owned” by the domain.
"""
new_dir = self._new_dir(new_name)
if os.path.basename(self.target_dir) == old_name:
file_name = os.path.basename(self.path)
new_path = os.path.join(new_dir, file_name)
old_path = self.path
os.rename(old_path, new_path)
self.target_dir = new_dir
self.path = new_path
self.vid = self.path
def verify(self):
''' Verifies the volume. '''
if not os.path.exists(self.path):
raise StoragePoolException('Missing image file: %s' % self.path)
class OriginFile(SizeMixIn):
''' Represents a readable, writeable & snapshotable file image based volume.
This is used for TemplateVM's
'''
script = 'block-origin'
def __init__(self, **kwargs):
super(OriginFile, self).__init__(**kwargs)
self.path_origin = os.path.join(self.target_dir, self.name + '.img')
self.path_cow = os.path.join(self.target_dir, self.name + '-cow.img')
self.path = '%s:%s' % (self.path_origin, self.path_cow)
self.vid = self.path_origin
def commit(self):
''' Commit Template changes '''
raise NotImplementedError
def rename_target_dir(self, old_name, new_name):
''' Called by :py:class:`FilePool` when a domain changes it's name.
''' # pylint: disable=unused-argument
new_dir = self._new_dir(new_name)
old_path_origin = self.path_origin
old_path_cow = self.path_cow
new_path_origin = os.path.join(new_dir, self.name + '.img')
new_path_cow = os.path.join(new_dir, self.name + '-cow.img')
os.rename(old_path_origin, new_path_origin)
os.rename(old_path_cow, new_path_cow)
self.target_dir = new_dir
self.path_origin = new_path_origin
self.path_cow = new_path_cow
self.path = '%s:%s' % (self.path_origin, self.path_cow)
self.vid = self.path_origin
@property @property
def usage(self): def _is_origin(self):
result = 0 ''' Internal helper. Useful for differentiating volume handling '''
if os.path.exists(self.path_origin): # pylint: disable=line-too-long
result += get_disk_usage(self.path_origin) return not self.snap_on_start and self.save_on_stop and self.revisions_to_keep > 0 # NOQA
if os.path.exists(self.path_cow):
result += get_disk_usage(self.path_cow)
return result
def verify(self): @property
''' Verifies the volume. ''' def _is_snapshot(self):
if not os.path.exists(self.path_origin): ''' Internal helper. Useful for differentiating volume handling '''
raise StoragePoolException('Missing image file: %s' % return self.snap_on_start and not self.save_on_stop
self.path_origin)
@property
def _is_origin_snapshot(self):
''' Internal helper. Useful for differentiating volume handling '''
return self.snap_on_start and self.save_on_stop
class SnapshotFile(FileVolume): @property
''' Represents a readonly snapshot of an :py:class:`OriginFile` volume ''' def _is_volume(self):
script = 'block-snapshot' ''' Internal helper. Usefull for differentiating volume handling '''
rw = False # pylint: disable=line-too-long
usage = 0 return not self.snap_on_start and self.save_on_stop and self.revisions_to_keep == 0 # NOQA
def __init__(self, name=None, size=None, **kwargs):
assert size
super(SnapshotFile, self).__init__(name=name, size=int(size), **kwargs)
self.path_origin = os.path.join(self.target_dir, name + '.img')
self.path_cow = os.path.join(self.target_dir, name + '-cow.img')
self.path = '%s:%s' % (self.path_origin, self.path_cow)
self.vid = self.path_origin
def verify(self):
''' Verifies the volume. '''
if not os.path.exists(self.path_origin):
raise StoragePoolException('Missing image file: %s' %
self.path_origin)
class VolatileFile(SizeMixIn):
''' Represents a readable & writeable file based volume, which will be
discarded and recreated at each startup.
'''
def __init__(self, **kwargs):
super(VolatileFile, self).__init__(**kwargs)
self.path = os.path.join(self.target_dir, self.name + '.img')
self.vid = self.path
def rename_target_dir(self, old_name, new_name):
''' Called by :py:class:`FilePool` when a domain changes it's name.
''' # pylint: disable=unused-argument
new_dir = self._new_dir(new_name)
_remove_if_exists(self.path)
file_name = os.path.basename(self.path)
self.target_dir = new_dir
new_path = os.path.join(new_dir, file_name)
self.path = new_path
self.vid = self.path
def verify(self):
''' Verifies the volume. '''
pass
def create_sparse_file(path, size): def create_sparse_file(path, size):
''' Create an empty sparse file ''' ''' Create an empty sparse file '''
@ -534,7 +474,9 @@ def copy_file(source, destination):
os.makedirs(parent_dir) os.makedirs(parent_dir)
try: try:
subprocess.check_call(['cp', '--reflink=auto', source, destination]) cmd = ['sudo', 'cp', '--sparse=auto',
'--reflink=auto', source, destination]
subprocess.check_call(cmd)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
raise IOError('Error while copying {!r} to {!r}'.format(source, raise IOError('Error while copying {!r} to {!r}'.format(source,
destination)) destination))
@ -549,17 +491,5 @@ def _remove_if_exists(path):
def _check_path(path): def _check_path(path):
''' Raise an StoragePoolException if ``path`` does not exist''' ''' Raise an StoragePoolException if ``path`` does not exist'''
if not os.path.exists(path): if not os.path.exists(path):
raise StoragePoolException('Missing image file: %s' % path) msg = 'Missing image file: %s' % path
raise qubes.storage.StoragePoolException(msg)
def _reset_volume(volume):
''' Remove and recreate a volatile volume '''
assert volume.volume_type == 'volatile', "Not a volatile volume"
assert volume.size
_remove_if_exists(volume.path)
with open(volume.path, "w") as f_volatile:
f_volatile.truncate(volume.size)
return volume

View File

@ -16,29 +16,27 @@
# with this program; if not, write to the Free Software Foundation, Inc., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
''' Tests for the file storage backend '''
import os import os
import shutil import shutil
import qubes.storage import qubes.storage
import qubes.tests.storage import qubes.tests.storage
import unittest
from qubes.config import defaults from qubes.config import defaults
from qubes.storage import Storage
from qubes.storage.file import (OriginFile, ReadOnlyFile, ReadWriteFile,
SnapshotFile, VolatileFile)
from qubes.tests import QubesTestCase, SystemTestsMixin
from qubes.tests.storage import TestVM
# :pylint: disable=invalid-name # :pylint: disable=invalid-name
class TestApp(qubes.Qubes): class TestApp(qubes.Qubes):
def __init__(self, *args, **kwargs): ''' A Mock App object '''
super(TestApp, self).__init__('/tmp/qubes-test.xml', def __init__(self, *args, **kwargs): # pylint: disable=unused-argument
load=False, offline_mode=True, **kwargs) super(TestApp, self).__init__('/tmp/qubes-test.xml', load=False,
offline_mode=True, **kwargs)
self.load_initial_values() self.load_initial_values()
self.pools['linux-kernel'].dir_path = '/tmp/qubes-test-kernel' self.pools['linux-kernel'].dir_path = '/tmp/qubes-test-kernel'
dummy_kernel = os.path.join( dummy_kernel = os.path.join(self.pools['linux-kernel'].dir_path,
self.pools['linux-kernel'].dir_path, 'dummy') 'dummy')
os.makedirs(dummy_kernel) os.makedirs(dummy_kernel)
open(os.path.join(dummy_kernel, 'vmlinuz'), 'w').close() open(os.path.join(dummy_kernel, 'vmlinuz'), 'w').close()
open(os.path.join(dummy_kernel, 'modules.img'), 'w').close() open(os.path.join(dummy_kernel, 'modules.img'), 'w').close()
@ -46,16 +44,23 @@ class TestApp(qubes.Qubes):
self.default_kernel = 'dummy' self.default_kernel = 'dummy'
def cleanup(self): def cleanup(self):
''' Remove temporary directories '''
shutil.rmtree(self.pools['linux-kernel'].dir_path) shutil.rmtree(self.pools['linux-kernel'].dir_path)
def create_dummy_template(self): def create_dummy_template(self):
self.add_new_vm(qubes.vm.templatevm.TemplateVM, ''' Initalizes a dummy TemplateVM as the `default_template` '''
template = self.add_new_vm(qubes.vm.templatevm.TemplateVM,
name='test-template', label='red', name='test-template', label='red',
memory=1024, maxmem=1024) memory=1024, maxmem=1024)
self.default_template = 'test-template' self.default_template = template
class TC_00_FilePool(QubesTestCase):
""" This class tests some properties of the 'default' pool. """ class TC_00_FilePool(qubes.tests.QubesTestCase):
""" This class tests some properties of the 'default' pool.
This test might become obsolete if we change the driver for the default
pool to something else as 'file'.
"""
def setUp(self): def setUp(self):
super(TC_00_FilePool, self).setUp() super(TC_00_FilePool, self).setUp()
@ -76,21 +81,23 @@ class TC_00_FilePool(QubesTestCase):
self.assertEquals(result, expected) self.assertEquals(result, expected)
def test001_default_storage_class(self): def test001_default_storage_class(self):
""" Check when using default pool the Storage is ``Storage``. """ """ Check when using default pool the Storage is
``qubes.storage.Storage``. """
result = self._init_app_vm().storage result = self._init_app_vm().storage
self.assertIsInstance(result, Storage) self.assertIsInstance(result, qubes.storage.Storage)
def _init_app_vm(self): def _init_app_vm(self):
""" Return initalised, but not created, AppVm. """ """ Return initalised, but not created, AppVm. """
vmname = self.make_vm_name('appvm') vmname = self.make_vm_name('appvm')
self.app.create_dummy_template() self.app.create_dummy_template()
return self.app.add_new_vm(qubes.vm.appvm.AppVM, return self.app.add_new_vm(qubes.vm.appvm.AppVM, name=vmname,
name=vmname,
template=self.app.default_template, template=self.app.default_template,
label='red') label='red')
class TC_01_FileVolumes(QubesTestCase): class TC_01_FileVolumes(qubes.tests.QubesTestCase):
''' Test correct handling of different types of volumes '''
POOL_DIR = '/tmp/test-pool' POOL_DIR = '/tmp/test-pool'
POOL_NAME = 'test-pool' POOL_NAME = 'test-pool'
POOL_CONF = {'driver': 'file', 'dir_path': POOL_DIR, 'name': POOL_NAME} POOL_CONF = {'driver': 'file', 'dir_path': POOL_DIR, 'name': POOL_NAME}
@ -113,91 +120,99 @@ class TC_01_FileVolumes(QubesTestCase):
config = { config = {
'name': 'root', 'name': 'root',
'pool': self.POOL_NAME, 'pool': self.POOL_NAME,
'volume_type': 'origin', 'save_on_stop': True,
'rw': True,
'size': defaults['root_img_size'], 'size': defaults['root_img_size'],
} }
vm = TestVM(self) vm = qubes.tests.storage.TestVM(self)
result = self.app.get_pool(self.POOL_NAME).init_volume(vm, config) volume = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
self.assertIsInstance(result, OriginFile) self.assertEqual(volume.name, 'root')
self.assertEqual(result.name, 'root') self.assertEqual(volume.pool, self.POOL_NAME)
self.assertEqual(result.pool, self.POOL_NAME) self.assertEqual(volume.size, defaults['root_img_size'])
self.assertEqual(result.size, defaults['root_img_size']) self.assertFalse(volume.snap_on_start)
self.assertTrue(volume.save_on_stop)
self.assertTrue(volume.rw)
def test_001_snapshot_volume(self): def test_001_snapshot_volume(self):
original_path = '/var/lib/qubes/vm-templates/fedora-23/root.img' source = 'vm-templates/fedora-23/root'
original_size = qubes.config.defaults['root_img_size'] original_size = qubes.config.defaults['root_img_size']
config = { config = {
'name': 'root', 'name': 'root',
'pool': 'default', 'pool': 'default',
'volume_type': 'snapshot', 'snap_on_start': True,
'vid': original_path, 'rw': False,
'source': source,
'size': original_size,
} }
vm = TestVM(self, template=self.app.default_template)
result = self.app.get_pool(self.POOL_NAME).init_volume(vm, config) template_vm = self.app.default_template
self.assertIsInstance(result, SnapshotFile) vm = qubes.tests.storage.TestVM(self, template=template_vm)
self.assertEqual(result.name, 'root') volume = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
self.assertEqual(result.pool, 'default') self.assertEqual(volume.name, 'root')
self.assertEqual(result.size, original_size) self.assertEqual(volume.pool, 'default')
self.assertEqual(volume.size, original_size)
self.assertTrue(volume.snap_on_start)
self.assertTrue(volume.snap_on_start)
self.assertFalse(volume.save_on_stop)
self.assertFalse(volume.rw)
self.assertEqual(volume.usage, 0)
def test_002_read_write_volume(self): def test_002_read_write_volume(self):
config = { config = {
'name': 'root', 'name': 'root',
'pool': self.POOL_NAME, 'pool': self.POOL_NAME,
'volume_type': 'read-write', 'rw': True,
'save_on_stop': True,
'size': defaults['root_img_size'], 'size': defaults['root_img_size'],
} }
vm = TestVM(self) vm = qubes.tests.storage.TestVM(self)
result = self.app.get_pool(self.POOL_NAME).init_volume(vm, config) volume = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
self.assertIsInstance(result, ReadWriteFile) self.assertEqual(volume.name, 'root')
self.assertEqual(result.name, 'root') self.assertEqual(volume.pool, self.POOL_NAME)
self.assertEqual(result.pool, self.POOL_NAME) self.assertEqual(volume.size, defaults['root_img_size'])
self.assertEqual(result.size, defaults['root_img_size']) self.assertFalse(volume.snap_on_start)
self.assertTrue(volume.save_on_stop)
self.assertTrue(volume.rw)
@unittest.expectedFailure def test_003_read_only_volume(self):
def test_003_read_volume(self):
template = self.app.default_template template = self.app.default_template
original_path = template.volumes['root'].vid vid = template.volumes['root'].vid
original_size = qubes.config.defaults['root_img_size'] config = {'name': 'root', 'pool': 'default', 'rw': False, 'vid': vid}
config = { vm = qubes.tests.storage.TestVM(self, template=template)
'name': 'root',
'pool': 'default',
'volume_type': 'read-only',
'vid': original_path
}
vm = TestVM(self, template=template)
result = self.app.get_pool(self.POOL_NAME).init_volume(vm, config) volume = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
self.assertIsInstance(result, ReadOnlyFile) self.assertEqual(volume.name, 'root')
self.assertEqual(result.name, 'root') self.assertEqual(volume.pool, 'default')
self.assertEqual(result.pool, 'default')
self.assertEqual(result.size, original_size) # original_size = qubes.config.defaults['root_img_size']
# FIXME: self.assertEqual(volume.size, original_size)
self.assertFalse(volume.snap_on_start)
self.assertFalse(volume.save_on_stop)
self.assertFalse(volume.rw)
def test_004_volatile_volume(self): def test_004_volatile_volume(self):
config = { config = {
'name': 'root', 'name': 'root',
'pool': self.POOL_NAME, 'pool': self.POOL_NAME,
'volume_type': 'volatile',
'size': defaults['root_img_size'], 'size': defaults['root_img_size'],
'rw': True,
} }
vm = TestVM(self) vm = qubes.tests.storage.TestVM(self)
result = self.app.get_pool(self.POOL_NAME).init_volume(vm, config) volume = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
self.assertIsInstance(result, VolatileFile) self.assertEqual(volume.name, 'root')
self.assertEqual(result.name, 'root') self.assertEqual(volume.pool, self.POOL_NAME)
self.assertEqual(result.pool, self.POOL_NAME) self.assertEqual(volume.size, defaults['root_img_size'])
self.assertEqual(result.size, defaults['root_img_size']) self.assertFalse(volume.snap_on_start)
self.assertFalse(volume.save_on_stop)
self.assertTrue(volume.rw)
def test_005_appvm_volumes(self): def test_005_appvm_volumes(self):
''' Check if AppVM volumes are propertly initialized ''' ''' Check if AppVM volumes are propertly initialized '''
vmname = self.make_vm_name('appvm') vmname = self.make_vm_name('appvm')
vm = self.app.add_new_vm(qubes.vm.appvm.AppVM, vm = self.app.add_new_vm(qubes.vm.appvm.AppVM, name=vmname,
name=vmname,
template=self.app.default_template, template=self.app.default_template,
label='red') label='red')
volumes = vm.volumes
self.assertIsInstance(volumes['root'], SnapshotFile)
self.assertIsInstance(volumes['private'], OriginFile)
self.assertIsInstance(volumes['volatile'], VolatileFile)
expected = vm.template.dir_path + '/root.img:' + vm.template.dir_path \ expected = vm.template.dir_path + '/root.img:' + vm.template.dir_path \
+ '/root-cow.img' + '/root-cow.img'
self.assertVolumePath(vm, 'root', expected, rw=False) self.assertVolumePath(vm, 'root', expected, rw=False)
@ -210,14 +225,9 @@ class TC_01_FileVolumes(QubesTestCase):
def test_006_template_volumes(self): def test_006_template_volumes(self):
''' Check if TemplateVM volumes are propertly initialized ''' ''' Check if TemplateVM volumes are propertly initialized '''
vmname = self.make_vm_name('appvm') vmname = self.make_vm_name('appvm')
vm = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM, vm = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM, name=vmname,
name=vmname,
label='red') label='red')
volumes = vm.volumes
self.assertIsInstance(volumes['root'], OriginFile)
self.assertIsInstance(volumes['private'], ReadWriteFile)
self.assertIsInstance(volumes['volatile'], VolatileFile)
expected = vm.dir_path + '/root.img:' + vm.dir_path + '/root-cow.img' expected = vm.dir_path + '/root.img:' + vm.dir_path + '/root-cow.img'
self.assertVolumePath(vm, 'root', expected, rw=True) self.assertVolumePath(vm, 'root', expected, rw=True)
expected = vm.dir_path + '/private.img' expected = vm.dir_path + '/private.img'
@ -233,7 +243,7 @@ class TC_01_FileVolumes(QubesTestCase):
self.assertEquals(b_dev.path, expected) self.assertEquals(b_dev.path, expected)
class TC_03_FilePool(QubesTestCase): class TC_03_FilePool(qubes.tests.QubesTestCase):
""" Test the paths for the default file based pool (``FilePool``). """ Test the paths for the default file based pool (``FilePool``).
""" """
@ -263,7 +273,6 @@ class TC_03_FilePool(QubesTestCase):
shutil.rmtree('/tmp/qubes-test') shutil.rmtree('/tmp/qubes-test')
qubes.config.system_path['qubes_base_dir'] = self._orig_qubes_base_dir qubes.config.system_path['qubes_base_dir'] = self._orig_qubes_base_dir
def test_001_pool_exists(self): def test_001_pool_exists(self):
""" Check if the storage pool was added to the storage pool config """ """ Check if the storage pool was added to the storage pool config """
self.assertIn('test-pool', self.app.pools.keys()) self.assertIn('test-pool', self.app.pools.keys())
@ -290,8 +299,7 @@ class TC_03_FilePool(QubesTestCase):
""" Check if all the needed image files are created for an AppVm""" """ Check if all the needed image files are created for an AppVm"""
vmname = self.make_vm_name('appvm') vmname = self.make_vm_name('appvm')
vm = self.app.add_new_vm(qubes.vm.appvm.AppVM, vm = self.app.add_new_vm(qubes.vm.appvm.AppVM, name=vmname,
name=vmname,
template=self.app.default_template, template=self.app.default_template,
volume_config={ volume_config={
'private': { 'private': {
@ -300,25 +308,17 @@ class TC_03_FilePool(QubesTestCase):
'volatile': { 'volatile': {
'pool': 'test-pool' 'pool': 'test-pool'
} }
}, }, label='red')
label='red') vm.create_on_disk()
vm.storage.create()
expected_vmdir = os.path.join(self.APPVMS_DIR, vm.name) expected_vmdir = os.path.join(self.APPVMS_DIR, vm.name)
expected_private_origin_path = \ expected_private_path = os.path.join(expected_vmdir, 'private.img')
os.path.join(expected_vmdir, 'private.img')
expected_private_cow_path = \
os.path.join(expected_vmdir, 'private-cow.img')
expected_private_path = '%s:%s' % (expected_private_origin_path,
expected_private_cow_path)
self.assertEquals(vm.volumes['private'].path, expected_private_path) self.assertEquals(vm.volumes['private'].path, expected_private_path)
self.assertEqualsAndExists(vm.volumes['private'].path_origin,
expected_private_origin_path)
self.assertEqualsAndExists(vm.volumes['private'].path_cow,
expected_private_cow_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'])\
.reset(vm.volumes['volatile'])
self.assertEqualsAndExists(vm.volumes['volatile'].path, self.assertEqualsAndExists(vm.volumes['volatile'].path,
expected_volatile_path) expected_volatile_path)
@ -327,8 +327,7 @@ class TC_03_FilePool(QubesTestCase):
created propertly by the storage system created propertly by the storage system
""" """
vmname = self.make_vm_name('tmvm') vmname = self.make_vm_name('tmvm')
vm = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM, vm = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM, name=vmname,
name=vmname,
volume_config={ volume_config={
'root': { 'root': {
'pool': 'test-pool' 'pool': 'test-pool'
@ -339,8 +338,7 @@ class TC_03_FilePool(QubesTestCase):
'volatile': { 'volatile': {
'pool': 'test-pool' 'pool': 'test-pool'
} }
}, }, label='red')
label='red')
vm.create_on_disk() vm.create_on_disk()
expected_vmdir = os.path.join(self.TEMPLATES_DIR, vm.name) expected_vmdir = os.path.join(self.TEMPLATES_DIR, vm.name)
@ -349,18 +347,14 @@ class TC_03_FilePool(QubesTestCase):
expected_root_cow_path = os.path.join(expected_vmdir, 'root-cow.img') expected_root_cow_path = os.path.join(expected_vmdir, 'root-cow.img')
expected_root_path = '%s:%s' % (expected_root_origin_path, expected_root_path = '%s:%s' % (expected_root_origin_path,
expected_root_cow_path) expected_root_cow_path)
self.assertEquals(vm.volumes['root'].path, expected_root_path) self.assertEquals(vm.volumes['root'].block_device().path,
self.assertExist(vm.volumes['root'].path_origin) expected_root_path)
self.assertExist(vm.volumes['root'].path)
expected_private_path = os.path.join(expected_vmdir, 'private.img') expected_private_path = os.path.join(expected_vmdir, 'private.img')
self.assertEqualsAndExists(vm.volumes['private'].path, self.assertEqualsAndExists(vm.volumes['private'].path,
expected_private_path) expected_private_path)
expected_volatile_path = os.path.join(expected_vmdir, 'volatile.img')
self.assertEqualsAndExists(vm.volumes['volatile'].path,
expected_volatile_path)
vm.storage.commit_template_changes()
expected_rootcow_path = os.path.join(expected_vmdir, 'root-cow.img') expected_rootcow_path = os.path.join(expected_vmdir, 'root-cow.img')
self.assertEqualsAndExists(vm.volumes['root'].path_cow, self.assertEqualsAndExists(vm.volumes['root'].path_cow,
expected_rootcow_path) expected_rootcow_path)
@ -377,4 +371,5 @@ class TC_03_FilePool(QubesTestCase):
def assertExist(self, path): def assertExist(self, path):
""" Assert that the given path exists. """ """ Assert that the given path exists. """
# :pylint: disable=invalid-name # :pylint: disable=invalid-name
self.assertTrue(os.path.exists(path), "Path %s does not exist" % path) self.assertTrue(
os.path.exists(path), "Path {!s} does not exist".format(path))