qvm_start.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. # -*- encoding: utf8 -*-
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2017 Marek Marczykowski-Górecki
  6. # <marmarek@invisiblethingslab.com>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU Lesser General Public License as published by
  10. # the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public License along
  19. # with this program; if not, see <http://www.gnu.org/licenses/>.
  20. '''qvm-start - start a domain'''
  21. import argparse
  22. import string
  23. import sys
  24. import subprocess
  25. import time
  26. import qubesadmin.devices
  27. import qubesadmin.exc
  28. import qubesadmin.tools
  29. class DriveAction(argparse.Action):
  30. '''Action for argument parser that stores drive image path.'''
  31. # pylint: disable=redefined-builtin,too-few-public-methods
  32. def __init__(self,
  33. option_strings,
  34. dest='drive',
  35. prefix='cdrom:',
  36. metavar='IMAGE',
  37. required=False,
  38. help='Attach drive'):
  39. super().__init__(option_strings, dest,
  40. metavar=metavar, help=help)
  41. self.prefix = prefix
  42. def __call__(self, parser, namespace, values, option_string=None):
  43. # pylint: disable=redefined-outer-name
  44. setattr(namespace, self.dest, self.prefix + values)
  45. parser = qubesadmin.tools.QubesArgumentParser(
  46. description='start a domain', vmname_nargs='+')
  47. parser.add_argument('--skip-if-running',
  48. action='store_true', default=False,
  49. help='Do not fail if the qube is already runnning')
  50. parser_drive = parser.add_mutually_exclusive_group()
  51. parser_drive.add_argument('--drive', metavar='DRIVE',
  52. help='temporarily attach specified drive as CD/DVD or hard disk (can be'
  53. ' specified with prefix "hd:" or "cdrom:", default is cdrom)')
  54. parser_drive.add_argument('--hddisk',
  55. action=DriveAction, dest='drive', prefix='hd:',
  56. help='temporarily attach specified drive as hard disk')
  57. parser_drive.add_argument('--cdrom', metavar='IMAGE',
  58. action=DriveAction, dest='drive', prefix='cdrom:',
  59. help='temporarily attach specified drive as CD/DVD')
  60. parser_drive.add_argument('--install-windows-tools',
  61. action='store_const', dest='drive', default=False,
  62. const='cdrom:dom0:/usr/lib/qubes/qubes-windows-tools.iso',
  63. help='temporarily attach Windows tools CDROM to the domain')
  64. def get_drive_assignment(app, drive_str):
  65. ''' Prepare :py:class:`qubesadmin.devices.DeviceAssignment` object for a
  66. given drive.
  67. If running in dom0, it will also take care about creating appropriate
  68. loop device (if necessary). Otherwise, only existing block devices are
  69. supported.
  70. :param app: Qubes() instance
  71. :param drive_str: drive argument
  72. :return: DeviceAssignment matching *drive_str*
  73. '''
  74. devtype = 'cdrom'
  75. if drive_str.startswith('cdrom:'):
  76. devtype = 'cdrom'
  77. drive_str = drive_str[len('cdrom:'):]
  78. elif drive_str.startswith('hd:'):
  79. devtype = 'disk'
  80. drive_str = drive_str[len('hd:'):]
  81. try:
  82. backend_domain_name, ident = drive_str.split(':', 1)
  83. except ValueError:
  84. raise ValueError("Incorrect image name: image must be in the format "
  85. "of VMNAME:full_path, for example "
  86. "dom0:/home/user/test.iso")
  87. try:
  88. backend_domain = app.domains[backend_domain_name]
  89. except KeyError:
  90. raise qubesadmin.exc.QubesVMNotFoundError(
  91. 'No such VM: %s', backend_domain_name)
  92. if ident.startswith('/'):
  93. # it is a path - if we're running in dom0, try to call losetup to
  94. # export the device, otherwise reject
  95. if app.qubesd_connection_type == 'qrexec':
  96. raise qubesadmin.exc.QubesException(
  97. 'Existing block device identifier needed when running from '
  98. 'outside of dom0 (see qvm-block)')
  99. try:
  100. if backend_domain.klass == 'AdminVM':
  101. loop_name = subprocess.check_output(
  102. ['sudo', 'losetup', '-f', '--show', ident])
  103. loop_name = loop_name.strip()
  104. else:
  105. untrusted_loop_name, _ = backend_domain.run_with_args(
  106. 'losetup', '-f', '--show', ident,
  107. user='root')
  108. untrusted_loop_name = untrusted_loop_name.strip()
  109. allowed_chars = string.ascii_lowercase + string.digits + '/'
  110. allowed_chars = allowed_chars.encode('ascii')
  111. if not all(c in allowed_chars for c in untrusted_loop_name):
  112. raise qubesadmin.exc.QubesException(
  113. 'Invalid loop device name received from {}'.format(
  114. backend_domain.name))
  115. loop_name = untrusted_loop_name
  116. del untrusted_loop_name
  117. except subprocess.CalledProcessError:
  118. raise qubesadmin.exc.QubesException(
  119. 'Failed to setup loop device for %s', ident)
  120. assert loop_name.startswith(b'/dev/loop')
  121. ident = loop_name.decode().split('/')[2]
  122. # wait for device to appear
  123. # FIXME: convert this to waiting for event
  124. timeout = 10
  125. while isinstance(backend_domain.devices['block'][ident],
  126. qubesadmin.devices.UnknownDevice):
  127. if timeout == 0:
  128. raise qubesadmin.exc.QubesException(
  129. 'Timeout waiting for {}:{} device to appear'.format(
  130. backend_domain.name, ident))
  131. timeout -= 1
  132. time.sleep(1)
  133. options = {
  134. 'devtype': devtype,
  135. 'read-only': devtype == 'cdrom'
  136. }
  137. assignment = qubesadmin.devices.DeviceAssignment(
  138. backend_domain,
  139. ident,
  140. options=options,
  141. persistent=True)
  142. return assignment
  143. def main(args=None, app=None):
  144. '''Main routine of :program:`qvm-start`.
  145. :param list args: Optional arguments to override those delivered from \
  146. command line.
  147. '''
  148. args = parser.parse_args(args, app=app)
  149. exit_code = 0
  150. for domain in args.domains:
  151. if domain.is_running():
  152. if args.skip_if_running:
  153. continue
  154. exit_code = 1
  155. parser.print_error(
  156. 'domain {} is already running'.format(domain.name))
  157. return exit_code
  158. drive_assignment = None
  159. try:
  160. if args.drive:
  161. drive_assignment = get_drive_assignment(args.app, args.drive)
  162. try:
  163. domain.devices['block'].attach(drive_assignment)
  164. except:
  165. drive_assignment = None
  166. raise
  167. domain.start()
  168. if drive_assignment:
  169. # don't reconnect this device after VM reboot
  170. domain.devices['block'].update_persistent(
  171. drive_assignment.device, False)
  172. except (IOError, OSError, qubesadmin.exc.QubesException,
  173. ValueError) as e:
  174. if drive_assignment:
  175. try:
  176. domain.devices['block'].detach(drive_assignment)
  177. except qubesadmin.exc.QubesException:
  178. pass
  179. exit_code = 1
  180. parser.print_error(str(e))
  181. return exit_code
  182. if __name__ == '__main__':
  183. sys.exit(main())