diff --git a/doc/manpages/qvm-ls.rst b/doc/manpages/qvm-ls.rst index 4f4d856e..d82034d5 100644 --- a/doc/manpages/qvm-ls.rst +++ b/doc/manpages/qvm-ls.rst @@ -1,27 +1,48 @@ .. program:: qvm-ls -:program:`qvm-ls` -- List VMs and various information about them +:program:`qvm-ls` -- List qubes and various information about them ================================================================ 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 ...] -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 +53,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 +75,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 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 @@ -62,11 +90,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 +102,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 qubes having specific tag(s). + +.. option:: --exclude-tags TAG ... + + Exclude qubes having specific tag(s). + +.. option:: --running, --paused, --halted + + 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 ... + + 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 (unset). Empty value means "" or '' (blank) + +.. option:: --prefs PREFERENCE=VALUE ... + + Filter results to qubes that match all specified preferences. Omitted VALUE + means None (unset). Empty value means "" or '' (blank) + +Sorting options +--------------- + +.. option:: --sort COLUMN + + Sort based on provided column rather than NAME. Sort key must be in the + output columns + +.. option:: --reverse + + Reverse sort + +.. option:: --ignore-case + + Ignore case distinctions when sorting Authors ------- diff --git a/qubesadmin/tests/tools/qvm_ls.py b/qubesadmin/tests/tools/qvm_ls.py index ab275758..81d37567 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') diff --git a/qubesadmin/tools/qvm_ls.py b/qubesadmin/tools/qvm_ls.py index 07052697..ee827783 100644 --- a/qubesadmin/tools/qvm_ls.py +++ b/qubesadmin/tools/qvm_ls.py @@ -382,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. @@ -390,13 +394,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).''' @@ -457,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...') @@ -479,6 +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 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: @@ -496,6 +526,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'), + 'prefs': ('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 +626,113 @@ 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_exclusive = parser_format.add_mutually_exclusive_group() - - parser_formats = parser.add_mutually_exclusive_group() - - parser_formats.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_formats.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)') - - 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_exclusive.add_argument('--disk', '-d', action='store_const', dest='format', const='disk', help='Same as --format=disk') - parser.add_argument('--network', '-n', + parser_format_exclusive.add_argument('--network', '-n', action='store_const', dest='format', const='network', help='Same as --format=network') - parser.add_argument('--kernel', '-k', + parser_format_exclusive.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 qubes of specific class(es)') + + parser_filter.add_argument('--label', nargs='+', metavar='LABEL', + action='store', + help='show only qubes with specific label(s)') + + parser_filter.add_argument('--tags', nargs='+', metavar='TAG', + help='show only qubes 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('--template-source', nargs='+', + metavar='TEMPLATE', action='store', + help='filter results to the qubes based on the TEMPLATE. ' + '"" means None') + + parser_filter.add_argument('--netvm-is', nargs='+', + metavar='NETVM', action='store', + help='filter results to the qubes connecting via NETVM') + + 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'], + 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') + + 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 (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 (unset). Empty value 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 when 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 +795,110 @@ 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 qubes to specific class(es) + domains = [d for d in domains if d.klass in args.klass] + + if args.label: + # filter only qubes 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 + # 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 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 qubes based on specific TemplateVM + domains_template = [] + spinner.show('Filtering results to qubes based on their templates...') + for dom in domains: + if getattr(dom, 'template', '') in args.template_source: + domains_template.append(dom) + spinner.update() + domains = domains_template + spinner.hide() + + if args.netvm_is: + # Filter only qubes connecting with specific netvm + domains_netvm = [] + spinner.show('Filtering results to qubes based on their netvm...') + for dom in domains: + if getattr(dom, 'netvm', '') in args.netvm_is: + domains_netvm.append(dom) + spinner.update() + domains = domains_netvm + 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 qubes 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 qubes 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