lvm.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863
  1. #
  2. # The Qubes OS Project, http://www.qubes-os.org
  3. #
  4. # Copyright (C) 2016 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.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. ''' Driver for storing vm images in a LVM thin pool '''
  20. import functools
  21. import logging
  22. import os
  23. import subprocess
  24. import time
  25. import asyncio
  26. import qubes
  27. import qubes.storage
  28. import qubes.utils
  29. def check_lvm_version():
  30. #Check if lvm is very very old, like in Travis-CI
  31. try:
  32. lvm_help = subprocess.check_output(['lvm', 'lvcreate', '--help'],
  33. stderr=subprocess.DEVNULL).decode()
  34. return '--setactivationskip' not in lvm_help
  35. except (subprocess.CalledProcessError, FileNotFoundError):
  36. pass
  37. lvm_is_very_old = check_lvm_version()
  38. class ThinPool(qubes.storage.Pool):
  39. ''' LVM Thin based pool implementation
  40. Volumes are stored as LVM thin volumes, in thin pool specified by
  41. *volume_group*/*thin_pool* arguments. LVM volume naming scheme:
  42. vm-{vm_name}-{volume_name}[-suffix]
  43. Where suffix can be one of:
  44. "-snap" - snapshot for currently running VM, at VM shutdown will be
  45. either discarded (if save_on_stop=False), or committed
  46. (if save_on_stop=True)
  47. "-{revision_id}" - volume revision - new revision is automatically
  48. created at each VM shutdown, *revisions_to_keep* control how many
  49. old revisions (in addition to the current one) should be stored
  50. "" (no suffix) - the most recent committed volume state; also volatile
  51. volume (snap_on_start=False, save_on_stop=False)
  52. On VM startup, new volume is created, depending on volume type,
  53. according to the table below:
  54. snap_on_start, save_on_stop
  55. False, False, - no suffix, fresh empty volume
  56. False, True, - "-snap", snapshot of last committed revision
  57. True , False, - "-snap", snapshot of last committed revision
  58. of source volume (from VM's template)
  59. True, True, - unsupported configuration
  60. Volume's revision_id format is "{timestamp}-back", where timestamp is in
  61. '%s' format (seconds since unix epoch)
  62. ''' # pylint: disable=protected-access
  63. size_cache = None
  64. driver = 'lvm_thin'
  65. def __init__(self, *, name, revisions_to_keep=1, volume_group, thin_pool):
  66. super().__init__(name=name, revisions_to_keep=revisions_to_keep)
  67. self.volume_group = volume_group
  68. self.thin_pool = thin_pool
  69. self._pool_id = "{!s}/{!s}".format(volume_group, thin_pool)
  70. self.log = logging.getLogger('qubes.storage.lvm.%s' % self._pool_id)
  71. self._volume_objects_cache = {}
  72. def __repr__(self):
  73. return '<{} at {:#x} name={!r} volume_group={!r} thin_pool={!r}>'.\
  74. format(
  75. type(self).__name__, id(self),
  76. self.name, self.volume_group, self.thin_pool)
  77. @property
  78. def config(self):
  79. return {
  80. 'name': self.name,
  81. 'volume_group': self.volume_group,
  82. 'thin_pool': self.thin_pool,
  83. 'driver': ThinPool.driver,
  84. 'revisions_to_keep': self.revisions_to_keep,
  85. }
  86. def destroy(self):
  87. pass # TODO Should we remove an existing pool?
  88. def init_volume(self, vm, volume_config):
  89. ''' Initialize a :py:class:`qubes.storage.Volume` from `volume_config`.
  90. '''
  91. if 'revisions_to_keep' not in volume_config.keys():
  92. volume_config['revisions_to_keep'] = self.revisions_to_keep
  93. if 'vid' not in volume_config.keys():
  94. if vm and hasattr(vm, 'name'):
  95. vm_name = vm.name
  96. else:
  97. # for the future if we have volumes not belonging to a vm
  98. vm_name = qubes.utils.random_string()
  99. assert self.name
  100. volume_config['vid'] = "{!s}/vm-{!s}-{!s}".format(
  101. self.volume_group, vm_name, volume_config['name'])
  102. volume_config['volume_group'] = self.volume_group
  103. volume_config['pool'] = self
  104. volume = ThinVolume(**volume_config)
  105. self._volume_objects_cache[volume_config['vid']] = volume
  106. return volume
  107. def setup(self):
  108. reset_cache()
  109. cache_key = self.volume_group + '/' + self.thin_pool
  110. if cache_key not in size_cache:
  111. raise qubes.storage.StoragePoolException(
  112. 'Thin pool {} does not exist'.format(cache_key))
  113. if size_cache[cache_key]['attr'][0] != 't':
  114. raise qubes.storage.StoragePoolException(
  115. 'Volume {} is not a thin pool'.format(cache_key))
  116. # TODO Should we create a non existing pool?
  117. def get_volume(self, vid):
  118. ''' Return a volume with given vid'''
  119. if vid in self._volume_objects_cache:
  120. return self._volume_objects_cache[vid]
  121. config = {
  122. 'pool': self,
  123. 'vid': vid,
  124. 'name': vid,
  125. 'volume_group': self.volume_group,
  126. }
  127. # don't cache this object, as it doesn't carry full configuration
  128. return ThinVolume(**config)
  129. def list_volumes(self):
  130. ''' Return a list of volumes managed by this pool '''
  131. volumes = []
  132. for vid, vol_info in size_cache.items():
  133. if not vid.startswith(self.volume_group + '/'):
  134. continue
  135. if vol_info['pool_lv'] != self.thin_pool:
  136. continue
  137. if vid.endswith('-snap') or vid.endswith('-import'):
  138. # implementation detail volume
  139. continue
  140. if vid.endswith('-back'):
  141. # old revisions
  142. continue
  143. volume = self.get_volume(vid)
  144. if volume in volumes:
  145. continue
  146. volumes.append(volume)
  147. return volumes
  148. @property
  149. def size(self):
  150. try:
  151. return qubes.storage.lvm.size_cache[
  152. self.volume_group + '/' + self.thin_pool]['size']
  153. except KeyError:
  154. return 0
  155. @property
  156. def usage(self):
  157. refresh_cache()
  158. try:
  159. return qubes.storage.lvm.size_cache[
  160. self.volume_group + '/' + self.thin_pool]['usage']
  161. except KeyError:
  162. return 0
  163. @property
  164. def usage_details(self):
  165. result = {}
  166. result['data_size'] = self.size
  167. result['data_usage'] = self.usage
  168. try:
  169. metadata_size = qubes.storage.lvm.size_cache[
  170. self.volume_group + '/' + self.thin_pool]['metadata_size']
  171. metadata_usage = qubes.storage.lvm.size_cache[
  172. self.volume_group + '/' + self.thin_pool]['metadata_usage']
  173. except KeyError:
  174. metadata_size = 0
  175. metadata_usage = 0
  176. result['metadata_size'] = metadata_size
  177. result['metadata_usage'] = metadata_usage
  178. return result
  179. _init_cache_cmd = ['lvs', '--noheadings', '-o',
  180. 'vg_name,pool_lv,name,lv_size,data_percent,lv_attr,origin,lv_metadata_size,'
  181. 'metadata_percent', '--units', 'b', '--separator', ';']
  182. def _parse_lvm_cache(lvm_output):
  183. result = {}
  184. for line in lvm_output.splitlines():
  185. line = line.decode().strip()
  186. pool_name, pool_lv, name, size, usage_percent, attr, \
  187. origin, metadata_size, metadata_percent = line.split(';', 8)
  188. if '' in [pool_name, name, size, usage_percent]:
  189. continue
  190. name = pool_name + "/" + name
  191. size = int(size[:-1]) # Remove 'B' suffix
  192. usage = int(size / 100 * float(usage_percent))
  193. if metadata_size:
  194. metadata_size = int(metadata_size[:-1])
  195. metadata_usage = int(metadata_size / 100 * float(metadata_percent))
  196. else:
  197. metadata_usage = None
  198. result[name] = {'size': size, 'usage': usage, 'pool_lv': pool_lv,
  199. 'attr': attr, 'origin': origin, 'metadata_size': metadata_size,
  200. 'metadata_usage': metadata_usage}
  201. return result
  202. def init_cache(log=logging.getLogger('qubes.storage.lvm')):
  203. cmd = _init_cache_cmd
  204. if os.getuid() != 0:
  205. cmd = ['sudo'] + cmd
  206. environ = os.environ.copy()
  207. environ['LC_ALL'] = 'C.utf8'
  208. p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  209. close_fds=True, env=environ)
  210. out, err = p.communicate()
  211. return_code = p.returncode
  212. if return_code == 0 and err:
  213. log.warning(err)
  214. elif return_code != 0:
  215. raise qubes.storage.StoragePoolException(err)
  216. return _parse_lvm_cache(out)
  217. @asyncio.coroutine
  218. def init_cache_coro(log=logging.getLogger('qubes.storage.lvm')):
  219. cmd = _init_cache_cmd
  220. if os.getuid() != 0:
  221. cmd = ['sudo'] + cmd
  222. environ = os.environ.copy()
  223. environ['LC_ALL'] = 'C.utf8'
  224. p = yield from asyncio.create_subprocess_exec(*cmd,
  225. stdout=subprocess.PIPE,
  226. stderr=subprocess.PIPE,
  227. close_fds=True, env=environ)
  228. out, err = yield from p.communicate()
  229. return_code = p.returncode
  230. if return_code == 0 and err:
  231. log.warning(err)
  232. elif return_code != 0:
  233. raise qubes.storage.StoragePoolException(err)
  234. return _parse_lvm_cache(out)
  235. size_cache_time = 0
  236. size_cache = init_cache()
  237. def _revision_sort_key(revision):
  238. '''Sort key for revisions. Sort them by time
  239. :returns timestamp
  240. '''
  241. if isinstance(revision, tuple):
  242. revision = revision[0]
  243. if '-' in revision:
  244. revision = revision.split('-')[0]
  245. return int(revision)
  246. def locked(method):
  247. '''Decorator running given Volume's coroutine under a lock.
  248. Needs to be added after wrapping with @asyncio.coroutine, for example:
  249. >>>@locked
  250. >>>@asyncio.coroutine
  251. >>>def start(self):
  252. >>> pass
  253. '''
  254. @asyncio.coroutine
  255. @functools.wraps(method)
  256. def wrapper(self, *args, **kwargs):
  257. with (yield from self._lock): # pylint: disable=protected-access
  258. return (yield from method(self, *args, **kwargs))
  259. return wrapper
  260. class ThinVolume(qubes.storage.Volume):
  261. ''' Default LVM thin volume implementation
  262. ''' # pylint: disable=too-few-public-methods
  263. def __init__(self, volume_group, **kwargs):
  264. self.volume_group = volume_group
  265. super().__init__(**kwargs)
  266. self.log = logging.getLogger('qubes.storage.lvm.%s' % str(self.pool))
  267. if self.snap_on_start or self.save_on_stop:
  268. self._vid_snap = self.vid + '-snap'
  269. if self.save_on_stop:
  270. self._vid_import = self.vid + '-import'
  271. self._lock = asyncio.Lock()
  272. @property
  273. def path(self):
  274. return '/dev/' + self._vid_current
  275. @property
  276. def _vid_current(self):
  277. if self.vid in size_cache:
  278. return self.vid
  279. vol_revisions = self.revisions
  280. if vol_revisions:
  281. last_revision = \
  282. max(vol_revisions.items(), key=_revision_sort_key)[0]
  283. return self.vid + '-' + last_revision
  284. # detached pool? return expected path
  285. return self.vid
  286. @property
  287. def revisions(self):
  288. name_prefix = self.vid + '-'
  289. revisions = {}
  290. for revision_vid in size_cache:
  291. if not revision_vid.startswith(name_prefix):
  292. continue
  293. if not revision_vid.endswith('-back'):
  294. continue
  295. revision_vid = revision_vid[len(name_prefix):]
  296. if revision_vid.count('-') > 1:
  297. # VM+volume name is a prefix of another VM, see #4680
  298. continue
  299. # get revision without suffix
  300. seconds = int(revision_vid.split('-')[0])
  301. iso_date = qubes.storage.isodate(seconds).split('.', 1)[0]
  302. revisions[revision_vid] = iso_date
  303. return revisions
  304. @property
  305. def size(self):
  306. try:
  307. if self.is_dirty():
  308. return qubes.storage.lvm.size_cache[self._vid_snap]['size']
  309. return qubes.storage.lvm.size_cache[self._vid_current]['size']
  310. except KeyError:
  311. return self._size
  312. @size.setter
  313. def size(self, _):
  314. raise qubes.storage.StoragePoolException(
  315. "You shouldn't use lvm size setter")
  316. @asyncio.coroutine
  317. def _reset(self):
  318. ''' Resets a volatile volume '''
  319. assert not self.snap_on_start and not self.save_on_stop, \
  320. "Not a volatile volume"
  321. self.log.debug('Resetting volatile %s', self.vid)
  322. try:
  323. cmd = ['remove', self.vid]
  324. yield from qubes_lvm_coro(cmd, self.log)
  325. except qubes.storage.StoragePoolException:
  326. pass
  327. # pylint: disable=protected-access
  328. cmd = ['create', self.pool._pool_id, self.vid.split('/')[1],
  329. str(self.size)]
  330. yield from qubes_lvm_coro(cmd, self.log)
  331. @asyncio.coroutine
  332. def _remove_revisions(self, revisions=None):
  333. '''Remove old volume revisions.
  334. If no revisions list is given, it removes old revisions according to
  335. :py:attr:`revisions_to_keep`
  336. :param revisions: list of revisions to remove
  337. '''
  338. if revisions is None:
  339. revisions = sorted(self.revisions.items(),
  340. key=_revision_sort_key)
  341. # pylint: disable=invalid-unary-operand-type
  342. revisions = revisions[:(-self.revisions_to_keep) or None]
  343. revisions = [rev_id for rev_id, _ in revisions]
  344. for rev_id in revisions:
  345. # safety check
  346. assert rev_id != self._vid_current
  347. try:
  348. cmd = ['remove', self.vid + '-' + rev_id]
  349. yield from qubes_lvm_coro(cmd, self.log)
  350. except qubes.storage.StoragePoolException:
  351. pass
  352. @asyncio.coroutine
  353. def _commit(self, vid_to_commit=None, keep=False):
  354. '''
  355. Commit temporary volume into current one. By default
  356. :py:attr:`_vid_snap` is used (which is created by :py:meth:`start()`),
  357. but can be overriden by *vid_to_commit* argument.
  358. :param vid_to_commit: LVM volume ID to commit into this one
  359. :param keep: whether to keep or not *vid_to_commit*.
  360. IOW use 'clone' or 'rename' methods.
  361. :return: None
  362. '''
  363. msg = "Trying to commit {!s}, but it has save_on_stop == False"
  364. msg = msg.format(self)
  365. assert self.save_on_stop, msg
  366. msg = "Trying to commit {!s}, but it has rw == False"
  367. msg = msg.format(self)
  368. assert self.rw, msg
  369. if vid_to_commit is None:
  370. assert hasattr(self, '_vid_snap')
  371. vid_to_commit = self._vid_snap
  372. assert self._lock.locked()
  373. if not os.path.exists('/dev/' + vid_to_commit):
  374. # nothing to commit
  375. return
  376. if self._vid_current == self.vid:
  377. cmd = ['rename', self.vid,
  378. '{}-{}-back'.format(self.vid, int(time.time()))]
  379. yield from qubes_lvm_coro(cmd, self.log)
  380. yield from reset_cache_coro()
  381. cmd = ['clone' if keep else 'rename',
  382. vid_to_commit,
  383. self.vid]
  384. yield from qubes_lvm_coro(cmd, self.log)
  385. yield from reset_cache_coro()
  386. # make sure the one we've committed right now is properly
  387. # detected as the current one - before removing anything
  388. assert self._vid_current == self.vid
  389. # and remove old snapshots, if needed
  390. yield from self._remove_revisions()
  391. @locked
  392. @asyncio.coroutine
  393. def create(self):
  394. assert self.vid
  395. assert self.size
  396. if self.save_on_stop:
  397. if self.source:
  398. cmd = ['clone', self.source.path, self.vid]
  399. else:
  400. cmd = [
  401. 'create',
  402. self.pool._pool_id, # pylint: disable=protected-access
  403. self.vid.split('/', 1)[1],
  404. str(self.size)
  405. ]
  406. yield from qubes_lvm_coro(cmd, self.log)
  407. yield from reset_cache_coro()
  408. return self
  409. @locked
  410. @asyncio.coroutine
  411. def remove(self):
  412. assert self.vid
  413. try:
  414. if os.path.exists('/dev/' + self._vid_snap):
  415. cmd = ['remove', self._vid_snap]
  416. yield from qubes_lvm_coro(cmd, self.log)
  417. except AttributeError:
  418. pass
  419. try:
  420. if os.path.exists('/dev/' + self._vid_import):
  421. cmd = ['remove', self._vid_import]
  422. yield from qubes_lvm_coro(cmd, self.log)
  423. except AttributeError:
  424. pass
  425. yield from self._remove_revisions(self.revisions.keys())
  426. if not os.path.exists(self.path):
  427. return
  428. cmd = ['remove', self.path]
  429. yield from qubes_lvm_coro(cmd, self.log)
  430. yield from reset_cache_coro()
  431. # pylint: disable=protected-access
  432. self.pool._volume_objects_cache.pop(self.vid, None)
  433. def export(self):
  434. ''' Returns an object that can be `open()`. '''
  435. # make sure the device node is available
  436. qubes_lvm(['activate', self.path], self.log)
  437. devpath = self.path
  438. return devpath
  439. @locked
  440. @asyncio.coroutine
  441. def import_volume(self, src_volume):
  442. if not src_volume.save_on_stop:
  443. return self
  444. if self.is_dirty():
  445. raise qubes.storage.StoragePoolException(
  446. 'Cannot import to dirty volume {} -'
  447. ' start and stop a qube to cleanup'.format(self.vid))
  448. self.abort_if_import_in_progress()
  449. # HACK: neat trick to speed up testing if you have same physical thin
  450. # pool assigned to two qubes-pools i.e: qubes_dom0 and test-lvm
  451. # pylint: disable=line-too-long
  452. if isinstance(src_volume.pool, ThinPool) and \
  453. src_volume.pool.thin_pool == self.pool.thin_pool: # NOQA
  454. yield from self._commit(src_volume.path[len('/dev/'):], keep=True)
  455. else:
  456. cmd = ['create',
  457. self.pool._pool_id, # pylint: disable=protected-access
  458. self._vid_import.split('/')[1],
  459. str(src_volume.size)]
  460. yield from qubes_lvm_coro(cmd, self.log)
  461. src_path = src_volume.export()
  462. cmd = ['dd', 'if=' + src_path, 'of=/dev/' + self._vid_import,
  463. 'conv=sparse', 'status=none', 'bs=128K']
  464. if not os.access('/dev/' + self._vid_import, os.W_OK) or \
  465. not os.access(src_path, os.R_OK):
  466. cmd.insert(0, 'sudo')
  467. p = yield from asyncio.create_subprocess_exec(*cmd)
  468. yield from p.wait()
  469. if p.returncode != 0:
  470. cmd = ['remove', self._vid_import]
  471. yield from qubes_lvm_coro(cmd, self.log)
  472. raise qubes.storage.StoragePoolException(
  473. 'Failed to import volume {!r}, dd exit code: {}'.format(
  474. src_volume, p.returncode))
  475. yield from self._commit(self._vid_import)
  476. return self
  477. @locked
  478. @asyncio.coroutine
  479. def import_data(self, size):
  480. ''' Returns an object that can be `open()`. '''
  481. if self.is_dirty():
  482. raise qubes.storage.StoragePoolException(
  483. 'Cannot import data to dirty volume {}, stop the qube first'.
  484. format(self.vid))
  485. self.abort_if_import_in_progress()
  486. # pylint: disable=protected-access
  487. cmd = ['create', self.pool._pool_id, self._vid_import.split('/')[1],
  488. str(size)]
  489. yield from qubes_lvm_coro(cmd, self.log)
  490. yield from reset_cache_coro()
  491. devpath = '/dev/' + self._vid_import
  492. return devpath
  493. @locked
  494. @asyncio.coroutine
  495. def import_data_end(self, success):
  496. '''Either commit imported data, or discard temporary volume'''
  497. if not os.path.exists('/dev/' + self._vid_import):
  498. raise qubes.storage.StoragePoolException(
  499. 'No import operation in progress on {}'.format(self.vid))
  500. if success:
  501. yield from self._commit(self._vid_import)
  502. else:
  503. cmd = ['remove', self._vid_import]
  504. yield from qubes_lvm_coro(cmd, self.log)
  505. def abort_if_import_in_progress(self):
  506. try:
  507. devpath = '/dev/' + self._vid_import
  508. if os.path.exists(devpath):
  509. raise qubes.storage.StoragePoolException(
  510. 'Import operation in progress on {}'.format(self.vid))
  511. except AttributeError: # self._vid_import
  512. # no vid_import - import definitely not in progress
  513. pass
  514. def is_dirty(self):
  515. if self.save_on_stop:
  516. return os.path.exists('/dev/' + self._vid_snap)
  517. return False
  518. def is_outdated(self):
  519. if not self.snap_on_start:
  520. return False
  521. if self._vid_snap not in size_cache:
  522. return False
  523. return (size_cache[self._vid_snap]['origin'] !=
  524. self.source.path.split('/')[-1])
  525. @locked
  526. @asyncio.coroutine
  527. def revert(self, revision=None):
  528. if self.is_dirty():
  529. raise qubes.storage.StoragePoolException(
  530. 'Cannot revert dirty volume {}, stop the qube first'.format(
  531. self.vid))
  532. self.abort_if_import_in_progress()
  533. if revision is None:
  534. revision = \
  535. max(self.revisions.items(), key=_revision_sort_key)[0]
  536. old_path = '/dev/' + self.vid + '-' + revision
  537. if not os.path.exists(old_path):
  538. msg = "Volume {!s} has no {!s}".format(self, old_path)
  539. raise qubes.storage.StoragePoolException(msg)
  540. if self.vid in size_cache:
  541. cmd = ['remove', self.vid]
  542. yield from qubes_lvm_coro(cmd, self.log)
  543. cmd = ['clone', self.vid + '-' + revision, self.vid]
  544. yield from qubes_lvm_coro(cmd, self.log)
  545. yield from reset_cache_coro()
  546. return self
  547. @locked
  548. @asyncio.coroutine
  549. def resize(self, size):
  550. ''' Expands volume, throws
  551. :py:class:`qubst.storage.qubes.storage.StoragePoolException` if
  552. given size is less than current_size
  553. '''
  554. if not self.rw:
  555. msg = 'Can not resize reađonly volume {!s}'.format(self)
  556. raise qubes.storage.StoragePoolException(msg)
  557. if size < self.size:
  558. raise qubes.storage.StoragePoolException(
  559. 'For your own safety, shrinking of %s is'
  560. ' disabled (%d < %d). If you really know what you'
  561. ' are doing, use `lvresize` on %s manually.' %
  562. (self.name, size, self.size, self.vid))
  563. if size == self.size:
  564. return
  565. if self.is_dirty():
  566. cmd = ['extend', self._vid_snap, str(size)]
  567. yield from qubes_lvm_coro(cmd, self.log)
  568. elif hasattr(self, '_vid_import') and \
  569. os.path.exists('/dev/' + self._vid_import):
  570. cmd = ['extend', self._vid_import, str(size)]
  571. yield from qubes_lvm_coro(cmd, self.log)
  572. elif self.save_on_stop and not self.snap_on_start:
  573. cmd = ['extend', self._vid_current, str(size)]
  574. yield from qubes_lvm_coro(cmd, self.log)
  575. self._size = size
  576. yield from reset_cache_coro()
  577. @asyncio.coroutine
  578. def _snapshot(self):
  579. try:
  580. cmd = ['remove', self._vid_snap]
  581. yield from qubes_lvm_coro(cmd, self.log)
  582. except: # pylint: disable=bare-except
  583. pass
  584. if self.source is None:
  585. cmd = ['clone', self._vid_current, self._vid_snap]
  586. else:
  587. cmd = ['clone', self.source.path, self._vid_snap]
  588. yield from qubes_lvm_coro(cmd, self.log)
  589. @locked
  590. @asyncio.coroutine
  591. def start(self):
  592. self.abort_if_import_in_progress()
  593. try:
  594. if self.snap_on_start or self.save_on_stop:
  595. if not self.save_on_stop or not self.is_dirty():
  596. yield from self._snapshot()
  597. else:
  598. yield from self._reset()
  599. finally:
  600. yield from reset_cache_coro()
  601. return self
  602. @locked
  603. @asyncio.coroutine
  604. def stop(self):
  605. try:
  606. if self.save_on_stop:
  607. yield from self._commit()
  608. if self.snap_on_start and not self.save_on_stop:
  609. cmd = ['remove', self._vid_snap]
  610. yield from qubes_lvm_coro(cmd, self.log)
  611. elif not self.snap_on_start and not self.save_on_stop:
  612. cmd = ['remove', self.vid]
  613. yield from qubes_lvm_coro(cmd, self.log)
  614. finally:
  615. yield from reset_cache_coro()
  616. return self
  617. def verify(self):
  618. ''' Verifies the volume. '''
  619. if not self.save_on_stop and not self.snap_on_start:
  620. # volatile volumes don't need any files
  621. return True
  622. if self.source is not None:
  623. vid = self.source.path[len('/dev/'):]
  624. else:
  625. vid = self._vid_current
  626. try:
  627. vol_info = size_cache[vid]
  628. if vol_info['attr'][4] != 'a':
  629. raise qubes.storage.StoragePoolException(
  630. 'volume {} not active'.format(vid))
  631. except KeyError:
  632. raise qubes.storage.StoragePoolException(
  633. 'volume {} missing'.format(vid))
  634. return True
  635. def block_device(self):
  636. ''' Return :py:class:`qubes.storage.BlockDevice` for serialization in
  637. the libvirt XML template as <disk>.
  638. '''
  639. if self.snap_on_start or self.save_on_stop:
  640. return qubes.storage.BlockDevice(
  641. '/dev/' + self._vid_snap, self.name, self.script,
  642. self.rw, self.domain, self.devtype)
  643. return super().block_device()
  644. @property
  645. def usage(self): # lvm thin usage always returns at least the same usage as
  646. # the parent
  647. refresh_cache()
  648. try:
  649. return qubes.storage.lvm.size_cache[self._vid_current]['usage']
  650. except KeyError:
  651. return 0
  652. def pool_exists(pool_id):
  653. ''' Return true if pool exists '''
  654. try:
  655. vol_info = size_cache[pool_id]
  656. return vol_info['attr'][0] == 't'
  657. except KeyError:
  658. return False
  659. def _get_lvm_cmdline(cmd):
  660. ''' Build command line for :program:`lvm` call.
  661. The purpose of this function is to keep all the detailed lvm options in
  662. one place.
  663. :param cmd: array of str, where cmd[0] is action and the rest are arguments
  664. :return array of str appropriate for subprocess.Popen
  665. '''
  666. action = cmd[0]
  667. if action == 'remove':
  668. lvm_cmd = ['lvremove', '-f', cmd[1]]
  669. elif action == 'clone':
  670. lvm_cmd = ['lvcreate', '-kn', '-ay', '-s', cmd[1], '-n', cmd[2]]
  671. elif action == 'create':
  672. lvm_cmd = ['lvcreate', '-T', cmd[1], '-kn', '-ay', '-n', cmd[2], '-V',
  673. str(cmd[3]) + 'B']
  674. elif action == 'extend':
  675. size = int(cmd[2]) / (1024 * 1024)
  676. lvm_cmd = ["lvextend", "-L%s" % size, cmd[1]]
  677. elif action == 'activate':
  678. lvm_cmd = ['lvchange', '-ay', cmd[1]]
  679. elif action == 'rename':
  680. lvm_cmd = ['lvrename', cmd[1], cmd[2]]
  681. else:
  682. raise NotImplementedError('unsupported action: ' + action)
  683. if lvm_is_very_old:
  684. # old lvm in trusty image used there does not support -k option
  685. lvm_cmd = [x for x in lvm_cmd if x != '-kn']
  686. if os.getuid() != 0:
  687. cmd = ['sudo', 'lvm'] + lvm_cmd
  688. else:
  689. cmd = ['lvm'] + lvm_cmd
  690. return cmd
  691. def _process_lvm_output(returncode, stdout, stderr, log):
  692. '''Process output of LVM, determine if the call was successful and
  693. possibly log warnings.'''
  694. # Filter out warning about intended over-provisioning.
  695. # Upstream discussion about missing option to silence it:
  696. # https://bugzilla.redhat.com/1347008
  697. err = '\n'.join(line for line in stderr.decode().splitlines()
  698. if 'exceeds the size of thin pool' not in line)
  699. if stdout:
  700. log.debug(stdout)
  701. if returncode == 0 and err:
  702. log.warning(err)
  703. elif returncode != 0:
  704. assert err, "Command exited unsuccessful, but printed nothing to stderr"
  705. err = err.replace('%', '%%')
  706. raise qubes.storage.StoragePoolException(err)
  707. return True
  708. def qubes_lvm(cmd, log=logging.getLogger('qubes.storage.lvm')):
  709. ''' Call :program:`lvm` to execute an LVM operation '''
  710. # the only caller for this non-coroutine version is ThinVolume.export()
  711. cmd = _get_lvm_cmdline(cmd)
  712. environ = os.environ.copy()
  713. environ['LC_ALL'] = 'C.utf8'
  714. p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  715. close_fds=True, env=environ)
  716. out, err = p.communicate()
  717. return _process_lvm_output(p.returncode, out, err, log)
  718. @asyncio.coroutine
  719. def qubes_lvm_coro(cmd, log=logging.getLogger('qubes.storage.lvm')):
  720. ''' Call :program:`lvm` to execute an LVM operation
  721. Coroutine version of :py:func:`qubes_lvm`'''
  722. environ = os.environ.copy()
  723. environ['LC_ALL'] = 'C.utf8'
  724. if cmd[0] == "remove":
  725. pre_cmd = ['blkdiscard', '/dev/'+cmd[1]]
  726. p = yield from asyncio.create_subprocess_exec(*pre_cmd,
  727. stdout=subprocess.DEVNULL,
  728. stderr=subprocess.DEVNULL,
  729. close_fds=True, env=environ)
  730. _, _ = yield from p.communicate()
  731. cmd = _get_lvm_cmdline(cmd)
  732. p = yield from asyncio.create_subprocess_exec(*cmd,
  733. stdout=subprocess.PIPE,
  734. stderr=subprocess.PIPE,
  735. close_fds=True, env=environ)
  736. out, err = yield from p.communicate()
  737. return _process_lvm_output(p.returncode, out, err, log)
  738. def reset_cache():
  739. qubes.storage.lvm.size_cache = init_cache()
  740. qubes.storage.lvm.size_cache_time = time.monotonic()
  741. @asyncio.coroutine
  742. def reset_cache_coro():
  743. qubes.storage.lvm.size_cache = yield from init_cache_coro()
  744. qubes.storage.lvm.size_cache_time = time.monotonic()
  745. def refresh_cache():
  746. '''Reset size cache, if it's older than 30sec '''
  747. if size_cache_time+30 < time.monotonic():
  748. reset_cache()