qvm-template: More docstrings.
This commit is contained in:
parent
41cf9f948e
commit
7b6fa39d1c
@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
'''Tool for managing VM templates.'''
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import collections
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
@ -154,34 +156,37 @@ class TemplateState(enum.Enum):
|
|||||||
class VersionSelector(enum.Enum):
|
class VersionSelector(enum.Enum):
|
||||||
"""Enum representing how the candidate template version is chosen."""
|
"""Enum representing how the candidate template version is chosen."""
|
||||||
LATEST = enum.auto()
|
LATEST = enum.auto()
|
||||||
|
"""Install latest version."""
|
||||||
REINSTALL = enum.auto()
|
REINSTALL = enum.auto()
|
||||||
|
"""Reinstall current version."""
|
||||||
LATEST_LOWER = enum.auto()
|
LATEST_LOWER = enum.auto()
|
||||||
|
"""Downgrade to the highest version that is lower than the current one."""
|
||||||
LATEST_HIGHER = enum.auto()
|
LATEST_HIGHER = enum.auto()
|
||||||
|
"""Upgrade to the highest version that is higher than the current one."""
|
||||||
|
|
||||||
# TODO: Docstrings and type hints for Template and DlEntry
|
class Template(typing.NamedTuple):
|
||||||
Template = collections.namedtuple('Template', [
|
"""Details of a template."""
|
||||||
'name',
|
name: str
|
||||||
'epoch',
|
epoch: str
|
||||||
'version',
|
version: str
|
||||||
'release',
|
release: str
|
||||||
'reponame',
|
reponame: str
|
||||||
'dlsize',
|
dlsize: int
|
||||||
'buildtime',
|
buildtime: datetime.datetime
|
||||||
'licence',
|
licence: str
|
||||||
'url',
|
url: str
|
||||||
'summary',
|
summary: str
|
||||||
'description'
|
description: str
|
||||||
])
|
|
||||||
|
|
||||||
DlEntry = collections.namedtuple('DlEntry', [
|
class DlEntry(typing.NamedTuple):
|
||||||
'evr',
|
"""Information about a template to be downloaded."""
|
||||||
'reponame',
|
evr: typing.Tuple[str, str, str]
|
||||||
'dlsize'
|
reponame: str
|
||||||
])
|
dlsize: int
|
||||||
|
|
||||||
def build_version_str(evr: typing.Tuple[str, str, str]) -> str:
|
def build_version_str(evr: typing.Tuple[str, str, str]) -> str:
|
||||||
"""Return version string described by ``evr`` in (epoch, version, release)
|
"""Return version string described by ``evr``, which is in (epoch, version,
|
||||||
format."""
|
release) format."""
|
||||||
return '%s:%s-%s' % evr
|
return '%s:%s-%s' % evr
|
||||||
|
|
||||||
def is_match_spec(name: str, epoch: str, version: str, release: str, spec: str
|
def is_match_spec(name: str, epoch: str, version: str, release: str, spec: str
|
||||||
@ -191,11 +196,11 @@ def is_match_spec(name: str, epoch: str, version: str, release: str, spec: str
|
|||||||
For the algorithm, refer to section "NEVRA Matching" in the DNF
|
For the algorithm, refer to section "NEVRA Matching" in the DNF
|
||||||
documentation.
|
documentation.
|
||||||
|
|
||||||
NOTE: Currently ``arch`` is ignored as the templates should be of
|
Note that currently ``arch`` is ignored as the templates should be of
|
||||||
``noarch``.
|
``noarch``.
|
||||||
|
|
||||||
:return: the first element indicates whether there is a match; the second
|
:return: A tuple. The first element indicates whether there is a match; the
|
||||||
element represents the priority of the match (lower is better).
|
second element represents the priority of the match (lower is better)
|
||||||
"""
|
"""
|
||||||
if epoch != 0:
|
if epoch != 0:
|
||||||
targets = [
|
targets = [
|
||||||
@ -263,24 +268,26 @@ def qrexec_popen(
|
|||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
app: qubesadmin.app.QubesBase,
|
app: qubesadmin.app.QubesBase,
|
||||||
service: str,
|
service: str,
|
||||||
stdout: int = subprocess.PIPE,
|
stdout: typing.Union[int, typing.IO] = subprocess.PIPE,
|
||||||
filter_esc: bool = True) -> subprocess.Popen:
|
filter_esc: bool = True) -> subprocess.Popen:
|
||||||
"""Return Popen object that communicates with the given qrexec call.
|
"""Return ``Popen`` object that communicates with the given qrexec call in
|
||||||
|
``args.updatevm``.
|
||||||
|
|
||||||
Note that this falls back to invoking /etc/qubes-rpc/* directly if
|
Note that this falls back to invoking ``/etc/qubes-rpc/*`` directly if
|
||||||
args.updatevm is None.
|
``args.updatevm`` is None.
|
||||||
|
|
||||||
:param args: arguments received by the application
|
:param args: Arguments received by the application. ``args.updatevm`` is
|
||||||
|
used
|
||||||
:param app: Qubes application object
|
:param app: Qubes application object
|
||||||
:param service: the qrexec call to invoke
|
:param service: The qrexec call to invoke
|
||||||
:param stdout: Where the process stdout points to. This is passed directly
|
:param stdout: Where the process stdout points to. This is passed directly
|
||||||
to subprocess.Popen. Defaults to subprocess.PIPE.
|
to ``subprocess.Popen``. Defaults to ``subprocess.PIPE``
|
||||||
|
|
||||||
Note that stderr is always set to subprocess.PIPE.
|
Note that stderr is always set to ``subprocess.PIPE``
|
||||||
:param filter_esc: whether to filter out escape sequences from
|
:param filter_esc: Whether to filter out escape sequences from
|
||||||
stdout/stderr. Defaults to True.
|
stdout/stderr. Defaults to True
|
||||||
|
|
||||||
:returns: Popen object that communicates with the given qrexec call
|
:returns: ``Popen`` object that communicates with the given qrexec call
|
||||||
"""
|
"""
|
||||||
if args.updatevm:
|
if args.updatevm:
|
||||||
return app.domains[args.updatevm].run_service(
|
return app.domains[args.updatevm].run_service(
|
||||||
@ -294,7 +301,21 @@ def qrexec_popen(
|
|||||||
stdout=stdout,
|
stdout=stdout,
|
||||||
stderr=subprocess.PIPE)
|
stderr=subprocess.PIPE)
|
||||||
|
|
||||||
def qrexec_payload(args, app, spec, refresh):
|
def qrexec_payload(args: argparse.Namespace, app: qubesadmin.app.QubesBase,
|
||||||
|
spec: str, refresh: bool) -> str:
|
||||||
|
"""Return payload string for the ``qubes.Template*`` qrexec calls.
|
||||||
|
|
||||||
|
:param args: Arguments received by the application. Specifically,
|
||||||
|
``args.{enablerepo,disablerepo,repoid,releasever,repo_files}`` are used
|
||||||
|
:param app: Qubes application object
|
||||||
|
:param spec: Package spec to query (refer to ``<package-name-spec>`` in the
|
||||||
|
DNF documentation)
|
||||||
|
:param refresh: Whether to force refresh repo metadata
|
||||||
|
|
||||||
|
:return: Payload string
|
||||||
|
|
||||||
|
:raises: Parser error if spec equals ``---`` or input contains ``\\n``
|
||||||
|
"""
|
||||||
_ = app # unused
|
_ = app # unused
|
||||||
|
|
||||||
if spec == '---':
|
if spec == '---':
|
||||||
@ -327,7 +348,25 @@ def qrexec_payload(args, app, spec, refresh):
|
|||||||
payload += fd.read() + '\n'
|
payload += fd.read() + '\n'
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
def qrexec_repoquery(args, app, spec='*', refresh=False):
|
def qrexec_repoquery(
|
||||||
|
args: argparse.Namespace,
|
||||||
|
app: qubesadmin.app.QubesBase,
|
||||||
|
spec: str = '*',
|
||||||
|
refresh: bool = False) -> typing.List[Template]:
|
||||||
|
"""Query template information from repositories.
|
||||||
|
|
||||||
|
:param args: Arguments received by the application. Specifically,
|
||||||
|
``args.{enablerepo,disablerepo,repoid,releasever,repo_files,updatevm}``
|
||||||
|
are used
|
||||||
|
:param app: Qubes application object
|
||||||
|
:param spec: Package spec to query (refer to ``<package-name-spec>`` in the
|
||||||
|
DNF documentation). Defaults to ``*``
|
||||||
|
:param refresh: Whether to force refresh repo metadata. Defaults to False
|
||||||
|
|
||||||
|
:raises ConnectionError: if the qrexec call fails
|
||||||
|
|
||||||
|
:return: List of ``Template`` objects representing the result of the query
|
||||||
|
"""
|
||||||
payload = qrexec_payload(args, app, spec, refresh)
|
payload = qrexec_payload(args, app, spec, refresh)
|
||||||
proc = qrexec_popen(args, app, 'qubes.TemplateSearch')
|
proc = qrexec_popen(args, app, 'qubes.TemplateSearch')
|
||||||
stdout, stderr = proc.communicate(payload.encode('UTF-8'))
|
stdout, stderr = proc.communicate(payload.encode('UTF-8'))
|
||||||
@ -386,7 +425,28 @@ def qrexec_repoquery(args, app, spec='*', refresh=False):
|
|||||||
" unexpected data format."))
|
" unexpected data format."))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def qrexec_download(args, app, spec, path, dlsize=None, refresh=False):
|
def qrexec_download(
|
||||||
|
args: argparse.Namespace,
|
||||||
|
app: qubesadmin.app.QubesBase,
|
||||||
|
spec: str,
|
||||||
|
path: str,
|
||||||
|
dlsize: typing.Optional[int] = None,
|
||||||
|
refresh: bool = False) -> None:
|
||||||
|
"""Download a template from repositories.
|
||||||
|
|
||||||
|
:param args: Arguments received by the application. Specifically,
|
||||||
|
``args.{enablerepo,disablerepo,repoid,releasever,repo_files,updatevm}``
|
||||||
|
are used
|
||||||
|
:param app: Qubes application object
|
||||||
|
:param spec: Package spec to query (refer to ``<package-name-spec>`` in the
|
||||||
|
DNF documentation)
|
||||||
|
:param path: Path to place the downloaded template
|
||||||
|
:param dlsize: Size of template to be downloaded. Used for the progress
|
||||||
|
bar. Optional
|
||||||
|
:param refresh: Whether to force refresh repo metadata. Defaults to False
|
||||||
|
|
||||||
|
:raises ConnectionError: if the qrexec call fails
|
||||||
|
"""
|
||||||
with open(path, 'wb') as fd:
|
with open(path, 'wb') as fd:
|
||||||
payload = qrexec_payload(args, app, spec, refresh)
|
payload = qrexec_payload(args, app, spec, refresh)
|
||||||
# Don't filter ESCs for binary files
|
# Don't filter ESCs for binary files
|
||||||
@ -405,12 +465,25 @@ def qrexec_download(args, app, spec, path, dlsize=None, refresh=False):
|
|||||||
if proc.wait() != 0:
|
if proc.wait() != 0:
|
||||||
raise ConnectionError(
|
raise ConnectionError(
|
||||||
"qrexec call 'qubes.TemplateDownload' failed.")
|
"qrexec call 'qubes.TemplateDownload' failed.")
|
||||||
return True
|
|
||||||
|
|
||||||
def verify_rpm(path, nogpgcheck=False, transaction_set=None):
|
def verify_rpm(
|
||||||
# NOTE: Verifying RPMs this way is prone to TOCTOU. This is okay for local
|
path: str,
|
||||||
# files, but may create problems if multiple instances of `qvm-template`
|
nogpgcheck: bool = False,
|
||||||
# are downloading the same file, so a lock is needed in that case.
|
transaction_set: typing.Optional[rpm.transaction.TransactionSet] = None
|
||||||
|
) -> bool:
|
||||||
|
"""Verify the digest and signature of a RPM package.
|
||||||
|
|
||||||
|
Note that 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 downloading the same file, so a lock is needed in that
|
||||||
|
case.
|
||||||
|
|
||||||
|
:param path: Location of the RPM package
|
||||||
|
:param nogpgcheck: Whether to allow invalid GPG signatures
|
||||||
|
:param transaction_set: Override RPM ``TransactionSet``. Optional
|
||||||
|
|
||||||
|
:return: Whether the RPM is verified
|
||||||
|
"""
|
||||||
if transaction_set is None:
|
if transaction_set is None:
|
||||||
transaction_set = rpm.TransactionSet()
|
transaction_set = rpm.TransactionSet()
|
||||||
with open(path, 'rb') as fd:
|
with open(path, 'rb') as fd:
|
||||||
@ -427,14 +500,34 @@ def verify_rpm(path, nogpgcheck=False, transaction_set=None):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_package_hdr(path, transaction_set=None):
|
def get_package_hdr(
|
||||||
|
path: str,
|
||||||
|
transaction_set: typing.Optional[rpm.transaction.TransactionSet] = None
|
||||||
|
) -> rpm.hdr:
|
||||||
|
"""Return header of a RPM package.
|
||||||
|
|
||||||
|
Note that this function **does not** check the integrity of the package.
|
||||||
|
|
||||||
|
:param path: Location of the RPM package
|
||||||
|
:param transaction_set: Override RPM ``TransactionSet``. Optional
|
||||||
|
|
||||||
|
:return: RPM headers
|
||||||
|
"""
|
||||||
if transaction_set is None:
|
if transaction_set is None:
|
||||||
transaction_set = rpm.TransactionSet()
|
transaction_set = rpm.TransactionSet()
|
||||||
with open(path, 'rb') as fd:
|
with open(path, 'rb') as fd:
|
||||||
hdr = transaction_set.hdrFromFdno(fd)
|
hdr = transaction_set.hdrFromFdno(fd)
|
||||||
return hdr
|
return hdr
|
||||||
|
|
||||||
def extract_rpm(name, path, target):
|
def extract_rpm(name: str, path: str, target: str) -> bool:
|
||||||
|
"""Extract a template RPM package.
|
||||||
|
|
||||||
|
:param name: Name of the template
|
||||||
|
:param path: Location of the RPM package
|
||||||
|
:param target: Target path to extract to
|
||||||
|
|
||||||
|
:return: Whether the extraction succeeded
|
||||||
|
"""
|
||||||
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([
|
||||||
@ -446,12 +539,26 @@ def extract_rpm(name, path, target):
|
|||||||
], 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 get_dl_list(args, app, version_selector=VersionSelector.LATEST):
|
def get_dl_list(
|
||||||
full_candid = {}
|
args: argparse.Namespace,
|
||||||
|
app: qubesadmin.app.QubesBase,
|
||||||
|
version_selector: VersionSelector = VersionSelector.LATEST
|
||||||
|
) -> typing.Dict[str, DlEntry]:
|
||||||
|
"""Return list of templates that needs to be downloaded.
|
||||||
|
|
||||||
|
:param args: Arguments received by the application.
|
||||||
|
:param app: Qubes application object
|
||||||
|
:param version_selector: Specify algorithm to select the candidate version
|
||||||
|
of a package. Defaults to ``VersionSelector.LATEST``
|
||||||
|
|
||||||
|
:return: Dictionary that maps to ``DlEntry`` the names of templates that
|
||||||
|
needs to be downloaded
|
||||||
|
"""
|
||||||
|
full_candid: typing.Dict[str, DlEntry] = {}
|
||||||
for template in args.templates:
|
for template in args.templates:
|
||||||
# This will be merged into `full_candid` later.
|
# This will be merged into `full_candid` later.
|
||||||
# It is separated so that we can check whether it is empty.
|
# It is separated so that we can check whether it is empty.
|
||||||
candid = {}
|
candid: typing.Dict[str, DlEntry] = {}
|
||||||
|
|
||||||
# Skip local RPMs
|
# Skip local RPMs
|
||||||
if template.endswith('.rpm'):
|
if template.endswith('.rpm'):
|
||||||
@ -505,15 +612,35 @@ def get_dl_list(args, app, version_selector=VersionSelector.LATEST):
|
|||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
|
|
||||||
# Merge & choose the template with the highest version
|
# Merge & choose the template with the highest version
|
||||||
for name, entry in candid.items():
|
for name, dlentry in candid.items():
|
||||||
if name not in full_candid \
|
if name not in full_candid \
|
||||||
or rpm.labelCompare(full_candid[name].evr, entry.evr) < 0:
|
or rpm.labelCompare(full_candid[name].evr, dlentry.evr) < 0:
|
||||||
full_candid[name] = entry
|
full_candid[name] = dlentry
|
||||||
|
|
||||||
return full_candid
|
return full_candid
|
||||||
|
|
||||||
def download(args, app, path_override=None,
|
def download(
|
||||||
dl_list=None, suffix='', version_selector=VersionSelector.LATEST):
|
args: argparse.Namespace,
|
||||||
|
app: qubesadmin.app.QubesBase,
|
||||||
|
path_override: typing.Optional[str] = None,
|
||||||
|
dl_list: typing.Optional[typing.Dict[str, DlEntry]] = None,
|
||||||
|
suffix: str = '',
|
||||||
|
version_selector: VersionSelector = VersionSelector.LATEST) -> None:
|
||||||
|
"""Command that downloads template packages.
|
||||||
|
|
||||||
|
:param args: Arguments received by the application.
|
||||||
|
:param app: Qubes application object
|
||||||
|
:param path_override: Override path to store downloads. If not set or set
|
||||||
|
to None, ``args.downloaddir`` is used. Optional
|
||||||
|
:param dl_list: Override list of templates to download. If not set or set
|
||||||
|
to None, ``get_dl_list`` is called, which generates the list from
|
||||||
|
``args``. Optional
|
||||||
|
:param suffix: Suffix to add to the file name of downloaded packages. This
|
||||||
|
is useful if you want to distinguish between verified and unverified
|
||||||
|
packages. Defaults to an empty string
|
||||||
|
:param version_selector: Specify algorithm to select the candidate version
|
||||||
|
of a package. Defaults to ``VersionSelector.LATEST``
|
||||||
|
"""
|
||||||
if dl_list is None:
|
if dl_list is None:
|
||||||
dl_list = get_dl_list(args, app, version_selector=version_selector)
|
dl_list = get_dl_list(args, app, version_selector=version_selector)
|
||||||
|
|
||||||
@ -553,8 +680,23 @@ def download(args, app, path_override=None,
|
|||||||
print('\'%s\' download failed.' % spec, file=sys.stderr)
|
print('\'%s\' download failed.' % spec, file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
def install(args, app, version_selector=VersionSelector.LATEST,
|
def install(
|
||||||
override_existing=False):
|
args: argparse.Namespace,
|
||||||
|
app: qubesadmin.app.QubesBase,
|
||||||
|
version_selector: VersionSelector = VersionSelector.LATEST,
|
||||||
|
override_existing: bool = False) -> None:
|
||||||
|
"""Command that installs template packages.
|
||||||
|
|
||||||
|
This command creates a lock file to ensure that two instances are not
|
||||||
|
running at the same time.
|
||||||
|
|
||||||
|
:param args: Arguments received by the application.
|
||||||
|
:param app: Qubes application object
|
||||||
|
:param version_selector: Specify algorithm to select the candidate version
|
||||||
|
of a package. Defaults to ``VersionSelector.LATEST``
|
||||||
|
:param override_existing: Whether to override existing packages. Used for
|
||||||
|
reinstall, upgrade, and downgrade operations
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
with open(LOCK_FILE, 'x') as _:
|
with open(LOCK_FILE, 'x') as _:
|
||||||
pass
|
pass
|
||||||
@ -700,7 +842,16 @@ def install(args, app, version_selector=VersionSelector.LATEST,
|
|||||||
finally:
|
finally:
|
||||||
os.remove(LOCK_FILE)
|
os.remove(LOCK_FILE)
|
||||||
|
|
||||||
def list_templates(args, app, operation):
|
def list_templates(args: argparse.Namespace,
|
||||||
|
app: qubesadmin.app.QubesBase, operation: str) -> None:
|
||||||
|
"""Command that lists templates.
|
||||||
|
|
||||||
|
:param args: Arguments received by the application.
|
||||||
|
:param app: Qubes application object
|
||||||
|
:param operation: If set to ``list``, display a listing similar to ``dnf
|
||||||
|
list``. If set to ``info``, display detailed template information
|
||||||
|
similar to ``dnf info``. Otherwise, an ``AssertionError`` is raised.
|
||||||
|
"""
|
||||||
tpl_list = []
|
tpl_list = []
|
||||||
|
|
||||||
def append_list(data, status, install_time=None):
|
def append_list(data, status, install_time=None):
|
||||||
@ -745,10 +896,10 @@ def list_templates(args, app, operation):
|
|||||||
|
|
||||||
if args.all or args.available or args.extras or args.upgrades:
|
if args.all or args.available or args.extras or args.upgrades:
|
||||||
if args.templates:
|
if args.templates:
|
||||||
query_res = set()
|
query_res_set: typing.Set[Template] = set()
|
||||||
for spec in args.templates:
|
for spec in args.templates:
|
||||||
query_res |= set(qrexec_repoquery(args, app, spec))
|
query_res_set |= set(qrexec_repoquery(args, app, spec))
|
||||||
query_res = list(query_res)
|
query_res = list(query_res_set)
|
||||||
else:
|
else:
|
||||||
query_res = qrexec_repoquery(args, app)
|
query_res = qrexec_repoquery(args, app)
|
||||||
|
|
||||||
@ -793,7 +944,12 @@ def list_templates(args, app, operation):
|
|||||||
print(k.title())
|
print(k.title())
|
||||||
qubesadmin.tools.print_table(list(map(lambda x: x[1:], grp)))
|
qubesadmin.tools.print_table(list(map(lambda x: x[1:], grp)))
|
||||||
|
|
||||||
def search(args, app):
|
def search(args: argparse.Namespace, app: qubesadmin.app.QubesBase) -> None:
|
||||||
|
"""Command that searches template details for given patterns.
|
||||||
|
|
||||||
|
:param args: Arguments received by the application.
|
||||||
|
:param app: Qubes application object
|
||||||
|
"""
|
||||||
# Search in both installed and available templates
|
# Search in both installed and available templates
|
||||||
query_res = qrexec_repoquery(args, app)
|
query_res = qrexec_repoquery(args, app)
|
||||||
for vm in app.domains:
|
for vm in app.domains:
|
||||||
@ -822,27 +978,29 @@ def search(args, app):
|
|||||||
(WEIGHT_DESCRIPTION, 'Description'),
|
(WEIGHT_DESCRIPTION, 'Description'),
|
||||||
(WEIGHT_URL, 'URL')]
|
(WEIGHT_URL, 'URL')]
|
||||||
|
|
||||||
search_res = collections.defaultdict(list)
|
search_res_by_idx: \
|
||||||
|
typing.Dict[int, typing.List[typing.Tuple[int, str, bool]]] = \
|
||||||
|
collections.defaultdict(list)
|
||||||
for keyword in args.templates:
|
for keyword in args.templates:
|
||||||
for idx, entry in enumerate(query_res):
|
for idx, entry in enumerate(query_res):
|
||||||
needles = \
|
needle_types = \
|
||||||
[(entry.name, WEIGHT_NAME), (entry.summary, WEIGHT_SUMMARY)]
|
[(entry.name, WEIGHT_NAME), (entry.summary, WEIGHT_SUMMARY)]
|
||||||
if args.all:
|
if args.all:
|
||||||
needles += [(entry.description, WEIGHT_DESCRIPTION),
|
needle_types += [(entry.description, WEIGHT_DESCRIPTION),
|
||||||
(entry.url, WEIGHT_URL)]
|
(entry.url, WEIGHT_URL)]
|
||||||
for key, weight in needles:
|
for key, weight in needle_types:
|
||||||
if fnmatch.fnmatch(key, '*' + keyword + '*'):
|
if fnmatch.fnmatch(key, '*' + keyword + '*'):
|
||||||
exact = keyword == key
|
exact = keyword == key
|
||||||
if exact and weight == WEIGHT_NAME:
|
if exact and weight == WEIGHT_NAME:
|
||||||
weight = WEIGHT_NAME_EXACT
|
weight = WEIGHT_NAME_EXACT
|
||||||
search_res[idx].append((weight, keyword, exact))
|
search_res_by_idx[idx].append((weight, keyword, exact))
|
||||||
|
|
||||||
if not args.all:
|
if not args.all:
|
||||||
keywords = set(args.templates)
|
keywords = set(args.templates)
|
||||||
idxs = list(search_res.keys())
|
idxs = list(search_res_by_idx.keys())
|
||||||
for idx in idxs:
|
for idx in idxs:
|
||||||
if keywords != set(x[1] for x in search_res[idx]):
|
if keywords != set(x[1] for x in search_res_by_idx[idx]):
|
||||||
del search_res[idx]
|
del search_res_by_idx[idx]
|
||||||
|
|
||||||
def key_func(x):
|
def key_func(x):
|
||||||
# ORDER BY weight DESC, list_of_needles ASC, name ASC
|
# ORDER BY weight DESC, list_of_needles ASC, name ASC
|
||||||
@ -851,7 +1009,7 @@ def search(args, app):
|
|||||||
name = query_res[idx][0]
|
name = query_res[idx][0]
|
||||||
return (-weight, needles, name)
|
return (-weight, needles, name)
|
||||||
|
|
||||||
search_res = sorted(search_res.items(), key=key_func)
|
search_res = sorted(search_res_by_idx.items(), key=key_func)
|
||||||
|
|
||||||
def gen_header(needles):
|
def gen_header(needles):
|
||||||
fields = []
|
fields = []
|
||||||
@ -874,7 +1032,12 @@ def search(args, app):
|
|||||||
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, app):
|
def remove(args: argparse.Namespace, app: qubesadmin.app.QubesBase) -> None:
|
||||||
|
"""Command that remove templates.
|
||||||
|
|
||||||
|
:param args: Arguments received by the application.
|
||||||
|
:param app: Qubes application object
|
||||||
|
"""
|
||||||
_ = args, app # unused
|
_ = args, app # unused
|
||||||
|
|
||||||
# Remove 'remove' entry from the args...
|
# Remove 'remove' entry from the args...
|
||||||
@ -885,17 +1048,29 @@ def remove(args, app):
|
|||||||
# Use exec so stdio can be shared easily
|
# Use exec so stdio can be shared easily
|
||||||
os.execvp('qvm-remove', ['qvm-remove'] + argv)
|
os.execvp('qvm-remove', ['qvm-remove'] + argv)
|
||||||
|
|
||||||
def clean(args, app):
|
def clean(args: argparse.Namespace, app: qubesadmin.app.QubesBase) -> None:
|
||||||
|
"""Command that cleans the local package cache.
|
||||||
|
|
||||||
|
:param args: Arguments received by the application.
|
||||||
|
:param app: Qubes application object
|
||||||
|
"""
|
||||||
# TODO: More fine-grained options
|
# TODO: More fine-grained options
|
||||||
_ = app # unused
|
_ = app # unused
|
||||||
|
|
||||||
shutil.rmtree(args.cachedir)
|
shutil.rmtree(args.cachedir)
|
||||||
|
|
||||||
def main(args=None, app=None):
|
def main(args: typing.Optional[typing.Sequence[str]] = None,
|
||||||
raw_args = args
|
app: typing.Optional[qubesadmin.app.QubesBase] = None) -> int:
|
||||||
args, unk_args = parser.parse_known_args(raw_args)
|
"""Main routine of **qvm-template**.
|
||||||
if args.operation != 'remove' and unk_args:
|
|
||||||
args = parser.parse_args(raw_args) # this should result in an error
|
:param args: Override arguments received by the application. Optional
|
||||||
|
:param app: Override Qubes application object. Optional
|
||||||
|
|
||||||
|
:return: Return code of the application
|
||||||
|
"""
|
||||||
|
p_args, unk_args = parser.parse_known_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.'
|
assert False and 'This line should not be executed.'
|
||||||
# FIXME: Currently doing things this way as we have to forward
|
# FIXME: Currently doing things this way as we have to forward
|
||||||
# arguments to qvm-remove. While argparse.REMAINDER should be able to
|
# arguments to qvm-remove. While argparse.REMAINDER should be able to
|
||||||
@ -906,34 +1081,34 @@ def main(args=None, app=None):
|
|||||||
if app is None:
|
if app is None:
|
||||||
app = qubesadmin.Qubes()
|
app = qubesadmin.Qubes()
|
||||||
|
|
||||||
if args.refresh:
|
if p_args.refresh:
|
||||||
qrexec_repoquery(args, app, refresh=True)
|
qrexec_repoquery(p_args, app, refresh=True)
|
||||||
|
|
||||||
if args.operation == 'download':
|
if p_args.operation == 'download':
|
||||||
download(args, app)
|
download(p_args, app)
|
||||||
elif args.operation == 'install':
|
elif p_args.operation == 'install':
|
||||||
install(args, app)
|
install(p_args, app)
|
||||||
elif args.operation == 'reinstall':
|
elif p_args.operation == 'reinstall':
|
||||||
install(args, app, version_selector=VersionSelector.REINSTALL,
|
install(p_args, app, version_selector=VersionSelector.REINSTALL,
|
||||||
override_existing=True)
|
override_existing=True)
|
||||||
elif args.operation == 'downgrade':
|
elif p_args.operation == 'downgrade':
|
||||||
install(args, app, version_selector=VersionSelector.LATEST_LOWER,
|
install(p_args, app, version_selector=VersionSelector.LATEST_LOWER,
|
||||||
override_existing=True)
|
override_existing=True)
|
||||||
elif args.operation == 'upgrade':
|
elif p_args.operation == 'upgrade':
|
||||||
install(args, app, version_selector=VersionSelector.LATEST_HIGHER,
|
install(p_args, app, version_selector=VersionSelector.LATEST_HIGHER,
|
||||||
override_existing=True)
|
override_existing=True)
|
||||||
elif args.operation == 'list':
|
elif p_args.operation == 'list':
|
||||||
list_templates(args, app, 'list')
|
list_templates(p_args, app, 'list')
|
||||||
elif args.operation == 'info':
|
elif p_args.operation == 'info':
|
||||||
list_templates(args, app, 'info')
|
list_templates(p_args, app, 'info')
|
||||||
elif args.operation == 'search':
|
elif p_args.operation == 'search':
|
||||||
search(args, app)
|
search(p_args, app)
|
||||||
elif args.operation == 'remove':
|
elif p_args.operation == 'remove':
|
||||||
remove(args, app)
|
remove(p_args, app)
|
||||||
elif args.operation == 'clean':
|
elif p_args.operation == 'clean':
|
||||||
clean(args, app)
|
clean(p_args, app)
|
||||||
else:
|
else:
|
||||||
parser.error('Operation \'%s\' not supported.' % args.operation)
|
parser.error('Operation \'%s\' not supported.' % p_args.operation)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user