From 1d70d934612c5467ca400eded429418f2409bb95 Mon Sep 17 00:00:00 2001 From: Ali Mirjamali Date: Thu, 25 Jul 2024 19:29:04 +0330 Subject: [PATCH 1/5] Improve qvm-ls options to avoid parsing fixes: https://github.com/QubesOS/qubes-issues/issues/8614 Filtering, Formatting & Sorting ArgumentParser Groups Sorting options based on output columns A new pref(erence) format Dedicated filter options for class, label, template, netvm preferences Dedicated filter options for internal,servicevm & updates-available Generic filtering options for preferences and features --- qubesadmin/tools/qvm_ls.py | 237 +++++++++++++++++++++++++++++++------ 1 file changed, 203 insertions(+), 34 deletions(-) diff --git a/qubesadmin/tools/qvm_ls.py b/qubesadmin/tools/qvm_ls.py index 070526970..0575d49f6 100644 --- a/qubesadmin/tools/qvm_ls.py +++ b/qubesadmin/tools/qvm_ls.py @@ -330,7 +330,10 @@ def calc_used(vm, volume_name): return '{}%'.format(usage * 100 // size) -# todo maxmem +# Show hyphen if maxmmem is zero (for HVMs) +Column('MAXMEM', + attr=(lambda vm: vm.maxmem if vm.virt_mode != "hvm" else '-'), + doc='Maximum memory allocatable to VM') Column('STATE', attr=(lambda vm: vm.get_power_state()), @@ -390,13 +393,17 @@ class Table(object): :param list colnames: Names of the columns (need not to be uppercase). ''' def __init__(self, domains, colnames, spinner, raw_data=False, - tree_sorted=False): + tree_sorted=False, sort_order='NAME', reverse_sort=False, + ignore_case=False): self.domains = domains self.columns = tuple(Column.columns[col.upper().replace('_', '-')] for col in colnames) self.spinner = spinner self.raw_data = raw_data self.tree_sorted = tree_sorted + self.sort_order = sort_order + self.reverse_sort = reverse_sort + self.ignore_case = ignore_case def get_head(self): '''Get table head data (all column heads).''' @@ -479,6 +486,18 @@ def write_table(self, stream=sys.stdout): table_data.append(self.get_row(vm)) except qubesadmin.exc.QubesVMNotFoundError: continue + if self.sort_order in self.get_head(): + sort_index = self.get_head().index(self.sort_order) + if self.ignore_case: + table_data[1:] = \ + sorted(table_data[1:], + key=(lambda row: row[sort_index].upper()), + reverse=self.reverse_sort) + else: + table_data[1:] = \ + sorted(table_data[1:], + key=(lambda row: row[sort_index]), + reverse=self.reverse_sort) self.spinner.hide() qubesadmin.tools.print_table(table_data, stream=stream) else: @@ -496,6 +515,8 @@ def write_table(self, stream=sys.stdout): 'kernel': ('name', 'state', 'class', 'template', 'kernel', 'kernelopts'), 'full': ('name', 'state', 'class', 'label', 'qid', 'xid', 'uuid'), # 'perf': ('name', 'state', 'cpu', 'memory'), + 'pref': ('name', 'label', 'template', 'netvm', + 'vcpus', 'initialmem', 'maxmem', 'virt_mode'), 'disk': ('name', 'state', 'disk', 'priv-curr', 'priv-max', 'priv-used', 'root-curr', 'root-max', 'root-used'), @@ -594,60 +615,115 @@ def get_parser(): wrapper.fill(', '.join(sorted(formats.keys()))), wrapper.fill(', '.join(sorted(sorted(Column.columns.keys())))))) - parser.add_argument('--help-columns', action=_HelpColumnsAction) - parser.add_argument('--help-formats', action=_HelpFormatsAction) + parser_format = parser.add_argument_group(title='formatting options') + parser_format_group = parser_format.add_mutually_exclusive_group() - - parser_formats = parser.add_mutually_exclusive_group() - - parser_formats.add_argument('--format', '-o', metavar='FORMAT', + parser_format_group.add_argument('--format', '-o', metavar='FORMAT', action='store', choices=formats.keys(), default='simple', help='preset format') - parser_formats.add_argument('--fields', '-O', metavar='FIELD,...', + parser_format_group.add_argument('--fields', '-O', metavar='FIELD,...', action='store', help='user specified format (see available columns below)') - - parser.add_argument('--tags', nargs='+', metavar='TAG', - help='show only VMs having specific tag(s)') - - for pwrstate in DOMAIN_POWER_STATES: - parser.add_argument('--{}'.format(pwrstate), action='store_true', - help='show {} VMs'.format(pwrstate)) - - parser.add_argument('--raw-data', action='store_true', - help='Display specify data of specified VMs. Intended for ' - 'bash-parsing.') - - parser.add_argument('--tree', '-t', + parser_format.add_argument('--tree', '-t', action='store_const', const='tree', help='sort domain list as network tree') - parser.add_argument('--spinner', - action='store_true', dest='spinner', - help='reenable spinner') - - parser.add_argument('--no-spinner', - action='store_false', dest='spinner', - help='disable spinner') + parser_format.add_argument('--raw-data', action='store_true', + help='Display specify data of specified VMs. Intended for ' + 'bash-parsing.') # shortcuts, compatibility with Qubes 3.2 - parser.add_argument('--raw-list', action='store_true', + parser_format.add_argument('--raw-list', action='store_true', help='Same as --raw-data --fields=name') - parser.add_argument('--disk', '-d', + parser_format_group.add_argument('--disk', '-d', action='store_const', dest='format', const='disk', help='Same as --format=disk') - parser.add_argument('--network', '-n', + parser_format_group.add_argument('--network', '-n', action='store_const', dest='format', const='network', help='Same as --format=network') - parser.add_argument('--kernel', '-k', + parser_format_group.add_argument('--kernel', '-k', action='store_const', dest='format', const='kernel', help='Same as --format=kernel') + parser_format.add_argument('--help-formats', action=_HelpFormatsAction) + parser_format.add_argument('--help-columns', action=_HelpColumnsAction) + + parser_filter = parser.add_argument_group(title='filtering options') + + parser_filter.add_argument('--class', nargs='+', metavar='CLASS', + dest='klass', action='store', + help='show only VMs of specific class(es)') + + parser_filter.add_argument('--label', nargs='+', metavar='LABEL', + action='store', + help='show only VMs with specific label(s)') + + parser_filter.add_argument('--tags', nargs='+', metavar='TAG', + help='show only VMs having specific tag(s)') + + parser_filter.add_argument('--no-tags', nargs='+', metavar='TAG', + help='exclude VMs having specific tag(s)') + + for pwstate in DOMAIN_POWER_STATES: + parser_filter.add_argument('--{}'.format(pwstate), action='store_true', + help='show {} VMs'.format(pwstate)) + + parser_filter.add_argument('--based-on', nargs='+', + metavar='TEMPLATE', action='store', + help='filter results to the AppVMs based on the TEMPLATE. ' + '"" means None') + + parser_filter.add_argument('--conn-netvm', nargs='+', + metavar='NETVM', action='store', + help='filter results to the VMs connecting via NETVM') + + parser_filter.add_argument('--internal', metavar='', + default='both', action='store', choices=['y', 'yes', 'n', 'no', 'both'], + help='show only internal VMs or option to hide them. ' + 'default is showing both regular & internal VMs') + + parser_filter.add_argument('--servicevm', metavar='', + default='both', action='store', choices=['y', 'yes', 'n', 'no', 'both'], + help='show only Service VMs or option to hide them. ' + 'default is showing both regular & Service VMs') + + parser_filter.add_argument('--pending-update', action='store_true', + help='filter results to VMs pending for update') + + parser_filter.add_argument('--features', nargs='+', metavar='FEATURE=VALUE', + action='store', + help='filter results to VMs with all of specified features. ' + 'omitted VALUE means None. "" means blank') + + parser_filter.add_argument('--prefs', nargs='+', metavar='PREFERENCE=VALUE', + action='store', + help='filter results to VMs with all of specified preferences. ' + 'omitted VALUE means None. "" means blank') + + parser_sort = parser.add_argument_group(title='sorting options') + + parser_sort.add_argument('--sort', metavar='COLUMN', action='store', + default='NAME', help='Sort based on provided column rather than NAME') + + parser_sort.add_argument('--reverse', action='store_true', default=False, + help='Reverse sort') + + parser_sort.add_argument('--ignore-case', action='store_true', + default=False, help='Ignore case distinctions for sorting') + + parser.add_argument('--spinner', + action='store_true', dest='spinner', + help='reenable spinner') + + parser.add_argument('--no-spinner', + action='store_false', dest='spinner', + help='disable spinner') + parser.set_defaults(spinner=True) # parser.add_argument('--conf', '-c', @@ -710,16 +786,109 @@ def main(args=None, app=None): vm for vm in args.app.domains if vm.name not in args.exclude ] + if args.klass: + # filter only VMs to specific class(es) + domains = [d for d in domains if d.klass in args.klass] + + if args.label: + # filter only VMs with specific label(s) + domains_labeled = [] + spinner.show('Filtering based on labels...') + for dom in domains: + if dom.label.name in args.label: + domains_labeled.append(dom) + spinner.update() + domains = domains_labeled + spinner.hide() + if args.tags: # filter only VMs having at least one of the specified tags domains = [dom for dom in domains if set(dom.tags).intersection(set(args.tags))] + if args.no_tags: + # exclude VMs having at least one of the specified tags + domains = [dom for dom in domains + if not set(dom.tags).intersection(set(args.no_tags))] + + if args.based_on: + # Filter only VMs based on specific TemplateVM + child_domains = [] + spinner.show('Filtering results to VMs based on their template...') + for dom in domains: + if getattr(dom, 'template', '') in args.based_on: + child_domains.append(dom) + spinner.update() + domains = child_domains + spinner.hide() + + if args.conn_netvm: + # Filter only VMs connecting with specific netvm + domains_connecting = [] + spinner.show('Filtering results to VMs based on their netvm...') + for dom in domains: + if getattr(dom, 'netvm', '') in args.conn_netvm: + domains_connecting.append(dom) + spinner.update() + domains = domains_connecting + spinner.hide() + + if args.internal in ['y', 'yes']: + domains = [d for d in domains if d.features.get('internal', None) + in ['1', 'true', 'True']] + elif args.internal in ['n', 'no']: + domains = [d for d in domains if not d.features.get('internal', None) + in ['1', 'true', 'True']] + + if args.servicevm in ['y', 'yes']: + domains = [d for d in domains if d.features.get('servicevm', None) + in ['1', 'true', 'True']] + elif args.servicevm in ['n', 'no']: + domains = [d for d in domains if not d.features.get('servicevm', None) + in ['1', 'true', 'True']] + + if args.pending_update: + domains = [d for d in domains if + d.features.get('updates-available', None)] + + if args.features: + # Filter only VMs with specified features + for feature in args.features: + try: + key, value = feature.split('=', 1) + except ValueError: + parser.error("Invalid argument --features {}".format(feature)) + if not key: + parser.error("Invalid argument --features {}".format(feature)) + if value == '': + value = None + elif value in ['\'\'', '""']: + value = '' + domains = [d for d in domains if d.features.get(key, None) == value] + + if args.prefs: + # Filter only VMs with specified preferences + for pref in args.prefs: + try: + key, value = pref.split('=', 1) + except ValueError: + parser.error("Invalid argument --prefs {}".format(pref)) + if not key: + parser.error("Invalid argument --prefs {}".format(pref)) + if value == '': + value = None + elif value in ['\'\'', '""']: + value = '' + domains = [d for d in domains if str(getattr(d, key, None))==value] + pwrstates = {state: getattr(args, state) for state in DOMAIN_POWER_STATES} domains = [d for d in domains if matches_power_states(d, **pwrstates)] - table = Table(domains, columns, spinner, args.raw_data, args.tree) + table = Table(domains=domains, colnames=columns, spinner=spinner, + raw_data=args.raw_data, tree_sorted=args.tree, + sort_order=args.sort.upper(), reverse_sort=args.reverse, + ignore_case=args.ignore_case) table.write_table(sys.stdout) return 0 From 1d8d4bdcdcf3d704e97612c53603d2c6241faa15 Mon Sep 17 00:00:00 2001 From: Ali Mirjamali Date: Fri, 26 Jul 2024 14:50:31 +0330 Subject: [PATCH 2/5] Add requested changes for qvm-ls, numeric sort Requested changes by ben-grande (#301) Numeric sort if applicable --- qubesadmin/tools/qvm_ls.py | 138 ++++++++++++++++++++----------------- 1 file changed, 74 insertions(+), 64 deletions(-) diff --git a/qubesadmin/tools/qvm_ls.py b/qubesadmin/tools/qvm_ls.py index 0575d49f6..a8b1c7cc9 100644 --- a/qubesadmin/tools/qvm_ls.py +++ b/qubesadmin/tools/qvm_ls.py @@ -330,10 +330,7 @@ def calc_used(vm, volume_name): return '{}%'.format(usage * 100 // size) -# Show hyphen if maxmmem is zero (for HVMs) -Column('MAXMEM', - attr=(lambda vm: vm.maxmem if vm.virt_mode != "hvm" else '-'), - doc='Maximum memory allocatable to VM') +# todo maxmem Column('STATE', attr=(lambda vm: vm.get_power_state()), @@ -385,6 +382,10 @@ def calc_used(vm, volume_name): FlagsColumn() +# Sorting columns based on numeric or string (default) values +SORT_NUMERIC = ['MEMORY', 'DISK', 'PRIV-CURR', 'PRIV-MAX', 'ROOT-CURR', 'XID', \ + 'ROOT-MAX', 'MAXMEM', 'QREXEC-TIMEOUT', 'SHUTDOWN-TIMEOUT', \ + 'VCPUS', 'PRIV-USED', 'ROOT-USED'] class Table(object): '''Table that is displayed to the user. @@ -464,11 +465,20 @@ def sort_to_tree(self, domains): return tree def write_table(self, stream=sys.stdout): - '''Write whole table to file-like object. + '''Sort & write whole table to file-like object. :param file stream: Stream to write the table to. ''' + def sort_string(field: str) -> str: + return field.upper() if self.ignore_case else field + + def sort_numeric(field: str) -> int: + try: + return int(field[:-1] if field.endswith('%') else field) + except ValueError: + return 0 + table_data = [] if not self.raw_data: self.spinner.show('please wait...') @@ -486,18 +496,19 @@ def write_table(self, stream=sys.stdout): table_data.append(self.get_row(vm)) except qubesadmin.exc.QubesVMNotFoundError: continue - if self.sort_order in self.get_head(): - sort_index = self.get_head().index(self.sort_order) - if self.ignore_case: - table_data[1:] = \ - sorted(table_data[1:], - key=(lambda row: row[sort_index].upper()), - reverse=self.reverse_sort) - else: - table_data[1:] = \ - sorted(table_data[1:], - key=(lambda row: row[sort_index]), - reverse=self.reverse_sort) + + if self.sort_order in SORT_NUMERIC: + sort_key = sort_numeric + else: + sort_key = sort_string + + titles = self.get_head() + if self.sort_order in titles: + # Sorting is currently possible if key is in actual output + col_index = titles.index(self.sort_order) + table_data[1:] = sorted(table_data[1:], key=lambda \ + row: sort_key(row[col_index]), + reverse=self.reverse_sort) self.spinner.hide() qubesadmin.tools.print_table(table_data, stream=stream) else: @@ -515,7 +526,7 @@ def write_table(self, stream=sys.stdout): 'kernel': ('name', 'state', 'class', 'template', 'kernel', 'kernelopts'), 'full': ('name', 'state', 'class', 'label', 'qid', 'xid', 'uuid'), # 'perf': ('name', 'state', 'cpu', 'memory'), - 'pref': ('name', 'label', 'template', 'netvm', + 'prefs': ('name', 'label', 'template', 'netvm', 'vcpus', 'initialmem', 'maxmem', 'virt_mode'), 'disk': ('name', 'state', 'disk', 'priv-curr', 'priv-max', 'priv-used', @@ -616,13 +627,13 @@ def get_parser(): wrapper.fill(', '.join(sorted(sorted(Column.columns.keys())))))) parser_format = parser.add_argument_group(title='formatting options') - parser_format_group = parser_format.add_mutually_exclusive_group() + parser_format_exclusive = parser_format.add_mutually_exclusive_group() - parser_format_group.add_argument('--format', '-o', metavar='FORMAT', + parser_format_exclusive.add_argument('--format', '-o', metavar='FORMAT', action='store', choices=formats.keys(), default='simple', help='preset format') - parser_format_group.add_argument('--fields', '-O', metavar='FIELD,...', + parser_format_exclusive.add_argument('--fields', '-O', metavar='FIELD,...', action='store', help='user specified format (see available columns below)') @@ -638,15 +649,15 @@ def get_parser(): parser_format.add_argument('--raw-list', action='store_true', help='Same as --raw-data --fields=name') - parser_format_group.add_argument('--disk', '-d', + parser_format_exclusive.add_argument('--disk', '-d', action='store_const', dest='format', const='disk', help='Same as --format=disk') - parser_format_group.add_argument('--network', '-n', + parser_format_exclusive.add_argument('--network', '-n', action='store_const', dest='format', const='network', help='Same as --format=network') - parser_format_group.add_argument('--kernel', '-k', + parser_format_exclusive.add_argument('--kernel', '-k', action='store_const', dest='format', const='kernel', help='Same as --format=kernel') @@ -657,58 +668,56 @@ def get_parser(): parser_filter.add_argument('--class', nargs='+', metavar='CLASS', dest='klass', action='store', - help='show only VMs of specific class(es)') + help='show only qubes of specific class(es)') parser_filter.add_argument('--label', nargs='+', metavar='LABEL', action='store', - help='show only VMs with specific label(s)') + help='show only qubes with specific label(s)') parser_filter.add_argument('--tags', nargs='+', metavar='TAG', - help='show only VMs having specific tag(s)') + help='show only qubes having specific tag(s)') - parser_filter.add_argument('--no-tags', nargs='+', metavar='TAG', - help='exclude VMs having specific tag(s)') + parser_filter.add_argument('--exclude-tags', nargs='+', metavar='TAG', + help='exclude qubes having specific tag(s)') for pwstate in DOMAIN_POWER_STATES: parser_filter.add_argument('--{}'.format(pwstate), action='store_true', help='show {} VMs'.format(pwstate)) - parser_filter.add_argument('--based-on', nargs='+', + parser_filter.add_argument('--template-source', nargs='+', metavar='TEMPLATE', action='store', - help='filter results to the AppVMs based on the TEMPLATE. ' + help='filter results to the qubes based on the TEMPLATE. ' '"" means None') - parser_filter.add_argument('--conn-netvm', nargs='+', + parser_filter.add_argument('--netvm-is', nargs='+', metavar='NETVM', action='store', - help='filter results to the VMs connecting via NETVM') + help='filter results to the qubes connecting via NETVM') - parser_filter.add_argument('--internal', metavar='', - default='both', action='store', choices=['y', 'yes', 'n', 'no', 'both'], - help='show only internal VMs or option to hide them. ' - 'default is showing both regular & internal VMs') + parser_filter.add_argument('--internal', metavar='', + default='both', action='store', choices=['y', 'yes', 'n', 'no'], + help='show only internal qubes or option to hide them') - parser_filter.add_argument('--servicevm', metavar='', - default='both', action='store', choices=['y', 'yes', 'n', 'no', 'both'], - help='show only Service VMs or option to hide them. ' - 'default is showing both regular & Service VMs') + parser_filter.add_argument('--servicevm', metavar='', + default='both', action='store', choices=['y', 'yes', 'n', 'no'], + help='show only Service VMs or option to hide them') parser_filter.add_argument('--pending-update', action='store_true', - help='filter results to VMs pending for update') + help='filter results to qubes pending for update') parser_filter.add_argument('--features', nargs='+', metavar='FEATURE=VALUE', action='store', - help='filter results to VMs with all of specified features. ' + help='filter results to qubes that matches all specified features. ' 'omitted VALUE means None. "" means blank') parser_filter.add_argument('--prefs', nargs='+', metavar='PREFERENCE=VALUE', action='store', - help='filter results to VMs with all of specified preferences. ' + help='filter results to qubes that matches all specified preferences. ' 'omitted VALUE means None. "" means blank') parser_sort = parser.add_argument_group(title='sorting options') parser_sort.add_argument('--sort', metavar='COLUMN', action='store', - default='NAME', help='Sort based on provided column rather than NAME') + default='NAME', help='sort based on provided column rather than NAME') parser_sort.add_argument('--reverse', action='store_true', default=False, help='Reverse sort') @@ -806,31 +815,31 @@ def main(args=None, app=None): domains = [dom for dom in domains if set(dom.tags).intersection(set(args.tags))] - if args.no_tags: + if args.exclude_tags: # exclude VMs having at least one of the specified tags domains = [dom for dom in domains - if not set(dom.tags).intersection(set(args.no_tags))] + if not set(dom.tags).intersection(set(args.exclude_tags))] - if args.based_on: + if args.template_source: # Filter only VMs based on specific TemplateVM - child_domains = [] - spinner.show('Filtering results to VMs based on their template...') + domains_template = [] + spinner.show('Filtering results to qubes based on their templates...') for dom in domains: - if getattr(dom, 'template', '') in args.based_on: - child_domains.append(dom) + if getattr(dom, 'template', '') in args.template_source: + domains_template.append(dom) spinner.update() - domains = child_domains + domains = domains_template spinner.hide() - if args.conn_netvm: + if args.netvm_is: # Filter only VMs connecting with specific netvm - domains_connecting = [] - spinner.show('Filtering results to VMs based on their netvm...') + domains_netvm = [] + spinner.show('Filtering results to qubes based on their netvm...') for dom in domains: - if getattr(dom, 'netvm', '') in args.conn_netvm: - domains_connecting.append(dom) + if getattr(dom, 'netvm', '') in args.netvm_is: + domains_netvm.append(dom) spinner.update() - domains = domains_connecting + domains = domains_netvm spinner.hide() if args.internal in ['y', 'yes']: @@ -857,9 +866,9 @@ def main(args=None, app=None): try: key, value = feature.split('=', 1) except ValueError: - parser.error("Invalid argument --features {}".format(feature)) + parser.error("Invalid argument: --features {}".format(feature)) if not key: - parser.error("Invalid argument --features {}".format(feature)) + parser.error("Invalid argument: --features {}".format(feature)) if value == '': value = None elif value in ['\'\'', '""']: @@ -872,14 +881,15 @@ def main(args=None, app=None): try: key, value = pref.split('=', 1) except ValueError: - parser.error("Invalid argument --prefs {}".format(pref)) + parser.error("Invalid argument: --prefs {}".format(pref)) if not key: - parser.error("Invalid argument --prefs {}".format(pref)) + parser.error("Invalid argument: --prefs {}".format(pref)) if value == '': value = None elif value in ['\'\'', '""']: value = '' - domains = [d for d in domains if str(getattr(d, key, None))==value] + domains = [d for d in domains + if str(getattr(d, key, None)) == value] pwrstates = {state: getattr(args, state) for state in DOMAIN_POWER_STATES} domains = [d for d in domains From bd26a0c11130633acde2d0d6dda554ce3c6a5198 Mon Sep 17 00:00:00 2001 From: Ali Mirjamali Date: Fri, 26 Jul 2024 16:37:26 +0330 Subject: [PATCH 3/5] Update documentation for new qvm-ls options Follow-up to #301 Options are grouped as general, formatting, filtering and sorting --- doc/manpages/qvm-ls.rst | 123 +++++++++++++++++++++++++++++-------- qubesadmin/tools/qvm_ls.py | 4 +- 2 files changed, 99 insertions(+), 28 deletions(-) diff --git a/doc/manpages/qvm-ls.rst b/doc/manpages/qvm-ls.rst index 4f4d856e1..ac51584d9 100644 --- a/doc/manpages/qvm-ls.rst +++ b/doc/manpages/qvm-ls.rst @@ -8,20 +8,27 @@ Synopsis :command:`qvm-ls` [-h] [--verbose] [--quiet] [--help-columns] [--help-formats] [--format *FORMAT* | --fields *FIELD*,...] [--tags *TAG* [*TAG* ...]] [--running] [--paused] [--halted] -Options -------- +Positional arguments +-------------------- + +.. option:: VMNAME + + Zero or more domain names + +General options +--------------- .. option:: --help, -h Show help message and exit -.. option:: --help-columns +.. option:: --verbose, -v - List all available columns with short descriptions and exit. + Increase verbosity. -.. option:: --help-formats +.. option:: --quiet, -q - List all available formats with their definitions and exit. + Decrease verbosity. .. option:: --all @@ -32,6 +39,17 @@ Options Exclude the qube from --all. You need to use --all option explicitly to use --exclude. +.. option:: --spinner + + Have a spinner spinning while the spinning mainloop spins new table cells. + +.. option:: --no-spinner + + No spinner today. + +Formatting options +------------------ + .. option:: --format=FORMAT, -o FORMAT Sets format to a list of columns defined by preset. All formats along with @@ -43,14 +61,10 @@ Options :option:`--format`. All columns along with short descriptions can be listed with :option:`--help-columns`. -.. option:: --tags TAG ... - - Shows only VMs having specific tag(s). - -.. option:: --running, --paused, --halted +.. option:: --tree, -t - Shows only VMs matching the specified power state(s). When none of these - options is used (default), all VMs are shown. + List domains as a network tree. Domains are sorted as they are connected to + their netvms. Names are indented relative to the number of connected netvms. .. option:: --raw-data @@ -62,11 +76,6 @@ Options Give plain list of VM names, without header or separator. Useful in scripts. Same as --raw-data --fields=name -.. option:: --tree, -t - - List domains as a network tree. Domains are sorted as they are connected to - their netvms. Names are indented relative to the number of connected netvms. - .. option:: --disk, -d Same as --format=disk, for compatibility with Qubes 3.x @@ -79,21 +88,83 @@ Options Same as --format=kernel, for compatibility with Qubes 3.x -.. option:: --verbose, -v +.. option:: --help-columns - Increase verbosity. + List all available columns with short descriptions and exit. -.. option:: --quiet, -q +.. option:: --help-formats - Decrease verbosity. + List all available formats with their definitions and exit. -.. option:: --spinner +Filtering options +----------------- - Have a spinner spinning while the spinning mainloop spins new table cells. +.. option:: --class CLASS ... -.. option:: --no-spinner + Show only qubes of specific class(es) - No spinner today. +.. option:: --label LABEL ... + + Show only qubes with specific label(s) + +.. option:: --tags TAG ... + + Shows only VMs having specific tag(s). + +.. option:: --exclude-tags TAG ... + + Exclude VMs having specific tag(s). + +.. option:: --running, --paused, --halted + + Shows only VMs matching the specified power state(s). When none of these + options is used (default), all VMs are shown. + +.. option:: --template-source TEMPLATE ... + + Filter results to the qubes based on the TEMPLATE(s) + +.. option:: --netvm-is NETVM ... + + Filter results to the qubes connecting via NETVM(s) + +.. option:: --internal + + Show only internal qubes or exclude them from output + +.. option:: --servicevm + + Show only servicevms or exclude them from output + +.. option:: --pending-update + + Filter results to qubes pending for update + +.. option:: --features FEATURE=VALUE ... + + Filter results to qubes that match all specified features. Omitted VALUE + means None (not set). "" or '' means blank + +.. option:: --prefs PREFERENCE=VALUE ... + + Filter results to qubes that match all specified preferences. Omitted VALUE + means None (not set). "" or '' means blank + +Sorting options +--------------- + +.. option:: --sort COLUMN + + Sort based on provided column rather than NAME. Sort key should be in the + output columns + +.. option:: --reverse + + Reverse sort + +.. option:: --ignore-case + + Ignore case distinctions for sorting Authors ------- diff --git a/qubesadmin/tools/qvm_ls.py b/qubesadmin/tools/qvm_ls.py index a8b1c7cc9..74e58534b 100644 --- a/qubesadmin/tools/qvm_ls.py +++ b/qubesadmin/tools/qvm_ls.py @@ -720,10 +720,10 @@ def get_parser(): default='NAME', help='sort based on provided column rather than NAME') parser_sort.add_argument('--reverse', action='store_true', default=False, - help='Reverse sort') + help='reverse sort') parser_sort.add_argument('--ignore-case', action='store_true', - default=False, help='Ignore case distinctions for sorting') + default=False, help='ignore case distinctions for sorting') parser.add_argument('--spinner', action='store_true', dest='spinner', From 54736f2d1bce191613ada4e79e8385750115c482 Mon Sep 17 00:00:00 2001 From: Ali Mirjamali Date: Sat, 27 Jul 2024 00:54:06 +0330 Subject: [PATCH 4/5] Update unittests for new qvm-ls options Follow-up to #301 Plus minor fix for documentation --- doc/manpages/qvm-ls.rst | 18 +- qubesadmin/tests/tools/qvm_ls.py | 352 +++++++++++++++++++++++++++++++ 2 files changed, 368 insertions(+), 2 deletions(-) diff --git a/doc/manpages/qvm-ls.rst b/doc/manpages/qvm-ls.rst index ac51584d9..a85f01086 100644 --- a/doc/manpages/qvm-ls.rst +++ b/doc/manpages/qvm-ls.rst @@ -6,12 +6,26 @@ Synopsis -------- -:command:`qvm-ls` [-h] [--verbose] [--quiet] [--help-columns] [--help-formats] [--format *FORMAT* | --fields *FIELD*,...] [--tags *TAG* [*TAG* ...]] [--running] [--paused] [--halted] +:command:`qvm-ls` [--verbose] [--quiet] [--help] [--all] + [--exclude *EXCLUDE*] [--spinner] [--no-spinner] + [--format *FORMAT* | --fields *FIELD* [*FIELD* ...] | --disk | --network | --kernel] + [--tree] [--raw-data] [--raw-list] [--help-formats] + [--help-columns] [--class *CLASS* [*CLASS* ...]] + [--label *LABEL* [*LABEL* ...]] [--tags *TAG* [*TAG* ...]] + [--exclude-tags *TAG* [*TAG* ...]] [--running] [--paused] + [--halted] [--template-source *TEMPLATE* [*TEMPLATE* ...]] + [--netvm-is *NETVM* [*NETVM* ...]] [--internal ] + [--servicevm ] [--pending-update] + [--features *FEATURE=VALUE* [*FEATURE=VALUE* ...]] + [--prefs *PREFERENCE=VALUE* [*PREFERENCE=VALUE* ...]] + [--sort *COLUMN*] [--reverse] [--ignore-case] + [VMNAME ...] + Positional arguments -------------------- -.. option:: VMNAME +.. option:: VMNAME ... Zero or more domain names diff --git a/qubesadmin/tests/tools/qvm_ls.py b/qubesadmin/tests/tools/qvm_ls.py index ab2757587..81d375674 100644 --- a/qubesadmin/tests/tools/qvm_ls.py +++ b/qubesadmin/tests/tools/qvm_ls.py @@ -234,6 +234,14 @@ def test_100_tags_nomatch(self): self.assertEqual(stdout.getvalue(), 'NAME STATE CLASS LABEL TEMPLATE NETVM\n') + def test_100_exclude_tag(self): + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + qubesadmin.tools.qvm_ls.main(['--exclude-tags', 'not-my'], \ + app=self.app) + self.assertEqual(stdout.getvalue(), + 'NAME STATE CLASS LABEL TEMPLATE NETVM\n' + 'dom0 Running TestVM black - -\n') + class TC_80_Power_state_filters(qubesadmin.tests.QubesTestCase): def setUp(self): @@ -355,3 +363,347 @@ def test_101_list_selected(self): 'sys-net Running AppVM red template1 sys-net\n' 'vm1 Running AppVM green template1 sys-net\n') self.assertAllCalled() + +class TC_100_Sort(qubesadmin.tests.QubesTestCase): + def setUp(self): + self.app = TestApp() + self.app.domains = TestVMCollection( + [ + ('a', TestVM('a', label='red', maxmem='100')), + ('B', TestVM('B', label='green', maxmem='1000')), + ('c', TestVM('c', label='blue', maxmem='300')), + ('dom0', TestVM('dom0', label='black', maxmem='-')) + ] + ) + + def test_101_sort_string(self): + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + qubesadmin.tools.qvm_ls.main( + ['--sort', 'NAME', '--reverse', '--ignore-case'], app=self.app) + self.assertEqual(stdout.getvalue(), + 'NAME STATE CLASS LABEL TEMPLATE NETVM\n' + 'dom0 Running TestVM black - -\n' + 'c Running TestVM blue - -\n' + 'B Running TestVM green - -\n' + 'a Running TestVM red - -\n') + + def test_102_sort_numeric(self): + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + qubesadmin.tools.qvm_ls.main( + ['--field', 'NAME,MAXMEM', '--sort', 'MAXMEM'], app=self.app) + self.assertEqual(stdout.getvalue(), + 'NAME MAXMEM\n' + 'dom0 -\n' + 'a 100\n' + 'c 300\n' + 'B 1000\n') + + +class TC_110_Filtering(qubesadmin.tests.QubesTestCase): + def test_111_filter_class(self): + self.app.expected_calls[ + ('dom0', 'admin.vm.List', None, None)] = \ + b'0\x00vm1 class=AppVM state=Running\n' \ + b'template1 class=TemplateVM state=Halted\n' \ + b'sys-net class=AppVM state=Running\n' + props = { + 'label': 'type=label green', + 'template': 'type=vm template1', + 'netvm': 'type=vm sys-net', + } + + # setup template1 + props['label'] = 'type=label black' + del props['template'] + self.app.expected_calls[ + ('template1', 'admin.vm.property.GetAll', None, None)] = \ + b'0\x00' + ''.join( + '{} default=True {}\n'.format(key, value) + for key, value in props.items()).encode() + + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + qubesadmin.tools.qvm_ls.main(['--class', 'TemplateVM'], app=self.app) + self.assertEqual(stdout.getvalue(), + 'NAME STATE CLASS LABEL TEMPLATE NETVM\n' + 'template1 Halted TemplateVM black - sys-net\n') + self.assertAllCalled() + + def test_112_filter_label(self): + self.app.expected_calls[ + ('dom0', 'admin.vm.List', None, None)] = \ + b'0\x00template1 class=TemplateVM state=Halted\n' \ + b'sys-net class=AppVM state=Running\n' + props = { + 'label': 'type=label green', + 'template': 'type=vm template1', + 'netvm': 'type=vm sys-net', + } + + # setup sys-net + props['label'] = 'type=label red' + self.app.expected_calls[ + ('sys-net', 'admin.vm.property.GetAll', None, None)] = \ + b'0\x00' + ''.join( + '{} default=True {}\n'.format(key, value) + for key, value in props.items()).encode() + + # setup template1 + props['label'] = 'type=label black' + del props['template'] + self.app.expected_calls[ + ('template1', 'admin.vm.property.GetAll', None, None)] = \ + b'0\x00' + ''.join( + '{} default=True {}\n'.format(key, value) + for key, value in props.items()).encode() + + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + qubesadmin.tools.qvm_ls.main(['--label', 'black'], app=self.app) + self.assertEqual(stdout.getvalue(), + 'NAME STATE CLASS LABEL TEMPLATE NETVM\n' + 'template1 Halted TemplateVM black - sys-net\n') + self.assertAllCalled() + + def test_113_filter_template_source(self): + self.app.expected_calls[ + ('dom0', 'admin.vm.List', None, None)] = \ + b'0\x00template1 class=TemplateVM state=Halted\n' \ + b'sys-net class=AppVM state=Running\n' + props = { + 'label': 'type=label green', + 'template': 'type=vm template1', + 'netvm': 'type=vm sys-net', + } + + # setup sys-net + props['label'] = 'type=label red' + self.app.expected_calls[ + ('sys-net', 'admin.vm.property.GetAll', None, None)] = \ + b'0\x00' + ''.join( + '{} default=True {}\n'.format(key, value) + for key, value in props.items()).encode() + + # setup template1 + props['label'] = 'type=label black' + del props['template'] + self.app.expected_calls[ + ('template1', 'admin.vm.property.GetAll', None, None)] = \ + b'0\x00' + ''.join( + '{} default=True {}\n'.format(key, value) + for key, value in props.items()).encode() + + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + qubesadmin.tools.qvm_ls.main(['--template-source', 'template1'], app=self.app) + self.assertEqual(stdout.getvalue(), + 'NAME STATE CLASS LABEL TEMPLATE NETVM\n' + 'sys-net Running AppVM red template1 sys-net\n') + self.assertAllCalled() + + def test_114_filter_netvm_is(self): + self.app.expected_calls[ + ('dom0', 'admin.vm.List', None, None)] = \ + b'0\x00template1 class=TemplateVM state=Halted\n' \ + b'sys-net class=AppVM state=Running\n' + props = { + 'label': 'type=label green', + 'template': 'type=vm template1', + 'netvm': 'type=vm sys-net', + } + + # setup sys-net + props['label'] = 'type=label red' + self.app.expected_calls[ + ('sys-net', 'admin.vm.property.GetAll', None, None)] = \ + b'0\x00' + ''.join( + '{} default=True {}\n'.format(key, value) + for key, value in props.items()).encode() + + # setup template1 + props['label'] = 'type=label black' + del props['template'] + self.app.expected_calls[ + ('template1', 'admin.vm.property.GetAll', None, None)] = \ + b'0\x00' + ''.join( + '{} default=True {}\n'.format(key, value) + for key, value in props.items()).encode() + + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + qubesadmin.tools.qvm_ls.main(['--netvm-is', 'sys-net'], app=self.app) + self.assertEqual(stdout.getvalue(), + 'NAME STATE CLASS LABEL TEMPLATE NETVM\n' + 'sys-net Running AppVM red template1 sys-net\n' + 'template1 Halted TemplateVM black - sys-net\n') + self.assertAllCalled() + + def test_115_internal_servicevm_pending_updates(self): + self.app.expected_calls[ + ('dom0', 'admin.vm.List', None, None)] = \ + b'0\x00internalvm class=AppVM state=Running\n' \ + b'service-vm class=AppVM state=Running\n' + props = { + 'label': 'type=label green', + 'template': 'type=vm template1', + 'netvm': 'type=vm sys-net', + } + + # setup internalvm + props['label'] = 'type=label red' + self.app.expected_calls[ + ('internalvm', 'admin.vm.property.GetAll', None, None)] = \ + b'0\x00' + ''.join( + '{} default=True {}\n'.format(key, value) + for key, value in props.items()).encode() + + self.app.expected_calls[ + ('internalvm', 'admin.vm.feature.Get', 'internal', None)] = \ + b'0\x00true' + + self.app.expected_calls[ + ('internalvm', 'admin.vm.feature.Get', 'servicevm', None)] = \ + b'0\x00' + + self.app.expected_calls[ + ('internalvm', 'admin.vm.feature.Get', 'updates-available', None)] = \ + b'0\x00true' + + # setup service-vm + props['label'] = 'type=label red' + self.app.expected_calls[ + ('service-vm', 'admin.vm.property.GetAll', None, None)] = \ + b'0\x00' + ''.join( + '{} default=True {}\n'.format(key, value) + for key, value in props.items()).encode() + + self.app.expected_calls[ + ('service-vm', 'admin.vm.feature.Get', 'internal', None)] = \ + b'0\x00' + + self.app.expected_calls[ + ('service-vm', 'admin.vm.feature.Get', 'servicevm', None)] = \ + b'0\x00true' + + self.app.expected_calls[ + ('service-vm', 'admin.vm.feature.Get', 'updates-available', None)] = \ + b'0\x00' + + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + qubesadmin.tools.qvm_ls.main(['--internal', 'y'], app=self.app) + self.assertEqual(stdout.getvalue(), + 'NAME STATE CLASS LABEL TEMPLATE NETVM\n' + 'internalvm Running AppVM red template1 sys-net\n') + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + qubesadmin.tools.qvm_ls.main(['--internal', 'n'], app=self.app) + self.assertEqual(stdout.getvalue(), + 'NAME STATE CLASS LABEL TEMPLATE NETVM\n' + 'service-vm Running AppVM red template1 sys-net\n') + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + qubesadmin.tools.qvm_ls.main(['--servicevm', 'y'], app=self.app) + self.assertEqual(stdout.getvalue(), + 'NAME STATE CLASS LABEL TEMPLATE NETVM\n' + 'service-vm Running AppVM red template1 sys-net\n') + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + qubesadmin.tools.qvm_ls.main(['--servicevm', 'n'], app=self.app) + self.assertEqual(stdout.getvalue(), + 'NAME STATE CLASS LABEL TEMPLATE NETVM\n' + 'internalvm Running AppVM red template1 sys-net\n') + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + qubesadmin.tools.qvm_ls.main(['--pending-update'], app=self.app) + self.assertEqual(stdout.getvalue(), + 'NAME STATE CLASS LABEL TEMPLATE NETVM\n' + 'internalvm Running AppVM red template1 sys-net\n') + self.assertAllCalled() + + def test_116_features(self): + self.app.expected_calls[ + ('dom0', 'admin.vm.List', None, None)] = \ + b'0\x00internalvm class=AppVM state=Running\n' \ + b'service-vm class=AppVM state=Running\n' + props = { + 'label': 'type=label green', + 'template': 'type=vm template1', + 'netvm': 'type=vm sys-net', + } + + # setup internalvm + props['label'] = 'type=label red' + self.app.expected_calls[ + ('internalvm', 'admin.vm.property.GetAll', None, None)] = \ + b'0\x00' + ''.join( + '{} default=True {}\n'.format(key, value) + for key, value in props.items()).encode() + + self.app.expected_calls[ + ('internalvm', 'admin.vm.feature.Get', 'cool-feature', None)] = \ + b'0\x00yes' + + # setup service-vm + props['label'] = 'type=label red' + self.app.expected_calls[ + ('service-vm', 'admin.vm.property.GetAll', None, None)] = \ + b'0\x00' + ''.join( + '{} default=True {}\n'.format(key, value) + for key, value in props.items()).encode() + + self.app.expected_calls[ + ('service-vm', 'admin.vm.feature.Get', 'cool-feature', None)] = \ + b'0\x00' + + with qubesadmin.tests.tools.StderrBuffer(): + with self.assertRaises(SystemExit): + qubesadmin.tools.qvm_ls.main(['--features', 'cool-feature:yes'], app=self.app) + + with qubesadmin.tests.tools.StderrBuffer(): + with self.assertRaises(SystemExit): + qubesadmin.tools.qvm_ls.main(['--features', '=yes'], app=self.app) + + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + qubesadmin.tools.qvm_ls.main(['--features', 'cool-feature=yes'], app=self.app) + self.assertEqual(stdout.getvalue(), + 'NAME STATE CLASS LABEL TEMPLATE NETVM\n' + 'internalvm Running AppVM red template1 sys-net\n') + + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + qubesadmin.tools.qvm_ls.main(['--features', 'cool-feature='], app=self.app) + self.assertEqual(stdout.getvalue(), + 'NAME STATE CLASS LABEL TEMPLATE NETVM\n') + + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + qubesadmin.tools.qvm_ls.main(['--features', 'cool-feature=""'], app=self.app) + self.assertEqual(stdout.getvalue(), + 'NAME STATE CLASS LABEL TEMPLATE NETVM\n' + 'service-vm Running AppVM red template1 sys-net\n') + + self.assertAllCalled() + + def test_117_preferences(self): + self.app = TestApp() + self.app.domains = TestVMCollection( + [ + ('a', TestVM('a', label='red', maxmem='100')), + ('b', TestVM('b', label='green', maxmem='')) + ] + ) + + with qubesadmin.tests.tools.StderrBuffer(): + with self.assertRaises(SystemExit): + qubesadmin.tools.qvm_ls.main(['--prefs', 'maxmem:100'], app=self.app) + + with qubesadmin.tests.tools.StderrBuffer(): + with self.assertRaises(SystemExit): + qubesadmin.tools.qvm_ls.main(['--prefs', ':100'], app=self.app) + + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + qubesadmin.tools.qvm_ls.main(['--prefs', 'maxmem=100'], app=self.app) + self.assertEqual(stdout.getvalue(), + 'NAME STATE CLASS LABEL TEMPLATE NETVM\n' + 'a Running TestVM red - -\n') + + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + qubesadmin.tools.qvm_ls.main(['--prefs', 'maxmem=""'], app=self.app) + self.assertEqual(stdout.getvalue(), + 'NAME STATE CLASS LABEL TEMPLATE NETVM\n' + 'b Running TestVM green - -\n') + + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + qubesadmin.tools.qvm_ls.main(['--prefs', 'non-existent='], app=self.app) + self.assertEqual(stdout.getvalue(), + 'NAME STATE CLASS LABEL TEMPLATE NETVM\n') From 615baf22486bf4e2e57005b0e285b3e71d3cc974 Mon Sep 17 00:00:00 2001 From: Ali Mirjamali Date: Fri, 9 Aug 2024 00:01:58 +0330 Subject: [PATCH 5/5] Improve new qvm-ls documentation & comments --- doc/manpages/qvm-ls.rst | 20 ++++++++++---------- qubesadmin/tools/qvm_ls.py | 24 ++++++++++++------------ 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/doc/manpages/qvm-ls.rst b/doc/manpages/qvm-ls.rst index a85f01086..d82034d5b 100644 --- a/doc/manpages/qvm-ls.rst +++ b/doc/manpages/qvm-ls.rst @@ -1,6 +1,6 @@ .. program:: qvm-ls -:program:`qvm-ls` -- List VMs and various information about them +:program:`qvm-ls` -- List qubes and various information about them ================================================================ Synopsis @@ -77,7 +77,7 @@ Formatting options .. option:: --tree, -t - List domains as a network tree. Domains are sorted as they are connected to + List qubes as a network tree. Qubes are sorted as they are connected to their netvms. Names are indented relative to the number of connected netvms. .. option:: --raw-data @@ -123,16 +123,16 @@ Filtering options .. option:: --tags TAG ... - Shows only VMs having specific tag(s). + Shows only qubes having specific tag(s). .. option:: --exclude-tags TAG ... - Exclude VMs having specific tag(s). + Exclude qubes having specific tag(s). .. option:: --running, --paused, --halted - Shows only VMs matching the specified power state(s). When none of these - options is used (default), all VMs are shown. + Shows only qubes matching the specified power state(s). When none of these + options is used (default), all qubes are shown. .. option:: --template-source TEMPLATE ... @@ -157,19 +157,19 @@ Filtering options .. option:: --features FEATURE=VALUE ... Filter results to qubes that match all specified features. Omitted VALUE - means None (not set). "" or '' means blank + means None (unset). Empty value means "" or '' (blank) .. option:: --prefs PREFERENCE=VALUE ... Filter results to qubes that match all specified preferences. Omitted VALUE - means None (not set). "" or '' means blank + means None (unset). Empty value means "" or '' (blank) Sorting options --------------- .. option:: --sort COLUMN - Sort based on provided column rather than NAME. Sort key should be in the + Sort based on provided column rather than NAME. Sort key must be in the output columns .. option:: --reverse @@ -178,7 +178,7 @@ Sorting options .. option:: --ignore-case - Ignore case distinctions for sorting + Ignore case distinctions when sorting Authors ------- diff --git a/qubesadmin/tools/qvm_ls.py b/qubesadmin/tools/qvm_ls.py index 74e58534b..ee827783c 100644 --- a/qubesadmin/tools/qvm_ls.py +++ b/qubesadmin/tools/qvm_ls.py @@ -699,7 +699,7 @@ def get_parser(): parser_filter.add_argument('--servicevm', metavar='', default='both', action='store', choices=['y', 'yes', 'n', 'no'], - help='show only Service VMs or option to hide them') + help='show only ServiceVMs or option to hide them') parser_filter.add_argument('--pending-update', action='store_true', help='filter results to qubes pending for update') @@ -707,12 +707,12 @@ def get_parser(): parser_filter.add_argument('--features', nargs='+', metavar='FEATURE=VALUE', action='store', help='filter results to qubes that matches all specified features. ' - 'omitted VALUE means None. "" means blank') + 'omitted VALUE means None (unset). Empty value means "" (blank)') parser_filter.add_argument('--prefs', nargs='+', metavar='PREFERENCE=VALUE', action='store', help='filter results to qubes that matches all specified preferences. ' - 'omitted VALUE means None. "" means blank') + 'omitted VALUE means None (unset). Empty value means "" (blank)') parser_sort = parser.add_argument_group(title='sorting options') @@ -723,7 +723,7 @@ def get_parser(): help='reverse sort') parser_sort.add_argument('--ignore-case', action='store_true', - default=False, help='ignore case distinctions for sorting') + default=False, help='ignore case distinctions when sorting') parser.add_argument('--spinner', action='store_true', dest='spinner', @@ -796,11 +796,11 @@ def main(args=None, app=None): ] if args.klass: - # filter only VMs to specific class(es) + # filter only qubes to specific class(es) domains = [d for d in domains if d.klass in args.klass] if args.label: - # filter only VMs with specific label(s) + # filter only qubes with specific label(s) domains_labeled = [] spinner.show('Filtering based on labels...') for dom in domains: @@ -811,17 +811,17 @@ def main(args=None, app=None): spinner.hide() if args.tags: - # filter only VMs having at least one of the specified tags + # filter only qubes having at least one of the specified tags domains = [dom for dom in domains if set(dom.tags).intersection(set(args.tags))] if args.exclude_tags: - # exclude VMs having at least one of the specified tags + # exclude qubes having at least one of the specified tags domains = [dom for dom in domains if not set(dom.tags).intersection(set(args.exclude_tags))] if args.template_source: - # Filter only VMs based on specific TemplateVM + # Filter only qubes based on specific TemplateVM domains_template = [] spinner.show('Filtering results to qubes based on their templates...') for dom in domains: @@ -832,7 +832,7 @@ def main(args=None, app=None): spinner.hide() if args.netvm_is: - # Filter only VMs connecting with specific netvm + # Filter only qubes connecting with specific netvm domains_netvm = [] spinner.show('Filtering results to qubes based on their netvm...') for dom in domains: @@ -861,7 +861,7 @@ def main(args=None, app=None): d.features.get('updates-available', None)] if args.features: - # Filter only VMs with specified features + # Filter only qubes with specified features for feature in args.features: try: key, value = feature.split('=', 1) @@ -876,7 +876,7 @@ def main(args=None, app=None): domains = [d for d in domains if d.features.get(key, None) == value] if args.prefs: - # Filter only VMs with specified preferences + # Filter only qubes with specified preferences for pref in args.prefs: try: key, value = pref.split('=', 1)