Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve qvm-ls options to avoid parsing #301

Merged
merged 5 commits into from
Aug 24, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 203 additions & 34 deletions qubesadmin/tools/qvm_ls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 '-'),
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
doc='Maximum memory allocatable to VM')

Column('STATE',
attr=(lambda vm: vm.get_power_state()),
Expand Down Expand Up @@ -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).'''
Expand Down Expand Up @@ -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)
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
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)
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
self.spinner.hide()
qubesadmin.tools.print_table(table_data, stream=stream)
else:
Expand All @@ -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'),
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
'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'),
Expand Down Expand Up @@ -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()
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved


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',
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
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='+',
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
metavar='TEMPLATE', action='store',
help='filter results to the AppVMs based on the TEMPLATE. '
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
'"" means None')
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved

parser_filter.add_argument('--conn-netvm', nargs='+',
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
metavar='NETVM', action='store',
help='filter results to the VMs connecting via NETVM')
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved

parser_filter.add_argument('--internal', metavar='<y|n|both>',
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')
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved

parser_filter.add_argument('--servicevm', metavar='<y|n|both>',
default='both', action='store', choices=['y', 'yes', 'n', 'no', 'both'],
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
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')
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved

parser_filter.add_argument('--prefs', nargs='+', metavar='PREFERENCE=VALUE',
action='store',
help='filter results to VMs with all of specified preferences. '
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
'omitted VALUE means None. "" means blank')
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved

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',
Expand Down Expand Up @@ -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)
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
domains = [d for d in domains if d.klass in args.klass]

if args.label:
# filter only VMs with specific label(s)
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
domains_labeled = []
spinner.show('Filtering based on labels...')
for dom in domains:
if dom.label.name in args.label:
domains_labeled.append(dom)
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
spinner.update()
domains = domains_labeled
spinner.hide()

if args.tags:
# filter only VMs having at least one of the specified tags
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
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
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
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
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
child_domains = []
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
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)
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
spinner.update()
domains = child_domains
spinner.hide()

if args.conn_netvm:
# Filter only VMs connecting with specific netvm
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
domains_connecting = []
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
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)
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
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)]
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved

if args.features:
# Filter only VMs with specified features
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
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]
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved

if args.prefs:
# Filter only VMs with specified preferences
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
for pref in args.prefs:
try:
key, value = pref.split('=', 1)
except ValueError:
parser.error("Invalid argument --prefs {}".format(pref))
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
if not key:
parser.error("Invalid argument --prefs {}".format(pref))
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved
if value == '':
value = None
elif value in ['\'\'', '""']:
value = ''
domains = [d for d in domains if str(getattr(d, key, None))==value]
alimirjamali marked this conversation as resolved.
Show resolved Hide resolved

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
Expand Down