qvm_template_postprocess.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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 time
  29. import qubesadmin
  30. import qubesadmin.exc
  31. import qubesadmin.tools
  32. try:
  33. # pylint: disable=wrong-import-position
  34. import qubesadmin.events.utils
  35. have_events = True
  36. except ImportError:
  37. have_events = False
  38. parser = qubesadmin.tools.QubesArgumentParser(
  39. description='Postprocess template package')
  40. parser.add_argument('--really', action='store_true', default=False,
  41. help='Really perform the action, YOU SHOULD REALLY KNOW WHAT YOU ARE DOING')
  42. parser.add_argument('--skip-start', action='store_true',
  43. help='Do not start the VM - do not retrieve menu entries etc.')
  44. parser.add_argument('--keep-source', action='store_true',
  45. help='Do not remove imported data')
  46. parser.add_argument('action', choices=['post-install', 'pre-remove'],
  47. help='Action to perform')
  48. parser.add_argument('name', action='store',
  49. help='Template name')
  50. parser.add_argument('dir', action='store',
  51. help='Template directory')
  52. def move_if_exists(source, dest_dir):
  53. '''Move file/directory if exists'''
  54. if os.path.exists(source):
  55. shutil.move(source, os.path.join(dest_dir, os.path.basename(source)))
  56. def get_root_img_size(source_dir):
  57. '''Extract size of root.img to be imported'''
  58. root_path = os.path.join(source_dir, 'root.img')
  59. if os.path.exists(root_path + '.part.00'):
  60. # get just file root_size from the tar header
  61. p = subprocess.Popen(['tar', 'tvf', root_path + '.part.00'],
  62. stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
  63. (stdout, _) = p.communicate()
  64. # -rw-r--r-- 0/0 1073741824 1970-01-01 01:00 root.img
  65. root_size = int(stdout.split()[2])
  66. elif os.path.exists(root_path):
  67. root_size = os.path.getsize(root_path)
  68. else:
  69. raise qubesadmin.exc.QubesException('root.img not found')
  70. return root_size
  71. def import_root_img(vm, source_dir):
  72. '''Import root.img into VM object'''
  73. root_size = get_root_img_size(source_dir)
  74. vm.volumes['root'].resize(root_size)
  75. root_path = os.path.join(source_dir, 'root.img')
  76. if os.path.exists(root_path + '.part.00'):
  77. input_files = glob.glob(root_path + '.part.*')
  78. cat = subprocess.Popen(['cat'] + sorted(input_files),
  79. stdout=subprocess.PIPE)
  80. tar = subprocess.Popen(['tar', 'xSOf', '-'],
  81. stdin=cat.stdout,
  82. stdout=subprocess.PIPE)
  83. vm.volumes['root'].import_data(stream=tar.stdout)
  84. if tar.wait() != 0:
  85. raise qubesadmin.exc.QubesException('root.img extraction failed')
  86. if cat.wait() != 0:
  87. raise qubesadmin.exc.QubesException('root.img extraction failed')
  88. cat.stdout.close()
  89. tar.stdout.close()
  90. elif os.path.exists(root_path):
  91. if vm.app.qubesd_connection_type == 'socket':
  92. # check if root.img was already overwritten, i.e. if the source
  93. # and destination paths are the same
  94. vid = vm.volumes['root'].vid
  95. pool = vm.app.pools[vm.volumes['root'].pool]
  96. if pool.driver == 'file' and root_path == os.path.join(
  97. pool.config['dir_path'], vid + '.img'):
  98. vm.log.info('root.img already in place, do not re-import')
  99. return
  100. with open(root_path, 'rb') as root_file:
  101. vm.volumes['root'].import_data(stream=root_file)
  102. def import_appmenus(vm, source_dir):
  103. '''Import appmenus settings into VM object (later: GUI VM)'''
  104. if os.getuid() == 0:
  105. try:
  106. qubes_group = grp.getgrnam('qubes')
  107. user = qubes_group.gr_mem[0]
  108. cmd_prefix = ['runuser', '-u', user, '--', 'env', 'DISPLAY=:0']
  109. except KeyError as e:
  110. vm.log.warning('Default user not found, not importing appmenus: ' +
  111. str(e))
  112. return
  113. else:
  114. cmd_prefix = []
  115. # TODO: change this to qrexec calls to GUI VM, when GUI VM will be
  116. # implemented
  117. subprocess.check_call(cmd_prefix + ['qvm-appmenus',
  118. '--set-default-whitelist={}'.format(os.path.join(source_dir,
  119. 'vm-whitelisted-appmenus.list')), vm.name])
  120. subprocess.check_call(cmd_prefix + ['qvm-appmenus',
  121. '--set-whitelist={}'.format(os.path.join(source_dir,
  122. 'whitelisted-appmenus.list')), vm.name])
  123. def post_install(args):
  124. '''Handle post-installation tasks'''
  125. app = args.app
  126. try:
  127. # reinstall
  128. vm = app.domains[args.name]
  129. except KeyError:
  130. if app.qubesd_connection_type == 'socket' and \
  131. args.dir == '/var/lib/qubes/vm-templates/' + args.name:
  132. # vm.create_on_disk() need to create the directory on its own,
  133. # move it away for from its way
  134. tmp_sourcedir = os.path.join('/var/lib/qubes/vm-templates',
  135. 'tmp-' + args.name)
  136. shutil.move(args.dir, tmp_sourcedir)
  137. args.dir = tmp_sourcedir
  138. vm = app.add_new_vm('TemplateVM',
  139. name=args.name,
  140. label=qubesadmin.config.defaults['template_label'])
  141. vm.log.info('Importing data')
  142. import_root_img(vm, args.dir)
  143. import_appmenus(vm, args.dir)
  144. if not args.skip_start:
  145. # just created, so no need to save previous value - we know what it was
  146. vm.netvm = None
  147. vm.start()
  148. try:
  149. vm.run_service_for_stdio('qubes.PostInstall')
  150. except qubesadmin.exc.QubesVMError:
  151. vm.log.error('qubes.PostInstall service failed')
  152. vm.shutdown()
  153. if have_events:
  154. try:
  155. # pylint: disable=no-member
  156. qubesadmin.events.utils.wait_for_domain_shutdown(vm,
  157. qubesadmin.config.defaults['shutdown_timeout'])
  158. except qubesadmin.exc.QubesVMShutdownTimeout:
  159. vm.kill()
  160. asyncio.get_event_loop().close()
  161. else:
  162. timeout = qubesadmin.config.defaults['shutdown_timeout']
  163. while timeout >= 0:
  164. if vm.is_halted():
  165. break
  166. time.sleep(1)
  167. timeout -= 1
  168. if not vm.is_halted():
  169. vm.kill()
  170. vm.netvm = qubesadmin.DEFAULT
  171. return 0
  172. def pre_remove(args):
  173. '''Handle pre-removal tasks'''
  174. app = args.app
  175. try:
  176. tpl = app.domains[args.name]
  177. except KeyError:
  178. parser.error('Qube with this name do not exist')
  179. for appvm in tpl.appvms:
  180. parser.error('Qube {} use this template'.format(appvm.name))
  181. del app.domains[args.name]
  182. return 0
  183. def main(args=None, app=None):
  184. '''Main function of qvm-template-postprocess'''
  185. args = parser.parse_args(args, app=app)
  186. if not args.really:
  187. parser.error('Do not call this tool directly.')
  188. if args.action == 'post-install':
  189. return post_install(args)
  190. elif args.action == 'pre-remove':
  191. pre_remove(args)
  192. else:
  193. parser.error('Unknown action')
  194. return 0
  195. if __name__ == '__main__':
  196. sys.exit(main())