Merge remote-tracking branch 'origin/pull/39/head' into core3-devel

This commit is contained in:
Wojtek Porczyk 2016-07-21 16:43:25 +02:00
commit 36e5bcd766
23 changed files with 1062 additions and 684 deletions

View File

@ -86,6 +86,15 @@ Detach the volume with *POOL_NAME:VOLUME_ID* from domain *VMNAME*
aliases: d, dt
revert
^^^^^^
| :command:`qvm-block revert` [-h] [--verbose] [--quiet] *POOL_NAME:VOLUME_ID*
Revert a volume to previous revision.
aliases: rv, r
Authors
-------

View File

@ -1,30 +1,39 @@
.. program:: qvm-clone
===========================================================================
:program:`qvm-clone` -- Clones an existing VM by copying all its disk files
===========================================================================
Synopsis
========
:command:`qvm-clone` [*options*] <*src-name*> <*new-name*>
--------
:command:`qvm-clone` [-h] [--verbose] [--quiet] [-p *POOL:VOLUME* | -P POOL] *VMNAME* *NEWVM*
Options
=======
-------
.. option:: --help, -h
Show this help message and exit
.. option:: -P POOL
Pool to use for the new domain. All volumes besides snapshots volumes are
imported in to the specified POOL. ~HIS IS WHAT YOU WANT TO USE NORMALLY.
.. option:: --pool=POOL:VOLUME, -p POOL:VOLUME
Specify the pool to use for the specific volume
.. option:: --quiet, -q
Be quiet
.. option:: --path=DIR_PATH, -p DIR_PATH
.. option:: --verbose, -v
Specify path to the template directory
Increase verbosity
Authors
=======
-------
| Joanna Rutkowska <joanna at invisiblethingslab dot com>
| Rafal Wojtczuk <rafal at invisiblethingslab dot com>
| Marek Marczykowski <marmarek at invisiblethingslab dot com>
| Bahtiar `kalkin-` Gadimov <bahtiar at gadimov dot de>

View File

@ -32,7 +32,7 @@ Options
The new domain class name (default: **AppVM** for
:py:class:`qubes.vm.appvm.AppVM`).
.. option:: --prop=NAME=VALUE, --property=NAME=VALUE, -p NAME=VALUE
.. option:: --prop=NAME=VALUE, --property=NAME=VALUE
Set domain's property, like "internal", "memory" or "vcpus". Any property may
be set this way, even "qid".
@ -57,9 +57,14 @@ Options
Use provided :file:`root.img` instead of default/empty one (file will be
*moved*). This option is mutually exclusive with :option:`--root-copy-from`.
.. option:: --pool=POOL_NAME:VOLUME_NAME, -P POOL_NAME:VOLUME_NAME
.. option:: -P POOL
Specify the pool to use for a volume
Pool to use for the new domain. All volumes besides snapshots volumes are
imported in to the specified POOL. ~HIS IS WHAT YOU WANT TO USE NORMALLY.
.. option:: --pool=POOL:VOLUME, -p POOL:VOLUME
Specify the pool to use for the specific volume
Options for internal use
------------------------

View File

@ -416,7 +416,6 @@ class VMCollection(object):
return value
def __getitem__(self, key):
if isinstance(key, int):
return self._dict[key]
@ -859,7 +858,6 @@ class Qubes(qubes.PropertyHolder):
'no such VM class: {!r}'.format(clsname))
# don't catch TypeError
def add_new_vm(self, cls, qid=None, **kwargs):
'''Add new Virtual Machine to colletion
@ -872,10 +870,11 @@ class Qubes(qubes.PropertyHolder):
# override it with default template)
if 'template' not in kwargs and hasattr(cls, 'template'):
kwargs['template'] = self.default_template
elif 'template' in kwargs and isinstance(kwargs['template'], str):
kwargs['template'] = self.domains[kwargs['template']]
return self.domains.add(cls(self, None, qid=qid, **kwargs))
def get_label(self, label):
'''Get label as identified by index or name

View File

@ -343,7 +343,7 @@ class Backup(object):
# TODO this is file pool specific. Change it to a more general
# solution
if vm.volumes['private'] is not None:
path_to_private_img = vm.volumes['private'].vid
path_to_private_img = vm.volumes['private'].path
vm_files.append(self.FileToBackup(path_to_private_img, subdir))
vm_files.append(self.FileToBackup(vm.icon_path, subdir))
@ -358,7 +358,7 @@ class Backup(object):
if vm.updateable:
# TODO this is file pool specific. Change it to a more general
# solution
path_to_root_img = vm.volumes['root'].vid
path_to_root_img = vm.volumes['root'].path
vm_files.append(self.FileToBackup(path_to_root_img, subdir))
files_to_backup[vm.qid] = self.VMToBackup(vm, vm_files, subdir)
@ -1725,11 +1725,10 @@ class BackupRestore(object):
# wait for other processes (if any)
for proc in self.processes_to_kill_on_cancel:
proc.wait()
if vmproc.returncode != 0:
raise qubes.exc.QubesException(
"Backup completed, but VM receiving it reported an error "
"(exit code {})".format(vmproc.returncode))
if proc.returncode != 0:
raise qubes.exc.QubesException(
"Backup completed, but VM receiving it reported an error "
"(exit code {})".format(proc.returncode))
if filename and filename != "EOF":
raise qubes.exc.QubesException(
@ -2178,7 +2177,7 @@ class BackupRestore(object):
vm.features['backup-path']),
new_vm.dir_path)
new_vm.verify_files()
new_vm.storage.verify()
except Exception as err:
self.log.error("ERROR: {0}".format(err))
self.log.warning("*** Skipping VM: {0}".format(vm.name))

View File

