Skip to content

Commit daf4841

Browse files
author
anon
committed
make flags a standard column
1 parent 1a623d1 commit daf4841

1 file changed

Lines changed: 87 additions & 220 deletions

File tree

qubesadmin/tools/qvm_ls.py

Lines changed: 87 additions & 220 deletions
Original file line numberDiff line numberDiff line change
@@ -30,75 +30,50 @@
3030
import collections.abc
3131
import sys
3232
import textwrap
33+
from collections.abc import Callable
3334

3435
import qubesadmin
3536
import qubesadmin.spinner
3637
import qubesadmin.tools
3738
import qubesadmin.utils
3839
import qubesadmin.vm
3940
import qubesadmin.exc
40-
41-
#
42-
# columns
43-
#
41+
from qubesadmin.vm import QubesVM
4442

4543
class Column:
46-
'''A column in qvm-ls output characterised by its head and a way
47-
to fetch a parameter describing the domain.
44+
"""A column in qvm-ls output.
4845
4946
:param str head: Column head (usually uppercase).
50-
:param str attr: Attribute, possibly complex (containing ``.``). This may \
51-
also be a callable that gets as its only argument the domain.
47+
:param attr: Attribute path (dotted string) or callable ``(vm) -> value``.
5248
:param str doc: Description of column (will be visible in --help-columns).
53-
'''
49+
"""
5450

5551
#: collection of all columns
5652
columns = {}
5753

58-
def __init__(self, head, attr=None, doc=None):
59-
self.ls_head = head
54+
def __init__(self, head: str,
55+
attr: str | Callable[[QubesVM], object],
56+
doc: str | None=None):
57+
self.head = head
6058
self.__doc__ = doc
61-
62-
# intentionally not always do set self._attr,
63-
# to cause AttributeError in self.format()
64-
if attr is not None:
65-
self._attr = attr
66-
67-
self.__class__.columns[self.ls_head] = self
68-
59+
self._attr = attr
60+
self.__class__.columns[self.head] = self
6961

7062
def cell(self, vm, insertion=0):
71-
'''Format one cell.
72-
73-
.. note::
74-
75-
This is only for technical formatting (filling with space). If you
76-
want to subclass the :py:class:`Column` class, you should override
77-
:py:meth:`Column.format` method instead.
63+
'''Format one cell, handling tree indentation for the NAME column.
7864
79-
:param qubes.vm.qubesvm.QubesVM: Domain to get a value from.
80-
:param int insertion: Intending to shift the value to the right.
65+
:param vm: Domain to get a value from.
66+
:param insertion: Tree depth; shifts NAME value to the right.
8167
:returns: string to display
82-
:rtype: str
8368
'''
84-
8569
value = self.format(vm) or '-'
86-
if insertion > 0 and self.ls_head == 'NAME':
70+
if insertion > 0 and self.head == 'NAME':
8771
value = '└─' + value
8872
value = ' ' * (insertion-1) + value
8973
return value
9074

91-
92-
def format(self, vm):
93-
'''Format one cell value.
94-
95-
Return value to put in a table cell.
96-
97-
:param qubes.vm.qubesvm.QubesVM: Domain to get a value from.
98-
:returns: Value to put, or :py:obj:`None` if no value.
99-
:rtype: str or None
100-
'''
101-
75+
def format(self, vm: QubesVM) -> str | None:
76+
'''Return the cell value for *vm*, or ``None`` if not applicable.'''
10277
ret = None
10378
try:
10479
if isinstance(self._attr, str):
@@ -120,189 +95,79 @@ def format(self, vm):
12095
return str(ret)
12196

12297
def __repr__(self):
123-
return '{}(head={!r})'.format(self.__class__.__name__,
124-
self.ls_head)
125-
126-
127-
def __eq__(self, other):
128-
return self.ls_head == other.ls_head
98+
return '{}(head={!r})'.format(self.__class__.__name__, self.head)
12999

100+
def __eq__(self, other: object) -> bool:
101+
if isinstance(other, Column):
102+
return self.head == other.head
103+
return NotImplemented
130104

131-
def __lt__(self, other):
132-
return self.ls_head < other.ls_head
105+
def __lt__(self, other: object) -> bool:
106+
if isinstance(other, Column):
107+
return self.head < other.head
108+
return NotImplemented
133109

134110

135111
class PropertyColumn(Column):
136-
'''Column that displays value from property (:py:class:`property` or
137-
:py:class:`qubes.property`) of domain.
112+
"""Column that displays a VM property by name.
138113
139114
:param name: Name of VM property.
140-
'''
115+
"""
141116

142117
def __init__(self, name):
143-
ls_head = name.replace('_', '-').upper()
144-
super().__init__(head=ls_head, attr=name)
145-
146-
def __repr__(self):
147-
return '{}(head={!r}'.format(
148-
self.__class__.__name__,
149-
self.ls_head)
150-
151-
152-
def process_vm(vm):
153-
'''Process VM object to find all listable properties.
154-
155-
:param qubesmgmt.vm.QubesVM vm: VM object.
118+
super().__init__(head=name.replace('_', '-').upper(), attr=name)
119+
120+
def __repr__(self) -> str:
121+
return '{}(head={!r}'.format(self.__class__.__name__, self.head)
122+
123+
124+
def _format_flags(vm: QubesVM) -> str:
125+
'''Format FLAGS column value for a single VM.
126+
127+
Each character position encodes one property:
128+
1 type: 0=AdminVM, a/A=AppVM, d/D=DispVM, s/S=StandaloneVM,
129+
t/T=TemplateVM
130+
(uppercase = HVM)
131+
2 power state: r=running, t=transient, p=paused, s=suspended,
132+
h=halting, d=dying, c=crashed, ?=unknown
133+
3 U updateable
134+
4 N provides_network
135+
5 R installed_by_rpm
136+
6 i internal
137+
7 D debug
138+
8 A autostart
156139
'''
140+
type_codes = {
141+
'AdminVM': '0', 'TemplateVM': 't', 'AppVM': 'a',
142+
'StandaloneVM': 's', 'DispVM': 'd',
143+
}
144+
type_letter = type_codes.get(vm.klass, '-')
145+
if type_letter not in ('0', '-'):
146+
if getattr(vm, 'virt_mode', 'pv') == 'hvm':
147+
type_letter = type_letter.upper()
148+
149+
state = vm.get_power_state().lower()
150+
if state == 'unknown':
151+
power_letter = '?'
152+
elif state in ('running', 'transient', 'paused', 'suspended',
153+
'halting', 'dying', 'crashed'):
154+
power_letter = state[0]
155+
else:
156+
power_letter = '-'
157157

