qvm_ls.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. # pylint: disable=too-few-public-methods
  2. #
  3. # The Qubes OS Project, https://www.qubes-os.org/
  4. #
  5. # Copyright (C) 2015 Joanna Rutkowska <joanna@invisiblethingslab.com>
  6. # Copyright (C) 2015 Wojtek Porczyk <woju@invisiblethingslab.com>
  7. # Copyright (C) 2017 Marek Marczykowski-Górecki
  8. # <marmarek@invisiblethingslab.com>
  9. #
  10. # This program is free software; you can redistribute it and/or modify
  11. # it under the terms of the GNU Lesser General Public License as published by
  12. # the Free Software Foundation; either version 2.1 of the License, or
  13. # (at your option) any later version.
  14. #
  15. # This program is distributed in the hope that it will be useful,
  16. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. # GNU Lesser General Public License for more details.
  19. #
  20. # You should have received a copy of the GNU Lesser General Public License along
  21. # with this program; if not, write to the Free Software Foundation, Inc.,
  22. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  23. #
  24. '''qvm-ls - List available domains'''
  25. from __future__ import print_function
  26. import argparse
  27. import collections
  28. import sys
  29. import textwrap
  30. import qubesadmin
  31. import qubesadmin.spinner
  32. import qubesadmin.tools
  33. import qubesadmin.utils
  34. import qubesadmin.vm
  35. #
  36. # columns
  37. #
  38. class Column(object):
  39. '''A column in qvm-ls output characterised by its head and a way
  40. to fetch a parameter describing the domain.
  41. :param str head: Column head (usually uppercase).
  42. :param str attr: Attribute, possibly complex (containing ``.``). This may \
  43. also be a callable that gets as its only argument the domain.
  44. :param str doc: Description of column (will be visible in --help-columns).
  45. '''
  46. #: collection of all columns
  47. columns = {}
  48. def __init__(self, head, attr=None, doc=None):
  49. self.ls_head = head
  50. self.__doc__ = doc
  51. # intentionally not always do set self._attr,
  52. # to cause AttributeError in self.format()
  53. if attr is not None:
  54. self._attr = attr
  55. self.__class__.columns[self.ls_head] = self
  56. def cell(self, vm):
  57. '''Format one cell.
  58. .. note::
  59. This is only for technical formatting (filling with space). If you
  60. want to subclass the :py:class:`Column` class, you should override
  61. :py:meth:`Column.format` method instead.
  62. :param qubes.vm.qubesvm.QubesVM: Domain to get a value from.
  63. :returns: string to display
  64. :rtype: str
  65. '''
  66. value = self.format(vm) or '-'
  67. return value
  68. def format(self, vm):
  69. '''Format one cell value.
  70. Return value to put in a table cell.
  71. :param qubes.vm.qubesvm.QubesVM: Domain to get a value from.
  72. :returns: Value to put, or :py:obj:`None` if no value.
  73. :rtype: str or None
  74. '''
  75. ret = None
  76. try:
  77. if isinstance(self._attr, str):
  78. ret = vm
  79. for attrseg in self._attr.split('.'):
  80. ret = getattr(ret, attrseg)
  81. elif isinstance(self._attr, collections.Callable):
  82. ret = self._attr(vm)
  83. except (AttributeError, ZeroDivisionError):
  84. # division by 0 may be caused by arithmetic in callable attr
  85. return None
  86. if ret is None:
  87. return None
  88. return str(ret)
  89. def __repr__(self):
  90. return '{}(head={!r})'.format(self.__class__.__name__,
  91. self.ls_head)
  92. def __eq__(self, other):
  93. return self.ls_head == other.ls_head
  94. def __lt__(self, other):
  95. return self.ls_head < other.ls_head
  96. class PropertyColumn(Column):
  97. '''Column that displays value from property (:py:class:`property` or
  98. :py:class:`qubes.property`) of domain.
  99. :param name: Name of VM property.
  100. '''
  101. def __init__(self, name):
  102. ls_head = name.replace('_', '-').upper()
  103. super(PropertyColumn, self).__init__(
  104. head=ls_head,
  105. attr=name)
  106. def __repr__(self):
  107. return '{}(head={!r}'.format(
  108. self.__class__.__name__,
  109. self.ls_head)
  110. def process_vm(vm):
  111. '''Process VM object to find all listable properties.
  112. :param qubesmgmt.vm.QubesVM vm: VM object.
  113. '''
  114. for prop_name in vm.property_list():
  115. PropertyColumn(prop_name)
  116. def flag(field):
  117. '''Mark method as flag field.
  118. :param int field: Which field to fill (counted from 1)
  119. '''
  120. def decorator(obj):
  121. # pylint: disable=missing-docstring
  122. obj.field = field
  123. return obj
  124. return decorator
  125. def simple_flag(field, letter, attr, doc=None):
  126. '''Create simple, binary flag.
  127. :param str attr: Attribute name to check. If result is true, flag is fired.
  128. :param str letter: The letter to show.
  129. '''
  130. def helper(self, vm):
  131. # pylint: disable=missing-docstring,unused-argument
  132. try:
  133. value = getattr(vm, attr)
  134. except AttributeError:
  135. value = False
  136. if value:
  137. return letter[0]
  138. helper.__doc__ = doc
  139. helper.field = field
  140. return helper
  141. class FlagsColumn(Column):
  142. '''Some fancy flags that describe general status of the domain.'''
  143. # pylint: disable=no-self-use
  144. def __init__(self):
  145. super(FlagsColumn, self).__init__(
  146. head='FLAGS',
  147. doc=self.__class__.__doc__)
  148. @flag(1)
  149. def type(self, vm):
  150. '''Type of domain.
  151. 0 AdminVM (AKA Dom0)
  152. aA AppVM
  153. dD DisposableVM
  154. sS StandaloneVM
  155. tT TemplateVM
  156. When it is HVM (optimised VM), the letter is capital.
  157. '''
  158. type_codes = {
  159. 'AdminVM': '0',
  160. 'TemplateVM': 't',
  161. 'AppVM': 'a',
  162. 'StandaloneVM': 's',
  163. 'DispVM': 'd',
  164. }
  165. ret = type_codes.get(vm.klass, None)
  166. if ret == '0':
  167. return ret
  168. if ret is not None:
  169. if getattr(vm, 'virt_mode', 'pv') == 'hvm':
  170. return ret.upper()
  171. return ret
  172. @flag(2)
  173. def power(self, vm):
  174. '''Current power state.
  175. r running
  176. t transient
  177. p paused
  178. s suspended
  179. h halting
  180. d dying
  181. c crashed
  182. ? unknown
  183. '''
  184. state = vm.get_power_state().lower()
  185. if state == 'unknown':
  186. return '?'
  187. elif state in ('running', 'transient', 'paused', 'suspended',
  188. 'halting', 'dying', 'crashed'):
  189. return state[0]
  190. updateable = simple_flag(3, 'U', 'updateable',
  191. doc='If the domain is updateable.')
  192. provides_network = simple_flag(4, 'N', 'provides_network',
  193. doc='If the domain provides network.')
  194. installed_by_rpm = simple_flag(5, 'R', 'installed_by_rpm',
  195. doc='If the domain is installed by RPM.')
  196. internal = simple_flag(6, 'i', 'internal',
  197. doc='If the domain is internal (not normally shown, no appmenus).')
  198. debug = simple_flag(7, 'D', 'debug',
  199. doc='If the domain is being debugged.')
  200. autostart = simple_flag(8, 'A', 'autostart',
  201. doc='If the domain is marked for autostart.')
  202. # TODO (not sure if really):
  203. # include in backups
  204. # uses_custom_config
  205. def _no_flag(self, vm):
  206. '''Reserved for future use.'''
  207. @classmethod
  208. def get_flags(cls):
  209. '''Get all flags as list.
  210. Holes between flags are filled with :py:meth:`_no_flag`.
  211. :rtype: list
  212. '''
  213. flags = {}
  214. for mycls in cls.__mro__:
  215. for attr in mycls.__dict__.values():
  216. if not hasattr(attr, 'field'):
  217. continue
  218. if attr.field in flags:
  219. continue
  220. flags[attr.field] = attr
  221. return [(flags[i] if i in flags else cls._no_flag)
  222. for i in range(1, max(flags) + 1)]
  223. def format(self, vm):
  224. return ''.join((flag(self, vm) or '-') for flag in self.get_flags())
  225. def calc_size(vm, volume_name):
  226. ''' Calculates the volume size in MB '''
  227. try:
  228. return vm.volumes[volume_name].size // 1024 // 1024
  229. except KeyError:
  230. return 0
  231. def calc_usage(vm, volume_name):
  232. ''' Calculates the volume usage in MB '''
  233. try:
  234. return vm.volumes[volume_name].usage // 1024 // 1024
  235. except KeyError:
  236. return 0
  237. def calc_used(vm, volume_name):
  238. ''' Calculates the volume usage in percent '''
  239. size = calc_size(vm, volume_name)
  240. if size == 0:
  241. return 0
  242. usage = calc_usage(vm, volume_name)
  243. return usage * 100 // size
  244. # todo maxmem
  245. Column('STATE',
  246. attr=(lambda vm: vm.get_power_state()),
  247. doc='Current power state.')
  248. Column('CLASS',
  249. attr=(lambda vm: vm.klass),
  250. doc='Class of the qube.')
  251. Column('GATEWAY',
  252. attr='netvm.gateway',
  253. doc='Network gateway.')
  254. Column('MEMORY',
  255. attr=(lambda vm: vm.get_mem() / 1024 if vm.is_running() else None),
  256. doc='Memory currently used by VM')
  257. Column('DISK',
  258. attr=(lambda vm: vm.get_disk_utilization() // 1024 // 1024),
  259. doc='Total disk utilisation.')
  260. Column('PRIV-CURR',
  261. attr=(lambda vm: calc_usage(vm, 'private')),
  262. doc='Disk utilisation by private image (/home, /usr/local).')
  263. Column('PRIV-MAX',
  264. attr=(lambda vm: calc_size(vm, 'private')),
  265. doc='Maximum available space for private image.')
  266. Column('PRIV-USED',
  267. attr=(lambda vm: calc_used(vm, 'private')),
  268. doc='Disk utilisation by private image as a percentage of available space.')
  269. Column('ROOT-CURR',
  270. attr=(lambda vm: calc_usage(vm, 'root')),
  271. doc='Disk utilisation by root image (/usr, /lib, /etc, ...).')
  272. Column('ROOT-MAX',
  273. attr=(lambda vm: calc_size(vm, 'root')),
  274. doc='Maximum available space for root image.')
  275. Column('ROOT-USED',
  276. attr=(lambda vm: calc_used(vm, 'root')),
  277. doc='Disk utilisation by root image as a percentage of available space.')
  278. FlagsColumn()
  279. class Table(object):
  280. '''Table that is displayed to the user.
  281. :param qubes.Qubes app: Qubes application object.
  282. :param list colnames: Names of the columns (need not to be uppercase).
  283. '''
  284. def __init__(self, app, colnames, spinner, raw_data=False):
  285. self.app = app
  286. self.columns = tuple(Column.columns[col.upper()] for col in colnames)
  287. self.spinner = spinner
  288. self.raw_data = raw_data
  289. def get_head(self):
  290. '''Get table head data (all column heads).'''
  291. return [col.ls_head for col in self.columns]
  292. def get_row(self, vm):
  293. '''Get single table row data (all columns for one domain).'''
  294. ret = []
  295. for col in self.columns:
  296. ret.append(col.cell(vm))
  297. self.spinner.update()
  298. return ret
  299. def write_table(self, stream=sys.stdout):
  300. '''Write whole table to file-like object.
  301. :param file stream: Stream to write the table to.
  302. '''
  303. table_data = []
  304. if not self.raw_data:
  305. self.spinner.show('please wait...')
  306. table_data.append(self.get_head())
  307. self.spinner.update()
  308. for vm in sorted(self.app.domains):
  309. table_data.append(self.get_row(vm))
  310. self.spinner.hide()
  311. qubesadmin.tools.print_table(table_data, stream=stream)
  312. else:
  313. for vm in sorted(self.app.domains):
  314. stream.write('|'.join(self.get_row(vm)) + '\n')
  315. #: Available formats. Feel free to plug your own one.
  316. formats = {
  317. 'simple': ('name', 'state', 'class', 'label', 'template', 'netvm'),
  318. 'network': ('name', 'state', 'netvm', 'ip', 'ipback', 'gateway'),
  319. 'full': ('name', 'state', 'class', 'label', 'qid', 'xid', 'uuid'),
  320. # 'perf': ('name', 'state', 'cpu', 'memory'),
  321. 'disk': ('name', 'state', 'disk',
  322. 'priv-curr', 'priv-max', 'priv-used',
  323. 'root-curr', 'root-max', 'root-used'),
  324. }
  325. class _HelpColumnsAction(argparse.Action):
  326. '''Action for argument parser that displays all columns and exits.'''
  327. # pylint: disable=redefined-builtin
  328. def __init__(self,
  329. option_strings,
  330. dest=argparse.SUPPRESS,
  331. default=argparse.SUPPRESS,
  332. help='list all available columns with short descriptions and exit'):
  333. super(_HelpColumnsAction, self).__init__(
  334. option_strings=option_strings,
  335. dest=dest,
  336. default=default,
  337. nargs=0,
  338. help=help)
  339. def __call__(self, parser, namespace, values, option_string=None):
  340. width = max(len(column.ls_head) for column in Column.columns.values())
  341. wrapper = textwrap.TextWrapper(width=80,
  342. initial_indent=' ', subsequent_indent=' ' * (width + 6))
  343. text = 'Available columns:\n' + '\n'.join(
  344. wrapper.fill('{head:{width}s} {doc}'.format(
  345. head=column.ls_head,
  346. doc=column.__doc__ or '',
  347. width=width))
  348. for column in sorted(Column.columns.values()))
  349. text += '\n\nAdditionally any VM property may be used as a column, ' \
  350. 'see qvm-prefs --help-properties for available values'
  351. parser.exit(message=text + '\n')
  352. class _HelpFormatsAction(argparse.Action):
  353. '''Action for argument parser that displays all formats and exits.'''
  354. # pylint: disable=redefined-builtin
  355. def __init__(self,
  356. option_strings,
  357. dest=argparse.SUPPRESS,
  358. default=argparse.SUPPRESS,
  359. help='list all available formats with their definitions and exit'):
  360. super(_HelpFormatsAction, self).__init__(
  361. option_strings=option_strings,
  362. dest=dest,
  363. default=default,
  364. nargs=0,
  365. help=help)
  366. def __call__(self, parser, namespace, values, option_string=None):
  367. width = max(len(fmt) for fmt in formats)
  368. text = 'Available formats:\n' + ''.join(
  369. ' {fmt:{width}s} {columns}\n'.format(
  370. fmt=fmt, columns=','.join(formats[fmt]).upper(), width=width)
  371. for fmt in sorted(formats))
  372. parser.exit(message=text)
  373. def get_parser():
  374. '''Create :py:class:`argparse.ArgumentParser` suitable for
  375. :program:`qvm-ls`.
  376. '''
  377. # parser creation is delayed to get all the columns that are scattered
  378. # thorough the modules
  379. wrapper = textwrap.TextWrapper(width=80, break_on_hyphens=False,
  380. initial_indent=' ', subsequent_indent=' ')
  381. parser = qubesadmin.tools.QubesArgumentParser(
  382. formatter_class=argparse.RawTextHelpFormatter,
  383. description='List Qubes domains and their parametres.',
  384. epilog='available formats (see --help-formats):\n{}\n\n'
  385. 'available columns (see --help-columns):\n{}'.format(
  386. wrapper.fill(', '.join(sorted(formats.keys()))),
  387. wrapper.fill(', '.join(sorted(sorted(Column.columns.keys()))))))
  388. parser.add_argument('--help-columns', action=_HelpColumnsAction)
  389. parser.add_argument('--help-formats', action=_HelpFormatsAction)
  390. parser_formats = parser.add_mutually_exclusive_group()
  391. parser_formats.add_argument('--format', '-o', metavar='FORMAT',
  392. action='store', choices=formats.keys(), default='simple',
  393. help='preset format')
  394. parser_formats.add_argument('--fields', '-O', metavar='FIELD,...',
  395. action='store',
  396. help='user specified format (see available columns below)')
  397. parser.add_argument('--raw-data', action='store_true',
  398. help='Display specify data of specified VMs. Intended for '
  399. 'bash-parsing.')
  400. parser.add_argument('--spinner',
  401. action='store_true', dest='spinner',
  402. help='reenable spinner')
  403. parser.add_argument('--no-spinner',
  404. action='store_false', dest='spinner',
  405. help='disable spinner')
  406. parser.set_defaults(spinner=True)
  407. # parser.add_argument('--conf', '-c',
  408. # action='store', metavar='CFGFILE',
  409. # help='Qubes config file')
  410. return parser
  411. def main(args=None, app=None):
  412. '''Main routine of :program:`qvm-ls`.
  413. :param list args: Optional arguments to override those delivered from \
  414. command line.
  415. :param app: Operate on given app object instead of instantiating new one.
  416. '''
  417. parser = get_parser()
  418. try:
  419. args = parser.parse_args(args, app=app)
  420. except qubesadmin.exc.QubesException as e:
  421. parser.print_error(str(e))
  422. return 1
  423. if args.fields:
  424. columns = [col.strip() for col in args.fields.split(',')]
  425. else:
  426. columns = formats[args.format]
  427. # assume unknown columns are VM properties
  428. for col in columns:
  429. if col.upper() not in Column.columns:
  430. PropertyColumn(col.lower())
  431. if args.spinner and not args.raw_data:
  432. # we need Enterprise Edition™, since it's the only one that detects TTY
  433. # and uses dots if we are redirected somewhere else
  434. spinner = qubesadmin.spinner.QubesSpinnerEnterpriseEdition(sys.stderr)
  435. else:
  436. spinner = qubesadmin.spinner.DummySpinner(sys.stderr)
  437. table = Table(args.app, columns, spinner, args.raw_data)
  438. table.write_table(sys.stdout)
  439. return 0
  440. if __name__ == '__main__':
  441. sys.exit(main())