3030import collections .abc
3131import sys
3232import textwrap
33+ from collections .abc import Callable
3334
3435import qubesadmin
3536import qubesadmin .spinner
3637import qubesadmin .tools
3738import qubesadmin .utils
3839import qubesadmin .vm
3940import qubesadmin .exc
40-
41- #
42- # columns
43- #
41+ from qubesadmin .vm import QubesVM
4442
4543class 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
135111class 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
308173def 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
398265class 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