From 5f152d4c691fb5c7f8ec1ac58afb5784845376e6 Mon Sep 17 00:00:00 2001 From: qubesuser Date: Wed, 8 Nov 2017 23:03:46 +0100 Subject: [PATCH 1/6] Use GetAll instead of Get and cache property values This is much more efficient. --- qubesadmin/app.py | 76 ++++++++++- qubesadmin/base.py | 251 ++++++++++++++++++++++++++++------- qubesadmin/tests/__init__.py | 2 + 3 files changed, 275 insertions(+), 54 deletions(-) diff --git a/qubesadmin/app.py b/qubesadmin/app.py index d374d2c2..2c941b66 100644 --- a/qubesadmin/app.py +++ b/qubesadmin/app.py @@ -39,16 +39,26 @@ BUF_SIZE = 4096 +def _decode_vm_line(vm_line): + '''Decode line from admin.vm.List''' + name, props = vm_line.decode('ascii').split(' ', 1) + name = str(name) + props = props.split(' ') + props = dict([prop.split('=', 1) for prop in props]) + return name, props + class VMCollection(object): '''Collection of VMs objects''' def __init__(self, app): self.app = app self._vm_list = None self._vm_objects = {} + self._get_all_data_result = None def clear_cache(self): '''Clear cached list of VMs''' self._vm_list = None + self._get_all_data_result = None def refresh_cache(self, force=False): '''Refresh cached list of VMs''' @@ -58,16 +68,62 @@ def refresh_cache(self, force=False): 'dom0', 'admin.vm.List' ) + new_vm_list = {} - # FIXME: this will probably change - for vm_data in vm_list_data.splitlines(): - vm_name, props = vm_data.decode('ascii').split(' ', 1) - vm_name = str(vm_name) - props = props.split(' ') - new_vm_list[vm_name] = dict( - [vm_prop.split('=', 1) for vm_prop in props]) + for vm_line in vm_list_data.splitlines(): + name, props = _decode_vm_line(vm_line) + new_vm_list[name] = props + + self._set_vm_list(new_vm_list) + + def _get_all_data(self): + '''Call GetAllData, update the VM list and the data in VMs''' + if not self.app.use_get_all_data: + return False + try: + data = self.app.qubesd_call( + 'dom0', + 'admin.vm.GetAllData' + ) + except qubesadmin.exc.QubesDaemonNoResponseError: + return False + + new_vm_list = {} + vm_props = {} + vm_name = None + for line in data.splitlines(): + if not line.startswith(b"\t"): + name, props = _decode_vm_line(line) + new_vm_list[name] = props + + vm_name = name + vm_props[vm_name] = [] + elif vm_name: + kind, data = line[1:].split(b"\t", 1) + if kind == b"P": + vm_props[vm_name].append(data) + + self._set_vm_list(new_vm_list) + + # we must delay processing of lines because setting VM-valued + # properties requires to know about other VMs and their classes + for vm_name in vm_props: + self[vm_name].process_response(vm_props[vm_name]) + + return True + + def get_all_data(self, force=False): + '''Load data for all VMs at once''' + if force or self._get_all_data_result is None: + self._get_all_data_result = self._get_all_data() + + return self._get_all_data_result + + def _set_vm_list(self, new_vm_list): + '''Apply a new VM list from qubesd''' self._vm_list = new_vm_list + for name, vm in list(self._vm_objects.items()): if vm.name not in self._vm_list: # VM no longer exists @@ -134,6 +190,10 @@ class QubesBase(qubesadmin.base.PropertyHolder): log = None #: do not check for object (VM, label etc) existence before really needed blind_mode = False + #: use GetAll + use_get_all = None + #: use GetAllData + use_get_all_data = None def __init__(self): super(QubesBase, self).__init__(self, 'admin.property.', 'dom0') @@ -145,6 +205,8 @@ def __init__(self): #: cache for available storage pool drivers and options to create them self._pool_drivers = None self.log = logging.getLogger('app') + self.use_get_all = True + self.use_get_all_data = True def _refresh_pool_drivers(self): ''' diff --git a/qubesadmin/base.py b/qubesadmin/base.py index 2de265b0..fed12a31 100644 --- a/qubesadmin/base.py +++ b/qubesadmin/base.py @@ -20,11 +20,34 @@ '''Base classes for managed objects''' +import sys +import traceback +import re + import ast import qubesadmin.exc DEFAULT = object() +ESCAPE_RE = re.compile("\\\\(.)") + +def unescape(escaped_string): + '''Unescape Admin API string''' + def replacement(match): + '''Replace escape sequence''' + esc = match.group(1) + if esc == "n": + return "\n" + if esc == "r": + return "\r" + if esc == "t": + return "\t" + if esc == "0": + return "\0" + return esc + return ESCAPE_RE.sub(replacement, escaped_string) + +CONFLICTING_KEYS = frozenset(["name", "klass"]) class PropertyHolder(object): '''A base class for object having properties retrievable using mgmt API. @@ -45,6 +68,12 @@ def __init__(self, app, method_prefix, method_dest): self._method_dest = method_dest self._properties = None self._properties_help = None + self._values = dict() + self._explicit = dict() + self._update_needed = dict() + self._exhaustive = False + self._use_cache = True + self._get_all = True def qubesd_call(self, dest, method, arg=None, payload=None, payload_stream=None): @@ -134,6 +163,31 @@ def property_help(self, name): None) return help_text.decode('ascii') + def _lookup(self, item): + '''Ensure that item is in the cache, updating it if required''' + if not self._use_cache: + self._update_one(item) + return + + if item in self._values: + return + + if item in self._update_needed: + self._update_one(item) + return + + if self._exhaustive: + raise qubesadmin.exc.QubesPropertyAccessError(item) + + if self._get_all and not self._update_all(): + self._get_all = False + + if not self._get_all: + self._update_one(item) + + if item not in self._values: + raise qubesadmin.exc.QubesPropertyAccessError(item) + def property_is_default(self, item): ''' Check if given property have default value @@ -143,17 +197,8 @@ def property_is_default(self, item): ''' if item.startswith('_'): raise AttributeError(item) - property_str = self.qubesd_call( - self._method_dest, - self._method_prefix + 'Get', - item, - None) - (default, _value) = property_str.split(b' ', 1) - assert default.startswith(b'default=') - is_default_str = default.split(b'=')[1] - is_default = ast.literal_eval(is_default_str.decode('ascii')) - assert isinstance(is_default, bool) - return is_default + self._lookup(item) + return item not in self._explicit def clone_properties(self, src, proplist=None): '''Clone properties from other object. @@ -176,43 +221,133 @@ def __getattr__(self, item): # pylint: disable=too-many-return-statements if item.startswith('_'): raise AttributeError(item) - try: - property_str = self.qubesd_call( - self._method_dest, - self._method_prefix + 'Get', - item, - None) - except qubesadmin.exc.QubesDaemonNoResponseError: - raise qubesadmin.exc.QubesPropertyAccessError(item) - (_default, prop_type, value) = property_str.split(b' ', 2) - prop_type = prop_type.decode('ascii') - if not prop_type.startswith('type='): - raise qubesadmin.exc.QubesDaemonCommunicationError( - 'Invalid type prefix received: {}'.format(prop_type)) - (_, prop_type) = prop_type.split('=', 1) + self._lookup(item) + return self._values[item] + + def clear_cache(self): + '''Clear the cache''' + self._get_all = True + self._clear_properties() + + def _clear_properties(self): + '''Clear cached properties''' + for key in self._values: + if key not in CONFLICTING_KEYS: + object.__delattr__(self, key) + + self._values.clear() + self._explicit.clear() + self._update_needed.clear() + self._exhaustive = False + + def enable_cache(self, enabled): + '''Enable or disable using data in the cache without updating it''' + self._use_cache = enabled + if not enabled: + for key in self._values: + if key not in CONFLICTING_KEYS: + object.__delattr__(self, key) + else: + for key in self._values: + if key not in CONFLICTING_KEYS: + object.__setattr__(self, key, self._values[key]) + + def _decode_value(self, value, prop_type): + '''Decode value from qubesd''' value = value.decode() if prop_type == 'str': - return str(value) + value = value elif prop_type == 'bool': if value == '': raise AttributeError - return ast.literal_eval(value) + value = ast.literal_eval(value) elif prop_type == 'int': if value == '': - raise AttributeError - return ast.literal_eval(value) + value = None # hack for stubdom_mem + #raise AttributeError + else: + value = ast.literal_eval(value) elif prop_type == 'vm': if value == '': - return None - return self.app.domains[value] + value = None + else: + value = self.app.domains[value] elif prop_type == 'label': if value == '': - return None - # TODO - return self.app.labels[value] + value = None + else: + value = self.app.labels[value] else: raise qubesadmin.exc.QubesDaemonCommunicationError( 'Received invalid value type: {}'.format(prop_type)) + return value + + def _update_one(self, item): + '''Call Get to update one property, raise on failure''' + try: + property_str = self.qubesd_call( + self._method_dest, + self._method_prefix + 'Get', + item, + None) + except qubesadmin.exc.QubesDaemonNoResponseError: + raise qubesadmin.exc.QubesPropertyAccessError(item) + (default, prop_type, value) = property_str.split(b' ', 2) + + assert default.startswith(b'default=') + is_default_str = default.split(b'=')[1] + is_default = ast.literal_eval(is_default_str.decode('ascii')) + assert isinstance(is_default, bool) + + prop_type = prop_type.decode('ascii') + if not prop_type.startswith('type='): + raise qubesadmin.exc.QubesDaemonCommunicationError( + 'Invalid type prefix received: {}'.format(prop_type)) + (_, prop_type) = prop_type.split('=', 1) + + value = self._decode_value(value, prop_type) + + self._set_item(item, value, is_default) + + def _update_all(self): + '''Call GetAll to update all properties, return False on failure''' + if not self.app.use_get_all: + return False + + try: + response = self.qubesd_call( + self._method_dest, + self._method_prefix + 'GetAll', + None, + None) + except qubesadmin.exc.QubesDaemonNoResponseError: + return False + + self.process_response(response.splitlines()) + return True + + # also called by get_all_data() + def process_response(self, lines): + '''Process data from the response to GetAll or GetAllData''' + self._clear_properties() + successful = True + for line in lines: + try: + (name, is_default, prop_type, value) = line.split(b'\t', 3) + name = name.decode('ascii') + prop_type = prop_type.decode('ascii') + is_default = is_default == b'D' + + value = self._decode_value(value, prop_type) + if prop_type == 'str' and value is not None: + value = unescape(value) + + self._set_item(name, value, is_default) + except: # pylint: disable=bare-except + successful = False + traceback.print_exc(file=sys.stderr) + + self._exhaustive = successful @classmethod def _local_properties(cls): @@ -233,28 +368,24 @@ def __setattr__(self, key, value): if key.startswith('_') or key in self._local_properties(): return super(PropertyHolder, self).__setattr__(key, value) if value is qubesadmin.DEFAULT: - try: - self.qubesd_call( - self._method_dest, - self._method_prefix + 'Reset', - key, - None) - except qubesadmin.exc.QubesDaemonNoResponseError: - raise qubesadmin.exc.QubesPropertyAccessError(key) + self.__delattr__(key) else: - if isinstance(value, qubesadmin.vm.QubesVM): - value = value.name - if value is None: - value = '' + send_value = value + if isinstance(send_value, qubesadmin.vm.QubesVM): + send_value = value.name + if send_value is None: + send_value = '' try: self.qubesd_call( self._method_dest, self._method_prefix + 'Set', key, - str(value).encode('utf-8')) + str(send_value).encode('utf-8')) except qubesadmin.exc.QubesDaemonNoResponseError: raise qubesadmin.exc.QubesPropertyAccessError(key) + self._set_item(key, value, False) + def __delattr__(self, name): if name.startswith('_') or name in self._local_properties(): return super(PropertyHolder, self).__delattr__(name) @@ -267,6 +398,32 @@ def __delattr__(self, name): except qubesadmin.exc.QubesDaemonNoResponseError: raise qubesadmin.exc.QubesPropertyAccessError(name) + # unfortunately, this has to trigger an API call on the next read + # since we don't know the default value + self._clear_item(name) + + def _set_item(self, key, value, is_default): + '''Set the cached value of a property''' + self._values[key] = value + if self._use_cache and key not in CONFLICTING_KEYS: + object.__setattr__(self, key, value) + if not is_default: + self._explicit[key] = value + elif key in self._explicit: + del self._explicit[key] + if key in self._update_needed: + del self._update_needed[key] + + def _clear_item(self, key): + '''Clear the cached value of a property''' + if key in self._values: + del self._values[key] + if self._use_cache and key not in CONFLICTING_KEYS: + object.__delattr__(self, key) + if key in self._explicit: + del self._explicit[key] + if self._exhaustive: + self._update_needed[key] = True class WrapperObjectsCollection(object): '''Collection of simple named objects''' diff --git a/qubesadmin/tests/__init__.py b/qubesadmin/tests/__init__.py index d6a23091..484e8478 100644 --- a/qubesadmin/tests/__init__.py +++ b/qubesadmin/tests/__init__.py @@ -126,6 +126,8 @@ def __init__(self): self.actual_calls = [] #: rpc service calls self.service_calls = [] + self.use_get_all_data = False + self.use_get_all = False def qubesd_call(self, dest, method, arg=None, payload=None, payload_stream=None): From 7644ea9597fad02b7191e270a14a6b3ba36e1734 Mon Sep 17 00:00:00 2001 From: qubesuser Date: Thu, 9 Nov 2017 15:35:26 +0100 Subject: [PATCH 2/6] cache VM state from List/GetAllData instead of requesting it every time --- qubesadmin/app.py | 5 ++++- qubesadmin/vm/__init__.py | 38 ++++++++++++++++++++++---------------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/qubesadmin/app.py b/qubesadmin/app.py index 2c941b66..836808da 100644 --- a/qubesadmin/app.py +++ b/qubesadmin/app.py @@ -146,9 +146,12 @@ def __getitem__(self, item): # done by 'item not in self' check above, unless blind_mode is # enabled klass = None + state = None if self._vm_list and item in self._vm_list: klass = self._vm_list[item]['class'] - self._vm_objects[item] = cls(self.app, item, klass=klass) + state = self._vm_list[item]['state'] + self._vm_objects[item] = cls(self.app, item, + klass=klass, state=state) return self._vm_objects[item] def __contains__(self, item): diff --git a/qubesadmin/vm/__init__.py b/qubesadmin/vm/__init__.py index d9f76e5e..0d2c7810 100644 --- a/qubesadmin/vm/__init__.py +++ b/qubesadmin/vm/__init__.py @@ -46,7 +46,7 @@ class QubesVM(qubesadmin.base.PropertyHolder): firewall = None - def __init__(self, app, name, klass=None): + def __init__(self, app, name, klass=None, state=None): super(QubesVM, self).__init__(app, 'admin.vm.property.', name) self._volumes = None self._klass = klass @@ -55,6 +55,11 @@ def __init__(self, app, name, klass=None): self.features = qubesadmin.features.Features(self) self.devices = qubesadmin.devices.DeviceManager(self) self.firewall = qubesadmin.firewall.Firewall(self) + self._state = state + + def clear_cache(self): + self._state = None + super(QubesVM, self).clear_cache() @property def name(self): @@ -138,7 +143,7 @@ def unpause(self): ''' self.qubesd_call(self._method_dest, 'admin.vm.Unpause') - def get_power_state(self): + def get_power_state(self, force=False): '''Return power state description string. Return value may be one of those: @@ -168,20 +173,21 @@ def get_power_state(self): ''' - try: - vm_list_info = [line - for line in self.qubesd_call( - self._method_dest, 'admin.vm.List', None, None - ).decode('ascii').split('\n') - if line.startswith(self._method_dest+' ')] - except qubesadmin.exc.QubesDaemonNoResponseError: - return 'NA' - assert len(vm_list_info) == 1 - # name class=... state=... other=... - # NOTE: when querying dom0, we get whole list - vm_state = vm_list_info[0].strip().partition('state=')[2].split(' ')[0] - return vm_state - + if not self._state or force: + try: + vm_list_info = [line + for line in self.qubesd_call( + self._method_dest, 'admin.vm.List', None, None + ).decode('ascii').split('\n') + if line.startswith(self._method_dest+' ')] + except qubesadmin.exc.QubesDaemonNoResponseError: + return 'NA' + assert len(vm_list_info) == 1 + # name class=... state=... other=... + # NOTE: when querying dom0, we get whole list + self._state = (vm_list_info[0].strip().partition('state=')[2] + .split(' ')[0]) + return self._state def is_halted(self): ''' Check whether this domain's state is 'Halted' From 9e1a0bd914b15decd85aa7c063276107bec91e45 Mon Sep 17 00:00:00 2001 From: qubesuser Date: Thu, 9 Nov 2017 15:41:05 +0100 Subject: [PATCH 3/6] qvm-ls: use GetAllData This makes it get all the data with a single qubesd call. --- qubesadmin/tools/qvm_ls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qubesadmin/tools/qvm_ls.py b/qubesadmin/tools/qvm_ls.py index e4f5c95e..e45a9123 100644 --- a/qubesadmin/tools/qvm_ls.py +++ b/qubesadmin/tools/qvm_ls.py @@ -578,6 +578,7 @@ def main(args=None, app=None): else: spinner = qubesadmin.spinner.DummySpinner(sys.stderr) + args.app.domains.get_all_data() table = Table(args.app, columns, spinner) table.write_table(sys.stdout) From 6eb828f1363a99ca5c491ebd03475b649898545a Mon Sep 17 00:00:00 2001 From: qubesuser Date: Thu, 9 Nov 2017 15:54:29 +0100 Subject: [PATCH 4/6] don't lookup list of labels just to read VM properties If qubesd returns a label name, we can just assume it's valid. This makes qvm-ls take only one qubesd call. --- qubesadmin/base.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/qubesadmin/base.py b/qubesadmin/base.py index fed12a31..68dd848a 100644 --- a/qubesadmin/base.py +++ b/qubesadmin/base.py @@ -276,7 +276,7 @@ def _decode_value(self, value, prop_type): if value == '': value = None else: - value = self.app.labels[value] + value = self.app.labels.get_blind(value) else: raise qubesadmin.exc.QubesDaemonCommunicationError( 'Received invalid value type: {}'.format(prop_type)) @@ -466,6 +466,13 @@ def refresh_cache(self, force=False): def __getitem__(self, item): if not self.app.blind_mode and item not in self: raise KeyError(item) + return self.get_blind(item) + + def get_blind(self, item): + ''' + Get a property without downloading the list + and checking if it's present + ''' if item not in self._objects: self._objects[item] = self._object_class(self.app, item) return self._objects[item] From 10c4c2ffef3fa6c69a245c315a99479527dc39e9 Mon Sep 17 00:00:00 2001 From: qubesuser Date: Thu, 9 Nov 2017 16:11:22 +0100 Subject: [PATCH 5/6] don't use ast.literal_eval, just directly convert to the desired type It's slow and unnecessary --- qubesadmin/base.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/qubesadmin/base.py b/qubesadmin/base.py index 68dd848a..fa4b5736 100644 --- a/qubesadmin/base.py +++ b/qubesadmin/base.py @@ -24,7 +24,6 @@ import traceback import re -import ast import qubesadmin.exc DEFAULT = object() @@ -260,13 +259,13 @@ def _decode_value(self, value, prop_type): elif prop_type == 'bool': if value == '': raise AttributeError - value = ast.literal_eval(value) + value = value == "True" elif prop_type == 'int': if value == '': value = None # hack for stubdom_mem #raise AttributeError else: - value = ast.literal_eval(value) + value = int(value) elif prop_type == 'vm': if value == '': value = None @@ -296,7 +295,7 @@ def _update_one(self, item): assert default.startswith(b'default=') is_default_str = default.split(b'=')[1] - is_default = ast.literal_eval(is_default_str.decode('ascii')) + is_default = is_default_str.decode('ascii') == "True" assert isinstance(is_default, bool) prop_type = prop_type.decode('ascii') From 671150da948fbc4957d167a71058fb9ed0442bb7 Mon Sep 17 00:00:00 2001 From: qubesuser Date: Thu, 9 Nov 2017 02:43:17 +0100 Subject: [PATCH 6/6] remove spinner code Tools should be written to be fast enough that spinners don't become necessary. --- doc/manpages/qvm-ls.rst | 8 -- qubesadmin/spinner.py | 147 ------------------------------------- qubesadmin/tools/qvm_ls.py | 27 +------ 3 files changed, 2 insertions(+), 180 deletions(-) delete mode 100644 qubesadmin/spinner.py diff --git a/doc/manpages/qvm-ls.rst b/doc/manpages/qvm-ls.rst index 02c4272c..7b5e2150 100644 --- a/doc/manpages/qvm-ls.rst +++ b/doc/manpages/qvm-ls.rst @@ -47,14 +47,6 @@ Options Decrease verbosity. -.. option:: --spinner - - Have a spinner spinning while the spinning mainloop spins new table cells. - -.. option:: --no-spinner - - No spinner today. - Authors ------- | Joanna Rutkowska diff --git a/qubesadmin/spinner.py b/qubesadmin/spinner.py deleted file mode 100644 index 58e67586..00000000 --- a/qubesadmin/spinner.py +++ /dev/null @@ -1,147 +0,0 @@ -# vim: fileencoding=utf-8 - -# -# The Qubes OS Project, https://www.qubes-os.org/ -# -# Copyright (C) 2017 Wojtek Porczyk -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation; either version 2.1 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program; if not, see . -# - -'''Qubes CLI spinner - -A novice asked the master: “In the east there is a great tree-structure that -men call `Corporate Headquarters'. It is bloated out of shape with vice -presidents and accountants. It issues a multitude of memos, each saying `Go, -Hence!' or `Go, Hither!' and nobody knows what is meant. Every year new names -are put onto the branches, but all to no avail. How can such an unnatural -entity be?" - -The master replied: “You perceive this immense structure and are disturbed that -it has no rational purpose. Can you not take amusement from its endless -gyrations? Do you not enjoy the untroubled ease of programming beneath its -sheltering branches? Why are you bothered by its uselessness?” - -(Geoffrey James, “The Tao of Programming”, 7.1) -''' - -import curses -import io -import itertools - -CHARSET = '-\\|/' -ENTERPRISE_CHARSET = CHARSET * 4 + '-._.-^' * 2 - -class AbstractSpinner(object): - '''The base class for all Spinners - - :param stream: file-like object with ``.write()`` method - :param str charset: the sequence of characters to display - - The spinner should be used as follows: - 1. exactly one call to :py:meth:`show()` - 2. zero or more calls to :py:meth:`update()` - 3. exactly one call to :py:meth:`hide()` - ''' - def __init__(self, stream, charset=CHARSET): - self.stream = stream - self.charset = itertools.cycle(charset) - - def show(self, prompt): - '''Show the spinner, with a prompt - - :param str prompt: prompt, like "please wait" - ''' - raise NotImplementedError() - - def hide(self): - '''Hide the spinner and the prompt''' - raise NotImplementedError() - - def update(self): - '''Show next spinner character''' - raise NotImplementedError() - - -class DummySpinner(AbstractSpinner): - '''Dummy spinner, does not do anything''' - def show(self, prompt): - pass - - def hide(self): - pass - - def update(self): - pass - - -class QubesSpinner(AbstractSpinner): - '''Basic spinner - - This spinner uses standard ASCII control characters''' - def __init__(self, *args, **kwargs): - super(QubesSpinner, self).__init__(*args, **kwargs) - self.hidelen = 0 - self.cub1 = '\b' - - def show(self, prompt): - self.hidelen = len(prompt) + 2 - self.stream.write('{} {}'.format(prompt, next(self.charset))) - self.stream.flush() - - def hide(self): - self.stream.write('\r' + ' ' * self.hidelen + '\r') - self.stream.flush() - - def update(self): - self.stream.write(self.cub1 + next(self.charset)) - self.stream.flush() - - -class QubesSpinnerEnterpriseEdition(QubesSpinner): - '''Enterprise spinner - - This is tty- and terminfo-aware spinner. Recommended. - ''' - def __init__(self, stream, charset=None): - # our Enterprise logic follows - self.stream_isatty = stream.isatty() - if charset is None: - charset = ENTERPRISE_CHARSET if self.stream_isatty else '.' - - super(QubesSpinnerEnterpriseEdition, self).__init__(stream, charset) - - if self.stream_isatty: - try: - curses.setupterm() - self.has_terminfo = True - self.cub1 = curses.tigetstr('cub1').decode() - except (curses.error, io.UnsupportedOperation): - # we are in very non-Enterprise environment - self.has_terminfo = False - else: - self.cub1 = '' - - def hide(self): - if self.stream_isatty: - hideseq = '\r' + ' ' * self.hidelen + '\r' - if self.has_terminfo: - hideseq_l = (curses.tigetstr('cr'), curses.tigetstr('clr_eol')) - if all(seq is not None for seq in hideseq_l): - hideseq = ''.join(seq.decode() for seq in hideseq_l) - else: - hideseq = '\n' - - self.stream.write(hideseq) - self.stream.flush() diff --git a/qubesadmin/tools/qvm_ls.py b/qubesadmin/tools/qvm_ls.py index e45a9123..26c5ec92 100644 --- a/qubesadmin/tools/qvm_ls.py +++ b/qubesadmin/tools/qvm_ls.py @@ -34,7 +34,6 @@ import textwrap import qubesadmin -import qubesadmin.spinner import qubesadmin.tools import qubesadmin.utils import qubesadmin.vm @@ -387,10 +386,9 @@ class Table(object): :param qubes.Qubes app: Qubes application object. :param list colnames: Names of the columns (need not to be uppercase). ''' - def __init__(self, app, colnames, spinner, raw_data=False): + def __init__(self, app, colnames, raw_data=False): self.app = app self.columns = tuple(Column.columns[col.upper()] for col in colnames) - self.spinner = spinner self.raw_data = raw_data def get_head(self): @@ -402,7 +400,6 @@ def get_row(self, vm): ret = [] for col in self.columns: ret.append(col.cell(vm)) - self.spinner.update() return ret def write_table(self, stream=sys.stdout): @@ -413,12 +410,9 @@ def write_table(self, stream=sys.stdout): table_data = [] if not self.raw_data: - self.spinner.show('please wait...') table_data.append(self.get_head()) - self.spinner.update() for vm in sorted(self.app.domains): table_data.append(self.get_row(vm)) - self.spinner.hide() qubesadmin.tools.print_table(table_data, stream=stream) else: for vm in sorted(self.app.domains): @@ -529,16 +523,6 @@ def get_parser(): help='Display specify data of specified VMs. Intended for ' 'bash-parsing.') - parser.add_argument('--spinner', - action='store_true', dest='spinner', - help='reenable spinner') - - parser.add_argument('--no-spinner', - action='store_false', dest='spinner', - help='disable spinner') - - parser.set_defaults(spinner=True) - # parser.add_argument('--conf', '-c', # action='store', metavar='CFGFILE', # help='Qubes config file') @@ -571,15 +555,8 @@ def main(args=None, app=None): if col.upper() not in Column.columns: PropertyColumn(col.lower()) - if args.spinner: - # we need Enterprise Edition™, since it's the only one that detects TTY - # and uses dots if we are redirected somewhere else - spinner = qubesadmin.spinner.QubesSpinnerEnterpriseEdition(sys.stderr) - else: - spinner = qubesadmin.spinner.DummySpinner(sys.stderr) - args.app.domains.get_all_data() - table = Table(args.app, columns, spinner) + table = Table(args.app, columns) table.write_table(sys.stdout) return 0