Source code for stoq.lib.status

# -*- Mode: Python; coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4

##
## Copyright (C) 2016 Async Open Source <http://www.async.com.br>
## All rights reserved
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 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 General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., or visit: http://www.gnu.org/.
##
## Author(s): Stoq Team <stoq-devel@async.com.br>
##

import datetime
import os
import threading

import dateutil.parser
import glib
import gobject
from kiwi.python import Settable
from kiwi.utils import gsignal

from stoqlib.api import api, safe_str
from stoqlib.gui.base.dialogs import run_dialog
from stoqlib.gui.editors.backupsettings import BackupSettingsEditor
from stoqlib.lib.threadutils import threadit, schedule_in_main_thread
from stoqlib.lib.translation import stoqlib_gettext as _
from stoqlib.lib.webservice import WebService
from stoqlib.net.server import ServerProxy, ServerError


[docs]class ResourceStatus(gobject.GObject): """The status of a given resource""" gsignal('status-changed', int, str) (STATUS_NA, STATUS_OK, STATUS_WARNING, STATUS_ERROR) = range(4) status_label = { STATUS_NA: _("N/A"), STATUS_OK: _("OK"), STATUS_WARNING: _("WARNING"), STATUS_ERROR: _("ERROR"), } name = None label = None priority = 0 def __init__(self): super(ResourceStatus, self).__init__() assert self.name is not None self.status = self.STATUS_OK self.reason = None self.reason_long = None def __eq__(self, other): if type(self) != type(other): return False return self.name == other.name @property def status_str(self): return self.status_label[self.status]
[docs] def refresh(self): """Refresh the resource status Subclasses should override this and update :obj:`.status` and :obj:`.reason` Note that this will not be running on the main thread, so be cautelous with non thread-safe operations. """ raise NotImplementedError
[docs] def get_actions(self): """Get the actions that can be run for this resource""" return []
[docs] def refresh_and_notify(self): """Refresh the resource status and notify for changes""" old_status, old_reason = self.status, self.reason self.refresh() if (self.status, self.reason) != (old_status, old_reason): # This is running on another so schedule the emit in the main one schedule_in_main_thread( self.emit, 'status-changed', self.status, self.reason)
[docs]class ResourceStatusAction(object): def __init__(self, resource, name, label, callback, threaded=True, admin_only=True): self.resource = resource self.name = name self.label = label self.callback = callback self.threaded = threaded self.admin_only = admin_only def __eq__(self, other): if type(self) != type(other): return False return (self.resource, self.name) == (other.resource, other.name)
[docs]class ResourceStatusManager(gobject.GObject): gsignal('status-changed', int) gsignal('action-finished', object, object) REFRESH_TIMEOUT = int(os.environ.get('STOQ_STATUS_REFRESH_TIMEOUT', 60)) _instance = None def __init__(self): super(ResourceStatusManager, self).__init__() self._lock = threading.Lock() self.running_action = None self.resources = {} glib.timeout_add_seconds(self.REFRESH_TIMEOUT, self.refresh_and_notify) # # Public API # @classmethod
[docs] def get_instance(cls): """Get the manager singleton instance""" if cls._instance is None: cls._instance = cls() return cls._instance
@property def status(self): """The general status of the resources""" if any(resource.status == ResourceStatus.STATUS_ERROR for resource in self._iter_resources()): return ResourceStatus.STATUS_ERROR elif any(resource.status == ResourceStatus.STATUS_WARNING for resource in self._iter_resources()): return ResourceStatus.STATUS_WARNING elif any(resource.status == ResourceStatus.STATUS_NA for resource in self._iter_resources()): return ResourceStatus.STATUS_NA else: return ResourceStatus.STATUS_OK
[docs] def add_resource(self, resource, refresh=False): """Add a :class:`.ResourceStatus` on the manager""" assert resource.name not in self.resources assert isinstance(resource, ResourceStatus) self.resources[resource.name] = resource if refresh: self.refresh_and_notify()
[docs] def refresh_and_notify(self, force=False): """Refresh the status and notify for changes""" # Do not run checks if we are running tests. It breaks the whole suite if os.environ.get('STOQ_TESTSUIT_RUNNING', '0') == '1': return False return threadit(self._refresh_and_notify, force=force)
[docs] def handle_action(self, action): """Ask the given resource to handle the given action""" if action.threaded: self.running_action = action return threadit(self._handle_action, action) else: return self._handle_action(action)
# # Private # def _iter_resources(self): return sorted(self.resources.itervalues(), key=lambda r: r.priority) def _refresh_and_notify(self, resources=None, force=False): with self._lock: old_status = self.status resources = resources or self._iter_resources() for resource in resources: resource.refresh_and_notify() status = self.status if status != old_status or force: # This is running on another so schedule the emit in the main one schedule_in_main_thread( self.emit, 'status-changed', status) def _handle_action(self, action): try: with self._lock: retval = action.callback() except Exception as e: retval = e finally: self.running_action = None schedule_in_main_thread(self.emit, 'action-finished', action, retval) return retval
[docs]def register(resource_class, refresh=False): manager = ResourceStatusManager.get_instance() manager.add_resource(resource_class(), refresh=refresh) return resource_class
@register class _ServerStatus(ResourceStatus): name = "stoqserver" label = _("Online Services") priority = 99 def __init__(self): ResourceStatus.__init__(self) self._server = ServerProxy() def refresh(self): if not api.sysparam.get_bool('ONLINE_SERVICES'): self.status = ResourceStatus.STATUS_NA self.reason = (_("Online services (Stoq.link integration, backup, " "etc) not enabled...")) self.reason_long = _('Enable the parameter "Online Services" ' 'on the "Admin" app to solve this issue') return if self._server.check_running(): self.status = self.STATUS_OK self.reason = _("Online services data hub is running fine.") self.reason_long = None else: self.status = ResourceStatus.STATUS_ERROR self.reason = _("Online services data hub not found...") package = '<a href="apt://stoq-server">stoq-server</a>' self.reason_long = safe_str( api.escape(_("Install and configure the %s package " "to solve this issue")) % (package, )) @register class _BackupStatus(ResourceStatus): name = "backup" label = _("Backup") priority = 98 def __init__(self): ResourceStatus.__init__(self) self._webservice = WebService() self._server = ServerProxy() def refresh(self): if not api.sysparam.get_bool('ONLINE_SERVICES'): self.status = ResourceStatus.STATUS_NA self.reason = _('Backup service not running because ' '"Online Services" is disabled') self.reason_long = _('Enable the parameter "Online Services" ' 'on the "Admin" app to solve this issue') return try: key = self._server.call('get_backup_key') except ServerError: pass else: if not key: self.status = self.STATUS_WARNING self.reason = _("Backup key not configured") self.reason_long = _('Click on "Configure" button to ' 'configure the backup key') return request = self._webservice.status() try: response = request.get_response() except Exception as e: self.status = self.STATUS_WARNING self.reason = _("Could not communicate with Stoq.link") self.reason_long = str(e) return if response.status_code != 200: self.status = self.STATUS_WARNING self.reason = _("Could not communicate with Stoq.link") self.reason_long = None return data = response.json() if data['latest_backup_date']: backup_date = dateutil.parser.parse(data['latest_backup_date']) delta = datetime.datetime.today() - backup_date if delta.days > 3: self.status = self.STATUS_WARNING self.reason = _("Backup is late. Last backup date is %s") % ( backup_date.strftime('%x')) self.reason_long = _("Check your Stoq Server logs to see if " "there's any problem with it") else: self.status = self.STATUS_OK self.reason = _("Backup is up-to-date. Last backup date is %s") % ( backup_date.strftime('%x')) self.reason_long = None else: self.status = self.STATUS_WARNING self.reason = _("There's no backup data yet") self.reason_long = None def get_actions(self): if self.status != ResourceStatus.STATUS_NA: yield ResourceStatusAction( self, 'backup-now', _("Backup now"), self._on_backup_now, threaded=True) yield ResourceStatusAction( self, 'configure', _("Configure"), self._on_configure, threaded=False) def _on_configure(self): key = self._server.call('get_backup_key') with api.new_store() as store: rv = run_dialog(BackupSettingsEditor, None, store, Settable(key=key)) if rv: key = self._server.call('set_backup_key', rv.key) def _on_backup_now(self): self._server.call('backup_database')