qvm_ls.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. #!/usr/bin/python -O
  2. # vim: fileencoding=utf-8
  3. # pylint: disable=too-few-public-methods
  4. #
  5. # The Qubes OS Project, https://www.qubes-os.org/
  6. #
  7. # Copyright (C) 2015 Joanna Rutkowska <joanna@invisiblethingslab.com>
  8. # Copyright (C) 2015 Wojtek Porczyk <woju@invisiblethingslab.com>
  9. #
  10. # This program is free software; you can redistribute it and/or modify
  11. # it under the terms of the GNU General Public License as published by
  12. # the Free Software Foundation; either version 2 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 General Public License for more details.
  19. #
  20. # You should have received a copy of the GNU 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 __builtin__
  27. import argparse
  28. import collections
  29. import os
  30. import sys
  31. import textwrap
  32. import qubes
  33. import qubes.config
  34. import qubes.utils
  35. #
  36. # columns
  37. #
  38. class Column(object):
  39. '''A column in qvm-ls output characterised by its head, a width and a way
  40. to fetch a parameter describing the domain.
  41. :param str head: Column head (usually uppercase).
  42. :param int width: Column width.
  43. :param str attr: Attribute, possibly complex (containing ``.``). This may \
  44. also be a callable that gets as its only argument the domain.
  45. :param str fmt: if specified, used as base for :py:meth:`str.format` for \
  46. column's value
  47. :param str doc: Description of column (will be visible in --help-columns).
  48. '''
  49. #: collection of all columns
  50. columns = {}
  51. def __init__(self, head, width=0, attr=None, fmt=None, doc=None):
  52. self.ls_head = head
  53. self.ls_width = max(width, len(self.ls_head) + 1)
  54. self._fmt = fmt
  55. self.__doc__ = doc if doc is None else qubes.utils.format_doc(doc)
  56. # intentionally not always do set self._attr,
  57. # to cause AttributeError in self.format()
  58. if attr is not None:
  59. self._attr = attr
  60. self.__class__.columns[self.ls_head] = self
  61. def cell(self, vm):
  62. '''Format one cell.
  63. .. note::
  64. This is only for technical formatting (filling with space). If you
  65. want to subclass the :py:class:`Column` class, you should override
  66. :py:meth:`Column.format` method instead.
  67. :param qubes.vm.qubesvm.QubesVM: Domain to get a value from.
  68. :returns: string that is at least as wide as needed to fill table row.
  69. :rtype: str
  70. '''
  71. value = self.format(vm) or '-'
  72. return value.ljust(self.ls_width)
  73. def format(self, vm):
  74. '''Format one cell value.
  75. Return value to put in a table cell.
  76. :param qubes.vm.qubesvm.QubesVM: Domain to get a value from.
  77. :returns: Value to put, or :py:obj:`None` if no value .
  78. :rtype: str or None
  79. '''
  80. ret = None
  81. try:
  82. if isinstance(self._attr, basestring):
  83. ret = vm
  84. for attrseg in self._attr.split('.'):
  85. ret = getattr(ret, attrseg)
  86. elif isinstance(self._attr, collections.Callable):
  87. ret = self._attr(vm)
  88. except (AttributeError, ZeroDivisionError):
  89. # division by 0 may be caused by arithmetic in callable attr
  90. return None
  91. if ret is None:
  92. return None
  93. if self._fmt is not None:
  94. return self._fmt.format(ret)
  95. # late import to avoid circular import
  96. # pylint: disable=redefined-outer-name
  97. import qubes.vm
  98. if isinstance(ret, (qubes.vm.BaseVM, qubes.Label)):
  99. return ret.name
  100. return ret
  101. def __repr__(self):
  102. return '{}(head={!r}, width={!r})'.format(self.__class__.__name__,
  103. self.ls_head, self.ls_width)
  104. def __eq__(self, other):
  105. return self.ls_head == other.ls_head
  106. def __lt__(self, other):
  107. return self.ls_head < other.ls_head
  108. def column(width=0, head=None, fmt=None):
  109. '''Mark function or plain property as valid column in :program:`qvm-ls`.
  110. By default all instances of :py:class:`qubes.property` are valid.
  111. :param int width: Column width
  112. :param str head: Column head (default: take property's name)
  113. '''
  114. def decorator(obj):
  115. # pylint: disable=missing-docstring
  116. # we keep hints on fget, so the order of decorators does not matter
  117. holder = obj.fget if isinstance(obj, __builtin__.property) else obj
  118. try:
  119. holder.ls_head = head or holder.__name__.replace('_', '-').upper()
  120. except AttributeError:
  121. raise TypeError('Cannot find default column name '
  122. 'for a strange object {!r}'.format(obj))
  123. holder.ls_width = max(width, len(holder.ls_head) + 1)
  124. holder.ls_fmt = fmt
  125. return obj
  126. return decorator
  127. class PropertyColumn(Column):
  128. '''Column that displays value from property (:py:class:`property` or
  129. :py:class:`qubes.property`) of domain.
  130. You shouldn't use this class directly, see :py:func:`column` decorator.
  131. :param holder: Holder of magic attributes.
  132. '''
  133. def __init__(self, holder):
  134. super(PropertyColumn, self).__init__(
  135. head=holder.ls_head,
  136. width=holder.ls_width,
  137. doc=holder.__doc__)
  138. self.holder = holder
  139. def format(self, vm):
  140. try:
  141. value = getattr(vm, self.holder.__name__)
  142. except AttributeError:
  143. return None
  144. if not hasattr(self.holder, 'ls_fmt') or self.holder.ls_fmt is None:
  145. return value
  146. return self.holder.ls_fmt.format(
  147. getattr(vm, self.holder.__name__)).ljust(
  148. self.ls_width)
  149. def __repr__(self):
  150. return '{}(head={!r}, width={!r} holder={!r})'.format(
  151. self.__class__.__name__,
  152. self.ls_head,
  153. self.ls_width,
  154. self.holder)
  155. def process_class(cls):
  156. '''Process class after definition to find all listable properties.
  157. It is used in metaclass of the domain.
  158. :param qubes.vm.BaseVMMeta cls: Class to round up.
  159. '''
  160. for prop in cls.__dict__.values():
  161. holder = prop.fget if isinstance(prop, __builtin__.property) else prop
  162. if not hasattr(holder, 'ls_head') or holder.ls_head is None:
  163. continue
  164. for col in Column.columns.values():
  165. if not isinstance(col, PropertyColumn):
  166. continue
  167. if col.holder.__name__ != holder.__name__:
  168. continue
  169. if col.ls_head != holder.ls_head:
  170. raise TypeError('Found column head mismatch in class {!r} '
  171. '({!r} != {!r})'.format(cls.__name__,
  172. holder.ls_head, col.ls_head))
  173. if col.ls_width != holder.ls_width:
  174. raise TypeError('Found column width mismatch in class {!r} '
  175. '({!r} != {!r})'.format(cls.__name__,
  176. holder.ls_width, col.ls_width))
  177. PropertyColumn(holder)
  178. def flag(field):
  179. '''Mark method as flag field.
  180. :param int field: Which field to fill (counted from 1)
  181. '''
  182. def decorator(obj):
  183. # pylint: disable=missing-docstring
  184. obj.field = field
  185. return obj
  186. return decorator
  187. def simple_flag(field, letter, attr, doc=None):
  188. '''Create simple, binary flag.
  189. :param str attr: Attribute name to check. If result is true, flag is fired.
  190. :param str letter: The letter to show.
  191. '''
  192. def helper(self, vm):
  193. # pylint: disable=missing-docstring,unused-argument
  194. try:
  195. value = getattr(vm, attr)
  196. except AttributeError:
  197. value = False
  198. if value:
  199. return letter[0]
  200. helper.__doc__ = doc
  201. helper.field = field
  202. return helper
  203. class StatusColumn(Column):
  204. '''Some fancy flags that describe general status of the domain.'''
  205. # pylint: disable=no-self-use
  206. def __init__(self):
  207. super(StatusColumn, self).__init__(
  208. head='STATUS',
  209. width=len(self.get_flags()) + 1,
  210. doc=self.__class__.__doc__)
  211. @flag(1)
  212. def type(self, vm):
  213. '''Type of domain.
  214. 0 AdminVM (AKA Dom0)
  215. aA AppVM
  216. dD DisposableVM
  217. sS StandaloneVM
  218. tT TemplateVM
  219. When it is HVM (optimised VM), the letter is capital.
  220. '''
  221. # late import because of circular dependency
  222. # pylint: disable=redefined-outer-name
  223. import qubes.vm
  224. import qubes.vm.adminvm
  225. import qubes.vm.appvm
  226. import qubes.vm.dispvm
  227. import qubes.vm.hvm
  228. import qubes.vm.qubesvm
  229. import qubes.vm.templatevm
  230. if isinstance(vm, qubes.vm.adminvm.AdminVM):
  231. return '0'
  232. ret = None
  233. # TODO right order, depending on inheritance
  234. if isinstance(vm, qubes.vm.templatevm.TemplateVM):
  235. ret = 't'
  236. if isinstance(vm, qubes.vm.appvm.AppVM):
  237. ret = 'a'
  238. # if isinstance(vm, qubes.vm.standalonevm.StandaloneVM):
  239. # ret = 's'
  240. if isinstance(vm, qubes.vm.dispvm.DispVM):
  241. ret = 'd'
  242. if ret is not None:
  243. if isinstance(vm, qubes.vm.hvm.HVM):
  244. return ret.upper()
  245. else:
  246. return ret
  247. @flag(2)
  248. def power(self, vm):
  249. '''Current power state.
  250. r running
  251. t transient
  252. p paused
  253. s suspended
  254. h halting
  255. d dying
  256. c crashed
  257. ? unknown
  258. '''
  259. state = vm.get_power_state().lower()
  260. if state == 'unknown':
  261. return '?'
  262. elif state in ('running', 'transient', 'paused', 'suspended',
  263. 'halting', 'dying', 'crashed'):
  264. return state[0]
  265. updateable = simple_flag(3, 'U', 'updateable',
  266. doc='If the domain is updateable.')
  267. provides_network = simple_flag(4, 'N', 'provides_network',
  268. doc='If the domain provides network.')
  269. installed_by_rpm = simple_flag(5, 'R', 'installed_by_rpm',
  270. doc='If the domain is installed by RPM.')
  271. internal = simple_flag(6, 'i', 'internal',
  272. doc='If the domain is internal (not normally shown, no appmenus).')
  273. debug = simple_flag(7, 'D', 'debug',
  274. doc='If the domain is being debugged.')
  275. autostart = simple_flag(8, 'A', 'autostart',
  276. doc='If the domain is marked for autostart.')
  277. # TODO (not sure if really):
  278. # include in backups
  279. # uses_custom_config
  280. def _no_flag(self, vm):
  281. '''Reserved for future use.'''
  282. @classmethod
  283. def get_flags(cls):
  284. '''Get all flags as list.
  285. Holes between flags are filled with :py:meth:`_no_flag`.
  286. :rtype: list
  287. '''
  288. flags = {}
  289. for mycls in cls.__mro__:
  290. for attr in mycls.__dict__.values():
  291. if not hasattr(attr, 'field'):
  292. continue
  293. if attr.field in flags:
  294. continue
  295. flags[attr.field] = attr
  296. return [(flags[i] if i in flags else cls._no_flag)
  297. for i in range(1, max(flags) + 1)]
  298. def format(self, vm):
  299. return ''.join((flag(self, vm) or '-') for flag in self.get_flags())
  300. # todo maxmem
  301. Column('GATEWAY', width=15,
  302. attr='netvm.gateway',
  303. doc='Network gateway.')
  304. Column('MEMORY', width=5,
  305. attr=(lambda vm: vm.get_mem()/1024 if vm.is_running() else None),
  306. doc='Memory currently used by VM')
  307. Column('DISK', width=5,
  308. attr=(lambda vm: vm.get_disk_utilization()/1024/1024),
  309. doc='Total disk utilisation.')
  310. Column('PRIV-CURR', width=5,
  311. attr=(lambda vm: vm.get_disk_utilization_private_img()/1024/1024),
  312. fmt='{:.0f}',
  313. doc='Disk utilisation by private image (/home, /usr/local).')
  314. Column('PRIV-MAX', width=5,
  315. attr=(lambda vm: vm.get_private_img_sz()/1024/1024),
  316. fmt='{:.0f}',
  317. doc='Maximum available space for private image.')
  318. Column('PRIV-USED', width=5,
  319. attr=(lambda vm: vm.get_disk_utilization_private_img() * 100
  320. / vm.get_private_img_sz()),
  321. fmt='{:.0f}',
  322. doc='Disk utilisation by private image as a percentage of available space.')
  323. Column('ROOT-CURR', width=5,
  324. attr=(lambda vm: vm.get_disk_utilization_private_img()/1024/1024),
  325. fmt='{:.0f}',
  326. doc='Disk utilisation by root image (/usr, /lib, /etc, ...).')
  327. Column('ROOT-MAX', width=5,
  328. attr=(lambda vm: vm.get_private_img_sz()/1024/1024),
  329. fmt='{:.0f}',
  330. doc='Maximum available space for root image.')
  331. Column('ROOT-USED', width=5,
  332. attr=(lambda vm: vm.get_disk_utilization_private_img() * 100
  333. / vm.get_private_img_sz()),
  334. fmt='{:.0f}',
  335. doc='Disk utilisation by root image as a percentage of available space.')
  336. StatusColumn()
  337. class Table(object):
  338. '''Table that is displayed to the user.
  339. :param qubes.Qubes app: Qubes application object.
  340. :param list colnames: Names of the columns (need not to be uppercase).
  341. '''
  342. def __init__(self, app, colnames):
  343. self.app = app
  344. self.columns = tuple(Column.columns[col.upper()] for col in colnames)
  345. def format_head(self):
  346. '''Format table head (all column heads).'''
  347. return ''.join('{head:{width}s}'.format(
  348. head=col.ls_head, width=col.ls_width)
  349. for col in self.columns[:-1]) + \
  350. self.columns[-1].ls_head
  351. def format_row(self, vm):
  352. '''Format single table row (all columns for one domain).'''
  353. return ''.join(col.cell(vm) for col in self.columns)
  354. def write_table(self, stream=sys.stdout):
  355. '''Write whole table to file-like object.
  356. :param file stream: Stream to write the table to.
  357. '''
  358. stream.write(self.format_head() + '\n')
  359. for vm in self.app.domains:
  360. stream.write(self.format_row(vm) + '\n')
  361. #: Available formats. Feel free to plug your own one.
  362. formats = {
  363. 'simple': ('name', 'status', 'label', 'template', 'netvm'),
  364. 'network': ('name', 'status', 'netvm', 'ip', 'ipback', 'gateway'),
  365. 'full': ('name', 'status', 'label', 'qid', 'xid', 'uuid'),
  366. # 'perf': ('name', 'status', 'cpu', 'memory'),
  367. 'disk': ('name', 'status', 'disk',
  368. 'priv-curr', 'priv-max', 'priv-used',
  369. 'root-curr', 'root-max', 'root-used'),
  370. }
  371. class _HelpColumnsAction(argparse.Action):
  372. '''Action for argument parser that displays all columns and exits.'''
  373. # pylint: disable=redefined-builtin
  374. def __init__(self,
  375. option_strings,
  376. dest=argparse.SUPPRESS,
  377. default=argparse.SUPPRESS,
  378. help='list all available columns with short descriptions and exit'):
  379. super(_HelpColumnsAction, self).__init__(
  380. option_strings=option_strings,
  381. dest=dest,
  382. default=default,
  383. nargs=0,
  384. help=help)
  385. def __call__(self, parser, namespace, values, option_string=None):
  386. width = max(len(column.ls_head) for column in Column.columns.values())
  387. wrapper = textwrap.TextWrapper(width=80,
  388. initial_indent=' ', subsequent_indent=' ' * (width + 6))
  389. text = 'Available columns:\n' + '\n'.join(
  390. wrapper.fill('{head:{width}s} {doc}'.format(
  391. head=column.ls_head,
  392. doc=column.__doc__ or '',
  393. width=width))
  394. for column in sorted(Column.columns.values()))
  395. parser.exit(message=text + '\n')
  396. class _HelpFormatsAction(argparse.Action):
  397. '''Action for argument parser that displays all formats and exits.'''
  398. # pylint: disable=redefined-builtin
  399. def __init__(self,
  400. option_strings,
  401. dest=argparse.SUPPRESS,
  402. default=argparse.SUPPRESS,
  403. help='list all available formats with their definitions and exit'):
  404. super(_HelpFormatsAction, self).__init__(
  405. option_strings=option_strings,
  406. dest=dest,
  407. default=default,
  408. nargs=0,
  409. help=help)
  410. def __call__(self, parser, namespace, values, option_string=None):
  411. width = max(len(fmt) for fmt in formats)
  412. text = 'Available formats:\n' + ''.join(
  413. ' {fmt:{width}s} {columns}\n'.format(
  414. fmt=fmt, columns=','.join(formats[fmt]).upper(), width=width)
  415. for fmt in sorted(formats))
  416. parser.exit(message=text)
  417. def get_parser():
  418. '''Create :py:class:`argparse.ArgumentParser` suitable for
  419. :program:`qvm-ls`.
  420. '''
  421. # parser creation is delayed to get all the columns that are scattered
  422. # thorough the modules
  423. wrapper = textwrap.TextWrapper(width=80, break_on_hyphens=False,
  424. initial_indent=' ', subsequent_indent=' ')
  425. parser = argparse.ArgumentParser(
  426. formatter_class=argparse.RawTextHelpFormatter,
  427. description='List Qubes domains and their parametres.',
  428. epilog='available formats (see --help-formats):\n{}\n\n'
  429. 'available columns (see --help-columns):\n{}'.format(
  430. wrapper.fill(', '.join(sorted(formats.keys()))),
  431. wrapper.fill(', '.join(sorted(sorted(Column.columns.keys()))))))
  432. parser.add_argument('--help-columns', action=_HelpColumnsAction)
  433. parser.add_argument('--help-formats', action=_HelpFormatsAction)
  434. parser_formats = parser.add_mutually_exclusive_group()
  435. parser_formats.add_argument('--format', '-o', metavar='FORMAT',
  436. action='store', choices=formats.keys(),
  437. help='preset format')
  438. parser_formats.add_argument('--fields', '-O', metavar='FIELD,...',
  439. action='store',
  440. help='user specified format (see available columns below)')
  441. # parser.add_argument('--conf', '-c',
  442. # action='store', metavar='CFGFILE',
  443. # help='Qubes config file')
  444. parser.add_argument('--xml', metavar='XMLFILE',
  445. action='store',
  446. help='Qubes store file')
  447. parser.set_defaults(
  448. qubesxml=os.path.join(qubes.config.system_path['qubes_base_dir'],
  449. qubes.config.system_path['qubes_store_filename']),
  450. format='simple')
  451. return parser
  452. def main(args=None):
  453. '''Main routine of :program:`qvm-ls`.
  454. :param list args: Optional arguments to override those delivered from \
  455. command line.
  456. '''
  457. parser = get_parser()
  458. args = parser.parse_args(args)
  459. app = qubes.Qubes(args.xml)
  460. if args.fields:
  461. columns = [col.strip() for col in args.fields.split(',')]
  462. for col in columns:
  463. if col.upper() not in Column.columns:
  464. parser.error('no such column: {!r}'.format(col))
  465. else:
  466. columns = formats[args.format]
  467. table = Table(app, columns)
  468. table.write_table(sys.stdout)
  469. return True
  470. if __name__ == '__main__':
  471. sys.exit(not main())