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 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 Authors
------- -------

View File

@ -1,30 +1,39 @@
.. program:: qvm-clone .. program:: qvm-clone
===========================================================================
:program:`qvm-clone` -- Clones an existing VM by copying all its disk files :program:`qvm-clone` -- Clones an existing VM by copying all its disk files
=========================================================================== ===========================================================================
Synopsis Synopsis
======== --------
:command:`qvm-clone` [*options*] <*src-name*> <*new-name*> :command:`qvm-clone` [-h] [--verbose] [--quiet] [-p *POOL:VOLUME* | -P POOL] *VMNAME* *NEWVM*
Options Options
======= -------
.. option:: --help, -h .. option:: --help, -h
Show this help message and exit 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 .. option:: --quiet, -q
Be quiet Be quiet
.. option:: --path=DIR_PATH, -p DIR_PATH .. option:: --verbose, -v
Specify path to the template directory Increase verbosity
Authors Authors
======= -------
| Joanna Rutkowska <joanna at invisiblethingslab dot com> | Joanna Rutkowska <joanna at invisiblethingslab dot com>
| Rafal Wojtczuk <rafal at invisiblethingslab dot com> | Rafal Wojtczuk <rafal at invisiblethingslab dot com>
| Marek Marczykowski <marmarek 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 The new domain class name (default: **AppVM** for
:py:class:`qubes.vm.appvm.AppVM`). :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 Set domain's property, like "internal", "memory" or "vcpus". Any property may
be set this way, even "qid". be set this way, even "qid".
@ -57,9 +57,14 @@ Options
Use provided :file:`root.img` instead of default/empty one (file will be Use provided :file:`root.img` instead of default/empty one (file will be
*moved*). This option is mutually exclusive with :option:`--root-copy-from`. *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 Options for internal use
------------------------ ------------------------

View File

@ -416,7 +416,6 @@ class VMCollection(object):
return value return value
def __getitem__(self, key): def __getitem__(self, key):
if isinstance(key, int): if isinstance(key, int):
return self._dict[key] return self._dict[key]
@ -859,7 +858,6 @@ class Qubes(qubes.PropertyHolder):
'no such VM class: {!r}'.format(clsname)) 'no such VM class: {!r}'.format(clsname))
# don't catch TypeError # don't catch TypeError
def add_new_vm(self, cls, qid=None, **kwargs): def add_new_vm(self, cls, qid=None, **kwargs):
'''Add new Virtual Machine to colletion '''Add new Virtual Machine to colletion
@ -872,10 +870,11 @@ class Qubes(qubes.PropertyHolder):
# override it with default template) # override it with default template)
if 'template' not in kwargs and hasattr(cls, 'template'): if 'template' not in kwargs and hasattr(cls, 'template'):
kwargs['template'] = self.default_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)) return self.domains.add(cls(self, None, qid=qid, **kwargs))
def get_label(self, label): def get_label(self, label):
'''Get label as identified by index or name '''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 # TODO this is file pool specific. Change it to a more general
# solution # solution
if vm.volumes['private'] is not None: 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(path_to_private_img, subdir))
vm_files.append(self.FileToBackup(vm.icon_path, subdir)) vm_files.append(self.FileToBackup(vm.icon_path, subdir))
@ -358,7 +358,7 @@ class Backup(object):
if vm.updateable: if vm.updateable:
# TODO this is file pool specific. Change it to a more general # TODO this is file pool specific. Change it to a more general
# solution # 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)) vm_files.append(self.FileToBackup(path_to_root_img, subdir))
files_to_backup[vm.qid] = self.VMToBackup(vm, vm_files, 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) # wait for other processes (if any)
for proc in self.processes_to_kill_on_cancel: for proc in self.processes_to_kill_on_cancel:
proc.wait() proc.wait()
if proc.returncode != 0:
if vmproc.returncode != 0:
raise qubes.exc.QubesException( raise qubes.exc.QubesException(
"Backup completed, but VM receiving it reported an error " "Backup completed, but VM receiving it reported an error "
"(exit code {})".format(vmproc.returncode)) "(exit code {})".format(proc.returncode))
if filename and filename != "EOF": if filename and filename != "EOF":
raise qubes.exc.QubesException( raise qubes.exc.QubesException(
@ -2178,7 +2177,7 @@ class BackupRestore(object):
vm.features['backup-path']), vm.features['backup-path']),
new_vm.dir_path) new_vm.dir_path)
new_vm.verify_files() new_vm.storage.verify()
except Exception as err: except Exception as err:
self.log.error("ERROR: {0}".format(err)) self.log.error("ERROR: {0}".format(err))
self.log.warning("*** Skipping VM: {0}".format(vm.name)) self.log.warning("*** Skipping VM: {0}".format(vm.name))

