qvm_start.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  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(DriveAction, self).__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. backend_domain_name, ident = drive_str.split(':', 1)
  82. try:
  83. backend_domain = app.domains[backend_domain_name]
  84. except KeyError:
  85. raise qubesadmin.exc.QubesVMNotFoundError(
  86. 'No such VM: %s', backend_domain_name)
  87. if ident.startswith('/'):
  88. # it is a path - if we're running in dom0, try to call losetup to
  89. # export the device, otherwise reject
  90. if app.qubesd_connection_type == 'qrexec':
  91. raise qubesadmin.exc.QubesException(
  92. 'Existing block device identifier needed when running from '
  93. 'outside of dom0 (see qvm-block)')
  94. try:
  95. if backend_domain.klass == 'AdminVM':
  96. loop_name = subprocess.check_output(
  97. ['sudo', 'losetup', '-f', '--show', ident])
  98. loop_name = loop_name.strip()
  99. else:
  100. untrusted_loop_name, _ = backend_domain.run_with_args(
  101. 'losetup', '-f', '--show', ident,
  102. user='root')
  103. untrusted_loop_name = untrusted_loop_name.strip()
  104. allowed_chars = string.ascii_lowercase + string.digits + '/'
  105. allowed_chars = allowed_chars.encode('ascii')
  106. if not all(c in allowed_chars for c in untrusted_loop_name):
  107. raise qubesadmin.exc.QubesException(
  108. 'Invalid loop device name received from {}'.format(
  109. backend_domain.name))
  110. loop_name = untrusted_loop_name
  111. del untrusted_loop_name
  112. except subprocess.CalledProcessError:
  113. raise qubesadmin.exc.QubesException(
  114. 'Failed to setup loop device for %s', ident)
  115. assert loop_name.startswith(b'/dev/loop')
  116. ident = loop_name.decode().split('/')[2]
  117. # wait for device to appear
  118. # FIXME: convert this to waiting for event
  119. timeout = 10
  120. while isinstance(backend_domain.devices['block'][ident],
  121. qubesadmin.devices.UnknownDevice):
  122. if timeout == 0:
  123. raise qubesadmin.exc.QubesException(
  124. 'Timeout waiting for {}:{} device to appear'.format(
  125. backend_domain.name, ident))
  126. timeout -= 1
  127. time.sleep(1)
  128. options = {
  129. 'devtype': devtype,
  130. 'read-only': devtype == 'cdrom'
  131. }
  132. assignment = qubesadmin.devices.DeviceAssignment(
  133. backend_domain,
  134. ident,
  135. options=options,
  136. persistent=True)
  137. return assignment
  138. def main(args=None, app=None):
  139. '''Main routine of :program:`qvm-start`.
  140. :param list args: Optional arguments to override those delivered from \
  141. command line.
  142. '''
  143. args = parser.parse_args(args, app=app)
  144. exit_code = 0
  145. for domain in args.domains:
  146. if domain.is_running():
  147. if args.skip_if_running:
  148. continue
  149. exit_code = 1
  150. parser.print_error(
  151. 'domain {} is already running'.format(domain.name))
  152. return exit_code
  153. drive_assignment = None
  154. try:
  155. if args.drive:
  156. drive_assignment = get_drive_assignment(args.app, args.drive)
  157. try:
  158. domain.devices['block'].attach(drive_assignment)
  159. except:
  160. drive_assignment = None
  161. raise
  162. domain.start()
  163. if drive_assignment:
  164. # don't reconnect this device after VM reboot
  165. domain.devices['block'].update_persistent(
  166. drive_assignment.device, False)
  167. except (IOError, OSError, qubesadmin.exc.QubesException) as e:
  168. if drive_assignment:
  169. try:
  170. domain.devices['block'].detach(drive_assignment)
  171. except qubesadmin.exc.QubesException:
  172. pass
  173. exit_code = 1
  174. parser.print_error(str(e))
  175. return exit_code
  176. if __name__ == '__main__':
  177. sys.exit(main())