#!/usr/bin/python -O # vim: fileencoding=utf-8 # pylint: disable=too-few-public-methods # # The Qubes OS Project, https://www.qubes-os.org/ # # Copyright (C) 2015 Joanna Rutkowska # Copyright (C) 2015 Wojtek Porczyk # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (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 # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # 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 __builtin__ import argparse import collections import os import sys import textwrap import qubes import qubes.config import qubes.tools import qubes.utils # # columns # class Column(object): '''A column in qvm-ls output characterised by its head, a width and a way to fetch a parameter describing the domain. :param str head: Column head (usually uppercase). :param int width: Column width. :param str attr: Attribute, possibly complex (containing ``.``). This may \ also be a callable that gets as its only argument the domain. :param str fmt: if specified, used as base for :py:meth:`str.format` for \ column's value :param str doc: Description of column (will be visible in --help-columns). ''' #: collection of all columns columns = {} def __init__(self, head, width=0, attr=None, fmt=None, doc=None): self.ls_head = head self.ls_width = max(width, len(self.ls_head) + 1) self._fmt = fmt self.__doc__ = doc if doc is None else qubes.utils.format_doc(doc) # 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 def cell(self, vm): '''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. :returns: string that is at least as wide as needed to fill table row. :rtype: str ''' value = self.format(vm) or '-' return value.ljust(self.ls_width) 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, basestring): 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 if self._fmt is not None: return self._fmt.format(ret) # late import to avoid circular import # pylint: disable=redefined-outer-name import qubes.vm if isinstance(ret, (qubes.vm.BaseVM, qubes.Label)): return ret.name return ret def __repr__(self): return '{}(head={!r}, width={!r})'.format(self.__class__.__name__, self.ls_head, self.ls_width) def __eq__(self, other): return self.ls_head == other.ls_head def __lt__(self, other): return self.ls_head < other.ls_head def column(width=0, head=None, fmt=None): '''Mark function or plain property as valid column in :program:`qvm-ls`. By default all instances of :py:class:`qubes.property` are valid. :param int width: Column width :param str head: Column head (default: take property's name) ''' def decorator(obj): # pylint: disable=missing-docstring # we keep hints on fget, so the order of decorators does not matter holder = obj.fget if isinstance(obj, __builtin__.property) else obj try: holder.ls_head = head or holder.__name__.replace('_', '-').upper() except AttributeError: raise TypeError('Cannot find default column name ' 'for a strange object {!r}'.format(obj)) holder.ls_width = max(width, len(holder.ls_head) + 1) holder.ls_fmt = fmt return obj return decorator class PropertyColumn(Column): '''Column that displays value from property (:py:class:`property` or :py:class:`qubes.property`) of domain. You shouldn't use this class directly, see :py:func:`column` decorator. :param holder: Holder of magic attributes. ''' def __init__(self, holder): super(PropertyColumn, self).__init__( head=holder.ls_head, width=holder.ls_width, attr=holder.__name__, fmt=getattr(holder, 'ls_fmt', None), doc=holder.__doc__) self.holder = holder def __repr__(self): return '{}(head={!r}, width={!r} holder={!r})'.format( self.__class__.__name__, self.ls_head, self.ls_width, self.holder) def process_class(cls): '''Process class after definition to find all listable properties. It is used in metaclass of the domain. :param qubes.vm.BaseVMMeta cls: Class to round up. ''' for prop in cls.__dict__.values(): holder = prop.fget if isinstance(prop, __builtin__.property) else prop if not hasattr(holder, 'ls_head') or holder.ls_head is None: continue for col in Column.columns.values(): if not isinstance(col, PropertyColumn): continue if col.holder.__name__ != holder.__name__: continue if col.ls_head != holder.ls_head: raise TypeError('Found column head mismatch in class {!r} ' '({!r} != {!r})'.format(cls.__name__, holder.ls_head, col.ls_head)) if col.ls_width != holder.ls_width: raise TypeError('Found column width mismatch in class {!r} ' '({!r} != {!r})'.format(cls.__name__, holder.ls_width, col.ls_width)) PropertyColumn(holder) 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 class StatusColumn(Column): '''Some fancy flags that describe general status of the domain.''' # pylint: disable=no-self-use def __init__(self): super(StatusColumn, self).__init__( head='STATUS', width=len(self.get_flags()) + 1, 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. ''' # late import because of circular dependency # pylint: disable=redefined-outer-name import qubes.vm import qubes.vm.adminvm import qubes.vm.appvm import qubes.vm.dispvm import qubes.vm.hvm import qubes.vm.qubesvm import qubes.vm.templatevm if isinstance(vm, qubes.vm.adminvm.AdminVM): return '0' ret = None # TODO right order, depending on inheritance if isinstance(vm, qubes.vm.templatevm.TemplateVM): ret = 't' if isinstance(vm, qubes.vm.appvm.AppVM): ret = 'a' # if isinstance(vm, qubes.vm.standalonevm.StandaloneVM): # ret = 's' if isinstance(vm, qubes.vm.dispvm.DispVM): ret = 'd' if ret is not None: if isinstance(vm, qubes.vm.hvm.HVM): return ret.upper() else: return ret @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 '?' elif state in ('running', 'transient', 'paused', 'suspended', '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()) # todo maxmem Column('GATEWAY', width=15, attr='netvm.gateway', doc='Network gateway.') Column('MEMORY', width=5, attr=(lambda vm: vm.get_mem()/1024 if vm.is_running() else None), doc='Memory currently used by VM') Column('DISK', width=5, attr=(lambda vm: vm.get_disk_utilization()/1024/1024), doc='Total disk utilisation.') Column('PRIV-CURR', width=5, attr=(lambda vm: vm.get_disk_utilization_private_img()/1024/1024), fmt='{:.0f}', doc='Disk utilisation by private image (/home, /usr/local).') Column('PRIV-MAX', width=5, attr=(lambda vm: vm.get_private_img_sz()/1024/1024), fmt='{:.0f}', doc='Maximum available space for private image.') Column('PRIV-USED', width=5, attr=(lambda vm: vm.get_disk_utilization_private_img() * 100 / vm.get_private_img_sz()), fmt='{:.0f}', doc='Disk utilisation by private image as a percentage of available space.') Column('ROOT-CURR', width=5, attr=(lambda vm: vm.get_disk_utilization_private_img()/1024/1024), fmt='{:.0f}', doc='Disk utilisation by root image (/usr, /lib, /etc, ...).') Column('ROOT-MAX', width=5, attr=(lambda vm: vm.get_private_img_sz()/1024/1024), fmt='{:.0f}', doc='Maximum available space for root image.') Column('ROOT-USED', width=5, attr=(lambda vm: vm.get_disk_utilization_private_img() * 100 / vm.get_private_img_sz()), fmt='{:.0f}', doc='Disk utilisation by root image as a percentage of available space.') StatusColumn() class Table(object): '''Table that is displayed to the user. :param qubes.Qubes app: Qubes application object. :param list colnames: Names of the columns (need not to be uppercase). ''' def __init__(self, app, colnames): self.app = app self.columns = tuple(Column.columns[col.upper()] for col in colnames) def format_head(self): '''Format table head (all column heads).''' return ''.join('{head:{width}s}'.format( head=col.ls_head, width=col.ls_width) for col in self.columns[:-1]) + \ self.columns[-1].ls_head def format_row(self, vm): '''Format single table row (all columns for one domain).''' return ''.join(col.cell(vm) for col in self.columns) def write_table(self, stream=sys.stdout): '''Write whole table to file-like object. :param file stream: Stream to write the table to. ''' stream.write(self.format_head() + '\n') for vm in self.app.domains: stream.write(self.format_row(vm) + '\n') #: Available formats. Feel free to plug your own one. formats = { 'simple': ('name', 'status', 'label', 'template', 'netvm'), 'network': ('name', 'status', 'netvm', 'ip', 'ipback', 'gateway'), 'full': ('name', 'status', 'label', 'qid', 'xid', 'uuid'), # 'perf': ('name', 'status', 'cpu', 'memory'), 'disk': ('name', 'status', 'disk', '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())) parser.exit(message=text + '\n') 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) 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=' ') parser = qubes.tools.get_parser_base( 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)') # parser.add_argument('--conf', '-c', # action='store', metavar='CFGFILE', # help='Qubes config file') return parser def main(args=None): '''Main routine of :program:`qvm-ls`. :param list args: Optional arguments to override those delivered from \ command line. ''' parser = get_parser() args = parser.parse_args(args) qubes.tools.set_verbosity(parser, args) app = qubes.Qubes(args.xml) if args.fields: columns = [col.strip() for col in args.fields.split(',')] for col in columns: if col.upper() not in Column.columns: parser.error('no such column: {!r}'.format(col)) else: columns = formats[args.format] table = Table(app, columns) table.write_table(sys.stdout) return True if __name__ == '__main__': sys.exit(not main())