Various cleanup and improvements.

- `qvm-template list`: show template state
- `qvm-template list`: only call qubes.TemplateSearch once
- `qvm-template list`: use `qubesadmin.tools.print_table()` instead of own implementation
- `qvm-template download`: custom progress bar
- Use `run_service` instead of own implementation
- Remove some erroneous/redundant lines
This commit is contained in:
WillyPillow 2020-07-10 02:43:03 +08:00
parent 0e8e8d98de
commit 3d42c988f0

View File

@ -2,6 +2,8 @@
import argparse import argparse
import datetime import datetime
import enum
import math
import os import os
import shutil import shutil
import subprocess import subprocess
@ -11,6 +13,7 @@ import time
import dnf import dnf
import qubesadmin import qubesadmin
import qubesadmin.tools
import rpm import rpm
import xdg.BaseDirectory import xdg.BaseDirectory
@ -63,6 +66,12 @@ parser.add_argument('--available', action='store_true')
parser.add_argument('--extras', action='store_true') parser.add_argument('--extras', action='store_true')
parser.add_argument('--upgrades', action='store_true') parser.add_argument('--upgrades', action='store_true')
class TemplateState(enum.Enum):
INSTALLED = 'installed'
AVAILABLE = 'available'
EXTRA = 'extra'
UPGRADABLE = 'upgradable'
# NOTE: Verifying RPMs this way is prone to TOCTOU. This is okay for local # NOTE: Verifying RPMs this way is prone to TOCTOU. This is okay for local
# files, but may create problems if multiple instances of `qvm-template` are # files, but may create problems if multiple instances of `qvm-template` are
# downloading the same file, so a lock is needed in that case. # downloading the same file, so a lock is needed in that case.
@ -91,17 +100,16 @@ def get_package_hdr(path, transaction_set=None):
return hdr return hdr
def extract_rpm(name, path, target): def extract_rpm(name, path, target):
with open(path, 'rb') as in_file: rpm2cpio = subprocess.Popen(['rpm2cpio', path], stdout=subprocess.PIPE)
rpm2cpio = subprocess.Popen(['rpm2cpio', path], stdout=subprocess.PIPE) # `-D` is GNUism
# `-D` is GNUism cpio = subprocess.Popen([
cpio = subprocess.Popen([ 'cpio',
'cpio', '-idm',
'-idm', '-D',
'-D', target,
target, '.%s/%s/*' % (PATH_PREFIX, name)
'.%s/%s/*' % (PATH_PREFIX, name) ], stdin=rpm2cpio.stdout, stdout=subprocess.DEVNULL)
], stdin=rpm2cpio.stdout, stdout=subprocess.DEVNULL) return rpm2cpio.wait() == 0 and cpio.wait() == 0
return rpm2cpio.wait() == 0 and cpio.wait() == 0
def parse_config(path): def parse_config(path):
with open(path, 'r') as f: with open(path, 'r') as f:
@ -123,7 +131,7 @@ def install(args, app):
dl_list = get_dl_list(args, app) dl_list = get_dl_list(args, app)
dl_list_copy = dl_list.copy() dl_list_copy = dl_list.copy()
# Verify that the templates are not yet installed # Verify that the templates are not yet installed
for name, ver in dl_list.items(): for name, (ver, _) in dl_list.items():
if name in app.domains: if name in app.domains:
print(('Template \'%s\' already installed, skipping...' print(('Template \'%s\' already installed, skipping...'
' (You may want to use the {reinstall,upgrade,downgrade}' ' (You may want to use the {reinstall,upgrade,downgrade}'
@ -163,7 +171,6 @@ def install(args, app):
extract_rpm(name, rpmfile, target) extract_rpm(name, rpmfile, target)
cmdline = [ cmdline = [
'qvm-template-postprocess', 'qvm-template-postprocess',
'--keep-source',
'--really', '--really',
'--no-installed-by-rpm', '--no-installed-by-rpm',
] ]
@ -188,35 +195,19 @@ def install(args, app):
tpl.features['template-name'] = name tpl.features['template-name'] = name
# TODO: Store source repo # TODO: Store source repo
def qrexec_popen(args, app, service, stdout=subprocess.PIPE, encoding='UTF-8'): def qrexec_popen(args, app, service, stdout=subprocess.PIPE, filter_esc=True):
if args.updatevm: if args.updatevm:
from_vm = shutil.which('qrexec-client-vm') is not None return app.domains[args.updatevm].run_service(
if from_vm: service,
return subprocess.Popen( filter_esc=filter_esc,
['qrexec-client-vm', args.updatevm, service], stdout=stdout)
stdin=subprocess.PIPE,
stdout=stdout,
stderr=subprocess.PIPE,
encoding=encoding)
else:
return subprocess.Popen([
'qrexec-client',
'-d',
args.updatevm,
'user:/etc/qubes-rpc/%s' % service
],
stdin=subprocess.PIPE,
stdout=stdout,
stderr=subprocess.PIPE,
encoding=encoding)
else: else:
return subprocess.Popen([ return subprocess.Popen([
'/etc/qubes-rpc/%s' % service, '/etc/qubes-rpc/%s' % service,
], ],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=stdout, stdout=stdout,
stderr=subprocess.PIPE, stderr=subprocess.PIPE)
encoding=encoding)
def qrexec_payload(args, app, spec): def qrexec_payload(args, app, spec):
payload = '' payload = ''
@ -237,7 +228,8 @@ def qrexec_payload(args, app, spec):
def qrexec_repoquery(args, app, spec='*'): def qrexec_repoquery(args, app, spec='*'):
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, stderr = proc.communicate(payload) stdout, stderr = proc.communicate(payload.encode('UTF-8'))
stdout = stdout.decode('ASCII')
if proc.wait() != 0: if proc.wait() != 0:
return None return None
result = [] result = []
@ -249,20 +241,33 @@ def qrexec_repoquery(args, app, spec='*'):
result.append(entry) result.append(entry)
return result return result
def qrexec_download(args, app, spec, path): def qrexec_download(args, app, spec, path, dlsize=None):
with open(path, 'wb') as f: with open(path, 'wb') as f:
# Don't filter ESCs for binary files
proc = qrexec_popen(args, app, 'qubes.TemplateDownload', proc = qrexec_popen(args, app, 'qubes.TemplateDownload',
stdout=f, encoding=None) stdout=f, filter_esc=False)
payload = qrexec_payload(args, app, spec) payload = qrexec_payload(args, app, spec)
proc.stdin.write(payload.encode('UTF-8')) proc.stdin.write(payload.encode('UTF-8'))
proc.stdin.close() proc.stdin.close()
while True: while proc.poll() == None:
c = proc.stderr.read(1) width = shutil.get_terminal_size((80, 20)).columns
if not c: pct = '%5.f%% ' % math.floor(f.tell() / dlsize * 100)
break bar_len = width - len(pct) - 2
# Write raw byte w/o decoding num_hash = math.floor(f.tell() / dlsize * bar_len)
sys.stdout.buffer.write(c) num_space = bar_len - num_hash
sys.stdout.flush() # Clear previous bar
print(u'\u001b[1000D', end='', file=sys.stderr)
print(pct + '[' + ('#' * num_hash) + (' ' * num_space) + ']',
end='', file=sys.stderr)
sys.stderr.flush()
time.sleep(0.1)
#while True:
# c = proc.stderr.read(1)
# if not c:
# break
# # Write raw byte w/o decoding
# sys.stdout.buffer.write(c)
# sys.stdout.flush()
if proc.wait() != 0: if proc.wait() != 0:
return False return False
return True return True
@ -270,16 +275,6 @@ def qrexec_download(args, app, spec, path):
def build_version_str(evr): def build_version_str(evr):
return '%s:%s-%s' % evr return '%s:%s-%s' % evr
def pretty_print_table(table):
if len(table) != 0:
widths = []
for i in range(len(table[0])):
widths.append(max(len(s[i]) for s in table))
for row in sorted(table):
cols = ['{key:{width}s}'.format(
key=row[i], width=widths[i]) for i in range(len(row))]
print(' '.join(cols))
def do_list(args, app): def do_list(args, app):
# TODO: Check local template name actually matches to account for renames # TODO: Check local template name actually matches to account for renames
# TODO: Also display repo like `dnf list` # TODO: Also display repo like `dnf list`
@ -288,6 +283,9 @@ def do_list(args, app):
if not (args.installed or args.available or args.extras or args.upgrades): if not (args.installed or args.available or args.extras or args.upgrades):
args.all = True args.all = True
if args.all or args.available or args.extras or args.upgrades:
query_res = qrexec_repoquery(args, app)
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-install-date' in vm.features:
@ -295,17 +293,16 @@ def do_list(args, app):
vm.features['template-epoch'], vm.features['template-epoch'],
vm.features['template-version'], vm.features['template-version'],
vm.features['template-release'])) vm.features['template-release']))
tpl_list.append((vm.name, version_str)) tpl_list.append(
(vm.name, version_str, TemplateState.INSTALLED.value))
if args.available or args.all: if args.available or args.all:
query_res = qrexec_repoquery(args, app)
for name, epoch, version, release, reponame, dlsize, summary \ for name, epoch, version, release, reponame, dlsize, summary \
in query_res: in query_res:
version_str = build_version_str((epoch, version, release)) version_str = build_version_str((epoch, version, release))
tpl_list.append((name, version_str)) tpl_list.append((name, version_str, TemplateState.AVAILABLE.value))
if args.extras: if args.extras:
query_res = qrexec_repoquery(args, app)
remote = set() remote = set()
for name, epoch, version, release, reponame, dlsize, summary \ for name, epoch, version, release, reponame, dlsize, summary \
in query_res: in query_res:
@ -317,10 +314,10 @@ def do_list(args, app):
vm.features['template-epoch'], vm.features['template-epoch'],
vm.features['template-version'], vm.features['template-version'],
vm.features['template-release'])) vm.features['template-release']))
tpl_list.append((vm.name, version_str)) tpl_list.append(
(vm.name, version_str, TemplateState.EXTRA.value))
if args.upgrades: if args.upgrades:
query_res = qrexec_repoquery(args, app)
local = {} local = {}
for vm in app.domains: for vm in app.domains:
if 'template-name' in vm.features: if 'template-name' in vm.features:
@ -333,9 +330,10 @@ def do_list(args, app):
if name in local: if name in local:
if rpm.labelCompare(local[name], (epoch, version, release)) < 0: if rpm.labelCompare(local[name], (epoch, version, release)) < 0:
version_str = build_version_str((epoch, version, release)) version_str = build_version_str((epoch, version, release))
tpl_list.append((name, version_str)) tpl_list.append(
(name, version_str, TemplateState.UPGRADABLE.value))
pretty_print_table(tpl_list) qubesadmin.tools.print_table(tpl_list)
def get_dl_list(args, app): def get_dl_list(args, app):
candid = {} candid = {}
@ -353,7 +351,7 @@ def get_dl_list(args, app):
in query_res: in query_res:
ver = (epoch, version, release) ver = (epoch, version, release)
if name not in candid or rpm.labelCompare(candid[name], ver) < 0: if name not in candid or rpm.labelCompare(candid[name], ver) < 0:
candid[name] = ver candid[name] = (ver, int(dlsize))
return candid return candid
def download(args, app, path_override=None, dl_list=None): def download(args, app, path_override=None, dl_list=None):
@ -361,7 +359,7 @@ def download(args, app, path_override=None, dl_list=None):
dl_list = get_dl_list(args, app) dl_list = get_dl_list(args, app)
path = path_override if path_override != None else args.downloaddir path = path_override if path_override != None else args.downloaddir
for name, ver in dl_list.items(): for name, (ver, dlsize) in dl_list.items():
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)
@ -370,7 +368,7 @@ def download(args, app, path_override=None, dl_list=None):
file=sys.stderr) file=sys.stderr)
else: else:
print('Downloading \'%s\'...' % spec, file=sys.stderr) print('Downloading \'%s\'...' % spec, file=sys.stderr)
ret = qrexec_download(args, app, spec, target) ret = qrexec_download(args, app, spec, target, dlsize)
if not ret: if not ret:
# TODO: Retry? # TODO: Retry?
print('\'%s\' download failed.' % spec, file=sys.stderr) print('\'%s\' download failed.' % spec, file=sys.stderr)