qvm_template_postprocess.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. #
  2. # The Qubes OS Project, https://www.qubes-os.org/
  3. #
  4. # Copyright (C) 2016 Marek Marczykowski-Górecki
  5. # <marmarek@invisiblethingslab.com>
  6. #
  7. # This program is free software; you can redistribute it and/or modify
  8. # it under the terms of the GNU Lesser General Public License as published by
  9. # the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Lesser 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. ''' Tool for importing rpm-installed template'''
  21. import asyncio
  22. import glob
  23. import os
  24. import shutil
  25. import subprocess
  26. import sys
  27. import grp
  28. import qubesadmin
  29. import qubesadmin.exc
  30. import qubesadmin.tools
  31. try:
  32. # pylint: disable=wrong-import-position
  33. import qubesadmin.events.utils
  34. have_events = True
  35. except ImportError:
  36. have_events = False
  37. parser = qubesadmin.tools.QubesArgumentParser(
  38. description='Postprocess template package')
  39. parser.add_argument('--really', action='store_true', default=False,
  40. help='Really perform the action, YOU SHOULD REALLY KNOW WHAT YOU ARE DOING')
  41. parser.add_argument('--skip-start', action='store_true',
  42. help='Do not start the VM - do not retrieve menu entries etc.')
  43. parser.add_argument('--keep-source', action='store_true',
  44. help='Do not remove source data (*dir* directory) after import')
  45. parser.add_argument('action', choices=['post-install', 'pre-remove'],
  46. help='Action to perform')
  47. parser.add_argument('name', action='store',
  48. help='Template name')
  49. parser.add_argument('dir', action='store',
  50. help='Template directory')
  51. def get_root_img_size(source_dir):
  52. '''Extract size of root.img to be imported'''
  53. root_path = os.path.join(source_dir, 'root.img')
  54. if os.path.exists(root_path + '.part.00'):
  55. # get just file root_size from the tar header
  56. p = subprocess.Popen(['tar', 'tvf', root_path + '.part.00'],
  57. stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
  58. (stdout, _) = p.communicate()
  59. # -rw-r--r-- 0/0 1073741824 1970-01-01 01:00 root.img
  60. root_size = int(stdout.split()[2])
  61. elif os.path.exists(root_path):
  62. root_size = os.path.getsize(root_path)
  63. else:
  64. raise qubesadmin.exc.QubesException('root.img not found')
  65. return root_size
  66. def import_root_img(vm, source_dir):
  67. '''Import root.img into VM object'''
  68. # Try not break existing data in the volume in case of import failure. If
  69. # volume needs to be extended, do it before import, if reduced - after.
  70. root_size = get_root_img_size(source_dir)
  71. if vm.volumes['root'].size < root_size:
  72. vm.volumes['root'].resize(root_size)
  73. root_path = os.path.join(source_dir, 'root.img')
  74. if os.path.exists(root_path + '.part.00'):
  75. input_files = glob.glob(root_path + '.part.*')
  76. cat = subprocess.Popen(['cat'] + sorted(input_files),
  77. stdout=subprocess.PIPE)
  78. tar = subprocess.Popen(['tar', 'xSOf', '-'],
  79. stdin=cat.stdout,
  80. stdout=subprocess.PIPE)
  81. cat.stdout.close()
  82. vm.volumes['root'].import_data(stream=tar.stdout)
  83. if tar.wait() != 0:
  84. raise qubesadmin.exc.QubesException('root.img extraction failed')
  85. if cat.wait() != 0:
  86. raise qubesadmin.exc.QubesException('root.img extraction failed')
  87. elif os.path.exists(root_path):
  88. if vm.app.qubesd_connection_type == 'socket':
  89. # check if root.img was already overwritten, i.e. if the source
  90. # and destination paths are the same
  91. vid = vm.volumes['root'].vid
  92. pool = vm.app.pools[vm.volumes['root'].pool]
  93. if (pool.driver in ('file', 'file-reflink')
  94. and root_path == os.path.join(pool.config['dir_path'],
  95. vid + '.img')):
  96. vm.log.info('root.img already in place, do not re-import')
  97. return
  98. with open(root_path, 'rb') as root_file:
  99. vm.volumes['root'].import_data(stream=root_file)
  100. if vm.volumes['root'].size > root_size:
  101. try:
  102. vm.volumes['root'].resize(root_size)
  103. except qubesadmin.exc.QubesException as err:
  104. vm.log.warning(
  105. 'Failed to resize root volume of {} from {} to {} after '
  106. 'import: {}'.format(vm.name, vm.volumes['root'].size,
  107. root_size, str(err)))
  108. def reset_private_img(vm):
  109. '''Clear private volume'''
  110. with open('/dev/null', 'rb') as null:
  111. vm.volumes['private'].import_data(stream=null)
  112. def import_appmenus(vm, source_dir):
  113. '''Import appmenus settings into VM object (later: GUI VM)'''
  114. if os.getuid() == 0:
  115. try:
  116. qubes_group = grp.getgrnam('qubes')
  117. user = qubes_group.gr_mem[0]
  118. cmd_prefix = ['runuser', '-u', user, '--', 'env', 'DISPLAY=:0']
  119. except KeyError as e:
  120. vm.log.warning('Default user not found, not importing appmenus: ' +
  121. str(e))
  122. return
  123. else:
  124. cmd_prefix = []
  125. # TODO: change this to qrexec calls to GUI VM, when GUI VM will be
  126. # implemented
  127. try:
  128. subprocess.check_call(cmd_prefix + ['qvm-appmenus',
  129. '--set-default-whitelist={}'.format(os.path.join(source_dir,
  130. 'vm-whitelisted-appmenus.list')), vm.name])
  131. subprocess.check_call(cmd_prefix + ['qvm-appmenus',
  132. '--set-whitelist={}'.format(os.path.join(source_dir,
  133. 'whitelisted-appmenus.list')), vm.name])
  134. except subprocess.CalledProcessError as e:
  135. vm.log.warning('Failed to set default application list: %s', e)
  136. @asyncio.coroutine
  137. def call_postinstall_service(vm):
  138. '''Call qubes.PostInstall service
  139. And adjust related settings (netvm, features).
  140. '''
  141. # just created, so no need to save previous value - we know what it was
  142. vm.netvm = None
  143. # temporarily enable qrexec feature - so vm.start() will wait for it;
  144. # if start fails, rollback it
  145. vm.features['qrexec'] = True
  146. try:
  147. vm.start()
  148. except qubesadmin.exc.QubesException:
  149. del vm.features['qrexec']
  150. else:
  151. try:
  152. vm.run_service_for_stdio('qubes.PostInstall')
  153. except subprocess.CalledProcessError:
  154. vm.log.error('qubes.PostInstall service failed')
  155. vm.shutdown()
  156. if have_events:
  157. try:
  158. # pylint: disable=no-member
  159. yield from asyncio.wait_for(
  160. qubesadmin.events.utils.wait_for_domain_shutdown([vm]),
  161. qubesadmin.config.defaults['shutdown_timeout'])
  162. except asyncio.TimeoutError:
  163. vm.kill()
  164. else:
  165. timeout = qubesadmin.config.defaults['shutdown_timeout']
  166. while timeout >= 0:
  167. if vm.is_halted():
  168. break
  169. yield from asyncio.sleep(1)
  170. timeout -= 1
  171. if not vm.is_halted():
  172. try:
  173. vm.kill()
  174. except qubesadmin.exc.QubesVMNotStartedError:
  175. pass
  176. finally:
  177. vm.netvm = qubesadmin.DEFAULT
  178. @asyncio.coroutine
  179. def post_install(args):
  180. '''Handle post-installation tasks'''
  181. app = args.app
  182. vm_created = False
  183. # reinstall and running in dom0, using the same directory as qubes core
  184. local_reinstall = False
  185. try:
  186. # reinstall
  187. vm = app.domains[args.name]
  188. if app.qubesd_connection_type == 'socket' and \
  189. args.dir == '/var/lib/qubes/vm-templates/' + args.name:
  190. # VM exists and use use the same directory as target vm - on
  191. # final cleanup remove only some files, not the whole directory
  192. local_reinstall = True
  193. except KeyError:
  194. if app.qubesd_connection_type == 'socket' and \
  195. args.dir == '/var/lib/qubes/vm-templates/' + args.name:
  196. # vm.create_on_disk() need to create the directory on its own,
  197. # move it away from its way
  198. tmp_sourcedir = os.path.join('/var/lib/qubes/vm-templates',
  199. 'tmp-' + args.name)
  200. shutil.move(args.dir, tmp_sourcedir)
  201. args.dir = tmp_sourcedir
  202. vm = app.add_new_vm('TemplateVM',
  203. name=args.name,
  204. label=qubesadmin.config.defaults['template_label'])
  205. vm_created = True
  206. vm.log.info('Importing data')
  207. try:
  208. import_root_img(vm, args.dir)
  209. except:
  210. # if data import fails, remove half-created VM
  211. if vm_created:
  212. del app.domains[vm.name]
  213. raise
  214. if not vm_created:
  215. vm.log.info('Clearing private volume')
  216. reset_private_img(vm)
  217. vm.installed_by_rpm = True
  218. import_appmenus(vm, args.dir)
  219. if not args.skip_start:
  220. yield from call_postinstall_service(vm)
  221. if not args.keep_source:
  222. if local_reinstall:
  223. # remove only imported root img
  224. root_path = os.path.join(args.dir, 'root.img')
  225. for root_part in glob.glob(root_path + '.part.*'):
  226. os.unlink(root_part)
  227. else:
  228. shutil.rmtree(args.dir)
  229. # if running as root, tell underlying storage layer about just freed
  230. # data blocks
  231. if os.getuid() == 0:
  232. subprocess.call(['sync', '-f', os.path.dirname(args.dir)])
  233. subprocess.call(['fstrim', os.path.dirname(args.dir)])
  234. return 0
  235. def pre_remove(args):
  236. '''Handle pre-removal tasks'''
  237. app = args.app
  238. try:
  239. tpl = app.domains[args.name]
  240. except KeyError:
  241. parser.error('No Qube with this name exists')
  242. for appvm in tpl.appvms:
  243. parser.error('Qube {} uses this template'.format(appvm.name))
  244. tpl.installed_by_rpm = False
  245. del app.domains[args.name]
  246. return 0
  247. def is_chroot():
  248. '''Detect if running inside chroot'''
  249. try:
  250. stat_root = os.stat('/')
  251. stat_init_root = os.stat('/proc/1/root/.')
  252. return (
  253. stat_root.st_dev != stat_init_root.st_dev or
  254. stat_root.st_ino != stat_init_root.st_ino)
  255. except IOError:
  256. print('Stat failed, assuming not chroot', file=sys.stderr)
  257. return False
  258. def main(args=None, app=None):
  259. '''Main function of qvm-template-postprocess'''
  260. args = parser.parse_args(args, app=app)
  261. if is_chroot():
  262. print('Running in chroot, ignoring request. Import template with:',
  263. file=sys.stderr)
  264. print(' '.join(sys.argv), file=sys.stderr)
  265. return
  266. if not args.really:
  267. parser.error('Do not call this tool directly.')
  268. if args.action == 'post-install':
  269. loop = asyncio.get_event_loop()
  270. try:
  271. loop.run_until_complete(post_install(args))
  272. loop.stop()
  273. loop.run_forever()
  274. finally:
  275. loop.close()
  276. elif args.action == 'pre-remove':
  277. pre_remove(args)
  278. else:
  279. parser.error('Unknown action')
  280. return 0
  281. if __name__ == '__main__':
  282. sys.exit(main())