@ -29,7 +29,9 @@ from __future__ import absolute_import
import os
import os.path
import string
import string # pylint: disable=deprecated-module
import time
from datetime import datetime
import lxml.etree
import pkg_resources
@ -49,58 +51,118 @@ class StoragePoolException(qubes.exc.QubesException):
class Volume(object):
''' Encapsulates all data about a volume for serialization to qubes.xml and
libvirt config.
Keep in mind!
volatile = not snap_on_start and not save_on_stop
snapshot = snap_on_start and not save_on_stop
origin = not snap_on_start and save_on_stop
origin_snapshot = snap_on_start and save_on_stop
'''
devtype = 'disk'
domain = None
path = None
rw = True
script = None
usage = 0
def __init__(self, name, pool, volume_type, vid=None, size=0,
removable=False, internal=False, **kwargs):
def __init__(self, name, pool, vid, internal=False, removable=False,
revisions_to_keep=0, rw=False, save_on_stop=False, size=0,
snap_on_start=False, source=None, **kwargs):
''' Initialize a volume.
:param str name: The domain name
:param str pool: The pool name
:param str vid: Volume identifier needs to be unique in pool
:param bool internal: If `True` volume is hidden when qvm-block ls
is used
:param bool removable: If `True` volume can be detached from vm at
run time
:param int revisions_to_keep: Amount of revisions to keep around
: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 save_on_stop: Write changes to disk in vm.stop()
:param str source: Vid of other volume in same pool
:param str/int size: Size of the volume
'''
super(Volume, self).__init__(**kwargs)
self.name = str(name)
self.pool = str(pool)
self.vid = vid
self.size = size
self.volume_type = volume_type
self.removable = removable
self.internal = internal
self.removable = removable
self.revisions_to_keep = revisions_to_keep
self.rw = rw
self.save_on_stop = save_on_stop
self.size = int(size)
self.snap_on_start = snap_on_start
self.source = source
self.vid = vid
def __xml__(self):
return lxml.etree.Element('volume', **self.config)
def __eq__(self, other):
return other.pool == self.pool and other.vid == self.vid
@property
def config(self):
''' return config data for serialization to qubes.xml '''
return {'name': self.name,
'pool': self.pool,
'volume_type': self.volume_type}
def __hash__(self):
return hash('%s:%s' % (self.pool, self.vid))
def __neq__(self, other):
return not self.__eq__(other)
def __repr__(self):
return '{!r}'.format(self.pool + ':' + self.vid)
def __str__(self):
return str(self.vid)
def __xml__(self):
config = _sanitize_config(self.config)
return lxml.etree.Element('volume', **config)
def block_device(self):
''' Return :py:class:`qubes.devices.BlockDevice` for serialization in
the libvirt XML template as <disk>.
'''
return qubes.devices.BlockDevice(self.path, self.name, self.script,
self.rw, self.domain, self.devtype)
self.rw, self.domain, self.devtype)
def __eq__(self, other):
return other.pool == self.pool and other.vid == self.vid \
and other.volume_type == self.volume_type
@property
def revisions(self):
''' Returns a `dict` containing revision identifiers and paths '''
msg = "{!s} has revisions not implemented".format(self.__class__)
raise NotImplementedError(msg)
def __neq__(self, other):
return not self.__eq__(other)
@property
def config(self):
''' return config data for serialization to qubes.xml '''
result = {'name': self.name, 'pool': self.pool, 'vid': self.vid, }
def __hash__(self):
return hash('%s:%s %s' % (self.pool, self.vid, self.volume_type))
if self.internal:
result['internal'] = self.internal
def __str__(self):
return "{!s}:{!s}".format(self.pool, self.vid)
if self.removable:
result['removable'] = self.removable
if self.revisions_to_keep:
result['revisions_to_keep'] = self.revisions_to_keep
if self.rw:
result['rw'] = self.rw
if self.save_on_stop:
result['save_on_stop'] = self.save_on_stop
if self.size:
result['size'] = self.size
if self.snap_on_start:
result['snap_on_start'] = self.snap_on_start
if self.source:
result['source'] = self.source
return result
class Storage(object):
@ -119,14 +181,59 @@ class Storage(object):
#: Additional drive (currently used only by HVM)
self.drive = None
self.pools = {}
if hasattr(vm, 'volume_config'):
for name, conf in self.vm.volume_config.items():
assert 'pool' in conf, "Pool missing in volume_config" % str(
conf)
if 'volume_type' in conf:
conf = self._migrate_config(conf)
pool = self.vm.app.get_pool(conf['pool'])
self.vm.volumes[name] = pool.init_volume(self.vm, conf)
self.pools[name] = pool
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):
''' Attach a volume to the domain '''
assert self.vm.is_running()
@ -187,7 +294,7 @@ class Storage(object):
If :py:attr:`self.vm.kernel` is :py:obj:`None`, the this points inside
:py:attr:`self.vm.dir_path`
'''
assert 'kernel' in self.vm.volumes, "VM has no kernel pool"
assert 'kernel' in self.vm.volumes, "VM has no kernel volume"
return self.vm.volumes['kernel'].kernels_dir
def get_disk_utilization(self):
@ -198,37 +305,55 @@ class Storage(object):
return result
def resize(self, volume, size):
''' Resize volume '''
''' Resizes volume a read-writable volume '''
self.get_pool(volume).resize(volume, size)
def create(self, source_template=None):
def create(self):
''' Creates volumes on disk '''
if source_template is None and hasattr(self.vm, 'template'):
source_template = self.vm.template
old_umask = os.umask(002)
for name, volume in self.vm.volumes.items():
source_volume = None
if source_template and hasattr(source_template, 'volumes'):
source_volume = source_template.volumes[name]
self.get_pool(volume).create(volume, source_volume=source_volume)
for volume in self.vm.volumes.values():
self.get_pool(volume).create(volume)
os.umask(old_umask)
def clone(self, src_vm):
''' Clone volumes from the specified vm '''
self.vm.log.info('Creating directory: {0}'.format(self.vm.dir_path))
if not os.path.exists(self.vm.dir_path):
self.log.info('Creating directory: {0}'.format(self.vm.dir_path))
os.makedirs(self.vm.dir_path)
for name, target in self.vm.volumes.items():
pool = self.get_pool(target)
source = src_vm.volumes[name]
volume = pool.clone(source, target)
assert volume, "%s.clone() returned '%s'" % (pool.__class__,
volume)
self.vm.volumes[name] = volume
src_path = src_vm.dir_path
msg = "Source path {!s} does not exist".format(src_path)
assert os.path.exists(src_path), msg
dst_path = self.vm.dir_path
msg = "Destination {!s} already exists".format(dst_path)
assert not os.path.exists(dst_path), msg
os.mkdir(dst_path)
self.vm.volumes = {}
with VmCreationManager(self.vm):
for name, config in self.vm.volume_config.items():
dst_pool = self.get_pool(config['pool'])
dst = dst_pool.init_volume(self.vm, config)
src_volume = src_vm.volumes[name]
src_pool = self.vm.app.get_pool(src_volume.pool)
if dst_pool == src_pool:
msg = "Cloning volume {!s} from vm {!s}"
self.vm.log.info(msg.format(src_volume.name, src_vm.name))
volume = dst_pool.clone(src_volume, dst)
else:
msg = "Importing volume {!s} from vm {!s}"
self.vm.log.info(msg.format(src_volume.name, src_vm.name))
volume = dst_pool.import_volume(dst_pool, dst, src_pool,
src_volume)
assert volume, "%s.clone() returned '%s'" % (
dst_pool.__class__.__name__, volume)
self.vm.volumes[name] = volume
msg = "Cloning directory: {!s} to {!s}"
msg = msg.format(src_path, dst_path)
self.log.info(msg)
@property
def outdated_volumes(self):
@ -248,11 +373,15 @@ class Storage(object):
def rename(self, old_name, new_name):
''' Notify the pools that the domain was renamed '''
volumes = self.vm.volumes
vm = self.vm
old_dir_path = os.path.join(os.path.dirname(vm.dir_path), old_name)
new_dir_path = os.path.join(os.path.dirname(vm.dir_path), new_name)
os.rename(old_dir_path, new_dir_path)
for name, volume in volumes.items():
pool = self.get_pool(volume)
volumes[name] = pool.rename(volume, old_name, new_name)
def verify_files(self):
def verify(self):
'''Verify that the storage is sane.
On success, returns normally. On failure, raises exception.
@ -264,6 +393,7 @@ class Storage(object):
for volume in self.vm.volumes.values():
self.get_pool(volume).verify(volume)
self.vm.fire_event('domain-verify-files')
return True
def remove(self):
''' Remove all the volumes.
@ -280,7 +410,8 @@ class Storage(object):
def start(self):
''' Execute the start method on each pool '''
for volume in self.vm.volumes.values():
self.get_pool(volume).start(volume)
pool = self.get_pool(volume)
volume = pool.start(volume)
def stop(self):
''' Execute the start method on each pool '''
@ -289,8 +420,12 @@ class Storage(object):
def get_pool(self, volume):
''' Helper function '''
assert isinstance(volume, Volume), "You need to pass a Volume"
return self.pools[volume.name]
assert isinstance(volume, (Volume, basestring)), \
"You need to pass a Volume or pool name as str"
if isinstance(volume, Volume):
return self.pools[volume.name]
else:
return self.vm.app.pools[volume]
def commit_template_changes(self):
''' Makes changes to an 'origin' volume persistent '''
@ -320,109 +455,168 @@ class Pool(object):
3rd Parties providing own storage implementations will need to extend
this class.
'''
''' # pylint: disable=unused-argument
private_img_size = qubes.config.defaults['private_img_size']
root_img_size = qubes.config.defaults['root_img_size']
def __init__(self, name, revisions_to_keep=1, **kwargs):
super(Pool, self).__init__(**kwargs)
self.name = name
self.revisions_to_keep = revisions_to_keep
kwargs['name'] = self.name
def __eq__(self, other):
return self.name == other.name
def __neq__(self, other):
return not self.__eq__(other)
def __init__(self, name, **kwargs):
super(Pool, self).__init__(**kwargs)
self.name = name
kwargs['name'] = self.name
def __str__(self):
return self.name
def __xml__(self):
return lxml.etree.Element('pool', **self.config)
config = _sanitize_config(self.config)
return lxml.etree.Element('pool', **config)
def create(self, volume, source_volume=None):
def create(self, volume):
''' Create the given volume on disk or copy from provided
`source_volume`.
'''
raise NotImplementedError("Pool %s has create() not implemented" %
self.name)
raise self._not_implemented("create")
def commit_template_changes(self, volume):
''' Update origin device '''
raise NotImplementedError(
"Pool %s has commit_template_changes() not implemented" %
self.name)
def commit(self, volume): # pylint: disable=no-self-use
''' Write the snapshot to disk '''
msg = "Got volume_type {!s} when expected 'snap'"
msg = msg.format(volume.volume_type)
assert volume.volume_type == 'snap', msg
@property
def config(self):
''' Returns the pool config to be written to qubes.xml '''
raise NotImplementedError("Pool %s has config() not implemented" %
self.name)
raise self._not_implemented("config")
def clone(self, source, target):
''' Clone volume '''
raise NotImplementedError("Pool %s has clone() not implemented" %
self.name)
raise self._not_implemented("clone")
def destroy(self):
''' Called when removing the pool. Use this for implementation specific
clean up.
'''
raise NotImplementedError("Pool %s has destroy() not implemented" %
self.name)
raise self._not_implemented("destroy")
def export(self, volume):
''' Returns an object that can be `open()`. '''
raise self._not_implemented("export")
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):
''' Initialize a :py:class:`qubes.storage.Volume` from `volume_config`.
'''
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):
raise NotImplementedError("Pool %s has is_outdated() not implemented" %
self.name)
''' 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):
''' Remove volume'''
raise NotImplementedError("Pool %s has remove() not implemented" %
self.name)
raise self._not_implemented("remove")
def rename(self, volume, old_name, new_name):
''' Called when the domain changes its name '''
raise NotImplementedError("Pool %s has rename() not implemented" %
self.name)
raise self._not_implemented("rename")
def start(self, volume):
''' Do what ever is needed on start '''
raise NotImplementedError("Pool %s has start() not implemented" %
self.name)
def reset(self, volume):
''' Drop and recreate volume without copying it's content from source.
'''
raise self._not_implemented("reset")
def revert(self, volume, revision=None):
''' Revert volume to previous revision '''
raise self._not_implemented("revert")
def setup(self):
''' Called when adding a pool to the system. Use this for implementation
specific set up.
'''
raise NotImplementedError("Pool %s has setup() not implemented" %
self.name)
raise self._not_implemented("setup")
def stop(self, volume):
def start(self, volume): # pylint: disable=no-self-use
''' Do what ever is needed on start '''
raise self._not_implemented("start")
def stop(self, volume): # pylint: disable=no-self-use
''' Do what ever is needed on stop'''
raise NotImplementedError("Pool %s has stop() not implemented" %
self.name)
def init_volume(self, vm, volume_config):
''' Initialize a :py:class:`qubes.storage.Volume` from `volume_config`.
'''
raise NotImplementedError("Pool %s has init_volume() not implemented" %
self.name)
def verify(self, volume):
''' Verifies the volume. '''
raise NotImplementedError("Pool %s has verify() not implemented" %
self.name)
raise self._not_implemented("verify")
@property
def volumes(self):
''' Return a list of volumes managed by this pool '''
raise NotImplementedError("Pool %s has volumes() not implemented" %
self.name)
raise self._not_implemented("volumes")
def _not_implemented(self, method_name):
''' Helper for emitting helpful `NotImplementedError` exceptions '''
msg = "Pool driver {!s} has {!s}() not implemented"
msg = msg.format(str(self.__class__.__name__), method_name)
return NotImplementedError(msg)
def _sanitize_config(config):
''' Helper function to convert types to appropriate strings
''' # FIXME: find another solution for serializing basic types
result = {}
for key, value in config.items():
if isinstance(value, bool):
if value:
result[key] = 'True'
else:
result[key] = str(value)
return result
def pool_drivers():
""" Return a list of EntryPoints names """
return [ep.name
for ep in pkg_resources.iter_entry_points(STORAGE_ENTRY_POINT)]
def isodate(seconds=time.time()):
''' Helper method which returns an iso date '''
return datetime.utcfromtimestamp(seconds).isoformat("T")
class VmCreationManager(object):
''' A `ContextManager` which cleans up if volume creation fails.
''' # pylint: disable=too-few-public-methods
def __init__(self, vm):
self.vm = vm
def __enter__(self):
pass
def __exit__(self, type, value, tb): # pylint: disable=redefined-builtin
if type is not None and value is not None and tb is not None:
for volume in self.vm.volumes.values():
try:
pool = self.vm.storage.get_pool(volume)
pool.remove(volume)
except Exception: # pylint: disable=broad-except
pass
os.rmdir(self.vm.dir_path)

