qubes_lvm.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. #!/usr/bin/python2
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2016 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
  6. #
  7. # This program is free software; you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation; either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License along
  18. # with this program; if not, write to the Free Software Foundation, Inc.,
  19. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  20. #
  21. ''' Manage pools and volumes managed by the 'lvm_thin' driver. '''
  22. from __future__ import print_function
  23. import datetime
  24. import logging
  25. import os
  26. import subprocess
  27. import sys
  28. import time
  29. import lvm # pylint: disable=import-error
  30. import qubes
  31. log = logging.getLogger('qubes.storage.lvm')
  32. def lvm_image_changed(vm):
  33. ''' Returns true if source image changed '''
  34. # TODO: reimplement lvm_image_changed
  35. vm_root = vm.root_img
  36. tp_root = vm.template.root_img
  37. if not os.path.exists(vm_root):
  38. return False
  39. cmd = 'date +"%%s" -d "' + \
  40. '`sudo tune2fs %s -l|grep "Last write time"|cut -d":" -f2,3,4`"'
  41. result1 = subprocess.check_output(cmd % vm_root, shell=True).strip()
  42. result2 = subprocess.check_output(cmd % tp_root, shell=True).strip()
  43. result1 = datetime.datetime.strptime(result1, '%c')
  44. result2 = datetime.datetime.strptime(result2, '%c')
  45. return result2 > result1
  46. def pool_exists(args):
  47. """ Check if given name is an lvm thin volume. """
  48. # TODO Implement a faster and proper working version pool_exists
  49. vg_name, thin_pool_name = args.pool_id.split('/', 1)
  50. volume_group = lvm.vgOpen(vg_name)
  51. for p in volume_group.listLVs():
  52. if p.getAttr()[0] == 't' and p.getName() == thin_pool_name:
  53. volume_group.close()
  54. return True
  55. volume_group.close()
  56. return False
  57. def thin_volume_exists(volume):
  58. """ Check if the given volume exists and is a thin volume """
  59. log.debug("Checking if the %s thin volume exists", volume)
  60. assert volume is not None
  61. cmd = ['sudo', 'lvs', '-o', 'lv_modules', '--rows', volume]
  62. try:
  63. output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
  64. log.debug(output)
  65. # Just because the above command succeded it does not mean that we
  66. # really have a volume managed by a thin pool. It could be just any
  67. # volume. Below we check that the volume uses the thin-pool module.
  68. if "thin-pool,thin" in output:
  69. return True
  70. except subprocess.CalledProcessError:
  71. return False
  72. def remove_volume(args):
  73. """ Tries to remove the specified logical volume.
  74. If the removal fails it will try up to 3 times waiting 1, 2 and 3
  75. seconds between tries. Most of the time this function fails if some
  76. process still has the volume locked.
  77. """
  78. img = args.name
  79. if not thin_volume_exists(img):
  80. log.info("Expected to remove %s, but volume does not exist", img)
  81. return
  82. tries = 1
  83. successful = False
  84. cmd = ['sudo', 'lvremove', '-f', img]
  85. while tries <= 3 and not successful:
  86. log.info("Trying to remove LVM %s", img)
  87. try:
  88. output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
  89. log.debug(output)
  90. successful = True
  91. except subprocess.CalledProcessError:
  92. successful = False
  93. if successful:
  94. break
  95. else:
  96. time.sleep(tries)
  97. tries += 1
  98. if not successful:
  99. log.error('Could not remove volume ' + img)
  100. def clone_volume(args):
  101. """ Calls lvcreate and creates new snapshot. """
  102. old = args.source
  103. new_name = args.destination
  104. cmd = ["sudo", "lvcreate", "-kn", "-ay", "-s", old, "-n", new_name]
  105. return subprocess.call(cmd)
  106. def new_volume(args):
  107. ''' Creates a new volume in the specified thin pool, formated with ext4 '''
  108. thin_pool = args.pool_id
  109. name = args.name
  110. size = args.size
  111. log.info('Creating new Thin LVM %s in %s VG %s bytes', name, thin_pool,
  112. size)
  113. cmd = ['sudo', 'lvcreate', '-T', thin_pool, '-kn', '-ay', '-n', name, '-V',
  114. str(size) + 'B']
  115. return subprocess.call(cmd)
  116. def rename_volume(old_name, new_name):
  117. ''' Rename volume '''
  118. log.debug("Renaming LVM %s to %s ", old_name, new_name)
  119. retcode = subprocess.call(["sudo", "lvrename", old_name, new_name])
  120. if retcode != 0:
  121. raise IOError("Error renaming LVM %s to %s " % (old_name, new_name))
  122. return new_name
  123. def init_pool_parser(sub_parsers):
  124. ''' Initialize pool subparser '''
  125. pool_parser = sub_parsers.add_parser(
  126. 'pool', aliases=('p', 'pl'),
  127. help="Exit with exit code 0 if pool exists")
  128. pool_parser.add_argument('pool_id', metavar='VG/POOL',
  129. help="volume_group/pool_name")
  130. pool_parser.set_defaults(func=pool_exists)
  131. def init_new_parser(sub_parsers):
  132. ''' Initialize the 'new' subparser '''
  133. new_parser = sub_parsers.add_parser(
  134. 'new', aliases=('n', 'create'),
  135. help='Creates a new thin ThinPoolLogicalVolume')
  136. new_parser.add_argument('pool_id', metavar='VG/POOL',
  137. help="volume_group/pool_name")
  138. new_parser.add_argument('name',
  139. help='name of the new ThinPoolLogicalVolume')
  140. new_parser.add_argument(
  141. 'size', help='size in bytes of the new ThinPoolLogicalVolume')
  142. new_parser.set_defaults(func=new_volume)
  143. def init_import_parser(sub_parsers):
  144. ''' Initialize import subparser '''
  145. import_parser = sub_parsers.add_parser(
  146. 'import', aliases=('imp', 'i'),
  147. help='sparse copy data from stdin to a thin volume')
  148. import_parser.add_argument('name', metavar='VG/VID',
  149. help='volume_group/volume_name')
  150. import_parser.set_defaults(func=import_volume)
  151. def init_clone_parser(sub_parsers):
  152. ''' Initialize clone subparser '''
  153. clone_parser = sub_parsers.add_parser(
  154. 'clone', aliases=('cln', 'c'),
  155. help='sparse copy data from stdin to a thin volume')
  156. clone_parser.add_argument('source', metavar='VG/VID',
  157. help='volume_group/volume_name')
  158. clone_parser.add_argument('destination', metavar='VG/VID',
  159. help='volume_group/volume_name')
  160. clone_parser.set_defaults(func=clone_volume)
  161. def import_volume(args):
  162. ''' Imports from stdin to a thin volume '''
  163. name = args.name
  164. src = sys.stdin
  165. blk_size = 4096
  166. zeros = '\x00' * blk_size
  167. dst_path = '/dev/%s' % name
  168. with open(dst_path, 'wb') as dst:
  169. while True:
  170. tmp = src.read(blk_size)
  171. if not tmp:
  172. break
  173. elif tmp == zeros:
  174. dst.seek(blk_size, 1)
  175. else:
  176. dst.write(tmp)
  177. def list_volumes(args):
  178. ''' lists volumes '''
  179. vg_name, _ = args.name.split('/')
  180. volume_group = lvm.vgOpen(vg_name)
  181. for p in volume_group.listLVs():
  182. if p.getAttr()[0] == 'V':
  183. print(vg_name + "/" + p.getName() + ' ' + p.getAttr())
  184. volume_group.close()
  185. def init_volumes_parser(sub_parsers):
  186. ''' Initialize volumes subparser '''
  187. parser = sub_parsers.add_parser('volumes', aliases=('v', 'vol'),
  188. help='list volumes in a pool')
  189. parser.add_argument('name', metavar='VG/THIN_POOL',
  190. help='volume_group/thin_pool_name')
  191. parser.set_defaults(func=list_volumes)
  192. def init_remove_parser(sub_parsers):
  193. ''' Initialize remove subparser '''
  194. remove_parser = sub_parsers.add_parser('remove', aliases=('rm', 'r'),
  195. help='Removes a LogicalVolume')
  196. remove_parser.add_argument('name', metavar='VG/VID',
  197. help='volume_group/volume_name')
  198. remove_parser.set_defaults(func=remove_volume)
  199. def get_parser():
  200. '''Create :py:class:`argparse.ArgumentParser` suitable for
  201. :program:`qubes-lvm`.
  202. '''
  203. parser = qubes.tools.QubesArgumentParser(description=__doc__, want_app=True)
  204. parser.register('action', 'parsers', qubes.tools.AliasedSubParsersAction)
  205. sub_parsers = parser.add_subparsers(
  206. title='commands',
  207. description="For more information see qubes-lvm command -h",
  208. dest='command')
  209. init_pool_parser(sub_parsers)
  210. init_import_parser(sub_parsers)
  211. init_new_parser(sub_parsers)
  212. init_volumes_parser(sub_parsers)
  213. init_remove_parser(sub_parsers)
  214. init_clone_parser(sub_parsers)
  215. return parser
  216. def main(args=None):
  217. '''Main routine of :program:`qubes-lvm`.'''
  218. args = get_parser().parse_args(args)
  219. return args.func(args)
  220. if __name__ == '__main__':
  221. sys.exit(main())