Merge remote-tracking branch 'origin/pull/39/head' into core3-devel
This commit is contained in:
commit
36e5bcd766
@ -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
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
------------------------
|
------------------------
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
@ -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)
|
||||||
|
@ -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 {}
|
||||||
|
@ -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
|
|
||||||
|
@ -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)]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
@ -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(
|
||||||
195*1024*1024-4096*3)
|
os.path.join(hvmtemplate.dir_path, '00file'),
|
||||||
self.fill_image(hvmtemplate.private_img, 195*1024*1024-4096*3)
|
195 * 1024 * 1024 - 4096 * 3)
|
||||||
self.fill_image(hvmtemplate.root_img, 1024*1024*1024, sparse=True)
|
self.fill_image(hvmtemplate.volumes['private'].path,
|
||||||
|
195 * 1024 * 1024 - 4096 * 3)
|
||||||
|
self.fill_image(hvmtemplate.volumes['root'].path, 1024 * 1024 * 1024,
|
||||||
|
sparse=True)
|
||||||
vms.append(hvmtemplate)
|
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:
|
||||||
|
@ -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()
|
||||||
@ -349,7 +349,7 @@ class TC_02_QvmPrefs(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
|||||||
('invalid', '', False),
|
('invalid', '', False),
|
||||||
('[invalid]', '', False),
|
('[invalid]', '', False),
|
||||||
# TODO:
|
# TODO:
|
||||||
#('["12:12.0"]', '', False)
|
# ('["12:12.0"]', '', False)
|
||||||
])
|
])
|
||||||
|
|
||||||
@unittest.skip('test not converted to core3 API')
|
@unittest.skip('test not converted to core3 API')
|
||||||
@ -368,6 +368,7 @@ class TC_02_QvmPrefs(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
|||||||
|
|
||||||
class TC_03_QvmRevertTemplateChanges(qubes.tests.SystemTestsMixin,
|
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
|
||||||
|
@ -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()
|
||||||
|
@ -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))
|
||||||
|
@ -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
73
qubes/tools/qvm_clone.py
Normal 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())
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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()')
|
||||||
|
@ -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*
|
||||||
|
Loading…
Reference in New Issue
Block a user