lvm.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  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 program is free software; you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation; either version 2 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program 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
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License along
  17. # with this program; if not, write to the Free Software Foundation, Inc.,
  18. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  19. #
  20. ''' Driver for storing vm images in a LVM thin pool '''
  21. import logging
  22. import operator
  23. import os
  24. import subprocess
  25. import time
  26. import asyncio
  27. import qubes
  28. import qubes.storage
  29. import qubes.utils
  30. def check_lvm_version():
  31. #Check if lvm is very very old, like in Travis-CI
  32. try:
  33. lvm_help = subprocess.check_output(['lvm', 'lvcreate', '--help'],
  34. stderr=subprocess.DEVNULL).decode()
  35. return '--setactivationskip' not in lvm_help
  36. except (subprocess.CalledProcessError, FileNotFoundError):
  37. pass
  38. lvm_is_very_old = check_lvm_version()
  39. class ThinPool(qubes.storage.Pool):
  40. ''' LVM Thin based pool implementation
  41. ''' # pylint: disable=protected-access
  42. size_cache = None
  43. driver = 'lvm_thin'
  44. def __init__(self, volume_group, thin_pool, revisions_to_keep=1, **kwargs):
  45. super(ThinPool, self).__init__(revisions_to_keep=revisions_to_keep,
  46. **kwargs)
  47. self.volume_group = volume_group
  48. self.thin_pool = thin_pool
  49. self._pool_id = "{!s}/{!s}".format(volume_group, thin_pool)
  50. self.log = logging.getLogger('qube.storage.lvm.%s' % self._pool_id)
  51. self._volume_objects_cache = {}
  52. @property
  53. def config(self):
  54. return {
  55. 'name': self.name,
  56. 'volume_group': self.volume_group,
  57. 'thin_pool': self.thin_pool,
  58. 'driver': ThinPool.driver
  59. }
  60. def destroy(self):
  61. pass # TODO Should we remove an existing pool?
  62. def init_volume(self, vm, volume_config):
  63. ''' Initialize a :py:class:`qubes.storage.Volume` from `volume_config`.
  64. '''
  65. if 'vid' not in volume_config.keys():
  66. if vm and hasattr(vm, 'name'):
  67. vm_name = vm.name
  68. else:
  69. # for the future if we have volumes not belonging to a vm
  70. vm_name = qubes.utils.random_string()
  71. assert self.name
  72. volume_config['vid'] = "{!s}/vm-{!s}-{!s}".format(
  73. self.volume_group, vm_name, volume_config['name'])
  74. volume_config['volume_group'] = self.volume_group
  75. volume_config['pool'] = self
  76. volume = ThinVolume(**volume_config)
  77. self._volume_objects_cache[volume_config['vid']] = volume
  78. return volume
  79. def setup(self):
  80. pass # TODO Should we create a non existing pool?
  81. def get_volume(self, vid):
  82. ''' Return a volume with given vid'''
  83. if vid in self._volume_objects_cache:
  84. return self._volume_objects_cache[vid]
  85. config = {
  86. 'pool': self,
  87. 'vid': vid,
  88. 'name': vid,
  89. 'volume_group': self.volume_group,
  90. }
  91. # don't cache this object, as it doesn't carry full configuration
  92. return ThinVolume(**config)
  93. def list_volumes(self):
  94. ''' Return a list of volumes managed by this pool '''
  95. volumes = []
  96. for vid, vol_info in size_cache.items():
  97. if not vid.startswith(self.volume_group + '/'):
  98. continue
  99. if vol_info['pool_lv'] != self.thin_pool:
  100. continue
  101. if vid.endswith('-snap'):
  102. # implementation detail volume
  103. continue
  104. if vid.endswith('-back'):
  105. # old revisions
  106. continue
  107. config = {
  108. 'pool': self,
  109. 'vid': vid,
  110. 'name': vid,
  111. 'volume_group': self.volume_group,
  112. 'rw': vol_info['attr'][1] == 'w',
  113. }
  114. volumes += [ThinVolume(**config)]
  115. return volumes
  116. def init_cache(log=logging.getLogger('qubes.storage.lvm')):
  117. cmd = ['lvs', '--noheadings', '-o',
  118. 'vg_name,pool_lv,name,lv_size,data_percent,lv_attr,origin',
  119. '--units', 'b', '--separator', ';']
  120. if os.getuid() != 0:
  121. cmd.insert(0, 'sudo')
  122. p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  123. close_fds=True)
  124. out, err = p.communicate()
  125. return_code = p.returncode
  126. if return_code == 0 and err:
  127. log.warning(err)
  128. elif return_code != 0:
  129. raise qubes.storage.StoragePoolException(err)
  130. result = {}
  131. for line in out.splitlines():
  132. line = line.decode().strip()
  133. pool_name, pool_lv, name, size, usage_percent, attr, \
  134. origin = line.split(';', 6)
  135. if '' in [pool_name, pool_lv, name, size, usage_percent]:
  136. continue
  137. name = pool_name + "/" + name
  138. size = int(size[:-1]) # Remove 'B' suffix
  139. usage = int(size / 100 * float(usage_percent))
  140. result[name] = {'size': size, 'usage': usage, 'pool_lv': pool_lv,
  141. 'attr': attr, 'origin': origin}
  142. return result
  143. size_cache = init_cache()
  144. class ThinVolume(qubes.storage.Volume):
  145. ''' Default LVM thin volume implementation
  146. ''' # pylint: disable=too-few-public-methods
  147. def __init__(self, volume_group, size=0, **kwargs):
  148. self.volume_group = volume_group
  149. super(ThinVolume, self).__init__(size=size, **kwargs)
  150. self.log = logging.getLogger('qube.storage.lvm.%s' % str(self.pool))
  151. if self.snap_on_start or self.save_on_stop:
  152. self._vid_snap = self.vid + '-snap'
  153. self._size = size
  154. @property
  155. def path(self):
  156. return '/dev/' + self.vid
  157. @property
  158. def revisions(self):
  159. name_prefix = self.vid + '-'
  160. revisions = {}
  161. for revision_vid in size_cache:
  162. if not revision_vid.startswith(name_prefix):
  163. continue
  164. if not revision_vid.endswith('-back'):
  165. continue
  166. revision_vid = revision_vid[len(name_prefix):]
  167. seconds = int(revision_vid[:-len('-back')])
  168. iso_date = qubes.storage.isodate(seconds).split('.', 1)[0]
  169. revisions[revision_vid] = iso_date
  170. return revisions
  171. @property
  172. def size(self):
  173. try:
  174. return qubes.storage.lvm.size_cache[self.vid]['size']
  175. except KeyError:
  176. return self._size
  177. @size.setter
  178. def size(self, _):
  179. raise qubes.storage.StoragePoolException(
  180. "You shouldn't use lvm size setter")
  181. def _reset(self):
  182. ''' Resets a volatile volume '''
  183. assert not self.snap_on_start and not self.save_on_stop, \
  184. "Not a volatile volume"
  185. self.log.debug('Resetting volatile ' + self.vid)
  186. try:
  187. cmd = ['remove', self.vid]
  188. qubes_lvm(cmd, self.log)
  189. except qubes.storage.StoragePoolException:
  190. pass
  191. # pylint: disable=protected-access
  192. cmd = ['create', self.pool._pool_id, self.vid.split('/')[1],
  193. str(self.size)]
  194. qubes_lvm(cmd, self.log)
  195. def _remove_revisions(self, revisions=None):
  196. '''Remove old volume revisions.
  197. If no revisions list is given, it removes old revisions according to
  198. :py:attr:`revisions_to_keep`
  199. :param revisions: list of revisions to remove
  200. '''
  201. if revisions is None:
  202. revisions = sorted(self.revisions.items(),
  203. key=operator.itemgetter(1))
  204. revisions = revisions[:-self.revisions_to_keep]
  205. revisions = [rev_id for rev_id, _ in revisions]
  206. for rev_id in revisions:
  207. try:
  208. cmd = ['remove', self.vid + rev_id]
  209. qubes_lvm(cmd, self.log)
  210. except qubes.storage.StoragePoolException:
  211. pass
  212. def _commit(self):
  213. msg = "Trying to commit {!s}, but it has save_on_stop == False"
  214. msg = msg.format(self)
  215. assert self.save_on_stop, msg
  216. msg = "Trying to commit {!s}, but it has rw == False"
  217. msg = msg.format(self)
  218. assert self.rw, msg
  219. assert hasattr(self, '_vid_snap')
  220. if self.revisions_to_keep > 0:
  221. cmd = ['clone', self.vid,
  222. '{}-{}-back'.format(self.vid, int(time.time()))]
  223. qubes_lvm(cmd, self.log)
  224. self._remove_revisions()
  225. cmd = ['remove', self.vid]
  226. qubes_lvm(cmd, self.log)
  227. cmd = ['clone', self._vid_snap, self.vid]
  228. qubes_lvm(cmd, self.log)
  229. def create(self):
  230. assert self.vid
  231. assert self.size
  232. if self.save_on_stop:
  233. if self.source:
  234. cmd = ['clone', str(self.source), self.vid]
  235. else:
  236. cmd = [
  237. 'create',
  238. self.pool._pool_id, # pylint: disable=protected-access
  239. self.vid.split('/', 1)[1],
  240. str(self.size)
  241. ]
  242. qubes_lvm(cmd, self.log)
  243. reset_cache()
  244. return self
  245. def remove(self):
  246. assert self.vid
  247. if self.is_dirty():
  248. cmd = ['remove', self._vid_snap]
  249. qubes_lvm(cmd, self.log)
  250. self._remove_revisions(self.revisions.keys())
  251. if not os.path.exists(self.path):
  252. return
  253. cmd = ['remove', self.vid]
  254. qubes_lvm(cmd, self.log)
  255. reset_cache()
  256. # pylint: disable=protected-access
  257. self.pool._volume_objects_cache.pop(self.vid, None)
  258. def export(self):
  259. ''' Returns an object that can be `open()`. '''
  260. # make sure the device node is available
  261. qubes_lvm(['activate', self.vid], self.log)
  262. devpath = '/dev/' + self.vid
  263. return devpath
  264. @asyncio.coroutine
  265. def import_volume(self, src_volume):
  266. if not src_volume.save_on_stop:
  267. return self
  268. # HACK: neat trick to speed up testing if you have same physical thin
  269. # pool assigned to two qubes-pools i.e: qubes_dom0 and test-lvm
  270. # pylint: disable=line-too-long
  271. if isinstance(src_volume.pool, ThinPool) and \
  272. src_volume.pool.thin_pool == self.pool.thin_pool: # NOQA
  273. cmd = ['remove', self.vid]
  274. qubes_lvm(cmd, self.log)
  275. cmd = ['clone', str(src_volume), str(self)]
  276. qubes_lvm(cmd, self.log)
  277. else:
  278. src_path = src_volume.export()
  279. cmd = ['dd', 'if=' + src_path, 'of=/dev/' + self.vid,
  280. 'conv=sparse']
  281. p = yield from asyncio.create_subprocess_exec(*cmd)
  282. yield from p.wait()
  283. if p.returncode != 0:
  284. raise qubes.storage.StoragePoolException(
  285. 'Failed to import volume {!r}, dd exit code: {}'.format(
  286. src_volume, p.returncode))
  287. reset_cache()
  288. return self
  289. def import_data(self):
  290. ''' Returns an object that can be `open()`. '''
  291. devpath = '/dev/' + self.vid
  292. return devpath
  293. def is_dirty(self):
  294. if self.save_on_stop:
  295. return os.path.exists('/dev/' + self._vid_snap)
  296. return False
  297. def is_outdated(self):
  298. if not self.snap_on_start:
  299. return False
  300. if self._vid_snap not in size_cache:
  301. return False
  302. return (size_cache[self._vid_snap]['origin'] !=
  303. self.source.vid.split('/')[1])
  304. def revert(self, revision=None):
  305. if revision is None:
  306. revision = \
  307. max(self.revisions.items(), key=operator.itemgetter(1))[0]
  308. old_path = self.path + '-' + revision
  309. if not os.path.exists(old_path):
  310. msg = "Volume {!s} has no {!s}".format(self, old_path)
  311. raise qubes.storage.StoragePoolException(msg)
  312. cmd = ['remove', self.vid]
  313. qubes_lvm(cmd, self.log)
  314. cmd = ['clone', self.vid + '-' + revision, self.vid]
  315. qubes_lvm(cmd, self.log)
  316. reset_cache()
  317. return self
  318. def resize(self, size):
  319. ''' Expands volume, throws
  320. :py:class:`qubst.storage.qubes.storage.StoragePoolException` if
  321. given size is less than current_size
  322. '''
  323. if not self.rw:
  324. msg = 'Can not resize reađonly volume {!s}'.format(self)
  325. raise qubes.storage.StoragePoolException(msg)
  326. if size < self.size:
  327. raise qubes.storage.StoragePoolException(
  328. 'For your own safety, shrinking of %s is'
  329. ' disabled. If you really know what you'
  330. ' are doing, use `lvresize` on %s manually.' %
  331. (self.name, self.vid))
  332. cmd = ['extend', self.vid, str(size)]
  333. qubes_lvm(cmd, self.log)
  334. if self.is_dirty():
  335. cmd = ['extend', self._vid_snap, str(size)]
  336. qubes_lvm(cmd, self.log)
  337. reset_cache()
  338. def _snapshot(self):
  339. try:
  340. cmd = ['remove', self._vid_snap]
  341. qubes_lvm(cmd, self.log)
  342. except: # pylint: disable=bare-except
  343. pass
  344. if self.source is None:
  345. cmd = ['clone', self.vid, self._vid_snap]
  346. else:
  347. cmd = ['clone', str(self.source), self._vid_snap]
  348. qubes_lvm(cmd, self.log)
  349. def start(self):
  350. if self.snap_on_start or self.save_on_stop:
  351. if not self.save_on_stop or not self.is_dirty():
  352. self._snapshot()
  353. else:
  354. self._reset()
  355. reset_cache()
  356. return self
  357. def stop(self):
  358. if self.save_on_stop:
  359. self._commit()
  360. if self.snap_on_start or self.save_on_stop:
  361. cmd = ['remove', self._vid_snap]
  362. qubes_lvm(cmd, self.log)
  363. else:
  364. cmd = ['remove', self.vid]
  365. qubes_lvm(cmd, self.log)
  366. reset_cache()
  367. return self
  368. def verify(self):
  369. ''' Verifies the volume. '''
  370. try:
  371. vol_info = size_cache[self.vid]
  372. return vol_info['attr'][4] == 'a'
  373. except KeyError:
  374. return False
  375. def block_device(self):
  376. ''' Return :py:class:`qubes.storage.BlockDevice` for serialization in
  377. the libvirt XML template as <disk>.
  378. '''
  379. if self.snap_on_start or self.save_on_stop:
  380. return qubes.storage.BlockDevice(
  381. '/dev/' + self._vid_snap, self.name, self.script,
  382. self.rw, self.domain, self.devtype)
  383. return super(ThinVolume, self).block_device()
  384. @property
  385. def usage(self): # lvm thin usage always returns at least the same usage as
  386. # the parent
  387. try:
  388. return qubes.storage.lvm.size_cache[self.vid]['usage']
  389. except KeyError:
  390. return 0
  391. def pool_exists(pool_id):
  392. ''' Return true if pool exists '''
  393. try:
  394. vol_info = size_cache[pool_id]
  395. return vol_info['attr'][0] == 't'
  396. except KeyError:
  397. return False
  398. def qubes_lvm(cmd, log=logging.getLogger('qubes.storage.lvm')):
  399. ''' Call :program:`lvm` to execute an LVM operation '''
  400. action = cmd[0]
  401. if action == 'remove':
  402. lvm_cmd = ['lvremove', '-f', cmd[1]]
  403. elif action == 'clone':
  404. lvm_cmd = ['lvcreate', '-kn', '-ay', '-s', cmd[1], '-n', cmd[2]]
  405. elif action == 'create':
  406. lvm_cmd = ['lvcreate', '-T', cmd[1], '-kn', '-ay', '-n', cmd[2], '-V',
  407. str(cmd[3]) + 'B']
  408. elif action == 'extend':
  409. size = int(cmd[2]) / (1000 * 1000)
  410. lvm_cmd = ["lvextend", "-L%s" % size, cmd[1]]
  411. elif action == 'activate':
  412. lvm_cmd = ['lvchange', '-ay', cmd[1]]
  413. else:
  414. raise NotImplementedError('unsupported action: ' + action)
  415. if lvm_is_very_old:
  416. # old lvm in trusty image used there does not support -k option
  417. lvm_cmd = [x for x in lvm_cmd if x != '-kn']
  418. if os.getuid() != 0:
  419. cmd = ['sudo', 'lvm'] + lvm_cmd
  420. else:
  421. cmd = ['lvm'] + lvm_cmd
  422. p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  423. close_fds=True)
  424. out, err = p.communicate()
  425. return_code = p.returncode
  426. if out:
  427. log.debug(out)
  428. if return_code == 0 and err:
  429. log.warning(err)
  430. elif return_code != 0:
  431. assert err, "Command exited unsuccessful, but printed nothing to stderr"
  432. raise qubes.storage.StoragePoolException(err)
  433. return True
  434. def reset_cache():
  435. qubes.storage.lvm.size_cache = init_cache()