core-admin-client/qubesadmin/tools/qvm_start.py
Marek Marczykowski-Górecki edcaed537a
Always use QubesVM objects, instead of AppVM/TemplateVM etc
Very few calls at client side really needs VM class name. So, even in
non-blind mode use just QubesVM class, to avoid strange cases depending
on blind mode being enabled or not. Then, have VM class name in 'klass'
property. If known at object creation time, cache it, otherwise query
qubesd at first access.
2017-10-02 21:12:16 +02:00

180 lines
6.1 KiB
Python

# -*- encoding: utf8 -*-
#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2017 Marek Marczykowski-Górecki
# <marmarek@invisiblethingslab.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with this program; if not, see <http://www.gnu.org/licenses/>.
'''qvm-start - start a domain'''
import argparse
import sys
import subprocess
import qubesadmin.devices
import qubesadmin.exc
import qubesadmin.tools
class DriveAction(argparse.Action):
'''Action for argument parser that stores drive image path.'''
# pylint: disable=redefined-builtin,too-few-public-methods
def __init__(self,
option_strings,
dest='drive',
prefix='cdrom:',
metavar='IMAGE',
required=False,
help='Attach drive'):
super(DriveAction, self).__init__(option_strings, dest,
metavar=metavar, help=help)
self.prefix = prefix
def __call__(self, parser, namespace, values, option_string=None):
# pylint: disable=redefined-outer-name
setattr(namespace, self.dest, self.prefix + values)
parser = qubesadmin.tools.QubesArgumentParser(
description='start a domain', vmname_nargs='+')
parser.add_argument('--skip-if-running',
action='store_true', default=False,
help='Do not fail if the qube is already runnning')
parser_drive = parser.add_mutually_exclusive_group()
parser_drive.add_argument('--drive', metavar='DRIVE',
help='temporarily attach specified drive as CD/DVD or hard disk (can be'
' specified with prefix "hd:" or "cdrom:", default is cdrom)')
parser_drive.add_argument('--hddisk',
action=DriveAction, dest='drive', prefix='hd:',
help='temporarily attach specified drive as hard disk')
parser_drive.add_argument('--cdrom', metavar='IMAGE',
action=DriveAction, dest='drive', prefix='cdrom:',
help='temporarily attach specified drive as CD/DVD')
parser_drive.add_argument('--install-windows-tools',
action='store_const', dest='drive', default=False,
const='cdrom:dom0:/usr/lib/qubes/qubes-windows-tools.iso',
help='temporarily attach Windows tools CDROM to the domain')
def get_drive_assignment(app, drive_str):
''' Prepare :py:class:`qubesadmin.devices.DeviceAssignment` object for a
given drive.
If running in dom0, it will also take care about creating appropriate
loop device (if necessary). Otherwise, only existing block devices are
supported.
:param app: Qubes() instance
:param drive_str: drive argument
:return: DeviceAssignment matching *drive_str*
'''
devtype = 'cdrom'
if drive_str.startswith('cdrom:'):
devtype = 'cdrom'
drive_str = drive_str[len('cdrom:'):]
elif drive_str.startswith('hd:'):
devtype = 'disk'
drive_str = drive_str[len('hd:'):]
backend_domain_name, ident = drive_str.split(':', 1)
try:
backend_domain = app.domains[backend_domain_name]
except KeyError:
raise qubesadmin.exc.QubesVMNotFoundError(
'No such VM: %s', backend_domain_name)
if ident.startswith('/'):
# it is a path - if we're running in dom0, try to call losetup to
# export the device, otherwise reject
if app.qubesd_connection_type == 'qrexec':
raise qubesadmin.exc.QubesException(
'Existing block device identifier needed when running from '
'outside of dom0 (see qvm-block)')
try:
if backend_domain.klass == 'AdminVM':
loop_name = subprocess.check_output(
['sudo', 'losetup', '-f', '--show', ident])
else:
loop_name, _ = backend_domain.run(
'losetup -f --show ' + ident, user='root')
except subprocess.CalledProcessError:
raise qubesadmin.exc.QubesException(
'Failed to setup loop device for %s', ident)
assert loop_name.startswith(b'/dev/loop')
ident = loop_name.decode().split('/')[2]
# FIXME: synchronize with udev + exposing device in qubesdb
options = {}
if devtype:
options['devtype'] = devtype
assignment = qubesadmin.devices.DeviceAssignment(
backend_domain,
ident,
options=options,
persistent=True)
return assignment
def main(args=None, app=None):
'''Main routine of :program:`qvm-start`.
:param list args: Optional arguments to override those delivered from \
command line.
'''
args = parser.parse_args(args, app=app)
exit_code = 0
for domain in args.domains:
if args.skip_if_running and domain.is_running():
continue
drive_assignment = None
try:
if args.drive:
drive_assignment = get_drive_assignment(args.app, args.drive)
try:
domain.devices['block'].attach(drive_assignment)
except:
drive_assignment = None
raise
domain.start()
if drive_assignment:
# don't reconnect this device after VM reboot
domain.devices['block'].update_persistent(
drive_assignment.device, False)
except (IOError, OSError, qubesadmin.exc.QubesException) as e:
if drive_assignment:
try:
domain.devices['block'].detach(drive_assignment)
except qubesadmin.exc.QubesException:
pass
exit_code = 1
parser.print_error(str(e))
return exit_code
if __name__ == '__main__':
sys.exit(main())