qvm-template: Add purge operation.

This commit is contained in:
WillyPillow 2020-08-14 11:38:30 +08:00
parent b7a603b9fe
commit 3314500a83
No known key found for this signature in database
GPG Key ID: 3839E194B1415A9C

View File

@ -21,6 +21,8 @@ import typing
import qubesadmin import qubesadmin
import qubesadmin.tools import qubesadmin.tools
import qubesadmin.tools.qvm_kill
import qubesadmin.tools.qvm_remove
import rpm import rpm
import tqdm import tqdm
import xdg.BaseDirectory import xdg.BaseDirectory
@ -49,9 +51,12 @@ def parser_gen() -> argparse.ArgumentParser:
subparsers = parser_main.add_subparsers(dest='operation', required=True, subparsers = parser_main.add_subparsers(dest='operation', required=True,
description='Command to run.') description='Command to run.')
def parser_add_command(cmd, help_str, add_help=True): def parser_add_command(cmd, help_str):
return subparsers.add_parser(cmd, formatter_class=formatter, return subparsers.add_parser(
help=help_str, description=help_str, add_help=add_help) cmd,
formatter_class=formatter,
help=help_str,
description=help_str)
# qrexec/DNF related # qrexec/DNF related
parser_main.add_argument('--repo-files', action='append', parser_main.add_argument('--repo-files', action='append',
@ -130,9 +135,14 @@ def parser_gen() -> argparse.ArgumentParser:
parser_search.add_argument('templates', nargs='*', metavar='PATTERN') parser_search.add_argument('templates', nargs='*', metavar='PATTERN')
# qvm-template remove # qvm-template remove
parser_remove = parser_add_command('remove', parser_remove = parser_add_command('remove',
help_str='Remove installed templates.', help_str='Remove installed templates.')
add_help=False) # Forward --help to qvm-remove parser_remove.add_argument('--disassoc', action='store_true',
_ = parser_remove # unused help='Also disassociate VMs from the templates to be removed.')
parser_remove.add_argument('templates', nargs='*', metavar='TEMPLATE')
# qvm-template purge
parser_purge = parser_add_command('purge',
help_str='Remove installed templates and associated VMs.')
parser_purge.add_argument('templates', nargs='*', metavar='TEMPLATE')
# qvm-template clean # qvm-template clean
parser_clean = parser_add_command('clean', parser_clean = parser_add_command('clean',
help_str='Remove cached data.') help_str='Remove cached data.')
@ -282,6 +292,19 @@ def get_managed_template_vm(app: qubesadmin.app.QubesBase, name: str
parser.error("Template '%s' is not managed by qvm-template." % name) parser.error("Template '%s' is not managed by qvm-template." % name)
return vm return vm
def confirm_action(msg: str, affected: typing.List[str]) -> None:
"""Confirm user action."""
print(msg)
for name in affected:
print(' ' + name)
confirm = ''
while confirm != 'y':
confirm = input('Are you sure? [y/N] ').lower()
if confirm == 'n':
print('Operation cancelled.')
sys.exit(1)
def qrexec_popen( def qrexec_popen(
args: argparse.Namespace, args: argparse.Namespace,
app: qubesadmin.app.QubesBase, app: qubesadmin.app.QubesBase,
@ -816,15 +839,9 @@ def install(
for name in dl_list: for name in dl_list:
override_tpls.append(name) override_tpls.append(name)
print('This will override changes made in the following VMs:', confirm_action(
file=sys.stderr) 'This will override changes made in the following VMs:',
for name in override_tpls: override_tpls)
print(' %s' % name, file=sys.stderr)
confirm = ''
while confirm != 'y':
confirm = input('Are you sure? [y/N] ').lower()
if confirm == 'n':
sys.exit(1)
download(args, app, path_override=args.cachedir, download(args, app, path_override=args.cachedir,
dl_list=dl_list, suffix=UNVERIFIED_SUFFIX, dl_list=dl_list, suffix=UNVERIFIED_SUFFIX,
@ -1118,21 +1135,83 @@ def search(args: argparse.Namespace, app: qubesadmin.app.QubesBase) -> None:
print('===', cur_header, '===') print('===', cur_header, '===')
print(query_res[idx].name, ':', query_res[idx].summary) print(query_res[idx].name, ':', query_res[idx].summary)
def remove(args: argparse.Namespace, app: qubesadmin.app.QubesBase) -> None: def remove(
args: argparse.Namespace,
app: qubesadmin.app.QubesBase,
disassoc: bool = False,
purge: bool = False,
dummy: str = 'dummy'
) -> None:
"""Command that remove templates. """Command that remove templates.
:param args: Arguments received by the application. :param args: Arguments received by the application.
:param app: Qubes application object :param app: Qubes application object
:param disassoc: Whether to disassociate VMs from the templates
:param purge: Whether to remove VMs based on the templates
:param dummy: Name of dummy VM if disassoc is used
""" """
_ = args, app # unused # NOTE: While QubesArgumentParser provide similar functionality
# it does not seem to work as a parent parser
for tpl in args.templates:
if tpl not in app.domains:
parser.error("no such domain: '%s'" % tpl)
# Remove 'remove' entry from the args... remove_list = args.templates
operation_idx = sys.argv.index('remove') if purge:
argv = sys.argv[1:operation_idx] + sys.argv[operation_idx+1:] # Not disassociating first may result in dependency ordering issues
disassoc = True
# Remove recursively via BFS
remove_set = set(remove_list) # visited
idx = 0
while idx < len(remove_list):
tpl = remove_list[idx]
idx += 1
vm = app.domains[tpl]
for holder, prop in qubesadmin.utils.vm_dependencies(app, vm):
if holder is not None and holder.name not in remove_set:
remove_list.append(holder.name)
remove_set.add(holder.name)
# ...then pass the args to qvm-remove if not args.yes:
# Use exec so stdio can be shared easily repeat = 3 if purge else 1
os.execvp('qvm-remove', ['qvm-remove'] + argv) for _ in range(repeat):
confirm_action(
'This will completely remove the selected VM(s)...',
remove_list)
if disassoc:
# Remove the dummy afterwards if we're purging
remove_dummy = purge
# Create dummy template; handle name collisions
orig_dummy = dummy
cnt = 1
while dummy in app.domains \
and not app.domains[dummy].features.get('template-dummy', 0):
dummy = '%s-%d' % (orig_dummy, cnt)
cnt += 1
if dummy not in app.domains:
dummy_vm = app.add_new_vm('TemplateVM', dummy, 'red')
else:
dummy_vm = app.domains[dummy]
for tpl in remove_list:
vm = app.domains[tpl]
for holder, prop in qubesadmin.utils.vm_dependencies(app, vm):
if holder:
setattr(holder, prop, dummy_vm)
holder.template = dummy_vm
print("Property '%s' of '%s' set to '%s'." % (
prop, holder.name, dummy), file=sys.stderr)
else:
print("Global property '%s' set to ''." % prop,
file=sys.stderr)
setattr(app, prop, '')
if remove_dummy:
remove_list.append(dummy)
if disassoc or purge:
qubesadmin.tools.qvm_kill.main(['--'] + remove_list, app)
qubesadmin.tools.qvm_remove.main(['--force', '--'] + remove_list, app)
def clean(args: argparse.Namespace, app: qubesadmin.app.QubesBase) -> None: def clean(args: argparse.Namespace, app: qubesadmin.app.QubesBase) -> None:
"""Command that cleans the local package cache. """Command that cleans the local package cache.
@ -1214,15 +1293,7 @@ def main(args: typing.Optional[typing.Sequence[str]] = None,
:return: Return code of the application :return: Return code of the application
""" """
p_args, unk_args = parser.parse_known_args(args) p_args = parser.parse_args(args)
if p_args.operation != 'remove' and unk_args:
p_args = parser.parse_args(args) # this should result in an error
assert False and 'This line should not be executed.'
# FIXME: Currently doing things this way as we have to forward
# arguments to qvm-remove. While argparse.REMAINDER should be able to
# solve this, there's a bug (issue 17050) that prevents it from working
# on inputs where the first argument is an option, like 'qvm-template
# remove --help'. The bug should be fixed in Python 3.9.
# If the user specified other repo files... # If the user specified other repo files...
if len(p_args.repo_files) > 1: if len(p_args.repo_files) > 1:
@ -1255,7 +1326,9 @@ def main(args: typing.Optional[typing.Sequence[str]] = None,
elif p_args.operation == 'search': elif p_args.operation == 'search':
search(p_args, app) search(p_args, app)
elif p_args.operation == 'remove': elif p_args.operation == 'remove':
remove(p_args, app) remove(p_args, app, disassoc=p_args.disassoc)
elif p_args.operation == 'purge':
remove(p_args, app, purge=True)
elif p_args.operation == 'clean': elif p_args.operation == 'clean':
clean(p_args, app) clean(p_args, app)
elif p_args.operation == 'repolist': elif p_args.operation == 'repolist':