2017-02-28 00:01:36 +01:00
|
|
|
# pylint: disable=too-few-public-methods
|
|
|
|
|
|
|
|
#
|
|
|
|
# The Qubes OS Project, https://www.qubes-os.org/
|
|
|
|
#
|
|
|
|
# Copyright (C) 2015 Joanna Rutkowska <joanna@invisiblethingslab.com>
|
|
|
|
# Copyright (C) 2015 Wojtek Porczyk <woju@invisiblethingslab.com>
|
2017-04-29 01:16:50 +02:00
|
|
|
# Copyright (C) 2017 Marek Marczykowski-Górecki
|
|
|
|
# <marmarek@invisiblethingslab.com>
|
2017-02-28 00:01:36 +01:00
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
2017-04-29 01:16:50 +02:00
|
|
|
# it under the terms of the GNU Lesser General Public License as published by
|
|
|
|
# the Free Software Foundation; either version 2.1 of the License, or
|
2017-02-28 00:01:36 +01:00
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
2017-04-29 01:16:50 +02:00
|
|
|
# GNU Lesser General Public License for more details.
|
2017-02-28 00:01:36 +01:00
|
|
|
#
|
2017-04-29 01:16:50 +02:00
|
|
|
# You should have received a copy of the GNU Lesser General Public License along
|
2017-02-28 00:01:36 +01:00
|
|
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
|
|
#
|
|
|
|
|
|
|
|
'''qvm-ls - List available domains'''
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import print_function
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import collections
|
|
|
|
import sys
|
|
|
|
import textwrap
|
|
|
|
|
2017-05-11 23:21:04 +02:00
|
|
|
import qubesadmin
|
2017-06-07 18:52:45 +02:00
|
|
|
import qubesadmin.spinner
|
2017-05-11 23:21:04 +02:00
|
|
|
import qubesadmin.tools
|
|
|
|
import qubesadmin.utils
|
|
|
|
import qubesadmin.vm
|
2019-11-08 00:15:27 +01:00
|
|
|
import qubesadmin.exc
|
2017-02-28 00:01:36 +01:00
|
|
|
|
|
|
|
#
|
|
|
|
# columns
|
|
|
|
#
|
|
|
|
|
|
|
|
class Column(object):
|
2017-03-11 01:47:12 +01:00
|
|
|
'''A column in qvm-ls output characterised by its head and a way
|
2017-02-28 00:01:36 +01:00
|
|
|
to fetch a parameter describing the domain.
|
|
|
|
|
|
|
|
:param str head: Column head (usually uppercase).
|
|
|
|
:param str attr: Attribute, possibly complex (containing ``.``). This may \
|
|
|
|
also be a callable that gets as its only argument the domain.
|
|
|
|
:param str doc: Description of column (will be visible in --help-columns).
|
|
|
|
'''
|
|
|
|
|
|
|
|
#: collection of all columns
|
|
|
|
columns = {}
|
|
|
|
|
2017-03-11 01:47:12 +01:00
|
|
|
def __init__(self, head, attr=None, doc=None):
|
2017-02-28 00:01:36 +01:00
|
|
|
self.ls_head = head
|
2017-11-09 17:05:14 +01:00
|
|
|
self.__doc__ = doc
|
2017-02-28 00:01:36 +01:00
|
|
|
|
|
|
|
# intentionally not always do set self._attr,
|
|
|
|
# to cause AttributeError in self.format()
|
|
|
|
if attr is not None:
|
|
|
|
self._attr = attr
|
|
|
|
|
|
|
|
self.__class__.columns[self.ls_head] = self
|
|
|
|
|
|
|
|
|
2019-11-01 17:16:43 +01:00
|
|
|
def cell(self, vm, insertion=0):
|
2017-02-28 00:01:36 +01:00
|
|
|
'''Format one cell.
|
|
|
|
|
|
|
|
.. note::
|
|
|
|
|
|
|
|
This is only for technical formatting (filling with space). If you
|
|
|
|
want to subclass the :py:class:`Column` class, you should override
|
|
|
|
:py:meth:`Column.format` method instead.
|
|
|
|
|
|
|
|
:param qubes.vm.qubesvm.QubesVM: Domain to get a value from.
|
2019-11-01 17:16:43 +01:00
|
|
|
:param int insertion: Intending to shift the value to the right.
|
2017-03-11 01:47:12 +01:00
|
|
|
:returns: string to display
|
2017-02-28 00:01:36 +01:00
|
|
|
:rtype: str
|
|
|
|
'''
|
|
|
|
|
|
|
|
value = self.format(vm) or '-'
|
2019-11-01 17:16:43 +01:00
|
|
|
if insertion > 0 and self.ls_head == 'NAME':
|
|
|
|
value = '└─' + value
|
|
|
|
value = ' ' * (insertion-1) + value
|
2017-03-11 01:47:12 +01:00
|
|
|
return value
|
2017-02-28 00:01:36 +01:00
|
|
|
|
|
|
|
|
|
|
|
def format(self, vm):
|
|
|
|
'''Format one cell value.
|
|
|
|
|
|
|
|
Return value to put in a table cell.
|
|
|
|
|
|
|
|
:param qubes.vm.qubesvm.QubesVM: Domain to get a value from.
|
|
|
|
:returns: Value to put, or :py:obj:`None` if no value.
|
|
|
|
:rtype: str or None
|
|
|
|
'''
|
|
|
|
|
|
|
|
ret = None
|
|
|
|
try:
|
|
|
|
if isinstance(self._attr, str):
|
|
|
|
ret = vm
|
|
|
|
for attrseg in self._attr.split('.'):
|
|
|
|
ret = getattr(ret, attrseg)
|
|
|
|
elif isinstance(self._attr, collections.Callable):
|
|
|
|
ret = self._attr(vm)
|
|
|
|
|
|
|
|
except (AttributeError, ZeroDivisionError):
|
|
|
|
# division by 0 may be caused by arithmetic in callable attr
|
|
|
|
return None
|
|
|
|
|
|
|
|
if ret is None:
|
|
|
|
return None
|
|
|
|
|
2017-03-12 00:44:33 +01:00
|
|
|
return str(ret)
|
2017-02-28 00:01:36 +01:00
|
|
|
|
|
|
|
def __repr__(self):
|
2017-03-11 01:47:12 +01:00
|
|
|
return '{}(head={!r})'.format(self.__class__.__name__,
|
|
|
|
self.ls_head)
|
2017-02-28 00:01:36 +01:00
|
|
|
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
return self.ls_head == other.ls_head
|
|
|
|
|
|
|
|
|
|
|
|
def __lt__(self, other):
|
|
|
|
return self.ls_head < other.ls_head
|
|
|
|
|
|
|
|
|
|
|
|
class PropertyColumn(Column):
|
|
|
|
'''Column that displays value from property (:py:class:`property` or
|
|
|
|
:py:class:`qubes.property`) of domain.
|
|
|
|
|
2017-02-28 01:38:54 +01:00
|
|
|
:param name: Name of VM property.
|
2017-02-28 00:01:36 +01:00
|
|
|
'''
|
|
|
|
|
2017-02-28 01:38:54 +01:00
|
|
|
def __init__(self, name):
|
|
|
|
ls_head = name.replace('_', '-').upper()
|
2017-02-28 00:01:36 +01:00
|
|
|
super(PropertyColumn, self).__init__(
|
2017-02-28 01:38:54 +01:00
|
|
|
head=ls_head,
|
|
|
|
attr=name)
|
2017-02-28 00:01:36 +01:00
|
|
|
|
|
|
|
def __repr__(self):
|
2017-02-28 01:38:54 +01:00
|
|
|
return '{}(head={!r}'.format(
|
2017-02-28 00:01:36 +01:00
|
|
|
self.__class__.__name__,
|
2017-02-28 01:38:54 +01:00
|
|
|
self.ls_head)
|
2017-02-28 00:01:36 +01:00
|
|
|
|
|
|
|
|
2017-02-28 01:38:54 +01:00
|
|
|
def process_vm(vm):
|
|
|
|
'''Process VM object to find all listable properties.
|
2017-02-28 00:01:36 +01:00
|
|
|
|
2017-02-28 01:38:54 +01:00
|
|
|
:param qubesmgmt.vm.QubesVM vm: VM object.
|
2017-02-28 00:01:36 +01:00
|
|
|
'''
|
|
|
|
|
2017-02-28 01:38:54 +01:00
|
|
|
for prop_name in vm.property_list():
|
|
|
|
PropertyColumn(prop_name)
|
2017-02-28 00:01:36 +01:00
|
|
|
|
|
|
|
|
|
|
|
def flag(field):
|
|
|
|
'''Mark method as flag field.
|
|
|
|
|
|
|
|
:param int field: Which field to fill (counted from 1)
|
|
|
|
'''
|
|
|
|
|
|
|
|
def decorator(obj):
|
|
|
|
# pylint: disable=missing-docstring
|
|
|
|
obj.field = field
|
|
|
|
return obj
|
|
|
|
return decorator
|
|
|
|
|
|
|
|
|
|
|
|
def simple_flag(field, letter, attr, doc=None):
|
|
|
|
'''Create simple, binary flag.
|
|
|
|
|
|
|
|
:param str attr: Attribute name to check. If result is true, flag is fired.
|
|
|
|
:param str letter: The letter to show.
|
|
|
|
'''
|
|
|
|
|
|
|
|
def helper(self, vm):
|
|
|
|
# pylint: disable=missing-docstring,unused-argument
|
|
|
|
try:
|
|
|
|
value = getattr(vm, attr)
|
|
|
|
except AttributeError:
|
|
|
|
value = False
|
|
|
|
|
|
|
|
if value:
|
|
|
|
return letter[0]
|
|
|
|
|
|
|
|
helper.__doc__ = doc
|
|
|
|
helper.field = field
|
|
|
|
return helper
|
|
|
|
|
|
|
|
|
2017-06-08 13:10:44 +02:00
|
|
|
class FlagsColumn(Column):
|
2017-02-28 00:01:36 +01:00
|
|
|
'''Some fancy flags that describe general status of the domain.'''
|
|
|
|
# pylint: disable=no-self-use
|
|
|
|
|
|
|
|
def __init__(self):
|
2017-06-08 13:10:44 +02:00
|
|
|
super(FlagsColumn, self).__init__(
|
|
|
|
head='FLAGS',
|
2017-02-28 00:01:36 +01:00
|
|
|
doc=self.__class__.__doc__)
|
|
|
|
|
|
|
|
|
|
|
|
@flag(1)
|
|
|
|
def type(self, vm):
|
|
|
|
'''Type of domain.
|
|
|
|
|
|
|
|
0 AdminVM (AKA Dom0)
|
|
|
|
aA AppVM
|
|
|
|
dD DisposableVM
|
|
|
|
sS StandaloneVM
|
|
|
|
tT TemplateVM
|
|
|
|
|
|
|
|
When it is HVM (optimised VM), the letter is capital.
|
|
|
|
'''
|
|
|
|
|
2017-10-02 20:43:01 +02:00
|
|
|
type_codes = {
|
|
|
|
'AdminVM': '0',
|
|
|
|
'TemplateVM': 't',
|
|
|
|
'AppVM': 'a',
|
|
|
|
'StandaloneVM': 's',
|
|
|
|
'DispVM': 'd',
|
|
|
|
}
|
|
|
|
ret = type_codes.get(vm.klass, None)
|
|
|
|
if ret == '0':
|
|
|
|
return ret
|
2017-02-28 00:01:36 +01:00
|
|
|
|
|
|
|
if ret is not None:
|
2017-07-18 04:22:34 +02:00
|
|
|
if getattr(vm, 'virt_mode', 'pv') == 'hvm':
|
2017-02-28 00:01:36 +01:00
|
|
|
return ret.upper()
|
2017-04-21 02:47:23 +02:00
|
|
|
return ret
|
2017-02-28 00:01:36 +01:00
|
|
|
|
|
|
|
|
|
|
|
@flag(2)
|
|
|
|
def power(self, vm):
|
|
|
|
'''Current power state.
|
|
|
|
|
|
|
|
r running
|
|
|
|
t transient
|
|
|
|
p paused
|
|
|
|
s suspended
|
|
|
|
h halting
|
|
|
|
d dying
|
|
|
|
c crashed
|
|
|
|
? unknown
|
|
|
|
'''
|
|
|
|
|
|
|
|
state = vm.get_power_state().lower()
|
|
|
|
if state == 'unknown':
|
|
|
|
return '?'
|
2018-07-16 02:25:25 +02:00
|
|
|
if state in ('running', 'transient', 'paused', 'suspended',
|
2017-02-28 00:01:36 +01:00
|
|
|
'halting', 'dying', 'crashed'):
|
|
|
|
return state[0]
|
|
|
|
|
|
|
|
|
|
|
|
updateable = simple_flag(3, 'U', 'updateable',
|
|
|
|
doc='If the domain is updateable.')
|
|
|
|
|
|
|
|
provides_network = simple_flag(4, 'N', 'provides_network',
|
|
|
|
doc='If the domain provides network.')
|
|
|
|
|
|
|
|
installed_by_rpm = simple_flag(5, 'R', 'installed_by_rpm',
|
|
|
|
doc='If the domain is installed by RPM.')
|
|
|
|
|
|
|
|
internal = simple_flag(6, 'i', 'internal',
|
|
|
|
doc='If the domain is internal (not normally shown, no appmenus).')
|
|
|
|
|
|
|
|
debug = simple_flag(7, 'D', 'debug',
|
|
|
|
doc='If the domain is being debugged.')
|
|
|
|
|
|
|
|
autostart = simple_flag(8, 'A', 'autostart',
|
|
|
|
doc='If the domain is marked for autostart.')
|
|
|
|
|
|
|
|
# TODO (not sure if really):
|
|
|
|
# include in backups
|
|
|
|
# uses_custom_config
|
|
|
|
|
|
|
|
def _no_flag(self, vm):
|
|
|
|
'''Reserved for future use.'''
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_flags(cls):
|
|
|
|
'''Get all flags as list.
|
|
|
|
|
|
|
|
Holes between flags are filled with :py:meth:`_no_flag`.
|
|
|
|
|
|
|
|
:rtype: list
|
|
|
|
'''
|
|
|
|
|
|
|
|
flags = {}
|
|
|
|
for mycls in cls.__mro__:
|
|
|
|
for attr in mycls.__dict__.values():
|
|
|
|
if not hasattr(attr, 'field'):
|
|
|
|
continue
|
|
|
|
if attr.field in flags:
|
|
|
|
continue
|
|
|
|
flags[attr.field] = attr
|
|
|
|
|
|
|
|
return [(flags[i] if i in flags else cls._no_flag)
|
|
|
|
for i in range(1, max(flags) + 1)]
|
|
|
|
|
|
|
|
|
|
|
|
def format(self, vm):
|
|
|
|
return ''.join((flag(self, vm) or '-') for flag in self.get_flags())
|
|
|
|
|
|
|
|
|
|
|
|
def calc_size(vm, volume_name):
|
|
|
|
''' Calculates the volume size in MB '''
|
|
|
|
try:
|
|
|
|
return vm.volumes[volume_name].size // 1024 // 1024
|
|
|
|
except KeyError:
|
|
|
|
return 0
|
|
|
|
|
|
|
|
def calc_usage(vm, volume_name):
|
|
|
|
''' Calculates the volume usage in MB '''
|
|
|
|
try:
|
|
|
|
return vm.volumes[volume_name].usage // 1024 // 1024
|
|
|
|
except KeyError:
|
|
|
|
return 0
|
|
|
|
|
|
|
|
def calc_used(vm, volume_name):
|
|
|
|
''' Calculates the volume usage in percent '''
|
|
|
|
size = calc_size(vm, volume_name)
|
|
|
|
if size == 0:
|
|
|
|
return 0
|
|
|
|
usage = calc_usage(vm, volume_name)
|
2017-12-22 23:24:48 +01:00
|
|
|
return '{}%'.format(usage * 100 // size)
|
2017-02-28 00:01:36 +01:00
|
|
|
|
|
|
|
|
|
|
|
# todo maxmem
|
|
|
|
|
2017-06-08 13:10:44 +02:00
|
|
|
Column('STATE',
|
|
|
|
attr=(lambda vm: vm.get_power_state()),
|
|
|
|
doc='Current power state.')
|
|
|
|
|
|
|
|
Column('CLASS',
|
2017-10-02 20:43:01 +02:00
|
|
|
attr=(lambda vm: vm.klass),
|
2017-06-08 13:10:44 +02:00
|
|
|
doc='Class of the qube.')
|
|
|
|
|
|
|
|
|
2017-03-11 01:47:12 +01:00
|
|
|
Column('GATEWAY',
|
2017-02-28 00:01:36 +01:00
|
|
|
attr='netvm.gateway',
|
|
|
|
doc='Network gateway.')
|
|
|
|
|
2017-03-11 01:47:12 +01:00
|
|
|
Column('MEMORY',
|
2020-01-15 17:27:41 +01:00
|
|
|
attr=(lambda vm: vm.get_mem() // 1024 if vm.is_running() else None),
|
2017-02-28 00:01:36 +01:00
|
|
|
doc='Memory currently used by VM')
|
|
|
|
|
2017-03-11 01:47:12 +01:00
|
|
|
Column('DISK',
|
2017-07-29 04:50:19 +02:00
|
|
|
attr=(lambda vm: vm.get_disk_utilization() // 1024 // 1024),
|
2017-02-28 00:01:36 +01:00
|
|
|
doc='Total disk utilisation.')
|
|
|
|
|
|
|
|
|
2017-03-11 01:47:12 +01:00
|
|
|
Column('PRIV-CURR',
|
2017-02-28 00:01:36 +01:00
|
|
|
attr=(lambda vm: calc_usage(vm, 'private')),
|
|
|
|
doc='Disk utilisation by private image (/home, /usr/local).')
|
|
|
|
|
2017-03-11 01:47:12 +01:00
|
|
|
Column('PRIV-MAX',
|
2017-02-28 00:01:36 +01:00
|
|
|
attr=(lambda vm: calc_size(vm, 'private')),
|
|
|
|
doc='Maximum available space for private image.')
|
|
|
|
|
2017-03-11 01:47:12 +01:00
|
|
|
Column('PRIV-USED',
|
2017-02-28 00:01:36 +01:00
|
|
|
attr=(lambda vm: calc_used(vm, 'private')),
|
|
|
|
doc='Disk utilisation by private image as a percentage of available space.')
|
|
|
|
|
|
|
|
|
2017-03-11 01:47:12 +01:00
|
|
|
Column('ROOT-CURR',
|
2017-02-28 00:01:36 +01:00
|
|
|
attr=(lambda vm: calc_usage(vm, 'root')),
|
|
|
|
doc='Disk utilisation by root image (/usr, /lib, /etc, ...).')
|
|
|
|
|
2017-03-11 01:47:12 +01:00
|
|
|
Column('ROOT-MAX',
|
2017-02-28 00:01:36 +01:00
|
|
|
attr=(lambda vm: calc_size(vm, 'root')),
|
|
|
|
doc='Maximum available space for root image.')
|
|
|
|
|
2017-03-11 01:47:12 +01:00
|
|
|
Column('ROOT-USED',
|
2017-02-28 00:01:36 +01:00
|
|
|
attr=(lambda vm: calc_used(vm, 'root')),
|
|
|
|
doc='Disk utilisation by root image as a percentage of available space.')
|
|
|
|
|
|
|
|
|
2017-06-08 13:10:44 +02:00
|
|
|
FlagsColumn()
|
2017-02-28 00:01:36 +01:00
|
|
|
|
|
|
|
|
|
|
|
class Table(object):
|
|
|
|
'''Table that is displayed to the user.
|
|
|
|
|
2017-12-22 23:04:42 +01:00
|
|
|
:param domains: Domains to include in the table.
|
2017-02-28 00:01:36 +01:00
|
|
|
:param list colnames: Names of the columns (need not to be uppercase).
|
|
|
|
'''
|
2019-11-01 17:16:43 +01:00
|
|
|
def __init__(self, domains, colnames, spinner, raw_data=False,
|
|
|
|
tree_sorted=False):
|
2017-12-22 23:04:42 +01:00
|
|
|
self.domains = domains
|
2017-12-29 00:56:16 +01:00
|
|
|
self.columns = tuple(Column.columns[col.upper().replace('_', '-')]
|
|
|
|
for col in colnames)
|
2017-06-07 18:52:45 +02:00
|
|
|
self.spinner = spinner
|
2017-02-28 00:01:36 +01:00
|
|
|
self.raw_data = raw_data
|
2019-11-01 17:16:43 +01:00
|
|
|
self.tree_sorted = tree_sorted
|
2017-02-28 00:01:36 +01:00
|
|
|
|
2017-03-11 01:47:12 +01:00
|
|
|
def get_head(self):
|
|
|
|
'''Get table head data (all column heads).'''
|
|
|
|
return [col.ls_head for col in self.columns]
|
2017-02-28 00:01:36 +01:00
|
|
|
|
2019-11-01 17:16:43 +01:00
|
|
|
def get_row(self, vm, insertion=0):
|
2017-03-11 01:47:12 +01:00
|
|
|
'''Get single table row data (all columns for one domain).'''
|
2017-06-07 18:52:45 +02:00
|
|
|
ret = []
|
|
|
|
for col in self.columns:
|
2019-11-01 17:16:43 +01:00
|
|
|
if self.tree_sorted and col.ls_head == 'NAME':
|
|
|
|
ret.append(col.cell(vm, insertion))
|
|
|
|
else:
|
|
|
|
ret.append(col.cell(vm))
|
2017-06-07 18:52:45 +02:00
|
|
|
self.spinner.update()
|
|
|
|
return ret
|
2017-02-28 00:01:36 +01:00
|
|
|
|
2019-11-01 17:16:43 +01:00
|
|
|
def tree_append_child(self, parent, level):
|
|
|
|
'''Concatenate the network children of the vm to a list.
|
|
|
|
|
|
|
|
:param qubes.vm.qubesvm.QubesVM: Parent vm of the children VMs
|
|
|
|
'''
|
|
|
|
childs = list()
|
|
|
|
for child in parent.connected_vms:
|
|
|
|
if child.provides_network and child in self.domains:
|
|
|
|
childs.append((level, child))
|
|
|
|
childs += self.tree_append_child(child, level+1)
|
|
|
|
elif child in self.domains:
|
|
|
|
childs.append((level, child))
|
|
|
|
return childs
|
|
|
|
|
|
|
|
def sort_to_tree(self, domains):
|
|
|
|
'''Sort the domains as a network tree. It returns a list of sets. Each
|
|
|
|
tuple stores the insertion of the cell name and the vm object.
|
|
|
|
|
|
|
|
:param list() domains: The domains which will be sorted
|
|
|
|
:return list(tuple()) tree: returns a list of tuple(insertion, vm)
|
|
|
|
'''
|
|
|
|
tree = list()
|
2019-11-08 00:15:27 +01:00
|
|
|
# We need a copy of domains, because domains.remove() is not allowed
|
|
|
|
# while iterating over it. Besides, the domains should be sorted anyway.
|
|
|
|
sorted_doms = sorted(domains)
|
|
|
|
for dom in sorted_doms:
|
2019-11-01 17:16:43 +01:00
|
|
|
try:
|
|
|
|
if dom.netvm is None and not dom.provides_network:
|
|
|
|
tree.append((0, dom))
|
2019-11-08 00:15:27 +01:00
|
|
|
domains.remove(dom)
|
2019-11-01 17:16:43 +01:00
|
|
|
# dom0 and eventually others have no netvm attribute
|
2019-11-08 00:15:27 +01:00
|
|
|
except (qubesadmin.exc.QubesNoSuchPropertyError, AttributeError):
|
2019-11-01 17:16:43 +01:00
|
|
|
tree.append((0, dom))
|
|
|
|
domains.remove(dom)
|
|
|
|
|
2019-11-08 00:15:27 +01:00
|
|
|
for dom in sorted(domains):
|
2019-11-01 17:16:43 +01:00
|
|
|
if dom.netvm is None and dom.provides_network:
|
|
|
|
tree.append((0, dom))
|
|
|
|
domains.remove(dom)
|
|
|
|
tree += self.tree_append_child(dom, 1)
|
|
|
|
|
|
|
|
return tree
|
|
|
|
|
2017-02-28 00:01:36 +01:00
|
|
|
def write_table(self, stream=sys.stdout):
|
|
|
|
'''Write whole table to file-like object.
|
|
|
|
|
|
|
|
:param file stream: Stream to write the table to.
|
|
|
|
'''
|
|
|
|
|
2017-03-11 01:47:12 +01:00
|
|
|
table_data = []
|
2017-02-28 00:01:36 +01:00
|
|
|
if not self.raw_data:
|
2017-06-07 18:52:45 +02:00
|
|
|
self.spinner.show('please wait...')
|
2017-03-11 01:47:12 +01:00
|
|
|
table_data.append(self.get_head())
|
2017-06-07 18:52:45 +02:00
|
|
|
self.spinner.update()
|
2019-11-01 17:16:43 +01:00
|
|
|
if self.tree_sorted:
|
|
|
|
insertion_vm_list = self.sort_to_tree(self.domains)
|
|
|
|
for insertion, vm in insertion_vm_list:
|
|
|
|
table_data.append(self.get_row(vm, insertion))
|
|
|
|
else:
|
|
|
|
for vm in sorted(self.domains):
|
|
|
|
table_data.append(self.get_row(vm))
|
2017-06-07 18:52:45 +02:00
|
|
|
self.spinner.hide()
|
2017-05-11 23:21:04 +02:00
|
|
|
qubesadmin.tools.print_table(table_data, stream=stream)
|
2017-03-11 01:47:12 +01:00
|
|
|
else:
|
2017-12-22 23:04:42 +01:00
|
|
|
for vm in sorted(self.domains):
|
2017-03-11 01:47:12 +01:00
|
|
|
stream.write('|'.join(self.get_row(vm)) + '\n')
|
2017-02-28 00:01:36 +01:00
|
|
|
|
|
|
|
|
|
|
|
#: Available formats. Feel free to plug your own one.
|
|
|
|
formats = {
|
2017-06-08 13:10:44 +02:00
|
|
|
'simple': ('name', 'state', 'class', 'label', 'template', 'netvm'),
|
|
|
|
'network': ('name', 'state', 'netvm', 'ip', 'ipback', 'gateway'),
|
2018-04-21 02:51:31 +02:00
|
|
|
'kernel': ('name', 'state', 'class', 'template', 'kernel', 'kernelopts'),
|
2017-06-08 13:10:44 +02:00
|
|
|
'full': ('name', 'state', 'class', 'label', 'qid', 'xid', 'uuid'),
|
|
|
|
# 'perf': ('name', 'state', 'cpu', 'memory'),
|
|
|
|
'disk': ('name', 'state', 'disk',
|
2017-02-28 00:01:36 +01:00
|
|
|
'priv-curr', 'priv-max', 'priv-used',
|
|
|
|
'root-curr', 'root-max', 'root-used'),
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _HelpColumnsAction(argparse.Action):
|
|
|
|
'''Action for argument parser that displays all columns and exits.'''
|
|
|
|
# pylint: disable=redefined-builtin
|
|
|
|
def __init__(self,
|
|
|
|
option_strings,
|
|
|
|
dest=argparse.SUPPRESS,
|
|
|
|
default=argparse.SUPPRESS,
|
|
|
|
help='list all available columns with short descriptions and exit'):
|
|
|
|
super(_HelpColumnsAction, self).__init__(
|
|
|
|
option_strings=option_strings,
|
|
|
|
dest=dest,
|
|
|
|
default=default,
|
|
|
|
nargs=0,
|
|
|
|
help=help)
|
|
|
|
|
|
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
|
|
width = max(len(column.ls_head) for column in Column.columns.values())
|
|
|
|
wrapper = textwrap.TextWrapper(width=80,
|
|
|
|
initial_indent=' ', subsequent_indent=' ' * (width + 6))
|
|
|
|
|
|
|
|
text = 'Available columns:\n' + '\n'.join(
|
|
|
|
wrapper.fill('{head:{width}s} {doc}'.format(
|
|
|
|
head=column.ls_head,
|
|
|
|
doc=column.__doc__ or '',
|
|
|
|
width=width))
|
|
|
|
for column in sorted(Column.columns.values()))
|
2017-03-11 01:49:04 +01:00
|
|
|
text += '\n\nAdditionally any VM property may be used as a column, ' \
|
|
|
|
'see qvm-prefs --help-properties for available values'
|
2017-02-28 00:01:36 +01:00
|
|
|
parser.exit(message=text + '\n')
|
|
|
|
|
2017-03-11 01:49:04 +01:00
|
|
|
|
2017-02-28 00:01:36 +01:00
|
|
|
class _HelpFormatsAction(argparse.Action):
|
|
|
|
'''Action for argument parser that displays all formats and exits.'''
|
|
|
|
# pylint: disable=redefined-builtin
|
|
|
|
def __init__(self,
|
|
|
|
option_strings,
|
|
|
|
dest=argparse.SUPPRESS,
|
|
|
|
default=argparse.SUPPRESS,
|
|
|
|
help='list all available formats with their definitions and exit'):
|
|
|
|
super(_HelpFormatsAction, self).__init__(
|
|
|
|
option_strings=option_strings,
|
|
|
|
dest=dest,
|
|
|
|
default=default,
|
|
|
|
nargs=0,
|
|
|
|
help=help)
|
|
|
|
|
|
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
|
|
width = max(len(fmt) for fmt in formats)
|
|
|
|
text = 'Available formats:\n' + ''.join(
|
|
|
|
' {fmt:{width}s} {columns}\n'.format(
|
|
|
|
fmt=fmt, columns=','.join(formats[fmt]).upper(), width=width)
|
|
|
|
for fmt in sorted(formats))
|
|
|
|
parser.exit(message=text)
|
|
|
|
|
|
|
|
|
2018-10-10 19:58:32 +02:00
|
|
|
# common VM power states for easy command-line filtering
|
|
|
|
DOMAIN_POWER_STATES = ['running', 'paused', 'halted']
|
|
|
|
|
|
|
|
|
|
|
|
def matches_power_states(domain, **states):
|
|
|
|
'''Filter domains by their power state'''
|
|
|
|
# if all values are False (default) => return match on every VM
|
|
|
|
if not states or set(states.values()) == {False}:
|
|
|
|
return True
|
|
|
|
|
|
|
|
# otherwise => only VMs matching True states
|
|
|
|
requested_states = [state for state, active in states.items() if active]
|
|
|
|
return domain.get_power_state().lower() in requested_states
|
|
|
|
|
|
|
|
|
2017-02-28 00:01:36 +01:00
|
|
|
def get_parser():
|
|
|
|
'''Create :py:class:`argparse.ArgumentParser` suitable for
|
|
|
|
:program:`qvm-ls`.
|
|
|
|
'''
|
|
|
|
# parser creation is delayed to get all the columns that are scattered
|
|
|
|
# thorough the modules
|
|
|
|
|
|
|
|
wrapper = textwrap.TextWrapper(width=80, break_on_hyphens=False,
|
|
|
|
initial_indent=' ', subsequent_indent=' ')
|
|
|
|
|
2017-05-11 23:21:04 +02:00
|
|
|
parser = qubesadmin.tools.QubesArgumentParser(
|
2017-12-22 23:04:42 +01:00
|
|
|
vmname_nargs=argparse.ZERO_OR_MORE,
|
2017-02-28 00:01:36 +01:00
|
|
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
|
|
description='List Qubes domains and their parametres.',
|
|
|
|
epilog='available formats (see --help-formats):\n{}\n\n'
|
|
|
|
'available columns (see --help-columns):\n{}'.format(
|
|
|
|
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_formats = parser.add_mutually_exclusive_group()
|
|
|
|
|
|
|
|
parser_formats.add_argument('--format', '-o', metavar='FORMAT',
|
|
|
|
action='store', choices=formats.keys(), default='simple',
|
|
|
|
help='preset format')
|
|
|
|
|
|
|
|
parser_formats.add_argument('--fields', '-O', metavar='FIELD,...',
|
|
|
|
action='store',
|
|
|
|
help='user specified format (see available columns below)')
|
|
|
|
|
|
|
|
|
2018-08-23 22:42:22 +02:00
|
|
|
parser.add_argument('--tags', nargs='+', metavar='TAG',
|
|
|
|
help='show only VMs having specific tag(s)')
|
|
|
|
|
2018-10-10 19:58:32 +02:00
|
|
|
for pwrstate in DOMAIN_POWER_STATES:
|
|
|
|
parser.add_argument('--{}'.format(pwrstate), action='store_true',
|
|
|
|
help='show {} VMs'.format(pwrstate))
|
|
|
|
|
2017-02-28 00:01:36 +01:00
|
|
|
parser.add_argument('--raw-data', action='store_true',
|
|
|
|
help='Display specify data of specified VMs. Intended for '
|
|
|
|
'bash-parsing.')
|
|
|
|
|
2019-11-01 17:16:43 +01:00
|
|
|
parser.add_argument('--tree', '-t',
|
|
|
|
action='store_const', const='tree',
|
|
|
|
help='sort domain list as network tree')
|
|
|
|
|
2017-06-07 18:52:45 +02:00
|
|
|
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')
|
|
|
|
|
2017-12-22 23:11:33 +01:00
|
|
|
# shortcuts, compatibility with Qubes 3.2
|
|
|
|
parser.add_argument('--raw-list', action='store_true',
|
|
|
|
help='Same as --raw-data --fields=name')
|
|
|
|
|
2017-12-22 23:23:57 +01:00
|
|
|
parser.add_argument('--disk', '-d',
|
|
|
|
action='store_const', dest='format', const='disk',
|
|
|
|
help='Same as --format=disk')
|
|
|
|
|
|
|
|
parser.add_argument('--network', '-n',
|
|
|
|
action='store_const', dest='format', const='network',
|
|
|
|
help='Same as --format=network')
|
|
|
|
|
2018-04-21 02:51:31 +02:00
|
|
|
parser.add_argument('--kernel', '-k',
|
|
|
|
action='store_const', dest='format', const='kernel',
|
|
|
|
help='Same as --format=kernel')
|
|
|
|
|
2017-06-07 18:52:45 +02:00
|
|
|
parser.set_defaults(spinner=True)
|
|
|
|
|
2017-02-28 00:01:36 +01:00
|
|
|
# parser.add_argument('--conf', '-c',
|
|
|
|
# action='store', metavar='CFGFILE',
|
|
|
|
# help='Qubes config file')
|
|
|
|
|
|
|
|
return parser
|
|
|
|
|
|
|
|
|
2017-03-12 00:44:33 +01:00
|
|
|
def main(args=None, app=None):
|
2017-02-28 00:01:36 +01:00
|
|
|
'''Main routine of :program:`qvm-ls`.
|
|
|
|
|
|
|
|
:param list args: Optional arguments to override those delivered from \
|
|
|
|
command line.
|
2017-03-12 00:44:33 +01:00
|
|
|
:param app: Operate on given app object instead of instantiating new one.
|
2017-02-28 00:01:36 +01:00
|
|
|
'''
|
|
|
|
|
|
|
|
parser = get_parser()
|
|
|
|
try:
|
2017-03-12 00:44:33 +01:00
|
|
|
args = parser.parse_args(args, app=app)
|
2017-05-11 23:21:04 +02:00
|
|
|
except qubesadmin.exc.QubesException as e:
|
2017-02-28 00:01:36 +01:00
|
|
|
parser.print_error(str(e))
|
|
|
|
return 1
|
|
|
|
|
2017-12-22 23:11:33 +01:00
|
|
|
if args.raw_list:
|
|
|
|
args.raw_data = True
|
|
|
|
args.fields = 'name'
|
|
|
|
|
2017-02-28 00:01:36 +01:00
|
|
|
if args.fields:
|
|
|
|
columns = [col.strip() for col in args.fields.split(',')]
|
|
|
|
else:
|
|
|
|
columns = formats[args.format]
|
|
|
|
|
2017-02-28 01:38:54 +01:00
|
|
|
# assume unknown columns are VM properties
|
|
|
|
for col in columns:
|
|
|
|
if col.upper() not in Column.columns:
|
2017-06-08 13:08:56 +02:00
|
|
|
PropertyColumn(col.lower())
|
2017-02-28 01:38:54 +01:00
|
|
|
|
2017-11-30 12:48:03 +01:00
|
|
|
if args.spinner and not args.raw_data:
|
2017-06-07 18:52:45 +02:00
|
|
|
# we need Enterprise Edition™, since it's the only one that detects TTY
|
|
|
|
# and uses dots if we are redirected somewhere else
|
|
|
|
spinner = qubesadmin.spinner.QubesSpinnerEnterpriseEdition(sys.stderr)
|
|
|
|
else:
|
|
|
|
spinner = qubesadmin.spinner.DummySpinner(sys.stderr)
|
|
|
|
|
2017-12-22 23:04:42 +01:00
|
|
|
if args.domains:
|
|
|
|
domains = args.domains
|
|
|
|
else:
|
|
|
|
domains = args.app.domains
|
2018-08-23 22:42:22 +02:00
|
|
|
|
2020-01-15 14:14:57 +01:00
|
|
|
if args.all_domains:
|
|
|
|
# Normally, --all means "all domains except for AdminVM".
|
|
|
|
# However, in the case of qvm-ls it does not make sense to exclude
|
|
|
|
# AdminVMs, so we override the list from parser.
|
|
|
|
domains = [
|
|
|
|
vm for vm in args.app.domains if vm.name not in args.exclude
|
|
|
|
]
|
|
|
|
|
2018-08-23 22:42:22 +02:00
|
|
|
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))]
|
|
|
|
|
2018-10-10 19:58:32 +02:00
|
|
|
pwrstates = {state: getattr(args, state) for state in DOMAIN_POWER_STATES}
|
2019-10-05 21:57:48 +02:00
|
|
|
domains = [d for d in domains
|
|
|
|
if matches_power_states(d, **pwrstates)]
|
2018-10-10 19:58:32 +02:00
|
|
|
|
2019-11-01 17:16:43 +01:00
|
|
|
table = Table(domains, columns, spinner, args.raw_data, args.tree)
|
2017-02-28 00:01:36 +01:00
|
|
|
table.write_table(sys.stdout)
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
sys.exit(main())
|