lvm.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. # vim: fileencoding=utf-8
  2. # pylint: disable=abstract-method
  3. #
  4. # The Qubes OS Project, http://www.qubes-os.org
  5. #
  6. # Copyright (C) 2016 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 2 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License along
  19. # with this program; if not, write to the Free Software Foundation, Inc.,
  20. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  21. #
  22. ''' Driver for storing vm images in a LVM thin pool '''
  23. import logging
  24. import os
  25. import subprocess
  26. import qubes
  27. class ThinPool(qubes.storage.Pool):
  28. ''' LVM Thin based pool implementation
  29. ''' # pylint: disable=protected-access
  30. driver = 'lvm_thin'
  31. def __init__(self, volume_group, thin_pool, **kwargs):
  32. super(ThinPool, self).__init__(**kwargs)
  33. self.volume_group = volume_group
  34. self.thin_pool = thin_pool
  35. self._pool_id = "{!s}/{!s}".format(volume_group, thin_pool)
  36. self.log = logging.getLogger('qube.storage.lvm.%s' % self._pool_id)
  37. def backup(self, volume):
  38. msg = "Expected volume_type 'snap' got {!s}"
  39. msg = msg.format(volume.volume_type)
  40. assert volume.volume_type == 'snap', msg
  41. cmd = ['remove', volume.vid + "-back"]
  42. qubes_lvm(cmd, self.log)
  43. cmd = ['clone', volume.vid, volume.vid + "-back"]
  44. qubes_lvm(cmd, self.log)
  45. volume.backups = [volume.vid + "-back"]
  46. return volume
  47. def clone(self, source, target):
  48. cmd = ['clone', source.vid, target.vid]
  49. qubes_lvm(cmd, self.log)
  50. return target
  51. def commit(self, volume):
  52. msg = "Expected rw:True & volume_type 'snap' got {!s} & rw:{!s}"
  53. msg = msg.format(volume.volume_type, volume.rw)
  54. assert volume.volume_type == 'snap' and volume.rw, msg
  55. assert volume.path.endswith("-snap")
  56. self.backup(volume)
  57. cmd = ['remove', volume.vid]
  58. qubes_lvm(cmd, self.log)
  59. cmd = ['clone', volume.path, volume.vid]
  60. qubes_lvm(cmd, self.log)
  61. def _backup(self, volume):
  62. msg = "Expected volume_type 'snap' got {!s}"
  63. msg = msg.format(volume.volume_type)
  64. assert volume.volume_type == 'snap', msg
  65. cmd = ['remove', volume.vid + "-back"]
  66. qubes_lvm(cmd, self.log)
  67. cmd = ['clone', volume.vid, volume.vid + "-back"]
  68. qubes_lvm(cmd, self.log)
  69. volume.backups = [volume.vid + "-back"]
  70. return 'XXX'
  71. @property
  72. def config(self):
  73. return {
  74. 'name': self.name,
  75. 'volume_group': self.volume_group,
  76. 'thin_pool': self.thin_pool,
  77. 'driver': ThinPool.driver
  78. }
  79. def create(self, volume):
  80. assert volume.vid
  81. assert volume.size
  82. if volume.source:
  83. return self.clone(volume.source, volume)
  84. else:
  85. cmd = [
  86. 'create',
  87. self._pool_id,
  88. volume.vid.split('/', 1)[1],
  89. str(volume.size)
  90. ]
  91. qubes_lvm(cmd, self.log)
  92. return volume
  93. def destroy(self):
  94. pass # TODO Should we remove an existing pool?
  95. def export(self, volume):
  96. ''' Returns an object that can be `open()`. '''
  97. return '/dev/' + volume.vid
  98. def init_volume(self, vm, volume_config):
  99. ''' Initialize a :py:class:`qubes.storage.Volume` from `volume_config`.
  100. '''
  101. if 'vid' not in volume_config.keys():
  102. if vm and hasattr(vm, 'name'):
  103. vm_name = vm.name
  104. else:
  105. # for the future if we have volumes not belonging to a vm
  106. vm_name = qubes.utils.random_string()
  107. assert self.name
  108. volume_config['vid'] = "{!s}/{!s}-{!s}".format(
  109. self.volume_group, vm_name, volume_config['name'])
  110. volume_config['volume_group'] = self.volume_group
  111. return ThinVolume(**volume_config)
  112. def import_volume(self, dst_pool, dst_volume, src_pool, src_volume):
  113. src_path = src_pool.export(src_volume)
  114. # HACK: neat trick to speed up testing if you have same physical thin
  115. # pool assigned to two qubes-pools i.e: qubes_dom0 and test-lvm
  116. # pylint: disable=line-too-long
  117. if isinstance(src_pool, ThinPool) and src_pool.thin_pool == dst_pool.thin_pool: # NOQA
  118. return self.clone(src_volume, dst_volume)
  119. else:
  120. dst_volume = self.create(dst_volume)
  121. cmd = ['sudo', 'qubes-lvm', 'import', dst_volume.vid]
  122. blk_size = 4096
  123. p = subprocess.Popen(cmd, stdin=subprocess.PIPE)
  124. dst = p.stdin
  125. with open(src_path, 'rb') as src:
  126. while True:
  127. tmp = src.read(blk_size)
  128. if not tmp:
  129. break
  130. else:
  131. dst.write(tmp)
  132. p.stdin.close()
  133. p.wait()
  134. return dst_volume
  135. def is_dirty(self, volume):
  136. if volume.save_on_stop:
  137. return os.path.exists(volume.path + '-snap')
  138. return False
  139. def remove(self, volume):
  140. assert volume.vid
  141. if self.is_dirty(volume):
  142. cmd = ['remove', volume._vid_snap]
  143. qubes_lvm(cmd, self.log)
  144. cmd = ['remove', volume.vid]
  145. qubes_lvm(cmd, self.log)
  146. def rename(self, volume, old_name, new_name):
  147. ''' Called when the domain changes its name '''
  148. new_vid = "{!s}/{!s}-{!s}".format(self.volume_group, new_name,
  149. volume.name)
  150. if volume.save_on_stop:
  151. cmd = ['clone', volume.vid, new_vid]
  152. qubes_lvm(cmd, self.log)
  153. if volume.save_on_stop or volume._is_volatile:
  154. cmd = ['remove', volume.vid]
  155. qubes_lvm(cmd, self.log)
  156. volume.vid = new_vid
  157. if not volume._is_volatile:
  158. volume._vid_snap = volume.vid + '-snap'
  159. return volume
  160. def _reset(self, volume):
  161. self.remove(volume)
  162. self.create(volume)
  163. def setup(self):
  164. pass # TODO Should we create a non existing pool?gt
  165. def start(self, volume):
  166. if volume._is_snapshot:
  167. self._snapshot(volume)
  168. elif volume._is_volatile:
  169. self._reset(volume)
  170. else:
  171. if not self.is_dirty(volume):
  172. self._snapshot(volume)
  173. return volume
  174. def stop(self, volume):
  175. if volume.save_on_stop:
  176. cmd = ['remove', volume.vid]
  177. qubes_lvm(cmd, self.log)
  178. cmd = ['clone', volume._vid_snap, volume.vid]
  179. qubes_lvm(cmd, self.log)
  180. cmd = ['remove', volume._vid_snap]
  181. qubes_lvm(cmd, self.log)
  182. elif volume._is_volatile:
  183. cmd = ['remove', volume.vid]
  184. qubes_lvm(cmd, self.log)
  185. else:
  186. cmd = ['remove', volume._vid_snap]
  187. qubes_lvm(cmd, self.log)
  188. def _snapshot(self, volume):
  189. if volume.source is None:
  190. cmd = ['clone', volume.vid, volume._vid_snap]
  191. else:
  192. cmd = ['clone', str(volume.source), volume._vid_snap]
  193. qubes_lvm(cmd, self.log)
  194. def verify(self, volume):
  195. ''' Verifies the volume. '''
  196. cmd = ['sudo', 'qubes-lvm', 'volumes',
  197. self.volume_group + '/' + self.thin_pool]
  198. p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
  199. result = p.communicate()[0]
  200. for line in result.splitlines():
  201. if not line.strip():
  202. continue
  203. vid, atr = line.strip().split(' ')
  204. if vid == volume.vid:
  205. return atr[4] == 'a'
  206. return False
  207. @property
  208. def volumes(self):
  209. ''' Return a list of volumes managed by this pool '''
  210. cmd = ['sudo', 'qubes-lvm', 'volumes',
  211. self.volume_group + '/' + self.thin_pool]
  212. p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
  213. result = p.communicate()[0]
  214. volumes = []
  215. for line in result.splitlines():
  216. if not line.strip():
  217. continue
  218. vid, atr = line.strip().split(' ')
  219. config = {
  220. 'pool': self.name,
  221. 'vid': vid,
  222. 'name': vid,
  223. 'volume_group': self.volume_group,
  224. 'rw': atr[1] == 'w',
  225. }
  226. volumes += [ThinVolume(**config)]
  227. return volumes
  228. def _reset_volume(self, volume):
  229. ''' Resets a volatile volume '''
  230. assert volume.volume_type == 'volatile', \
  231. 'Expected a volatile volume, but got {!r}'.format(volume)
  232. self.log.debug('Resetting volatile ' + volume.vid)
  233. cmd = ['remove', volume.vid]
  234. qubes_lvm(cmd, self.log)
  235. cmd = ['create', self._pool_id, volume.vid.split('/')[1],
  236. str(volume.size)]
  237. qubes_lvm(cmd, self.log)
  238. class ThinVolume(qubes.storage.Volume):
  239. ''' Default LVM thin volume implementation
  240. ''' # pylint: disable=too-few-public-methods
  241. def __init__(self, volume_group, **kwargs):
  242. self.volume_group = volume_group
  243. super(ThinVolume, self).__init__(**kwargs)
  244. if self.snap_on_start and self.source is None:
  245. msg = "snap_on_start specified on {!r} but no volume source set"
  246. msg = msg.format(self.name)
  247. raise qubes.storage.StoragePoolException(msg)
  248. elif not self.snap_on_start and self.source is not None:
  249. msg = "source specified on {!r} but no snap_on_start set"
  250. msg = msg.format(self.name)
  251. raise qubes.storage.StoragePoolException(msg)
  252. self.path = '/dev/' + self.vid
  253. if not self._is_volatile:
  254. self._vid_snap = self.vid + '-snap'
  255. @property
  256. def revisions(self):
  257. return {}
  258. @property
  259. def _is_origin(self):
  260. return not self.snap_on_start and self.save_on_stop
  261. @property
  262. def _is_origin_snapshot(self):
  263. return self.snap_on_start and self.save_on_stop
  264. @property
  265. def _is_snapshot(self):
  266. return self.snap_on_start and not self.save_on_stop
  267. @property
  268. def _is_volatile(self):
  269. return not self.snap_on_start and not self.save_on_stop
  270. def pool_exists(pool_id):
  271. ''' Return true if pool exists '''
  272. cmd = ['pool', pool_id]
  273. return qubes_lvm(cmd)
  274. def qubes_lvm(cmd, log=logging.getLogger('qube.storage.lvm')):
  275. ''' Call :program:`qubes-lvm` to execute an LVM operation '''
  276. # TODO Refactor this ones the udev groups gets fixed and we don't need root
  277. # for operations on lvm devices
  278. cmd = ['sudo', 'qubes-lvm'] + cmd
  279. p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  280. out, err = p.communicate()
  281. return_code = p.returncode
  282. if out:
  283. log.info(out)
  284. if return_code == 0 and err:
  285. log.warning(err)
  286. elif return_code != 0:
  287. assert err, "Command exited unsuccessful, but printed nothing to stderr"
  288. raise qubes.storage.StoragePoolException(err)
  289. return True