Merge branch 'core3-storage3'
This commit is contained in:
commit
dddd94b339
@ -16,6 +16,7 @@ manpages and API documentation. For primary user documentation, see
|
||||
qubes
|
||||
qubes-vm/index
|
||||
qubes-events
|
||||
qubes-storage
|
||||
qubes-exc
|
||||
qubes-ext
|
||||
qubes-log
|
||||
|
145
doc/qubes-storage.rst
Normal file
145
doc/qubes-storage.rst
Normal file
@ -0,0 +1,145 @@
|
||||
:py:mod:`qubes.storage` -- Qubes data storage
|
||||
=============================================
|
||||
|
||||
Qubes provide extensible API for domains data storage. Each domain have
|
||||
multiple storage volumes, for different purposes. Each volume is provided by
|
||||
some storage pool. Qubes support different storage pool drivers, and it's
|
||||
possible to register additional 3rd-party drivers.
|
||||
|
||||
Domain's storage volumes:
|
||||
|
||||
- `root` - this is where operating system is installed. The volume is
|
||||
available read-write to :py:class:`~qubes.vm.templatevm.TemplateVM` and
|
||||
:py:class:`~qubes.vm.standalonevm.StandaloneVM`, and read-only to others
|
||||
(:py:class:`~qubes.vm.appvm.AppVM` and :py:class:`~qubes.vm.dispvm.DispVM`).
|
||||
- `private` - this is where domain's data live. The volume is available
|
||||
read-write to all domain classes (including :py:class:`~qubes.vm.dispvm.DispVM`,
|
||||
but data written there is discarded on domain shutdown).
|
||||
- `volatile` - this is used for any data that do not to persist. This include
|
||||
swap, copy-on-write layer for `root` volume etc.
|
||||
- `kernel` - domain boot files - operating system kernel, initial ramdisk,
|
||||
kernel modules etc. This volume is provided read-only and should be provided by
|
||||
a storage pool respecting :py:attr:`qubes.vm.qubesvm.QubesVM.kernel` property.
|
||||
|
||||
Storage pool concept
|
||||
--------------------
|
||||
|
||||
Storage pool is responsible for managing its volumes. Qubes have defined
|
||||
storage pool driver API, allowing to put domains storage in various places. By
|
||||
default two drivers are provided: :py:class:`qubes.storage.file.FilePool`
|
||||
(named `file`) and :py:class:`qubes.storage.lvm.ThinPool` (named `lvm_thin`).
|
||||
But the API allow to implement variety of other drivers (like additionally
|
||||
encrypted storage, external disk, drivers using special features of some
|
||||
filesystems like btrfs, etc).
|
||||
|
||||
Most of storage API focus on storage volumes. Each volume have at least those
|
||||
properties:
|
||||
- :py:attr:`~qubes.storage.Volume.rw` - should the volume be available
|
||||
read-only or read-write to the domain
|
||||
- :py:attr:`~qubes.storage.Volume.snap_on_start` - should the domain start
|
||||
with its own state of the volume, or rather a snapshot of its template volume
|
||||
(pointed by a :py:attr:`~qubes.storage.Volume.source` property). This can be
|
||||
set to `True` only if a domain do have `template` property (AppVM and DispVM).
|
||||
If the domain's template is running already, the snapshot should be made out of
|
||||
the template's before its startup.
|
||||
- :py:attr:`~qubes.storage.Volume.save_on_stop` - should the volume state be
|
||||
saved or discarded on domain
|
||||
stop. In either case, while the domain is running, volume's current state
|
||||
should not be committed immediately. This is to allow creating snapshots of the
|
||||
volume's state from before domain start (see
|
||||
:py:attr:`~qubes.storage.Volume.snap_on_start`).
|
||||
- :py:attr:`~qubes.storage.Volume.revisions_to_keep` - number of volume
|
||||
revisions to keep. If greater than zero, at each domain stop (and if
|
||||
:py:attr:`~qubes.storage.Volume.save_on_stop` is `True`) new revision is saved
|
||||
and old ones exceeding :py:attr:`~qubes.storage.Volume.revisions_to_keep` limit
|
||||
are removed.
|
||||
- :py:attr:`~qubes.storage.Volume.source` - source volume for
|
||||
:py:attr:`~qubes.storage.Volume.snap_on_start` volumes
|
||||
- :py:attr:`~qubes.storage.Volume.vid` - pool specific volume identifier, must
|
||||
be unique inside given pool
|
||||
- :py:attr:`~qubes.storage.Volume.pool` - storage pool object owning this volume
|
||||
- :py:attr:`~qubes.storage.Volume.name` - name of the volume inside owning
|
||||
domain (like `root`, or `private`)
|
||||
- :py:attr:`~qubes.storage.Volume.size` - size of the volume, in bytes
|
||||
|
||||
Storage pool driver may define additional properties.
|
||||
|
||||
Storage pool driver API
|
||||
-----------------------
|
||||
|
||||
Storage pool driver need to implement two classes:
|
||||
- pool class - inheriting from :py:class:`qubes.storage.Pool`
|
||||
- volume class - inheriting from :py:class:`qubes.storage.Volume`
|
||||
|
||||
Pool class should be registered with `qubes.storage` entry_point, under the
|
||||
name of storage pool driver. Volume class instances should be returned by
|
||||
:py:meth:`qubes.storage.Pool.init_volume` method of pool class instance.
|
||||
|
||||
Methods required to be implemented by the pool class:
|
||||
- :py:meth:`~qubes.storage.Pool.init_volume` - return instance of appropriate
|
||||
volume class; this method should not alter any persistent disk state, it is
|
||||
used to instantiate both existing volumes and create new ones
|
||||
- :py:meth:`~qubes.storage.Pool.setup` - setup new storage pool
|
||||
- :py:meth:`~qubes.storage.Pool.destroy` - destroy storage pool
|
||||
|
||||
Methods and properties required to be implemented by the volume class:
|
||||
- :py:meth:`~qubes.storage.Volume.create` - create volume on disk
|
||||
- :py:meth:`~qubes.storage.Volume.remove` - remove volume from disk
|
||||
- :py:meth:`~qubes.storage.Volume.start` - prepare the volume for domain start;
|
||||
this include making a snapshot if
|
||||
:py:attr:`~qubes.storage.Volume.snap_on_start` is `True`
|
||||
- :py:meth:`~qubes.storage.Volume.stop` - cleanup after domain shutdown; this
|
||||
include committing changes to the volume if
|
||||
:py:attr:`~qubes.storage.Volume.save_on_stop` is `True`
|
||||
- :py:meth:`~qubes.storage.Volume.export` - return a path to be read to extract
|
||||
volume data; for complex formats, this can be a pipe (connected to some
|
||||
data-extracting process)
|
||||
- :py:meth:`~qubes.storage.Volume.import_data` - return a path the data should
|
||||
be written to, to import volume data; for complex formats, this can be pipe
|
||||
(connected to some data-importing process)
|
||||
- :py:meth:`~qubes.storage.Volume.import_data_end` - finish data import
|
||||
operation (cleanup temporary files etc); this methods is called always after
|
||||
:py:meth:`~qubes.storage.Volume.import_data` regardless if operation was
|
||||
successful or not
|
||||
- :py:meth:`~qubes.storage.Volume.import_volume` - import data from another volume
|
||||
- :py:meth:`~qubes.storage.Volume.resize` - resize volume
|
||||
- :py:meth:`~qubes.storage.Volume.revert` - revert volume state to a given revision
|
||||
- :py:attr:`~qubes.storage.Volume.revisions` - collection of volume revisions (to use
|
||||
with :py:meth:`qubes.storage.Volume.revert`)
|
||||
- :py:meth:`~qubes.storage.Volume.is_dirty` - is volume properly committed
|
||||
after domain shutdown? Applies only to volumes with
|
||||
:py:attr:`~qubes.storage.Volume.save_on_stop` set to `True`
|
||||
- :py:meth:`~qubes.storage.Volume.is_outdated` - have the source volume started
|
||||
since domain startup? applies only to volumes with
|
||||
:py:attr:`~qubes.storage.Volume.snap_on_start` set to `True`
|
||||
- :py:attr:`~qubes.storage.Volume.config` - volume configuration, this should
|
||||
be enough to later reinstantiate the same volume object
|
||||
- :py:meth:`~qubes.storage.Volume.block_device` - return
|
||||
:py:class:`qubes.storage.BlockDevice` instance required to configure volume in
|
||||
libvirt
|
||||
|
||||
Some storage pool drivers can provide limited functionality only - for example
|
||||
support only `volatile` volumes (those with
|
||||
:py:attr:`~qubes.storage.Volume.snap_on_start` is `False`,
|
||||
:py:attr:`~qubes.storage.Volume.save_on_stop` is `False`, and
|
||||
:py:attr:`~qubes.storage.Volume.rw` is `True`). In that case, it should raise
|
||||
:py:exc:`NotImplementedError` in :py:meth:`qubes.storage.Pool.init_volume` when
|
||||
trying to instantiate unsupported volume.
|
||||
|
||||
Note that pool driver should be prepared to recover from power loss before
|
||||
stopping a domain - so, if volume have
|
||||
:py:attr:`~qubes.storage.Volume.save_on_stop` is `True`, and
|
||||
:py:meth:`qubes.storage.Volume.stop` wasn't called, next
|
||||
:py:meth:`~qubes.storage.Volume.start` should pick up previous (not committed)
|
||||
state.
|
||||
|
||||
See specific methods documentation for details.
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: qubes.storage
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. vim: ts=3 sw=3 et
|
@ -275,7 +275,7 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
|
||||
volume = self.dest.volumes[self.arg]
|
||||
# properties defined in API
|
||||
volume_properties = [
|
||||
'pool', 'vid', 'size', 'usage', 'rw', 'internal', 'source',
|
||||
'pool', 'vid', 'size', 'usage', 'rw', 'source',
|
||||
'save_on_stop', 'snap_on_start']
|
||||
return ''.join('{}={}\n'.format(key, getattr(volume, key)) for key in
|
||||
volume_properties)
|
||||
|
66
qubes/app.py
66
qubes/app.py
@ -28,6 +28,7 @@ import grp
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
@ -548,6 +549,46 @@ class VMCollection(object):
|
||||
'https://xkcd.com/221/',
|
||||
'http://dilbert.com/strip/2001-10-25')[random.randint(0, 1)])
|
||||
|
||||
def _default_pool(app):
|
||||
''' Default storage pool.
|
||||
|
||||
1. If there is one named 'default', use it.
|
||||
2. Check if root fs is on LVM thin - use that
|
||||
3. Look for file-based pool pointing /var/lib/qubes
|
||||
4. Fail
|
||||
'''
|
||||
if 'default' in app.pools:
|
||||
return app.pools['default']
|
||||
else:
|
||||
rootfs = os.stat('/')
|
||||
root_major = (rootfs.st_dev & 0xff00) >> 8
|
||||
root_minor = rootfs.st_dev & 0xff
|
||||
for pool in app.pools.values():
|
||||
if pool.config.get('driver', None) != 'lvm_thin':
|
||||
continue
|
||||
thin_pool = pool.config['thin_pool']
|
||||
thin_volumes = subprocess.check_output(
|
||||
['lvs', '--select', 'pool_lv=' + thin_pool,
|
||||
'-o', 'lv_kernel_major,lv_kernel_minor', '--noheadings'])
|
||||
if any((str(root_major), str(root_minor)) == thin_vol.split()
|
||||
for thin_vol in thin_volumes.splitlines()):
|
||||
return pool
|
||||
# not a thin volume? look for file pools
|
||||
for pool in app.pools.values():
|
||||
if pool.config.get('driver', None) != 'file':
|
||||
continue
|
||||
if pool.config['dir_path'] == '/var/lib/qubes':
|
||||
return pool
|
||||
raise AttributeError('Cannot determine default storage pool')
|
||||
|
||||
def _setter_pool(app, prop, value):
|
||||
if isinstance(value, qubes.storage.Pool):
|
||||
return value
|
||||
try:
|
||||
return app.pools[value]
|
||||
except KeyError:
|
||||
raise qubes.exc.QubesPropertyValueError(app, prop, value,
|
||||
'No such storage pool')
|
||||
|
||||
class Qubes(qubes.PropertyHolder):
|
||||
'''Main Qubes application
|
||||
@ -629,6 +670,27 @@ class Qubes(qubes.PropertyHolder):
|
||||
default_dispvm = qubes.VMProperty('default_dispvm', load_stage=3,
|
||||
doc='Default DispVM base for service calls')
|
||||
|
||||
default_pool = qubes.property('default_pool', load_stage=3,
|
||||
default=_default_pool,
|
||||
doc='Default storage pool')
|
||||
|
||||
default_pool_private = qubes.property('default_pool_private', load_stage=3,
|
||||
default=lambda app: app.default_pool,
|
||||
doc='Default storage pool for private volumes')
|
||||
|
||||
default_pool_root = qubes.property('default_pool_root', load_stage=3,
|
||||
default=lambda app: app.default_pool,
|
||||
doc='Default storage pool for root volumes')
|
||||
|
||||
default_pool_volatile = qubes.property('default_pool_volatile',
|
||||
load_stage=3,
|
||||
default=lambda app: app.default_pool,
|
||||
doc='Default storage pool for volatile volumes')
|
||||
|
||||
default_pool_kernel = qubes.property('default_pool_kernel', load_stage=3,
|
||||
default=lambda app: app.default_pool,
|
||||
doc='Default storage pool for kernel volumes')
|
||||
|
||||
# TODO #1637 #892
|
||||
check_updates_vm = qubes.property('check_updates_vm',
|
||||
type=bool, setter=qubes.property.bool,
|
||||
@ -662,7 +724,7 @@ class Qubes(qubes.PropertyHolder):
|
||||
else:
|
||||
self._store = os.environ.get('QUBES_XML_PATH',
|
||||
os.path.join(
|
||||
qubes.config.system_path['qubes_base_dir'],
|
||||
qubes.config.qubes_base_dir,
|
||||
qubes.config.system_path['qubes_store_filename']))
|
||||
|
||||
super(Qubes, self).__init__(xml=None, **kwargs)
|
||||
@ -907,6 +969,8 @@ class Qubes(qubes.PropertyHolder):
|
||||
for name, config in qubes.config.defaults['pool_configs'].items():
|
||||
self.pools[name] = self._get_pool(**config)
|
||||
|
||||
self.default_pool_kernel = 'linux-kernel'
|
||||
|
||||
self.domains.add(
|
||||
qubes.vm.adminvm.AdminVM(self, None, label='black'))
|
||||
|
||||
|
@ -36,8 +36,6 @@ system_path = {
|
||||
'qrexec_client_path': '/usr/lib/qubes/qrexec-client',
|
||||
'qubesdb_daemon_path': '/usr/sbin/qubesdb-daemon',
|
||||
|
||||
'qubes_base_dir': qubes_base_dir,
|
||||
|
||||
# Relative to qubes_base_dir
|
||||
'qubes_appvms_dir': 'appvms',
|
||||
'qubes_templates_dir': 'vm-templates',
|
||||
|
@ -78,25 +78,26 @@ class Volume(object):
|
||||
domain = None
|
||||
path = None
|
||||
script = None
|
||||
#: disk space used by this volume, can be smaller than :py:attr:`size`
|
||||
#: for sparse volumes
|
||||
usage = 0
|
||||
|
||||
def __init__(self, name, pool, vid, internal=False, removable=False,
|
||||
def __init__(self, name, pool, vid,
|
||||
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 name: The name of the volume inside owning domain
|
||||
:param Pool pool: The pool object
|
||||
: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 Volume source: other volume in same pool, or None
|
||||
:param bool snap_on_start: Create a snapshot from source on
|
||||
start, instead of using volume own data
|
||||
:param bool save_on_stop: Write changes to the volume in
|
||||
vm.stop(), otherwise - discard
|
||||
:param Volume source: other volume in same pool to make snapshot
|
||||
from, required if *snap_on_start*=`True`
|
||||
:param str/int size: Size of the volume
|
||||
|
||||
'''
|
||||
@ -106,16 +107,34 @@ class Volume(object):
|
||||
assert source is None or (isinstance(source, Volume)
|
||||
and source.pool == pool)
|
||||
|
||||
if snap_on_start and source is None:
|
||||
msg = "snap_on_start specified on {!r} but no volume source set"
|
||||
msg = msg.format(name)
|
||||
raise StoragePoolException(msg)
|
||||
elif not snap_on_start and source is not None:
|
||||
msg = "source specified on {!r} but no snap_on_start set"
|
||||
msg = msg.format(name)
|
||||
raise StoragePoolException(msg)
|
||||
|
||||
#: Name of the volume in a domain it's attached to (like `root` or
|
||||
#: `private`).
|
||||
self.name = str(name)
|
||||
#: :py:class:`Pool` instance owning this volume
|
||||
self.pool = pool
|
||||
self.internal = internal
|
||||
self.removable = removable
|
||||
#: How many revisions of the volume to keep. Each revision is created
|
||||
# at :py:meth:`stop`, if :py:attr:`save_on_stop` is True
|
||||
self.revisions_to_keep = int(revisions_to_keep)
|
||||
#: Should this volume be writable by domain.
|
||||
self.rw = rw
|
||||
#: Should volume state be saved or discarded at :py:meth:`stop`
|
||||
self.save_on_stop = save_on_stop
|
||||
self._size = int(size)
|
||||
#: Should the volume state be initialized with a snapshot of
|
||||
#: same-named volume of domain's template.
|
||||
self.snap_on_start = snap_on_start
|
||||
#: source volume for :py:attr:`snap_on_start` volumes
|
||||
self.source = source
|
||||
#: Volume unique (inside given pool) identifier
|
||||
self.vid = vid
|
||||
|
||||
def __eq__(self, other):
|
||||
@ -142,6 +161,10 @@ class Volume(object):
|
||||
def create(self):
|
||||
''' Create the given volume on disk.
|
||||
|
||||
This method is called only once in the volume lifetime. Before
|
||||
calling this method, no data on disk should be touched (in
|
||||
context of this volume).
|
||||
|
||||
This can be implemented as a coroutine.
|
||||
'''
|
||||
raise self._not_implemented("create")
|
||||
@ -152,24 +175,38 @@ class Volume(object):
|
||||
This can be implemented as a coroutine.'''
|
||||
raise self._not_implemented("remove")
|
||||
|
||||
def commit(self):
|
||||
''' Write the snapshot to disk
|
||||
|
||||
This can be implemented as a coroutine.'''
|
||||
raise self._not_implemented("commit")
|
||||
|
||||
def export(self):
|
||||
''' Returns an object that can be `open()`. '''
|
||||
''' Returns a path to read the volume data from.
|
||||
|
||||
Reading from this path when domain owning this volume is
|
||||
running (i.e. when :py:meth:`is_dirty` is True) should return the
|
||||
data from before domain startup.
|
||||
|
||||
Reading from the path returned by this method should return the
|
||||
volume data. If extracting volume data require something more
|
||||
than just reading from file (for example connecting to some other
|
||||
domain, or decompressing the data), the returned path may be a pipe.
|
||||
'''
|
||||
raise self._not_implemented("export")
|
||||
|
||||
def import_data(self):
|
||||
''' Returns an object that can be `open()`. '''
|
||||
''' Returns a path to overwrite volume data.
|
||||
|
||||
This method is called after volume was already :py:meth:`create`-ed.
|
||||
|
||||
Writing to this path should overwrite volume data. If importing
|
||||
volume data require something more than just writing to a file (
|
||||
for example connecting to some other domain, or converting data
|
||||
on the fly), the returned path may be a pipe.
|
||||
'''
|
||||
raise self._not_implemented("import")
|
||||
|
||||
def import_data_end(self, success):
|
||||
''' End data import operation. This may be used by pool
|
||||
''' End the data import operation. This may be used by pool
|
||||
implementation to commit changes, cleanup temporary files etc.
|
||||
|
||||
This method is called regardless the operation was successful or not.
|
||||
|
||||
:param success: True if data import was successful, otherwise False
|
||||
'''
|
||||
# by default do nothing
|
||||
@ -179,54 +216,60 @@ class Volume(object):
|
||||
''' Imports data from a different volume (possibly in a different
|
||||
pool.
|
||||
|
||||
The needs to be create()d first.
|
||||
The volume needs to be create()d first.
|
||||
|
||||
This can be implemented as a coroutine. '''
|
||||
# pylint: disable=unused-argument
|
||||
raise self._not_implemented("import_volume")
|
||||
|
||||
def is_dirty(self):
|
||||
''' Return `True` if volume was not properly shutdown and commited '''
|
||||
''' Return `True` if volume was not properly shutdown and committed.
|
||||
|
||||
This include the situation when domain owning the volume is still
|
||||
running.
|
||||
|
||||
'''
|
||||
raise self._not_implemented("is_dirty")
|
||||
|
||||
def is_outdated(self):
|
||||
''' Returns `True` if the currently used `volume.source` of a snapshot
|
||||
volume is outdated.
|
||||
''' Returns `True` if this snapshot of a source volume (for
|
||||
`snap_on_start`=True) is outdated.
|
||||
'''
|
||||
raise self._not_implemented("is_outdated")
|
||||
|
||||
def recover(self):
|
||||
''' Try to recover a :py:class:`Volume` or :py:class:`SnapVolume` '''
|
||||
raise self._not_implemented("recover")
|
||||
|
||||
def reset(self):
|
||||
''' Drop and recreate volume without copying it's content from source.
|
||||
'''
|
||||
raise self._not_implemented("reset")
|
||||
|
||||
def resize(self, size):
|
||||
''' Expands volume, throws
|
||||
:py:class:`qubes.storage.StoragePoolException` if
|
||||
given size is less than current_size
|
||||
|
||||
This can be implemented as a coroutine.
|
||||
|
||||
:param int size: new size in bytes
|
||||
'''
|
||||
# pylint: disable=unused-argument
|
||||
raise self._not_implemented("resize")
|
||||
|
||||
def revert(self, revision=None):
|
||||
''' Revert volume to previous revision '''
|
||||
''' Revert volume to previous revision
|
||||
|
||||
:param revision: revision to revert volume to, see :py:attr:`revisions`
|
||||
'''
|
||||
# pylint: disable=unused-argument
|
||||
raise self._not_implemented("revert")
|
||||
|
||||
def start(self):
|
||||
''' Do what ever is needed on start
|
||||
''' Do what ever is needed on start.
|
||||
|
||||
This include making a snapshot of template's volume if
|
||||
:py:attr:`snap_on_start` is set.
|
||||
|
||||
This can be implemented as a coroutine.'''
|
||||
raise self._not_implemented("start")
|
||||
|
||||
def stop(self):
|
||||
''' Do what ever is needed on stop
|
||||
''' Do what ever is needed on stop.
|
||||
|
||||
This include committing data if :py:attr:`save_on_stop` is set.
|
||||
|
||||
This can be implemented as a coroutine.'''
|
||||
|
||||
@ -245,12 +288,14 @@ class Volume(object):
|
||||
|
||||
@property
|
||||
def revisions(self):
|
||||
''' Returns a `dict` containing revision identifiers and paths '''
|
||||
''' Returns a dict containing revision identifiers and time of their
|
||||
creation '''
|
||||
msg = "{!s} has revisions not implemented".format(self.__class__)
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
''' Volume size in bytes '''
|
||||
return self._size
|
||||
|
||||
@size.setter
|
||||
@ -262,29 +307,19 @@ class Volume(object):
|
||||
@property
|
||||
def config(self):
|
||||
''' return config data for serialization to qubes.xml '''
|
||||
result = {'name': self.name, 'pool': str(self.pool), 'vid': self.vid, }
|
||||
|
||||
if self.internal:
|
||||
result['internal'] = self.internal
|
||||
|
||||
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
|
||||
result = {
|
||||
'name': self.name,
|
||||
'pool': str(self.pool),
|
||||
'vid': self.vid,
|
||||
'revisions_to_keep': self.revisions_to_keep,
|
||||
'rw': self.rw,
|
||||
'save_on_stop': self.save_on_stop,
|
||||
'snap_on_start': self.snap_on_start,
|
||||
}
|
||||
|
||||
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'] = str(self.source)
|
||||
|
||||
@ -335,7 +370,13 @@ class Storage(object):
|
||||
|
||||
if 'name' not in volume_config:
|
||||
volume_config['name'] = name
|
||||
pool = self.vm.app.get_pool(volume_config['pool'])
|
||||
if 'pool' not in volume_config:
|
||||
pool = getattr(self.vm.app, 'default_pool_' + name)
|
||||
else:
|
||||
pool = self.vm.app.get_pool(volume_config['pool'])
|
||||
if 'internal' in volume_config:
|
||||
# migrate old config
|
||||
del volume_config['internal']
|
||||
volume = pool.init_volume(self.vm, volume_config)
|
||||
self.vm.volumes[name] = volume
|
||||
return volume
|
||||
@ -486,13 +527,6 @@ class Storage(object):
|
||||
|
||||
return result
|
||||
|
||||
def rename(self, old_name, new_name):
|
||||
''' Notify the pools that the domain was renamed '''
|
||||
volumes = self.vm.volumes
|
||||
for name, volume in volumes.items():
|
||||
pool = volume.pool
|
||||
volumes[name] = pool.rename(volume, old_name, new_name)
|
||||
|
||||
@asyncio.coroutine
|
||||
def verify(self):
|
||||
'''Verify that the storage is sane.
|
||||
@ -559,19 +593,6 @@ class Storage(object):
|
||||
if futures:
|
||||
yield from asyncio.wait(futures)
|
||||
|
||||
@asyncio.coroutine
|
||||
def commit(self):
|
||||
''' Makes changes to an 'origin' volume persistent '''
|
||||
futures = []
|
||||
for volume in self.vm.volumes.values():
|
||||
if volume.save_on_stop:
|
||||
ret = volume.commit()
|
||||
if asyncio.iscoroutine(ret):
|
||||
futures.append(ret)
|
||||
|
||||
if futures:
|
||||
yield asyncio.wait(futures)
|
||||
|
||||
def unused_frontend(self):
|
||||
''' Find an unused device name '''
|
||||
unused_frontends = self.AVAILABLE_FRONTENDS.difference(
|
||||
@ -722,10 +743,6 @@ class Pool(object):
|
||||
'''
|
||||
raise self._not_implemented("init_volume")
|
||||
|
||||
def rename(self, volume, old_name, new_name):
|
||||
''' Called when the domain changes its name '''
|
||||
raise self._not_implemented("rename")
|
||||
|
||||
def setup(self):
|
||||
''' Called when adding a pool to the system. Use this for implementation
|
||||
specific set up.
|
||||
|
@ -37,6 +37,20 @@ BLKSIZE = 512
|
||||
|
||||
class FilePool(qubes.storage.Pool):
|
||||
''' File based 'original' disk implementation
|
||||
|
||||
Volumes are stored in sparse files. Additionally device-mapper is used for
|
||||
applying copy-on-write layer.
|
||||
|
||||
Quick reference on device-mapper layers:
|
||||
|
||||
snap_on_start save_on_stop layout
|
||||
yes yes not supported
|
||||
no yes snapshot-origin(volume.img, volume-cow.img)
|
||||
yes no snapshot(
|
||||
snapshot(source.img, source-cow.img),
|
||||
volume-cow.img)
|
||||
no no volume.img directly
|
||||
|
||||
''' # pylint: disable=protected-access
|
||||
driver = 'file'
|
||||
|
||||
@ -57,16 +71,18 @@ class FilePool(qubes.storage.Pool):
|
||||
}
|
||||
|
||||
def init_volume(self, vm, volume_config):
|
||||
if volume_config.get('snap_on_start', False) and \
|
||||
volume_config.get('save_on_stop', False):
|
||||
raise NotImplementedError(
|
||||
'snap_on_start + save_on_stop not supported by file driver')
|
||||
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']:
|
||||
if not volume_config.get('save_on_stop', False):
|
||||
volume_config['revisions_to_keep'] = 0
|
||||
except KeyError:
|
||||
pass
|
||||
@ -74,31 +90,15 @@ class FilePool(qubes.storage.Pool):
|
||||
if 'revisions_to_keep' not in volume_config:
|
||||
volume_config['revisions_to_keep'] = self.revisions_to_keep
|
||||
|
||||
if int(volume_config['revisions_to_keep']) > 1:
|
||||
raise NotImplementedError(
|
||||
'FilePool supports maximum 1 volume revision to keep')
|
||||
|
||||
volume_config['pool'] = self
|
||||
volume = FileVolume(**volume_config)
|
||||
self._volumes += [volume]
|
||||
return volume
|
||||
|
||||
def rename(self, volume, old_name, new_name):
|
||||
assert issubclass(volume.__class__, FileVolume)
|
||||
subdir, _, volume_path = volume.vid.split('/', 2)
|
||||
|
||||
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, 0o755)
|
||||
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.vid = os.path.join(subdir, new_name, volume_path)
|
||||
|
||||
return volume
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
@ -150,59 +150,37 @@ class FileVolume(qubes.storage.Volume):
|
||||
''' Parent class for the xen volumes implementation which expects a
|
||||
`target_dir` param on initialization. '''
|
||||
|
||||
def __init__(self, dir_path, backward_comp=False, **kwargs):
|
||||
def __init__(self, dir_path, **kwargs):
|
||||
self.dir_path = dir_path
|
||||
self.backward_comp = backward_comp
|
||||
assert self.dir_path, "dir_path not specified"
|
||||
super(FileVolume, self).__init__(**kwargs)
|
||||
|
||||
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:
|
||||
if self.snap_on_start:
|
||||
img_name = self.source.vid + '-cow.img'
|
||||
self.path_source_cow = os.path.join(self.dir_path, img_name)
|
||||
elif self._is_volume or self._is_volatile:
|
||||
pass
|
||||
elif self._is_origin:
|
||||
pass
|
||||
else:
|
||||
assert False, 'This should not happen'
|
||||
|
||||
def create(self):
|
||||
assert isinstance(self.size, int) and self.size > 0, \
|
||||
'Volatile volume size must be > 0'
|
||||
if self._is_origin:
|
||||
'Volume size must be > 0'
|
||||
if not self.snap_on_start:
|
||||
create_sparse_file(self.path, self.size)
|
||||
# path_cow not needed only in volatile volume
|
||||
if self.save_on_stop or self.snap_on_start:
|
||||
create_sparse_file(self.path_cow, self.size)
|
||||
elif not self._is_snapshot:
|
||||
if self.source is not None:
|
||||
source_path = os.path.join(self.dir_path,
|
||||
self.source.vid + '.img')
|
||||
copy_file(source_path, self.path)
|
||||
elif self._is_volatile:
|
||||
pass
|
||||
else:
|
||||
create_sparse_file(self.path, self.size)
|
||||
|
||||
def remove(self):
|
||||
if not self.internal:
|
||||
return # do not remove random attached file volumes
|
||||
elif self._is_snapshot:
|
||||
return # no need to remove, because it's just a snapshot
|
||||
else:
|
||||
if not self.snap_on_start:
|
||||
_remove_if_exists(self.path)
|
||||
if self._is_origin:
|
||||
_remove_if_exists(self.path_cow)
|
||||
if self.snap_on_start or self.save_on_stop:
|
||||
_remove_if_exists(self.path_cow)
|
||||
|
||||
def is_dirty(self):
|
||||
return False # TODO: How to implement this?
|
||||
if not self.save_on_stop:
|
||||
return False
|
||||
if os.path.exists(self.path_cow):
|
||||
stat = os.stat(self.path_cow)
|
||||
return stat.st_blocks > 0
|
||||
return False
|
||||
|
||||
def resize(self, size):
|
||||
''' Expands volume, throws
|
||||
@ -223,7 +201,7 @@ class FileVolume(qubes.storage.Volume):
|
||||
with open(self.path, 'a+b') as fd:
|
||||
fd.truncate(size)
|
||||
|
||||
p = subprocess.Popen(['sudo', 'losetup', '--associated', self.path],
|
||||
p = subprocess.Popen(['losetup', '--associated', self.path],
|
||||
stdout=subprocess.PIPE)
|
||||
result = p.communicate()
|
||||
|
||||
@ -232,28 +210,27 @@ class FileVolume(qubes.storage.Volume):
|
||||
loop_dev = m.group(1)
|
||||
|
||||
# resize loop device
|
||||
subprocess.check_call(['sudo', 'losetup', '--set-capacity',
|
||||
subprocess.check_call(['losetup', '--set-capacity',
|
||||
loop_dev])
|
||||
self.size = size
|
||||
|
||||
def commit(self):
|
||||
msg = 'Tried to commit a non commitable volume {!r}'.format(self)
|
||||
assert (self._is_origin or self._is_volume) and self.rw, msg
|
||||
|
||||
if self._is_volume:
|
||||
return self
|
||||
assert self.save_on_stop and self.rw, msg
|
||||
|
||||
if os.path.exists(self.path_cow):
|
||||
old_path = self.path_cow + '.old'
|
||||
os.rename(self.path_cow, old_path)
|
||||
if self.revisions_to_keep:
|
||||
old_path = self.path_cow + '.old'
|
||||
os.rename(self.path_cow, old_path)
|
||||
else:
|
||||
os.unlink(self.path_cow)
|
||||
|
||||
old_umask = os.umask(0o002)
|
||||
with open(self.path_cow, 'w') as f_cow:
|
||||
f_cow.truncate(self.size)
|
||||
os.umask(old_umask)
|
||||
create_sparse_file(self.path_cow, self.size)
|
||||
return self
|
||||
|
||||
def export(self):
|
||||
# FIXME: this should rather return snapshot(self.path, self.path_cow)
|
||||
# if domain is running
|
||||
return self.path
|
||||
|
||||
def import_volume(self, src_volume):
|
||||
@ -271,53 +248,30 @@ class FileVolume(qubes.storage.Volume):
|
||||
|
||||
def reset(self):
|
||||
''' Remove and recreate a volatile volume '''
|
||||
assert self._is_volatile, "Not a volatile volume"
|
||||
assert not self.snap_on_start and not self.save_on_stop, \
|
||||
"Not a volatile volume"
|
||||
assert isinstance(self.size, int) and self.size > 0, \
|
||||
'Volatile volume size must be > 0'
|
||||
|
||||
_remove_if_exists(self.path)
|
||||
|
||||
with open(self.path, "w") as f_volatile:
|
||||
f_volatile.truncate(self.size)
|
||||
create_sparse_file(self.path, self.size)
|
||||
return self
|
||||
|
||||
def revert(self, revision=None):
|
||||
if revision is not None:
|
||||
try:
|
||||
return self.revisions[revision]
|
||||
except KeyError:
|
||||
msg = "Volume {!r} does not have revision {!s}"
|
||||
msg = msg.format(self, revision)
|
||||
raise qubes.storage.StoragePoolException(msg)
|
||||
else:
|
||||
try:
|
||||
old_path = self.revisions.values().pop()
|
||||
os.rename(old_path, self.path_cow)
|
||||
except IndexError:
|
||||
msg = "Volume {!r} does not have old revisions".format(self)
|
||||
raise qubes.storage.StoragePoolException(msg)
|
||||
|
||||
def start(self):
|
||||
if self._is_volatile:
|
||||
if not self.save_on_stop and not self.snap_on_start:
|
||||
self.reset()
|
||||
else:
|
||||
_check_path(self.path)
|
||||
if self.snap_on_start:
|
||||
if not self.save_on_stop:
|
||||
# make sure previous snapshot is removed - even if VM
|
||||
# shutdown routing wasn't called (power interrupt or so)
|
||||
_remove_if_exists(self.path_cow)
|
||||
try:
|
||||
_check_path(self.path_cow)
|
||||
except qubes.storage.StoragePoolException:
|
||||
create_sparse_file(self.path_cow, self.size)
|
||||
_check_path(self.path_cow)
|
||||
if hasattr(self, 'path_source_cow'):
|
||||
try:
|
||||
_check_path(self.path_source_cow)
|
||||
except qubes.storage.StoragePoolException:
|
||||
create_sparse_file(self.path_source_cow, self.size)
|
||||
_check_path(self.path_source_cow)
|
||||
if not self.save_on_stop:
|
||||
# make sure previous snapshot is removed - even if VM
|
||||
# shutdown routine wasn't called (power interrupt or so)
|
||||
_remove_if_exists(self.path_cow)
|
||||
if not os.path.exists(self.path_cow):
|
||||
create_sparse_file(self.path_cow, self.size)
|
||||
if not self.snap_on_start:
|
||||
_check_path(self.path)
|
||||
if hasattr(self, 'path_source_cow'):
|
||||
if not os.path.exists(self.path_source_cow):
|
||||
create_sparse_file(self.path_source_cow, self.size)
|
||||
return self
|
||||
|
||||
def stop(self):
|
||||
@ -331,7 +285,7 @@ class FileVolume(qubes.storage.Volume):
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
if self._is_snapshot:
|
||||
if self.snap_on_start:
|
||||
return os.path.join(self.dir_path, self.source.vid + '.img')
|
||||
return os.path.join(self.dir_path, self.vid + '.img')
|
||||
|
||||
@ -342,18 +296,19 @@ class FileVolume(qubes.storage.Volume):
|
||||
|
||||
def verify(self):
|
||||
''' Verifies the volume. '''
|
||||
if not os.path.exists(self.path) and not self._is_volatile:
|
||||
if not os.path.exists(self.path) and \
|
||||
(self.snap_on_start or self.save_on_stop):
|
||||
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:
|
||||
if not self.snap_on_start and not self.save_on_stop:
|
||||
return None
|
||||
elif self._is_origin:
|
||||
elif not self.snap_on_start and self.save_on_stop:
|
||||
return 'block-origin'
|
||||
elif self._is_origin_snapshot or self._is_snapshot:
|
||||
elif self.snap_on_start:
|
||||
return 'block-snapshot'
|
||||
|
||||
def block_device(self):
|
||||
@ -361,9 +316,9 @@ class FileVolume(qubes.storage.Volume):
|
||||
the libvirt XML template as <disk>.
|
||||
'''
|
||||
path = self.path
|
||||
if self._is_snapshot:
|
||||
if self.snap_on_start:
|
||||
path += ":" + self.path_source_cow
|
||||
if self._is_origin or self._is_snapshot:
|
||||
if self.snap_on_start or self.save_on_stop:
|
||||
path += ":" + self.path_cow
|
||||
return qubes.storage.BlockDevice(path, self.name, self.script, self.rw,
|
||||
self.domain, self.devtype)
|
||||
@ -380,39 +335,13 @@ class FileVolume(qubes.storage.Volume):
|
||||
|
||||
seconds = os.path.getctime(old_revision)
|
||||
iso_date = qubes.storage.isodate(seconds).split('.', 1)[0]
|
||||
return {iso_date: old_revision}
|
||||
return {'old': iso_date}
|
||||
|
||||
@property
|
||||
def usage(self):
|
||||
''' Returns the actualy used space '''
|
||||
return get_disk_usage(self.vid)
|
||||
|
||||
@property
|
||||
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 _is_origin(self):
|
||||
''' Internal helper. Useful for differentiating volume handling '''
|
||||
# pylint: disable=line-too-long
|
||||
return self.save_on_stop and self.revisions_to_keep > 0 # NOQA
|
||||
|
||||
@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
|
||||
|
||||
@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 '''
|
||||
|
@ -138,6 +138,14 @@ class LinuxKernel(Pool):
|
||||
def init_volume(self, vm, volume_config):
|
||||
assert not volume_config['rw']
|
||||
|
||||
# migrate old config
|
||||
if volume_config.get('snap_on_start', False) and not \
|
||||
volume_config.get('source', None):
|
||||
volume_config['snap_on_start'] = False
|
||||
|
||||
if volume_config.get('save_on_stop', False):
|
||||
raise NotImplementedError(
|
||||
'LinuxKernel pool does not support save_on_stop=True')
|
||||
volume_config['pool'] = self
|
||||
volume = LinuxModules(self.dir_path, lambda: vm.kernel, **volume_config)
|
||||
|
||||
@ -157,9 +165,6 @@ class LinuxKernel(Pool):
|
||||
def import_volume(self, dst_pool, dst_volume, src_pool, src_volume):
|
||||
pass
|
||||
|
||||
def rename(self, volume, old_name, new_name):
|
||||
return volume
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
@ -170,7 +175,6 @@ class LinuxKernel(Pool):
|
||||
kernel_version,
|
||||
pool=self,
|
||||
name=kernel_version,
|
||||
internal=True,
|
||||
rw=False
|
||||
)
|
||||
for kernel_version in os.listdir(self.dir_path)]
|
||||
|
@ -21,9 +21,14 @@
|
||||
''' Driver for storing vm images in a LVM thin pool '''
|
||||
|
||||
import logging
|
||||
import operator
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import time
|
||||
|
||||
import asyncio
|
||||
|
||||
import qubes
|
||||
import qubes.storage
|
||||
import qubes.utils
|
||||
@ -88,23 +93,6 @@ class ThinPool(qubes.storage.Pool):
|
||||
volume_config['pool'] = self
|
||||
return ThinVolume(**volume_config)
|
||||
|
||||
def rename(self, volume, old_name, new_name):
|
||||
''' Called when the domain changes its name '''
|
||||
new_vid = "{!s}/vm-{!s}-{!s}".format(self.volume_group, new_name,
|
||||
volume.name)
|
||||
if volume.save_on_stop:
|
||||
cmd = ['clone', volume.vid, new_vid]
|
||||
qubes_lvm(cmd, self.log)
|
||||
cmd = ['remove', volume.vid]
|
||||
qubes_lvm(cmd, self.log)
|
||||
|
||||
volume.vid = new_vid
|
||||
|
||||
if volume.snap_on_start:
|
||||
volume._vid_snap = volume.vid + '-snap'
|
||||
reset_cache()
|
||||
return volume
|
||||
|
||||
def setup(self):
|
||||
pass # TODO Should we create a non existing pool?
|
||||
|
||||
@ -119,6 +107,9 @@ class ThinPool(qubes.storage.Pool):
|
||||
if vid.endswith('-snap'):
|
||||
# implementation detail volume
|
||||
continue
|
||||
if vid.endswith('-back'):
|
||||
# old revisions
|
||||
continue
|
||||
config = {
|
||||
'pool': self,
|
||||
'vid': vid,
|
||||
@ -132,7 +123,7 @@ class ThinPool(qubes.storage.Pool):
|
||||
|
||||
def init_cache(log=logging.getLogger('qube.storage.lvm')):
|
||||
cmd = ['lvs', '--noheadings', '-o',
|
||||
'vg_name,pool_lv,name,lv_size,data_percent,lv_attr',
|
||||
'vg_name,pool_lv,name,lv_size,data_percent,lv_attr,origin',
|
||||
'--units', 'b', '--separator', ',']
|
||||
if os.getuid() != 0:
|
||||
cmd.insert(0, 'sudo')
|
||||
@ -149,14 +140,15 @@ def init_cache(log=logging.getLogger('qube.storage.lvm')):
|
||||
|
||||
for line in out.splitlines():
|
||||
line = line.decode().strip()
|
||||
pool_name, pool_lv, name, size, usage_percent, attr = line.split(',', 5)
|
||||
pool_name, pool_lv, name, size, usage_percent, attr, \
|
||||
origin = line.split(',', 6)
|
||||
if '' in [pool_name, pool_lv, name, size, usage_percent]:
|
||||
continue
|
||||
name = pool_name + "/" + name
|
||||
size = int(size[:-1])
|
||||
usage = int(size / 100 * float(usage_percent))
|
||||
result[name] = {'size': size, 'usage': usage, 'pool_lv': pool_lv,
|
||||
'attr': attr}
|
||||
'attr': attr, 'origin': origin}
|
||||
|
||||
return result
|
||||
|
||||
@ -173,16 +165,7 @@ class ThinVolume(qubes.storage.Volume):
|
||||
super(ThinVolume, self).__init__(size=size, **kwargs)
|
||||
self.log = logging.getLogger('qube.storage.lvm.%s' % str(self.pool))
|
||||
|
||||
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.snap_on_start:
|
||||
if self.snap_on_start or self.save_on_stop:
|
||||
self._vid_snap = self.vid + '-snap'
|
||||
|
||||
self._size = size
|
||||
@ -193,28 +176,18 @@ class ThinVolume(qubes.storage.Volume):
|
||||
|
||||
@property
|
||||
def revisions(self):
|
||||
path = self.path + '-back'
|
||||
if os.path.exists(path):
|
||||
seconds = os.path.getctime(path)
|
||||
name_prefix = self.vid + '-'
|
||||
revisions = {}
|
||||
for revision_vid in size_cache:
|
||||
if not revision_vid.startswith(name_prefix):
|
||||
continue
|
||||
if not revision_vid.endswith('-back'):
|
||||
continue
|
||||
revision_vid = revision_vid[len(name_prefix):]
|
||||
seconds = int(revision_vid[:-len('-back')])
|
||||
iso_date = qubes.storage.isodate(seconds).split('.', 1)[0]
|
||||
return {iso_date: path}
|
||||
return {}
|
||||
|
||||
@property
|
||||
def _is_origin(self):
|
||||
return not self.snap_on_start and self.save_on_stop
|
||||
|
||||
@property
|
||||
def _is_origin_snapshot(self):
|
||||
return self.snap_on_start and self.save_on_stop
|
||||
|
||||
@property
|
||||
def _is_snapshot(self):
|
||||
return self.snap_on_start and not self.save_on_stop
|
||||
|
||||
@property
|
||||
def _is_volatile(self):
|
||||
return not self.snap_on_start and not self.save_on_stop
|
||||
revisions[revision_vid] = iso_date
|
||||
return revisions
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
@ -230,8 +203,8 @@ class ThinVolume(qubes.storage.Volume):
|
||||
|
||||
def _reset(self):
|
||||
''' Resets a volatile volume '''
|
||||
assert self._is_volatile, \
|
||||
'Expected a volatile volume, but got {!r}'.format(self)
|
||||
assert not self.snap_on_start and not self.save_on_stop, \
|
||||
"Not a volatile volume"
|
||||
self.log.debug('Resetting volatile ' + self.vid)
|
||||
try:
|
||||
cmd = ['remove', self.vid]
|
||||
@ -243,6 +216,27 @@ class ThinVolume(qubes.storage.Volume):
|
||||
str(self.size)]
|
||||
qubes_lvm(cmd, self.log)
|
||||
|
||||
def _remove_revisions(self, revisions=None):
|
||||
'''Remove old volume revisions.
|
||||
|
||||
If no revisions list is given, it removes old revisions according to
|
||||
:py:attr:`revisions_to_keep`
|
||||
|
||||
:param revisions: list of revisions to remove
|
||||
'''
|
||||
if revisions is None:
|
||||
revisions = sorted(self.revisions.items(),
|
||||
key=operator.itemgetter(1))
|
||||
revisions = revisions[:-self.revisions_to_keep]
|
||||
revisions = [rev_id for rev_id, _ in revisions]
|
||||
|
||||
for rev_id in revisions:
|
||||
try:
|
||||
cmd = ['remove', self.vid + rev_id]
|
||||
qubes_lvm(cmd, self.log)
|
||||
except qubes.storage.StoragePoolException:
|
||||
pass
|
||||
|
||||
def _commit(self):
|
||||
msg = "Trying to commit {!s}, but it has save_on_stop == False"
|
||||
msg = msg.format(self)
|
||||
@ -253,13 +247,11 @@ class ThinVolume(qubes.storage.Volume):
|
||||
assert self.rw, msg
|
||||
assert hasattr(self, '_vid_snap')
|
||||
|
||||
try:
|
||||
cmd = ['remove', self.vid + "-back"]
|
||||
if self.revisions_to_keep > 0:
|
||||
cmd = ['clone', self.vid,
|
||||
'{}-{}-back'.format(self.vid, int(time.time()))]
|
||||
qubes_lvm(cmd, self.log)
|
||||
except qubes.storage.StoragePoolException:
|
||||
pass
|
||||
cmd = ['clone', self.vid, self.vid + "-back"]
|
||||
qubes_lvm(cmd, self.log)
|
||||
self._remove_revisions()
|
||||
|
||||
cmd = ['remove', self.vid]
|
||||
qubes_lvm(cmd, self.log)
|
||||
@ -290,6 +282,7 @@ class ThinVolume(qubes.storage.Volume):
|
||||
cmd = ['remove', self._vid_snap]
|
||||
qubes_lvm(cmd, self.log)
|
||||
|
||||
self._remove_revisions(self.revisions.keys())
|
||||
if not os.path.exists(self.path):
|
||||
return
|
||||
cmd = ['remove', self.vid]
|
||||
@ -301,6 +294,7 @@ class ThinVolume(qubes.storage.Volume):
|
||||
devpath = '/dev/' + self.vid
|
||||
return devpath
|
||||
|
||||
@asyncio.coroutine
|
||||
def import_volume(self, src_volume):
|
||||
if not src_volume.save_on_stop:
|
||||
return self
|
||||
@ -316,9 +310,14 @@ class ThinVolume(qubes.storage.Volume):
|
||||
qubes_lvm(cmd, self.log)
|
||||
else:
|
||||
src_path = src_volume.export()
|
||||
cmd = ['sudo', 'dd', 'if=' + src_path, 'of=/dev/' + self.vid,
|
||||
cmd = ['dd', 'if=' + src_path, 'of=/dev/' + self.vid,
|
||||
'conv=sparse']
|
||||
subprocess.check_call(cmd)
|
||||
p = yield from asyncio.create_subprocess_exec(*cmd)
|
||||
yield from p.wait()
|
||||
if p.returncode != 0:
|
||||
raise qubes.storage.StoragePoolException(
|
||||
'Failed to import volume {!r}, dd exit code: {}'.format(
|
||||
src_volume, p.returncode))
|
||||
reset_cache()
|
||||
|
||||
return self
|
||||
@ -330,18 +329,30 @@ class ThinVolume(qubes.storage.Volume):
|
||||
|
||||
def is_dirty(self):
|
||||
if self.save_on_stop:
|
||||
return os.path.exists(self.path + '-snap')
|
||||
return os.path.exists('/dev/' + self._vid_snap)
|
||||
return False
|
||||
|
||||
def is_outdated(self):
|
||||
if not self.snap_on_start:
|
||||
return False
|
||||
if self._vid_snap not in size_cache:
|
||||
return False
|
||||
return (size_cache[self._vid_snap]['origin'] !=
|
||||
self.source.vid.split('/')[1])
|
||||
|
||||
|
||||
def revert(self, revision=None):
|
||||
old_path = self.path + '-back'
|
||||
if revision is None:
|
||||
revision = \
|
||||
max(self.revisions.items(), key=operator.itemgetter(1))[0]
|
||||
old_path = self.path + '-' + revision
|
||||
if not os.path.exists(old_path):
|
||||
msg = "Volume {!s} has no {!s}".format(self, old_path)
|
||||
raise qubes.storage.StoragePoolException(msg)
|
||||
|
||||
cmd = ['remove', self.vid]
|
||||
qubes_lvm(cmd, self.log)
|
||||
cmd = ['clone', self.vid + '-back', self.vid]
|
||||
cmd = ['clone', self.vid + '-' + revision, self.vid]
|
||||
qubes_lvm(cmd, self.log)
|
||||
reset_cache()
|
||||
return self
|
||||
@ -381,22 +392,22 @@ class ThinVolume(qubes.storage.Volume):
|
||||
|
||||
|
||||
def start(self):
|
||||
if self.snap_on_start:
|
||||
if self.snap_on_start or self.save_on_stop:
|
||||
if not self.save_on_stop or not self.is_dirty():
|
||||
self._snapshot()
|
||||
elif not self.save_on_stop:
|
||||
else:
|
||||
self._reset()
|
||||
|
||||
reset_cache()
|
||||
return self
|
||||
|
||||
def stop(self):
|
||||
if self.save_on_stop and self.snap_on_start:
|
||||
if self.save_on_stop:
|
||||
self._commit()
|
||||
if self.snap_on_start:
|
||||
if self.snap_on_start or self.save_on_stop:
|
||||
cmd = ['remove', self._vid_snap]
|
||||
qubes_lvm(cmd, self.log)
|
||||
elif not self.save_on_stop:
|
||||
else:
|
||||
cmd = ['remove', self.vid]
|
||||
qubes_lvm(cmd, self.log)
|
||||
reset_cache()
|
||||
@ -415,7 +426,7 @@ class ThinVolume(qubes.storage.Volume):
|
||||
''' Return :py:class:`qubes.storage.BlockDevice` for serialization in
|
||||
the libvirt XML template as <disk>.
|
||||
'''
|
||||
if self.snap_on_start:
|
||||
if self.snap_on_start or self.save_on_stop:
|
||||
return qubes.storage.BlockDevice(
|
||||
'/dev/' + self._vid_snap, self.name, self.script,
|
||||
self.rw, self.domain, self.devtype)
|
||||
|
@ -585,7 +585,7 @@ class SystemTestsMixin(object):
|
||||
# need some information from the real qubes.xml - at least installed
|
||||
# templates; should not be used for testing, only to initialize self.app
|
||||
self.host_app = qubes.Qubes(os.path.join(
|
||||
qubes.config.system_path['qubes_base_dir'],
|
||||
qubes.config.qubes_base_dir,
|
||||
qubes.config.system_path['qubes_store_filename']))
|
||||
if os.path.exists(CLASS_XMLPATH):
|
||||
shutil.copy(CLASS_XMLPATH, XMLPATH)
|
||||
@ -729,7 +729,7 @@ class SystemTestsMixin(object):
|
||||
'qubes_appvms_dir',
|
||||
'qubes_servicevms_dir',
|
||||
'qubes_templates_dir'):
|
||||
dirpath = os.path.join(qubes.config.system_path['qubes_base_dir'],
|
||||
dirpath = os.path.join(qubes.config.qubes_base_dir,
|
||||
qubes.config.system_path[dirspec], vmname)
|
||||
if os.path.exists(dirpath):
|
||||
if os.path.isdir(dirpath):
|
||||
@ -793,7 +793,7 @@ class SystemTestsMixin(object):
|
||||
'qubes_appvms_dir',
|
||||
'qubes_servicevms_dir',
|
||||
'qubes_templates_dir'):
|
||||
dirpath = os.path.join(qubes.config.system_path['qubes_base_dir'],
|
||||
dirpath = os.path.join(qubes.config.qubes_base_dir,
|
||||
qubes.config.system_path[dirspec])
|
||||
for name in os.listdir(dirpath):
|
||||
if name.startswith(prefix):
|
||||
|
@ -37,7 +37,7 @@ import qubes.storage
|
||||
|
||||
# properties defined in API
|
||||
volume_properties = [
|
||||
'pool', 'vid', 'size', 'usage', 'rw', 'internal', 'source',
|
||||
'pool', 'vid', 'size', 'usage', 'rw', 'source',
|
||||
'save_on_stop', 'snap_on_start']
|
||||
|
||||
|
||||
|
@ -77,68 +77,6 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
label='red')
|
||||
self.loop.run_until_complete(self.vm.create_on_disk())
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_000_rename(self):
|
||||
newname = self.make_vm_name('newname')
|
||||
|
||||
self.assertEqual(self.vm.name, self.vmname)
|
||||
self.vm.firewall.policy = 'drop'
|
||||
self.vm.firewall.rules = [
|
||||
qubes.firewall.Rule(None, action='accept', specialtarget='dns')
|
||||
]
|
||||
self.vm.firewall.save()
|
||||
self.vm.autostart = True
|
||||
self.addCleanup(os.system,
|
||||
'sudo systemctl -q disable qubes-vm@{}.service || :'.
|
||||
format(self.vmname))
|
||||
pre_rename_firewall = self.vm.firewall.rules
|
||||
|
||||
with self.assertNotRaises(
|
||||
(OSError, libvirt.libvirtError, qubes.exc.QubesException)):
|
||||
self.vm.name = newname
|
||||
self.assertEqual(self.vm.name, newname)
|
||||
self.assertEqual(self.vm.dir_path,
|
||||
os.path.join(
|
||||
qubes.config.system_path['qubes_base_dir'],
|
||||
qubes.config.system_path['qubes_appvms_dir'], newname))
|
||||
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(os.getenv("HOME"), ".local/share/desktop-directories",
|
||||
newname + "-vm.directory")))
|
||||
self.assertTrue(os.path.exists(
|
||||
os.path.join(os.getenv("HOME"), ".local/share/applications",
|
||||
newname + "-firefox.desktop")))
|
||||
self.assertFalse(os.path.exists(
|
||||
os.path.join(os.getenv("HOME"), ".local/share/desktop-directories",
|
||||
self.vmname + "-vm.directory")))
|
||||
self.assertFalse(os.path.exists(
|
||||
os.path.join(os.getenv("HOME"), ".local/share/applications",
|
||||
self.vmname + "-firefox.desktop")))
|
||||
self.vm.firewall.load()
|
||||
self.assertEqual(pre_rename_firewall, self.vm.firewall.rules)
|
||||
with self.assertNotRaises((qubes.exc.QubesException, OSError)):
|
||||
self.vm.firewall.save()
|
||||
self.assertTrue(self.vm.autostart)
|
||||
self.assertTrue(os.path.exists(
|
||||
'/etc/systemd/system/multi-user.target.wants/'
|
||||
'qubes-vm@{}.service'.format(newname)))
|
||||
self.assertFalse(os.path.exists(
|
||||
'/etc/systemd/system/multi-user.target.wants/'
|
||||
'qubes-vm@{}.service'.format(self.vmname)))
|
||||
|
||||
def test_001_rename_libvirt_undefined(self):
|
||||
self.vm.libvirt_domain.undefine()
|
||||
self.vm._libvirt_domain = None # pylint: disable=protected-access
|
||||
|
||||
newname = self.make_vm_name('newname')
|
||||
with self.assertNotRaises(
|
||||
(OSError, libvirt.libvirtError, qubes.exc.QubesException)):
|
||||
self.vm.name = newname
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_030_clone(self):
|
||||
testvm1 = self.app.add_new_vm(
|
||||
|
@ -59,7 +59,6 @@ class StorageTestMixin(qubes.tests.SystemTestsMixin):
|
||||
volume_config = {
|
||||
'pool': self.pool.name,
|
||||
'size': size,
|
||||
'internal': False,
|
||||
'save_on_stop': False,
|
||||
'rw': True,
|
||||
}
|
||||
@ -91,7 +90,6 @@ class StorageTestMixin(qubes.tests.SystemTestsMixin):
|
||||
volume_config = {
|
||||
'pool': self.pool.name,
|
||||
'size': size,
|
||||
'internal': False,
|
||||
'save_on_stop': True,
|
||||
'rw': True,
|
||||
}
|
||||
@ -126,7 +124,6 @@ class StorageTestMixin(qubes.tests.SystemTestsMixin):
|
||||
volume_config = {
|
||||
'pool': self.pool.name,
|
||||
'size': size,
|
||||
'internal': False,
|
||||
'save_on_stop': False,
|
||||
'rw': False,
|
||||
}
|
||||
@ -157,7 +154,6 @@ class StorageTestMixin(qubes.tests.SystemTestsMixin):
|
||||
volume_config = {
|
||||
'pool': self.pool.name,
|
||||
'size': size,
|
||||
'internal': False,
|
||||
'save_on_stop': True,
|
||||
'rw': True,
|
||||
}
|
||||
@ -166,7 +162,6 @@ class StorageTestMixin(qubes.tests.SystemTestsMixin):
|
||||
volume_config = {
|
||||
'pool': self.pool.name,
|
||||
'size': size,
|
||||
'internal': False,
|
||||
'snap_on_start': True,
|
||||
'source': testvol.vid,
|
||||
'rw': True,
|
||||
@ -225,7 +220,6 @@ class StorageTestMixin(qubes.tests.SystemTestsMixin):
|
||||
volume_config = {
|
||||
'pool': self.pool.name,
|
||||
'size': size,
|
||||
'internal': False,
|
||||
'save_on_stop': True,
|
||||
'rw': True,
|
||||
}
|
||||
@ -234,7 +228,6 @@ class StorageTestMixin(qubes.tests.SystemTestsMixin):
|
||||
volume_config = {
|
||||
'pool': self.pool.name,
|
||||
'size': size,
|
||||
'internal': False,
|
||||
'snap_on_start': True,
|
||||
'source': testvol.vid,
|
||||
'rw': True,
|
||||
|
@ -136,6 +136,15 @@ class TC_01_FileVolumes(qubes.tests.QubesTestCase):
|
||||
self.assertFalse(volume.snap_on_start)
|
||||
self.assertTrue(volume.save_on_stop)
|
||||
self.assertTrue(volume.rw)
|
||||
block = volume.block_device()
|
||||
self.assertEqual(block.path,
|
||||
'{base}.img:{base}-cow.img'.format(
|
||||
base=self.POOL_DIR + '/appvms/' + vm.name + '/root'))
|
||||
self.assertEqual(block.script, 'block-origin')
|
||||
self.assertEqual(block.rw, True)
|
||||
self.assertEqual(block.name, 'root')
|
||||
self.assertEqual(block.devtype, 'disk')
|
||||
self.assertIsNone(block.domain)
|
||||
|
||||
def test_001_snapshot_volume(self):
|
||||
template_vm = self.app.default_template
|
||||
@ -169,6 +178,19 @@ class TC_01_FileVolumes(qubes.tests.QubesTestCase):
|
||||
self.assertFalse(volume.save_on_stop)
|
||||
self.assertFalse(volume.rw)
|
||||
self.assertEqual(volume.usage, 0)
|
||||
block = volume.block_device()
|
||||
assert isinstance(block, qubes.storage.BlockDevice)
|
||||
self.assertEqual(block.path,
|
||||
'{base}/{src}.img:{base}/{src}-cow.img:'
|
||||
'{base}/{dst}-cow.img'.format(
|
||||
base=self.POOL_DIR,
|
||||
src='vm-templates/' + template_vm.name + '/root',
|
||||
dst='appvms/' + vm.name + '/root',
|
||||
))
|
||||
self.assertEqual(block.name, 'root')
|
||||
self.assertEqual(block.script, 'block-snapshot')
|
||||
self.assertEqual(block.rw, False)
|
||||
self.assertEqual(block.devtype, 'disk')
|
||||
|
||||
def test_002_read_write_volume(self):
|
||||
config = {
|
||||
@ -186,6 +208,12 @@ class TC_01_FileVolumes(qubes.tests.QubesTestCase):
|
||||
self.assertFalse(volume.snap_on_start)
|
||||
self.assertTrue(volume.save_on_stop)
|
||||
self.assertTrue(volume.rw)
|
||||
block = volume.block_device()
|
||||
self.assertEqual(block.name, 'root')
|
||||
self.assertEqual(block.path, '{base}.img:{base}-cow.img'.format(
|
||||
base=self.POOL_DIR + '/appvms/' + vm.name + '/root'))
|
||||
self.assertEqual(block.rw, True)
|
||||
self.assertEqual(block.script, 'block-origin')
|
||||
|
||||
def test_003_read_only_volume(self):
|
||||
template = self.app.default_template
|
||||
@ -207,6 +235,8 @@ class TC_01_FileVolumes(qubes.tests.QubesTestCase):
|
||||
self.assertFalse(volume.snap_on_start)
|
||||
self.assertFalse(volume.save_on_stop)
|
||||
self.assertFalse(volume.rw)
|
||||
block = volume.block_device()
|
||||
self.assertEqual(block.rw, False)
|
||||
|
||||
def test_004_volatile_volume(self):
|
||||
config = {
|
||||
@ -248,7 +278,8 @@ class TC_01_FileVolumes(qubes.tests.QubesTestCase):
|
||||
|
||||
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'
|
||||
expected = vm.dir_path + '/private.img:' + \
|
||||
vm.dir_path + '/private-cow.img'
|
||||
self.assertVolumePath(vm, 'private', expected, rw=True)
|
||||
expected = vm.dir_path + '/volatile.img'
|
||||
self.assertVolumePath(vm, 'volatile', expected, rw=True)
|
||||
@ -275,8 +306,8 @@ class TC_03_FilePool(qubes.tests.QubesTestCase):
|
||||
def setUp(self):
|
||||
""" Add a test file based storage pool """
|
||||
super(TC_03_FilePool, self).setUp()
|
||||
self._orig_qubes_base_dir = qubes.config.system_path['qubes_base_dir']
|
||||
qubes.config.system_path['qubes_base_dir'] = '/tmp/qubes-test'
|
||||
self._orig_qubes_base_dir = qubes.config.qubes_base_dir
|
||||
qubes.config.qubes_base_dir = '/tmp/qubes-test'
|
||||
self.app = TestApp()
|
||||
self.app.create_dummy_template()
|
||||
self.app.add_pool(**self.POOL_CONFIG)
|
||||
@ -289,7 +320,7 @@ class TC_03_FilePool(qubes.tests.QubesTestCase):
|
||||
shutil.rmtree(self.POOL_DIR, ignore_errors=True)
|
||||
if os.path.exists('/tmp/qubes-test'):
|
||||
shutil.rmtree('/tmp/qubes-test')
|
||||
qubes.config.system_path['qubes_base_dir'] = self._orig_qubes_base_dir
|
||||
qubes.config.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 """
|
||||
|
@ -80,50 +80,6 @@ class TC_00_setters(qubes.tests.QubesTestCase):
|
||||
qubes.vm.qubesvm._setter_qid(self.vm,
|
||||
self.prop, qubes.config.max_qid + 5)
|
||||
|
||||
|
||||
def test_010_setter_name(self):
|
||||
self.assertEqual(
|
||||
qubes.vm.qubesvm._setter_name(self.vm, self.prop, 'test_name-1'),
|
||||
'test_name-1')
|
||||
|
||||
def test_011_setter_name_not_a_string(self):
|
||||
# pylint: disable=invalid-name
|
||||
with self.assertRaises(TypeError):
|
||||
qubes.vm.qubesvm._setter_name(self.vm, self.prop, False)
|
||||
|
||||
def test_012_setter_name_longer_than_31(self):
|
||||
# pylint: disable=invalid-name
|
||||
with self.assertRaises(ValueError):
|
||||
qubes.vm.qubesvm._setter_name(self.vm, self.prop, 't' * 32)
|
||||
|
||||
def test_013_setter_name_illegal_character(self):
|
||||
# pylint: disable=invalid-name
|
||||
with self.assertRaises(ValueError):
|
||||
qubes.vm.qubesvm._setter_name(self.vm, self.prop, 'test#')
|
||||
|
||||
def test_014_setter_name_first_not_letter(self):
|
||||
# pylint: disable=invalid-name
|
||||
with self.assertRaises(ValueError):
|
||||
qubes.vm.qubesvm._setter_name(self.vm, self.prop, '1test')
|
||||
|
||||
def test_015_setter_name_running(self):
|
||||
self.vm.running = True
|
||||
with self.assertRaises(qubes.exc.QubesVMNotHaltedError):
|
||||
qubes.vm.qubesvm._setter_name(self.vm, self.prop, 'testname')
|
||||
|
||||
def test_016_setter_name_installed_by_rpm(self):
|
||||
# pylint: disable=invalid-name
|
||||
self.vm.installed_by_rpm = True
|
||||
with self.assertRaises(qubes.exc.QubesException):
|
||||
qubes.vm.qubesvm._setter_name(self.vm, self.prop, 'testname')
|
||||
|
||||
def test_017_setter_name_duplicate(self):
|
||||
# pylint: disable=invalid-name
|
||||
self.vm.app.domains['duplicate'] = TestVM(name='duplicate')
|
||||
with self.assertRaises(qubes.exc.QubesException):
|
||||
qubes.vm.qubesvm._setter_name(self.vm, self.prop, 'duplicate')
|
||||
|
||||
|
||||
@unittest.skip('test not implemented')
|
||||
def test_020_setter_kernel(self):
|
||||
pass
|
||||
|
@ -49,7 +49,6 @@ class AppVM(qubes.vm.qubesvm.QubesVM):
|
||||
'save_on_stop': False,
|
||||
'rw': False,
|
||||
'source': None,
|
||||
'internal': True
|
||||
},
|
||||
'private': {
|
||||
'name': 'private',
|
||||
@ -58,20 +57,21 @@ class AppVM(qubes.vm.qubesvm.QubesVM):
|
||||
'save_on_stop': True,
|
||||
'rw': True,
|
||||
'size': defaults['private_img_size'],
|
||||
'internal': True
|
||||
},
|
||||
'volatile': {
|
||||
'name': 'volatile',
|
||||
'pool': 'default',
|
||||
'snap_on_start': False,
|
||||
'save_on_stop': False,
|
||||
'size': defaults['root_img_size'],
|
||||
'internal': True,
|
||||
'rw': True,
|
||||
},
|
||||
'kernel': {
|
||||
'name': 'kernel',
|
||||
'pool': 'linux-kernel',
|
||||
'snap_on_start': False,
|
||||
'save_on_stop': False,
|
||||
'rw': False,
|
||||
'internal': True
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,20 +47,19 @@ class DispVM(qubes.vm.qubesvm.QubesVM):
|
||||
'snap_on_start': True,
|
||||
'save_on_stop': False,
|
||||
'rw': False,
|
||||
'internal': True
|
||||
},
|
||||
'private': {
|
||||
'name': 'private',
|
||||
'pool': 'default',
|
||||
'snap_on_start': True,
|
||||
'save_on_stop': False,
|
||||
'internal': True,
|
||||
'rw': True,
|
||||
},
|
||||
'volatile': {
|
||||
'name': 'volatile',
|
||||
'pool': 'default',
|
||||
'internal': True,
|
||||
'snap_on_start': False,
|
||||
'save_on_stop': False,
|
||||
'rw': True,
|
||||
'size': qubes.config.defaults['root_img_size'] +
|
||||
qubes.config.defaults['private_img_size'],
|
||||
@ -68,8 +67,9 @@ class DispVM(qubes.vm.qubesvm.QubesVM):
|
||||
'kernel': {
|
||||
'name': 'kernel',
|
||||
'pool': 'linux-kernel',
|
||||
'snap_on_start': False,
|
||||
'save_on_stop': False,
|
||||
'rw': False,
|
||||
'internal': True
|
||||
}
|
||||
}
|
||||
if 'name' not in kwargs and 'dispid' in kwargs:
|
||||
|
@ -72,28 +72,6 @@ def _setter_qid(self, prop, value):
|
||||
return value
|
||||
|
||||
|
||||
def _setter_name(self, prop, value):
|
||||
''' Helper for setting the domain name '''
|
||||
qubes.vm.validate_name(self, prop, value)
|
||||
|
||||
if self.is_running():
|
||||
raise qubes.exc.QubesVMNotHaltedError(
|
||||
self, 'Cannot change name of running VM')
|
||||
|
||||
try:
|
||||
if self.installed_by_rpm:
|
||||
raise qubes.exc.QubesException('Cannot rename VM installed by RPM '
|
||||
'-- first clone VM and then use yum to remove package.')
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if value in self.app.domains:
|
||||
raise qubes.exc.QubesPropertyValueError(self, prop, value,
|
||||
'VM named {} alread exists'.format(value))
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def _setter_kernel(self, prop, value):
|
||||
''' Helper for setting the domain kernel and running sanity checks on it.
|
||||
''' # pylint: disable=unused-argument
|
||||
@ -615,7 +593,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
|
||||
def dir_path(self):
|
||||
'''Root directory for files related to this domain'''
|
||||
return os.path.join(
|
||||
qubes.config.system_path['qubes_base_dir'],
|
||||
qubes.config.qubes_base_dir,
|
||||
self.dir_path_prefix,
|
||||
self.name)
|
||||
|
||||
@ -748,33 +726,13 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
|
||||
else:
|
||||
shutil.copy(newvalue.icon_path, self.icon_path)
|
||||
|
||||
@qubes.events.handler('property-pre-set:name')
|
||||
def on_property_pre_set_name(self, event, name, newvalue, oldvalue=None):
|
||||
# pylint: disable=unused-argument
|
||||
try:
|
||||
self.app.domains[newvalue]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
raise qubes.exc.QubesValueError(
|
||||
'VM named {!r} already exists'.format(newvalue))
|
||||
|
||||
# TODO not self.is_stopped() would be more appropriate
|
||||
if self.is_running():
|
||||
raise qubes.exc.QubesVMNotHaltedError(
|
||||
'Cannot change name of running domain {!r}'.format(oldvalue))
|
||||
|
||||
if self.autostart:
|
||||
subprocess.check_call(['sudo', 'systemctl', '-q', 'disable',
|
||||
'qubes-vm@{}.service'.format(oldvalue)])
|
||||
|
||||
@qubes.events.handler('property-pre-set:kernel')
|
||||
def on_property_pre_set_kernel(self, event, name, newvalue, oldvalue=None):
|
||||
# pylint: disable=unused-argument
|
||||
if not newvalue:
|
||||
return
|
||||
dirname = os.path.join(
|
||||
qubes.config.system_path['qubes_base_dir'],
|
||||
qubes.config.qubes_base_dir,
|
||||
qubes.config.system_path['qubes_kernels_base_dir'],
|
||||
newvalue)
|
||||
if not os.path.exists(dirname):
|
||||
@ -788,29 +746,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
|
||||
'Kernel {!r} not properly installed: '
|
||||
'missing {!r} file'.format(newvalue, filename))
|
||||
|
||||
@qubes.events.handler('property-set:name')
|
||||
def on_property_set_name(self, event, name, newvalue, oldvalue=None):
|
||||
# pylint: disable=unused-argument
|
||||
self.init_log()
|
||||
|
||||
old_dir_path = os.path.join(os.path.dirname(self.dir_path), oldvalue)
|
||||
new_dir_path = os.path.join(os.path.dirname(self.dir_path), newvalue)
|
||||
os.rename(old_dir_path, new_dir_path)
|
||||
|
||||
self.storage.rename(oldvalue, newvalue)
|
||||
|
||||
if self._libvirt_domain is not None:
|
||||
self.libvirt_domain.undefine()
|
||||
self._libvirt_domain = None
|
||||
if self._qdb_connection is not None:
|
||||
self._qdb_connection.close()
|
||||
self._qdb_connection = None
|
||||
|
||||
self._update_libvirt_domain()
|
||||
|
||||
if self.autostart:
|
||||
self.autostart = self.autostart
|
||||
|
||||
@qubes.events.handler('property-pre-set:autostart')
|
||||
def on_property_pre_set_autostart(self, event, name, newvalue,
|
||||
oldvalue=None):
|
||||
@ -1728,45 +1663,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
|
||||
|
||||
return None
|
||||
|
||||
def is_outdated(self):
|
||||
'''Check whether domain needs restart to update root image from \
|
||||
template.
|
||||
|
||||
:returns: :py:obj:`True` if is outdated, :py:obj:`False` otherwise.
|
||||
:rtype: bool
|
||||
'''
|
||||
# pylint: disable=no-member
|
||||
|
||||
# Makes sense only on VM based on template
|
||||
if self.template is None:
|
||||
return False
|
||||
|
||||
if not self.is_running():
|
||||
return False
|
||||
|
||||
if not hasattr(self.template, 'rootcow_img'):
|
||||
return False
|
||||
|
||||
rootimg_inode = os.stat(self.template.root_img)
|
||||
try:
|
||||
rootcow_inode = os.stat(self.template.rootcow_img)
|
||||
except OSError:
|
||||
# The only case when rootcow_img doesn't exists is in the middle of
|
||||
# commit_changes, so VM is outdated right now
|
||||
return True
|
||||
|
||||
current_dmdev = "/dev/mapper/snapshot-{0:x}:{1}-{2:x}:{3}".format(
|
||||
rootimg_inode[2], rootimg_inode[1],
|
||||
rootcow_inode[2], rootcow_inode[1])
|
||||
|
||||
# FIXME
|
||||
# 51712 (0xCA00) is xvda
|
||||
# backend node name not available through xenapi :(
|
||||
used_dmdev = self.app.vmm.xs.read('',
|
||||
'/local/domain/0/backend/vbd/{}/51712/node'.format(self.xid))
|
||||
|
||||
return used_dmdev != current_dmdev
|
||||
|
||||
#
|
||||
# helper methods
|
||||
#
|
||||
@ -1900,29 +1796,26 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
|
||||
|
||||
|
||||
def _clean_volume_config(config):
|
||||
common_attributes = ['name', 'pool', 'size', 'internal', 'removable',
|
||||
'revisions_to_keep', 'rw', 'snap_on_start',
|
||||
'save_on_stop', 'source']
|
||||
common_attributes = ['name', 'pool', 'size',
|
||||
'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
|
||||
is_snapshot = config['snap_on_start']
|
||||
is_rw = config['rw']
|
||||
|
||||
name = config['name']
|
||||
|
||||
if pool and is_exportable and config['pool'] == 'default':
|
||||
if pool and not is_snapshot and is_rw:
|
||||
config['pool'] = str(pool)
|
||||
elif pool and not is_exportable:
|
||||
elif pool:
|
||||
pass
|
||||
elif pools and name in pools.keys():
|
||||
if is_exportable:
|
||||
if not is_snapshot:
|
||||
config['pool'] = str(pools[name])
|
||||
else:
|
||||
msg = "Can't clone a snapshot volume {!s} to pool {!s} " \
|
||||
|
@ -35,7 +35,6 @@ class StandaloneVM(qubes.vm.qubesvm.QubesVM):
|
||||
'save_on_stop': True,
|
||||
'rw': True,
|
||||
'source': None,
|
||||
'internal': True,
|
||||
'size': qubes.config.defaults['root_img_size'],
|
||||
},
|
||||
'private': {
|
||||
@ -45,21 +44,22 @@ class StandaloneVM(qubes.vm.qubesvm.QubesVM):
|
||||
'save_on_stop': True,
|
||||
'rw': True,
|
||||
'source': None,
|
||||
'internal': True,
|
||||
'size': qubes.config.defaults['private_img_size'],
|
||||
},
|
||||
'volatile': {
|
||||
'name': 'volatile',
|
||||
'pool': 'default',
|
||||
'internal': True,
|
||||
'snap_on_start': False,
|
||||
'save_on_stop': False,
|
||||
'rw': True,
|
||||
'size': qubes.config.defaults['root_img_size'],
|
||||
},
|
||||
'kernel': {
|
||||
'name': 'kernel',
|
||||
'pool': 'linux-kernel',
|
||||
'snap_on_start': False,
|
||||
'save_on_stop': False,
|
||||
'rw': False,
|
||||
'internal': True
|
||||
}
|
||||
}
|
||||
super(StandaloneVM, self).__init__(*args, **kwargs)
|
||||
|
@ -71,7 +71,6 @@ class TemplateVM(QubesVM):
|
||||
'rw': True,
|
||||
'source': None,
|
||||
'size': defaults['root_img_size'],
|
||||
'internal': True
|
||||
},
|
||||
'private': {
|
||||
'name': 'private',
|
||||
@ -82,30 +81,21 @@ class TemplateVM(QubesVM):
|
||||
'source': None,
|
||||
'size': defaults['private_img_size'],
|
||||
'revisions_to_keep': 0,
|
||||
'internal': True
|
||||
},
|
||||
'volatile': {
|
||||
'name': 'volatile',
|
||||
'pool': 'default',
|
||||
'size': defaults['root_img_size'],
|
||||
'internal': True,
|
||||
'snap_on_start': False,
|
||||
'save_on_stop': False,
|
||||
'rw': True,
|
||||
},
|
||||
'kernel': {
|
||||
'name': 'kernel',
|
||||
'pool': 'linux-kernel',
|
||||
'internal': True,
|
||||
'snap_on_start': False,
|
||||
'save_on_stop': False,
|
||||
'rw': False
|
||||
}
|
||||
}
|
||||
super(TemplateVM, self).__init__(*args, **kwargs)
|
||||
|
||||
def commit_changes(self):
|
||||
'''Commit changes to template'''
|
||||
self.log.debug('commit_changes()')
|
||||
|
||||
if not self.app.vmm.offline_mode:
|
||||
assert not self.is_running(), \
|
||||
'Attempt to commit changes on running Template VM!'
|
||||
|
||||
self.storage.commit()
|
||||
|
Loading…
Reference in New Issue
Block a user