View File

@ -21,7 +21,7 @@
#
''' Manages block devices in a domain '''
import string
import string # pylint: disable=deprecated-module
from qubes.storage import Pool, Volume
@ -99,15 +99,12 @@ class DomainPool(Pool):
class DomainVolume(Volume):
''' A volume provided by a block device in an domain '''
def __init__(self, name, pool, desc, mode, size):
if mode == 'w':
volume_type = 'read-write'
else:
volume_type = 'read-only'
def __init__(self, name, pool, desc, mode, **kwargs):
rw = (mode == 'w')
super(DomainVolume, self).__init__(desc,
pool,
volume_type,
vid=name,
size=size,
removable=True)
super(DomainVolume, self).__init__(desc, pool, vid=name, removable=True,
rw=rw, **kwargs)
@property
def revisions(self):
return {}

View File

@ -22,10 +22,8 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
''' This module contains pool implementations backed by file images'''
from __future__ import absolute_import
import os
@ -33,88 +31,102 @@ import os.path
import re
import subprocess
from qubes.storage import Pool, StoragePoolException, Volume
import qubes.storage
BLKSIZE = 512
class FilePool(Pool):
''' File based 'original' disk implementation '''
class FilePool(qubes.storage.Pool):
''' File based 'original' disk implementation
''' # pylint: disable=protected-access
driver = 'file'
def __init__(self, name=None, dir_path=None):
super(FilePool, self).__init__(name=name)
def __init__(self, revisions_to_keep=1, dir_path=None, **kwargs):
super(FilePool, self).__init__(revisions_to_keep=revisions_to_keep,
**kwargs)
assert dir_path, "No pool dir_path specified"
self.dir_path = os.path.normpath(dir_path)
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
def config(self):
return {
'name': self.name,
'dir_path': self.dir_path,
'driver': FilePool.driver,
'revisions_to_keep': self.revisions_to_keep
}
def is_outdated(self, volume):
# FIX: Implement or remove this at all?
raise NotImplementedError
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, 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):
''' Expands volume, throws
:py:class:`qubst.storage.StoragePoolException` if given size is
less than current_size
:py:class:`qubst.storage.qubes.storage.StoragePoolException` if
given size is less than current_size
''' # pylint: disable=no-self-use
_type = volume.volume_type
if _type not in ['origin', 'read-write', 'volatile']:
raise StoragePoolException('Can not resize a %s volume %s' %
(_type, volume.vid))
if not volume.rw:
msg = 'Can not resize reađonly volume {!s}'.format(volume)
raise qubes.storage.StoragePoolException(msg)
if size <= volume.size:
raise StoragePoolException(
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))
if _type == 'origin':
path = volume.path_origin
elif _type in ['read-write', 'volatile']:
path = volume.path
with open(path, 'a+b') as fd:
with open(volume.path, 'a+b') as fd:
fd.truncate(size)
p = subprocess.Popen(
['sudo', 'losetup', '--associated', path],
stdout=subprocess.PIPE)
p = subprocess.Popen(['sudo', 'losetup', '--associated', volume.path],
stdout=subprocess.PIPE)
result = p.communicate()
m = re.match(r'^(/dev/loop\d+):\s', result[0])
@ -122,34 +134,57 @@ class FilePool(Pool):
loop_dev = m.group(1)
# 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):
if volume.volume_type in ['read-write', 'volatile']:
if not volume.internal:
return # do not remove random attached file volumes
elif volume._is_snapshot:
return # no need to remove, because it's just a snapshot
else:
_remove_if_exists(volume.path)
elif volume.volume_type == 'origin':
_remove_if_exists(volume.path)
_remove_if_exists(volume.path_cow)
if volume._is_origin:
_remove_if_exists(volume.path_cow)
def rename(self, volume, old_name, new_name):
assert issubclass(volume.__class__, FileVolume)
old_dir = os.path.dirname(volume.path)
new_dir = os.path.join(os.path.dirname(old_dir), new_name)
subdir, _, volume_path = volume.vid.split('/', 2)
if not os.path.exists(new_dir):
os.makedirs(new_dir)
if volume._is_origin:
# 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
def commit_template_changes(self, volume):
if volume.volume_type != 'origin':
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):
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)
with open(volume.path_cow, 'w') as f_cow:
@ -160,6 +195,37 @@ class FilePool(Pool):
def destroy(self):
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):
create_dir_if_not_exists(self.dir_path)
appvms_path = os.path.join(self.dir_path, 'appvms')
@ -168,18 +234,39 @@ class FilePool(Pool):
create_dir_if_not_exists(vm_templates_path)
def start(self, volume):
if volume.volume_type == 'volatile':
_reset_volume(volume)
if volume.volume_type in ['origin', 'snapshot']:
_check_path(volume.path_origin)
_check_path(volume.path_cow)
else:
if volume._is_snapshot or volume._is_origin:
_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
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):
""" 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
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)
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
return os.path.join(self.dir_path, self._vid_prefix(vm))
def verify(self, volume):
return volume.verify()
@ -262,33 +296,77 @@ class FilePool(Pool):
return self._volumes
class FileVolume(Volume):
class FileVolume(qubes.storage.Volume):
''' 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):
self.target_dir = target_dir
assert self.target_dir, "target_dir not specified"
def __init__(self, dir_path, backward_comp=False, **kwargs):
self.dir_path = dir_path
self.backward_comp = backward_comp
assert self.dir_path, "dir_path not specified"
super(FileVolume, self).__init__(**kwargs)
def _new_dir(self, new_name):
''' Returns a new directory path based on the new_name. This is a helper
method for moving file images during vm renaming.
if self.snap_on_start and self.source is None:
msg = "snap_on_start specified on {!r} but no volume source set"
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)
return os.path.join(os.path.dirname(old_dir), new_name)
path = self.path
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):
''' A mix in which expects a `size` param to be > 0 on initialization and
provides a usage property wrapper.
'''
old_revision = self.path_cow + '.old' # pylint: disable=no-member
def __init__(self, size=0, **kwargs):
assert size, 'Empty size provided'
assert size > 0, 'Size for volume ' + kwargs['name'] + ' is <=0'
super(SizeMixIn, self).__init__(size=int(size), **kwargs)
if not os.path.exists(old_revision):
return {}
else:
seconds = os.path.getctime(old_revision)
iso_date = qubes.storage.isodate(seconds).split('.', 1)[0]
return {iso_date: old_revision}
@property
def usage(self):
@ -296,169 +374,31 @@ class SizeMixIn(FileVolume):
return get_disk_usage(self.vid)
@property
def config(self):
''' return config data for serialization to qubes.xml '''
return {'name': self.name,
'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
def _is_volatile(self):
''' Internal helper. Useful for differentiating volume handling '''
return not self.snap_on_start and not self.save_on_stop
@property
def usage(self):
result = 0
if os.path.exists(self.path_origin):
result += get_disk_usage(self.path_origin)
if os.path.exists(self.path_cow):
result += get_disk_usage(self.path_cow)
return result
def _is_origin(self):
''' Internal helper. Useful for differentiating volume handling '''
# pylint: disable=line-too-long
return not self.snap_on_start and self.save_on_stop and self.revisions_to_keep > 0 # NOQA
def verify(self):
''' Verifies the volume. '''
if not os.path.exists(self.path_origin):
raise StoragePoolException('Missing image file: %s' %
self.path_origin)
@property
def _is_snapshot(self):
''' Internal helper. Useful for differentiating volume handling '''
return self.snap_on_start and not self.save_on_stop
@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):
''' Represents a readonly snapshot of an :py:class:`OriginFile` volume '''
script = 'block-snapshot'
rw = False
usage = 0
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
@property
def _is_volume(self):
''' Internal helper. Usefull for differentiating volume handling '''
# pylint: disable=line-too-long
return not self.snap_on_start and self.save_on_stop and self.revisions_to_keep == 0 # NOQA
def create_sparse_file(path, size):
''' Create an empty sparse file '''
@ -534,7 +474,9 @@ def copy_file(source, destination):
os.makedirs(parent_dir)
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:
raise IOError('Error while copying {!r} to {!r}'.format(source,
destination))
@ -549,17 +491,5 @@ def _remove_if_exists(path):
def _check_path(path):
''' Raise an StoragePoolException if ``path`` does not exist'''
if not os.path.exists(path):
raise StoragePoolException('Missing image file: %s' % path)
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
msg = 'Missing image file: %s' % path
raise qubes.storage.StoragePoolException(msg)

View File

@ -33,12 +33,16 @@ class LinuxModules(Volume):
def __init__(self, target_dir, kernel_version, **kwargs):
kwargs['vid'] = kernel_version
kwargs['source'] = self
super(LinuxModules, self).__init__(**kwargs)
self.kernels_dir = os.path.join(target_dir, kernel_version)
self.path = os.path.join(self.kernels_dir, 'modules.img')
self.vmlinuz = os.path.join(self.kernels_dir, 'vmlinuz')
self.initramfs = os.path.join(self.kernels_dir, 'initramfs')
@property
def revisions(self):
return {}
class LinuxKernel(Pool):
''' Provides linux kernels '''
@ -50,23 +54,22 @@ class LinuxKernel(Pool):
self.dir_path = dir_path
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']
if volume_type != 'read-only':
raise StoragePoolException("Unknown volume type " + volume_type)
assert not volume_config['rw']
volume = LinuxModules(self.dir_path, vm.kernel, **volume_config)
return volume
def is_dirty(self, volume):
return False
def clone(self, source, target):
return target
def create(self, volume, source_volume=None):
def create(self, volume):
return volume
def commit_template_changes(self, volume):
def commit(self, volume):
return volume
@property
@ -80,6 +83,12 @@ class LinuxKernel(Pool):
def destroy(self):
pass
def export(self, volume):
return volume.path
def import_volume(self, dst_pool, dst_volume, src_pool, src_volume):
pass
def is_outdated(self, volume):
return False
@ -115,7 +124,8 @@ class LinuxKernel(Pool):
pool=self.name,
name=kernel_version,
internal=True,
volume_type='read-only')
rw=False
)
for kernel_version in os.listdir(self.dir_path)]

View File

@ -820,7 +820,7 @@ class BackupTestsMixin(SystemTestsMixin):
name=vmname, template=template, provides_network=True, label='red')
testnet.create_on_disk()
vms.append(testnet)
self.fill_image(testnet.volumes['private'].vid, 20*1024*1024)
self.fill_image(testnet.volumes['private'].path, 20*1024*1024)
vmname = self.make_vm_name('test1')
if self.verbose:
@ -831,7 +831,7 @@ class BackupTestsMixin(SystemTestsMixin):
testvm1.netvm = testnet
testvm1.create_on_disk()
vms.append(testvm1)
self.fill_image(testvm1.volumes['private'].vid, 100*1024*1024)
self.fill_image(testvm1.volumes['private'].path, 100*1024*1024)
vmname = self.make_vm_name('testhvm1')
if self.verbose:
@ -841,7 +841,7 @@ class BackupTestsMixin(SystemTestsMixin):
hvm=True,
label='red')
testvm2.create_on_disk()
self.fill_image(testvm2.volumes['root'].vid, 1024 * 1024 * 1024, True)
self.fill_image(testvm2.volumes['root'].path, 1024 * 1024 * 1024, True)
vms.append(testvm2)
vmname = self.make_vm_name('template')
@ -850,7 +850,7 @@ class BackupTestsMixin(SystemTestsMixin):
testvm3 = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM,
name=vmname, label='red')
testvm3.create_on_disk()
self.fill_image(testvm3.root_img, 100*1024*1024, True)
self.fill_image(testvm3.volumes['root'].path, 100 * 1024 * 1024, True)
vms.append(testvm3)
vmname = self.make_vm_name('custom')

View File

@ -26,7 +26,6 @@
import os
import unittest
import sys
import qubes
import qubes.exc
@ -77,10 +76,13 @@ class TC_00_Backup(qubes.tests.BackupTestsMixin, qubes.tests.QubesTestCase):
hvmtemplate = self.app.add_new_vm(
qubes.vm.templatevm.TemplateVM, name=vmname, hvm=True, label='red')
hvmtemplate.create_on_disk()
self.fill_image(os.path.join(hvmtemplate.dir_path, '00file'),
195*1024*1024-4096*3)
self.fill_image(hvmtemplate.private_img, 195*1024*1024-4096*3)
self.fill_image(hvmtemplate.root_img, 1024*1024*1024, sparse=True)
self.fill_image(
os.path.join(hvmtemplate.dir_path, '00file'),
195 * 1024 * 1024 - 4096 * 3)
self.fill_image(hvmtemplate.volumes['private'].path,
195 * 1024 * 1024 - 4096 * 3)
self.fill_image(hvmtemplate.volumes['root'].path, 1024 * 1024 * 1024,
sparse=True)
vms.append(hvmtemplate)
self.app.save()
@ -93,7 +95,7 @@ class TC_00_Backup(qubes.tests.BackupTestsMixin, qubes.tests.QubesTestCase):
def test_005_compressed_custom(self):
vms = self.create_backup_vms()
self.make_backup(vms, compressed="bzip2")
self.make_backup(vms, compression_filter="bzip2")
self.remove_vms(reversed(vms))
self.restore_backup()
for vm in vms:

View File

@ -1,6 +1,6 @@
#!/usr/bin/python
# vim: fileencoding=utf-8
# pylint: disable=invalid-name
#
# The Qubes OS Project, https://www.qubes-os.org/
#
@ -35,7 +35,7 @@ import qubes.vm.appvm
import qubes.vm.qubesvm
import qubes.vm.templatevm
import libvirt
import libvirt # pylint: disable=import-error
class TC_00_Basic(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
@ -46,17 +46,12 @@ class TC_00_Basic(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
def test_000_qubes_create(self):
self.assertIsInstance(self.app, qubes.Qubes)
def test_001_qvm_create_default_template(self):
self.app.add_new_vm
def test_100_qvm_create(self):
vmname = self.make_vm_name('appvm')
vm = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=vmname, template=self.app.default_template,
label='red'
)
label='red')
self.assertIsNotNone(vm)
self.assertEqual(vm.name, vmname)
@ -64,17 +59,18 @@ class TC_00_Basic(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
vm.create_on_disk()
with self.assertNotRaises(qubes.exc.QubesException):
vm.verify_files()
vm.storage.verify()
class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
# pylint: disable=attribute-defined-outside-init
def setUp(self):
super(TC_01_Properties, self).setUp()
self.init_default_template()
self.vmname = self.make_vm_name('appvm')
self.vm = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=self.vmname,
label='red')
self.vm = self.app.add_new_vm(qubes.vm.appvm.AppVM, name=self.vmname,
template=self.app.default_template,
label='red')
self.vm.create_on_disk()
def save_and_reload_db(self):
@ -82,7 +78,7 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
if hasattr(self, 'vm'):
self.vm = self.app.domains[self.vm.qid]
if hasattr(self, 'netvm'):
self.netvm = self.app[self.netvm.qid]
self.netvm = self.app.domains[self.netvm.qid]
def test_000_rename(self):
newname = self.make_vm_name('newname')
@ -103,13 +99,11 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
os.path.join(
qubes.config.system_path['qubes_base_dir'],
qubes.config.system_path['qubes_appvms_dir'], newname))
self.assertEqual(self.vm.conf_file,
os.path.join(self.vm.dir_path, 'libvirt.xml'))
self.assertTrue(os.path.exists(
os.path.join(self.vm.dir_path, "apps", newname + "-vm.directory")))
# FIXME: set whitelisted-appmenus.list first
self.assertTrue(os.path.exists(
os.path.join(self.vm.dir_path, "apps", newname + "-firefox.desktop")))
self.assertTrue(os.path.exists(os.path.join(
self.vm.dir_path, "apps", newname + "-firefox.desktop")))
self.assertTrue(os.path.exists(
os.path.join(os.getenv("HOME"), ".local/share/desktop-directories",
newname + "-vm.directory")))
@ -135,7 +129,7 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
def test_001_rename_libvirt_undefined(self):
self.vm.libvirt_domain.undefine()
self.vm._libvirt_domain = None
self.vm._libvirt_domain = None # pylint: disable=protected-access
newname = self.make_vm_name('newname')
with self.assertNotRaises(
@ -146,15 +140,20 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
testvm1 = self.app.add_new_vm(
qubes.vm.appvm.AppVM,
name=self.make_vm_name("vm"),
template=self.app.default_template,
label='red')
testvm1.create_on_disk()
testvm2 = self.app.add_new_vm(testvm1.__class__,
name=self.make_vm_name("clone"),
template=testvm1.template,
label='red',
)
label='red')
testvm2.clone_properties(testvm1)
testvm2.clone_disk_files(testvm1)
self.assertTrue(testvm1.storage.verify())
self.assertIn('source', testvm1.volumes['root'].config)
self.assertNotEquals(testvm2, None)
self.assertNotEquals(testvm2.volumes, {})
self.assertIn('source', testvm2.volumes['root'].config)
# qubes.xml reload
self.save_and_reload_db()
@ -200,8 +199,7 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
testvm3 = self.app.add_new_vm(testvm1.__class__,
name=self.make_vm_name("clone2"),
template=testvm1.template,
label='red',
)
label='red',)
testvm3.clone_properties(testvm1)
testvm3.clone_disk_files(testvm1)
@ -235,14 +233,15 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
# TODO decide what exception should be here
with self.assertRaises((qubes.exc.QubesException, ValueError)):
self.vm2 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=self.vmname, template=self.app.default_template)
name=self.vmname, template=self.app.default_template,
label='red')
self.vm2.create_on_disk()
def test_021_name_conflict_template(self):
# TODO decide what exception should be here
with self.assertRaises((qubes.exc.QubesException, ValueError)):
self.vm2 = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM,
name=self.vmname)
name=self.vmname, label='red')
self.vm2.create_on_disk()
def test_030_rename_conflict_app(self):
@ -257,6 +256,7 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
self.vm2.name = self.vmname
class TC_02_QvmPrefs(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
# pylint: disable=attribute-defined-outside-init
def setUp(self):
super(TC_02_QvmPrefs, self).setUp()
@ -349,7 +349,7 @@ class TC_02_QvmPrefs(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
('invalid', '', False),
('[invalid]', '', False),
# TODO:
#('["12:12.0"]', '', False)
# ('["12:12.0"]', '', False)
])
@unittest.skip('test not converted to core3 API')
@ -368,6 +368,7 @@ class TC_02_QvmPrefs(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
class TC_03_QvmRevertTemplateChanges(qubes.tests.SystemTestsMixin,
qubes.tests.QubesTestCase):
# pylint: disable=attribute-defined-outside-init
def setUp(self):
super(TC_03_QvmRevertTemplateChanges, self).setUp()
@ -395,7 +396,7 @@ class TC_03_QvmRevertTemplateChanges(qubes.tests.SystemTestsMixin,
def get_rootimg_checksum(self):
p = subprocess.Popen(
['sha1sum', self.test_template.volumes['root'].vid],
['sha1sum', self.test_template.volumes['root'].path_cow],
stdout=subprocess.PIPE)
return p.communicate()[0]
@ -407,9 +408,11 @@ class TC_03_QvmRevertTemplateChanges(qubes.tests.SystemTestsMixin,
if checksum_before == checksum_changed:
self.log.warning("template not modified, test result will be "
"unreliable")
self.assertNotEqual(self.test_template.volumes['root'].revisions, {})
with self.assertNotRaises(subprocess.CalledProcessError):
subprocess.check_call(['sudo', 'qvm-revert-template-changes',
'--force', self.test_template.name])
pool_vid = repr(self.test_template.volumes['root']).strip("'")
revert_cmd = ['qvm-block', 'revert', pool_vid]
subprocess.check_call(revert_cmd)
checksum_after = self.get_rootimg_checksum()
self.assertEquals(checksum_before, checksum_after)
@ -442,8 +445,7 @@ class TC_04_DispVM(qubes.tests.SystemTestsMixin,
self.disp_tpl = self.app.add_new_vm(disp_tpl.__class__,
name=disp_tpl.name,
template=disp_tpl.template,
label='red'
)
label='red')
self.app.save()
@staticmethod
@ -458,6 +460,7 @@ class TC_04_DispVM(qubes.tests.SystemTestsMixin,
"""
testvm1 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
template=self.app.default_template,
name=self.make_vm_name('vm1'),
label='red')
testvm1.create_on_disk()
@ -512,6 +515,7 @@ class TC_04_DispVM(qubes.tests.SystemTestsMixin,
Check firewall propagation VM->DispVM, when VM have no firewall rules
"""
testvm1 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
template=self.app.default_template,
name=self.make_vm_name('vm1'),
label='red')
testvm1.create_on_disk()
@ -544,8 +548,8 @@ class TC_04_DispVM(qubes.tests.SystemTestsMixin,
dispvm_name = p.stdout.readline().strip()
self.reload_db()
dispvm = self.app.domains[dispvm_name]
self.assertIsNotNone(dispvm, "DispVM {} not found in qubes.xml".format(
dispvm_name))
self.assertIsNotNone(
dispvm, "DispVM {} not found in qubes.xml".format(dispvm_name))
# check if firewall was propagated to the DispVM from the right VM
self.assertEquals(testvm1.get_firewall_conf(),
dispvm.get_firewall_conf())
@ -625,6 +629,4 @@ class TC_04_DispVM(qubes.tests.SystemTestsMixin,
self.assertIsNone(dispvm, "DispVM {} still exists in qubes.xml".format(
dispvm_name))
# vim: ts=4 sw=4 et

View File

@ -20,19 +20,16 @@ import qubes.log
from qubes.exc import QubesException
from qubes.storage import pool_drivers
from qubes.storage.file import FilePool
from qubes.tests import QubesTestCase, SystemTestsMixin
from qubes.tests import QubesTestCase
# :pylint: disable=invalid-name
class TestApp(qubes.tests.TestEmitter):
pass
class TestVM(object):
def __init__(self, test, template=None):
self.app = test.app
self.name = test.make_vm_name('appvm')
self.dir_path = '/var/lib/qubes/appvms/' + self.name
self.log = qubes.log.get_vm_logger(self.name)
if template:
@ -50,6 +47,10 @@ class TestVM(object):
class TestTemplateVM(TestVM):
dir_path_prefix = qubes.config.system_path['qubes_templates_dir']
def __init__(self, test, template=None):
super(TestTemplateVM, self).__init__(test, template)
self.dir_path = '/var/lib/qubes/vm-templates/' + self.name
def is_template(self):
return True
@ -59,7 +60,7 @@ class TestDisposableVM(TestVM):
return True
class TestApp(qubes.Qubes):
def __init__(self, *args, **kwargs):
def __init__(self, *args, **kwargs): # pylint: disable=unused-argument
super(TestApp, self).__init__('/tmp/qubes-test.xml',
load=False, offline_mode=True, **kwargs)
self.load_initial_values()

View File

@ -16,29 +16,27 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
''' Tests for the file storage backend '''
import os
import shutil
import qubes.storage
import qubes.tests.storage
import unittest
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
class TestApp(qubes.Qubes):
def __init__(self, *args, **kwargs):
super(TestApp, self).__init__('/tmp/qubes-test.xml',
load=False, offline_mode=True, **kwargs)
''' A Mock App object '''
def __init__(self, *args, **kwargs): # pylint: disable=unused-argument
super(TestApp, self).__init__('/tmp/qubes-test.xml', load=False,
offline_mode=True, **kwargs)
self.load_initial_values()
self.pools['linux-kernel'].dir_path = '/tmp/qubes-test-kernel'
dummy_kernel = os.path.join(
self.pools['linux-kernel'].dir_path, 'dummy')
dummy_kernel = os.path.join(self.pools['linux-kernel'].dir_path,
'dummy')
os.makedirs(dummy_kernel)
open(os.path.join(dummy_kernel, 'vmlinuz'), 'w').close()
open(os.path.join(dummy_kernel, 'modules.img'), 'w').close()
@ -46,16 +44,23 @@ class TestApp(qubes.Qubes):
self.default_kernel = 'dummy'
def cleanup(self):
''' Remove temporary directories '''
shutil.rmtree(self.pools['linux-kernel'].dir_path)
def create_dummy_template(self):
self.add_new_vm(qubes.vm.templatevm.TemplateVM,
name='test-template', label='red',
memory=1024, maxmem=1024)
self.default_template = 'test-template'
''' Initalizes a dummy TemplateVM as the `default_template` '''
template = self.add_new_vm(qubes.vm.templatevm.TemplateVM,
name='test-template', label='red',
memory=1024, maxmem=1024)
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):
super(TC_00_FilePool, self).setUp()
@ -76,21 +81,23 @@ class TC_00_FilePool(QubesTestCase):
self.assertEquals(result, expected)
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
self.assertIsInstance(result, Storage)
self.assertIsInstance(result, qubes.storage.Storage)
def _init_app_vm(self):
""" Return initalised, but not created, AppVm. """
vmname = self.make_vm_name('appvm')
self.app.create_dummy_template()
return self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=vmname,
return self.app.add_new_vm(qubes.vm.appvm.AppVM, name=vmname,
template=self.app.default_template,
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_NAME = 'test-pool'
POOL_CONF = {'driver': 'file', 'dir_path': POOL_DIR, 'name': POOL_NAME}
@ -113,91 +120,99 @@ class TC_01_FileVolumes(QubesTestCase):
config = {
'name': 'root',
'pool': self.POOL_NAME,
'volume_type': 'origin',
'save_on_stop': True,
'rw': True,
'size': defaults['root_img_size'],
}
vm = TestVM(self)
result = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
self.assertIsInstance(result, OriginFile)
self.assertEqual(result.name, 'root')
self.assertEqual(result.pool, self.POOL_NAME)
self.assertEqual(result.size, defaults['root_img_size'])
vm = qubes.tests.storage.TestVM(self)
volume = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
self.assertEqual(volume.name, 'root')
self.assertEqual(volume.pool, self.POOL_NAME)
self.assertEqual(volume.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):
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']
config = {
'name': 'root',
'pool': 'default',
'volume_type': 'snapshot',
'vid': original_path,
'snap_on_start': True,
'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)
self.assertIsInstance(result, SnapshotFile)
self.assertEqual(result.name, 'root')
self.assertEqual(result.pool, 'default')
self.assertEqual(result.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)
self.assertEqual(volume.name, 'root')
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):
config = {
'name': 'root',
'pool': self.POOL_NAME,
'volume_type': 'read-write',
'rw': True,
'save_on_stop': True,
'size': defaults['root_img_size'],
}
vm = TestVM(self)
result = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
self.assertIsInstance(result, ReadWriteFile)
self.assertEqual(result.name, 'root')
self.assertEqual(result.pool, self.POOL_NAME)
self.assertEqual(result.size, defaults['root_img_size'])
vm = qubes.tests.storage.TestVM(self)
volume = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
self.assertEqual(volume.name, 'root')
self.assertEqual(volume.pool, self.POOL_NAME)
self.assertEqual(volume.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_volume(self):
def test_003_read_only_volume(self):
template = self.app.default_template
original_path = template.volumes['root'].vid
original_size = qubes.config.defaults['root_img_size']
config = {
'name': 'root',
'pool': 'default',
'volume_type': 'read-only',
'vid': original_path
}
vm = TestVM(self, template=template)
vid = template.volumes['root'].vid
config = {'name': 'root', 'pool': 'default', 'rw': False, 'vid': vid}
vm = qubes.tests.storage.TestVM(self, template=template)
result = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
self.assertIsInstance(result, ReadOnlyFile)
self.assertEqual(result.name, 'root')
self.assertEqual(result.pool, 'default')
self.assertEqual(result.size, original_size)
volume = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
self.assertEqual(volume.name, 'root')
self.assertEqual(volume.pool, 'default')
# 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):
config = {
'name': 'root',
'pool': self.POOL_NAME,
'volume_type': 'volatile',
'size': defaults['root_img_size'],
'rw': True,
}
vm = TestVM(self)
result = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
self.assertIsInstance(result, VolatileFile)
self.assertEqual(result.name, 'root')
self.assertEqual(result.pool, self.POOL_NAME)
self.assertEqual(result.size, defaults['root_img_size'])
vm = qubes.tests.storage.TestVM(self)
volume = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
self.assertEqual(volume.name, 'root')
self.assertEqual(volume.pool, self.POOL_NAME)
self.assertEqual(volume.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):
''' Check if AppVM volumes are propertly initialized '''
vmname = self.make_vm_name('appvm')
vm = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=vmname,
vm = self.app.add_new_vm(qubes.vm.appvm.AppVM, name=vmname,
template=self.app.default_template,
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 \
+ '/root-cow.img'
self.assertVolumePath(vm, 'root', expected, rw=False)
@ -210,14 +225,9 @@ class TC_01_FileVolumes(QubesTestCase):
def test_006_template_volumes(self):
''' Check if TemplateVM volumes are propertly initialized '''
vmname = self.make_vm_name('appvm')
vm = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM,
name=vmname,
vm = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM, name=vmname,
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'
self.assertVolumePath(vm, 'root', expected, rw=True)
expected = vm.dir_path + '/private.img'
@ -233,7 +243,7 @@ class TC_01_FileVolumes(QubesTestCase):
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``).
"""
@ -263,7 +273,6 @@ class TC_03_FilePool(QubesTestCase):
shutil.rmtree('/tmp/qubes-test')
qubes.config.system_path['qubes_base_dir'] = self._orig_qubes_base_dir
def test_001_pool_exists(self):
""" Check if the storage pool was added to the storage pool config """
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"""
vmname = self.make_vm_name('appvm')
vm = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=vmname,
vm = self.app.add_new_vm(qubes.vm.appvm.AppVM, name=vmname,
template=self.app.default_template,
volume_config={
'private': {
@ -300,25 +308,17 @@ class TC_03_FilePool(QubesTestCase):
'volatile': {
'pool': 'test-pool'
}
},
label='red')
vm.storage.create()
}, label='red')
vm.create_on_disk()
expected_vmdir = os.path.join(self.APPVMS_DIR, vm.name)
expected_private_origin_path = \
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)
expected_private_path = os.path.join(expected_vmdir, 'private.img')
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')
vm.storage.get_pool(vm.volumes['volatile'])\
.reset(vm.volumes['volatile'])
self.assertEqualsAndExists(vm.volumes['volatile'].path,
expected_volatile_path)
@ -327,8 +327,7 @@ class TC_03_FilePool(QubesTestCase):
created propertly by the storage system
"""
vmname = self.make_vm_name('tmvm')
vm = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM,
name=vmname,
vm = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM, name=vmname,
volume_config={
'root': {
'pool': 'test-pool'
@ -339,8 +338,7 @@ class TC_03_FilePool(QubesTestCase):
'volatile': {
'pool': 'test-pool'
}
},
label='red')
}, label='red')
vm.create_on_disk()
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_path = '%s:%s' % (expected_root_origin_path,
expected_root_cow_path)
self.assertEquals(vm.volumes['root'].path, expected_root_path)
self.assertExist(vm.volumes['root'].path_origin)
self.assertEquals(vm.volumes['root'].block_device().path,
expected_root_path)
self.assertExist(vm.volumes['root'].path)
expected_private_path = os.path.join(expected_vmdir, 'private.img')
self.assertEqualsAndExists(vm.volumes['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')
self.assertEqualsAndExists(vm.volumes['root'].path_cow,
expected_rootcow_path)
@ -377,4 +371,5 @@ class TC_03_FilePool(QubesTestCase):
def assertExist(self, path):
""" Assert that the given path exists. """
# :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))

View File

@ -45,19 +45,21 @@ def prepare_table(vd_list, full=False):
'''
output = []
if sys.stdout.isatty():
output += [('POOL_NAME:VOLUME_ID', 'VOLUME_TYPE', 'VMNAME')]
output += [('POOL:VOLUME', 'VMNAME', 'VOLUME_NAME')] # NOQA
for volume in vd_list:
if volume.domains:
vmname = volume.domains.pop()
output += [(str(volume), volume.volume_type, vmname)]
for vmname in volume.domains:
vmname, volume_name = volume.domains.pop()
output += [(str(volume), vmname, volume_name, volume.revisions)]
for tupple in volume.domains:
vmname, volume_name = tupple
if full or not sys.stdout.isatty():
output += [(str(volume), volume.volume_type, vmname)]
output += [(str(volume), vmname, volume_name,
volume.revisions)]
else:
output += [('', '', vmname)]
output += [('', vmname, volume_name, '', volume.revisions)]
else:
output += [(str(volume), volume.volume_type)]
output += [(str(volume), "")]
return output
@ -70,8 +72,11 @@ class VolumeData(object):
def __init__(self, volume):
self.name = volume.name
self.pool = volume.pool
self.volume_type = volume.volume_type
self.vid = volume.vid
if volume.revisions != {}:
self.revisions = 'Yes'
else:
self.revisions = 'No'
self.domains = []
def __str__(self):
@ -110,7 +115,7 @@ def list_volumes(args):
for volume in domain.attached_volumes:
try:
volume_data = vd_dict[volume.pool][volume.vid]
volume_data.domains += [domain.name]
volume_data.domains += [(domain.name, volume.name)]
except KeyError:
# Skipping volume
continue
@ -126,6 +131,15 @@ def list_volumes(args):
result = [x for p in vd_dict.itervalues() for x in p.itervalues()]
qubes.tools.print_table(prepare_table(result, full=args.full))
def revert_volume(args):
volume = args.volume
app = args.app
try:
pool = app.pools[volume.pool]
pool.revert(volume)
except qubes.storage.StoragePoolException as e:
print(e.message, file=sys.stderr)
sys.exit(1)
def attach_volumes(args):
''' Called by the parser to execute the :program:`qvm-block attach`
@ -173,6 +187,14 @@ def init_list_parser(sub_parsers):
list_parser._mutually_exclusive_groups.append(vm_name_group)
list_parser.set_defaults(func=list_volumes)
def init_revert_parser(sub_parsers):
revert_parser = sub_parsers.add_parser(
'revert', aliases=('rv', 'r'),
help='revert volume to previous revision')
revert_parser.add_argument(metavar='POOL_NAME:VOLUME_ID', dest='volume',
action=qubes.tools.VolumeAction)
revert_parser.set_defaults(func=revert_volume)
def get_parser():
'''Create :py:class:`argparse.ArgumentParser` suitable for
@ -185,6 +207,7 @@ def get_parser():
description="For more information see qvm-block command -h",
dest='command')
init_list_parser(sub_parsers)
init_revert_parser(sub_parsers)
attach_parser = sub_parsers.add_parser(
'attach', help="Attach volume to domain", aliases=('at', 'a'))
attach_parser.add_argument('--ro', help='attach device read-only',

73
qubes/tools/qvm_clone.py Normal file
View File

@ -0,0 +1,73 @@
#!/usr/bin/python2
# -*- encoding: utf8 -*-
#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2016 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
''' Clone a domain '''
import sys
from qubes.tools import QubesArgumentParser, SinglePropertyAction
parser = QubesArgumentParser(description=__doc__, vmname_nargs=1)
parser.add_argument('new_name',
metavar='NEWVM',
action=SinglePropertyAction,
help='name of the domain to create')
group = parser.add_mutually_exclusive_group()
group.add_argument('-P',
metavar='POOL',
dest='one_pool',
default='',
help='pool to use for the new domain')
group.add_argument('-p',
'--pool',
action='append',
metavar='POOL:VOLUME',
help='specify the pool to use for the specific volume')
def main(args=None):
''' Clones an existing VM by copying all its disk files '''
args = parser.parse_args(args)
app = args.app
src_vm = args.domains[0]
new_name = args.properties['new_name']
dst_vm = app.add_new_vm(src_vm.__class__, name=new_name)
dst_vm.clone_properties(src_vm)
if args.one_pool:
dst_vm.clone_disk_files(src_vm, pool=args.one_pool)
elif hasattr(args, 'pools') and args.pools:
dst_vm.clone_disk_files(src_vm, pools=args.pools)
else:
dst_vm.clone_disk_files(src_vm)
# try:
app.save() # HACK remove_from_disk on exception hangs for some reason
# except Exception as e: # pylint: disable=broad-except
# dst_vm.remove_from_disk()
# parser.print_error(e)
# return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -42,15 +42,21 @@ parser.add_argument('--class', '-C', dest='cls',
default='AppVM',
help='specify the class of the new domain (default: %(default)s)')
parser.add_argument('--property', '--prop', '-p',
parser.add_argument('--property', '--prop',
action=qubes.tools.PropertyAction,
help='set domain\'s property, like "internal", "memory" or "vcpus"')
parser.add_argument('--pool', '-P',
parser.add_argument('--pool', '-p',
action='append',
metavar='POOL_NAME:VOLUME_NAME',
help='specify the pool to use for a volume')
parser.add_argument('-P',
metavar='POOL_NAME',
dest='one_pool',
default='',
help='change all volume pools to specified pool')
parser.add_argument('--template', '-t',
action=qubes.tools.SinglePropertyAction,
help='specify the TemplateVM to use')
@ -80,16 +86,18 @@ parser.add_argument('name', metavar='VMNAME',
def main(args=None):
args = parser.parse_args(args)
if args.pool:
args.properties['volume_config'] = {}
pools = {}
pool = None
if hasattr(args, 'pools') and args.pools:
for pool_vol in args.pool:
try:
pool_name, volume_name = pool_vol.split(':')
config = {'pool': pool_name, 'name': volume_name}
args.properties['volume_config'][volume_name] = config
pools[volume_name] = pool_name
except ValueError:
parser.error(
'Pool argument must be of form: -P pool_name:volume_name')
if args.one_pool:
pool = args.one_pool
if 'label' not in args.properties:
parser.error('--label option is mandatory')
@ -144,7 +152,7 @@ def main(args=None):
if not args.no_root:
try:
vm.create_on_disk()
vm.create_on_disk(pool, pools)
# TODO this is file pool specific. Change it to a more general
# solution

View File

@ -21,12 +21,12 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
''' This module contains the AppVM implementation '''
import copy
import qubes.events
import qubes.vm.qubesvm
from qubes.config import defaults
@ -39,36 +39,75 @@ class AppVM(qubes.vm.qubesvm.QubesVM):
ls_width=31,
doc='Template, on which this AppVM is based.')
def __init__(self, *args, **kwargs):
def __init__(self, app, xml, template=None, **kwargs):
self.volume_config = {
'root': {
'name': 'root',
'pool': 'default',
'volume_type': 'snapshot',
'snap_on_start': True,
'save_on_stop': False,
'rw': False,
'internal': True
},
'private': {
'name': 'private',
'pool': 'default',
'volume_type': 'origin',
'snap_on_start': False,
'save_on_stop': True,
'rw': True,
'source': None,
'size': defaults['private_img_size'],
'internal': True
},
'volatile': {
'name': 'volatile',
'pool': 'default',
'volume_type': 'volatile',
'size': defaults['root_img_size'],
'internal': True
'internal': True,
'rw': True,
},
'kernel': {
'name': 'kernel',
'pool': 'linux-kernel',
'volume_type': 'read-only',
'snap_on_start': True,
'rw': False,
'internal': True
}
}
super(AppVM, self).__init__(*args, **kwargs)
if template is not None:
# template is only passed if the AppVM is created, in other cases we
# don't need to patch the volume_config because the config is
# coming from XML, already as we need it
for name, conf in self.volume_config.items():
tpl_volume = template.volumes[name]
conf['size'] = tpl_volume.size
conf['pool'] = tpl_volume.pool
has_source = ('source' in conf and conf['source'] is not None)
is_snapshot = 'snap_on_start' in conf and conf['snap_on_start']
if is_snapshot and not has_source:
if tpl_volume.source is not None:
conf['source'] = tpl_volume.source
else:
conf['source'] = tpl_volume.vid
for name, config in template.volume_config.items():
# in case the template vm has more volumes add them to own
# config
if name not in self.volume_config:
self.volume_config[name] = copy.deepcopy(config)
if 'vid' in self.volume_config[name]:
del self.volume_config[name]['vid']
super(AppVM, self).__init__(app, xml, **kwargs)
if not hasattr(template, 'template') and template is not None:
self.template = template
if 'source' not in self.volume_config['root']:
msg = 'missing source for root volume'
raise qubes.exc.QubesException(msg)
@qubes.events.handler('domain-load')
def on_domain_loaded(self, event):

View File

@ -46,24 +46,32 @@ class DispVM(qubes.vm.qubesvm.QubesVM):
'root': {
'name': 'root',
'pool': 'default',
'volume_type': 'snapshot',
'snap_on_start': True,
'save_on_stop': False,
'rw': False,
'internal': True
},
'private': {
'name': 'private',
'pool': 'default',
'volume_type': 'snapshot',
'snap_on_start': True,
'save_on_stop': False,
'internal': True,
'rw': True,
},
'volatile': {
'name': 'volatile',
'pool': 'default',
'volume_type': 'volatile',
'internal': True,
'size': qubes.config.defaults['root_img_size'] +
qubes.config.defaults['private_img_size'],
},
'kernel': {
'name': 'kernel',
'pool': 'linux-kernel',
'volume_type': 'read-only',
'snap_on_start': True,
'rw': False,
'internal': True
}
}

View File

@ -25,6 +25,7 @@
from __future__ import absolute_import
import copy
import base64
import datetime
import itertools
@ -421,10 +422,15 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
name = node.get('name')
assert name
for key, value in node.items():
self.volume_config[name][key] = value
# pylint: disable=no-member
if value == 'True':
self.volume_config[name][key] = True
else:
self.volume_config[name][key] = value
for name, conf in volume_config.items():
for key, value in conf.items():
# pylint: disable=no-member
self.volume_config[name][key] = value
elif volume_config:
@ -542,6 +548,8 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
# pylint: disable=unused-argument
self.init_log()
self.storage.rename(old_name, new_name)
if self._libvirt_domain is not None:
self.libvirt_domain.undefine()
self._libvirt_domain = None
@ -549,8 +557,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
self._qdb_connection.close()
self._qdb_connection = None
self.storage.rename(old_name, new_name)
self._update_libvirt_domain()
if self.autostart:
@ -675,7 +681,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
self.fire_event_pre('domain-pre-start', preparing_dvm=preparing_dvm,
start_guid=start_guid, mem_required=mem_required)
self.storage.verify_files()
self.storage.verify()
if self.netvm is not None:
# pylint: disable = no-member
@ -1061,21 +1067,20 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
user="root", passio_popen=True, gui=False, wait=True)
p.communicate(input=self.default_user)
def create_on_disk(self, source_template=None):
def create_on_disk(self, pool=None, pools=None):
'''Create files needed for VM.
:param qubes.vm.templatevm.TemplateVM source_template: Template to use
(if :py:obj:`None`, use domain's own template
'''
if source_template is None and hasattr(self, 'template'):
# pylint: disable=no-member
source_template = self.template
self.log.info('Creating directory: {0}'.format(self.dir_path))
os.makedirs(self.dir_path, mode=0o775)
self.storage.create(source_template)
if pool or pools:
# pylint: disable=attribute-defined-outside-init
self.volume_config = _patch_volume_config(self.volume_config, pool,
pools)
self.storage = qubes.storage.Storage(self)
self.storage.create()
self.log.info('Creating icon symlink: {} -> {}'.format(
self.icon_path, self.label.icon_path))
@ -1085,29 +1090,37 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
shutil.copy(self.label.icon_path, self.icon_path)
# fire hooks
self.fire_event('domain-create-on-disk', source_template)
self.fire_event('domain-create-on-disk')
def remove_from_disk(self):
'''Remove domain remnants from disk.'''
self.fire_event('domain-remove-from-disk')
self.storage.remove()
shutil.rmtree(self.dir_path)
self.storage.remove()
def clone_disk_files(self, src):
def clone_disk_files(self, src, pool=None, pools=None, ):
'''Clone files from other vm.
:param qubes.vm.qubesvm.QubesVM src: source VM
'''
if not self.is_halted():
# If the current vm name is not a part of `self.app.domains.keys()`,
# then the current vm is in creation process. Calling
# `self.is_halted()` at this point, would instantiate libvirt, we want
# avoid that.
if self.name in self.app.domains.keys() and not self.is_halted():
raise qubes.exc.QubesVMNotHaltedError(
self, 'Cannot clone a running domain {!r}'.format(self.name))
if hasattr(src, 'volume_config'):
if pool or pools:
# pylint: disable=attribute-defined-outside-init
self.volume_config = src.volume_config
self.volume_config = _patch_volume_config(self.volume_config, pool,
pools)
self.storage = qubes.storage.Storage(self)
self.storage.clone(src)
self.storage.verify()
assert self.volumes != {}
if src.icon_path is not None \
and os.path.exists(src.dir_path) \
@ -1580,3 +1593,53 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
domain.memory_maximum = self.get_mem_static_max() * 1024
return qubes.qmemman.algo.prefmem(domain) / 1024
def _clean_volume_config(config):
common_attributes = ['name', 'pool', 'size', 'internal', 'removable',
'revisions_to_keep', 'rw', 'snap_on_start',
'save_on_stop', 'source']
config_copy = copy.deepcopy(config)
return {k: v for k, v in config_copy.items() if k in common_attributes}
def _patch_pool_config(config, pool=None, pools=None):
assert pool is not None or pools is not None
is_saveable = 'save_on_stop' in config and config['save_on_stop']
is_resetable = not ('snap_on_start' in config and # volatile
config['snap_on_start'] and not is_saveable)
is_exportable = is_saveable or is_resetable
name = config['name']
if pool and is_exportable:
config['pool'] = str(pool)
elif pool and not is_exportable:
pass
elif pools and name in pools.keys():
if is_exportable:
config['pool'] = str(pools[name])
else:
msg = "Can't clone a snapshot volume {!s} to pool {!s} " \
.format(name, pools[name])
raise qubes.exc.QubesException(msg)
return config
def _patch_volume_config(volume_config, pool=None, pools=None):
assert not (pool and pools), \
'You can not pass pool & pools parameter at same time'
assert pool or pools
result = {}
for name, config in volume_config.items():
# copy only the subset of volume_config key/values
dst_config = _clean_volume_config(config)
if pool is not None or pools is not None:
dst_config = _patch_pool_config(dst_config, pool, pools)
result[name] = dst_config
return result

View File

@ -34,25 +34,34 @@ class StandaloneVM(qubes.vm.qubesvm.QubesVM):
'root': {
'name': 'root',
'pool': 'default',
'volume_type': 'origin',
'snap_on_start': False,
'save_on_stop': True,
'rw': True,
'source': None,
'internal': True,
'size': qubes.config.defaults['root_img_size'],
},
'private': {
'name': 'private',
'pool': 'default',
'volume_type': 'origin',
'snap_on_start': False,
'save_on_stop': True,
'rw': True,
'source': None,
'internal': True,
'size': qubes.config.defaults['private_img_size'],
},
'volatile': {
'name': 'volatile',
'pool': 'default',
'volume_type': 'volatile',
'internal': True,
'size': qubes.config.defaults['root_img_size'],
},
'kernel': {
'name': 'kernel',
'pool': 'linux-kernel',
'volume_type': 'read-only',
'rw': False,
'internal': True
}
}
super(StandaloneVM, self).__init__(*args, **kwargs)

View File

@ -60,39 +60,41 @@ class TemplateVM(QubesVM):
'root': {
'name': 'root',
'pool': 'default',
'volume_type': 'origin',
'snap_on_start': False,
'save_on_stop': True,
'rw': True,
'source': None,
'size': defaults['root_img_size'],
'internal': True
},
'private': {
'name': 'private',
'pool': 'default',
'volume_type': 'read-write',
'snap_on_start': False,
'save_on_stop': True,
'rw': True,
'source': None,
'size': defaults['private_img_size'],
'revisions_to_keep': 0,
'internal': True
},
'volatile': {
'name': 'volatile',
'pool': 'default',
'size': defaults['root_img_size'],
'volume_type': 'volatile',
'internal': True
'internal': True,
'rw': True,
},
'kernel': {
'name': 'kernel',
'pool': 'linux-kernel',
'volume_type': 'read-only',
'internal': True
'snap_on_start': True,
'internal': True,
'rw': False
}
}
super(TemplateVM, self).__init__(*args, **kwargs)
def clone_disk_files(self, src):
super(TemplateVM, self).clone_disk_files(src)
# Create root-cow.img
self.commit_changes()
def commit_changes(self):
'''Commit changes to template'''
self.log.debug('commit_changes()')

View File

@ -247,6 +247,7 @@ fi
%{python_sitelib}/qubes/tools/qvm_block.py*
%{python_sitelib}/qubes/tools/qvm_create.py*
%{python_sitelib}/qubes/tools/qvm_features.py*
%{python_sitelib}/qubes/tools/qvm_clone.py*
%{python_sitelib}/qubes/tools/qvm_kill.py*
%{python_sitelib}/qubes/tools/qvm_ls.py*
%{python_sitelib}/qubes/tools/qvm_pause.py*