core-admin-client/qubesadmin/tools/qvm_create.py
Marek Marczykowski-Górecki 759fafea63
tools/qvm-create: properly create template-based StandaloneVM
By definition StandaloneVM is not linked to the template. Creating one
from a template is a clone operation. It's already possible using
qvm-clone tool, but it's logical to do that using qvm-create tool too.
This was the case in R3.2 too.

While adding this special case, skip cloning private volume, to preserve
behaviour of TemplateBaseVMs which do not inherit private volume either.

Fixes QubesOS/qubes-issues#3793
2018-10-18 03:24:15 +02:00

190 lines
6.4 KiB
Python

#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2010-2015 Joanna Rutkowska <joanna@invisiblethingslab.com>
# Copyright (C) 2015 Wojtek Porczyk <woju@invisiblethingslab.com>
# 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
'''qvm-create tool'''
# TODO list available classes
# TODO list labels (maybe in qvm-prefs)
# TODO features, devices, tags
from __future__ import print_function
import argparse
import os
import sys
import qubesadmin
import qubesadmin.tools
parser = qubesadmin.tools.QubesArgumentParser()
parser.add_argument('--class', '-C', dest='cls',
default='AppVM',
help='specify the class of the new domain (default: %(default)s)')
parser.add_argument('--property', '--prop',
action=qubesadmin.tools.PropertyAction,
help='set domain\'s property, like "internal", "memory" or "vcpus"')
parser.add_argument('--pool', '-p',
action='append',
metavar='VOLUME_NAME=POOL_NAME',
help='specify the pool to use for a volume')
parser.add_argument('-P',
metavar='POOL_NAME',
dest='one_pool',
default='',
help='change all volume pools to specified pool')
parser.add_argument('--template', '-t',
action=qubesadmin.tools.SinglePropertyAction,
help='specify the TemplateVM to use')
parser.add_argument('--label', '-l',
action=qubesadmin.tools.SinglePropertyAction,
help='specify the label to use for the new domain'
' (e.g. red, yellow, green, ...)')
parser.add_argument('--help-classes',
action='store_true',
help='List available classes and exit')
parser_root = parser.add_mutually_exclusive_group()
parser_root.add_argument('--root-copy-from', '-r', metavar='FILENAME',
help='use provided root.img instead of default/empty one'
' (file will be COPIED)')
parser_root.add_argument('--root-move-from', '-R', metavar='FILENAME',
help='use provided root.img instead of default/empty one'
' (file will be MOVED)')
# silently ignored
parser_root.add_argument('--no-root',
action='store_true', default=False,
help=argparse.SUPPRESS)
parser.add_argument('name', metavar='VMNAME',
action=qubesadmin.tools.SinglePropertyAction,
nargs='?',
help='name of the domain to create')
def main(args=None, app=None):
'''Main function of qvm-create tool'''
args = parser.parse_args(args, app=app)
if args.help_classes:
vm_classes = args.app.qubesd_call('dom0', 'admin.vmclass.List').decode()
vm_classes = vm_classes.splitlines()
print('\n'.join(sorted(vm_classes)))
return 0
pools = {}
pool = None
if hasattr(args, 'pool') and args.pool:
for pool_vol in args.pool:
try:
volume_name, pool_name = pool_vol.split('=')
pools[volume_name] = pool_name
except ValueError:
parser.error(
'Pool argument must be of form: -P volume_name=pool_name')
if args.one_pool:
pool = args.one_pool
if 'label' not in args.properties:
parser.error('--label option is mandatory')
if 'name' not in args.properties:
parser.error('VMNAME is mandatory')
root_source_path = args.root_copy_from or args.root_move_from
if root_source_path and not os.path.exists(root_source_path):
parser.error(
'File pointed by --root-copy-from/--root-move-from does not exist')
try:
args.app.get_label(args.properties['label'])
except KeyError:
parser.error('no such label: {!r}; available: {}'.format(
args.properties['label'],
', '.join(repr(l.name) for l in args.app.labels)))
try:
args.app.get_vm_class(args.cls)
except KeyError:
parser.error('no such domain class: {!r}'.format(args.cls))
try:
if args.cls == 'StandaloneVM' and 'template' in args.properties:
# "template-based" StandaloneVM is special, as it's a clone of
# the template
vm = args.app.clone_vm(args.properties.pop('template'),
args.properties.pop('name'),
new_cls=args.cls,
pool=pool,
pools=pools,
ignore_volumes=('private',))
else:
vm = args.app.add_new_vm(args.cls,
name=args.properties.pop('name'),
label=args.properties.pop('label'),
template=args.properties.pop('template', None),
pool=pool,
pools=pools)
except qubesadmin.exc.QubesException as e:
args.app.log.error('Error creating VM: {!s}'.format(e))
return 1
retcode = 0
for prop, value in args.properties.items():
try:
setattr(vm, prop, value)
except qubesadmin.exc.QubesException as e:
args.app.log.error(
'Error setting property {} (but VM created): {!s}'.
format(prop, e))
retcode = 2
if root_source_path:
try:
root_size = os.path.getsize(root_source_path)
if root_size > vm.volumes['root'].size:
vm.volumes['root'].resize(root_size)
with open(root_source_path, 'rb') as root_file:
vm.volumes['root'].import_data(root_file)
if args.root_move_from:
os.unlink(root_source_path)
except qubesadmin.exc.QubesException as e:
args.app.log.error(
'Error importing root volume (but VM created): {}'.
format(e))
retcode = 3
return retcode
if __name__ == '__main__':
sys.exit(main())