callback.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. #
  2. # The Qubes OS Project, https://www.qubes-os.org/
  3. #
  4. # Copyright (C) 2020 David Hobach <david@hobach.de>
  5. #
  6. # This library is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU Lesser General Public
  8. # License as published by the Free Software Foundation; either
  9. # version 2.1 of the License, or (at your option) any later version.
  10. #
  11. # This library is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. # Lesser General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Lesser General Public
  17. # License along with this library; if not, see <https://www.gnu.org/licenses/>.
  18. #
  19. # pylint: disable=line-too-long
  20. import logging
  21. import subprocess
  22. import json
  23. import asyncio
  24. import locale
  25. from shlex import quote
  26. from qubes.utils import coro_maybe
  27. import qubes.storage
  28. class UnhandledSignalException(qubes.storage.StoragePoolException):
  29. def __init__(self, pool, signal):
  30. super().__init__('The pool %s failed to handle the signal %s, likely because it was run from synchronous code.' % (pool.name, signal))
  31. class CallbackPool(qubes.storage.Pool):
  32. ''' Proxy storage pool driver adding callback functionality to other pool drivers.
  33. This way, users can extend storage pool drivers with custom functionality using the programming language of their choice.
  34. All configuration for this pool driver must be done in `/etc/qubes_callback.json`. Each configuration ID `conf_id` can be used
  35. to create a callback pool with e.g. `qvm-pool -o conf_id=your_conf_id -a pool_name callback`.
  36. Check `/usr/share/doc/qubes/qubes_callback.json.example` for an overview of the available options.
  37. Example applications of this driver:
  38. - custom pool mounts
  39. - encryption
  40. - debugging
  41. **Integration tests**:
  42. (all of these tests assume the `qubes_callback.json.example` configuration)
  43. Tests that should **fail**:
  44. ```
  45. qvm-pool -a test callback
  46. qvm-pool -o conf_id=non-existing -a test callback
  47. qvm-pool -o conf_id=conf_id -a test callback
  48. qvm-pool -o conf_id=testing-fail-missing-all -a test callback
  49. qvm-pool -o conf_id=testing-fail-missing-bdriver-args -a test callback
  50. ```
  51. Tests that should **work**:
  52. ```
  53. qvm-pool -o conf_id=testing-succ-file-01 -a test callback
  54. qvm-pool
  55. ls /mnt/test01
  56. qvm-pool -r test && sudo rm -rf /mnt/test01
  57. 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
  58. rm -f /tmp/callback.log
  59. qvm-pool -o conf_id=testing-succ-file-02 -a test callback
  60. qvm-pool
  61. ls /mnt/test02
  62. less /tmp/callback.log (pre_setup should be there)
  63. qvm-create -l red -P test test-vm
  64. cat /tmp/callback.log (2x pre_volume_create + 2x post_volume_create should be added)
  65. qvm-start test-vm
  66. qvm-volume | grep test-vm
  67. grep test-vm /var/lib/qubes/qubes.xml
  68. ls /mnt/test02/appvms/
  69. cat /tmp/callback.log (2x pre_volume_start & 2x post_volume_start should be added)
  70. qvm-shutdown test-vm
  71. cat /tmp/callback.log (2x post_volume_stop should be added)
  72. #reboot
  73. cat /tmp/callback.log (it should not exist)
  74. qvm-start test-vm
  75. cat /tmp/callback.log (pre_sinit & 2x pre_volume_start & 2x post_volume_start should be added)
  76. qvm-shutdown --wait test-vm && qvm-remove test-vm
  77. qvm-pool -r test && sudo rm -rf /mnt/test02
  78. less /tmp/callback.log (2x post_volume_stop, 2x post_volume_remove, post_destroy should be added)
  79. qvm-pool -o conf_id=testing-succ-file-02 -a test callback
  80. qvm-create -l red -P test test-dvm
  81. qvm-prefs test-dvm template_for_dispvms True
  82. qvm-run --dispvm test-dvm xterm
  83. grep -E 'test-dvm|disp' /var/lib/qubes/qubes.xml
  84. qvm-volume | grep -E 'test-dvm|disp' (unexpected by most users: Qubes OS places only the private volume on the pool, cf. #5933)
  85. ls /mnt/test02/appvms/
  86. cat /tmp/callback.log
  87. #close the disposable VM
  88. qvm-remove test-dvm
  89. qvm-pool -r test && sudo rm -rf /mnt/test02
  90. qvm-pool -o conf_id=testing-succ-file-03 -a test callback
  91. qvm-pool
  92. ls /mnt/test03
  93. less /tmp/callback.log (pre_setup should be there, no more arguments)
  94. qvm-pool -r test && sudo rm -rf /mnt/test03
  95. less /tmp/callback.log (nothing should have been added)
  96. #luks pool test:
  97. #(make sure /mnt/test.key & /mnt/test.luks don't exist)
  98. qvm-pool -o conf_id=testing-succ-file-luks -a tluks callback
  99. ls /mnt/
  100. qvm-pool
  101. sudo cryptsetup status test-luks
  102. sudo mount | grep test_luks
  103. ls /mnt/test_luks/
  104. qvm-create -l red -P tluks test-luks (journalctl -b0 should show two pre_volume_create callbacks)
  105. ls /mnt/test_luks/appvms/test-luks/
  106. qvm-volume | grep test-luks
  107. qvm-start test-luks
  108. #reboot
  109. grep luks /var/lib/qubes/qubes.xml
  110. sudo cryptsetup status test-luks (should be inactive due to late pre_sinit!)
  111. qvm-start test-luks
  112. sudo mount | grep test_luks
  113. qvm-shutdown --wait test-luks
  114. qvm-remove test-luks
  115. qvm-pool -r tluks
  116. sudo cryptsetup status test-luks
  117. ls -l /mnt/
  118. #ephemeral luks pool test (key in RAM / lost on reboot):
  119. qvm-pool -o conf_id=testing-succ-file-luks-eph -a teph callback (executes setup() twice due to signal_back)
  120. ls /mnt/
  121. ls /mnt/ram
  122. md5sum /mnt/ram/teph.key (1)
  123. sudo mount|grep -E 'ram|test'
  124. sudo cryptsetup status test-eph
  125. qvm-create -l red -P teph test-eph (should execute two pre_volume_create callbacks)
  126. qvm-volume | grep test-eph
  127. ls /mnt/test_eph/appvms/test-eph/ (should have private.img and volatile.img)
  128. ls /var/lib/qubes/appvms/test-eph (should only have the icon)
  129. qvm-start test-eph
  130. #reboot
  131. ls /mnt/ram (should be empty)
  132. ls /mnt/
  133. sudo mount|grep -E 'ram|test' (should be empty)
  134. qvm-ls | grep eph (should still have test-eph)
  135. grep eph /var/lib/qubes/qubes.xml (should still have test-eph)
  136. qvm-remove test-eph (should create a new encrypted pool backend)
  137. sudo cryptsetup status test-eph
  138. grep eph /var/lib/qubes/qubes.xml (only the pool should be left)
  139. ls /mnt/test_eph/ (should have the appvms directory etc.)
  140. qvm-create -l red -P teph test-eph2
  141. ls /mnt/test_eph/appvms/
  142. ls /mnt/ram
  143. qvm-start test-eph2
  144. md5sum /mnt/ram/teph.key ((2), different than in (1))
  145. qvm-shutdown --wait test-eph2
  146. systemctl restart qubesd
  147. qvm-start test-eph2 (trigger storage re-init)
  148. md5sum /mnt/ram/teph.key (same as in (2))
  149. qvm-shutdown --wait test-eph2
  150. sudo umount /mnt/test_eph
  151. qvm-create -l red -P teph test-eph-fail (must fail with error in journalctl)
  152. ls /mnt/test_eph/ (should be empty)
  153. systemctl restart qubesd
  154. qvm-remove test-eph2
  155. qvm-create -l red -P teph test-eph3
  156. md5sum /mnt/ram/teph.key (same as in (2))
  157. sudo mount|grep -E 'ram|test'
  158. ls /mnt/test_eph/appvms/test-eph3
  159. qvm-remove test-eph3
  160. qvm-ls | grep test-eph
  161. qvm-pool -r teph
  162. grep eph /var/lib/qubes/qubes.xml (nothing should be left)
  163. qvm-pool
  164. ls /mnt/
  165. ls /mnt/ram/ (should be empty)
  166. ```
  167. '''
  168. def __init__(self, *, name, conf_id):
  169. '''Constructor.
  170. :param conf_id: Identifier as found inside the user-controlled configuration at `/etc/qubes_callback.json`.
  171. Non-ASCII, non-alphanumeric characters may be disallowed.
  172. **Security Note**: Depending on your RPC policy (admin.pool.Add) this constructor and its parameters
  173. may be called from an untrusted VM (not by default though). In those cases it may be security-relevant
  174. not to pick easily guessable `conf_id` values for your configuration as untrusted VMs may otherwise
  175. execute callbacks meant for other pools.
  176. :raise StoragePoolException: For user configuration issues.
  177. '''
  178. #NOTE: attribute names **must** start with `_cb_` unless they are meant to be stored as self._cb_impl attributes
  179. self._cb_ctor_done = False #: Boolean to indicate whether or not `__init__` successfully ran through.
  180. self._cb_log = logging.getLogger('qubes.storage.callback') #: Logger instance.
  181. if not isinstance(conf_id, str):
  182. raise qubes.storage.StoragePoolException('conf_id is no String. VM attack?!')
  183. self._cb_conf_id = conf_id #: Configuration ID as passed to `__init__()`.
  184. config_path = '/etc/qubes_callback.json'
  185. with open(config_path) as json_file:
  186. conf_all = json.load(json_file)
  187. if not isinstance(conf_all, dict):
  188. raise qubes.storage.StoragePoolException('The file %s is supposed to define a dict.' % config_path)
  189. try:
  190. self._cb_conf = conf_all[self._cb_conf_id] #: Dictionary holding all configuration for the given _cb_conf_id.
  191. except KeyError:
  192. #we cannot throw KeyErrors as we'll otherwise generate incorrect error messages @qubes.app._get_pool()
  193. raise qubes.storage.StoragePoolException('The specified conf_id %s could not be found inside %s.' % (self._cb_conf_id, config_path))
  194. try:
  195. bdriver = self._cb_conf['bdriver']
  196. except KeyError:
  197. raise qubes.storage.StoragePoolException('Missing bdriver for the conf_id %s inside %s.' % (self._cb_conf_id, config_path))
  198. self._cb_cmd_arg = json.dumps(self._cb_conf, sort_keys=True, indent=2) #: Full configuration as string in the format required by _callback().
  199. try:
  200. cls = qubes.utils.get_entry_point_one(qubes.storage.STORAGE_ENTRY_POINT, bdriver)
  201. except KeyError:
  202. raise qubes.storage.StoragePoolException('The driver %s was not found on your system.' % bdriver)
  203. if not issubclass(cls, qubes.storage.Pool):
  204. raise qubes.storage.StoragePoolException('The class %s must be a subclass of qubes.storage.Pool.' % cls)
  205. self._cb_requires_init = self._check_init() #: Boolean indicating whether late storage initialization yet has to be done or not.
  206. self._cb_init_lock = asyncio.Lock() #: Lock ensuring that late storage initialization is only run exactly once.
  207. bdriver_args = self._cb_conf.get('bdriver_args', {})
  208. self._cb_impl = cls(name=name, **bdriver_args) #: Instance of the backend pool driver.
  209. super().__init__(name=name, revisions_to_keep=int(bdriver_args.get('revisions_to_keep', 1)))
  210. self._cb_ctor_done = True
  211. def _check_init(self):
  212. ''' Whether or not this object requires late storage initialization via callback. '''
  213. cmd = self._cb_conf.get('pre_sinit')
  214. if not cmd:
  215. cmd = self._cb_conf.get('cmd')
  216. return bool(cmd and cmd != '-')
  217. @asyncio.coroutine
  218. def _init(self, callback=True):
  219. ''' Late storage initialization on first use for e.g. decryption on first usage request.
  220. :param callback: Whether to trigger the `pre_sinit` callback or not.
  221. '''
  222. with (yield from self._cb_init_lock):
  223. if self._cb_requires_init:
  224. if callback:
  225. yield from self._callback('pre_sinit')
  226. self._cb_requires_init = False
  227. @asyncio.coroutine
  228. def _assert_initialized(self, **kwargs):
  229. if self._cb_requires_init:
  230. yield from self._init(**kwargs)
  231. @asyncio.coroutine
  232. def _callback(self, cb, cb_args=None):
  233. '''Run a callback.
  234. :param cb: Callback identifier string.
  235. :param cb_args: Optional list of arguments to pass to the command as last arguments.
  236. Only passed on for the generic command specified as `cmd`, not for `on_xyz` callbacks.
  237. :return: Nothing.
  238. '''
  239. if self._cb_ctor_done:
  240. cmd = self._cb_conf.get(cb)
  241. args = [] #on_xyz callbacks should never receive arguments
  242. if not cmd:
  243. if cb_args is None:
  244. cb_args = []
  245. cmd = self._cb_conf.get('cmd')
  246. args = [self.name, self._cb_conf['bdriver'], cb, self._cb_conf_id, self._cb_cmd_arg, *cb_args]
  247. if cmd and cmd != '-':
  248. args = ' '.join(quote(str(a)) for a in args)
  249. cmd = ' '.join(filter(None, [cmd, args]))
  250. self._cb_log.info('callback driver executing (%s, %s %s): %s', self._cb_conf_id, cb, cb_args, cmd)
  251. cmd_arr = ['/bin/bash', '-c', cmd]
  252. proc = yield from asyncio.create_subprocess_exec(*cmd_arr, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  253. stdout, stderr = yield from proc.communicate()
  254. encoding = locale.getpreferredencoding()
  255. stdout = stdout.decode(encoding)
  256. stderr = stderr.decode(encoding)
  257. if proc.returncode != 0:
  258. raise subprocess.CalledProcessError(returncode=proc.returncode, cmd=cmd, output=stdout, stderr=stderr)
  259. self._cb_log.debug('callback driver stdout (%s, %s %s): %s', self._cb_conf_id, cb, cb_args, stdout)
  260. self._cb_log.debug('callback driver stderr (%s, %s %s): %s', self._cb_conf_id, cb, cb_args, stderr)
  261. if self._cb_conf.get('signal_back', False) is True:
  262. yield from self._process_signals(stdout)
  263. @asyncio.coroutine
  264. def _process_signals(self, out):
  265. '''Process any signals found inside a string.
  266. :param out: String to check for signals. Each signal must be on a dedicated line.
  267. They are executed in the order they are found. Callbacks are not triggered.
  268. '''
  269. for line in out.splitlines():
  270. if line == 'SIGNAL_setup':
  271. self._cb_log.info('callback driver processing SIGNAL_setup for %s', self._cb_conf_id)
  272. #NOTE: calling our own methods may lead to a deadlock / qubesd freeze due to `self._assert_initialized()` / `self._cb_init_lock`
  273. yield from coro_maybe(self._cb_impl.setup())
  274. @property
  275. def backend_class(self):
  276. '''Class of the first non-CallbackPool backend Pool.'''
  277. if isinstance(self._cb_impl, CallbackPool):
  278. return self._cb_impl.backend_class
  279. return self._cb_impl.__class__
  280. @property
  281. def config(self):
  282. return {
  283. 'name': self.name,
  284. 'driver': 'callback',
  285. 'conf_id': self._cb_conf_id,
  286. }
  287. @asyncio.coroutine
  288. def destroy(self):
  289. yield from self._assert_initialized()
  290. ret = yield from coro_maybe(self._cb_impl.destroy())
  291. yield from self._callback('post_destroy')
  292. return ret
  293. def init_volume(self, vm, volume_config):
  294. ret = CallbackVolume(self, self._cb_impl.init_volume(vm, volume_config))
  295. volume_config['pool'] = self
  296. return ret
  297. @asyncio.coroutine
  298. def setup(self):
  299. yield from self._assert_initialized(callback=False) #setup is assumed to include storage initialization
  300. yield from self._callback('pre_setup')
  301. return (yield from coro_maybe(self._cb_impl.setup()))
  302. @property
  303. def volumes(self):
  304. for vol in self._cb_impl.volumes:
  305. yield CallbackVolume(self, vol)
  306. def list_volumes(self):
  307. for vol in self._cb_impl.list_volumes():
  308. yield CallbackVolume(self, vol)
  309. def get_volume(self, vid):
  310. return CallbackVolume(self, self._cb_impl.get_volume(vid))
  311. def included_in(self, app):
  312. if self._cb_requires_init:
  313. return None
  314. return self._cb_impl.included_in(app)
  315. @property
  316. def size(self):
  317. if self._cb_requires_init:
  318. return None
  319. return self._cb_impl.size
  320. @property
  321. def usage(self):
  322. if self._cb_requires_init:
  323. return None
  324. return self._cb_impl.usage
  325. @property
  326. def usage_details(self):
  327. if self._cb_requires_init:
  328. return {}
  329. return self._cb_impl.usage_details
  330. #shadow all qubes.storage.Pool class attributes as instance properties
  331. #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
  332. @property
  333. def private_img_size(self):
  334. return self._cb_impl.private_img_size
  335. @private_img_size.setter
  336. def private_img_size(self, private_img_size):
  337. self._cb_impl.private_img_size = private_img_size
  338. @property
  339. def root_img_size(self):
  340. return self._cb_impl.root_img_size
  341. @root_img_size.setter
  342. def root_img_size(self, root_img_size):
  343. self._cb_impl.root_img_size = root_img_size
  344. #remaining method & attribute delegation ("delegation pattern")
  345. #Convention: The methods of this object have priority over the delegated object's methods. All attributes are
  346. # passed to the delegated object unless their name starts with '_cb_'.
  347. def __getattr__(self, name):
  348. #NOTE: This method is only called when an attribute cannot be resolved locally (not part of the instance,
  349. # not part of the class tree). It is also called for methods that cannot be resolved.
  350. return getattr(self._cb_impl, name)
  351. def __setattr__(self, name, value):
  352. #NOTE: This method is called on every attribute assignment.
  353. if name.startswith('_cb_'):
  354. super().__setattr__(name, value)
  355. else:
  356. setattr(self._cb_impl, name, value)
  357. def __delattr__(self, name):
  358. if name.startswith('_cb_'):
  359. super().__delattr__(name)
  360. else:
  361. delattr(self._cb_impl, name)
  362. class CallbackVolume(qubes.storage.Volume):
  363. ''' Proxy volume adding callback functionality to other volumes.
  364. Required to support the `pre_sinit` and other callbacks.
  365. '''
  366. def __init__(self, pool, impl):
  367. '''Constructor.
  368. :param pool: `CallbackPool` of this volume
  369. :param impl: `qubes.storage.Volume` object to wrap
  370. '''
  371. # pylint: disable=super-init-not-called
  372. #NOTE: we must *not* call super().__init__() as it would prevent attribute delegation
  373. assert isinstance(impl, qubes.storage.Volume), 'impl must be a qubes.storage.Volume instance. Found a %s instance.' % impl.__class__
  374. assert isinstance(pool, CallbackPool), 'pool must use a qubes.storage.CallbackPool instance. Found a %s instance.' % pool.__class__
  375. impl.pool = pool #enforce the CallbackPool instance as the parent pool of the volume
  376. self._cb_pool = pool #: CallbackPool instance the Volume belongs to.
  377. self._cb_impl = impl #: Backend volume implementation instance.
  378. @asyncio.coroutine
  379. def _assert_initialized(self, **kwargs):
  380. yield from self._cb_pool._assert_initialized(**kwargs) # pylint: disable=protected-access
  381. @asyncio.coroutine
  382. def _callback(self, cb, cb_args=None, **kwargs):
  383. if cb_args is None:
  384. cb_args = []
  385. vol_args = [self.name, self.vid, self.source, *cb_args]
  386. yield from self._cb_pool._callback(cb, cb_args=vol_args, **kwargs) # pylint: disable=protected-access
  387. @property
  388. def backend_class(self):
  389. '''Class of the first non-CallbackVolume backend Volume.'''
  390. if isinstance(self._cb_impl, CallbackVolume):
  391. return self._cb_impl.backend_class
  392. return self._cb_impl.__class__
  393. @asyncio.coroutine
  394. def create(self):
  395. yield from self._assert_initialized()
  396. yield from self._callback('pre_volume_create')
  397. ret = yield from coro_maybe(self._cb_impl.create())
  398. yield from self._callback('post_volume_create')
  399. return ret
  400. @asyncio.coroutine
  401. def remove(self):
  402. yield from self._assert_initialized()
  403. ret = yield from coro_maybe(self._cb_impl.remove())
  404. yield from self._callback('post_volume_remove')
  405. return ret
  406. @asyncio.coroutine
  407. def resize(self, size):
  408. yield from self._assert_initialized()
  409. yield from self._callback('pre_volume_resize', cb_args=[size])
  410. return (yield from coro_maybe(self._cb_impl.resize(size)))
  411. @asyncio.coroutine
  412. def start(self):
  413. yield from self._assert_initialized()
  414. yield from self._callback('pre_volume_start')
  415. ret = yield from coro_maybe(self._cb_impl.start())
  416. yield from self._callback('post_volume_start')
  417. return ret
  418. @asyncio.coroutine
  419. def stop(self):
  420. yield from self._assert_initialized()
  421. ret = yield from coro_maybe(self._cb_impl.stop())
  422. yield from self._callback('post_volume_stop')
  423. return ret
  424. @asyncio.coroutine
  425. def import_data(self, size):
  426. yield from self._assert_initialized()
  427. yield from self._callback('pre_volume_import_data', cb_args=[size])
  428. return (yield from coro_maybe(self._cb_impl.import_data(size)))
  429. @asyncio.coroutine
  430. def import_data_end(self, success):
  431. yield from self._assert_initialized()
  432. ret = yield from coro_maybe(self._cb_impl.import_data_end(success))
  433. yield from self._callback('post_volume_import_data_end', cb_args=[success])
  434. return ret
  435. @asyncio.coroutine
  436. def import_volume(self, src_volume):
  437. yield from self._assert_initialized()
  438. yield from self._callback('pre_volume_import', cb_args=[src_volume.vid])
  439. ret = yield from coro_maybe(self._cb_impl.import_volume(src_volume))
  440. yield from self._callback('post_volume_import', cb_args=[src_volume.vid])
  441. return ret
  442. def is_dirty(self):
  443. # pylint: disable=protected-access
  444. if self._cb_pool._cb_requires_init:
  445. return False
  446. return self._cb_impl.is_dirty()
  447. def is_outdated(self):
  448. # pylint: disable=protected-access
  449. if self._cb_pool._cb_requires_init:
  450. return False
  451. return self._cb_impl.is_outdated()
  452. @property
  453. def revisions(self):
  454. return self._cb_impl.revisions
  455. @property
  456. def size(self):
  457. return self._cb_impl.size
  458. @size.setter
  459. def size(self, size):
  460. self._cb_impl.size = size
  461. @property
  462. def config(self):
  463. return self._cb_impl.config
  464. def block_device(self):
  465. # pylint: disable=protected-access
  466. if self._cb_pool._cb_requires_init:
  467. # usually Volume.start() is called beforehand
  468. # --> we should be initialized in 99% of cases
  469. return None
  470. return self._cb_impl.block_device()
  471. @asyncio.coroutine
  472. def export(self):
  473. yield from self._assert_initialized()
  474. yield from self._callback('pre_volume_export')
  475. return (yield from coro_maybe(self._cb_impl.export()))
  476. @asyncio.coroutine
  477. def export_end(self, path):
  478. yield from self._assert_initialized()
  479. ret = yield from coro_maybe(self._cb_impl.export_end(path))
  480. yield from self._callback('post_volume_export_end', cb_args=[path])
  481. return ret
  482. @asyncio.coroutine
  483. def verify(self):
  484. yield from self._assert_initialized()
  485. return (yield from coro_maybe(self._cb_impl.verify()))
  486. @asyncio.coroutine
  487. def revert(self, revision=None):
  488. yield from self._assert_initialized()
  489. return (yield from coro_maybe(self._cb_impl.revert(revision=revision)))
  490. #shadow all qubes.storage.Volume class attributes as instance properties
  491. #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
  492. @property
  493. def devtype(self):
  494. return self._cb_impl.devtype
  495. @devtype.setter
  496. def devtype(self, devtype):
  497. self._cb_impl.devtype = devtype
  498. @property
  499. def domain(self):
  500. return self._cb_impl.domain
  501. @domain.setter
  502. def domain(self, domain):
  503. self._cb_impl.domain = domain
  504. @property
  505. def path(self):
  506. return self._cb_impl.path
  507. @path.setter
  508. def path(self, path):
  509. self._cb_impl.path = path
  510. @property
  511. def script(self):
  512. return self._cb_impl.script
  513. @script.setter
  514. def script(self, script):
  515. self._cb_impl.script = script
  516. @property
  517. def usage(self):
  518. return self._cb_impl.usage
  519. @usage.setter
  520. def usage(self, usage):
  521. self._cb_impl.usage = usage
  522. #remaining method & attribute delegation
  523. def __getattr__(self, name):
  524. return getattr(self._cb_impl, name)
  525. def __setattr__(self, name, value):
  526. if name.startswith('_cb_'):
  527. super().__setattr__(name, value)
  528. else:
  529. setattr(self._cb_impl, name, value)
  530. def __delattr__(self, name):
  531. if name.startswith('_cb_'):
  532. super().__delattr__(name)
  533. else:
  534. delattr(self._cb_impl, name)