qvm_ls.py 21 KB

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