158-
for prop_name in vm.property_list():
159-
PropertyColumn(prop_name)
160-
161-
162-
def flag(field):
163-
'''Mark method as flag field.
164-
165-
:param int field: Which field to fill (counted from 1)
166-
'''
167-
168-
def decorator(obj):
169-
# pylint: disable=missing-docstring
170-
obj.field = field
171-
return obj
172-
return decorator
173-
174-
175-
def simple_flag(field, letter, attr, doc=None):
176-
'''Create simple, binary flag.
177-
178-
:param str attr: Attribute name to check. If result is true, flag is fired.
179-
:param str letter: The letter to show.
180-
'''
181-
182-
def helper(self, vm):
183-
# pylint: disable=missing-docstring,unused-argument
184-
try:
185-
value = getattr(vm, attr)
186-
except AttributeError:
187-
value = False
188-
189-
if value:
190-
return letter[0]
191-
192-
helper.__doc__ = doc
193-
helper.field = field
194-
return helper
195-
196-
197-
class FlagsColumn(Column):
198-
'''Some fancy flags that describe general status of the domain.'''
199-
200-
def __init__(self):
201-
super().__init__(head='FLAGS', doc=self.__class__.__doc__)
202-
203-
204-
@flag(1)
205-
def type(self, vm):
206-
'''Type of domain.
207-
208-
0 AdminVM (AKA Dom0)
209-
aA AppVM
210-
dD DisposableVM
211-
sS StandaloneVM
212-
tT TemplateVM
213-
214-
When it is HVM (optimised VM), the letter is capital.
215-
'''
216-
217-
type_codes = {
218-
'AdminVM': '0',
219-
'TemplateVM': 't',
220-
'AppVM': 'a',
221-
'StandaloneVM': 's',
222-
'DispVM': 'd',
223-
}
224-
ret = type_codes.get(vm.klass, None)
225-
if ret == '0':
226-
return ret
227-
228-
if ret is not None:
229-
if getattr(vm, 'virt_mode', 'pv') == 'hvm':
230-
return ret.upper()
231-
return ret
232-
233-
234-
@flag(2)
235-
def power(self, vm):
236-
'''Current power state.
237-
238-
r running
239-
t transient
240-
p paused
241-
s suspended
242-
h halting
243-
d dying
244-
c crashed
245-
? unknown
246-
'''
247-
248-
state = vm.get_power_state().lower()
249-
if state == 'unknown':
250-
return '?'
251-
if state in ('running', 'transient', 'paused', 'suspended',
252-
'halting', 'dying', 'crashed'):
253-
return state[0]
254-
255-
256-
updateable = simple_flag(3, 'U', 'updateable',
257-
doc='If the domain is updateable.')
258-
259-
provides_network = simple_flag(4, 'N', 'provides_network',
260-
doc='If the domain provides network.')
261-
262-
installed_by_rpm = simple_flag(5, 'R', 'installed_by_rpm',
263-
doc='If the domain is installed by RPM.')
264-
265-
internal = simple_flag(6, 'i', 'internal',
266-
doc='If the domain is internal (not normally shown, no appmenus).')
267-
268-
debug = simple_flag(7, 'D', 'debug',
269-
doc='If the domain is being debugged.')
270-
271-
autostart = simple_flag(8, 'A', 'autostart',
272-
doc='If the domain is marked for autostart.')
273-
274-
# TODO (not sure if really):
275-
# include in backups
276-
# uses_custom_config
277-
278-
def _no_flag(self, vm):
279-
'''Reserved for future use.'''
280-
281-
282-
@classmethod
283-
def get_flags(cls):
284-
'''Get all flags as list.
285-
286-
Holes between flags are filled with :py:meth:`_no_flag`.
287-
288-
:rtype: list
289-
'''
290-
291-
flags = {}
292-
for mycls in cls.__mro__:
293-
for attr in mycls.__dict__.values():
294-
if not hasattr(attr, 'field'):
295-
continue
296-
if attr.field in flags:
297-
continue
298-
flags[attr.field] = attr
299-
300-
return [(flags[i] if i in flags else cls._no_flag)
301-
for i in range(1, max(flags) + 1)]
302-
158+
def bool_flag(attr: str, letter: str) -> str:
159+
return letter if getattr(vm, attr, False) else '-'
303160

