storage: add API documentation

QubesOS/qubes-issues#2256
This commit is contained in:
Marek Marczykowski-Górecki 2017-07-01 21:27:56 +02:00
parent 5971873680
commit 82c3f85042
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
3 changed files with 218 additions and 15 deletions

View File

@ -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
View 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

View File

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