View File

@ -29,7 +29,9 @@ from __future__ import absolute_import
import os import os
import os.path import os.path
import string import string # pylint: disable=deprecated-module
import time
from datetime import datetime
import lxml.etree import lxml.etree
import pkg_resources import pkg_resources
@ -49,39 +51,75 @@ class StoragePoolException(qubes.exc.QubesException):
class Volume(object): class Volume(object):
''' Encapsulates all data about a volume for serialization to qubes.xml and ''' Encapsulates all data about a volume for serialization to qubes.xml and
libvirt config. 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' devtype = 'disk'
domain = None domain = None
path = None path = None
rw = True
script = None script = None
usage = 0 usage = 0
def __init__(self, name, pool, volume_type, vid=None, size=0, def __init__(self, name, pool, vid, internal=False, removable=False,
removable=False, internal=False, **kwargs): 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) super(Volume, self).__init__(**kwargs)
self.name = str(name) self.name = str(name)
self.pool = str(pool) self.pool = str(pool)
self.vid = vid
self.size = size
self.volume_type = volume_type
self.removable = removable
self.internal = internal 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): def __eq__(self, other):
return lxml.etree.Element('volume', **self.config) return other.pool == self.pool and other.vid == self.vid
@property def __hash__(self):
def config(self): return hash('%s:%s' % (self.pool, self.vid))
''' return config data for serialization to qubes.xml '''
return {'name': self.name, def __neq__(self, other):
'pool': self.pool, return not self.__eq__(other)
'volume_type': self.volume_type}
def __repr__(self): def __repr__(self):
return '{!r}'.format(self.pool + ':' + self.vid) 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): def block_device(self):
''' Return :py:class:`qubes.devices.BlockDevice` for serialization in ''' Return :py:class:`qubes.devices.BlockDevice` for serialization in
the libvirt XML template as <disk>. the libvirt XML template as <disk>.
@ -89,18 +127,42 @@ class Volume(object):
return qubes.devices.BlockDevice(self.path, self.name, self.script, 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): @property
return other.pool == self.pool and other.vid == self.vid \ def revisions(self):
and other.volume_type == self.volume_type ''' Returns a `dict` containing revision identifiers and paths '''
msg = "{!s} has revisions not implemented".format(self.__class__)
raise NotImplementedError(msg)
def __neq__(self, other): @property
return not self.__eq__(other) def config(self):
''' return config data for serialization to qubes.xml '''
result = {'name': self.name, 'pool': self.pool, 'vid': self.vid, }
def __hash__(self): if self.internal:
return hash('%s:%s %s' % (self.pool, self.vid, self.volume_type)) result['internal'] = self.internal
def __str__(self): if self.removable:
return "{!s}:{!s}".format(self.pool, self.vid) 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): class Storage(object):
@ -119,14 +181,59 @@ class Storage(object):
#: Additional drive (currently used only by HVM) #: Additional drive (currently used only by HVM)
self.drive = None self.drive = None
self.pools = {} self.pools = {}
if hasattr(vm, 'volume_config'): if hasattr(vm, 'volume_config'):
for name, conf in self.vm.volume_config.items(): for name, conf in self.vm.volume_config.items():
assert 'pool' in conf, "Pool missing in volume_config" % str( assert 'pool' in conf, "Pool missing in volume_config" % str(
conf) conf)
if 'volume_type' in conf:
conf = self._migrate_config(conf)
pool = self.vm.app.get_pool(conf['pool']) pool = self.vm.app.get_pool(conf['pool'])
self.vm.volumes[name] = pool.init_volume(self.vm, conf) self.vm.volumes[name] = pool.init_volume(self.vm, conf)
self.pools[name] = pool 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): def attach(self, volume, rw=False):
''' Attach a volume to the domain ''' ''' Attach a volume to the domain '''
assert self.vm.is_running() assert self.vm.is_running()
@ -187,7 +294,7 @@ class Storage(object):
If :py:attr:`self.vm.kernel` is :py:obj:`None`, the this points inside If :py:attr:`self.vm.kernel` is :py:obj:`None`, the this points inside
:py:attr:`self.vm.dir_path` :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 return self.vm.volumes['kernel'].kernels_dir
def get_disk_utilization(self): def get_disk_utilization(self):
@ -198,38 +305,56 @@ class Storage(object):
return result return result
def resize(self, volume, size): def resize(self, volume, size):
''' Resize volume ''' ''' Resizes volume a read-writable volume '''
self.get_pool(volume).resize(volume, size) self.get_pool(volume).resize(volume, size)
def create(self, source_template=None): def create(self):
''' Creates volumes on disk ''' ''' Creates volumes on disk '''
if source_template is None and hasattr(self.vm, 'template'):
source_template = self.vm.template
old_umask = os.umask(002) old_umask = os.umask(002)
for name, volume in self.vm.volumes.items(): for volume in self.vm.volumes.values():
source_volume = None self.get_pool(volume).create(volume)
if source_template and hasattr(source_template, 'volumes'):
source_volume = source_template.volumes[name]
self.get_pool(volume).create(volume, source_volume=source_volume)
os.umask(old_umask) os.umask(old_umask)
def clone(self, src_vm): def clone(self, src_vm):
''' Clone volumes from the specified 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): src_path = src_vm.dir_path
self.log.info('Creating directory: {0}'.format(self.vm.dir_path)) msg = "Source path {!s} does not exist".format(src_path)
os.makedirs(self.vm.dir_path) assert os.path.exists(src_path), msg
for name, target in self.vm.volumes.items():
pool = self.get_pool(target) dst_path = self.vm.dir_path
source = src_vm.volumes[name] msg = "Destination {!s} already exists".format(dst_path)
volume = pool.clone(source, target) assert not os.path.exists(dst_path), msg
assert volume, "%s.clone() returned '%s'" % (pool.__class__, os.mkdir(dst_path)
volume)
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 self.vm.volumes[name] = volume
msg = "Cloning directory: {!s} to {!s}"
msg = msg.format(src_path, dst_path)
self.log.info(msg)
@property @property
def outdated_volumes(self): def outdated_volumes(self):
''' Returns a list of outdated volumes ''' ''' Returns a list of outdated volumes '''
@ -248,11 +373,15 @@ class Storage(object):
def rename(self, old_name, new_name): def rename(self, old_name, new_name):
''' Notify the pools that the domain was renamed ''' ''' Notify the pools that the domain was renamed '''
volumes = self.vm.volumes 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(): for name, volume in volumes.items():
pool = self.get_pool(volume) pool = self.get_pool(volume)
volumes[name] = pool.rename(volume, old_name, new_name) volumes[name] = pool.rename(volume, old_name, new_name)
def verify_files(self): def verify(self):
'''Verify that the storage is sane. '''Verify that the storage is sane.
On success, returns normally. On failure, raises exception. On success, returns normally. On failure, raises exception.
@ -264,6 +393,7 @@ class Storage(object):
for volume in self.vm.volumes.values(): for volume in self.vm.volumes.values():
self.get_pool(volume).verify(volume) self.get_pool(volume).verify(volume)
self.vm.fire_event('domain-verify-files') self.vm.fire_event('domain-verify-files')
return True
def remove(self): def remove(self):
''' Remove all the volumes. ''' Remove all the volumes.
@ -280,7 +410,8 @@ class Storage(object):
def start(self): def start(self):
''' Execute the start method on each pool ''' ''' Execute the start method on each pool '''
for volume in self.vm.volumes.values(): 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): def stop(self):
''' Execute the start method on each pool ''' ''' Execute the start method on each pool '''
@ -289,8 +420,12 @@ class Storage(object):
def get_pool(self, volume): def get_pool(self, volume):
''' Helper function ''' ''' Helper function '''
assert isinstance(volume, Volume), "You need to pass a Volume" 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] return self.pools[volume.name]
else:
return self.vm.app.pools[volume]
def commit_template_changes(self): def commit_template_changes(self):
''' Makes changes to an 'origin' volume persistent ''' ''' Makes changes to an 'origin' volume persistent '''
@ -320,109 +455,168 @@ class Pool(object):
3rd Parties providing own storage implementations will need to extend 3rd Parties providing own storage implementations will need to extend
this class. this class.
''' ''' # pylint: disable=unused-argument
private_img_size = qubes.config.defaults['private_img_size'] private_img_size = qubes.config.defaults['private_img_size']
root_img_size = qubes.config.defaults['root_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): def __eq__(self, other):
return self.name == other.name return self.name == other.name
def __neq__(self, other): def __neq__(self, other):
return not self.__eq__(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): def __str__(self):
return self.name return self.name
def __xml__(self): 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 ''' Create the given volume on disk or copy from provided
`source_volume`. `source_volume`.
''' '''
raise NotImplementedError("Pool %s has create() not implemented" % raise self._not_implemented("create")
self.name)
def commit_template_changes(self, volume): def commit(self, volume): # pylint: disable=no-self-use
''' Update origin device ''' ''' Write the snapshot to disk '''
raise NotImplementedError( msg = "Got volume_type {!s} when expected 'snap'"
"Pool %s has commit_template_changes() not implemented" % msg = msg.format(volume.volume_type)
self.name) assert volume.volume_type == 'snap', msg
@property @property
def config(self): def config(self):
''' Returns the pool config to be written to qubes.xml ''' ''' Returns the pool config to be written to qubes.xml '''
raise NotImplementedError("Pool %s has config() not implemented" % raise self._not_implemented("config")
self.name)
def clone(self, source, target): def clone(self, source, target):
''' Clone volume ''' ''' Clone volume '''
raise NotImplementedError("Pool %s has clone() not implemented" % raise self._not_implemented("clone")
self.name)
def destroy(self): def destroy(self):
''' Called when removing the pool. Use this for implementation specific ''' Called when removing the pool. Use this for implementation specific
clean up. clean up.
''' '''
raise NotImplementedError("Pool %s has destroy() not implemented" % raise self._not_implemented("destroy")
self.name)
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): def is_outdated(self, volume):
raise NotImplementedError("Pool %s has is_outdated() not implemented" % ''' Returns `True` if the currently used `volume.source` of a snapshot
self.name) volume is outdated.
'''
raise self._not_implemented("is_outdated")
def recover(self, volume):
''' Try to recover a :py:class:`Volume` or :py:class:`SnapVolume` '''
raise self._not_implemented("recover")
def remove(self, volume): def remove(self, volume):
''' Remove volume''' ''' Remove volume'''
raise NotImplementedError("Pool %s has remove() not implemented" % raise self._not_implemented("remove")
self.name)
def rename(self, volume, old_name, new_name): def rename(self, volume, old_name, new_name):
''' Called when the domain changes its name ''' ''' Called when the domain changes its name '''
raise NotImplementedError("Pool %s has rename() not implemented" % raise self._not_implemented("rename")
self.name)
def start(self, volume): def reset(self, volume):
''' Do what ever is needed on start ''' ''' Drop and recreate volume without copying it's content from source.
raise NotImplementedError("Pool %s has start() not implemented" % '''
self.name) raise self._not_implemented("reset")
def revert(self, volume, revision=None):
''' Revert volume to previous revision '''
raise self._not_implemented("revert")
def setup(self): def setup(self):
''' Called when adding a pool to the system. Use this for implementation ''' Called when adding a pool to the system. Use this for implementation
specific set up. specific set up.
''' '''
raise NotImplementedError("Pool %s has setup() not implemented" % raise self._not_implemented("setup")
self.name)
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''' ''' 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): def verify(self, volume):
''' Verifies the volume. ''' ''' Verifies the volume. '''
raise NotImplementedError("Pool %s has verify() not implemented" % raise self._not_implemented("verify")
self.name)
@property @property
def volumes(self): def volumes(self):
''' Return a list of volumes managed by this pool ''' ''' Return a list of volumes managed by this pool '''
raise NotImplementedError("Pool %s has volumes() not implemented" % raise self._not_implemented("volumes")
self.name)
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(): def pool_drivers():
""" Return a list of EntryPoints names """ """ Return a list of EntryPoints names """
return [ep.name return [ep.name
for ep in pkg_resources.iter_entry_points(STORAGE_ENTRY_POINT)] 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 ''' ''' Manages block devices in a domain '''
import string import string # pylint: disable=deprecated-module
from qubes.storage import Pool, Volume from qubes.storage import Pool, Volume
@ -99,15 +99,12 @@ class DomainPool(Pool):
class DomainVolume(Volume): class DomainVolume(Volume):
''' A volume provided by a block device in an domain ''' ''' A volume provided by a block device in an domain '''
def __init__(self, name, pool, desc, mode, size): def __init__(self, name, pool, desc, mode, **kwargs):
if mode == 'w': rw = (mode == 'w')
volume_type = 'read-write'
else:
volume_type = 'read-only'
super(DomainVolume, self).__init__(desc, super(DomainVolume, self).__init__(desc, pool, vid=name, removable=True,
pool, rw=rw, **kwargs)
volume_type,
vid=name, @property
size=size, def revisions(self):
removable=True) return {}

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

@ -33,12 +33,16 @@ class LinuxModules(Volume):
def __init__(self, target_dir, kernel_version, **kwargs): def __init__(self, target_dir, kernel_version, **kwargs):
kwargs['vid'] = kernel_version kwargs['vid'] = kernel_version
kwargs['source'] = self
super(LinuxModules, self).__init__(**kwargs) super(LinuxModules, self).__init__(**kwargs)
self.kernels_dir = os.path.join(target_dir, kernel_version) self.kernels_dir = os.path.join(target_dir, kernel_version)
self.path = os.path.join(self.kernels_dir, 'modules.img') self.path = os.path.join(self.kernels_dir, 'modules.img')
self.vmlinuz = os.path.join(self.kernels_dir, 'vmlinuz') self.vmlinuz = os.path.join(self.kernels_dir, 'vmlinuz')
self.initramfs = os.path.join(self.kernels_dir, 'initramfs') self.initramfs = os.path.join(self.kernels_dir, 'initramfs')
@property
def revisions(self):
return {}
class LinuxKernel(Pool): class LinuxKernel(Pool):
''' Provides linux kernels ''' ''' Provides linux kernels '''
@ -50,23 +54,22 @@ class LinuxKernel(Pool):
self.dir_path = dir_path self.dir_path = dir_path
def init_volume(self, vm, volume_config): def init_volume(self, vm, volume_config):
assert 'volume_type' in volume_config, "Volume type missing " \ assert not volume_config['rw']
+ str(volume_config)
volume_type = volume_config['volume_type']
if volume_type != 'read-only':
raise StoragePoolException("Unknown volume type " + volume_type)
volume = LinuxModules(self.dir_path, vm.kernel, **volume_config) volume = LinuxModules(self.dir_path, vm.kernel, **volume_config)
return volume return volume
def is_dirty(self, volume):
return False
def clone(self, source, target): def clone(self, source, target):
return target return target
def create(self, volume, source_volume=None): def create(self, volume):
return volume return volume
def commit_template_changes(self, volume): def commit(self, volume):
return volume return volume
@property @property
@ -80,6 +83,12 @@ class LinuxKernel(Pool):
def destroy(self): def destroy(self):
pass pass
def export(self, volume):
return volume.path
def import_volume(self, dst_pool, dst_volume, src_pool, src_volume):
pass
def is_outdated(self, volume): def is_outdated(self, volume):
return False return False
@ -115,7 +124,8 @@ class LinuxKernel(Pool):
pool=self.name, pool=self.name,
name=kernel_version, name=kernel_version,
internal=True, internal=True,
volume_type='read-only') rw=False
)
for kernel_version in os.listdir(self.dir_path)] 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') name=vmname, template=template, provides_network=True, label='red')
testnet.create_on_disk() testnet.create_on_disk()
vms.append(testnet) 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') vmname = self.make_vm_name('test1')
if self.verbose: if self.verbose:
@ -831,7 +831,7 @@ class BackupTestsMixin(SystemTestsMixin):
testvm1.netvm = testnet testvm1.netvm = testnet
testvm1.create_on_disk() testvm1.create_on_disk()
vms.append(testvm1) 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') vmname = self.make_vm_name('testhvm1')
if self.verbose: if self.verbose:
@ -841,7 +841,7 @@ class BackupTestsMixin(SystemTestsMixin):
hvm=True, hvm=True,
label='red') label='red')
testvm2.create_on_disk() 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) vms.append(testvm2)
vmname = self.make_vm_name('template') vmname = self.make_vm_name('template')
@ -850,7 +850,7 @@ class BackupTestsMixin(SystemTestsMixin):
testvm3 = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM, testvm3 = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM,
name=vmname, label='red') name=vmname, label='red')
testvm3.create_on_disk() 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) vms.append(testvm3)
vmname = self.make_vm_name('custom') vmname = self.make_vm_name('custom')

View File

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

View File

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

View File

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

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))

View File

@ -45,19 +45,21 @@ def prepare_table(vd_list, full=False):
''' '''
output = [] output = []
if sys.stdout.isatty(): if sys.stdout.isatty():
output += [('POOL_NAME:VOLUME_ID', 'VOLUME_TYPE', 'VMNAME')] output += [('POOL:VOLUME', 'VMNAME', 'VOLUME_NAME')] # NOQA
for volume in vd_list: for volume in vd_list:
if volume.domains: if volume.domains:
vmname = volume.domains.pop() vmname, volume_name = volume.domains.pop()
output += [(str(volume), volume.volume_type, vmname)] output += [(str(volume), vmname, volume_name, volume.revisions)]
for vmname in volume.domains: for tupple in volume.domains:
vmname, volume_name = tupple
if full or not sys.stdout.isatty(): if full or not sys.stdout.isatty():
output += [(str(volume), volume.volume_type, vmname)] output += [(str(volume), vmname, volume_name,
volume.revisions)]
else: else:
output += [('', '', vmname)] output += [('', vmname, volume_name, '', volume.revisions)]
else: else:
output += [(str(volume), volume.volume_type)] output += [(str(volume), "")]
return output return output
@ -70,8 +72,11 @@ class VolumeData(object):
def __init__(self, volume): def __init__(self, volume):
self.name = volume.name self.name = volume.name
self.pool = volume.pool self.pool = volume.pool
self.volume_type = volume.volume_type
self.vid = volume.vid self.vid = volume.vid
if volume.revisions != {}:
self.revisions = 'Yes'
else:
self.revisions = 'No'
self.domains = [] self.domains = []
def __str__(self): def __str__(self):
@ -110,7 +115,7 @@ def list_volumes(args):
for volume in domain.attached_volumes: for volume in domain.attached_volumes:
try: try:
volume_data = vd_dict[volume.pool][volume.vid] volume_data = vd_dict[volume.pool][volume.vid]
volume_data.domains += [domain.name] volume_data.domains += [(domain.name, volume.name)]
except KeyError: except KeyError:
# Skipping volume # Skipping volume
continue continue
@ -126,6 +131,15 @@ def list_volumes(args):
result = [x for p in vd_dict.itervalues() for x in p.itervalues()] result = [x for p in vd_dict.itervalues() for x in p.itervalues()]
qubes.tools.print_table(prepare_table(result, full=args.full)) 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): def attach_volumes(args):
''' Called by the parser to execute the :program:`qvm-block attach` ''' 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._mutually_exclusive_groups.append(vm_name_group)
list_parser.set_defaults(func=list_volumes) 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(): def get_parser():
'''Create :py:class:`argparse.ArgumentParser` suitable for '''Create :py:class:`argparse.ArgumentParser` suitable for
@ -185,6 +207,7 @@ def get_parser():
description="For more information see qvm-block command -h", description="For more information see qvm-block command -h",
dest='command') dest='command')
init_list_parser(sub_parsers) init_list_parser(sub_parsers)
init_revert_parser(sub_parsers)
attach_parser = sub_parsers.add_parser( attach_parser = sub_parsers.add_parser(
'attach', help="Attach volume to domain", aliases=('at', 'a')) 'attach', help="Attach volume to domain", aliases=('at', 'a'))
attach_parser.add_argument('--ro', help='attach device read-only', 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', default='AppVM',
help='specify the class of the new domain (default: %(default)s)') 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, action=qubes.tools.PropertyAction,
help='set domain\'s property, like "internal", "memory" or "vcpus"') help='set domain\'s property, like "internal", "memory" or "vcpus"')
parser.add_argument('--pool', '-P', parser.add_argument('--pool', '-p',
action='append', action='append',
metavar='POOL_NAME:VOLUME_NAME', metavar='POOL_NAME:VOLUME_NAME',
help='specify the pool to use for a volume') 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', parser.add_argument('--template', '-t',
action=qubes.tools.SinglePropertyAction, action=qubes.tools.SinglePropertyAction,
help='specify the TemplateVM to use') help='specify the TemplateVM to use')
@ -80,16 +86,18 @@ parser.add_argument('name', metavar='VMNAME',
def main(args=None): def main(args=None):
args = parser.parse_args(args) args = parser.parse_args(args)
if args.pool: pools = {}
args.properties['volume_config'] = {} pool = None
if hasattr(args, 'pools') and args.pools:
for pool_vol in args.pool: for pool_vol in args.pool:
try: try:
pool_name, volume_name = pool_vol.split(':') pool_name, volume_name = pool_vol.split(':')
config = {'pool': pool_name, 'name': volume_name} pools[volume_name] = pool_name
args.properties['volume_config'][volume_name] = config
except ValueError: except ValueError:
parser.error( parser.error(
'Pool argument must be of form: -P pool_name:volume_name') '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: if 'label' not in args.properties:
parser.error('--label option is mandatory') parser.error('--label option is mandatory')
@ -144,7 +152,7 @@ def main(args=None):
if not args.no_root: if not args.no_root:
try: try:
vm.create_on_disk() vm.create_on_disk(pool, pools)
# TODO this is file pool specific. Change it to a more general # TODO this is file pool specific. Change it to a more general
# solution # solution

View File

@ -21,12 +21,12 @@
# 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 the AppVM implementation ''' ''' This module contains the AppVM implementation '''
import copy
import qubes.events import qubes.events
import qubes.vm.qubesvm import qubes.vm.qubesvm
from qubes.config import defaults from qubes.config import defaults
@ -39,36 +39,75 @@ class AppVM(qubes.vm.qubesvm.QubesVM):
ls_width=31, ls_width=31,
doc='Template, on which this AppVM is based.') doc='Template, on which this AppVM is based.')
def __init__(self, *args, **kwargs): def __init__(self, app, xml, template=None, **kwargs):
self.volume_config = { self.volume_config = {
'root': { 'root': {
'name': 'root', 'name': 'root',
'pool': 'default', 'pool': 'default',
'volume_type': 'snapshot', 'snap_on_start': True,
'save_on_stop': False,
'rw': False,
'internal': True 'internal': True
}, },
'private': { 'private': {
'name': 'private', 'name': 'private',
'pool': 'default', 'pool': 'default',
'volume_type': 'origin', 'snap_on_start': False,
'save_on_stop': True,
'rw': True,
'source': None,
'size': defaults['private_img_size'], 'size': defaults['private_img_size'],
'internal': True 'internal': True
}, },
'volatile': { 'volatile': {
'name': 'volatile', 'name': 'volatile',
'pool': 'default', 'pool': 'default',
'volume_type': 'volatile',
'size': defaults['root_img_size'], 'size': defaults['root_img_size'],
'internal': True 'internal': True,
'rw': True,
}, },
'kernel': { 'kernel': {
'name': 'kernel', 'name': 'kernel',
'pool': 'linux-kernel', 'pool': 'linux-kernel',
'volume_type': 'read-only', 'snap_on_start': True,
'rw': False,
'internal': True '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') @qubes.events.handler('domain-load')
def on_domain_loaded(self, event): def on_domain_loaded(self, event):

View File

@ -46,24 +46,32 @@ class DispVM(qubes.vm.qubesvm.QubesVM):
'root': { 'root': {
'name': 'root', 'name': 'root',
'pool': 'default', 'pool': 'default',
'volume_type': 'snapshot', 'snap_on_start': True,
'save_on_stop': False,
'rw': False,
'internal': True
}, },
'private': { 'private': {
'name': 'private', 'name': 'private',
'pool': 'default', 'pool': 'default',
'volume_type': 'snapshot', 'snap_on_start': True,
'save_on_stop': False,
'internal': True,
'rw': True,
}, },
'volatile': { 'volatile': {
'name': 'volatile', 'name': 'volatile',
'pool': 'default', 'pool': 'default',
'volume_type': 'volatile', 'internal': True,
'size': qubes.config.defaults['root_img_size'] + 'size': qubes.config.defaults['root_img_size'] +
qubes.config.defaults['private_img_size'], qubes.config.defaults['private_img_size'],
}, },
'kernel': { 'kernel': {
'name': 'kernel', 'name': 'kernel',
'pool': 'linux-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 from __future__ import absolute_import
import copy
import base64 import base64
import datetime import datetime
import itertools import itertools
@ -421,10 +422,15 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
name = node.get('name') name = node.get('name')
assert name assert name
for key, value in node.items(): for key, value in node.items():
# pylint: disable=no-member
if value == 'True':
self.volume_config[name][key] = True
else:
self.volume_config[name][key] = value self.volume_config[name][key] = value
for name, conf in volume_config.items(): for name, conf in volume_config.items():
for key, value in conf.items(): for key, value in conf.items():
# pylint: disable=no-member
self.volume_config[name][key] = value self.volume_config[name][key] = value
elif volume_config: elif volume_config:
@ -542,6 +548,8 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
# pylint: disable=unused-argument # pylint: disable=unused-argument
self.init_log() self.init_log()
self.storage.rename(old_name, new_name)
if self._libvirt_domain is not None: if self._libvirt_domain is not None:
self.libvirt_domain.undefine() self.libvirt_domain.undefine()
self._libvirt_domain = None 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.close()
self._qdb_connection = None self._qdb_connection = None
self.storage.rename(old_name, new_name)
self._update_libvirt_domain() self._update_libvirt_domain()
if self.autostart: 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, self.fire_event_pre('domain-pre-start', preparing_dvm=preparing_dvm,
start_guid=start_guid, mem_required=mem_required) start_guid=start_guid, mem_required=mem_required)
self.storage.verify_files() self.storage.verify()
if self.netvm is not None: if self.netvm is not None:
# pylint: disable = no-member # 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) user="root", passio_popen=True, gui=False, wait=True)
p.communicate(input=self.default_user) 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. '''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)) self.log.info('Creating directory: {0}'.format(self.dir_path))
os.makedirs(self.dir_path, mode=0o775) 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.log.info('Creating icon symlink: {} -> {}'.format(
self.icon_path, self.label.icon_path)) 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) shutil.copy(self.label.icon_path, self.icon_path)
# fire hooks # fire hooks
self.fire_event('domain-create-on-disk', source_template) self.fire_event('domain-create-on-disk')
def remove_from_disk(self): def remove_from_disk(self):
'''Remove domain remnants from disk.''' '''Remove domain remnants from disk.'''
self.fire_event('domain-remove-from-disk') self.fire_event('domain-remove-from-disk')
self.storage.remove()
shutil.rmtree(self.dir_path) 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. '''Clone files from other vm.
:param qubes.vm.qubesvm.QubesVM src: source 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( raise qubes.exc.QubesVMNotHaltedError(
self, 'Cannot clone a running domain {!r}'.format(self.name)) 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 # 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 = qubes.storage.Storage(self)
self.storage.clone(src) self.storage.clone(src)
self.storage.verify()
assert self.volumes != {}
if src.icon_path is not None \ if src.icon_path is not None \
and os.path.exists(src.dir_path) \ 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 domain.memory_maximum = self.get_mem_static_max() * 1024
return qubes.qmemman.algo.prefmem(domain) / 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': { 'root': {
'name': 'root', 'name': 'root',
'pool': 'default', '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'], 'size': qubes.config.defaults['root_img_size'],
}, },
'private': { 'private': {
'name': 'private', 'name': 'private',
'pool': 'default', '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'], 'size': qubes.config.defaults['private_img_size'],
}, },
'volatile': { 'volatile': {
'name': 'volatile', 'name': 'volatile',
'pool': 'default', 'pool': 'default',
'volume_type': 'volatile', 'internal': True,
'size': qubes.config.defaults['root_img_size'], 'size': qubes.config.defaults['root_img_size'],
}, },
'kernel': { 'kernel': {
'name': 'kernel', 'name': 'kernel',
'pool': 'linux-kernel', 'pool': 'linux-kernel',
'volume_type': 'read-only', 'rw': False,
'internal': True
} }
} }
super(StandaloneVM, self).__init__(*args, **kwargs) super(StandaloneVM, self).__init__(*args, **kwargs)

View File

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

View File

@ -247,6 +247,7 @@ fi
%{python_sitelib}/qubes/tools/qvm_block.py* %{python_sitelib}/qubes/tools/qvm_block.py*
%{python_sitelib}/qubes/tools/qvm_create.py* %{python_sitelib}/qubes/tools/qvm_create.py*
%{python_sitelib}/qubes/tools/qvm_features.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_kill.py*
%{python_sitelib}/qubes/tools/qvm_ls.py* %{python_sitelib}/qubes/tools/qvm_ls.py*
%{python_sitelib}/qubes/tools/qvm_pause.py* %{python_sitelib}/qubes/tools/qvm_pause.py*