2020-06-25 20:06:29 +02:00
#
# The Qubes OS Project, https://www.qubes-os.org/
#
# Copyright (C) 2020 David Hobach <david@hobach.de>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
#
2020-07-02 10:42:45 +02:00
# pylint: disable=line-too-long
2020-06-25 20:06:29 +02:00
import logging
import subprocess
import json
2020-07-01 19:48:28 +02:00
import asyncio
2020-07-29 17:06:23 +02:00
import locale
2020-06-25 20:06:29 +02:00
from shlex import quote
2020-07-28 18:42:02 +02:00
from qubes . utils import coro_maybe
2020-06-25 20:06:29 +02:00
import qubes . storage
2020-07-01 19:48:28 +02:00
class UnhandledSignalException ( qubes . storage . StoragePoolException ) :
def __init__ ( self , pool , signal ) :
super ( ) . __init__ ( ' The pool %s failed to handle the signal %s , likely because it was run from synchronous code. ' % ( pool . name , signal ) )
2020-06-25 20:06:29 +02:00
class CallbackPool ( qubes . storage . Pool ) :
''' Proxy storage pool driver adding callback functionality to other pool drivers.
This way , users can extend storage pool drivers with custom functionality using the programming language of their choice .
All configuration for this pool driver must be done in ` / etc / qubes_callback . json ` . Each configuration ID ` conf_id ` can be used
to create a callback pool with e . g . ` qvm - pool - o conf_id = your_conf_id - a pool_name callback ` .
2020-06-30 14:43:55 +02:00
Check ` / usr / share / doc / qubes / qubes_callback . json . example ` for an overview of the available options .
2020-06-25 20:06:29 +02:00
Example applications of this driver :
- custom pool mounts
- encryption
- debugging
2020-07-28 18:42:02 +02:00
2020-06-25 20:06:29 +02:00
* * Integration tests * * :
( all of these tests assume the ` qubes_callback . json . example ` configuration )
2020-06-30 11:24:15 +02:00
2020-06-25 20:06:29 +02:00
Tests that should * * fail * * :
` ` `
qvm - pool - a test callback
qvm - pool - o conf_id = non - existing - a test callback
qvm - pool - o conf_id = conf_id - a test callback
qvm - pool - o conf_id = testing - fail - missing - all - a test callback
qvm - pool - o conf_id = testing - fail - missing - bdriver - args - a test callback
` ` `
2020-06-30 11:24:15 +02:00
2020-06-25 20:06:29 +02:00
Tests that should * * work * * :
` ` `
qvm - pool - o conf_id = testing - succ - file - 01 - a test callback
qvm - pool
ls / mnt / test01
qvm - pool - r test & & sudo rm - rf / mnt / test01
2020-06-30 11:24:15 +02:00
2020-07-01 19:48:28 +02:00
echo ' #!/bin/bash ' $ ' \n ' ' i=1 ; for arg in " $@ " ; do echo " $i: $arg " >> /tmp/callback.log ; (( i++)) ; done ; exit 0 ' > / usr / bin / testCbLogArgs & & chmod + x / usr / bin / testCbLogArgs
2020-06-25 20:06:29 +02:00
rm - f / tmp / callback . log
qvm - pool - o conf_id = testing - succ - file - 02 - a test callback
qvm - pool
ls / mnt / test02
2020-07-29 19:45:35 +02:00
less / tmp / callback . log ( pre_setup should be there )
2020-06-25 20:06:29 +02:00
qvm - create - l red - P test test - vm
2020-07-29 17:06:23 +02:00
cat / tmp / callback . log ( 2 x pre_volume_create + 2 x post_volume_create should be added )
2020-06-25 20:06:29 +02:00
qvm - start test - vm
qvm - volume | grep test - vm
grep test - vm / var / lib / qubes / qubes . xml
ls / mnt / test02 / appvms /
2020-07-05 17:53:52 +02:00
cat / tmp / callback . log ( 2 x pre_volume_start & 2 x post_volume_start should be added )
2020-06-25 20:06:29 +02:00
qvm - shutdown test - vm
2020-07-05 17:45:40 +02:00
cat / tmp / callback . log ( 2 x post_volume_stop should be added )
2020-06-25 20:06:29 +02:00
#reboot
2020-07-29 19:45:35 +02:00
cat / tmp / callback . log ( it should not exist )
2020-06-25 20:06:29 +02:00
qvm - start test - vm
2020-07-05 17:53:52 +02:00
cat / tmp / callback . log ( pre_sinit & 2 x pre_volume_start & 2 x post_volume_start should be added )
2020-06-25 20:06:29 +02:00
qvm - shutdown - - wait test - vm & & qvm - remove test - vm
qvm - pool - r test & & sudo rm - rf / mnt / test02
2020-07-05 17:45:40 +02:00
less / tmp / callback . log ( 2 x post_volume_stop , 2 x post_volume_remove , post_destroy should be added )
2020-06-30 11:24:15 +02:00
2020-07-05 16:32:47 +02:00
qvm - pool - o conf_id = testing - succ - file - 02 - a test callback
qvm - create - l red - P test test - dvm
qvm - prefs test - dvm template_for_dispvms True
qvm - run - - dispvm test - dvm xterm
grep - E ' test-dvm|disp ' / var / lib / qubes / qubes . xml
2020-07-05 17:23:21 +02:00
qvm - volume | grep - E ' test-dvm|disp ' ( unexpected by most users : Qubes OS places only the private volume on the pool , cf . #5933)
2020-07-05 16:32:47 +02:00
ls / mnt / test02 / appvms /
cat / tmp / callback . log
#close the disposable VM
qvm - remove test - dvm
2020-07-29 19:45:35 +02:00
qvm - pool - r test & & sudo rm - rf / mnt / test02
2020-07-05 16:32:47 +02:00
2020-06-25 20:06:29 +02:00
qvm - pool - o conf_id = testing - succ - file - 03 - a test callback
qvm - pool
ls / mnt / test03
2020-07-29 17:06:23 +02:00
less / tmp / callback . log ( pre_setup should be there , no more arguments )
2020-06-25 20:06:29 +02:00
qvm - pool - r test & & sudo rm - rf / mnt / test03
less / tmp / callback . log ( nothing should have been added )
2020-06-30 11:24:15 +02:00
2020-06-25 20:06:29 +02:00
#luks pool test:
#(make sure /mnt/test.key & /mnt/test.luks don't exist)
qvm - pool - o conf_id = testing - succ - file - luks - a tluks callback
ls / mnt /
qvm - pool
sudo cryptsetup status test - luks
sudo mount | grep test_luks
ls / mnt / test_luks /
2020-07-05 17:45:40 +02:00
qvm - create - l red - P tluks test - luks ( journalctl - b0 should show two pre_volume_create callbacks )
2020-06-25 20:06:29 +02:00
ls / mnt / test_luks / appvms / test - luks /
qvm - volume | grep test - luks
qvm - start test - luks
#reboot
grep luks / var / lib / qubes / qubes . xml
2020-07-05 17:45:40 +02:00
sudo cryptsetup status test - luks ( should be inactive due to late pre_sinit ! )
2020-06-25 20:06:29 +02:00
qvm - start test - luks
sudo mount | grep test_luks
qvm - shutdown - - wait test - luks
qvm - remove test - luks
qvm - pool - r tluks
sudo cryptsetup status test - luks
ls - l / mnt /
2020-06-30 11:24:15 +02:00
2020-06-25 20:06:29 +02:00
#ephemeral luks pool test (key in RAM / lost on reboot):
qvm - pool - o conf_id = testing - succ - file - luks - eph - a teph callback ( executes setup ( ) twice due to signal_back )
ls / mnt /
ls / mnt / ram
md5sum / mnt / ram / teph . key ( 1 )
sudo mount | grep - E ' ram|test '
sudo cryptsetup status test - eph
2020-07-05 17:45:40 +02:00
qvm - create - l red - P teph test - eph ( should execute two pre_volume_create callbacks )
2020-06-25 20:06:29 +02:00
qvm - volume | grep test - eph
2020-07-29 19:45:35 +02:00
ls / mnt / test_eph / appvms / test - eph / ( should have private . img and volatile . img )
2020-07-05 16:32:47 +02:00
ls / var / lib / qubes / appvms / test - eph ( should only have the icon )
2020-06-25 20:06:29 +02:00
qvm - start test - eph
#reboot
ls / mnt / ram ( should be empty )
ls / mnt /
sudo mount | grep - E ' ram|test ' ( should be empty )
qvm - ls | grep eph ( should still have test - eph )
grep eph / var / lib / qubes / qubes . xml ( should still have test - eph )
qvm - remove test - eph ( should create a new encrypted pool backend )
sudo cryptsetup status test - eph
grep eph / var / lib / qubes / qubes . xml ( only the pool should be left )
ls / mnt / test_eph / ( should have the appvms directory etc . )
qvm - create - l red - P teph test - eph2
ls / mnt / test_eph / appvms /
ls / mnt / ram
qvm - start test - eph2
md5sum / mnt / ram / teph . key ( ( 2 ) , different than in ( 1 ) )
qvm - shutdown - - wait test - eph2
systemctl restart qubesd
qvm - start test - eph2 ( trigger storage re - init )
md5sum / mnt / ram / teph . key ( same as in ( 2 ) )
2020-07-01 19:48:28 +02:00
qvm - shutdown - - wait test - eph2
2020-06-25 20:06:29 +02:00
sudo umount / mnt / test_eph
qvm - create - l red - P teph test - eph - fail ( must fail with error in journalctl )
ls / mnt / test_eph / ( should be empty )
systemctl restart qubesd
qvm - remove test - eph2
qvm - create - l red - P teph test - eph3
md5sum / mnt / ram / teph . key ( same as in ( 2 ) )
sudo mount | grep - E ' ram|test '
2020-07-01 19:48:28 +02:00
ls / mnt / test_eph / appvms / test - eph3
2020-06-25 20:06:29 +02:00
qvm - remove test - eph3
qvm - ls | grep test - eph
qvm - pool - r teph
grep eph / var / lib / qubes / qubes . xml ( nothing should be left )
qvm - pool
ls / mnt /
ls / mnt / ram / ( should be empty )
` ` `
2020-06-30 12:39:55 +02:00
'''
2020-06-25 20:06:29 +02:00
def __init__ ( self , * , name , conf_id ) :
''' Constructor.
: param conf_id : Identifier as found inside the user - controlled configuration at ` / etc / qubes_callback . json ` .
Non - ASCII , non - alphanumeric characters may be disallowed .
* * Security Note * * : Depending on your RPC policy ( admin . pool . Add ) this constructor and its parameters
may be called from an untrusted VM ( not by default though ) . In those cases it may be security - relevant
not to pick easily guessable ` conf_id ` values for your configuration as untrusted VMs may otherwise
execute callbacks meant for other pools .
2020-07-01 19:48:28 +02:00
: raise StoragePoolException : For user configuration issues .
2020-06-25 20:06:29 +02:00
'''
2020-06-30 12:39:55 +02:00
#NOTE: attribute names **must** start with `_cb_` unless they are meant to be stored as self._cb_impl attributes
2020-06-30 11:56:24 +02:00
self . _cb_ctor_done = False #: Boolean to indicate whether or not `__init__` successfully ran through.
self . _cb_log = logging . getLogger ( ' qubes.storage.callback ' ) #: Logger instance.
2020-06-30 12:16:14 +02:00
if not isinstance ( conf_id , str ) :
raise qubes . storage . StoragePoolException ( ' conf_id is no String. VM attack?! ' )
2020-07-01 19:48:28 +02:00
self . _cb_conf_id = conf_id #: Configuration ID as passed to `__init__()`.
2020-06-25 20:06:29 +02:00
2020-07-11 13:01:22 +02:00
config_path = ' /etc/qubes_callback.json '
with open ( config_path ) as json_file :
2020-06-25 20:06:29 +02:00
conf_all = json . load ( json_file )
2020-06-30 12:16:14 +02:00
if not isinstance ( conf_all , dict ) :
2020-07-11 13:01:22 +02:00
raise qubes . storage . StoragePoolException ( ' The file %s is supposed to define a dict. ' % config_path )
2020-06-25 20:06:29 +02:00
try :
2020-06-30 11:56:24 +02:00
self . _cb_conf = conf_all [ self . _cb_conf_id ] #: Dictionary holding all configuration for the given _cb_conf_id.
2020-06-25 20:06:29 +02:00
except KeyError :
#we cannot throw KeyErrors as we'll otherwise generate incorrect error messages @qubes.app._get_pool()
2020-07-11 13:01:22 +02:00
raise qubes . storage . StoragePoolException ( ' The specified conf_id %s could not be found inside %s . ' % ( self . _cb_conf_id , config_path ) )
2020-06-25 20:06:29 +02:00
try :
bdriver = self . _cb_conf [ ' bdriver ' ]
except KeyError :
2020-07-11 13:01:22 +02:00
raise qubes . storage . StoragePoolException ( ' Missing bdriver for the conf_id %s inside %s . ' % ( self . _cb_conf_id , config_path ) )
2020-06-25 20:06:29 +02:00
2020-06-30 11:56:24 +02:00
self . _cb_cmd_arg = json . dumps ( self . _cb_conf , sort_keys = True , indent = 2 ) #: Full configuration as string in the format required by _callback().
2020-06-25 20:06:29 +02:00
try :
cls = qubes . utils . get_entry_point_one ( qubes . storage . STORAGE_ENTRY_POINT , bdriver )
except KeyError :
2020-06-30 12:16:14 +02:00
raise qubes . storage . StoragePoolException ( ' The driver %s was not found on your system. ' % bdriver )
if not issubclass ( cls , qubes . storage . Pool ) :
raise qubes . storage . StoragePoolException ( ' The class %s must be a subclass of qubes.storage.Pool. ' % cls )
2020-06-25 20:06:29 +02:00
2020-06-30 11:56:24 +02:00
self . _cb_requires_init = self . _check_init ( ) #: Boolean indicating whether late storage initialization yet has to be done or not.
2020-07-16 17:02:19 +02:00
self . _cb_init_lock = asyncio . Lock ( ) #: Lock ensuring that late storage initialization is only run exactly once.
2020-06-25 20:06:29 +02:00
bdriver_args = self . _cb_conf . get ( ' bdriver_args ' , { } )
2020-06-30 11:56:24 +02:00
self . _cb_impl = cls ( name = name , * * bdriver_args ) #: Instance of the backend pool driver.
2020-06-25 20:06:29 +02:00
super ( ) . __init__ ( name = name , revisions_to_keep = int ( bdriver_args . get ( ' revisions_to_keep ' , 1 ) ) )
self . _cb_ctor_done = True
def _check_init ( self ) :
''' Whether or not this object requires late storage initialization via callback. '''
2020-07-05 17:45:40 +02:00
cmd = self . _cb_conf . get ( ' pre_sinit ' )
2020-06-25 20:06:29 +02:00
if not cmd :
cmd = self . _cb_conf . get ( ' cmd ' )
return bool ( cmd and cmd != ' - ' )
2020-07-01 19:48:28 +02:00
@asyncio.coroutine
2020-06-25 20:06:29 +02:00
def _init ( self , callback = True ) :
2020-06-30 11:24:15 +02:00
''' Late storage initialization on first use for e.g. decryption on first usage request.
2020-07-05 17:45:40 +02:00
: param callback : Whether to trigger the ` pre_sinit ` callback or not .
2020-06-30 11:24:15 +02:00
'''
2020-07-16 17:02:19 +02:00
with ( yield from self . _cb_init_lock ) :
2020-07-01 19:48:28 +02:00
if self . _cb_requires_init :
if callback :
2020-07-05 17:45:40 +02:00
yield from self . _callback ( ' pre_sinit ' )
2020-07-01 19:48:28 +02:00
self . _cb_requires_init = False
@asyncio.coroutine
2020-06-30 11:24:15 +02:00
def _assert_initialized ( self , * * kwargs ) :
2020-06-25 20:06:29 +02:00
if self . _cb_requires_init :
2020-07-01 19:48:28 +02:00
yield from self . _init ( * * kwargs )
2020-06-25 20:06:29 +02:00
2020-07-29 17:06:23 +02:00
@asyncio.coroutine
def _callback ( self , cb , cb_args = None ) :
''' Run a callback.
2020-06-25 20:06:29 +02:00
: param cb : Callback identifier string .
2020-06-30 11:24:15 +02:00
: param cb_args : Optional list of arguments to pass to the command as last arguments .
2020-06-25 20:06:29 +02:00
Only passed on for the generic command specified as ` cmd ` , not for ` on_xyz ` callbacks .
2020-07-29 17:06:23 +02:00
: return : Nothing .
2020-06-25 20:06:29 +02:00
'''
if self . _cb_ctor_done :
cmd = self . _cb_conf . get ( cb )
args = [ ] #on_xyz callbacks should never receive arguments
if not cmd :
2020-06-30 11:24:15 +02:00
if cb_args is None :
cb_args = [ ]
2020-06-25 20:06:29 +02:00
cmd = self . _cb_conf . get ( ' cmd ' )
2020-07-29 17:30:47 +02:00
args = [ self . name , self . _cb_conf [ ' bdriver ' ] , cb , self . _cb_conf_id , self . _cb_cmd_arg , * cb_args ]
2020-06-25 20:06:29 +02:00
if cmd and cmd != ' - ' :
args = ' ' . join ( quote ( str ( a ) ) for a in args )
cmd = ' ' . join ( filter ( None , [ cmd , args ] ) )
2020-06-30 11:45:42 +02:00
self . _cb_log . info ( ' callback driver executing ( %s , %s %s ): %s ' , self . _cb_conf_id , cb , cb_args , cmd )
2020-07-29 17:06:23 +02:00
cmd_arr = [ ' /bin/bash ' , ' -c ' , cmd ]
proc = yield from asyncio . create_subprocess_exec ( * cmd_arr , stdout = subprocess . PIPE , stderr = subprocess . PIPE )
stdout , stderr = yield from proc . communicate ( )
encoding = locale . getpreferredencoding ( )
stdout = stdout . decode ( encoding )
stderr = stderr . decode ( encoding )
if proc . returncode != 0 :
raise subprocess . CalledProcessError ( returncode = proc . returncode , cmd = cmd , output = stdout , stderr = stderr )
self . _cb_log . debug ( ' callback driver stdout ( %s , %s %s ): %s ' , self . _cb_conf_id , cb , cb_args , stdout )
self . _cb_log . debug ( ' callback driver stderr ( %s , %s %s ): %s ' , self . _cb_conf_id , cb , cb_args , stderr )
2020-06-25 20:06:29 +02:00
if self . _cb_conf . get ( ' signal_back ' , False ) is True :
2020-07-29 17:30:47 +02:00
yield from self . _process_signals ( stdout )
2020-06-25 20:06:29 +02:00
2020-07-01 19:48:28 +02:00
@asyncio.coroutine
2020-06-30 11:45:42 +02:00
def _process_signals ( self , out ) :
2020-06-25 20:06:29 +02:00
''' Process any signals found inside a string.
: param out : String to check for signals . Each signal must be on a dedicated line .
They are executed in the order they are found . Callbacks are not triggered .
'''
for line in out . splitlines ( ) :
if line == ' SIGNAL_setup ' :
2020-06-30 11:45:42 +02:00
self . _cb_log . info ( ' callback driver processing SIGNAL_setup for %s ' , self . _cb_conf_id )
2020-07-01 19:48:28 +02:00
#NOTE: calling our own methods may lead to a deadlock / qubesd freeze due to `self._assert_initialized()` / `self._cb_init_lock`
2020-07-28 18:42:02 +02:00
yield from coro_maybe ( self . _cb_impl . setup ( ) )
2020-07-01 19:48:28 +02:00
2020-07-15 18:14:42 +02:00
@property
def backend_class ( self ) :
''' Class of the first non-CallbackPool backend Pool. '''
if isinstance ( self . _cb_impl , CallbackPool ) :
return self . _cb_impl . backend_class
return self . _cb_impl . __class__
2020-06-25 20:06:29 +02:00
@property
def config ( self ) :
return {
' name ' : self . name ,
2020-07-11 13:01:22 +02:00
' driver ' : ' callback ' ,
2020-06-25 20:06:29 +02:00
' conf_id ' : self . _cb_conf_id ,
}
2020-07-01 19:48:28 +02:00
@asyncio.coroutine
2020-06-25 20:06:29 +02:00
def destroy ( self ) :
2020-07-01 19:48:28 +02:00
yield from self . _assert_initialized ( )
2020-07-28 18:42:02 +02:00
ret = yield from coro_maybe ( self . _cb_impl . destroy ( ) )
2020-07-05 17:45:40 +02:00
yield from self . _callback ( ' post_destroy ' )
2020-06-25 20:06:29 +02:00
return ret
def init_volume ( self , vm , volume_config ) :
2020-07-05 16:32:47 +02:00
ret = CallbackVolume ( self , self . _cb_impl . init_volume ( vm , volume_config ) )
volume_config [ ' pool ' ] = self
return ret
2020-06-25 20:06:29 +02:00
2020-07-01 19:48:28 +02:00
@asyncio.coroutine
2020-06-30 11:24:15 +02:00
def setup ( self ) :
2020-07-01 19:48:28 +02:00
yield from self . _assert_initialized ( callback = False ) #setup is assumed to include storage initialization
2020-07-05 17:45:40 +02:00
yield from self . _callback ( ' pre_setup ' )
2020-07-28 18:42:02 +02:00
return ( yield from coro_maybe ( self . _cb_impl . setup ( ) ) )
2020-06-30 11:24:15 +02:00
2020-06-25 20:06:29 +02:00
@property
def volumes ( self ) :
for vol in self . _cb_impl . volumes :
yield CallbackVolume ( self , vol )
def list_volumes ( self ) :
for vol in self . _cb_impl . list_volumes ( ) :
yield CallbackVolume ( self , vol )
def get_volume ( self , vid ) :
return CallbackVolume ( self , self . _cb_impl . get_volume ( vid ) )
def included_in ( self , app ) :
if self . _cb_requires_init :
return None
2020-06-30 11:24:15 +02:00
return self . _cb_impl . included_in ( app )
2020-06-25 20:06:29 +02:00
@property
def size ( self ) :
if self . _cb_requires_init :
return None
2020-06-30 11:24:15 +02:00
return self . _cb_impl . size
2020-06-25 20:06:29 +02:00
@property
def usage ( self ) :
if self . _cb_requires_init :
return None
2020-06-30 11:24:15 +02:00
return self . _cb_impl . usage
2020-06-25 20:06:29 +02:00
2020-07-11 13:01:22 +02:00
@property
def usage_details ( self ) :
if self . _cb_requires_init :
return { }
return self . _cb_impl . usage_details
#shadow all qubes.storage.Pool class attributes as instance properties
#NOTE: this will cause a subtle difference to using an actual _cb_impl instance: CallbackPool.private_img_size will return a property object, Pool.private_img_size the actual value
@property
def private_img_size ( self ) :
return self . _cb_impl . private_img_size
@private_img_size.setter
def private_img_size ( self , private_img_size ) :
self . _cb_impl . private_img_size = private_img_size
@property
def root_img_size ( self ) :
return self . _cb_impl . root_img_size
@root_img_size.setter
def root_img_size ( self , root_img_size ) :
self . _cb_impl . root_img_size = root_img_size
2020-06-25 20:06:29 +02:00
#remaining method & attribute delegation ("delegation pattern")
#Convention: The methods of this object have priority over the delegated object's methods. All attributes are
# passed to the delegated object unless their name starts with '_cb_'.
def __getattr__ ( self , name ) :
#NOTE: This method is only called when an attribute cannot be resolved locally (not part of the instance,
# not part of the class tree). It is also called for methods that cannot be resolved.
return getattr ( self . _cb_impl , name )
def __setattr__ ( self , name , value ) :
#NOTE: This method is called on every attribute assignment.
if name . startswith ( ' _cb_ ' ) :
super ( ) . __setattr__ ( name , value )
else :
setattr ( self . _cb_impl , name , value )
def __delattr__ ( self , name ) :
if name . startswith ( ' _cb_ ' ) :
super ( ) . __delattr__ ( name )
else :
delattr ( self . _cb_impl , name )
2020-07-05 17:23:21 +02:00
class CallbackVolume ( qubes . storage . Volume ) :
2020-06-25 20:06:29 +02:00
''' Proxy volume adding callback functionality to other volumes.
2020-07-05 17:45:40 +02:00
Required to support the ` pre_sinit ` and other callbacks .
2020-06-25 20:06:29 +02:00
'''
def __init__ ( self , pool , impl ) :
''' Constructor.
: param pool : ` CallbackPool ` of this volume
: param impl : ` qubes . storage . Volume ` object to wrap
'''
2020-07-05 19:44:30 +02:00
# pylint: disable=super-init-not-called
#NOTE: we must *not* call super().__init__() as it would prevent attribute delegation
2020-06-25 20:06:29 +02:00
assert isinstance ( impl , qubes . storage . Volume ) , ' impl must be a qubes.storage.Volume instance. Found a %s instance. ' % impl . __class__
assert isinstance ( pool , CallbackPool ) , ' pool must use a qubes.storage.CallbackPool instance. Found a %s instance. ' % pool . __class__
2020-07-05 16:32:47 +02:00
impl . pool = pool #enforce the CallbackPool instance as the parent pool of the volume
2020-06-30 11:56:24 +02:00
self . _cb_pool = pool #: CallbackPool instance the Volume belongs to.
self . _cb_impl = impl #: Backend volume implementation instance.
2020-06-25 20:06:29 +02:00
2020-07-01 19:48:28 +02:00
@asyncio.coroutine
2020-06-30 11:24:15 +02:00
def _assert_initialized ( self , * * kwargs ) :
2020-07-01 19:48:28 +02:00
yield from self . _cb_pool . _assert_initialized ( * * kwargs ) # pylint: disable=protected-access
2020-06-25 20:06:29 +02:00
2020-07-01 19:48:28 +02:00
@asyncio.coroutine
2020-06-30 11:24:15 +02:00
def _callback ( self , cb , cb_args = None , * * kwargs ) :
if cb_args is None :
cb_args = [ ]
2020-07-05 18:07:28 +02:00
vol_args = [ self . name , self . vid , self . source , * cb_args ]
2020-07-01 19:48:28 +02:00
yield from self . _cb_pool . _callback ( cb , cb_args = vol_args , * * kwargs ) # pylint: disable=protected-access
2020-06-25 20:06:29 +02:00
2020-07-15 18:14:42 +02:00
@property
def backend_class ( self ) :
''' Class of the first non-CallbackVolume backend Volume. '''
if isinstance ( self . _cb_impl , CallbackVolume ) :
return self . _cb_impl . backend_class
return self . _cb_impl . __class__
2020-07-01 19:48:28 +02:00
@asyncio.coroutine
2020-06-25 20:06:29 +02:00
def create ( self ) :
2020-07-01 19:48:28 +02:00
yield from self . _assert_initialized ( )
2020-07-05 17:45:40 +02:00
yield from self . _callback ( ' pre_volume_create ' )
2020-07-29 17:06:23 +02:00
ret = yield from coro_maybe ( self . _cb_impl . create ( ) )
yield from self . _callback ( ' post_volume_create ' )
return ret
2020-06-25 20:06:29 +02:00
2020-07-01 19:48:28 +02:00
@asyncio.coroutine
2020-06-25 20:06:29 +02:00
def remove ( self ) :
2020-07-01 19:48:28 +02:00
yield from self . _assert_initialized ( )
2020-07-28 18:42:02 +02:00
ret = yield from coro_maybe ( self . _cb_impl . remove ( ) )
2020-07-05 17:45:40 +02:00
yield from self . _callback ( ' post_volume_remove ' )
2020-06-25 20:06:29 +02:00
return ret
2020-07-01 19:48:28 +02:00
@asyncio.coroutine
2020-06-25 20:06:29 +02:00
def resize ( self , size ) :
2020-07-01 19:48:28 +02:00
yield from self . _assert_initialized ( )
2020-07-05 17:45:40 +02:00
yield from self . _callback ( ' pre_volume_resize ' , cb_args = [ size ] )
2020-07-28 18:42:02 +02:00
return ( yield from coro_maybe ( self . _cb_impl . resize ( size ) ) )
2020-06-25 20:06:29 +02:00
2020-07-01 19:48:28 +02:00
@asyncio.coroutine
2020-06-25 20:06:29 +02:00
def start ( self ) :
2020-07-01 19:48:28 +02:00
yield from self . _assert_initialized ( )
2020-07-05 17:45:40 +02:00
yield from self . _callback ( ' pre_volume_start ' )
2020-07-28 18:42:02 +02:00
ret = yield from coro_maybe ( self . _cb_impl . start ( ) )
2020-07-05 17:53:52 +02:00
yield from self . _callback ( ' post_volume_start ' )
return ret
2020-06-25 20:06:29 +02:00
2020-07-01 19:48:28 +02:00
@asyncio.coroutine
2020-06-25 20:06:29 +02:00
def stop ( self ) :
2020-07-01 19:48:28 +02:00
yield from self . _assert_initialized ( )
2020-07-28 18:42:02 +02:00
ret = yield from coro_maybe ( self . _cb_impl . stop ( ) )
2020-07-05 17:45:40 +02:00
yield from self . _callback ( ' post_volume_stop ' )
2020-06-25 20:06:29 +02:00
return ret
2020-07-01 19:48:28 +02:00
@asyncio.coroutine
2020-07-05 19:44:30 +02:00
def import_data ( self , size ) :
2020-07-01 19:48:28 +02:00
yield from self . _assert_initialized ( )
2020-07-05 19:44:30 +02:00
yield from self . _callback ( ' pre_volume_import_data ' , cb_args = [ size ] )
2020-07-28 18:42:02 +02:00
return ( yield from coro_maybe ( self . _cb_impl . import_data ( size ) ) )
2020-06-25 20:06:29 +02:00
2020-07-01 19:48:28 +02:00
@asyncio.coroutine
2020-06-25 20:06:29 +02:00
def import_data_end ( self , success ) :
2020-07-01 19:48:28 +02:00
yield from self . _assert_initialized ( )
2020-07-28 18:42:02 +02:00
ret = yield from coro_maybe ( self . _cb_impl . import_data_end ( success ) )
2020-07-05 17:45:40 +02:00
yield from self . _callback ( ' post_volume_import_data_end ' , cb_args = [ success ] )
2020-06-25 20:06:29 +02:00
return ret
2020-07-01 19:48:28 +02:00
@asyncio.coroutine
2020-06-25 20:06:29 +02:00
def import_volume ( self , src_volume ) :
2020-07-01 19:48:28 +02:00
yield from self . _assert_initialized ( )
2020-07-05 17:45:40 +02:00
yield from self . _callback ( ' pre_volume_import ' , cb_args = [ src_volume . vid ] )
2020-07-29 17:06:23 +02:00
ret = yield from coro_maybe ( self . _cb_impl . import_volume ( src_volume ) )
yield from self . _callback ( ' post_volume_import ' , cb_args = [ src_volume . vid ] )
return ret
2020-06-25 20:06:29 +02:00
def is_dirty ( self ) :
2020-07-01 19:48:28 +02:00
# pylint: disable=protected-access
if self . _cb_pool . _cb_requires_init :
2020-06-25 20:06:29 +02:00
return False
2020-06-30 11:24:15 +02:00
return self . _cb_impl . is_dirty ( )
2020-06-25 20:06:29 +02:00
def is_outdated ( self ) :
2020-07-01 19:48:28 +02:00
# pylint: disable=protected-access
if self . _cb_pool . _cb_requires_init :
2020-06-25 20:06:29 +02:00
return False
2020-06-30 11:24:15 +02:00
return self . _cb_impl . is_outdated ( )
2020-06-25 20:06:29 +02:00
2020-07-05 17:23:21 +02:00
@property
def revisions ( self ) :
return self . _cb_impl . revisions
@property
def size ( self ) :
return self . _cb_impl . size
@size.setter
def size ( self , size ) :
self . _cb_impl . size = size
@property
def config ( self ) :
return self . _cb_impl . config
2020-07-01 19:48:28 +02:00
def block_device ( self ) :
# pylint: disable=protected-access
if self . _cb_pool . _cb_requires_init :
# usually Volume.start() is called beforehand
# --> we should be initialized in 99% of cases
return None
return self . _cb_impl . block_device ( )
2020-07-16 17:02:19 +02:00
@asyncio.coroutine
2020-07-05 19:44:30 +02:00
def export ( self ) :
2020-07-16 17:02:19 +02:00
yield from self . _assert_initialized ( )
yield from self . _callback ( ' pre_volume_export ' )
2020-07-28 18:42:02 +02:00
return ( yield from coro_maybe ( self . _cb_impl . export ( ) ) )
2020-07-16 17:02:19 +02:00
@asyncio.coroutine
def export_end ( self , path ) :
yield from self . _assert_initialized ( )
2020-07-28 18:42:02 +02:00
ret = yield from coro_maybe ( self . _cb_impl . export_end ( path ) )
2020-07-16 17:02:19 +02:00
yield from self . _callback ( ' post_volume_export_end ' , cb_args = [ path ] )
return ret
2020-07-01 19:48:28 +02:00
@asyncio.coroutine
def verify ( self ) :
yield from self . _assert_initialized ( )
2020-07-28 18:42:02 +02:00
return ( yield from coro_maybe ( self . _cb_impl . verify ( ) ) )
2020-07-01 19:48:28 +02:00
@asyncio.coroutine
def revert ( self , revision = None ) :
yield from self . _assert_initialized ( )
2020-07-28 18:42:02 +02:00
return ( yield from coro_maybe ( self . _cb_impl . revert ( revision = revision ) ) )
2020-07-01 19:48:28 +02:00
2020-07-11 13:01:22 +02:00
#shadow all qubes.storage.Volume class attributes as instance properties
#NOTE: this will cause a subtle difference to using an actual _cb_impl instance: CallbackVolume.devtype will return a property object, Volume.devtype the actual value
@property
def devtype ( self ) :
return self . _cb_impl . devtype
@devtype.setter
def devtype ( self , devtype ) :
self . _cb_impl . devtype = devtype
@property
def domain ( self ) :
return self . _cb_impl . domain
@domain.setter
def domain ( self , domain ) :
self . _cb_impl . domain = domain
@property
def path ( self ) :
return self . _cb_impl . path
@path.setter
def path ( self , path ) :
self . _cb_impl . path = path
@property
def script ( self ) :
return self . _cb_impl . script
@script.setter
def script ( self , script ) :
self . _cb_impl . script = script
@property
def usage ( self ) :
return self . _cb_impl . usage
@usage.setter
def usage ( self , usage ) :
self . _cb_impl . usage = usage
2020-06-25 20:06:29 +02:00
#remaining method & attribute delegation
def __getattr__ ( self , name ) :
return getattr ( self . _cb_impl , name )
def __setattr__ ( self , name , value ) :
if name . startswith ( ' _cb_ ' ) :
super ( ) . __setattr__ ( name , value )
else :
setattr ( self . _cb_impl , name , value )
def __delattr__ ( self , name ) :
if name . startswith ( ' _cb_ ' ) :
super ( ) . __delattr__ ( name )
else :
delattr ( self . _cb_impl , name )