parent
5971873680
commit
82c3f85042
@ -16,6 +16,7 @@ manpages and API documentation. For primary user documentation, see
|
|||||||
qubes
|
qubes
|
||||||
qubes-vm/index
|
qubes-vm/index
|
||||||
qubes-events
|
qubes-events
|
||||||
|
qubes-storage
|
||||||
qubes-exc
|
qubes-exc
|
||||||
qubes-ext
|
qubes-ext
|
||||||
qubes-log
|
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
|
@ -78,6 +78,8 @@ class Volume(object):
|
|||||||
domain = None
|
domain = None
|
||||||
path = None
|
path = None
|
||||||
script = None
|
script = None
|
||||||
|
#: disk space used by this volume, can be smaller than :py:attr:`size`
|
||||||
|
#: for sparse volumes
|
||||||
usage = 0
|
usage = 0
|
||||||
|
|
||||||
def __init__(self, name, pool, vid, internal=False, removable=False,
|
def __init__(self, name, pool, vid, internal=False, removable=False,
|
||||||
@ -85,7 +87,7 @@ class Volume(object):
|
|||||||
snap_on_start=False, source=None, **kwargs):
|
snap_on_start=False, source=None, **kwargs):
|
||||||
''' Initialize a volume.
|
''' 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 Pool pool: The pool object
|
||||||
:param str vid: Volume identifier needs to be unique in pool
|
:param str vid: Volume identifier needs to be unique in pool
|
||||||
:param bool internal: If `True` volume is hidden when qvm-block ls
|
:param bool internal: If `True` volume is hidden when qvm-block ls
|
||||||
@ -94,9 +96,12 @@ class Volume(object):
|
|||||||
run time
|
run time
|
||||||
:param int revisions_to_keep: Amount of revisions to keep around
|
:param int revisions_to_keep: Amount of revisions to keep around
|
||||||
:param bool rw: If true volume will be mounted read-write
|
: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 snap_on_start: Create a snapshot from source on
|
||||||
:param bool save_on_stop: Write changes to disk in vm.stop()
|
start, instead of using volume own data
|
||||||
:param Volume source: other volume in same pool, or None
|
: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
|
:param str/int size: Size of the volume
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@ -106,16 +111,27 @@ class Volume(object):
|
|||||||
assert source is None or (isinstance(source, Volume)
|
assert source is None or (isinstance(source, Volume)
|
||||||
and source.pool == pool)
|
and source.pool == pool)
|
||||||
|
|
||||||
|
#: Name of the volume in a domain it's attached to (like `root` or
|
||||||
|
#: `private`).
|
||||||
self.name = str(name)
|
self.name = str(name)
|
||||||
|
#: :py:class:`Pool` instance owning this volume
|
||||||
self.pool = pool
|
self.pool = pool
|
||||||
self.internal = internal
|
self.internal = internal
|
||||||
self.removable = removable
|
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)
|
self.revisions_to_keep = int(revisions_to_keep)
|
||||||
|
#: Should this volume be writable by domain.
|
||||||
self.rw = rw
|
self.rw = rw
|
||||||
|
#: Should volume state be saved or discarded at :py:meth:`stop`
|
||||||
self.save_on_stop = save_on_stop
|
self.save_on_stop = save_on_stop
|
||||||
self._size = int(size)
|
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
|
self.snap_on_start = snap_on_start
|
||||||
|
#: source volume for :py:attr:`snap_on_start` volumes
|
||||||
self.source = source
|
self.source = source
|
||||||
|
#: Volume unique (inside given pool) identifier
|
||||||
self.vid = vid
|
self.vid = vid
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
@ -142,6 +158,10 @@ class Volume(object):
|
|||||||
def create(self):
|
def create(self):
|
||||||
''' Create the given volume on disk.
|
''' 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.
|
This can be implemented as a coroutine.
|
||||||
'''
|
'''
|
||||||
raise self._not_implemented("create")
|
raise self._not_implemented("create")
|
||||||
@ -153,17 +173,37 @@ class Volume(object):
|
|||||||
raise self._not_implemented("remove")
|
raise self._not_implemented("remove")
|
||||||
|
|
||||||
def export(self):
|
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")
|
raise self._not_implemented("export")
|
||||||
|
|
||||||
def import_data(self):
|
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")
|
raise self._not_implemented("import")
|
||||||
|
|
||||||
def import_data_end(self, success):
|
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.
|
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
|
:param success: True if data import was successful, otherwise False
|
||||||
'''
|
'''
|
||||||
# by default do nothing
|
# by default do nothing
|
||||||
@ -173,19 +213,24 @@ class Volume(object):
|
|||||||
''' Imports data from a different volume (possibly in a different
|
''' Imports data from a different volume (possibly in a different
|
||||||
pool.
|
pool.
|
||||||
|
|
||||||
The needs to be create()d first.
|
The volume needs to be create()d first.
|
||||||
|
|
||||||
This can be implemented as a coroutine. '''
|
This can be implemented as a coroutine. '''
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
raise self._not_implemented("import_volume")
|
raise self._not_implemented("import_volume")
|
||||||
|
|
||||||
def is_dirty(self):
|
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")
|
raise self._not_implemented("is_dirty")
|
||||||
|
|
||||||
def is_outdated(self):
|
def is_outdated(self):
|
||||||
''' Returns `True` if the currently used `volume.source` of a snapshot
|
''' Returns `True` if this snapshot of a source volume (for
|
||||||
volume is outdated.
|
`snap_on_start`=True) is outdated.
|
||||||
'''
|
'''
|
||||||
raise self._not_implemented("is_outdated")
|
raise self._not_implemented("is_outdated")
|
||||||
|
|
||||||
@ -195,23 +240,33 @@ class Volume(object):
|
|||||||
given size is less than current_size
|
given size is less than current_size
|
||||||
|
|
||||||
This can be implemented as a coroutine.
|
This can be implemented as a coroutine.
|
||||||
|
|
||||||
|
:param int size: new size in bytes
|
||||||
'''
|
'''
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
raise self._not_implemented("resize")
|
raise self._not_implemented("resize")
|
||||||
|
|
||||||
def revert(self, revision=None):
|
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
|
# pylint: disable=unused-argument
|
||||||
raise self._not_implemented("revert")
|
raise self._not_implemented("revert")
|
||||||
|
|
||||||
def start(self):
|
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.'''
|
This can be implemented as a coroutine.'''
|
||||||
raise self._not_implemented("start")
|
raise self._not_implemented("start")
|
||||||
|
|
||||||
def stop(self):
|
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.'''
|
This can be implemented as a coroutine.'''
|
||||||
|
|
||||||
@ -230,12 +285,14 @@ class Volume(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def revisions(self):
|
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__)
|
msg = "{!s} has revisions not implemented".format(self.__class__)
|
||||||
raise NotImplementedError(msg)
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self):
|
def size(self):
|
||||||
|
''' Volume size in bytes '''
|
||||||
return self._size
|
return self._size
|
||||||
|
|
||||||
@size.setter
|
@size.setter
|
||||||
|
Loading…
Reference in New Issue
Block a user