callback.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  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. async def _init(self, callback=True):
  218. ''' Late storage initialization on first use for e.g. decryption on first usage request.
  219. :param callback: Whether to trigger the `pre_sinit` callback or not.
  220. '''
  221. async with self._cb_init_lock:
  222. if self._cb_requires_init:
  223. if callback:
  224. await self._callback('pre_sinit')
  225. self._cb_requires_init = False
  226. @asyncio.coroutine
  227. def _assert_initialized(self, **kwargs):
  228. if self._cb_requires_init:
  229. yield from self._init(**kwargs)
  230. @asyncio.coroutine
  231. def _callback(self, cb, cb_args=None):
  232. '''Run a callback.
  233. :param cb: Callback identifier string.
  234. :param cb_args: Optional list of arguments to pass to the command as last arguments.
  235. Only passed on for the generic command specified as `cmd`, not for `on_xyz` callbacks.
  236. :return: Nothing.
  237. '''
  238. if self._cb_ctor_done:
  239. cmd = self._cb_conf.get(cb)
  240. args = [] #on_xyz callbacks should never receive arguments
  241. if not cmd:
  242. if cb_args is None:
  243. cb_args = []
  244. cmd = self._cb_conf.get('cmd')
  245. args = [self.name, self._cb_conf['bdriver'], cb, self._cb_conf_id, self._cb_cmd_arg, *cb_args]
  246. if cmd and cmd != '-':
  247. args = ' '.join(quote(str(a)) for a in args)
  248. cmd = ' '.join(filter(None, [cmd, args]))
  249. self._cb_log.info('callback driver executing (%s, %s %s): %s', self._cb_conf_id, cb, cb_args, cmd)
  250. cmd_arr = ['/bin/bash', '-c', cmd]
  251. proc = yield from asyncio.create_subprocess_exec(*cmd_arr, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  252. stdout, stderr = yield from proc.communicate()
  253. encoding = locale.getpreferredencoding()
  254. stdout = stdout.decode(encoding)
  255. stderr = stderr.decode(encoding)
  256. if proc.returncode != 0:
  257. raise subprocess.CalledProcessError(returncode=proc.returncode, cmd=cmd, output=stdout, stderr=stderr)
  258. self._cb_log.debug('callback driver stdout (%s, %s %s): %s', self._cb_conf_id, cb, cb_args, stdout)
  259. self._cb_log.debug('callback driver stderr (%s, %s %s): %s', self._cb_conf_id, cb, cb_args, stderr)
  260. if self._cb_conf.get('signal_back', False) is True:
  261. yield from self._process_signals(stdout)
  262. @asyncio.coroutine
  263. def _process_signals(self, out):
  264. '''Process any signals found inside a string.
  265. :param out: String to check for signals. Each signal must be on a dedicated line.
  266. They are executed in the order they are found. Callbacks are not triggered.
  267. '''
  268. for line in out.splitlines():
  269. if line == 'SIGNAL_setup':
  270. self._cb_log.info('callback driver processing SIGNAL_setup for %s', self._cb_conf_id)
  271. #NOTE: calling our own methods may lead to a deadlock / qubesd freeze due to `self._assert_initialized()` / `self._cb_init_lock`
  272. yield from coro_maybe(self._cb_impl.setup())
  273. @property
  274. def backend_class(self):
  275. '''Class of the first non-CallbackPool backend Pool.'''
  276. if isinstance(self._cb_impl, CallbackPool):
  277. return self._cb_impl.backend_class
  278. return self._cb_impl.__class__
  279. @property
  280. def config(self):
  281. return {
  282. 'name': self.name,
  283. 'driver': 'callback',
  284. 'conf_id': self._cb_conf_id,
  285. }
  286. @asyncio.coroutine
  287. def destroy(self):
  288. yield from self._assert_initialized()
  289. ret = yield from coro_maybe(self._cb_impl.destroy())
  290. yield from self._callback('post_destroy')
  291. return ret
  292. def init_volume(self, vm, volume_config):
  293. ret = CallbackVolume(self, self._cb_impl.init_volume(vm, volume_config))
  294. volume_config['pool'] = self
  295. return ret
  296. @asyncio.coroutine
  297. def setup(self):
  298. yield from self._assert_initialized(callback=False) #setup is assumed to include storage initialization
  299. yield from self._callback('pre_setup')
  300. return (yield from coro_maybe(self._cb_impl.setup()))
  301. @property
  302. def volumes(self):
  303. for vol in self._cb_impl.volumes:
  304. yield CallbackVolume(self, vol)
  305. def list_volumes(self):
  306. for vol in self._cb_impl.list_volumes():
  307. yield CallbackVolume(self, vol)
  308. def get_volume(self, vid):
  309. return CallbackVolume(self, self._cb_impl.get_volume(vid))
  310. def included_in(self, app):
  311. if self._cb_requires_init:
  312. return None
  313. return self._cb_impl.included_in(app)
  314. @property
  315. def size(self):
  316. if self._cb_requires_init:
  317. return None
  318. return self._cb_impl.size
  319. @property
  320. def usage(self):
  321. if self._cb_requires_init:
  322. return None
  323. return self._cb_impl.usage
  324. @property
  325. def usage_details(self):
  326. if self._cb_requires_init:
  327. return {}
  328. return self._cb_impl.usage_details
  329. #shadow all qubes.storage.Pool class attributes as instance properties
  330. #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
  331. @property
  332. def private_img_size(self):
  333. return self._cb_impl.private_img_size
  334. @private_img_size.setter
  335. def private_img_size(self, private_img_size):
  336. self._cb_impl.private_img_size = private_img_size
  337. @property
  338. def root_img_size(self):
  339. return self._cb_impl.root_img_size
  340. @root_img_size.setter
  341. def root_img_size(self, root_img_size):
  342. self._cb_impl.root_img_size = root_img_size
  343. #remaining method & attribute delegation ("delegation pattern")
  344. #Convention: The methods of this object have priority over the delegated object's methods. All attributes are
  345. # passed to the delegated object unless their name starts with '_cb_'.
  346. def __getattr__(self, name):
  347. #NOTE: This method is only called when an attribute cannot be resolved locally (not part of the instance,
  348. # not part of the class tree). It is also called for methods that cannot be resolved.
  349. return getattr(self._cb_impl, name)
  350. def __setattr__(self, name, value):
  351. #NOTE: This method is called on every attribute assignment.
  352. if name.startswith('_cb_'):
  353. super().__setattr__(name, value)
  354. else:
  355. setattr(self._cb_impl, name, value)
  356. def __delattr__(self, name):
  357. if name.startswith('_cb_'):
  358. super().__delattr__(name)
  359. else:
  360. delattr(self._cb_impl, name)
  361. class CallbackVolume(qubes.storage.Volume):
  362. ''' Proxy volume adding callback functionality to other volumes.
  363. Required to support the `pre_sinit` and other callbacks.
  364. '''
  365. def __init__(self, pool, impl):
  366. '''Constructor.
  367. :param pool: `CallbackPool` of this volume
  368. :param impl: `qubes.storage.Volume` object to wrap
  369. '''
  370. # pylint: disable=super-init-not-called
  371. #NOTE: we must *not* call super().__init__() as it would prevent attribute delegation
  372. assert isinstance(impl, qubes.storage.Volume), 'impl must be a qubes.storage.Volume instance. Found a %s instance.' % impl.__class__
  373. assert isinstance(pool, CallbackPool), 'pool must use a qubes.storage.CallbackPool instance. Found a %s instance.' % pool.__class__
  374. impl.pool = pool #enforce the CallbackPool instance as the parent pool of the volume
  375. self._cb_pool = pool #: CallbackPool instance the Volume belongs to.
  376. self._cb_impl = impl #: Backend volume implementation instance.
  377. @asyncio.coroutine
  378. def _assert_initialized(self, **kwargs):
  379. yield from self._cb_pool._assert_initialized(**kwargs) # pylint: disable=protected-access
  380. @asyncio.coroutine
  381. def _callback(self, cb, cb_args=None, **kwargs):
  382. if cb_args is None:
  383. cb_args = []
  384. vol_args = [self.name, self.vid, self.source, *cb_args]
  385. yield from self._cb_pool._callback(cb, cb_args=vol_args, **kwargs) # pylint: disable=protected-access
  386. @property
  387. def backend_class(self):
  388. '''Class of the first non-CallbackVolume backend Volume.'''
  389. if isinstance(self._cb_impl, CallbackVolume):
  390. return self._cb_impl.backend_class
  391. return self._cb_impl.__class__
  392. @asyncio.coroutine
  393. def create(self):
  394. yield from self._assert_initialized()
  395. yield from self._callback('pre_volume_create')
  396. ret = yield from coro_maybe(self._cb_impl.create())
  397. yield from self._callback('post_volume_create')
  398. return ret
  399. @asyncio.coroutine
  400. def remove(self):
  401. yield from self._assert_initialized()
  402. ret = yield from coro_maybe(self._cb_impl.remove())
  403. yield from self._callback('post_volume_remove')
  404. return ret
  405. @asyncio.coroutine
  406. def resize(self, size):
  407. yield from self._assert_initialized()
  408. yield from self._callback('pre_volume_resize', cb_args=[size])
  409. return (yield from coro_maybe(self._cb_impl.resize(size)))
  410. @asyncio.coroutine
  411. def start(self):
  412. yield from self._assert_initialized()
  413. yield from self._callback('pre_volume_start')
  414. ret = yield from coro_maybe(self._cb_impl.start())
  415. yield from self._callback('post_volume_start')
  416. return ret
  417. @asyncio.coroutine
  418. def stop(self):
  419. yield from self._assert_initialized()
  420. ret = yield from coro_maybe(self._cb_impl.stop())
  421. yield from self._callback('post_volume_stop')
  422. return ret
  423. @asyncio.coroutine
  424. def import_data(self, size):
  425. yield from self._assert_initialized()
  426. yield from self._callback('pre_volume_import_data', cb_args=[size])
  427. return (yield from coro_maybe(self._cb_impl.import_data(size)))
  428. @asyncio.coroutine
  429. def import_data_end(self, success):
  430. yield from self._assert_initialized()
  431. ret = yield from coro_maybe(self._cb_impl.import_data_end(success))
  432. yield from self._callback('post_volume_import_data_end', cb_args=[success])
  433. return ret
  434. @asyncio.coroutine
  435. def import_volume(self, src_volume):
  436. yield from self._assert_initialized()
  437. yield from self._callback('pre_volume_import', cb_args=[src_volume.vid])
  438. ret = yield from coro_maybe(self._cb_impl.import_volume(src_volume))
  439. yield from self._callback('post_volume_import', cb_args=[src_volume.vid])
  440. return ret
  441. def is_dirty(self):
  442. # pylint: disable=protected-access
  443. if self._cb_pool._cb_requires_init:
  444. return False
  445. return self._cb_impl.is_dirty()
  446. def is_outdated(self):
  447. # pylint: disable=protected-access
  448. if self._cb_pool._cb_requires_init:
  449. return False
  450. return self._cb_impl.is_outdated()
  451. @property
  452. def revisions(self):
  453. return self._cb_impl.revisions
  454. @property
  455. def size(self):
  456. return self._cb_impl.size
  457. @size.setter
  458. def size(self, size):
  459. self._cb_impl.size = size
  460. @property
  461. def config(self):
  462. return self._cb_impl.config
  463. def block_device(self):
  464. # pylint: disable=protected-access
  465. if self._cb_pool._cb_requires_init:
  466. # usually Volume.start() is called beforehand
  467. # --> we should be initialized in 99% of cases
  468. return None
  469. return self._cb_impl.block_device()
  470. @asyncio.coroutine
  471. def export(self):
  472. yield from self._assert_initialized()
  473. yield from self._callback('pre_volume_export')
  474. return (yield from coro_maybe(self._cb_impl.export()))
  475. @asyncio.coroutine
  476. def export_end(self, path):
  477. yield from self._assert_initialized()
  478. ret = yield from coro_maybe(self._cb_impl.export_end(path))
  479. yield from self._callback('post_volume_export_end', cb_args=[path])
  480. return ret
  481. @asyncio.coroutine
  482. def verify(self):
  483. yield from self._assert_initialized()
  484. return (yield from coro_maybe(self._cb_impl.verify()))
  485. @asyncio.coroutine
  486. def revert(self, revision=None):
  487. yield from self._assert_initialized()
  488. return (yield from coro_maybe(self._cb_impl.revert(revision=revision)))
  489. #shadow all qubes.storage.Volume class attributes as instance properties
  490. #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
  491. @property
  492. def devtype(self):
  493. return self._cb_impl.devtype
  494. @devtype.setter
  495. def devtype(self, devtype):
  496. self._cb_impl.devtype = devtype
  497. @property
  498. def domain(self):
  499. return self._cb_impl.domain
  500. @domain.setter
  501. def domain(self, domain):
  502. self._cb_impl.domain = domain
  503. @property
  504. def path(self):
  505. return self._cb_impl.path
  506. @path.setter
  507. def path(self, path):
  508. self._cb_impl.path = path
  509. @property
  510. def script(self):
  511. return self._cb_impl.script
  512. @script.setter
  513. def script(self, script):
  514. self._cb_impl.script = script
  515. @property
  516. def usage(self):
  517. return self._cb_impl.usage
  518. @usage.setter
  519. def usage(self, usage):
  520. self._cb_impl.usage = usage
  521. #remaining method & attribute delegation
  522. def __getattr__(self, name):
  523. return getattr(self._cb_impl, name)
  524. def __setattr__(self, name, value):
  525. if name.startswith('_cb_'):
  526. super().__setattr__(name, value)
  527. else:
  528. setattr(self._cb_impl, name, value)
  529. def __delattr__(self, name):
  530. if name.startswith('_cb_'):
  531. super().__delattr__(name)
  532. else:
  533. delattr(self._cb_impl, name)