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
|
||||
|
||||
revert
|
||||
^^^^^^
|
||||
|
||||
| :command:`qvm-block revert` [-h] [--verbose] [--quiet] *POOL_NAME:VOLUME_ID*
|
||||
|
||||
Revert a volume to previous revision.
|
||||
|
||||
aliases: rv, r
|
||||
|
||||
Authors
|
||||
-------
|
||||
|
||||
|
@ -1,30 +1,39 @@
|
||||
.. program:: qvm-clone
|
||||
|
||||
===========================================================================
|
||||
:program:`qvm-clone` -- Clones an existing VM by copying all its disk files
|
||||
===========================================================================
|
||||
|
||||
Synopsis
|
||||
========
|
||||
:command:`qvm-clone` [*options*] <*src-name*> <*new-name*>
|
||||
--------
|
||||
:command:`qvm-clone` [-h] [--verbose] [--quiet] [-p *POOL:VOLUME* | -P POOL] *VMNAME* *NEWVM*
|
||||
|
||||
Options
|
||||
=======
|
||||
-------
|
||||
|
||||
.. option:: --help, -h
|
||||
|
||||
Show this help message and exit
|
||||
|
||||
.. option:: -P POOL
|
||||
|
||||
Pool to use for the new domain. All volumes besides snapshots volumes are
|
||||
imported in to the specified POOL. ~HIS IS WHAT YOU WANT TO USE NORMALLY.
|
||||
|
||||
.. option:: --pool=POOL:VOLUME, -p POOL:VOLUME
|
||||
|
||||
Specify the pool to use for the specific volume
|
||||
|
||||
.. option:: --quiet, -q
|
||||
|
||||
Be quiet
|
||||
|
||||
.. option:: --path=DIR_PATH, -p DIR_PATH
|
||||
.. option:: --verbose, -v
|
||||
|
||||
Specify path to the template directory
|
||||
Increase verbosity
|
||||
|
||||
Authors
|
||||
=======
|
||||
-------
|
||||
| Joanna Rutkowska <joanna at invisiblethingslab dot com>
|
||||
| Rafal Wojtczuk <rafal at invisiblethingslab dot com>
|
||||
| Marek Marczykowski <marmarek at invisiblethingslab dot com>
|
||||
| Bahtiar `kalkin-` Gadimov <bahtiar at gadimov dot de>
|
||||
|
@ -32,7 +32,7 @@ Options
|
||||
The new domain class name (default: **AppVM** for
|
||||
:py:class:`qubes.vm.appvm.AppVM`).
|
||||
|
||||
.. option:: --prop=NAME=VALUE, --property=NAME=VALUE, -p NAME=VALUE
|
||||
.. option:: --prop=NAME=VALUE, --property=NAME=VALUE
|
||||
|
||||
Set domain's property, like "internal", "memory" or "vcpus". Any property may
|
||||
be set this way, even "qid".
|
||||
@ -57,9 +57,14 @@ Options
|
||||
Use provided :file:`root.img` instead of default/empty one (file will be
|
||||
*moved*). This option is mutually exclusive with :option:`--root-copy-from`.
|
||||
|
||||
.. option:: --pool=POOL_NAME:VOLUME_NAME, -P POOL_NAME:VOLUME_NAME
|
||||
.. option:: -P POOL
|
||||
|
||||
Specify the pool to use for a volume
|
||||
Pool to use for the new domain. All volumes besides snapshots volumes are
|
||||
imported in to the specified POOL. ~HIS IS WHAT YOU WANT TO USE NORMALLY.
|
||||
|
||||
.. option:: --pool=POOL:VOLUME, -p POOL:VOLUME
|
||||
|
||||
Specify the pool to use for the specific volume
|
||||
|
||||
Options for internal use
|
||||
------------------------
|
||||
|
@ -416,7 +416,6 @@ class VMCollection(object):
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def __getitem__(self, key):
|
||||
if isinstance(key, int):
|
||||
return self._dict[key]
|
||||
@ -859,7 +858,6 @@ class Qubes(qubes.PropertyHolder):
|
||||
'no such VM class: {!r}'.format(clsname))
|
||||
# don't catch TypeError
|
||||
|
||||
|
||||
def add_new_vm(self, cls, qid=None, **kwargs):
|
||||
'''Add new Virtual Machine to colletion
|
||||
|
||||
@ -872,10 +870,11 @@ class Qubes(qubes.PropertyHolder):
|
||||
# override it with default template)
|
||||
if 'template' not in kwargs and hasattr(cls, 'template'):
|
||||
kwargs['template'] = self.default_template
|
||||
elif 'template' in kwargs and isinstance(kwargs['template'], str):
|
||||
kwargs['template'] = self.domains[kwargs['template']]
|
||||
|
||||
return self.domains.add(cls(self, None, qid=qid, **kwargs))
|
||||
|
||||
|
||||
def get_label(self, label):
|
||||
'''Get label as identified by index or name
|
||||
|
||||
|
@ -343,7 +343,7 @@ class Backup(object):
|
||||
# TODO this is file pool specific. Change it to a more general
|
||||
# solution
|
||||
if vm.volumes['private'] is not None:
|
||||
path_to_private_img = vm.volumes['private'].vid
|
||||
path_to_private_img = vm.volumes['private'].path
|
||||
vm_files.append(self.FileToBackup(path_to_private_img, subdir))
|
||||
|
||||
vm_files.append(self.FileToBackup(vm.icon_path, subdir))
|
||||
@ -358,7 +358,7 @@ class Backup(object):
|
||||
if vm.updateable:
|
||||
# TODO this is file pool specific. Change it to a more general
|
||||
# solution
|
||||
path_to_root_img = vm.volumes['root'].vid
|
||||
path_to_root_img = vm.volumes['root'].path
|
||||
vm_files.append(self.FileToBackup(path_to_root_img, subdir))
|
||||
files_to_backup[vm.qid] = self.VMToBackup(vm, vm_files, subdir)
|
||||
|
||||
@ -1725,11 +1725,10 @@ class BackupRestore(object):
|
||||
# wait for other processes (if any)
|
||||
for proc in self.processes_to_kill_on_cancel:
|
||||
proc.wait()
|
||||
|
||||
if vmproc.returncode != 0:
|
||||
raise qubes.exc.QubesException(
|
||||
"Backup completed, but VM receiving it reported an error "
|
||||
"(exit code {})".format(vmproc.returncode))
|
||||
if proc.returncode != 0:
|
||||
raise qubes.exc.QubesException(
|
||||
"Backup completed, but VM receiving it reported an error "
|
||||
"(exit code {})".format(proc.returncode))
|
||||
|
||||
if filename and filename != "EOF":
|
||||
raise qubes.exc.QubesException(
|
||||
@ -2178,7 +2177,7 @@ class BackupRestore(object):
|
||||
vm.features['backup-path']),
|
||||
new_vm.dir_path)
|
||||
|
||||
new_vm.verify_files()
|
||||
new_vm.storage.verify()
|
||||
except Exception as err:
|
||||
self.log.error("ERROR: {0}".format(err))
|
||||
self.log.warning("*** Skipping VM: {0}".format(vm.name))
|
||||
|
@ -29,7 +29,9 @@ from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import string
|
||||
import string # pylint: disable=deprecated-module
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
import lxml.etree
|
||||
import pkg_resources
|
||||
@ -49,58 +51,118 @@ class StoragePoolException(qubes.exc.QubesException):
|
||||
class Volume(object):
|
||||
''' Encapsulates all data about a volume for serialization to qubes.xml and
|
||||
libvirt config.
|
||||
|
||||
|
||||
Keep in mind!
|
||||
volatile = not snap_on_start and not save_on_stop
|
||||
snapshot = snap_on_start and not save_on_stop
|
||||
origin = not snap_on_start and save_on_stop
|
||||
origin_snapshot = snap_on_start and save_on_stop
|
||||
'''
|
||||
|
||||
devtype = 'disk'
|
||||
domain = None
|
||||
path = None
|
||||
rw = True
|
||||
script = None
|
||||
usage = 0
|
||||
|
||||
def __init__(self, name, pool, volume_type, vid=None, size=0,
|
||||
removable=False, internal=False, **kwargs):
|
||||
def __init__(self, name, pool, vid, internal=False, removable=False,
|
||||
revisions_to_keep=0, rw=False, save_on_stop=False, size=0,
|
||||
snap_on_start=False, source=None, **kwargs):
|
||||
''' Initialize a volume.
|
||||
|
||||
:param str name: The domain name
|
||||
:param str pool: The pool name
|
||||
:param str vid: Volume identifier needs to be unique in pool
|
||||
:param bool internal: If `True` volume is hidden when qvm-block ls
|
||||
is used
|
||||
:param bool removable: If `True` volume can be detached from vm at
|
||||
run time
|
||||
:param int revisions_to_keep: Amount of revisions to keep around
|
||||
:param bool rw: If true volume will be mounted read-write
|
||||
:param bool snap_on_start: Create a snapshot from source on start
|
||||
:param bool save_on_stop: Write changes to disk in vm.stop()
|
||||
:param str source: Vid of other volume in same pool
|
||||
:param str/int size: Size of the volume
|
||||
|
||||
'''
|
||||
|
||||
super(Volume, self).__init__(**kwargs)
|
||||
|
||||
self.name = str(name)
|
||||
self.pool = str(pool)
|
||||
self.vid = vid
|
||||
self.size = size
|
||||
self.volume_type = volume_type
|
||||
self.removable = removable
|
||||
self.internal = internal
|
||||
self.removable = removable
|
||||
self.revisions_to_keep = revisions_to_keep
|
||||
self.rw = rw
|
||||
self.save_on_stop = save_on_stop
|
||||
self.size = int(size)
|
||||
self.snap_on_start = snap_on_start
|
||||
self.source = source
|
||||
self.vid = vid
|
||||
|
||||
def __xml__(self):
|
||||
return lxml.etree.Element('volume', **self.config)
|
||||
def __eq__(self, other):
|
||||
return other.pool == self.pool and other.vid == self.vid
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
''' return config data for serialization to qubes.xml '''
|
||||
return {'name': self.name,
|
||||
'pool': self.pool,
|
||||
'volume_type': self.volume_type}
|
||||
def __hash__(self):
|
||||
return hash('%s:%s' % (self.pool, self.vid))
|
||||
|
||||
def __neq__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return '{!r}'.format(self.pool + ':' + self.vid)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.vid)
|
||||
|
||||
def __xml__(self):
|
||||
config = _sanitize_config(self.config)
|
||||
return lxml.etree.Element('volume', **config)
|
||||
|
||||
def block_device(self):
|
||||
''' Return :py:class:`qubes.devices.BlockDevice` for serialization in
|
||||
the libvirt XML template as <disk>.
|
||||
'''
|
||||
return qubes.devices.BlockDevice(self.path, self.name, self.script,
|
||||
self.rw, self.domain, self.devtype)
|
||||
self.rw, self.domain, self.devtype)
|
||||
|
||||
def __eq__(self, other):
|
||||
return other.pool == self.pool and other.vid == self.vid \
|
||||
and other.volume_type == self.volume_type
|
||||
@property
|
||||
def revisions(self):
|
||||
''' Returns a `dict` containing revision identifiers and paths '''
|
||||
msg = "{!s} has revisions not implemented".format(self.__class__)
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
def __neq__(self, other):
|
||||
return not self.__eq__(other)
|
||||
@property
|
||||
def config(self):
|
||||
''' return config data for serialization to qubes.xml '''
|
||||
result = {'name': self.name, 'pool': self.pool, 'vid': self.vid, }
|
||||
|
||||
def __hash__(self):
|
||||
return hash('%s:%s %s' % (self.pool, self.vid, self.volume_type))
|
||||
if self.internal:
|
||||
result['internal'] = self.internal
|
||||
|
||||
def __str__(self):
|
||||
return "{!s}:{!s}".format(self.pool, self.vid)
|
||||
if self.removable:
|
||||
result['removable'] = self.removable
|
||||
|
||||
if self.revisions_to_keep:
|
||||
result['revisions_to_keep'] = self.revisions_to_keep
|
||||
|
||||
if self.rw:
|
||||
result['rw'] = self.rw
|
||||
|
||||
if self.save_on_stop:
|
||||
result['save_on_stop'] = self.save_on_stop
|
||||
|
||||
if self.size:
|
||||
result['size'] = self.size
|
||||
|
||||
if self.snap_on_start:
|
||||
result['snap_on_start'] = self.snap_on_start
|
||||
|
||||
if self.source:
|
||||
result['source'] = self.source
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class Storage(object):
|
||||
@ -119,14 +181,59 @@ class Storage(object):
|
||||
#: Additional drive (currently used only by HVM)
|
||||
self.drive = None
|
||||
self.pools = {}
|
||||
|
||||
if hasattr(vm, 'volume_config'):
|
||||
for name, conf in self.vm.volume_config.items():
|
||||
assert 'pool' in conf, "Pool missing in volume_config" % str(
|
||||
conf)
|
||||
if 'volume_type' in conf:
|
||||
conf = self._migrate_config(conf)
|
||||
|
||||
pool = self.vm.app.get_pool(conf['pool'])
|
||||
self.vm.volumes[name] = pool.init_volume(self.vm, conf)
|
||||
self.pools[name] = pool
|
||||
|
||||
def _migrate_config(self, conf):
|
||||
''' Migrates from the old config style to new
|
||||
''' # FIXME: Remove this compatibility hack
|
||||
assert 'volume_type' in conf
|
||||
_type = conf['volume_type']
|
||||
old_volume_types = [
|
||||
'read-write', 'read-only', 'origin', 'snapshot', 'volatile'
|
||||
]
|
||||
msg = "Volume {!s} has unknown type {!s}".format(conf['name'], _type)
|
||||
assert conf['volume_type'] in old_volume_types, msg
|
||||
if _type == 'origin':
|
||||
conf['rw'] = True
|
||||
conf['source'] = None
|
||||
conf['save_on_stop'] = True
|
||||
conf['revisions_to_keep'] = 1
|
||||
elif _type == 'snapshot':
|
||||
conf['rw'] = False
|
||||
if conf['pool'] == 'default':
|
||||
template_vid = os.path.join('vm-templates',
|
||||
self.vm.template.name, conf['name'])
|
||||
elif conf['pool'] == 'qubes_dom0':
|
||||
template_vid = os.path.join(
|
||||
'qubes_dom0', self.vm.template.name + '-' + conf['name'])
|
||||
conf['source'] = template_vid
|
||||
conf['snap_on_start'] = True
|
||||
elif _type == 'read-write':
|
||||
conf['rw'] = True
|
||||
conf['save_on_stop'] = True
|
||||
conf['revisions_to_keep'] = 0
|
||||
elif _type == 'read-only':
|
||||
conf['rw'] = False
|
||||
conf['snap_on_start'] = True
|
||||
conf['save_on_stop'] = False
|
||||
conf['revisions_to_keep'] = 0
|
||||
elif _type == 'volatile':
|
||||
conf['snap_on_start'] = False
|
||||
conf['save_on_stop'] = False
|
||||
conf['revisions_to_keep'] = 0
|
||||
del conf['volume_type']
|
||||
return conf
|
||||
|
||||
def attach(self, volume, rw=False):
|
||||
''' Attach a volume to the domain '''
|
||||
assert self.vm.is_running()
|
||||
@ -187,7 +294,7 @@ class Storage(object):
|
||||
If :py:attr:`self.vm.kernel` is :py:obj:`None`, the this points inside
|
||||
:py:attr:`self.vm.dir_path`
|
||||
'''
|
||||
assert 'kernel' in self.vm.volumes, "VM has no kernel pool"
|
||||
assert 'kernel' in self.vm.volumes, "VM has no kernel volume"
|
||||
return self.vm.volumes['kernel'].kernels_dir
|
||||
|
||||
def get_disk_utilization(self):
|
||||
@ -198,37 +305,55 @@ class Storage(object):
|
||||
return result
|
||||
|
||||
def resize(self, volume, size):
|
||||
''' Resize volume '''
|
||||
''' Resizes volume a read-writable volume '''
|
||||
self.get_pool(volume).resize(volume, size)
|
||||
|
||||
def create(self, source_template=None):
|
||||
def create(self):
|
||||
''' Creates volumes on disk '''
|
||||
if source_template is None and hasattr(self.vm, 'template'):
|
||||
source_template = self.vm.template
|
||||
|
||||
old_umask = os.umask(002)
|
||||
|
||||
for name, volume in self.vm.volumes.items():
|
||||
source_volume = None
|
||||
if source_template and hasattr(source_template, 'volumes'):
|
||||
source_volume = source_template.volumes[name]
|
||||
self.get_pool(volume).create(volume, source_volume=source_volume)
|
||||
for volume in self.vm.volumes.values():
|
||||
self.get_pool(volume).create(volume)
|
||||
|
||||
os.umask(old_umask)
|
||||
|
||||
def clone(self, src_vm):
|
||||
''' Clone volumes from the specified vm '''
|
||||
self.vm.log.info('Creating directory: {0}'.format(self.vm.dir_path))
|
||||
if not os.path.exists(self.vm.dir_path):
|
||||
self.log.info('Creating directory: {0}'.format(self.vm.dir_path))
|
||||
os.makedirs(self.vm.dir_path)
|
||||
for name, target in self.vm.volumes.items():
|
||||
pool = self.get_pool(target)
|
||||
source = src_vm.volumes[name]
|
||||
volume = pool.clone(source, target)
|
||||
assert volume, "%s.clone() returned '%s'" % (pool.__class__,
|
||||
volume)
|
||||
self.vm.volumes[name] = volume
|
||||
|
||||
src_path = src_vm.dir_path
|
||||
msg = "Source path {!s} does not exist".format(src_path)
|
||||
assert os.path.exists(src_path), msg
|
||||
|
||||
dst_path = self.vm.dir_path
|
||||
msg = "Destination {!s} already exists".format(dst_path)
|
||||
assert not os.path.exists(dst_path), msg
|
||||
os.mkdir(dst_path)
|
||||
|
||||
self.vm.volumes = {}
|
||||
with VmCreationManager(self.vm):
|
||||
for name, config in self.vm.volume_config.items():
|
||||
dst_pool = self.get_pool(config['pool'])
|
||||
dst = dst_pool.init_volume(self.vm, config)
|
||||
src_volume = src_vm.volumes[name]
|
||||
src_pool = self.vm.app.get_pool(src_volume.pool)
|
||||
if dst_pool == src_pool:
|
||||
msg = "Cloning volume {!s} from vm {!s}"
|
||||
self.vm.log.info(msg.format(src_volume.name, src_vm.name))
|
||||
volume = dst_pool.clone(src_volume, dst)
|
||||
else:
|
||||
msg = "Importing volume {!s} from vm {!s}"
|
||||
self.vm.log.info(msg.format(src_volume.name, src_vm.name))
|
||||
volume = dst_pool.import_volume(dst_pool, dst, src_pool,
|
||||
src_volume)
|
||||
|
||||
assert volume, "%s.clone() returned '%s'" % (
|
||||
dst_pool.__class__.__name__, volume)
|
||||
|
||||
self.vm.volumes[name] = volume
|
||||
|
||||
msg = "Cloning directory: {!s} to {!s}"
|
||||
msg = msg.format(src_path, dst_path)
|
||||
self.log.info(msg)
|
||||
|
||||
@property
|
||||
def outdated_volumes(self):
|
||||
@ -248,11 +373,15 @@ class Storage(object):
|
||||
def rename(self, old_name, new_name):
|
||||
''' Notify the pools that the domain was renamed '''
|
||||
volumes = self.vm.volumes
|
||||
vm = self.vm
|
||||
old_dir_path = os.path.join(os.path.dirname(vm.dir_path), old_name)
|
||||
new_dir_path = os.path.join(os.path.dirname(vm.dir_path), new_name)
|
||||
os.rename(old_dir_path, new_dir_path)
|
||||
for name, volume in volumes.items():
|
||||
pool = self.get_pool(volume)
|
||||
volumes[name] = pool.rename(volume, old_name, new_name)
|
||||
|
||||
def verify_files(self):
|
||||
def verify(self):
|
||||
'''Verify that the storage is sane.
|
||||
|
||||
On success, returns normally. On failure, raises exception.
|
||||
@ -264,6 +393,7 @@ class Storage(object):
|
||||
for volume in self.vm.volumes.values():
|
||||
self.get_pool(volume).verify(volume)
|
||||
self.vm.fire_event('domain-verify-files')
|
||||
return True
|
||||
|
||||
def remove(self):
|
||||
''' Remove all the volumes.
|
||||
@ -280,7 +410,8 @@ class Storage(object):
|
||||
def start(self):
|
||||
''' Execute the start method on each pool '''
|
||||
for volume in self.vm.volumes.values():
|
||||
self.get_pool(volume).start(volume)
|
||||
pool = self.get_pool(volume)
|
||||
volume = pool.start(volume)
|
||||
|
||||
def stop(self):
|
||||
''' Execute the start method on each pool '''
|
||||
@ -289,8 +420,12 @@ class Storage(object):
|
||||
|
||||
def get_pool(self, volume):
|
||||
''' Helper function '''
|
||||
assert isinstance(volume, Volume), "You need to pass a Volume"
|
||||
return self.pools[volume.name]
|
||||
assert isinstance(volume, (Volume, basestring)), \
|
||||
"You need to pass a Volume or pool name as str"
|
||||
if isinstance(volume, Volume):
|
||||
return self.pools[volume.name]
|
||||
else:
|
||||
return self.vm.app.pools[volume]
|
||||
|
||||
def commit_template_changes(self):
|
||||
''' Makes changes to an 'origin' volume persistent '''
|
||||
@ -320,109 +455,168 @@ class Pool(object):
|
||||
|
||||
3rd Parties providing own storage implementations will need to extend
|
||||
this class.
|
||||
'''
|
||||
''' # pylint: disable=unused-argument
|
||||
private_img_size = qubes.config.defaults['private_img_size']
|
||||
root_img_size = qubes.config.defaults['root_img_size']
|
||||
|
||||
def __init__(self, name, revisions_to_keep=1, **kwargs):
|
||||
super(Pool, self).__init__(**kwargs)
|
||||
self.name = name
|
||||
self.revisions_to_keep = revisions_to_keep
|
||||
kwargs['name'] = self.name
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.name == other.name
|
||||
|
||||
|
||||
def __neq__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __init__(self, name, **kwargs):
|
||||
super(Pool, self).__init__(**kwargs)
|
||||
self.name = name
|
||||
kwargs['name'] = self.name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __xml__(self):
|
||||
return lxml.etree.Element('pool', **self.config)
|
||||
config = _sanitize_config(self.config)
|
||||
return lxml.etree.Element('pool', **config)
|
||||
|
||||
def create(self, volume, source_volume=None):
|
||||
def create(self, volume):
|
||||
''' Create the given volume on disk or copy from provided
|
||||
`source_volume`.
|
||||
'''
|
||||
raise NotImplementedError("Pool %s has create() not implemented" %
|
||||
self.name)
|
||||
raise self._not_implemented("create")
|
||||
|
||||
def commit_template_changes(self, volume):
|
||||
''' Update origin device '''
|
||||
raise NotImplementedError(
|
||||
"Pool %s has commit_template_changes() not implemented" %
|
||||
self.name)
|
||||
def commit(self, volume): # pylint: disable=no-self-use
|
||||
''' Write the snapshot to disk '''
|
||||
msg = "Got volume_type {!s} when expected 'snap'"
|
||||
msg = msg.format(volume.volume_type)
|
||||
assert volume.volume_type == 'snap', msg
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
''' Returns the pool config to be written to qubes.xml '''
|
||||
raise NotImplementedError("Pool %s has config() not implemented" %
|
||||
self.name)
|
||||
raise self._not_implemented("config")
|
||||
|
||||
def clone(self, source, target):
|
||||
''' Clone volume '''
|
||||
raise NotImplementedError("Pool %s has clone() not implemented" %
|
||||
self.name)
|
||||
raise self._not_implemented("clone")
|
||||
|
||||
def destroy(self):
|
||||
''' Called when removing the pool. Use this for implementation specific
|
||||
clean up.
|
||||
'''
|
||||
raise NotImplementedError("Pool %s has destroy() not implemented" %
|
||||
self.name)
|
||||
raise self._not_implemented("destroy")
|
||||
|
||||
def export(self, volume):
|
||||
''' Returns an object that can be `open()`. '''
|
||||
raise self._not_implemented("export")
|
||||
|
||||
def import_volume(self, dst_pool, dst_volume, src_pool, src_volume):
|
||||
''' Imports data to a volume in this pool '''
|
||||
raise self._not_implemented("import_volume")
|
||||
|
||||
def init_volume(self, vm, volume_config):
|
||||
''' Initialize a :py:class:`qubes.storage.Volume` from `volume_config`.
|
||||
'''
|
||||
raise self._not_implemented("init_volume")
|
||||
|
||||
def is_dirty(self, volume):
|
||||
''' Return `True` if volume was not properly shutdown and commited '''
|
||||
raise self._not_implemented("is_dirty")
|
||||
|
||||
def is_outdated(self, volume):
|
||||
raise NotImplementedError("Pool %s has is_outdated() not implemented" %
|
||||
self.name)
|
||||
''' Returns `True` if the currently used `volume.source` of a snapshot
|
||||
volume is outdated.
|
||||
'''
|
||||
raise self._not_implemented("is_outdated")
|
||||
|
||||
def recover(self, volume):
|
||||
''' Try to recover a :py:class:`Volume` or :py:class:`SnapVolume` '''
|
||||
raise self._not_implemented("recover")
|
||||
|
||||
def remove(self, volume):
|
||||
''' Remove volume'''
|
||||
raise NotImplementedError("Pool %s has remove() not implemented" %
|
||||
self.name)
|
||||
raise self._not_implemented("remove")
|
||||
|
||||
def rename(self, volume, old_name, new_name):
|
||||
''' Called when the domain changes its name '''
|
||||
raise NotImplementedError("Pool %s has rename() not implemented" %
|
||||
self.name)
|
||||
raise self._not_implemented("rename")
|
||||
|
||||
def start(self, volume):
|
||||
''' Do what ever is needed on start '''
|
||||
raise NotImplementedError("Pool %s has start() not implemented" %
|
||||
self.name)
|
||||
def reset(self, volume):
|
||||
''' Drop and recreate volume without copying it's content from source.
|
||||
'''
|
||||
raise self._not_implemented("reset")
|
||||
|
||||
def revert(self, volume, revision=None):
|
||||
''' Revert volume to previous revision '''
|
||||
raise self._not_implemented("revert")
|
||||
|
||||
def setup(self):
|
||||
''' Called when adding a pool to the system. Use this for implementation
|
||||
specific set up.
|
||||
'''
|
||||
raise NotImplementedError("Pool %s has setup() not implemented" %
|
||||
self.name)
|
||||
raise self._not_implemented("setup")
|
||||
|
||||
def stop(self, volume):
|
||||
def start(self, volume): # pylint: disable=no-self-use
|
||||
''' Do what ever is needed on start '''
|
||||
raise self._not_implemented("start")
|
||||
|
||||
def stop(self, volume): # pylint: disable=no-self-use
|
||||
''' Do what ever is needed on stop'''
|
||||
raise NotImplementedError("Pool %s has stop() not implemented" %
|
||||
self.name)
|
||||
|
||||
def init_volume(self, vm, volume_config):
|
||||
''' Initialize a :py:class:`qubes.storage.Volume` from `volume_config`.
|
||||
'''
|
||||
raise NotImplementedError("Pool %s has init_volume() not implemented" %
|
||||
self.name)
|
||||
|
||||
def verify(self, volume):
|
||||
''' Verifies the volume. '''
|
||||
raise NotImplementedError("Pool %s has verify() not implemented" %
|
||||
self.name)
|
||||
raise self._not_implemented("verify")
|
||||
|
||||
@property
|
||||
def volumes(self):
|
||||
''' Return a list of volumes managed by this pool '''
|
||||
raise NotImplementedError("Pool %s has volumes() not implemented" %
|
||||
self.name)
|
||||
raise self._not_implemented("volumes")
|
||||
|
||||
def _not_implemented(self, method_name):
|
||||
''' Helper for emitting helpful `NotImplementedError` exceptions '''
|
||||
msg = "Pool driver {!s} has {!s}() not implemented"
|
||||
msg = msg.format(str(self.__class__.__name__), method_name)
|
||||
return NotImplementedError(msg)
|
||||
|
||||
|
||||
def _sanitize_config(config):
|
||||
''' Helper function to convert types to appropriate strings
|
||||
''' # FIXME: find another solution for serializing basic types
|
||||
result = {}
|
||||
for key, value in config.items():
|
||||
if isinstance(value, bool):
|
||||
if value:
|
||||
result[key] = 'True'
|
||||
else:
|
||||
result[key] = str(value)
|
||||
return result
|
||||
|
||||
|
||||
def pool_drivers():
|
||||
""" Return a list of EntryPoints names """
|
||||
return [ep.name
|
||||
for ep in pkg_resources.iter_entry_points(STORAGE_ENTRY_POINT)]
|
||||
|
||||
|
||||
def isodate(seconds=time.time()):
|
||||
''' Helper method which returns an iso date '''
|
||||
return datetime.utcfromtimestamp(seconds).isoformat("T")
|
||||
|
||||
|
||||
class VmCreationManager(object):
|
||||
''' A `ContextManager` which cleans up if volume creation fails.
|
||||
''' # pylint: disable=too-few-public-methods
|
||||
def __init__(self, vm):
|
||||
self.vm = vm
|
||||
|
||||
def __enter__(self):
|
||||
pass
|
||||
|
||||
def __exit__(self, type, value, tb): # pylint: disable=redefined-builtin
|
||||
if type is not None and value is not None and tb is not None:
|
||||
for volume in self.vm.volumes.values():
|
||||
try:
|
||||
pool = self.vm.storage.get_pool(volume)
|
||||
pool.remove(volume)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
os.rmdir(self.vm.dir_path)
|
||||
|
@ -21,7 +21,7 @@
|
||||
#
|
||||
''' Manages block devices in a domain '''
|
||||
|
||||
import string
|
||||
import string # pylint: disable=deprecated-module
|
||||
|
||||
from qubes.storage import Pool, Volume
|
||||
|
||||
@ -99,15 +99,12 @@ class DomainPool(Pool):
|
||||
class DomainVolume(Volume):
|
||||
''' A volume provided by a block device in an domain '''
|
||||
|
||||
def __init__(self, name, pool, desc, mode, size):
|
||||
if mode == 'w':
|
||||
volume_type = 'read-write'
|
||||
else:
|
||||
volume_type = 'read-only'
|
||||
def __init__(self, name, pool, desc, mode, **kwargs):
|
||||
rw = (mode == 'w')
|
||||
|
||||
super(DomainVolume, self).__init__(desc,
|
||||
pool,
|
||||
volume_type,
|
||||
vid=name,
|
||||
size=size,
|
||||
removable=True)
|
||||
super(DomainVolume, self).__init__(desc, pool, vid=name, removable=True,
|
||||
rw=rw, **kwargs)
|
||||
|
||||
@property
|
||||
def revisions(self):
|
||||
return {}
|
||||
|
@ -22,10 +22,8 @@
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
#
|
||||
|
||||
''' This module contains pool implementations backed by file images'''
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
@ -33,88 +31,102 @@ import os.path
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from qubes.storage import Pool, StoragePoolException, Volume
|
||||
import qubes.storage
|
||||
|
||||
BLKSIZE = 512
|
||||
|
||||
|
||||
class FilePool(Pool):
|
||||
''' File based 'original' disk implementation '''
|
||||
class FilePool(qubes.storage.Pool):
|
||||
''' File based 'original' disk implementation
|
||||
''' # pylint: disable=protected-access
|
||||
driver = 'file'
|
||||
|
||||
def __init__(self, name=None, dir_path=None):
|
||||
super(FilePool, self).__init__(name=name)
|
||||
def __init__(self, revisions_to_keep=1, dir_path=None, **kwargs):
|
||||
super(FilePool, self).__init__(revisions_to_keep=revisions_to_keep,
|
||||
**kwargs)
|
||||
assert dir_path, "No pool dir_path specified"
|
||||
self.dir_path = os.path.normpath(dir_path)
|
||||
self._volumes = []
|
||||
|
||||
def clone(self, source, target):
|
||||
''' Clones the volume if the `source.pool` if the source is a
|
||||
:py:class:`FileVolume`.
|
||||
'''
|
||||
if issubclass(FileVolume, source.__class__):
|
||||
raise StoragePoolException('Volumes %s and %s use different pools'
|
||||
% (source.__class__, target.__class__))
|
||||
|
||||
if source.volume_type not in ['origin', 'read-write']:
|
||||
return target
|
||||
|
||||
copy_file(source.vid, target.vid)
|
||||
return target
|
||||
|
||||
def create(self, volume, source_volume=None):
|
||||
_type = volume.volume_type
|
||||
size = volume.size
|
||||
if _type == 'origin':
|
||||
create_sparse_file(volume.path_origin, size)
|
||||
create_sparse_file(volume.path_cow, size)
|
||||
elif _type in ['read-write'] and source_volume:
|
||||
copy_file(source_volume.path, volume.path)
|
||||
elif _type in ['read-write', 'volatile']:
|
||||
create_sparse_file(volume.path, size)
|
||||
|
||||
return volume
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'dir_path': self.dir_path,
|
||||
'driver': FilePool.driver,
|
||||
'revisions_to_keep': self.revisions_to_keep
|
||||
}
|
||||
|
||||
def is_outdated(self, volume):
|
||||
# FIX: Implement or remove this at all?
|
||||
raise NotImplementedError
|
||||
def clone(self, source, target):
|
||||
new_dir = os.path.dirname(target.path)
|
||||
if target._is_origin or target._is_volume:
|
||||
if not os.path.exists:
|
||||
os.makedirs(new_dir)
|
||||
copy_file(source.path, target.path)
|
||||
return target
|
||||
|
||||
def create(self, volume):
|
||||
assert isinstance(volume.size, (int, long)) and volume.size > 0, \
|
||||
'Volatile volume size must be > 0'
|
||||
if volume._is_origin:
|
||||
create_sparse_file(volume.path, volume.size)
|
||||
create_sparse_file(volume.path_cow, volume.size)
|
||||
elif not volume._is_snapshot:
|
||||
if volume.source is not None:
|
||||
source_path = os.path.join(self.dir_path,
|
||||
volume.source + '.img')
|
||||
copy_file(source_path, volume.path)
|
||||
elif volume._is_volatile:
|
||||
pass
|
||||
else:
|
||||
create_sparse_file(volume.path, volume.size)
|
||||
|
||||
def init_volume(self, vm, volume_config):
|
||||
volume_config['dir_path'] = self.dir_path
|
||||
if os.path.join(self.dir_path, self._vid_prefix(vm)) == vm.dir_path:
|
||||
volume_config['backward_comp'] = True
|
||||
|
||||
if 'vid' not in volume_config:
|
||||
volume_config['vid'] = os.path.join(
|
||||
self._vid_prefix(vm), volume_config['name'])
|
||||
|
||||
try:
|
||||
if volume_config['reset_on_start']:
|
||||
volume_config['revisions_to_keep'] = 0
|
||||
except KeyError:
|
||||
pass
|
||||
finally:
|
||||
if 'revisions_to_keep' not in volume_config:
|
||||
volume_config['revisions_to_keep'] = self.revisions_to_keep
|
||||
|
||||
volume = FileVolume(**volume_config)
|
||||
self._volumes += [volume]
|
||||
return volume
|
||||
|
||||
def is_dirty(self, volume):
|
||||
return False # TODO: How to implement this?
|
||||
|
||||
def resize(self, volume, size):
|
||||
''' Expands volume, throws
|
||||
:py:class:`qubst.storage.StoragePoolException` if given size is
|
||||
less than current_size
|
||||
:py:class:`qubst.storage.qubes.storage.StoragePoolException` if
|
||||
given size is less than current_size
|
||||
''' # pylint: disable=no-self-use
|
||||
_type = volume.volume_type
|
||||
if _type not in ['origin', 'read-write', 'volatile']:
|
||||
raise StoragePoolException('Can not resize a %s volume %s' %
|
||||
(_type, volume.vid))
|
||||
if not volume.rw:
|
||||
msg = 'Can not resize reađonly volume {!s}'.format(volume)
|
||||
raise qubes.storage.StoragePoolException(msg)
|
||||
|
||||
if size <= volume.size:
|
||||
raise StoragePoolException(
|
||||
raise qubes.storage.StoragePoolException(
|
||||
'For your own safety, shrinking of %s is'
|
||||
' disabled. If you really know what you'
|
||||
' are doing, use `truncate` on %s manually.' %
|
||||
(volume.name, volume.vid))
|
||||
|
||||
if _type == 'origin':
|
||||
path = volume.path_origin
|
||||
elif _type in ['read-write', 'volatile']:
|
||||
path = volume.path
|
||||
|
||||
with open(path, 'a+b') as fd:
|
||||
with open(volume.path, 'a+b') as fd:
|
||||
fd.truncate(size)
|
||||
|
||||
p = subprocess.Popen(
|
||||
['sudo', 'losetup', '--associated', path],
|
||||
stdout=subprocess.PIPE)
|
||||
p = subprocess.Popen(['sudo', 'losetup', '--associated', volume.path],
|
||||
stdout=subprocess.PIPE)
|
||||
result = p.communicate()
|
||||
|
||||
m = re.match(r'^(/dev/loop\d+):\s', result[0])
|
||||
@ -122,34 +134,57 @@ class FilePool(Pool):
|
||||
loop_dev = m.group(1)
|
||||
|
||||
# resize loop device
|
||||
subprocess.check_call(['sudo', 'losetup', '--set-capacity', loop_dev
|
||||
])
|
||||
subprocess.check_call(['sudo', 'losetup', '--set-capacity',
|
||||
loop_dev])
|
||||
|
||||
def remove(self, volume):
|
||||
if volume.volume_type in ['read-write', 'volatile']:
|
||||
if not volume.internal:
|
||||
return # do not remove random attached file volumes
|
||||
elif volume._is_snapshot:
|
||||
return # no need to remove, because it's just a snapshot
|
||||
else:
|
||||
_remove_if_exists(volume.path)
|
||||
elif volume.volume_type == 'origin':
|
||||
_remove_if_exists(volume.path)
|
||||
_remove_if_exists(volume.path_cow)
|
||||
if volume._is_origin:
|
||||
_remove_if_exists(volume.path_cow)
|
||||
|
||||
def rename(self, volume, old_name, new_name):
|
||||
assert issubclass(volume.__class__, FileVolume)
|
||||
old_dir = os.path.dirname(volume.path)
|
||||
new_dir = os.path.join(os.path.dirname(old_dir), new_name)
|
||||
subdir, _, volume_path = volume.vid.split('/', 2)
|
||||
|
||||
if not os.path.exists(new_dir):
|
||||
os.makedirs(new_dir)
|
||||
if volume._is_origin:
|
||||
# TODO: Renaming the old revisions
|
||||
new_path = os.path.join(self.dir_path, subdir, new_name)
|
||||
if not os.path.exists(new_path):
|
||||
os.mkdir(new_path, 0755)
|
||||
new_volume_path = os.path.join(new_path, self.name + '.img')
|
||||
if not volume.backward_comp:
|
||||
os.rename(volume.path, new_volume_path)
|
||||
new_volume_path_cow = os.path.join(new_path, self.name + '-cow.img')
|
||||
if os.path.exists(new_volume_path_cow) and not volume.backward_comp:
|
||||
os.rename(volume.path_cow, new_volume_path_cow)
|
||||
|
||||
volume.rename_target_dir(old_name, new_name)
|
||||
volume.vid = os.path.join(subdir, new_name, volume_path)
|
||||
|
||||
return volume
|
||||
|
||||
def commit_template_changes(self, volume):
|
||||
if volume.volume_type != 'origin':
|
||||
def import_volume(self, dst_pool, dst_volume, src_pool, src_volume):
|
||||
msg = "Can not import snapshot volume {!s} in to pool {!s} "
|
||||
msg = msg.format(src_volume, self)
|
||||
assert not src_volume.snap_on_start, msg
|
||||
if dst_volume.save_on_stop:
|
||||
copy_file(src_pool.export(src_volume), dst_volume.path)
|
||||
return dst_volume
|
||||
|
||||
def commit(self, volume):
|
||||
msg = 'Tried to commit a non commitable volume {!r}'.format(volume)
|
||||
assert (volume._is_origin or volume._is_volume) and volume.rw, msg
|
||||
|
||||
if volume._is_volume:
|
||||
return volume
|
||||
|
||||
if os.path.exists(volume.path_cow):
|
||||
os.rename(volume.path_cow, volume.path_cow + '.old')
|
||||
old_path = volume.path_cow + '.old'
|
||||
os.rename(volume.path_cow, old_path)
|
||||
|
||||
old_umask = os.umask(002)
|
||||
with open(volume.path_cow, 'w') as f_cow:
|
||||
@ -160,6 +195,37 @@ class FilePool(Pool):
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def export(self, volume):
|
||||
return volume.path
|
||||
|
||||
def reset(self, volume):
|
||||
''' Remove and recreate a volatile volume '''
|
||||
assert volume._is_volatile, "Not a volatile volume"
|
||||
assert isinstance(volume.size, (int, long)) and volume.size > 0, \
|
||||
'Volatile volume size must be > 0'
|
||||
|
||||
_remove_if_exists(volume.path)
|
||||
|
||||
with open(volume.path, "w") as f_volatile:
|
||||
f_volatile.truncate(volume.size)
|
||||
return volume
|
||||
|
||||
def revert(self, volume, revision=None):
|
||||
if revision is not None:
|
||||
try:
|
||||
return volume.revisions[revision]
|
||||
except KeyError:
|
||||
msg = "Volume {!r} does not have revision {!s}"
|
||||
msg = msg.format(volume, revision)
|
||||
raise qubes.storage.StoragePoolException(msg)
|
||||
else:
|
||||
try:
|
||||
old_path = volume.revisions.values().pop()
|
||||
os.rename(old_path, volume.path_cow)
|
||||
except IndexError:
|
||||
msg = "Volume {!r} does not have old revisions".format(volume)
|
||||
raise qubes.storage.StoragePoolException(msg)
|
||||
|
||||
def setup(self):
|
||||
create_dir_if_not_exists(self.dir_path)
|
||||
appvms_path = os.path.join(self.dir_path, 'appvms')
|
||||
@ -168,18 +234,39 @@ class FilePool(Pool):
|
||||
create_dir_if_not_exists(vm_templates_path)
|
||||
|
||||
def start(self, volume):
|
||||
if volume.volume_type == 'volatile':
|
||||
_reset_volume(volume)
|
||||
if volume.volume_type in ['origin', 'snapshot']:
|
||||
_check_path(volume.path_origin)
|
||||
_check_path(volume.path_cow)
|
||||
else:
|
||||
if volume._is_snapshot or volume._is_origin:
|
||||
_check_path(volume.path)
|
||||
|
||||
try:
|
||||
_check_path(volume.path_cow)
|
||||
except qubes.storage.StoragePoolException:
|
||||
create_sparse_file(volume.path_cow, volume.size)
|
||||
_check_path(volume.path_cow)
|
||||
elif volume._is_volatile:
|
||||
self.reset(volume)
|
||||
return volume
|
||||
|
||||
def stop(self, volume):
|
||||
pass
|
||||
if volume.save_on_stop:
|
||||
self.commit(volume)
|
||||
elif volume._is_volatile:
|
||||
_remove_if_exists(volume.path)
|
||||
return volume
|
||||
|
||||
@staticmethod
|
||||
def _vid_prefix(vm):
|
||||
''' Helper to create a prefix for the vid for volume
|
||||
''' # FIX Remove this if we drop the file backend
|
||||
import qubes.vm.templatevm # pylint: disable=redefined-outer-name
|
||||
import qubes.vm.dispvm # pylint: disable=redefined-outer-name
|
||||
if isinstance(vm, qubes.vm.templatevm.TemplateVM):
|
||||
subdir = 'vm-templates'
|
||||
elif isinstance(vm, qubes.vm.dispvm.DispVM):
|
||||
subdir = 'appvms'
|
||||
return os.path.join(subdir, vm.template.name + '-dvm')
|
||||
else:
|
||||
subdir = 'appvms'
|
||||
|
||||
return os.path.join(subdir, vm.name)
|
||||
|
||||
def target_dir(self, vm):
|
||||
""" Returns the path to vmdir depending on the type of the VM.
|
||||
@ -198,61 +285,8 @@ class FilePool(Pool):
|
||||
string (str) absolute path to the directory where the vm files
|
||||
are stored
|
||||
"""
|
||||
# FIX Remove this if we drop the file backend
|
||||
import qubes.vm.templatevm # nopep8
|
||||
import qubes.vm.dispvm # nopep8
|
||||
if isinstance(vm, qubes.vm.templatevm.TemplateVM):
|
||||
subdir = 'vm-templates'
|
||||
elif isinstance(vm, qubes.vm.dispvm.DispVM):
|
||||
subdir = 'appvms'
|
||||
return os.path.join(self.dir_path, subdir,
|
||||
vm.template.name + '-dvm')
|
||||
else:
|
||||
subdir = 'appvms'
|
||||
|
||||
return os.path.join(self.dir_path, subdir, vm.name)
|
||||
|
||||
def init_volume(self, vm, volume_config):
|
||||
assert 'volume_type' in volume_config, "Volume type missing " \
|
||||
+ str(volume_config)
|
||||
volume_type = volume_config['volume_type']
|
||||
known_types = {
|
||||
'read-write': ReadWriteFile,
|
||||
'read-only': ReadOnlyFile,
|
||||
'origin': OriginFile,
|
||||
'snapshot': SnapshotFile,
|
||||
'volatile': VolatileFile,
|
||||
}
|
||||
if volume_type not in known_types:
|
||||
raise StoragePoolException("Unknown volume type " + volume_type)
|
||||
|
||||
if volume_type in ['snapshot', 'read-only']:
|
||||
name = volume_config['name']
|
||||
|
||||
origin_vm = vm.template
|
||||
while origin_vm.volume_config[name]['volume_type'] == volume_type:
|
||||
origin_vm = origin_vm.template
|
||||
|
||||
expected_origin_type = {
|
||||
'snapshot': 'origin',
|
||||
'read-only': 'read-write', # FIXME: really?
|
||||
}[volume_type]
|
||||
assert origin_vm.volume_config[name]['volume_type'] == \
|
||||
expected_origin_type
|
||||
|
||||
origin_pool = vm.app.get_pool(origin_vm.volume_config[name]['pool'])
|
||||
|
||||
assert isinstance(origin_pool,
|
||||
FilePool), 'Origin volume not a file volume'
|
||||
|
||||
volume_config['target_dir'] = origin_pool.target_dir(origin_vm)
|
||||
volume_config['size'] = origin_vm.volume_config[name]['size']
|
||||
else:
|
||||
volume_config['target_dir'] = self.target_dir(vm)
|
||||
|
||||
volume = known_types[volume_type](**volume_config)
|
||||
self._volumes += [volume]
|
||||
return volume
|
||||
return os.path.join(self.dir_path, self._vid_prefix(vm))
|
||||
|
||||
def verify(self, volume):
|
||||
return volume.verify()
|
||||
@ -262,33 +296,77 @@ class FilePool(Pool):
|
||||
return self._volumes
|
||||
|
||||
|
||||
class FileVolume(Volume):
|
||||
class FileVolume(qubes.storage.Volume):
|
||||
''' Parent class for the xen volumes implementation which expects a
|
||||
`target_dir` param on initialization.
|
||||
'''
|
||||
`target_dir` param on initialization. '''
|
||||
|
||||
def __init__(self, target_dir, **kwargs):
|
||||
self.target_dir = target_dir
|
||||
assert self.target_dir, "target_dir not specified"
|
||||
def __init__(self, dir_path, backward_comp=False, **kwargs):
|
||||
self.dir_path = dir_path
|
||||
self.backward_comp = backward_comp
|
||||
assert self.dir_path, "dir_path not specified"
|
||||
super(FileVolume, self).__init__(**kwargs)
|
||||
|
||||
def _new_dir(self, new_name):
|
||||
''' Returns a new directory path based on the new_name. This is a helper
|
||||
method for moving file images during vm renaming.
|
||||
if self.snap_on_start and self.source is None:
|
||||
msg = "snap_on_start specified on {!r} but no volume source set"
|
||||
msg = msg.format(self.name)
|
||||
raise qubes.storage.StoragePoolException(msg)
|
||||
elif not self.snap_on_start and self.source is not None:
|
||||
msg = "source specified on {!r} but no snap_on_start set"
|
||||
msg = msg.format(self.name)
|
||||
raise qubes.storage.StoragePoolException(msg)
|
||||
|
||||
if self._is_snapshot:
|
||||
self.path = os.path.join(self.dir_path, self.source + '.img')
|
||||
img_name = self.source + '-cow.img'
|
||||
self.path_cow = os.path.join(self.dir_path, img_name)
|
||||
elif self._is_volume or self._is_volatile:
|
||||
self.path = os.path.join(self.dir_path, self.vid + '.img')
|
||||
elif self._is_origin:
|
||||
self.path = os.path.join(self.dir_path, self.vid + '.img')
|
||||
img_name = self.vid + '-cow.img'
|
||||
self.path_cow = os.path.join(self.dir_path, img_name)
|
||||
else:
|
||||
assert False, 'This should not happen'
|
||||
|
||||
def verify(self):
|
||||
''' Verifies the volume. '''
|
||||
if not os.path.exists(self.path) and not self._is_volatile:
|
||||
msg = 'Missing image file: {!s}.'.format(self.path)
|
||||
raise qubes.storage.StoragePoolException(msg)
|
||||
return True
|
||||
|
||||
@property
|
||||
def script(self):
|
||||
if self._is_volume or self._is_volatile:
|
||||
return None
|
||||
elif self._is_origin:
|
||||
return 'block-origin'
|
||||
elif self._is_origin_snapshot or self._is_snapshot:
|
||||
return 'block-snapshot'
|
||||
|
||||
def block_device(self):
|
||||
''' Return :py:class:`qubes.devices.BlockDevice` for serialization in
|
||||
the libvirt XML template as <disk>.
|
||||
'''
|
||||
old_dir = os.path.dirname(self.path)
|
||||
return os.path.join(os.path.dirname(old_dir), new_name)
|
||||
path = self.path
|
||||
if self._is_origin or self._is_snapshot:
|
||||
path += ":" + self.path_cow
|
||||
return qubes.devices.BlockDevice(path, self.name, self.script, self.rw,
|
||||
self.domain, self.devtype)
|
||||
|
||||
@property
|
||||
def revisions(self):
|
||||
if not hasattr(self, 'path_cow'):
|
||||
return {}
|
||||
|
||||
class SizeMixIn(FileVolume):
|
||||
''' A mix in which expects a `size` param to be > 0 on initialization and
|
||||
provides a usage property wrapper.
|
||||
'''
|
||||
old_revision = self.path_cow + '.old' # pylint: disable=no-member
|
||||
|
||||
def __init__(self, size=0, **kwargs):
|
||||
assert size, 'Empty size provided'
|
||||
assert size > 0, 'Size for volume ' + kwargs['name'] + ' is <=0'
|
||||
super(SizeMixIn, self).__init__(size=int(size), **kwargs)
|
||||
if not os.path.exists(old_revision):
|
||||
return {}
|
||||
else:
|
||||
seconds = os.path.getctime(old_revision)
|
||||
iso_date = qubes.storage.isodate(seconds).split('.', 1)[0]
|
||||
return {iso_date: old_revision}
|
||||
|
||||
@property
|
||||
def usage(self):
|
||||
@ -296,169 +374,31 @@ class SizeMixIn(FileVolume):
|
||||
return get_disk_usage(self.vid)
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
''' return config data for serialization to qubes.xml '''
|
||||
return {'name': self.name,
|
||||
'pool': self.pool,
|
||||
'size': str(self.size),
|
||||
'volume_type': self.volume_type}
|
||||
|
||||
|
||||
class ReadWriteFile(SizeMixIn):
|
||||
''' Represents a readable & writable file image based volume '''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(ReadWriteFile, self).__init__(**kwargs)
|
||||
self.path = os.path.join(self.target_dir, self.name + '.img')
|
||||
self.vid = self.path
|
||||
|
||||
def rename_target_dir(self, new_name, new_dir):
|
||||
''' Called by :py:class:`FilePool` when a domain changes it's name '''
|
||||
# pylint: disable=unused-argument
|
||||
old_path = self.path
|
||||
file_name = os.path.basename(self.path)
|
||||
new_path = os.path.join(new_dir, file_name)
|
||||
|
||||
os.rename(old_path, new_path)
|
||||
self.target_dir = new_dir
|
||||
self.path = new_path
|
||||
self.vid = self.path
|
||||
|
||||
def verify(self):
|
||||
''' Verifies the volume. '''
|
||||
if not os.path.exists(self.path):
|
||||
raise StoragePoolException('Missing image file: %s' % self.path)
|
||||
|
||||
|
||||
class ReadOnlyFile(FileVolume):
|
||||
''' Represents a readonly file image based volume '''
|
||||
usage = 0
|
||||
|
||||
def __init__(self, size=0, **kwargs):
|
||||
super(ReadOnlyFile, self).__init__(size=int(size), **kwargs)
|
||||
self.path = self.vid
|
||||
|
||||
def rename_target_dir(self, old_name, new_name):
|
||||
""" Called by :py:class:`FilePool` when a domain changes it's name.
|
||||
|
||||
Only copies the volume if it belongs to the domain being renamed.
|
||||
Currently if a volume is in a directory named the same as the domain,
|
||||
it's ”owned” by the domain.
|
||||
"""
|
||||
new_dir = self._new_dir(new_name)
|
||||
if os.path.basename(self.target_dir) == old_name:
|
||||
file_name = os.path.basename(self.path)
|
||||
new_path = os.path.join(new_dir, file_name)
|
||||
old_path = self.path
|
||||
|
||||
os.rename(old_path, new_path)
|
||||
|
||||
self.target_dir = new_dir
|
||||
self.path = new_path
|
||||
self.vid = self.path
|
||||
|
||||
def verify(self):
|
||||
''' Verifies the volume. '''
|
||||
if not os.path.exists(self.path):
|
||||
raise StoragePoolException('Missing image file: %s' % self.path)
|
||||
|
||||
|
||||
class OriginFile(SizeMixIn):
|
||||
''' Represents a readable, writeable & snapshotable file image based volume.
|
||||
|
||||
This is used for TemplateVM's
|
||||
'''
|
||||
|
||||
script = 'block-origin'
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(OriginFile, self).__init__(**kwargs)
|
||||
self.path_origin = os.path.join(self.target_dir, self.name + '.img')
|
||||
self.path_cow = os.path.join(self.target_dir, self.name + '-cow.img')
|
||||
self.path = '%s:%s' % (self.path_origin, self.path_cow)
|
||||
self.vid = self.path_origin
|
||||
|
||||
def commit(self):
|
||||
''' Commit Template changes '''
|
||||
raise NotImplementedError
|
||||
|
||||
def rename_target_dir(self, old_name, new_name):
|
||||
''' Called by :py:class:`FilePool` when a domain changes it's name.
|
||||
''' # pylint: disable=unused-argument
|
||||
new_dir = self._new_dir(new_name)
|
||||
old_path_origin = self.path_origin
|
||||
old_path_cow = self.path_cow
|
||||
new_path_origin = os.path.join(new_dir, self.name + '.img')
|
||||
new_path_cow = os.path.join(new_dir, self.name + '-cow.img')
|
||||
os.rename(old_path_origin, new_path_origin)
|
||||
os.rename(old_path_cow, new_path_cow)
|
||||
self.target_dir = new_dir
|
||||
self.path_origin = new_path_origin
|
||||
self.path_cow = new_path_cow
|
||||
self.path = '%s:%s' % (self.path_origin, self.path_cow)
|
||||
self.vid = self.path_origin
|
||||
def _is_volatile(self):
|
||||
''' Internal helper. Useful for differentiating volume handling '''
|
||||
return not self.snap_on_start and not self.save_on_stop
|
||||
|
||||
@property
|
||||
def usage(self):
|
||||
result = 0
|
||||
if os.path.exists(self.path_origin):
|
||||
result += get_disk_usage(self.path_origin)
|
||||
if os.path.exists(self.path_cow):
|
||||
result += get_disk_usage(self.path_cow)
|
||||
return result
|
||||
def _is_origin(self):
|
||||
''' Internal helper. Useful for differentiating volume handling '''
|
||||
# pylint: disable=line-too-long
|
||||
return not self.snap_on_start and self.save_on_stop and self.revisions_to_keep > 0 # NOQA
|
||||
|
||||
def verify(self):
|
||||
''' Verifies the volume. '''
|
||||
if not os.path.exists(self.path_origin):
|
||||
raise StoragePoolException('Missing image file: %s' %
|
||||
self.path_origin)
|
||||
@property
|
||||
def _is_snapshot(self):
|
||||
''' Internal helper. Useful for differentiating volume handling '''
|
||||
return self.snap_on_start and not self.save_on_stop
|
||||
|
||||
@property
|
||||
def _is_origin_snapshot(self):
|
||||
''' Internal helper. Useful for differentiating volume handling '''
|
||||
return self.snap_on_start and self.save_on_stop
|
||||
|
||||
class SnapshotFile(FileVolume):
|
||||
''' Represents a readonly snapshot of an :py:class:`OriginFile` volume '''
|
||||
script = 'block-snapshot'
|
||||
rw = False
|
||||
usage = 0
|
||||
|
||||
def __init__(self, name=None, size=None, **kwargs):
|
||||
assert size
|
||||
super(SnapshotFile, self).__init__(name=name, size=int(size), **kwargs)
|
||||
self.path_origin = os.path.join(self.target_dir, name + '.img')
|
||||
self.path_cow = os.path.join(self.target_dir, name + '-cow.img')
|
||||
self.path = '%s:%s' % (self.path_origin, self.path_cow)
|
||||
self.vid = self.path_origin
|
||||
|
||||
def verify(self):
|
||||
''' Verifies the volume. '''
|
||||
if not os.path.exists(self.path_origin):
|
||||
raise StoragePoolException('Missing image file: %s' %
|
||||
self.path_origin)
|
||||
|
||||
|
||||
class VolatileFile(SizeMixIn):
|
||||
''' Represents a readable & writeable file based volume, which will be
|
||||
discarded and recreated at each startup.
|
||||
'''
|
||||
def __init__(self, **kwargs):
|
||||
super(VolatileFile, self).__init__(**kwargs)
|
||||
self.path = os.path.join(self.target_dir, self.name + '.img')
|
||||
self.vid = self.path
|
||||
|
||||
def rename_target_dir(self, old_name, new_name):
|
||||
''' Called by :py:class:`FilePool` when a domain changes it's name.
|
||||
''' # pylint: disable=unused-argument
|
||||
new_dir = self._new_dir(new_name)
|
||||
_remove_if_exists(self.path)
|
||||
file_name = os.path.basename(self.path)
|
||||
self.target_dir = new_dir
|
||||
new_path = os.path.join(new_dir, file_name)
|
||||
self.path = new_path
|
||||
self.vid = self.path
|
||||
|
||||
def verify(self):
|
||||
''' Verifies the volume. '''
|
||||
pass
|
||||
|
||||
@property
|
||||
def _is_volume(self):
|
||||
''' Internal helper. Usefull for differentiating volume handling '''
|
||||
# pylint: disable=line-too-long
|
||||
return not self.snap_on_start and self.save_on_stop and self.revisions_to_keep == 0 # NOQA
|
||||
|
||||
def create_sparse_file(path, size):
|
||||
''' Create an empty sparse file '''
|
||||
@ -534,7 +474,9 @@ def copy_file(source, destination):
|
||||
os.makedirs(parent_dir)
|
||||
|
||||
try:
|
||||
subprocess.check_call(['cp', '--reflink=auto', source, destination])
|
||||
cmd = ['sudo', 'cp', '--sparse=auto',
|
||||
'--reflink=auto', source, destination]
|
||||
subprocess.check_call(cmd)
|
||||
except subprocess.CalledProcessError:
|
||||
raise IOError('Error while copying {!r} to {!r}'.format(source,
|
||||
destination))
|
||||
@ -549,17 +491,5 @@ def _remove_if_exists(path):
|
||||
def _check_path(path):
|
||||
''' Raise an StoragePoolException if ``path`` does not exist'''
|
||||
if not os.path.exists(path):
|
||||
raise StoragePoolException('Missing image file: %s' % path)
|
||||
|
||||
|
||||
def _reset_volume(volume):
|
||||
''' Remove and recreate a volatile volume '''
|
||||
assert volume.volume_type == 'volatile', "Not a volatile volume"
|
||||
|
||||
assert volume.size
|
||||
|
||||
_remove_if_exists(volume.path)
|
||||
|
||||
with open(volume.path, "w") as f_volatile:
|
||||
f_volatile.truncate(volume.size)
|
||||
return volume
|
||||
msg = 'Missing image file: %s' % path
|
||||
raise qubes.storage.StoragePoolException(msg)
|
||||
|
@ -33,12 +33,16 @@ class LinuxModules(Volume):
|
||||
|
||||
def __init__(self, target_dir, kernel_version, **kwargs):
|
||||
kwargs['vid'] = kernel_version
|
||||
kwargs['source'] = self
|
||||
super(LinuxModules, self).__init__(**kwargs)
|
||||
self.kernels_dir = os.path.join(target_dir, kernel_version)
|
||||
self.path = os.path.join(self.kernels_dir, 'modules.img')
|
||||
self.vmlinuz = os.path.join(self.kernels_dir, 'vmlinuz')
|
||||
self.initramfs = os.path.join(self.kernels_dir, 'initramfs')
|
||||
|
||||
@property
|
||||
def revisions(self):
|
||||
return {}
|
||||
|
||||
class LinuxKernel(Pool):
|
||||
''' Provides linux kernels '''
|
||||
@ -50,23 +54,22 @@ class LinuxKernel(Pool):
|
||||
self.dir_path = dir_path
|
||||
|
||||
def init_volume(self, vm, volume_config):
|
||||
assert 'volume_type' in volume_config, "Volume type missing " \
|
||||
+ str(volume_config)
|
||||
volume_type = volume_config['volume_type']
|
||||
if volume_type != 'read-only':
|
||||
raise StoragePoolException("Unknown volume type " + volume_type)
|
||||
assert not volume_config['rw']
|
||||
|
||||
volume = LinuxModules(self.dir_path, vm.kernel, **volume_config)
|
||||
|
||||
return volume
|
||||
|
||||
def is_dirty(self, volume):
|
||||
return False
|
||||
|
||||
def clone(self, source, target):
|
||||
return target
|
||||
|
||||
def create(self, volume, source_volume=None):
|
||||
def create(self, volume):
|
||||
return volume
|
||||
|
||||
def commit_template_changes(self, volume):
|
||||
def commit(self, volume):
|
||||
return volume
|
||||
|
||||
@property
|
||||
@ -80,6 +83,12 @@ class LinuxKernel(Pool):
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def export(self, volume):
|
||||
return volume.path
|
||||
|
||||
def import_volume(self, dst_pool, dst_volume, src_pool, src_volume):
|
||||
pass
|
||||
|
||||
def is_outdated(self, volume):
|
||||
return False
|
||||
|
||||
@ -115,7 +124,8 @@ class LinuxKernel(Pool):
|
||||
pool=self.name,
|
||||
name=kernel_version,
|
||||
internal=True,
|
||||
volume_type='read-only')
|
||||
rw=False
|
||||
)
|
||||
for kernel_version in os.listdir(self.dir_path)]
|
||||
|
||||
|
||||
|
@ -820,7 +820,7 @@ class BackupTestsMixin(SystemTestsMixin):
|
||||
name=vmname, template=template, provides_network=True, label='red')
|
||||
testnet.create_on_disk()
|
||||
vms.append(testnet)
|
||||
self.fill_image(testnet.volumes['private'].vid, 20*1024*1024)
|
||||
self.fill_image(testnet.volumes['private'].path, 20*1024*1024)
|
||||
|
||||
vmname = self.make_vm_name('test1')
|
||||
if self.verbose:
|
||||
@ -831,7 +831,7 @@ class BackupTestsMixin(SystemTestsMixin):
|
||||
testvm1.netvm = testnet
|
||||
testvm1.create_on_disk()
|
||||
vms.append(testvm1)
|
||||
self.fill_image(testvm1.volumes['private'].vid, 100*1024*1024)
|
||||
self.fill_image(testvm1.volumes['private'].path, 100*1024*1024)
|
||||
|
||||
vmname = self.make_vm_name('testhvm1')
|
||||
if self.verbose:
|
||||
@ -841,7 +841,7 @@ class BackupTestsMixin(SystemTestsMixin):
|
||||
hvm=True,
|
||||
label='red')
|
||||
testvm2.create_on_disk()
|
||||
self.fill_image(testvm2.volumes['root'].vid, 1024 * 1024 * 1024, True)
|
||||
self.fill_image(testvm2.volumes['root'].path, 1024 * 1024 * 1024, True)
|
||||
vms.append(testvm2)
|
||||
|
||||
vmname = self.make_vm_name('template')
|
||||
@ -850,7 +850,7 @@ class BackupTestsMixin(SystemTestsMixin):
|
||||
testvm3 = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM,
|
||||
name=vmname, label='red')
|
||||
testvm3.create_on_disk()
|
||||
self.fill_image(testvm3.root_img, 100*1024*1024, True)
|
||||
self.fill_image(testvm3.volumes['root'].path, 100 * 1024 * 1024, True)
|
||||
vms.append(testvm3)
|
||||
|
||||
vmname = self.make_vm_name('custom')
|
||||
|
@ -26,7 +26,6 @@
|
||||
|
||||
import os
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import qubes
|
||||
import qubes.exc
|
||||
@ -77,10 +76,13 @@ class TC_00_Backup(qubes.tests.BackupTestsMixin, qubes.tests.QubesTestCase):
|
||||
hvmtemplate = self.app.add_new_vm(
|
||||
qubes.vm.templatevm.TemplateVM, name=vmname, hvm=True, label='red')
|
||||
hvmtemplate.create_on_disk()
|
||||
self.fill_image(os.path.join(hvmtemplate.dir_path, '00file'),
|
||||
195*1024*1024-4096*3)
|
||||
self.fill_image(hvmtemplate.private_img, 195*1024*1024-4096*3)
|
||||
self.fill_image(hvmtemplate.root_img, 1024*1024*1024, sparse=True)
|
||||
self.fill_image(
|
||||
os.path.join(hvmtemplate.dir_path, '00file'),
|
||||
195 * 1024 * 1024 - 4096 * 3)
|
||||
self.fill_image(hvmtemplate.volumes['private'].path,
|
||||
195 * 1024 * 1024 - 4096 * 3)
|
||||
self.fill_image(hvmtemplate.volumes['root'].path, 1024 * 1024 * 1024,
|
||||
sparse=True)
|
||||
vms.append(hvmtemplate)
|
||||
self.app.save()
|
||||
|
||||
@ -93,7 +95,7 @@ class TC_00_Backup(qubes.tests.BackupTestsMixin, qubes.tests.QubesTestCase):
|
||||
|
||||
def test_005_compressed_custom(self):
|
||||
vms = self.create_backup_vms()
|
||||
self.make_backup(vms, compressed="bzip2")
|
||||
self.make_backup(vms, compression_filter="bzip2")
|
||||
self.remove_vms(reversed(vms))
|
||||
self.restore_backup()
|
||||
for vm in vms:
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/python
|
||||
# vim: fileencoding=utf-8
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
#
|
||||
# The Qubes OS Project, https://www.qubes-os.org/
|
||||
#
|
||||
@ -35,7 +35,7 @@ import qubes.vm.appvm
|
||||
import qubes.vm.qubesvm
|
||||
import qubes.vm.templatevm
|
||||
|
||||
import libvirt
|
||||
import libvirt # pylint: disable=import-error
|
||||
|
||||
|
||||
class TC_00_Basic(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
@ -46,17 +46,12 @@ class TC_00_Basic(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
def test_000_qubes_create(self):
|
||||
self.assertIsInstance(self.app, qubes.Qubes)
|
||||
|
||||
def test_001_qvm_create_default_template(self):
|
||||
self.app.add_new_vm
|
||||
|
||||
|
||||
def test_100_qvm_create(self):
|
||||
vmname = self.make_vm_name('appvm')
|
||||
|
||||
vm = self.app.add_new_vm(qubes.vm.appvm.AppVM,
|
||||
name=vmname, template=self.app.default_template,
|
||||
label='red'
|
||||
)
|
||||
label='red')
|
||||
|
||||
self.assertIsNotNone(vm)
|
||||
self.assertEqual(vm.name, vmname)
|
||||
@ -64,17 +59,18 @@ class TC_00_Basic(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
vm.create_on_disk()
|
||||
|
||||
with self.assertNotRaises(qubes.exc.QubesException):
|
||||
vm.verify_files()
|
||||
vm.storage.verify()
|
||||
|
||||
|
||||
class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
def setUp(self):
|
||||
super(TC_01_Properties, self).setUp()
|
||||
self.init_default_template()
|
||||
self.vmname = self.make_vm_name('appvm')
|
||||
self.vm = self.app.add_new_vm(qubes.vm.appvm.AppVM,
|
||||
name=self.vmname,
|
||||
label='red')
|
||||
self.vm = self.app.add_new_vm(qubes.vm.appvm.AppVM, name=self.vmname,
|
||||
template=self.app.default_template,
|
||||
label='red')
|
||||
self.vm.create_on_disk()
|
||||
|
||||
def save_and_reload_db(self):
|
||||
@ -82,7 +78,7 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
if hasattr(self, 'vm'):
|
||||
self.vm = self.app.domains[self.vm.qid]
|
||||
if hasattr(self, 'netvm'):
|
||||
self.netvm = self.app[self.netvm.qid]
|
||||
self.netvm = self.app.domains[self.netvm.qid]
|
||||
|
||||
def test_000_rename(self):
|
||||
newname = self.make_vm_name('newname')
|
||||
@ -103,13 +99,11 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
os.path.join(
|
||||
qubes.config.system_path['qubes_base_dir'],
|
||||
qubes.config.system_path['qubes_appvms_dir'], newname))
|
||||
self.assertEqual(self.vm.conf_file,
|
||||
os.path.join(self.vm.dir_path, 'libvirt.xml'))
|
||||
self.assertTrue(os.path.exists(
|
||||
os.path.join(self.vm.dir_path, "apps", newname + "-vm.directory")))
|
||||
# FIXME: set whitelisted-appmenus.list first
|
||||
self.assertTrue(os.path.exists(
|
||||
os.path.join(self.vm.dir_path, "apps", newname + "-firefox.desktop")))
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.vm.dir_path, "apps", newname + "-firefox.desktop")))
|
||||
self.assertTrue(os.path.exists(
|
||||
os.path.join(os.getenv("HOME"), ".local/share/desktop-directories",
|
||||
newname + "-vm.directory")))
|
||||
@ -135,7 +129,7 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
|
||||
def test_001_rename_libvirt_undefined(self):
|
||||
self.vm.libvirt_domain.undefine()
|
||||
self.vm._libvirt_domain = None
|
||||
self.vm._libvirt_domain = None # pylint: disable=protected-access
|
||||
|
||||
newname = self.make_vm_name('newname')
|
||||
with self.assertNotRaises(
|
||||
@ -146,15 +140,20 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
testvm1 = self.app.add_new_vm(
|
||||
qubes.vm.appvm.AppVM,
|
||||
name=self.make_vm_name("vm"),
|
||||
template=self.app.default_template,
|
||||
label='red')
|
||||
testvm1.create_on_disk()
|
||||
testvm2 = self.app.add_new_vm(testvm1.__class__,
|
||||
name=self.make_vm_name("clone"),
|
||||
template=testvm1.template,
|
||||
label='red',
|
||||
)
|
||||
label='red')
|
||||
testvm2.clone_properties(testvm1)
|
||||
testvm2.clone_disk_files(testvm1)
|
||||
self.assertTrue(testvm1.storage.verify())
|
||||
self.assertIn('source', testvm1.volumes['root'].config)
|
||||
self.assertNotEquals(testvm2, None)
|
||||
self.assertNotEquals(testvm2.volumes, {})
|
||||
self.assertIn('source', testvm2.volumes['root'].config)
|
||||
|
||||
# qubes.xml reload
|
||||
self.save_and_reload_db()
|
||||
@ -200,8 +199,7 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
testvm3 = self.app.add_new_vm(testvm1.__class__,
|
||||
name=self.make_vm_name("clone2"),
|
||||
template=testvm1.template,
|
||||
label='red',
|
||||
)
|
||||
label='red',)
|
||||
testvm3.clone_properties(testvm1)
|
||||
testvm3.clone_disk_files(testvm1)
|
||||
|
||||
@ -235,14 +233,15 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
# TODO decide what exception should be here
|
||||
with self.assertRaises((qubes.exc.QubesException, ValueError)):
|
||||
self.vm2 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
|
||||
name=self.vmname, template=self.app.default_template)
|
||||
name=self.vmname, template=self.app.default_template,
|
||||
label='red')
|
||||
self.vm2.create_on_disk()
|
||||
|
||||
def test_021_name_conflict_template(self):
|
||||
# TODO decide what exception should be here
|
||||
with self.assertRaises((qubes.exc.QubesException, ValueError)):
|
||||
self.vm2 = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM,
|
||||
name=self.vmname)
|
||||
name=self.vmname, label='red')
|
||||
self.vm2.create_on_disk()
|
||||
|
||||
def test_030_rename_conflict_app(self):
|
||||
@ -257,6 +256,7 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
self.vm2.name = self.vmname
|
||||
|
||||
class TC_02_QvmPrefs(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
|
||||
def setUp(self):
|
||||
super(TC_02_QvmPrefs, self).setUp()
|
||||
@ -349,7 +349,7 @@ class TC_02_QvmPrefs(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
('invalid', '', False),
|
||||
('[invalid]', '', False),
|
||||
# TODO:
|
||||
#('["12:12.0"]', '', False)
|
||||
# ('["12:12.0"]', '', False)
|
||||
])
|
||||
|
||||
@unittest.skip('test not converted to core3 API')
|
||||
@ -368,6 +368,7 @@ class TC_02_QvmPrefs(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
|
||||
class TC_03_QvmRevertTemplateChanges(qubes.tests.SystemTestsMixin,
|
||||
qubes.tests.QubesTestCase):
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
|
||||
def setUp(self):
|
||||
super(TC_03_QvmRevertTemplateChanges, self).setUp()
|
||||
@ -395,7 +396,7 @@ class TC_03_QvmRevertTemplateChanges(qubes.tests.SystemTestsMixin,
|
||||
|
||||
def get_rootimg_checksum(self):
|
||||
p = subprocess.Popen(
|
||||
['sha1sum', self.test_template.volumes['root'].vid],
|
||||
['sha1sum', self.test_template.volumes['root'].path_cow],
|
||||
stdout=subprocess.PIPE)
|
||||
return p.communicate()[0]
|
||||
|
||||
@ -407,9 +408,11 @@ class TC_03_QvmRevertTemplateChanges(qubes.tests.SystemTestsMixin,
|
||||
if checksum_before == checksum_changed:
|
||||
self.log.warning("template not modified, test result will be "
|
||||
"unreliable")
|
||||
self.assertNotEqual(self.test_template.volumes['root'].revisions, {})
|
||||
with self.assertNotRaises(subprocess.CalledProcessError):
|
||||
subprocess.check_call(['sudo', 'qvm-revert-template-changes',
|
||||
'--force', self.test_template.name])
|
||||
pool_vid = repr(self.test_template.volumes['root']).strip("'")
|
||||
revert_cmd = ['qvm-block', 'revert', pool_vid]
|
||||
subprocess.check_call(revert_cmd)
|
||||
|
||||
checksum_after = self.get_rootimg_checksum()
|
||||
self.assertEquals(checksum_before, checksum_after)
|
||||
@ -442,8 +445,7 @@ class TC_04_DispVM(qubes.tests.SystemTestsMixin,
|
||||
self.disp_tpl = self.app.add_new_vm(disp_tpl.__class__,
|
||||
name=disp_tpl.name,
|
||||
template=disp_tpl.template,
|
||||
label='red'
|
||||
)
|
||||
label='red')
|
||||
self.app.save()
|
||||
|
||||
@staticmethod
|
||||
@ -458,6 +460,7 @@ class TC_04_DispVM(qubes.tests.SystemTestsMixin,
|
||||
"""
|
||||
|
||||
testvm1 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
|
||||
template=self.app.default_template,
|
||||
name=self.make_vm_name('vm1'),
|
||||
label='red')
|
||||
testvm1.create_on_disk()
|
||||
@ -512,6 +515,7 @@ class TC_04_DispVM(qubes.tests.SystemTestsMixin,
|
||||
Check firewall propagation VM->DispVM, when VM have no firewall rules
|
||||
"""
|
||||
testvm1 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
|
||||
template=self.app.default_template,
|
||||
name=self.make_vm_name('vm1'),
|
||||
label='red')
|
||||
testvm1.create_on_disk()
|
||||
@ -544,8 +548,8 @@ class TC_04_DispVM(qubes.tests.SystemTestsMixin,
|
||||
dispvm_name = p.stdout.readline().strip()
|
||||
self.reload_db()
|
||||
dispvm = self.app.domains[dispvm_name]
|
||||
self.assertIsNotNone(dispvm, "DispVM {} not found in qubes.xml".format(
|
||||
dispvm_name))
|
||||
self.assertIsNotNone(
|
||||
dispvm, "DispVM {} not found in qubes.xml".format(dispvm_name))
|
||||
# check if firewall was propagated to the DispVM from the right VM
|
||||
self.assertEquals(testvm1.get_firewall_conf(),
|
||||
dispvm.get_firewall_conf())
|
||||
@ -625,6 +629,4 @@ class TC_04_DispVM(qubes.tests.SystemTestsMixin,
|
||||
self.assertIsNone(dispvm, "DispVM {} still exists in qubes.xml".format(
|
||||
dispvm_name))
|
||||
|
||||
|
||||
|
||||
# vim: ts=4 sw=4 et
|
||||
|
@ -20,19 +20,16 @@ import qubes.log
|
||||
from qubes.exc import QubesException
|
||||
from qubes.storage import pool_drivers
|
||||
from qubes.storage.file import FilePool
|
||||
from qubes.tests import QubesTestCase, SystemTestsMixin
|
||||
from qubes.tests import QubesTestCase
|
||||
|
||||
# :pylint: disable=invalid-name
|
||||
|
||||
|
||||
class TestApp(qubes.tests.TestEmitter):
|
||||
pass
|
||||
|
||||
|
||||
class TestVM(object):
|
||||
def __init__(self, test, template=None):
|
||||
self.app = test.app
|
||||
self.name = test.make_vm_name('appvm')
|
||||
self.dir_path = '/var/lib/qubes/appvms/' + self.name
|
||||
self.log = qubes.log.get_vm_logger(self.name)
|
||||
|
||||
if template:
|
||||
@ -50,6 +47,10 @@ class TestVM(object):
|
||||
class TestTemplateVM(TestVM):
|
||||
dir_path_prefix = qubes.config.system_path['qubes_templates_dir']
|
||||
|
||||
def __init__(self, test, template=None):
|
||||
super(TestTemplateVM, self).__init__(test, template)
|
||||
self.dir_path = '/var/lib/qubes/vm-templates/' + self.name
|
||||
|
||||
def is_template(self):
|
||||
return True
|
||||
|
||||
@ -59,7 +60,7 @@ class TestDisposableVM(TestVM):
|
||||
return True
|
||||
|
||||
class TestApp(qubes.Qubes):
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
super(TestApp, self).__init__('/tmp/qubes-test.xml',
|
||||
load=False, offline_mode=True, **kwargs)
|
||||
self.load_initial_values()
|
||||
|
@ -16,29 +16,27 @@
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
''' Tests for the file storage backend '''
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import qubes.storage
|
||||
import qubes.tests.storage
|
||||
import unittest
|
||||
from qubes.config import defaults
|
||||
from qubes.storage import Storage
|
||||
from qubes.storage.file import (OriginFile, ReadOnlyFile, ReadWriteFile,
|
||||
SnapshotFile, VolatileFile)
|
||||
from qubes.tests import QubesTestCase, SystemTestsMixin
|
||||
from qubes.tests.storage import TestVM
|
||||
|
||||
# :pylint: disable=invalid-name
|
||||
|
||||
|
||||
class TestApp(qubes.Qubes):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TestApp, self).__init__('/tmp/qubes-test.xml',
|
||||
load=False, offline_mode=True, **kwargs)
|
||||
''' A Mock App object '''
|
||||
def __init__(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
super(TestApp, self).__init__('/tmp/qubes-test.xml', load=False,
|
||||
offline_mode=True, **kwargs)
|
||||
self.load_initial_values()
|
||||
self.pools['linux-kernel'].dir_path = '/tmp/qubes-test-kernel'
|
||||
dummy_kernel = os.path.join(
|
||||
self.pools['linux-kernel'].dir_path, 'dummy')
|
||||
dummy_kernel = os.path.join(self.pools['linux-kernel'].dir_path,
|
||||
'dummy')
|
||||
os.makedirs(dummy_kernel)
|
||||
open(os.path.join(dummy_kernel, 'vmlinuz'), 'w').close()
|
||||
open(os.path.join(dummy_kernel, 'modules.img'), 'w').close()
|
||||
@ -46,16 +44,23 @@ class TestApp(qubes.Qubes):
|
||||
self.default_kernel = 'dummy'
|
||||
|
||||
def cleanup(self):
|
||||
''' Remove temporary directories '''
|
||||
shutil.rmtree(self.pools['linux-kernel'].dir_path)
|
||||
|
||||
def create_dummy_template(self):
|
||||
self.add_new_vm(qubes.vm.templatevm.TemplateVM,
|
||||
name='test-template', label='red',
|
||||
memory=1024, maxmem=1024)
|
||||
self.default_template = 'test-template'
|
||||
''' Initalizes a dummy TemplateVM as the `default_template` '''
|
||||
template = self.add_new_vm(qubes.vm.templatevm.TemplateVM,
|
||||
name='test-template', label='red',
|
||||
memory=1024, maxmem=1024)
|
||||
self.default_template = template
|
||||
|
||||
class TC_00_FilePool(QubesTestCase):
|
||||
""" This class tests some properties of the 'default' pool. """
|
||||
|
||||
class TC_00_FilePool(qubes.tests.QubesTestCase):
|
||||
""" This class tests some properties of the 'default' pool.
|
||||
|
||||
This test might become obsolete if we change the driver for the default
|
||||
pool to something else as 'file'.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TC_00_FilePool, self).setUp()
|
||||
@ -76,21 +81,23 @@ class TC_00_FilePool(QubesTestCase):
|
||||
self.assertEquals(result, expected)
|
||||
|
||||
def test001_default_storage_class(self):
|
||||
""" Check when using default pool the Storage is ``Storage``. """
|
||||
""" Check when using default pool the Storage is
|
||||
``qubes.storage.Storage``. """
|
||||
result = self._init_app_vm().storage
|
||||
self.assertIsInstance(result, Storage)
|
||||
self.assertIsInstance(result, qubes.storage.Storage)
|
||||
|
||||
def _init_app_vm(self):
|
||||
""" Return initalised, but not created, AppVm. """
|
||||
vmname = self.make_vm_name('appvm')
|
||||
self.app.create_dummy_template()
|
||||
return self.app.add_new_vm(qubes.vm.appvm.AppVM,
|
||||
name=vmname,
|
||||
return self.app.add_new_vm(qubes.vm.appvm.AppVM, name=vmname,
|
||||
template=self.app.default_template,
|
||||
label='red')
|
||||
|
||||
|
||||
class TC_01_FileVolumes(QubesTestCase):
|
||||
class TC_01_FileVolumes(qubes.tests.QubesTestCase):
|
||||
''' Test correct handling of different types of volumes '''
|
||||
|
||||
POOL_DIR = '/tmp/test-pool'
|
||||
POOL_NAME = 'test-pool'
|
||||
POOL_CONF = {'driver': 'file', 'dir_path': POOL_DIR, 'name': POOL_NAME}
|
||||
@ -113,91 +120,99 @@ class TC_01_FileVolumes(QubesTestCase):
|
||||
config = {
|
||||
'name': 'root',
|
||||
'pool': self.POOL_NAME,
|
||||
'volume_type': 'origin',
|
||||
'save_on_stop': True,
|
||||
'rw': True,
|
||||
'size': defaults['root_img_size'],
|
||||
}
|
||||
vm = TestVM(self)
|
||||
result = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
|
||||
self.assertIsInstance(result, OriginFile)
|
||||
self.assertEqual(result.name, 'root')
|
||||
self.assertEqual(result.pool, self.POOL_NAME)
|
||||
self.assertEqual(result.size, defaults['root_img_size'])
|
||||
vm = qubes.tests.storage.TestVM(self)
|
||||
volume = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
|
||||
self.assertEqual(volume.name, 'root')
|
||||
self.assertEqual(volume.pool, self.POOL_NAME)
|
||||
self.assertEqual(volume.size, defaults['root_img_size'])
|
||||
self.assertFalse(volume.snap_on_start)
|
||||
self.assertTrue(volume.save_on_stop)
|
||||
self.assertTrue(volume.rw)
|
||||
|
||||
def test_001_snapshot_volume(self):
|
||||
original_path = '/var/lib/qubes/vm-templates/fedora-23/root.img'
|
||||
source = 'vm-templates/fedora-23/root'
|
||||
original_size = qubes.config.defaults['root_img_size']
|
||||
config = {
|
||||
'name': 'root',
|
||||
'pool': 'default',
|
||||
'volume_type': 'snapshot',
|
||||
'vid': original_path,
|
||||
'snap_on_start': True,
|
||||
'rw': False,
|
||||
'source': source,
|
||||
'size': original_size,
|
||||
}
|
||||
vm = TestVM(self, template=self.app.default_template)
|
||||
result = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
|
||||
self.assertIsInstance(result, SnapshotFile)
|
||||
self.assertEqual(result.name, 'root')
|
||||
self.assertEqual(result.pool, 'default')
|
||||
self.assertEqual(result.size, original_size)
|
||||
|
||||
template_vm = self.app.default_template
|
||||
vm = qubes.tests.storage.TestVM(self, template=template_vm)
|
||||
volume = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
|
||||
self.assertEqual(volume.name, 'root')
|
||||
self.assertEqual(volume.pool, 'default')
|
||||
self.assertEqual(volume.size, original_size)
|
||||
self.assertTrue(volume.snap_on_start)
|
||||
self.assertTrue(volume.snap_on_start)
|
||||
self.assertFalse(volume.save_on_stop)
|
||||
self.assertFalse(volume.rw)
|
||||
self.assertEqual(volume.usage, 0)
|
||||
|
||||
def test_002_read_write_volume(self):
|
||||
config = {
|
||||
'name': 'root',
|
||||
'pool': self.POOL_NAME,
|
||||
'volume_type': 'read-write',
|
||||
'rw': True,
|
||||
'save_on_stop': True,
|
||||
'size': defaults['root_img_size'],
|
||||
}
|
||||
vm = TestVM(self)
|
||||
result = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
|
||||
self.assertIsInstance(result, ReadWriteFile)
|
||||
self.assertEqual(result.name, 'root')
|
||||
self.assertEqual(result.pool, self.POOL_NAME)
|
||||
self.assertEqual(result.size, defaults['root_img_size'])
|
||||
vm = qubes.tests.storage.TestVM(self)
|
||||
volume = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
|
||||
self.assertEqual(volume.name, 'root')
|
||||
self.assertEqual(volume.pool, self.POOL_NAME)
|
||||
self.assertEqual(volume.size, defaults['root_img_size'])
|
||||
self.assertFalse(volume.snap_on_start)
|
||||
self.assertTrue(volume.save_on_stop)
|
||||
self.assertTrue(volume.rw)
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_003_read_volume(self):
|
||||
def test_003_read_only_volume(self):
|
||||
template = self.app.default_template
|
||||
original_path = template.volumes['root'].vid
|
||||
original_size = qubes.config.defaults['root_img_size']
|
||||
config = {
|
||||
'name': 'root',
|
||||
'pool': 'default',
|
||||
'volume_type': 'read-only',
|
||||
'vid': original_path
|
||||
}
|
||||
vm = TestVM(self, template=template)
|
||||
vid = template.volumes['root'].vid
|
||||
config = {'name': 'root', 'pool': 'default', 'rw': False, 'vid': vid}
|
||||
vm = qubes.tests.storage.TestVM(self, template=template)
|
||||
|
||||
result = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
|
||||
self.assertIsInstance(result, ReadOnlyFile)
|
||||
self.assertEqual(result.name, 'root')
|
||||
self.assertEqual(result.pool, 'default')
|
||||
self.assertEqual(result.size, original_size)
|
||||
volume = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
|
||||
self.assertEqual(volume.name, 'root')
|
||||
self.assertEqual(volume.pool, 'default')
|
||||
|
||||
# original_size = qubes.config.defaults['root_img_size']
|
||||
# FIXME: self.assertEqual(volume.size, original_size)
|
||||
self.assertFalse(volume.snap_on_start)
|
||||
self.assertFalse(volume.save_on_stop)
|
||||
self.assertFalse(volume.rw)
|
||||
|
||||
def test_004_volatile_volume(self):
|
||||
config = {
|
||||
'name': 'root',
|
||||
'pool': self.POOL_NAME,
|
||||
'volume_type': 'volatile',
|
||||
'size': defaults['root_img_size'],
|
||||
'rw': True,
|
||||
}
|
||||
vm = TestVM(self)
|
||||
result = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
|
||||
self.assertIsInstance(result, VolatileFile)
|
||||
self.assertEqual(result.name, 'root')
|
||||
self.assertEqual(result.pool, self.POOL_NAME)
|
||||
self.assertEqual(result.size, defaults['root_img_size'])
|
||||
vm = qubes.tests.storage.TestVM(self)
|
||||
volume = self.app.get_pool(self.POOL_NAME).init_volume(vm, config)
|
||||
self.assertEqual(volume.name, 'root')
|
||||
self.assertEqual(volume.pool, self.POOL_NAME)
|
||||
self.assertEqual(volume.size, defaults['root_img_size'])
|
||||
self.assertFalse(volume.snap_on_start)
|
||||
self.assertFalse(volume.save_on_stop)
|
||||
self.assertTrue(volume.rw)
|
||||
|
||||
def test_005_appvm_volumes(self):
|
||||
''' Check if AppVM volumes are propertly initialized '''
|
||||
vmname = self.make_vm_name('appvm')
|
||||
vm = self.app.add_new_vm(qubes.vm.appvm.AppVM,
|
||||
name=vmname,
|
||||
vm = self.app.add_new_vm(qubes.vm.appvm.AppVM, name=vmname,
|
||||
template=self.app.default_template,
|
||||
label='red')
|
||||
|
||||
volumes = vm.volumes
|
||||
self.assertIsInstance(volumes['root'], SnapshotFile)
|
||||
self.assertIsInstance(volumes['private'], OriginFile)
|
||||
self.assertIsInstance(volumes['volatile'], VolatileFile)
|
||||
expected = vm.template.dir_path + '/root.img:' + vm.template.dir_path \
|
||||
+ '/root-cow.img'
|
||||
self.assertVolumePath(vm, 'root', expected, rw=False)
|
||||
@ -210,14 +225,9 @@ class TC_01_FileVolumes(QubesTestCase):
|
||||
def test_006_template_volumes(self):
|
||||
''' Check if TemplateVM volumes are propertly initialized '''
|
||||
vmname = self.make_vm_name('appvm')
|
||||
vm = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM,
|
||||
name=vmname,
|
||||
vm = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM, name=vmname,
|
||||
label='red')
|
||||
|
||||
volumes = vm.volumes
|
||||
self.assertIsInstance(volumes['root'], OriginFile)
|
||||
self.assertIsInstance(volumes['private'], ReadWriteFile)
|
||||
self.assertIsInstance(volumes['volatile'], VolatileFile)
|
||||
expected = vm.dir_path + '/root.img:' + vm.dir_path + '/root-cow.img'
|
||||
self.assertVolumePath(vm, 'root', expected, rw=True)
|
||||
expected = vm.dir_path + '/private.img'
|
||||
@ -233,7 +243,7 @@ class TC_01_FileVolumes(QubesTestCase):
|
||||
self.assertEquals(b_dev.path, expected)
|
||||
|
||||
|
||||
class TC_03_FilePool(QubesTestCase):
|
||||
class TC_03_FilePool(qubes.tests.QubesTestCase):
|
||||
""" Test the paths for the default file based pool (``FilePool``).
|
||||
"""
|
||||
|
||||
@ -263,7 +273,6 @@ class TC_03_FilePool(QubesTestCase):
|
||||
shutil.rmtree('/tmp/qubes-test')
|
||||
qubes.config.system_path['qubes_base_dir'] = self._orig_qubes_base_dir
|
||||
|
||||
|
||||
def test_001_pool_exists(self):
|
||||
""" Check if the storage pool was added to the storage pool config """
|
||||
self.assertIn('test-pool', self.app.pools.keys())
|
||||
@ -290,8 +299,7 @@ class TC_03_FilePool(QubesTestCase):
|
||||
""" Check if all the needed image files are created for an AppVm"""
|
||||
|
||||
vmname = self.make_vm_name('appvm')
|
||||
vm = self.app.add_new_vm(qubes.vm.appvm.AppVM,
|
||||
name=vmname,
|
||||
vm = self.app.add_new_vm(qubes.vm.appvm.AppVM, name=vmname,
|
||||
template=self.app.default_template,
|
||||
volume_config={
|
||||
'private': {
|
||||
@ -300,25 +308,17 @@ class TC_03_FilePool(QubesTestCase):
|
||||
'volatile': {
|
||||
'pool': 'test-pool'
|
||||
}
|
||||
},
|
||||
label='red')
|
||||
vm.storage.create()
|
||||
}, label='red')
|
||||
vm.create_on_disk()
|
||||
|
||||
expected_vmdir = os.path.join(self.APPVMS_DIR, vm.name)
|
||||
|
||||
expected_private_origin_path = \
|
||||
os.path.join(expected_vmdir, 'private.img')
|
||||
expected_private_cow_path = \
|
||||
os.path.join(expected_vmdir, 'private-cow.img')
|
||||
expected_private_path = '%s:%s' % (expected_private_origin_path,
|
||||
expected_private_cow_path)
|
||||
expected_private_path = os.path.join(expected_vmdir, 'private.img')
|
||||
self.assertEquals(vm.volumes['private'].path, expected_private_path)
|
||||
self.assertEqualsAndExists(vm.volumes['private'].path_origin,
|
||||
expected_private_origin_path)
|
||||
self.assertEqualsAndExists(vm.volumes['private'].path_cow,
|
||||
expected_private_cow_path)
|
||||
|
||||
expected_volatile_path = os.path.join(expected_vmdir, 'volatile.img')
|
||||
vm.storage.get_pool(vm.volumes['volatile'])\
|
||||
.reset(vm.volumes['volatile'])
|
||||
self.assertEqualsAndExists(vm.volumes['volatile'].path,
|
||||
expected_volatile_path)
|
||||
|
||||
@ -327,8 +327,7 @@ class TC_03_FilePool(QubesTestCase):
|
||||
created propertly by the storage system
|
||||
"""
|
||||
vmname = self.make_vm_name('tmvm')
|
||||
vm = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM,
|
||||
name=vmname,
|
||||
vm = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM, name=vmname,
|
||||
volume_config={
|
||||
'root': {
|
||||
'pool': 'test-pool'
|
||||
@ -339,8 +338,7 @@ class TC_03_FilePool(QubesTestCase):
|
||||
'volatile': {
|
||||
'pool': 'test-pool'
|
||||
}
|
||||
},
|
||||
label='red')
|
||||
}, label='red')
|
||||
vm.create_on_disk()
|
||||
|
||||
expected_vmdir = os.path.join(self.TEMPLATES_DIR, vm.name)
|
||||
@ -349,18 +347,14 @@ class TC_03_FilePool(QubesTestCase):
|
||||
expected_root_cow_path = os.path.join(expected_vmdir, 'root-cow.img')
|
||||
expected_root_path = '%s:%s' % (expected_root_origin_path,
|
||||
expected_root_cow_path)
|
||||
self.assertEquals(vm.volumes['root'].path, expected_root_path)
|
||||
self.assertExist(vm.volumes['root'].path_origin)
|
||||
self.assertEquals(vm.volumes['root'].block_device().path,
|
||||
expected_root_path)
|
||||
self.assertExist(vm.volumes['root'].path)
|
||||
|
||||
expected_private_path = os.path.join(expected_vmdir, 'private.img')
|
||||
self.assertEqualsAndExists(vm.volumes['private'].path,
|
||||
expected_private_path)
|
||||
|
||||
expected_volatile_path = os.path.join(expected_vmdir, 'volatile.img')
|
||||
self.assertEqualsAndExists(vm.volumes['volatile'].path,
|
||||
expected_volatile_path)
|
||||
|
||||
vm.storage.commit_template_changes()
|
||||
expected_rootcow_path = os.path.join(expected_vmdir, 'root-cow.img')
|
||||
self.assertEqualsAndExists(vm.volumes['root'].path_cow,
|
||||
expected_rootcow_path)
|
||||
@ -377,4 +371,5 @@ class TC_03_FilePool(QubesTestCase):
|
||||
def assertExist(self, path):
|
||||
""" Assert that the given path exists. """
|
||||
# :pylint: disable=invalid-name
|
||||
self.assertTrue(os.path.exists(path), "Path %s does not exist" % path)
|
||||
self.assertTrue(
|
||||
os.path.exists(path), "Path {!s} does not exist".format(path))
|
||||
|
@ -45,19 +45,21 @@ def prepare_table(vd_list, full=False):
|
||||
'''
|
||||
output = []
|
||||
if sys.stdout.isatty():
|
||||
output += [('POOL_NAME:VOLUME_ID', 'VOLUME_TYPE', 'VMNAME')]
|
||||
output += [('POOL:VOLUME', 'VMNAME', 'VOLUME_NAME')] # NOQA
|
||||
|
||||
for volume in vd_list:
|
||||
if volume.domains:
|
||||
vmname = volume.domains.pop()
|
||||
output += [(str(volume), volume.volume_type, vmname)]
|
||||
for vmname in volume.domains:
|
||||
vmname, volume_name = volume.domains.pop()
|
||||
output += [(str(volume), vmname, volume_name, volume.revisions)]
|
||||
for tupple in volume.domains:
|
||||
vmname, volume_name = tupple
|
||||
if full or not sys.stdout.isatty():
|
||||
output += [(str(volume), volume.volume_type, vmname)]
|
||||
output += [(str(volume), vmname, volume_name,
|
||||
volume.revisions)]
|
||||
else:
|
||||
output += [('', '', vmname)]
|
||||
output += [('', vmname, volume_name, '', volume.revisions)]
|
||||
else:
|
||||
output += [(str(volume), volume.volume_type)]
|
||||
output += [(str(volume), "")]
|
||||
|
||||
return output
|
||||
|
||||
@ -70,8 +72,11 @@ class VolumeData(object):
|
||||
def __init__(self, volume):
|
||||
self.name = volume.name
|
||||
self.pool = volume.pool
|
||||
self.volume_type = volume.volume_type
|
||||
self.vid = volume.vid
|
||||
if volume.revisions != {}:
|
||||
self.revisions = 'Yes'
|
||||
else:
|
||||
self.revisions = 'No'
|
||||
self.domains = []
|
||||
|
||||
def __str__(self):
|
||||
@ -110,7 +115,7 @@ def list_volumes(args):
|
||||
for volume in domain.attached_volumes:
|
||||
try:
|
||||
volume_data = vd_dict[volume.pool][volume.vid]
|
||||
volume_data.domains += [domain.name]
|
||||
volume_data.domains += [(domain.name, volume.name)]
|
||||
except KeyError:
|
||||
# Skipping volume
|
||||
continue
|
||||
@ -126,6 +131,15 @@ def list_volumes(args):
|
||||
result = [x for p in vd_dict.itervalues() for x in p.itervalues()]
|
||||
qubes.tools.print_table(prepare_table(result, full=args.full))
|
||||
|
||||
def revert_volume(args):
|
||||
volume = args.volume
|
||||
app = args.app
|
||||
try:
|
||||
pool = app.pools[volume.pool]
|
||||
pool.revert(volume)
|
||||
except qubes.storage.StoragePoolException as e:
|
||||
print(e.message, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def attach_volumes(args):
|
||||
''' Called by the parser to execute the :program:`qvm-block attach`
|
||||
@ -173,6 +187,14 @@ def init_list_parser(sub_parsers):
|
||||
list_parser._mutually_exclusive_groups.append(vm_name_group)
|
||||
list_parser.set_defaults(func=list_volumes)
|
||||
|
||||
def init_revert_parser(sub_parsers):
|
||||
revert_parser = sub_parsers.add_parser(
|
||||
'revert', aliases=('rv', 'r'),
|
||||
help='revert volume to previous revision')
|
||||
revert_parser.add_argument(metavar='POOL_NAME:VOLUME_ID', dest='volume',
|
||||
action=qubes.tools.VolumeAction)
|
||||
revert_parser.set_defaults(func=revert_volume)
|
||||
|
||||
|
||||
def get_parser():
|
||||
'''Create :py:class:`argparse.ArgumentParser` suitable for
|
||||
@ -185,6 +207,7 @@ def get_parser():
|
||||
description="For more information see qvm-block command -h",
|
||||
dest='command')
|
||||
init_list_parser(sub_parsers)
|
||||
init_revert_parser(sub_parsers)
|
||||
attach_parser = sub_parsers.add_parser(
|
||||
'attach', help="Attach volume to domain", aliases=('at', 'a'))
|
||||
attach_parser.add_argument('--ro', help='attach device read-only',
|
||||
|
73
qubes/tools/qvm_clone.py
Normal file
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',
|
||||
help='specify the class of the new domain (default: %(default)s)')
|
||||
|
||||
parser.add_argument('--property', '--prop', '-p',
|
||||
parser.add_argument('--property', '--prop',
|
||||
action=qubes.tools.PropertyAction,
|
||||
help='set domain\'s property, like "internal", "memory" or "vcpus"')
|
||||
|
||||
parser.add_argument('--pool', '-P',
|
||||
parser.add_argument('--pool', '-p',
|
||||
action='append',
|
||||
metavar='POOL_NAME:VOLUME_NAME',
|
||||
help='specify the pool to use for a volume')
|
||||
|
||||
parser.add_argument('-P',
|
||||
metavar='POOL_NAME',
|
||||
dest='one_pool',
|
||||
default='',
|
||||
help='change all volume pools to specified pool')
|
||||
|
||||
parser.add_argument('--template', '-t',
|
||||
action=qubes.tools.SinglePropertyAction,
|
||||
help='specify the TemplateVM to use')
|
||||
@ -80,16 +86,18 @@ parser.add_argument('name', metavar='VMNAME',
|
||||
def main(args=None):
|
||||
args = parser.parse_args(args)
|
||||
|
||||
if args.pool:
|
||||
args.properties['volume_config'] = {}
|
||||
pools = {}
|
||||
pool = None
|
||||
if hasattr(args, 'pools') and args.pools:
|
||||
for pool_vol in args.pool:
|
||||
try:
|
||||
pool_name, volume_name = pool_vol.split(':')
|
||||
config = {'pool': pool_name, 'name': volume_name}
|
||||
args.properties['volume_config'][volume_name] = config
|
||||
pools[volume_name] = pool_name
|
||||
except ValueError:
|
||||
parser.error(
|
||||
'Pool argument must be of form: -P pool_name:volume_name')
|
||||
if args.one_pool:
|
||||
pool = args.one_pool
|
||||
|
||||
if 'label' not in args.properties:
|
||||
parser.error('--label option is mandatory')
|
||||
@ -144,7 +152,7 @@ def main(args=None):
|
||||
|
||||
if not args.no_root:
|
||||
try:
|
||||
vm.create_on_disk()
|
||||
vm.create_on_disk(pool, pools)
|
||||
|
||||
# TODO this is file pool specific. Change it to a more general
|
||||
# solution
|
||||
|
@ -21,12 +21,12 @@
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
#
|
||||
|
||||
''' This module contains the AppVM implementation '''
|
||||
|
||||
import copy
|
||||
|
||||
import qubes.events
|
||||
import qubes.vm.qubesvm
|
||||
|
||||
from qubes.config import defaults
|
||||
|
||||
|
||||
@ -39,36 +39,75 @@ class AppVM(qubes.vm.qubesvm.QubesVM):
|
||||
ls_width=31,
|
||||
doc='Template, on which this AppVM is based.')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, app, xml, template=None, **kwargs):
|
||||
self.volume_config = {
|
||||
'root': {
|
||||
'name': 'root',
|
||||
'pool': 'default',
|
||||
'volume_type': 'snapshot',
|
||||
'snap_on_start': True,
|
||||
'save_on_stop': False,
|
||||
'rw': False,
|
||||
'internal': True
|
||||
},
|
||||
'private': {
|
||||
'name': 'private',
|
||||
'pool': 'default',
|
||||
'volume_type': 'origin',
|
||||
'snap_on_start': False,
|
||||
'save_on_stop': True,
|
||||
'rw': True,
|
||||
'source': None,
|
||||
'size': defaults['private_img_size'],
|
||||
'internal': True
|
||||
},
|
||||
'volatile': {
|
||||
'name': 'volatile',
|
||||
'pool': 'default',
|
||||
'volume_type': 'volatile',
|
||||
'size': defaults['root_img_size'],
|
||||
'internal': True
|
||||
'internal': True,
|
||||
'rw': True,
|
||||
},
|
||||
'kernel': {
|
||||
'name': 'kernel',
|
||||
'pool': 'linux-kernel',
|
||||
'volume_type': 'read-only',
|
||||
'snap_on_start': True,
|
||||
'rw': False,
|
||||
'internal': True
|
||||
}
|
||||
}
|
||||
super(AppVM, self).__init__(*args, **kwargs)
|
||||
|
||||
if template is not None:
|
||||
# template is only passed if the AppVM is created, in other cases we
|
||||
# don't need to patch the volume_config because the config is
|
||||
# coming from XML, already as we need it
|
||||
|
||||
for name, conf in self.volume_config.items():
|
||||
tpl_volume = template.volumes[name]
|
||||
|
||||
conf['size'] = tpl_volume.size
|
||||
conf['pool'] = tpl_volume.pool
|
||||
|
||||
has_source = ('source' in conf and conf['source'] is not None)
|
||||
is_snapshot = 'snap_on_start' in conf and conf['snap_on_start']
|
||||
if is_snapshot and not has_source:
|
||||
if tpl_volume.source is not None:
|
||||
conf['source'] = tpl_volume.source
|
||||
else:
|
||||
conf['source'] = tpl_volume.vid
|
||||
|
||||
for name, config in template.volume_config.items():
|
||||
# in case the template vm has more volumes add them to own
|
||||
# config
|
||||
if name not in self.volume_config:
|
||||
self.volume_config[name] = copy.deepcopy(config)
|
||||
if 'vid' in self.volume_config[name]:
|
||||
del self.volume_config[name]['vid']
|
||||
|
||||
super(AppVM, self).__init__(app, xml, **kwargs)
|
||||
if not hasattr(template, 'template') and template is not None:
|
||||
self.template = template
|
||||
if 'source' not in self.volume_config['root']:
|
||||
msg = 'missing source for root volume'
|
||||
raise qubes.exc.QubesException(msg)
|
||||
|
||||
@qubes.events.handler('domain-load')
|
||||
def on_domain_loaded(self, event):
|
||||
|
@ -46,24 +46,32 @@ class DispVM(qubes.vm.qubesvm.QubesVM):
|
||||
'root': {
|
||||
'name': 'root',
|
||||
'pool': 'default',
|
||||
'volume_type': 'snapshot',
|
||||
'snap_on_start': True,
|
||||
'save_on_stop': False,
|
||||
'rw': False,
|
||||
'internal': True
|
||||
},
|
||||
'private': {
|
||||
'name': 'private',
|
||||
'pool': 'default',
|
||||
'volume_type': 'snapshot',
|
||||
'snap_on_start': True,
|
||||
'save_on_stop': False,
|
||||
'internal': True,
|
||||
'rw': True,
|
||||
},
|
||||
'volatile': {
|
||||
'name': 'volatile',
|
||||
'pool': 'default',
|
||||
'volume_type': 'volatile',
|
||||
'internal': True,
|
||||
'size': qubes.config.defaults['root_img_size'] +
|
||||
qubes.config.defaults['private_img_size'],
|
||||
},
|
||||
'kernel': {
|
||||
'name': 'kernel',
|
||||
'pool': 'linux-kernel',
|
||||
'volume_type': 'read-only',
|
||||
'snap_on_start': True,
|
||||
'rw': False,
|
||||
'internal': True
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,7 @@
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import copy
|
||||
import base64
|
||||
import datetime
|
||||
import itertools
|
||||
@ -421,10 +422,15 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
|
||||
name = node.get('name')
|
||||
assert name
|
||||
for key, value in node.items():
|
||||
self.volume_config[name][key] = value
|
||||
# pylint: disable=no-member
|
||||
if value == 'True':
|
||||
self.volume_config[name][key] = True
|
||||
else:
|
||||
self.volume_config[name][key] = value
|
||||
|
||||
for name, conf in volume_config.items():
|
||||
for key, value in conf.items():
|
||||
# pylint: disable=no-member
|
||||
self.volume_config[name][key] = value
|
||||
|
||||
elif volume_config:
|
||||
@ -542,6 +548,8 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
|
||||
# pylint: disable=unused-argument
|
||||
self.init_log()
|
||||
|
||||
self.storage.rename(old_name, new_name)
|
||||
|
||||
if self._libvirt_domain is not None:
|
||||
self.libvirt_domain.undefine()
|
||||
self._libvirt_domain = None
|
||||
@ -549,8 +557,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
|
||||
self._qdb_connection.close()
|
||||
self._qdb_connection = None
|
||||
|
||||
self.storage.rename(old_name, new_name)
|
||||
|
||||
self._update_libvirt_domain()
|
||||
|
||||
if self.autostart:
|
||||
@ -675,7 +681,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
|
||||
self.fire_event_pre('domain-pre-start', preparing_dvm=preparing_dvm,
|
||||
start_guid=start_guid, mem_required=mem_required)
|
||||
|
||||
self.storage.verify_files()
|
||||
self.storage.verify()
|
||||
|
||||
if self.netvm is not None:
|
||||
# pylint: disable = no-member
|
||||
@ -1061,21 +1067,20 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
|
||||
user="root", passio_popen=True, gui=False, wait=True)
|
||||
p.communicate(input=self.default_user)
|
||||
|
||||
def create_on_disk(self, source_template=None):
|
||||
def create_on_disk(self, pool=None, pools=None):
|
||||
'''Create files needed for VM.
|
||||
|
||||
:param qubes.vm.templatevm.TemplateVM source_template: Template to use
|
||||
(if :py:obj:`None`, use domain's own template
|
||||
'''
|
||||
|
||||
if source_template is None and hasattr(self, 'template'):
|
||||
# pylint: disable=no-member
|
||||
source_template = self.template
|
||||
|
||||
self.log.info('Creating directory: {0}'.format(self.dir_path))
|
||||
os.makedirs(self.dir_path, mode=0o775)
|
||||
|
||||
self.storage.create(source_template)
|
||||
if pool or pools:
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.volume_config = _patch_volume_config(self.volume_config, pool,
|
||||
pools)
|
||||
self.storage = qubes.storage.Storage(self)
|
||||
|
||||
self.storage.create()
|
||||
|
||||
self.log.info('Creating icon symlink: {} -> {}'.format(
|
||||
self.icon_path, self.label.icon_path))
|
||||
@ -1085,29 +1090,37 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
|
||||
shutil.copy(self.label.icon_path, self.icon_path)
|
||||
|
||||
# fire hooks
|
||||
self.fire_event('domain-create-on-disk', source_template)
|
||||
self.fire_event('domain-create-on-disk')
|
||||
|
||||
def remove_from_disk(self):
|
||||
'''Remove domain remnants from disk.'''
|
||||
self.fire_event('domain-remove-from-disk')
|
||||
self.storage.remove()
|
||||
shutil.rmtree(self.dir_path)
|
||||
self.storage.remove()
|
||||
|
||||
def clone_disk_files(self, src):
|
||||
def clone_disk_files(self, src, pool=None, pools=None, ):
|
||||
'''Clone files from other vm.
|
||||
|
||||
:param qubes.vm.qubesvm.QubesVM src: source VM
|
||||
'''
|
||||
|
||||
if not self.is_halted():
|
||||
# If the current vm name is not a part of `self.app.domains.keys()`,
|
||||
# then the current vm is in creation process. Calling
|
||||
# `self.is_halted()` at this point, would instantiate libvirt, we want
|
||||
# avoid that.
|
||||
if self.name in self.app.domains.keys() and not self.is_halted():
|
||||
raise qubes.exc.QubesVMNotHaltedError(
|
||||
self, 'Cannot clone a running domain {!r}'.format(self.name))
|
||||
|
||||
if hasattr(src, 'volume_config'):
|
||||
if pool or pools:
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.volume_config = src.volume_config
|
||||
self.volume_config = _patch_volume_config(self.volume_config, pool,
|
||||
pools)
|
||||
|
||||
self.storage = qubes.storage.Storage(self)
|
||||
self.storage.clone(src)
|
||||
self.storage.verify()
|
||||
assert self.volumes != {}
|
||||
|
||||
if src.icon_path is not None \
|
||||
and os.path.exists(src.dir_path) \
|
||||
@ -1580,3 +1593,53 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
|
||||
domain.memory_maximum = self.get_mem_static_max() * 1024
|
||||
|
||||
return qubes.qmemman.algo.prefmem(domain) / 1024
|
||||
|
||||
|
||||
def _clean_volume_config(config):
|
||||
common_attributes = ['name', 'pool', 'size', 'internal', 'removable',
|
||||
'revisions_to_keep', 'rw', 'snap_on_start',
|
||||
'save_on_stop', 'source']
|
||||
config_copy = copy.deepcopy(config)
|
||||
return {k: v for k, v in config_copy.items() if k in common_attributes}
|
||||
|
||||
|
||||
def _patch_pool_config(config, pool=None, pools=None):
|
||||
assert pool is not None or pools is not None
|
||||
is_saveable = 'save_on_stop' in config and config['save_on_stop']
|
||||
is_resetable = not ('snap_on_start' in config and # volatile
|
||||
config['snap_on_start'] and not is_saveable)
|
||||
|
||||
is_exportable = is_saveable or is_resetable
|
||||
|
||||
name = config['name']
|
||||
|
||||
if pool and is_exportable:
|
||||
config['pool'] = str(pool)
|
||||
elif pool and not is_exportable:
|
||||
pass
|
||||
elif pools and name in pools.keys():
|
||||
if is_exportable:
|
||||
config['pool'] = str(pools[name])
|
||||
else:
|
||||
msg = "Can't clone a snapshot volume {!s} to pool {!s} " \
|
||||
.format(name, pools[name])
|
||||
raise qubes.exc.QubesException(msg)
|
||||
return config
|
||||
|
||||
def _patch_volume_config(volume_config, pool=None, pools=None):
|
||||
assert not (pool and pools), \
|
||||
'You can not pass pool & pools parameter at same time'
|
||||
assert pool or pools
|
||||
|
||||
result = {}
|
||||
|
||||
for name, config in volume_config.items():
|
||||
# copy only the subset of volume_config key/values
|
||||
dst_config = _clean_volume_config(config)
|
||||
|
||||
if pool is not None or pools is not None:
|
||||
dst_config = _patch_pool_config(dst_config, pool, pools)
|
||||
|
||||
result[name] = dst_config
|
||||
|
||||
return result
|
||||
|
@ -34,25 +34,34 @@ class StandaloneVM(qubes.vm.qubesvm.QubesVM):
|
||||
'root': {
|
||||
'name': 'root',
|
||||
'pool': 'default',
|
||||
'volume_type': 'origin',
|
||||
'snap_on_start': False,
|
||||
'save_on_stop': True,
|
||||
'rw': True,
|
||||
'source': None,
|
||||
'internal': True,
|
||||
'size': qubes.config.defaults['root_img_size'],
|
||||
},
|
||||
'private': {
|
||||
'name': 'private',
|
||||
'pool': 'default',
|
||||
'volume_type': 'origin',
|
||||
'snap_on_start': False,
|
||||
'save_on_stop': True,
|
||||
'rw': True,
|
||||
'source': None,
|
||||
'internal': True,
|
||||
'size': qubes.config.defaults['private_img_size'],
|
||||
},
|
||||
'volatile': {
|
||||
'name': 'volatile',
|
||||
'pool': 'default',
|
||||
'volume_type': 'volatile',
|
||||
'internal': True,
|
||||
'size': qubes.config.defaults['root_img_size'],
|
||||
},
|
||||
'kernel': {
|
||||
'name': 'kernel',
|
||||
'pool': 'linux-kernel',
|
||||
'volume_type': 'read-only',
|
||||
'rw': False,
|
||||
'internal': True
|
||||
}
|
||||
}
|
||||
super(StandaloneVM, self).__init__(*args, **kwargs)
|
||||
|
@ -60,39 +60,41 @@ class TemplateVM(QubesVM):
|
||||
'root': {
|
||||
'name': 'root',
|
||||
'pool': 'default',
|
||||
'volume_type': 'origin',
|
||||
'snap_on_start': False,
|
||||
'save_on_stop': True,
|
||||
'rw': True,
|
||||
'source': None,
|
||||
'size': defaults['root_img_size'],
|
||||
'internal': True
|
||||
},
|
||||
'private': {
|
||||
'name': 'private',
|
||||
'pool': 'default',
|
||||
'volume_type': 'read-write',
|
||||
'snap_on_start': False,
|
||||
'save_on_stop': True,
|
||||
'rw': True,
|
||||
'source': None,
|
||||
'size': defaults['private_img_size'],
|
||||
'revisions_to_keep': 0,
|
||||
'internal': True
|
||||
},
|
||||
'volatile': {
|
||||
'name': 'volatile',
|
||||
'pool': 'default',
|
||||
'size': defaults['root_img_size'],
|
||||
'volume_type': 'volatile',
|
||||
'internal': True
|
||||
'internal': True,
|
||||
'rw': True,
|
||||
},
|
||||
'kernel': {
|
||||
'name': 'kernel',
|
||||
'pool': 'linux-kernel',
|
||||
'volume_type': 'read-only',
|
||||
'internal': True
|
||||
'snap_on_start': True,
|
||||
'internal': True,
|
||||
'rw': False
|
||||
}
|
||||
}
|
||||
super(TemplateVM, self).__init__(*args, **kwargs)
|
||||
|
||||
def clone_disk_files(self, src):
|
||||
super(TemplateVM, self).clone_disk_files(src)
|
||||
|
||||
# Create root-cow.img
|
||||
self.commit_changes()
|
||||
|
||||
def commit_changes(self):
|
||||
'''Commit changes to template'''
|
||||
self.log.debug('commit_changes()')
|
||||
|
@ -247,6 +247,7 @@ fi
|
||||
%{python_sitelib}/qubes/tools/qvm_block.py*
|
||||
%{python_sitelib}/qubes/tools/qvm_create.py*
|
||||
%{python_sitelib}/qubes/tools/qvm_features.py*
|
||||
%{python_sitelib}/qubes/tools/qvm_clone.py*
|
||||
%{python_sitelib}/qubes/tools/qvm_kill.py*
|
||||
%{python_sitelib}/qubes/tools/qvm_ls.py*
|
||||
%{python_sitelib}/qubes/tools/qvm_pause.py*
|
||||
|
Loading…
Reference in New Issue
Block a user