qvm_template_postprocess.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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 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. import argparse
  21. import glob
  22. import os
  23. import shutil
  24. import subprocess
  25. import sys
  26. import grp
  27. import qubes
  28. import qubes.tools
  29. parser = qubes.tools.QubesArgumentParser(
  30. description='Postprocess template package, for internal use only')
  31. parser.add_argument('--really', action='store_true', default=False,
  32. help=argparse.SUPPRESS)
  33. parser.add_argument('action', choices=['post-install', 'pre-remove'],
  34. help='Action to perform')
  35. parser.add_argument('name', action='store',
  36. help='Template name')
  37. parser.add_argument('dir', action='store',
  38. help='Template directory')
  39. def move_if_exists(source, dest_dir):
  40. if os.path.exists(source):
  41. shutil.move(source, os.path.join(dest_dir, os.path.basename(source)))
  42. def import_data(source_dir, vm):
  43. # FIXME: this abuses volume export() for importing purposes
  44. root_path = os.path.join(source_dir, 'root.img')
  45. target_path = vm.storage.export('root')
  46. if os.path.exists(root_path + '.part.00'):
  47. input_files = glob.glob(root_path + '.*')
  48. cat = subprocess.Popen(['cat'] + sorted(input_files),
  49. stdout=subprocess.PIPE)
  50. tar = subprocess.Popen(['tar', 'xSOf', '-'],
  51. stdin=cat.stdout,
  52. stdout=open(target_path, 'w'))
  53. if tar.wait() != 0:
  54. raise qubes.exc.QubesException('root.img extraction failed')
  55. if cat.wait() != 0:
  56. raise qubes.exc.QubesException('root.img extraction failed')
  57. elif os.path.exists(root_path):
  58. subprocess.check_call(
  59. ['dd', 'if='+root_path, 'of='+target_path, 'conv=sparse']
  60. )
  61. move_if_exists(os.path.join(source_dir, 'whitelisted-appmenus.list'),
  62. vm.dir_path)
  63. move_if_exists(os.path.join(source_dir, 'vm-whitelisted-appmenus.list'),
  64. vm.dir_path)
  65. move_if_exists(os.path.join(source_dir, 'netvm-whitelisted-appmenus.list'),
  66. vm.dir_path)
  67. shutil.rmtree(source_dir)
  68. def post_install(args):
  69. root_path = os.path.join(args.dir, 'root.img')
  70. if os.path.exists(root_path + '.part.00'):
  71. # get just file root_size from the tar header
  72. p = subprocess.Popen(['tar', 'tvf', root_path + '.part.00'],
  73. stdout=subprocess.PIPE, stderr=open(os.devnull, 'w'))
  74. (stdout, _) = p.communicate()
  75. # -rw-r--r-- 0/0 1073741824 1970-01-01 01:00 root.img
  76. root_size = int(stdout.split()[2])
  77. elif os.path.exists(root_path):
  78. root_size = os.path.getsize(root_path)
  79. else:
  80. raise qubes.exc.QubesException('root.img not found')
  81. volume_config = {'root': {'size': root_size}}
  82. # TODO: add lock=True
  83. app = args.app
  84. reinstall = False
  85. try:
  86. # reinstall
  87. vm = app.domains[args.name]
  88. reinstall = True
  89. except KeyError:
  90. vm = app.add_new_vm(qubes.vm.templatevm.TemplateVM,
  91. name=args.name,
  92. label=qubes.config.defaults['template_label'],
  93. volume_config=volume_config)
  94. # vm.create_on_disk() need to create the directory on its own, move it
  95. # away for from its way
  96. if vm.dir_path == args.dir:
  97. shutil.move(args.dir,
  98. os.path.join(qubes.config.qubes_base_dir, 'tmp-' + args.name))
  99. args.dir = os.path.join(qubes.config.qubes_base_dir, 'tmp-' + args.name)
  100. if reinstall:
  101. vm.remove_from_disk()
  102. vm.create_on_disk()
  103. vm.log.info('Importing data')
  104. import_data(args.dir, vm)
  105. app.save()
  106. if os.getuid() == 0:
  107. # fix permissions, do it only here (not after starting the VM),
  108. # because we're running as root only at early installation phase,
  109. # when offline mode is enabled anyway - otherwise main() would switch
  110. # to non-root user
  111. try:
  112. qubes_group = grp.getgrnam('qubes')
  113. for dirpath, _, filenames in os.walk(vm.dir_path):
  114. os.chown(dirpath, -1, qubes_group.gr_gid)
  115. os.chmod(dirpath, 0o2775)
  116. for name in filenames:
  117. filename = os.path.join(dirpath, name)
  118. os.chown(filename, -1, qubes_group.gr_gid)
  119. os.chmod(filename, 0o664)
  120. except KeyError:
  121. raise qubes.exc.QubesException('\'qubes\' group missing')
  122. if not app.vmm.offline_mode:
  123. # just created, so no need to save previous value - we know what it was
  124. vm.netvm = None
  125. vm.start(start_guid=False)
  126. vm.fire_event('template-postinstall')
  127. vm.shutdown(wait=True)
  128. vm.netvm = qubes.property.DEFAULT
  129. return 0
  130. def pre_remove(args):
  131. # TODO: add lock=True
  132. app = args.app
  133. try:
  134. tpl = app.domains[args.name]
  135. except KeyError:
  136. parser.error('Qube with this name do not exist')
  137. return 1
  138. for appvm in app.domains:
  139. if hasattr(appvm, 'template') and appvm.template == tpl:
  140. parser.error('Qube {} use this template'.format(appvm.name))
  141. return 1
  142. del app.domains[args.name]
  143. tpl.remove_from_disk()
  144. app.save()
  145. return 0
  146. def main(args=None):
  147. if os.getuid() == 0:
  148. try:
  149. qubes_group = grp.getgrnam('qubes')
  150. prefix_cmd = ['runuser', '-u', qubes_group.gr_mem[0], '--']
  151. os.execvp('runuser', prefix_cmd + sys.argv)
  152. except (KeyError, IndexError):
  153. # When group or user do not exist yet, continue as root. This
  154. # probably also means we're still in installer, so some actions
  155. # will not be taken anyway (because of running in chroot ->
  156. # offline mode).
  157. pass
  158. args = parser.parse_args(args)
  159. if not args.really:
  160. parser.error('Do not call this tool directly.')
  161. return 1
  162. if args.action == 'post-install':
  163. return post_install(args)
  164. elif args.action == 'pre-remove':
  165. pre_remove(args)
  166. else:
  167. parser.error('Unknown action')
  168. return 1
  169. return 0
  170. if __name__ == '__main__':
  171. sys.exit(main())