qvm_ls.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  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.abc
  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. import qubesadmin.exc
  36. #
  37. # columns
  38. #
  39. class Column(object):
  40. '''A column in qvm-ls output characterised by its head and a way
  41. to fetch a parameter describing the domain.
  42. :param str head: Column head (usually uppercase).
  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 doc: Description of column (will be visible in --help-columns).
  46. '''
  47. #: collection of all columns
  48. columns = {}
  49. def __init__(self, head, attr=None, doc=None):
  50. self.ls_head = head
  51. self.__doc__ = doc
  52. # intentionally not always do set self._attr,
  53. # to cause AttributeError in self.format()
  54. if attr is not None:
  55. self._attr = attr
  56. self.__class__.columns[self.ls_head] = self
  57. def cell(self, vm, insertion=0):
  58. '''Format one cell.
  59. .. note::
  60. This is only for technical formatting (filling with space). If you
  61. want to subclass the :py:class:`Column` class, you should override
  62. :py:meth:`Column.format` method instead.
  63. :param qubes.vm.qubesvm.QubesVM: Domain to get a value from.
  64. :param int insertion: Intending to shift the value to the right.
  65. :returns: string to display
  66. :rtype: str
  67. '''
  68. value = self.format(vm) or '-'
  69. if insertion > 0 and self.ls_head == 'NAME':
  70. value = '└─' + value
  71. value = ' ' * (insertion-1) + value
  72. return value
  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, str):
  83. ret = vm
  84. for attrseg in self._attr.split('.'):
  85. ret = getattr(ret, attrseg)
  86. elif isinstance(self._attr, collections.abc.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. return str(ret)
  94. def __repr__(self):
  95. return '{}(head={!r})'.format(self.__class__.__name__,
  96. self.ls_head)
  97. def __eq__(self, other):
  98. return self.ls_head == other.ls_head
  99. def __lt__(self, other):
  100. return self.ls_head < other.ls_head
  101. class PropertyColumn(Column):
  102. '''Column that displays value from property (:py:class:`property` or
  103. :py:class:`qubes.property`) of domain.
  104. :param name: Name of VM property.
  105. '''
  106. def __init__(self, name):
  107. ls_head = name.replace('_', '-').upper()
  108. super().__init__(head=ls_head, attr=name)
  109. def __repr__(self):
  110. return '{}(head={!r}'.format(
  111. self.__class__.__name__,
  112. self.ls_head)
  113. def process_vm(vm):
  114. '''Process VM object to find all listable properties.
  115. :param qubesmgmt.vm.QubesVM vm: VM object.
  116. '''
  117. for prop_name in vm.property_list():
  118. PropertyColumn(prop_name)
  119. def flag(field):
  120. '''Mark method as flag field.
  121. :param int field: Which field to fill (counted from 1)
  122. '''
  123. def decorator(obj):
  124. # pylint: disable=missing-docstring
  125. obj.field = field
  126. return obj
  127. return decorator
  128. def simple_flag(field, letter, attr, doc=None):
  129. '''Create simple, binary flag.
  130. :param str attr: Attribute name to check. If result is true, flag is fired.
  131. :param str letter: The letter to show.
  132. '''
  133. def helper(self, vm):
  134. # pylint: disable=missing-docstring,unused-argument
  135. try:
  136. value = getattr(vm, attr)
  137. except AttributeError:
  138. value = False
  139. if value:
  140. return letter[0]
  141. helper.__doc__ = doc
  142. helper.field = field
  143. return helper
  144. class FlagsColumn(Column):
  145. '''Some fancy flags that describe general status of the domain.'''
  146. # pylint: disable=no-self-use
  147. def __init__(self):
  148. super().__init__(head='FLAGS', doc=self.__class__.__doc__)
  149. @flag(1)
  150. def type(self, vm):
  151. '''Type of domain.
  152. 0 AdminVM (AKA Dom0)
  153. aA AppVM
  154. dD DisposableVM
  155. sS StandaloneVM
  156. tT TemplateVM
  157. When it is HVM (optimised VM), the letter is capital.
  158. '''
  159. type_codes = {
  160. 'AdminVM': '0',
  161. 'TemplateVM': 't',
  162. 'AppVM': 'a',
  163. 'StandaloneVM': 's',
  164. 'DispVM': 'd',
  165. }
  166. ret = type_codes.get(vm.klass, None)
  167. if ret == '0':
  168. return ret
  169. if ret is not None:
  170. if getattr(vm, 'virt_mode', 'pv') == 'hvm':
  171. return ret.upper()
  172. return ret
  173. @flag(2)
  174. def power(self, vm):
  175. '''Current power state.
  176. r running
  177. t transient
  178. p paused
  179. s suspended
  180. h halting
  181. d dying
  182. c crashed
  183. ? unknown
  184. '''
  185. state = vm.get_power_state().lower()
  186. if state == 'unknown':
  187. return '?'
  188. if state in ('running', 'transient', 'paused', 'suspended',
  189. 'halting', 'dying', 'crashed'):
  190. return state[0]
  191. updateable = simple_flag(3, 'U', 'updateable',
  192. doc='If the domain is updateable.')
  193. provides_network = simple_flag(4, 'N', 'provides_network',
  194. doc='If the domain provides network.')
  195. installed_by_rpm = simple_flag(5, 'R', 'installed_by_rpm',
  196. doc='If the domain is installed by RPM.')
  197. internal = simple_flag(6, 'i', 'internal',
  198. doc='If the domain is internal (not normally shown, no appmenus).')
  199. debug = simple_flag(7, 'D', 'debug',
  200. doc='If the domain is being debugged.')
  201. autostart = simple_flag(8, 'A', 'autostart',
  202. doc='If the domain is marked for autostart.')
  203. # TODO (not sure if really):
  204. # include in backups
  205. # uses_custom_config
  206. def _no_flag(self, vm):
  207. '''Reserved for future use.'''
  208. @classmethod
  209. def get_flags(cls):
  210. '''Get all flags as list.
  211. Holes between flags are filled with :py:meth:`_no_flag`.
  212. :rtype: list
  213. '''
  214. flags = {}
  215. for mycls in cls.__mro__:
  216. for attr in mycls.__dict__.values():
  217. if not hasattr(attr, 'field'):
  218. continue
  219. if attr.field in flags:
  220. continue
  221. flags[attr.field] = attr
  222. return [(flags[i] if i in flags else cls._no_flag)
  223. for i in range(1, max(flags) + 1)]
  224. def format(self, vm):
  225. return ''.join((flag(self, vm) or '-') for flag in self.get_flags())
  226. def calc_size(vm, volume_name):
  227. ''' Calculates the volume size in MB '''
  228. try:
  229. return vm.volumes[volume_name].size // 1024 // 1024
  230. except KeyError:
  231. return 0
  232. def calc_usage(vm, volume_name):
  233. ''' Calculates the volume usage in MB '''
  234. try:
  235. return vm.volumes[volume_name].usage // 1024 // 1024
  236. except KeyError:
  237. return 0
  238. def calc_used(vm, volume_name):
  239. ''' Calculates the volume usage in percent '''
  240. size = calc_size(vm, volume_name)
  241. if size == 0:
  242. return 0
  243. usage = calc_usage(vm, volume_name)
  244. return '{}%'.format(usage * 100 // size)
  245. # todo maxmem
  246. Column('STATE',
  247. attr=(lambda vm: vm.get_power_state()),
  248. doc='Current power state.')
  249. Column('CLASS',
  250. attr=(lambda vm: vm.klass),
  251. doc='Class of the qube.')
  252. Column('GATEWAY',
  253. attr='netvm.gateway',
  254. doc='Network gateway.')
  255. Column('MEMORY',
  256. attr=(lambda vm: vm.get_mem() // 1024 if vm.is_running() else None),
  257. doc='Memory currently used by VM')
  258. Column('DISK',
  259. attr=(lambda vm: vm.get_disk_utilization() // 1024 // 1024),
  260. doc='Total disk utilisation.')
  261. Column('PRIV-CURR',
  262. attr=(lambda vm: calc_usage(vm, 'private')),
  263. doc='Disk utilisation by private image (/home, /usr/local).')
  264. Column('PRIV-MAX',
  265. attr=(lambda vm: calc_size(vm, 'private')),
  266. doc='Maximum available space for private image.')
  267. Column('PRIV-USED',
  268. attr=(lambda vm: calc_used(vm, 'private')),
  269. doc='Disk utilisation by private image as a percentage of available space.')
  270. Column('ROOT-CURR',
  271. attr=(lambda vm: calc_usage(vm, 'root')),
  272. doc='Disk utilisation by root image (/usr, /lib, /etc, ...).')
  273. Column('ROOT-MAX',
  274. attr=(lambda vm: calc_size(vm, 'root')),
  275. doc='Maximum available space for root image.')
  276. Column('ROOT-USED',
  277. attr=(lambda vm: calc_used(vm, 'root')),
  278. doc='Disk utilisation by root image as a percentage of available space.')
  279. FlagsColumn()
  280. class Table(object):
  281. '''Table that is displayed to the user.
  282. :param domains: Domains to include in the table.
  283. :param list colnames: Names of the columns (need not to be uppercase).
  284. '''
  285. def __init__(self, domains, colnames, spinner, raw_data=False,
  286. tree_sorted=False):
  287. self.domains = domains
  288. self.columns = tuple(Column.columns[col.upper().replace('_', '-')]
  289. for col in colnames)
  290. self.spinner = spinner
  291. self.raw_data = raw_data
  292. self.tree_sorted = tree_sorted
  293. def get_head(self):
  294. '''Get table head data (all column heads).'''
  295. return [col.ls_head for col in self.columns]
  296. def get_row(self, vm, insertion=0):
  297. '''Get single table row data (all columns for one domain).'''
  298. ret = []
  299. for col in self.columns:
  300. if self.tree_sorted and col.ls_head == 'NAME':
  301. ret.append(col.cell(vm, insertion))
  302. else:
  303. ret.append(col.cell(vm))
  304. self.spinner.update()
  305. return ret
  306. def tree_append_child(self, parent, level):
  307. '''Concatenate the network children of the vm to a list.
  308. :param qubes.vm.qubesvm.QubesVM: Parent vm of the children VMs
  309. '''
  310. childs = list()
  311. for child in parent.connected_vms:
  312. if child.provides_network and child in self.domains:
  313. childs.append((level, child))
  314. childs += self.tree_append_child(child, level+1)
  315. elif child in self.domains:
  316. childs.append((level, child))
  317. return childs
  318. def sort_to_tree(self, domains):
  319. '''Sort the domains as a network tree. It returns a list of sets. Each
  320. tuple stores the insertion of the cell name and the vm object.
  321. :param list() domains: The domains which will be sorted
  322. :return list(tuple()) tree: returns a list of tuple(insertion, vm)
  323. '''
  324. tree = list()
  325. # We need a copy of domains, because domains.remove() is not allowed
  326. # while iterating over it. Besides, the domains should be sorted anyway.
  327. sorted_doms = sorted(domains)
  328. for dom in sorted_doms:
  329. try:
  330. if dom.netvm is None and not dom.provides_network:
  331. tree.append((0, dom))
  332. domains.remove(dom)
  333. # dom0 and eventually others have no netvm attribute
  334. except (qubesadmin.exc.QubesNoSuchPropertyError, AttributeError):
  335. tree.append((0, dom))
  336. domains.remove(dom)
  337. for dom in sorted(domains):
  338. if dom.netvm is None and dom.provides_network:
  339. tree.append((0, dom))
  340. domains.remove(dom)
  341. tree += self.tree_append_child(dom, 1)
  342. return tree
  343. def write_table(self, stream=sys.stdout):
  344. '''Write whole table to file-like object.
  345. :param file stream: Stream to write the table to.
  346. '''
  347. table_data = []
  348. if not self.raw_data:
  349. self.spinner.show('please wait...')
  350. table_data.append(self.get_head())
  351. self.spinner.update()
  352. if self.tree_sorted:
  353. insertion_vm_list = self.sort_to_tree(self.domains)
  354. for insertion, vm in insertion_vm_list:
  355. table_data.append(self.get_row(vm, insertion))
  356. else:
  357. for vm in sorted(self.domains):
  358. table_data.append(self.get_row(vm))
  359. self.spinner.hide()
  360. qubesadmin.tools.print_table(table_data, stream=stream)
  361. else:
  362. for vm in sorted(self.domains):
  363. stream.write('|'.join(self.get_row(vm)) + '\n')
  364. #: Available formats. Feel free to plug your own one.
  365. formats = {
  366. 'simple': ('name', 'state', 'class', 'label', 'template', 'netvm'),
  367. 'network': ('name', 'state', 'netvm', 'ip', 'ipback', 'gateway'),
  368. 'kernel': ('name', 'state', 'class', 'template', 'kernel', 'kernelopts'),
  369. 'full': ('name', 'state', 'class', 'label', 'qid', 'xid', 'uuid'),
  370. # 'perf': ('name', 'state', 'cpu', 'memory'),
  371. 'disk': ('name', 'state', 'disk',
  372. 'priv-curr', 'priv-max', 'priv-used',
  373. 'root-curr', 'root-max', 'root-used'),
  374. }
  375. class _HelpColumnsAction(argparse.Action):
  376. '''Action for argument parser that displays all columns and exits.'''
  377. # pylint: disable=redefined-builtin
  378. def __init__(self,
  379. option_strings,
  380. dest=argparse.SUPPRESS,
  381. default=argparse.SUPPRESS,
  382. help='list all available columns with short descriptions and exit'):
  383. super().__init__(
  384. option_strings=option_strings,
  385. dest=dest,
  386. default=default,
  387. nargs=0,
  388. help=help)
  389. def __call__(self, parser, namespace, values, option_string=None):
  390. width = max(len(column.ls_head) for column in Column.columns.values())
  391. wrapper = textwrap.TextWrapper(width=80,
  392. initial_indent=' ', subsequent_indent=' ' * (width + 6))
  393. text = 'Available columns:\n' + '\n'.join(
  394. wrapper.fill('{head:{width}s} {doc}'.format(
  395. head=column.ls_head,
  396. doc=column.__doc__ or '',
  397. width=width))
  398. for column in sorted(Column.columns.values()))
  399. text += '\n\nAdditionally any VM property may be used as a column, ' \
  400. 'see qvm-prefs --help-properties for available values'
  401. parser.exit(message=text + '\n')
  402. class _HelpFormatsAction(argparse.Action):
  403. '''Action for argument parser that displays all formats and exits.'''
  404. # pylint: disable=redefined-builtin
  405. def __init__(self,
  406. option_strings,
  407. dest=argparse.SUPPRESS,
  408. default=argparse.SUPPRESS,
  409. help='list all available formats with their definitions and exit'):
  410. super().__init__(
  411. option_strings=option_strings,
  412. dest=dest,
  413. default=default,
  414. nargs=0,
  415. help=help)
  416. def __call__(self, parser, namespace, values, option_string=None):
  417. width = max(len(fmt) for fmt in formats)
  418. text = 'Available formats:\n' + ''.join(
  419. ' {fmt:{width}s} {columns}\n'.format(
  420. fmt=fmt, columns=','.join(formats[fmt]).upper(), width=width)
  421. for fmt in sorted(formats))
  422. parser.exit(message=text)
  423. # common VM power states for easy command-line filtering
  424. DOMAIN_POWER_STATES = ['running', 'paused', 'halted']
  425. def matches_power_states(domain, **states):
  426. '''Filter domains by their power state'''
  427. # if all values are False (default) => return match on every VM
  428. if not states or set(states.values()) == {False}:
  429. return True
  430. # otherwise => only VMs matching True states
  431. requested_states = [state for state, active in states.items() if active]
  432. return domain.get_power_state().lower() in requested_states
  433. def get_parser():
  434. '''Create :py:class:`argparse.ArgumentParser` suitable for
  435. :program:`qvm-ls`.
  436. '''
  437. # parser creation is delayed to get all the columns that are scattered
  438. # thorough the modules
  439. wrapper = textwrap.TextWrapper(width=80, break_on_hyphens=False,
  440. initial_indent=' ', subsequent_indent=' ')
  441. parser = qubesadmin.tools.QubesArgumentParser(
  442. vmname_nargs=argparse.ZERO_OR_MORE,
  443. formatter_class=argparse.RawTextHelpFormatter,
  444. description='List Qubes domains and their parametres.',
  445. epilog='available formats (see --help-formats):\n{}\n\n'
  446. 'available columns (see --help-columns):\n{}'.format(
  447. wrapper.fill(', '.join(sorted(formats.keys()))),
  448. wrapper.fill(', '.join(sorted(sorted(Column.columns.keys()))))))
  449. parser.add_argument('--help-columns', action=_HelpColumnsAction)
  450. parser.add_argument('--help-formats', action=_HelpFormatsAction)
  451. parser_formats = parser.add_mutually_exclusive_group()
  452. parser_formats.add_argument('--format', '-o', metavar='FORMAT',
  453. action='store', choices=formats.keys(), default='simple',
  454. help='preset format')
  455. parser_formats.add_argument('--fields', '-O', metavar='FIELD,...',
  456. action='store',
  457. help='user specified format (see available columns below)')
  458. parser.add_argument('--tags', nargs='+', metavar='TAG',
  459. help='show only VMs having specific tag(s)')
  460. for pwrstate in DOMAIN_POWER_STATES:
  461. parser.add_argument('--{}'.format(pwrstate), action='store_true',
  462. help='show {} VMs'.format(pwrstate))
  463. parser.add_argument('--raw-data', action='store_true',
  464. help='Display specify data of specified VMs. Intended for '
  465. 'bash-parsing.')
  466. parser.add_argument('--tree', '-t',
  467. action='store_const', const='tree',
  468. help='sort domain list as network tree')
  469. parser.add_argument('--spinner',
  470. action='store_true', dest='spinner',
  471. help='reenable spinner')
  472. parser.add_argument('--no-spinner',
  473. action='store_false', dest='spinner',
  474. help='disable spinner')
  475. # shortcuts, compatibility with Qubes 3.2
  476. parser.add_argument('--raw-list', action='store_true',
  477. help='Same as --raw-data --fields=name')
  478. parser.add_argument('--disk', '-d',
  479. action='store_const', dest='format', const='disk',
  480. help='Same as --format=disk')
  481. parser.add_argument('--network', '-n',
  482. action='store_const', dest='format', const='network',
  483. help='Same as --format=network')
  484. parser.add_argument('--kernel', '-k',
  485. action='store_const', dest='format', const='kernel',
  486. help='Same as --format=kernel')
  487. parser.set_defaults(spinner=True)
  488. # parser.add_argument('--conf', '-c',
  489. # action='store', metavar='CFGFILE',
  490. # help='Qubes config file')
  491. return parser
  492. def main(args=None, app=None):
  493. '''Main routine of :program:`qvm-ls`.
  494. :param list args: Optional arguments to override those delivered from \
  495. command line.
  496. :param app: Operate on given app object instead of instantiating new one.
  497. '''
  498. parser = get_parser()
  499. try:
  500. args = parser.parse_args(args, app=app)
  501. except qubesadmin.exc.QubesException as e:
  502. parser.print_error(str(e))
  503. return 1
  504. # fetch all the properties with one Admin API call, instead of issuing
  505. # one call per property
  506. args.app.cache_enabled = True
  507. if args.raw_list:
  508. args.raw_data = True
  509. args.fields = 'name'
  510. if args.fields:
  511. columns = [col.strip() for col in args.fields.split(',')]
  512. else:
  513. columns = formats[args.format]
  514. # assume unknown columns are VM properties
  515. for col in columns:
  516. if col.upper() not in Column.columns:
  517. PropertyColumn(col.lower())
  518. if args.spinner and not args.raw_data:
  519. # we need Enterprise Edition™, since it's the only one that detects TTY
  520. # and uses dots if we are redirected somewhere else
  521. spinner = qubesadmin.spinner.QubesSpinnerEnterpriseEdition(sys.stderr)
  522. else:
  523. spinner = qubesadmin.spinner.DummySpinner(sys.stderr)
  524. if args.domains:
  525. domains = args.domains
  526. else:
  527. domains = args.app.domains
  528. if args.all_domains:
  529. # Normally, --all means "all domains except for AdminVM".
  530. # However, in the case of qvm-ls it does not make sense to exclude
  531. # AdminVMs, so we override the list from parser.
  532. domains = [
  533. vm for vm in args.app.domains if vm.name not in args.exclude
  534. ]
  535. if args.tags:
  536. # filter only VMs having at least one of the specified tags
  537. domains = [dom for dom in domains
  538. if set(dom.tags).intersection(set(args.tags))]
  539. pwrstates = {state: getattr(args, state) for state in DOMAIN_POWER_STATES}
  540. domains = [d for d in domains
  541. if matches_power_states(d, **pwrstates)]
  542. table = Table(domains, columns, spinner, args.raw_data, args.tree)
  543. table.write_table(sys.stdout)
  544. return 0
  545. if __name__ == '__main__':
  546. sys.exit(main())