304-
def format(self, vm):
305-
return ''.join((flag(self, vm) or '-') for flag in self.get_flags())
161+
return ''.join([
162+
type_letter,
163+
power_letter,
164+
bool_flag('updateable', 'U'),
165+
bool_flag('provides_network', 'N'),
166+
bool_flag('installed_by_rpm', 'R'),
167+
bool_flag('internal', 'i'),
168+
bool_flag('debug', 'D'),
169+
bool_flag('autostart', 'A'),
170+
])
306171

307172

308173
def calc_size(vm, volume_name):
@@ -388,11 +253,13 @@ def calc_used(vm, volume_name):
388253
doc='Disk utilisation by root image as a percentage of available space.')
389254

390255

391-
FlagsColumn()
256+
Column('FLAGS', attr=_format_flags,
257+
doc='Various flags: type, power state, updateable, provides_network, '
258+
'installed_by_rpm, internal, debug, autostart.')
392259

393260
# Sorting columns based on numeric or string (default) values
394-
SORT_NUMERIC = ['MEMORY', 'DISK', 'PRIV-CURR', 'PRIV-MAX', 'ROOT-CURR', 'XID', \
395-
'ROOT-MAX', 'MAXMEM', 'QREXEC-TIMEOUT', 'SHUTDOWN-TIMEOUT', \
261+
SORT_NUMERIC = ['MEMORY', 'DISK', 'PRIV-CURR', 'PRIV-MAX', 'ROOT-CURR', 'XID',
262+
'ROOT-MAX', 'MAXMEM', 'QREXEC-TIMEOUT', 'SHUTDOWN-TIMEOUT',
396263
'VCPUS', 'PRIV-USED', 'ROOT-USED']
397264

398265
class Table:
@@ -416,13 +283,13 @@ def __init__(self, domains, colnames, spinner, *, raw_data=False,
416283

417284
def get_head(self):
418285
'''Get table head data (all column heads).'''
419-
return [col.ls_head for col in self.columns]
286+
return [col.head for col in self.columns]
420287

421288
def get_row(self, vm, insertion=0):
422289
'''Get single table row data (all columns for one domain).'''
423290
ret = []
424291
for col in self.columns:
425-
if self.tree_sorted and col.ls_head == 'NAME':
292+
if self.tree_sorted and col.head == 'NAME':
426293
ret.append(col.cell(vm, insertion))
427294
else:
428295
ret.append(col.cell(vm))
@@ -558,13 +425,13 @@ def __init__(self,
558425
help=help)
559426

560427
def __call__(self, parser, namespace, values, option_string=None):
561-
width = max(len(column.ls_head) for column in Column.columns.values())
428+
width = max(len(column.head) for column in Column.columns.values())
562429
wrapper = textwrap.TextWrapper(width=80,
563430
initial_indent=' ', subsequent_indent=' ' * (width + 6))
564431

565432
text = 'Available columns:\n' + '\n'.join(
566433
wrapper.fill('{head:{width}s} {doc}'.format(
567-
head=column.ls_head,
434+
head=column.head,
568435
doc=column.__doc__ or '',
569436
width=width))
570437
for column in sorted(Column.columns.values()))

0 commit comments

Comments
 (0)