Initial implementation for "qvm-template search".

This commit is contained in:
WillyPillow 2020-07-21 19:34:16 +08:00
parent d656554822
commit c573faa9c0

View File

@ -1,9 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import argparse
import collections
import datetime import datetime
import enum import enum
import fnmatch import fnmatch
import functools
import itertools import itertools
import os import os
import shutil import shutil
@ -80,6 +82,7 @@ class TemplateState(enum.Enum):
UPGRADABLE = 'upgradable' UPGRADABLE = 'upgradable'
def title(self): def title(self):
#pylint: disable=invalid-name
TEMPLATE_TITLES = { TEMPLATE_TITLES = {
TemplateState.INSTALLED: 'Installed Templates', TemplateState.INSTALLED: 'Installed Templates',
TemplateState.AVAILABLE: 'Available Templates', TemplateState.AVAILABLE: 'Available Templates',
@ -273,6 +276,8 @@ def qrexec_payload(args, app, spec):
return payload return payload
def qrexec_repoquery(args, app, spec='*'): def qrexec_repoquery(args, app, spec='*'):
# TODO: Perhaps expose stderr for error messages?
# At least need to provide message that, e.g., repoid does not exist.
proc = qrexec_popen(args, app, 'qubes.TemplateSearch') proc = qrexec_popen(args, app, 'qubes.TemplateSearch')
payload = qrexec_payload(args, app, spec) payload = qrexec_payload(args, app, spec)
stdout, _ = proc.communicate(payload.encode('UTF-8')) stdout, _ = proc.communicate(payload.encode('UTF-8'))
@ -338,6 +343,7 @@ def list_templates(args, app, operation):
tpl_list = [] tpl_list = []
def append_list(data, status): def append_list(data, status):
#pylint: disable=unused-variable
name, epoch, version, release, reponame, dlsize, summary = data name, epoch, version, release, reponame, dlsize, summary = data
version_str = build_version_str((epoch, version, release)) version_str = build_version_str((epoch, version, release))
tpl_list.append((status, name, version_str, reponame)) tpl_list.append((status, name, version_str, reponame))
@ -386,7 +392,7 @@ def list_templates(args, app, operation):
if args.installed or args.all: if args.installed or args.all:
for vm in app.domains: for vm in app.domains:
if 'template-install-date' in vm.features: if 'template-name' in vm.features:
if not args.templates or \ if not args.templates or \
any(is_match_spec( any(is_match_spec(
vm.features['template-name'], vm.features['template-name'],
@ -403,6 +409,7 @@ def list_templates(args, app, operation):
if args.extras: if args.extras:
remote = set() remote = set()
#pylint: disable=unused-variable
for name, epoch, version, release, reponame, dlsize, summary \ for name, epoch, version, release, reponame, dlsize, summary \
in query_res: in query_res:
remote.add(name) remote.add(name)
@ -428,12 +435,88 @@ def list_templates(args, app, operation):
if len(tpl_list) == 0: if len(tpl_list) == 0:
parser.error('No matching templates to list') parser.error('No matching templates to list')
for k, g in itertools.groupby(tpl_list, lambda x: x[0]): for k, grp in itertools.groupby(tpl_list, lambda x: x[0]):
print(k.title()) print(k.title())
qubesadmin.tools.print_table(list(map(lambda x: x[1:], g))) qubesadmin.tools.print_table(list(map(lambda x: x[1:], grp)))
def search(args, app): def search(args, app):
raise NotImplementedError # Search in both installed and available templates
query_res = qrexec_repoquery(args, app)
for vm in app.domains:
if 'template-name' in vm.features:
query_res.append((
vm.features['template-name'],
vm.features['template-epoch'],
vm.features['template-version'],
vm.features['template-release'],
vm.features['template-reponame'],
vm.get_disk_utilization(),
vm.features['template-summary']))
# Get latest version for each template
query_res_tmp = []
for name, grp in itertools.groupby(query_res, lambda x: x[0]):
def compare(lhs, rhs):
return lhs if rpm.labelCompare(lhs[1:4], rhs[1:4]) < 0 else rhs
query_res_tmp.append(functools.reduce(compare, grp))
query_res = query_res_tmp
#pylint: disable=invalid-name
WEIGHT_NAME_EXACT = 1 << 3
WEIGHT_NAME = 1 << 2
WEIGHT_SUMMARY = 1 << 1
search_res = collections.defaultdict(int)
first = True
for keyword in args.templates:
local_res = collections.defaultdict(int)
#pylint: disable=unused-variable
for idx, (name, epoch, version, release, reponame, dlsize, summary) \
in enumerate(query_res):
if fnmatch.fnmatch(name, '*' + keyword + '*'):
local_res[idx] += WEIGHT_NAME
if fnmatch.fnmatch(summary, '*' + keyword + '*'):
local_res[idx] += WEIGHT_SUMMARY
if keyword == name:
local_res[idx] += WEIGHT_NAME_EXACT
for key, val in local_res.items():
if args.all or first or key in search_res:
search_res[key] += val
first = False
def key_func(x):
# Order by weight DESC, name ASC
weight = x[1]
name = query_res[x[0]][0]
return (-weight, name)
search_res = sorted(search_res.items(), key=key_func)
def gen_header(idx, weight):
# FIXME: "Exactly Matched" is printed even if the summary is not
# exactly matching
# TODO: Print matching keywords
#pylint: disable=unused-variable
name, epoch, version, release, reponame, dlsize, summary = \
query_res[idx]
keys = []
if weight & WEIGHT_NAME:
keys.append('Name')
if weight & WEIGHT_SUMMARY:
keys.append('Summary')
match = 'Exactly Matched' if weight & WEIGHT_NAME_EXACT else 'Matched'
return ' & '.join(keys) + ' ' + match
last_weight = -1
for idx, weight in search_res:
if last_weight != weight:
last_weight = weight
# Print headers
# XXX: The style is different from that of DNF
print(gen_header(idx, weight))
name, epoch, version, release, reponame, dlsize, summary = \
query_res[idx]
print(name, ':', summary)
def get_dl_list(args, app, version_selector=VersionSelector.LATEST): def get_dl_list(args, app, version_selector=VersionSelector.LATEST):
full_candid = {} full_candid = {}
@ -513,6 +596,7 @@ def download(args, app, path_override=None,
path = path_override if path_override is not None else args.downloaddir path = path_override if path_override is not None else args.downloaddir
for name, (ver, dlsize, reponame) in dl_list.items(): for name, (ver, dlsize, reponame) in dl_list.items():
_ = reponame # unused
version_str = build_version_str(ver) version_str = build_version_str(ver)
spec = PACKAGE_NAME_PREFIX + name + '-' + version_str spec = PACKAGE_NAME_PREFIX + name + '-' + version_str
target = os.path.join(path, '%s.rpm' % spec) target = os.path.join(path, '%s.rpm' % spec)
@ -543,7 +627,7 @@ def download(args, app, path_override=None,
sys.exit(1) sys.exit(1)
def remove(args, app): def remove(args, app):
_ = app # unused _ = args, app # unused
# Remove 'remove' entry from the args... # Remove 'remove' entry from the args...
operation_idx = sys.argv.index('remove') operation_idx = sys.argv.index('remove')