diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..166b8f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +.DS_Store +.editorconfig +.idea +**/*.swp +__pycache__/ +linux/debian/.debhelper/ +linux/debian/files +linux/debian/ogagent/ +linux/debian/ogagent.debhelper.log +linux/debian/ogagent.postinst.debhelper +linux/debian/ogagent.postrm.debhelper +linux/configure-stamp +linux/build-stamp +ogagent_*_all.deb +ogagent_*_amd64.buildinfo +ogagent_*_amd64.changes +ogagent_*_amd64.build +src/about_dialog_ui.py +src/message_dialog_ui.py +src/OGAgent_rc.py diff --git a/INSTALL.es.txt b/INSTALL.es.txt index 9eb2e17..ad466f6 100644 --- a/INSTALL.es.txt +++ b/INSTALL.es.txt @@ -1,25 +1,22 @@ OGAgent: agente OpenGnsys para sistemas operativos INSTALL.es.txt ==================================================================== - Requisitos de creación ---------------------- -Sisitema operativo Linux con los siguientes paquetes instalados: -- Subversion -- GNU C++, Python, librerías PyQt4 +Sistema operativo Linux con los siguientes paquetes instalados: +- GNU C++, Python, librerías PyQt6 - Creación de instalador Exe (Wine 32 bits, Wine Gecko, Wine Mono, Samba Winbind, Cabextrct) - Creación de paquetes Deb (debhelper, dpkg-dev) -- Creación de paquetes RPM (rpm-build) - Creación de paquetes Pkg (xar, bomutils) Crear instaladores de OGAgent ----------------------------- -- Paso previo: actaulizar componentes gráficos de PyQt para OGAgnet: +- Paso previo: actualizar componentes gráficos de PyQt para OGAgent: src/update.sh -- Crear paquetes Deb y RPM para distribuciones Linux (requiere permisos de "root"): - sudo linux/build-packages.sh +- Crear paquetes Deb distribuciones debian/ubuntu + linux/build-packages.sh - Crear paquete Pkg para sistemas operativos macOS X: sudo macos/build-pkg.sh @@ -27,8 +24,8 @@ Crear instaladores de OGAgent - Crear el programa instalador para sistemas operativos Windows: windows/build-windows.sh -- Subir los nuevos ficheros .deb, .rpm, .pkg y .exe generados al directorio -/opt/opengnsys/www/descargas del servidor OpenGnsys. +- Subir los nuevos ficheros .deb, .pkg y .exe generados al directorio +/opt/opengnsys/www/descargas del servidor OpenGnsys. Instalar OGAgent en cliente modelo @@ -43,20 +40,6 @@ Instalar OGAgent en cliente modelo - Iniciar el servicio (se iniciará automáticamente en el proceso de arranque): sudo service ogagent start -- Red Hat, Fedora y derivados (como root): - - Descargar e instalar el agente: - yum install ogagent-Version.noarch.rpm (Red Hat/CentOS) - dnf install ogagent-Version.noarch.rpm (Fedora) - - Configurar el agente: - sed -i "0,/remote=/ s,remote=.*,remote=https://IPServidorOpenGnsys/opengnsys/rest/," /usr/share/OGAgent/cfg/ogagent.cfg - - Puede ser necesario corregir permisos antes de iniciar el servicio: - chmod +x /etc/init.d/ogagent - - Iniciar el servicio (se iniciará automáticamente en el proceso de arranque): - service ogagent start - -- OpenSuSE: - (en preparación) - - Windows (como usuario administrador): - Descargar e instalar el agente ejecutando: OGAgentSetup-Version.exe @@ -83,5 +66,3 @@ Postconfiguración para clientes clonados - Ejecutar manualmente o configurar automáticamente OGAgent en los clientes clonados en el script de postconfiguración tras restuarar imagen: ogConfigureOgagent NDisco Npart - - diff --git a/linux/build-packages.sh b/linux/build-packages.sh index facde2c..baf9582 100755 --- a/linux/build-packages.sh +++ b/linux/build-packages.sh @@ -1,37 +1,6 @@ #!/bin/bash cd $(dirname "$0") -top=$(pwd) - -VERSION="$(cat ../src/VERSION 2>/dev/null)" || VERSION="1.1.1" -RELEASE="1" # Debian based -dpkg-buildpackage -b -d - -# Fix version number. -sed -e "s/version 0.0.0/version ${VERSION}/g" \ - -e "s/release 1/release ${RELEASE}/g" ogagent-template.spec > ogagent-$VERSION.spec - -# Now fix dependencies for opensuse -sed -e "s/name ogagent/name ogagent-opensuse/g" \ - -e "s/version 0.0.0/version ${VERSION}/g" \ - -e "s/release 1/release ${RELEASE}/g" \ - -e "s/chkconfig//g" \ - -e "s/initscripts/insserv/g" \ - -e "s/PyQt4/python-qt4/g" \ - -e "s/libXScrnSaver/libXss1/g" ogagent-template.spec > ogagent-opensuse-$VERSION.spec - - -# Right now, ogagent-xrdp-1.7.0.spec is not needed -for pkg in ogagent-$VERSION.spec ogagent-opensuse-$VERSION.spec; do - - rm -rf rpm - for folder in SOURCES BUILD RPMS SPECS SRPMS; do - mkdir -p rpm/$folder - done - - rpmbuild -v -bb --clean --buildroot=$top/rpm/BUILD/$pkg-root --target noarch $pkg 2>&1 -done - -#rm ogagent-$VERSION +debuild -D --build=binary --post-clean --lintian-opts --profile debian diff --git a/linux/debian/changelog b/linux/debian/changelog index 9b59a25..e21205c 100644 --- a/linux/debian/changelog +++ b/linux/debian/changelog @@ -1,3 +1,21 @@ +ogagent (1.3.0-2) stable; urgency=medium + + * Add missing dependency on zenity + + -- OpenGnsys developers Thu, 25 Apr 2024 15:53:16 +0200 + +ogagent (1.3.0-1) stable; urgency=medium + + * Upgrade to Qt 6 + + -- OpenGnsys developers Thu, 25 Apr 2024 12:50:20 +0200 + +ogagent (1.2.0) unstable; urgency=medium + + * Python 3 and Qt 5 compatibility + + -- OpenGnsys developers Mon, 4 May 2020 18:00:00 +0100 + ogagent (1.1.1b) stable; urgency=medium * Use python-distro to detect the distribution version diff --git a/linux/debian/compat b/linux/debian/compat index f11c82a..f599e28 100644 --- a/linux/debian/compat +++ b/linux/debian/compat @@ -1 +1 @@ -9 \ No newline at end of file +10 diff --git a/linux/debian/control b/linux/debian/control index 7df4dea..c8d9e6e 100644 --- a/linux/debian/control +++ b/linux/debian/control @@ -1,7 +1,7 @@ Source: ogagent Section: admin Priority: optional -Maintainer: Ramón M. Gómez +Maintainer: OpenGnsys developers Build-Depends: debhelper (>= 7), po-debconf Standards-Version: 3.9.2 Homepage: https://opengnsys.es/ @@ -11,8 +11,7 @@ Section: admin Priority: optional Architecture: all Depends: - policykit-1 (>= 0.100), python2 (>=2.7) | python (>= 2.7), python-qt4 (>= 4.9),, python-requests (>= 0.8.2), - python-six (>= 1.1), python-prctl (>= 1.1.1), python-distro, libxss1, ${misc:Depends} -Suggests: gnome-shell-extension-top-icons-plus + policykit-1 (>= 0.100), python3 (>=3.4) | python (>= 3.4), python3-pyqt6, python3-requests, + python3-six, python3-prctl, python3-distro, libxss1, zenity, ${misc:Depends} Description: OpenGnsys Agent for Operating Systems This package provides the required components to allow this machine to work on an environment managed by OpenGnsys. diff --git a/linux/debian/copyright b/linux/debian/copyright index 7b6ef31..c333aaa 100644 --- a/linux/debian/copyright +++ b/linux/debian/copyright @@ -1,6 +1,6 @@ Format-Specification: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=135 Name: ogagent -Maintainer: Ramón M. Gómez +Maintainer: OpenGnsys developers Source: https://opengnsys.es Copyright: 2014 Virtual Cable S.L.U. diff --git a/linux/debian/ogagent.postinst.debhelper b/linux/debian/ogagent.postinst.debhelper deleted file mode 100644 index e75924d..0000000 --- a/linux/debian/ogagent.postinst.debhelper +++ /dev/null @@ -1,5 +0,0 @@ -# Automatically added by dh_installinit -if [ -x "/etc/init.d/ogagent" ]; then - update-rc.d ogagent defaults >/dev/null || exit $? -fi -# End automatically added section diff --git a/linux/debian/ogagent.postrm.debhelper b/linux/debian/ogagent.postrm.debhelper deleted file mode 100644 index 3167f1f..0000000 --- a/linux/debian/ogagent.postrm.debhelper +++ /dev/null @@ -1,12 +0,0 @@ -# Automatically added by dh_installinit -if [ "$1" = "purge" ] ; then - update-rc.d ogagent remove >/dev/null -fi - - -# In case this system is running systemd, we make systemd reload the unit files -# to pick up changes. -if [ -d /run/systemd/system ] ; then - systemctl --system daemon-reload >/dev/null || true -fi -# End automatically added section diff --git a/linux/debian/ogagent.substvars b/linux/debian/ogagent.substvars deleted file mode 100644 index 978fc8b..0000000 --- a/linux/debian/ogagent.substvars +++ /dev/null @@ -1,2 +0,0 @@ -misc:Depends= -misc:Pre-Depends= diff --git a/linux/debian/rules b/linux/debian/rules index fbe82e6..ead6aa0 100755 --- a/linux/debian/rules +++ b/linux/debian/rules @@ -31,7 +31,7 @@ binary-indep: build install dh_installdocs dh_installdebconf dh_installinit --no-start - dh_python2=python + dh_python3=python dh_compress dh_link dh_fixperms diff --git a/linux/ogagent-template.spec b/linux/ogagent-template.spec deleted file mode 100644 index 7f55454..0000000 --- a/linux/ogagent-template.spec +++ /dev/null @@ -1,83 +0,0 @@ -%define _topdir %(echo $PWD)/rpm -%define name ogagent -%define version 0.0.0 -%define release 1 -%define buildroot %{_topdir}/%{name}-%{version}-%{release}-root - -BuildRoot: %{buildroot} -Name: %{name} -Version: %{version} -Release: %{release} -Summary: OpenGnsys Agent for Operating Systems -License: BSD3 -Group: Admin -Requires: chkconfig initscripts python-six python-requests python-distro PyQt4 libXScrnSaver -Vendor: OpenGnsys Project -URL: https://opengnsys.es/ -Provides: ogagent - -%define _rpmdir ../ -%define _rpmfilename %%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm - - -%install -curdir=`pwd` -cd ../.. -make DESTDIR=$RPM_BUILD_ROOT DISTRO=rh install-ogagent -cd $curdir - -%clean -rm -rf $RPM_BUILD_ROOT -curdir=`pwd` -cd ../.. -make DESTDIR=$RPM_BUILD_ROOT DISTRO=rh clean -cd $curdir - - -%post -systemctl enable ogagent.service > /dev/null 2>&1 - -%preun -systemctl disable ogagent.service > /dev/null 2>&1 -systemctl stop ogagent.service > /dev/null 2>&1 - -%postun -# $1 == 0 on uninstall, == 1 on upgrade for preun and postun (just a reminder for me... :) ) -if [ $1 -eq 0 ]; then - rm -rf /etc/ogagent - rm /var/log/ogagent.log -fi -# And, posibly, the .pyc leaved behind on /usr/share/OGAgent -rm -rf /usr/share/OGAgent > /dev/null 2>&1 - -%description -This package provides the required components to allow this machine to work on an environment managed by OpenGnsys. - -%files -%defattr(-,root,root) -/etc/ogagent -/etc/xdg/autostart/OGAgentTool.desktop -/etc/init.d/ogagent -/usr/bin/OGAgentTool-startup -/usr/bin/ogagent -/usr/bin/OGAgentTool -/usr/share/OGAgent/* -/usr/share/autostart/OGAgentTool.desktop - -%changelog -* Fri Feb 07 2020 Ramón M. Gómez - 1.1.1b-1 -- Use python-distro to detect the distribution version - -* Thu May 23 2019 Ramón M. Gómez - 1.1.1-1 -- Set connection timeout -- Compatibility with "Exam Mode" from the University of Seville - -* Wed May 22 2019 Ramón M. Gómez - 1.1.0a-1 -- Fix a bug when activating the agent with some network devices - -* Tue Oct 13 2016 Ramón M. Gómez - 1.1.0-1 -- Functional OpenGnsys Agent interacting with OpenGnsys Server 1.1.0 - -* Tue Jul 18 2015 Adolfo Gómez García - 1.0.0-1 -- Initial release for OpenGnsys Agent - diff --git a/linux/scripts/OGAgentTool b/linux/scripts/OGAgentTool index da91704..0801591 100755 --- a/linux/scripts/OGAgentTool +++ b/linux/scripts/OGAgentTool @@ -1,10 +1,10 @@ #!/bin/sh -for p in python python2; do - [ -z "$PYTHON" ] && [ $($p -c 'import sys; print(sys.version_info[0])') -eq 2 ] && PYTHON=$p +for p in python python3; do + [ "$(command -v $p)" ] && [ -z "$PYTHON" ] && [ $($p -c 'import sys; print(sys.version_info[0])') -eq 3 ] && PYTHON=$p done if [ -z "$PYTHON" ]; then - echo "ERROR: OGAgent needs Python 2" &>2 + echo "ERROR: OGAgent needs Python 3" &>2 exit 1 fi diff --git a/linux/scripts/ogagent b/linux/scripts/ogagent index 155af67..ba83021 100755 --- a/linux/scripts/ogagent +++ b/linux/scripts/ogagent @@ -1,10 +1,10 @@ #!/bin/sh -for p in python python2; do - [ -z "$PYTHON" ] && [ $($p -c 'import sys; print(sys.version_info[0])') -eq 2 ] && PYTHON=$p +for p in python python3; do + [ "$(command -v $p)" ] && [ -z "$PYTHON" ] && [ $($p -c 'import sys; print(sys.version_info[0])') -eq 3 ] && PYTHON=$p done if [ -z "$PYTHON" ]; then - echo "ERROR: OGAgent needs Python 2" &>2 + echo "ERROR: OGAgent needs Python 3" &>2 exit 1 fi diff --git a/requires.txt b/requires.txt deleted file mode 100644 index 07ce387..0000000 --- a/requires.txt +++ /dev/null @@ -1,3 +0,0 @@ -six -requests - diff --git a/src/OGAServiceHelper.py b/src/OGAServiceHelper.py index ca75a09..ddcdf2d 100644 --- a/src/OGAServiceHelper.py +++ b/src/OGAServiceHelper.py @@ -29,7 +29,6 @@ ''' @author: Adolfo Gómez, dkmaster at dkmon dot com ''' -from __future__ import unicode_literals import win32service import win32serviceutil diff --git a/src/OGAgentUser.py b/src/OGAgentUser.py index 2df807b..d4ec146 100644 --- a/src/OGAgentUser.py +++ b/src/OGAgentUser.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (c) 2014 Virtual Cable S.L. @@ -29,28 +29,21 @@ """ @author: Adolfo Gómez, dkmaster at dkmon dot com """ -from __future__ import unicode_literals - +import atexit +import base64 +import json import sys import time -import json -import six -import atexit -from PyQt4 import QtCore, QtGui # @UnresolvedImport +from PyQt6 import QtCore, QtGui, QtWidgets -from opengnsys import VERSION, ipc, operations, utils -from opengnsys.log import logger -from opengnsys.service import IPC_PORT from about_dialog_ui import Ui_OGAAboutDialog from message_dialog_ui import Ui_OGAMessageDialog -from opengnsys.scriptThread import ScriptExecutorThread +from opengnsys import VERSION, ipc, operations, utils from opengnsys.config import readConfig from opengnsys.loader import loadModules - -# Set default characters encoding to UTF-8 -reload(sys) -if hasattr(sys, 'setdefaultencoding'): - sys.setdefaultencoding('utf-8') +from opengnsys.log import logger +from opengnsys.scriptThread import ScriptExecutorThread +from opengnsys.service import IPC_PORT trayIcon = None @@ -61,9 +54,9 @@ def sigAtExit(): # About dialog -class OGAAboutDialog(QtGui.QDialog): +class OGAAboutDialog(QtWidgets.QDialog): def __init__(self, parent=None): - QtGui.QDialog.__init__(self, parent) + QtWidgets.QDialog.__init__(self, parent) self.ui = Ui_OGAAboutDialog() self.ui.setupUi(self) self.ui.VersionLabel.setText("Version " + VERSION) @@ -72,9 +65,9 @@ class OGAAboutDialog(QtGui.QDialog): self.hide() -class OGAMessageDialog(QtGui.QDialog): +class OGAMessageDialog(QtWidgets.QDialog): def __init__(self, parent=None): - QtGui.QDialog.__init__(self, parent) + QtWidgets.QDialog.__init__(self, parent) self.ui = Ui_OGAMessageDialog() self.ui.setupUi(self) @@ -89,7 +82,7 @@ class OGAMessageDialog(QtGui.QDialog): class MessagesProcessor(QtCore.QThread): logoff = QtCore.pyqtSignal(name='logoff') message = QtCore.pyqtSignal(tuple, name='message') - script = QtCore.pyqtSignal(QtCore.QString, name='script') + script = QtCore.pyqtSignal(str, name='script') exit = QtCore.pyqtSignal(name='exit') def __init__(self, port): @@ -119,9 +112,9 @@ class MessagesProcessor(QtCore.QThread): if self.ipc: self.ipc.sendLogin(user_data) - def sendLogout(self, userName): + def sendLogout(self, username): if self.ipc: - self.ipc.sendLogout(userName) + self.ipc.sendLogout(username) def run(self): if self.ipc is None: @@ -136,20 +129,17 @@ class MessagesProcessor(QtCore.QThread): msg = self.ipc.getMessage() if msg is None: break - msgId, data = msg - logger.debug('Got Message on User Space: {}:{}'.format(msgId, data)) - if msgId == ipc.MSG_MESSAGE: - module, message, data = data.split('\0') + msg_id, data = msg + logger.debug('Got Message on User Space: {}:{}'.format(msg_id, data)) + if msg_id == ipc.MSG_MESSAGE: + module, message, data = data.decode('utf-8').split('\0') self.message.emit((module, message, data)) - elif msgId == ipc.MSG_LOGOFF: + elif msg_id == ipc.MSG_LOGOFF: self.logoff.emit() - elif msgId == ipc.MSG_SCRIPT: - self.script.emit(QtCore.QString.fromUtf8(data)) + elif msg_id == ipc.MSG_SCRIPT: + self.script.emit(data.decode('utf-8')) except Exception as e: - try: - logger.error('Got error on IPC thread {}'.format(utils.exceptionToMessage(e))) - except: - logger.error('Got error on IPC thread (an unicode error??)') + logger.error('Got error on IPC thread {}'.format(utils.exceptionToMessage(e))) if self.ipc.running is False and self.running is True: logger.warn('Lost connection with Service, closing program') @@ -157,27 +147,24 @@ class MessagesProcessor(QtCore.QThread): self.exit.emit() -class OGASystemTray(QtGui.QSystemTrayIcon): +class OGASystemTray(QtWidgets.QSystemTrayIcon): def __init__(self, app_, parent=None): self.app = app_ self.config = readConfig(client=True) - + self.modules = None # Get opengnsys section as dict cfg = dict(self.config.items('opengnsys')) - # Set up log level logger.setLevel(cfg.get('log', 'INFO')) self.ipcport = int(cfg.get('ipc_port', IPC_PORT)) - # style = app.style() - # icon = QtGui.QIcon(style.standardPixmap(QtGui.QStyle.SP_ComputerIcon)) icon = QtGui.QIcon(':/images/img/oga.png') - QtGui.QSystemTrayIcon.__init__(self, icon, parent) - self.menu = QtGui.QMenu(parent) - exitAction = self.menu.addAction("About") - exitAction.triggered.connect(self.about) + QtWidgets.QSystemTrayIcon.__init__(self, icon, parent) + self.menu = QtWidgets.QMenu(parent) + exit_action = self.menu.addAction("About") + exit_action.triggered.connect(self.about) self.setContextMenu(self.menu) self.ipc = MessagesProcessor(self.ipcport) @@ -208,18 +195,16 @@ class OGASystemTray(QtGui.QSystemTrayIcon): logger.debug('Modules: {}'.format(list(v.name for v in self.modules))) # Send init to all modules - validMods = [] + valid_mods = [] for mod in self.modules: try: logger.debug('Activating module {}'.format(mod.name)) mod.activate() - validMods.append(mod) + valid_mods.append(mod) except Exception as e: logger.exception() logger.error("Activation of {} failed: {}".format(mod.name, utils.exceptionToMessage(e))) - - self.modules[:] = validMods # copy instead of assignment - + self.modules[:] = valid_mods # copy instead of assignment # If this is running, it's because he have logged in, inform service of this fact self.ipc.sendLogin((operations.getCurrentUser(), operations.getSessionLanguage(), operations.get_session_type())) @@ -260,7 +245,7 @@ class OGASystemTray(QtGui.QSystemTrayIcon): def executeScript(self, script): logger.debug('Executing script') - script = six.text_type(script.toUtf8()).decode('base64') + script = base64.b64decode(script.encode('ascii')) th = ScriptExecutorThread(script) th.start() @@ -269,7 +254,7 @@ class OGASystemTray(QtGui.QSystemTrayIcon): operations.logoff() # Invoke log off def about(self): - self.aboutDlg.exec_() + self.aboutDlg.exec() def cleanup(self): logger.debug('Quit invoked') @@ -311,14 +296,14 @@ class OGASystemTray(QtGui.QSystemTrayIcon): if __name__ == '__main__': - app = QtGui.QApplication(sys.argv) + app = QtWidgets.QApplication(sys.argv) - if not QtGui.QSystemTrayIcon.isSystemTrayAvailable(): + if not QtWidgets.QSystemTrayIcon.isSystemTrayAvailable(): # QtGui.QMessageBox.critical(None, "Systray", "I couldn't detect any system tray on this system.") sys.exit(1) # This is important so our app won't close on messages windows (alerts, etc...) - QtGui.QApplication.setQuitOnLastWindowClosed(False) + QtWidgets.QApplication.setQuitOnLastWindowClosed(False) try: trayIcon = OGASystemTray(app) @@ -342,7 +327,7 @@ if __name__ == '__main__': # Catch kill and logout user :) atexit.register(sigAtExit) - res = app.exec_() + res = app.exec() logger.debug('Exiting') trayIcon.quit() diff --git a/src/OGAgent_rc.py b/src/OGAgent_rc.py deleted file mode 100644 index b0a9562..0000000 --- a/src/OGAgent_rc.py +++ /dev/null @@ -1,292 +0,0 @@ -# -*- coding: utf-8 -*- - -# Resource object code -# -# Created by: The Resource Compiler for PyQt4 (Qt v4.8.6) -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore - -qt_resource_data = b"\ -\x00\x00\x0f\x42\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x30\x00\x00\x00\x30\x08\x06\x00\x00\x01\x20\x05\xc9\x11\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0e\xc7\x00\x00\x0e\xc7\ -\x01\x38\x92\x2f\x76\x00\x00\x0e\xf4\x49\x44\x41\x54\x78\xda\xc5\ -\x59\x07\x58\x53\xd9\x12\xbe\x29\xd4\x84\x12\x21\xf4\x22\x08\x08\ -\x02\x22\xa2\x22\x22\x22\x8a\xa0\xa0\x14\x1b\xae\x62\x05\xac\xb8\ -\x76\xd7\xde\xd6\xde\x15\xf5\xd9\x7b\x17\x51\x41\x50\x14\x10\x04\ -\x44\x94\x8e\xd2\x25\x80\x80\x48\xef\x24\xb4\xbc\x39\x59\x6e\x4c\ -\x42\xe2\x0a\xea\x7b\xf3\x7d\xe1\xde\x7b\xca\xcc\x9c\x73\xe6\xcc\ -\xfc\x33\x90\xd9\x6c\x36\x86\xe8\xe8\xe9\x9b\xe9\xab\x97\x79\x9a\ -\x62\x5d\x44\x46\x7f\xd6\x6f\x3f\xc1\xb6\xb7\x1d\xf6\xf7\xc3\xc0\ -\xf0\xf3\x53\x5c\xc6\x2e\xe4\x74\x9c\xbe\x78\xff\xcd\xc1\x9d\x2b\ -\x08\xf8\x48\x34\x48\x55\x85\x9e\x4a\x5e\xe6\x3d\x7d\x04\xfa\x40\ -\x8d\xd3\xdd\xc6\x6d\x73\x1c\x6b\x55\xd8\xd9\xd1\xf9\x88\xc3\x0a\ -\x9f\x11\x19\x93\xc8\x4e\x4e\xcb\xce\x5e\xeb\x3b\x7b\x07\x19\xe3\ -\xa1\xd1\x23\x2d\x08\xd0\x91\xcc\x91\x81\x6b\x15\x15\x9b\xb4\xd6\ -\xd6\x7a\xf0\x61\xae\x56\x1b\x77\x9d\x62\xed\xdb\xe6\x2b\x11\xfc\ -\x22\xfa\x10\x5f\x47\x47\x47\x87\x38\xc6\x4f\x6c\xee\x3a\x0e\x9f\ -\xba\x91\xd1\xa5\x00\x6a\x24\x30\x99\x2c\x39\x32\xef\x1a\x10\xed\ -\x3e\x7c\x91\xb5\x65\xad\xb7\x38\x9f\x56\x57\x6f\x3f\x79\xda\xd9\ -\xc9\x76\xae\xaa\xae\xd5\xe7\x6a\x85\x2f\x12\x71\x40\xef\x82\x9c\ -\xba\xed\xe1\x9c\x19\x13\x27\x9b\x18\xf5\x7b\x84\x77\xf8\x07\x86\ -\x5f\x98\xea\x32\xd6\x07\xbd\x57\xd7\xd4\xe9\xef\x3f\x7e\x35\x47\ -\x4d\x85\xde\x40\x3e\x70\xe2\x6a\x1e\x6a\xec\xec\xec\xe4\x4c\x6e\ -\x6f\xef\x90\x44\x4f\x34\xf8\xaf\x1d\x27\xd8\x07\x76\xac\xc0\x52\ -\xd3\x73\x36\x4d\x18\x67\x1d\x6a\x37\x72\x88\x23\xf9\xaf\x15\xf3\ -\xf4\xf8\x44\x92\x49\x4c\xa4\xce\xd5\xdb\x81\x9c\xc1\x70\x58\x71\ -\x70\x2e\xfb\x22\x5e\xbf\xdb\xcb\x55\x49\x18\x95\x55\xd4\xb4\xc0\ -\x43\x0a\x06\x5b\x9d\xba\x70\xaf\xc8\xd7\xc7\x03\xdb\xb0\xd3\xaf\ -\x43\xe4\x84\x0d\x2b\xe6\x4a\x27\xa5\x64\xb2\x07\x0f\x32\xc2\x5c\ -\x27\xd8\x6a\xc2\xc9\xb4\xed\xdf\xbe\x5c\x9c\xbb\x4b\x88\x92\xd3\ -\xb2\x66\xdd\x79\x18\x7a\x53\xd4\x0e\x71\x55\x82\x3d\x26\x6d\xd8\ -\x79\xb2\x7d\xa9\xd7\x34\x9b\x95\x4b\x66\x9a\xff\xeb\xb6\xa2\xc1\ -\x5b\xd7\xf9\xa8\xca\x50\xa5\xcb\xf0\xf3\x10\x29\x01\x1f\x80\x06\ -\xa3\xe7\xd0\xc1\xc6\x97\xdf\x27\x7d\x5c\xc0\x3b\x68\xdf\xb1\xcb\ -\x0d\x34\x79\x39\xaa\x89\xa1\xee\xca\x6e\x8b\xd6\xd3\xd1\x88\xc0\ -\x27\x7c\x29\xab\x1c\x25\x2f\x27\x13\x45\xa5\x50\x72\x86\x0e\x32\ -\x32\xa0\x50\xa4\x8f\x77\x9b\x80\x16\xbd\x69\xf5\x02\xed\xd6\xd6\ -\x36\x8a\xaa\x8a\x62\x14\xd8\x6b\xd6\xf2\x85\x1e\x46\x99\xd9\x05\ -\xec\x80\xa7\xaf\x0a\xc9\x72\xb2\xd4\xe2\xba\xfa\x46\x0d\x5c\x35\ -\xdd\xbe\xea\x51\xc0\xb5\x28\xe8\xf9\x6b\xf6\xa4\xf1\xa3\x30\x65\ -\xba\xc2\x01\xd4\x2e\x21\x21\x6e\xeb\x3e\xd1\x4e\x86\xbc\x79\x8d\ -\x97\xa6\xb0\xc5\xa1\xc1\x88\x86\x98\x0f\xb8\xfa\x22\x22\x2e\xc8\ -\x61\x8c\xd5\xc4\x80\xa0\x08\x86\xd0\x83\xcb\xc9\x2b\x74\x34\xd0\ -\xd3\xe6\x7e\x9b\x9b\x19\x4d\x44\xcf\xf8\xc4\xf4\xbe\x42\x27\xc0\ -\xe0\x50\xde\x2b\x95\x5f\x50\x82\xd1\x15\xe4\x31\xb0\x2d\xa2\x48\ -\xd3\x68\x6a\x6e\xa1\x1f\x3e\x75\x93\xb1\x75\xad\x17\xcd\xd2\xc2\ -\xb8\xcd\xef\xfc\xbd\x42\x58\xbc\x68\xe3\xcb\x67\x14\x8f\xd9\xbe\ -\xde\x87\xfa\x24\x24\xaa\xcd\xd5\xc9\x16\xf3\x99\xe3\x66\xc6\xe7\ -\x07\x78\xe9\xdc\xd5\x87\x11\x9f\x18\xc5\x76\xf8\xb7\xa6\xba\xf2\ -\xfb\xe5\x0b\x67\x0c\xc3\x7a\x41\x7c\x1a\xf1\x9a\x11\x5a\x1f\x81\ -\x80\xb1\x13\x53\x32\xe7\xdc\x7b\xf4\xe2\x1a\xde\xb7\x78\xfe\xd4\ -\xd1\xe8\xa8\x7a\x24\x20\x2a\x36\x71\x5d\xf0\x8b\x98\x83\xe8\x5d\ -\x52\x52\xa2\x6e\xd7\xc6\xc5\xf2\xf8\x00\x8b\x41\x46\xd7\x91\x00\ -\xfc\x1b\x8d\xed\x91\x80\xbc\xfc\xcf\x63\x71\xe6\x88\x78\x99\xe3\ -\xb4\x68\xfe\x14\xbb\x73\x57\x1e\xbe\xfa\xc7\xf6\x07\x5c\xe6\xf3\ -\x92\x6c\x36\x71\xcb\x9e\x33\x6d\xfa\xba\x5a\xc4\xd9\x1e\xce\x18\ -\x89\x44\xe4\xf1\x7e\x81\x35\xe4\xf3\xd7\x02\xc2\x04\x27\x10\x08\ -\x84\x4e\x3e\x73\xf7\x7f\x7e\x0b\x3d\x89\x44\x62\xbb\x89\x91\x5e\ -\x00\x7a\x2f\x2a\x2e\x73\x3a\x7b\xc5\x3f\x78\xef\x56\x5f\x6c\xcf\ -\x96\x65\xd8\xf6\xfd\x67\xeb\xd3\x3e\xe6\x2c\x95\x92\x92\x6c\xaf\ -\xab\x6f\xba\x0b\x96\x81\xa9\xa9\x2a\x7d\x21\x7b\xcf\x76\x1b\x7f\ -\xf1\xc6\xe3\xe7\x5c\xcf\xb2\xd3\xaf\xed\xc0\x8e\x3f\x49\x82\xe7\ -\x62\xa8\xdf\x37\x64\x81\xa7\xab\x33\xde\x06\xde\x06\x43\xcc\xbb\ -\xbe\x91\x3b\x96\xe3\x84\xb2\x33\x37\xd9\x43\x06\x0d\xa8\xdd\xba\ -\xe7\x0c\x79\xc3\xca\x79\xa3\xc9\xc8\x88\x91\x33\x89\x8e\x4b\x5e\ -\x05\xf7\xeb\x28\x5a\x01\xce\x14\x1d\xf2\x8a\xc5\x33\x07\xab\xab\ -\xd2\x93\x79\x05\xc2\x78\x5e\xc3\xc0\x78\x9d\x51\x7f\x3d\xed\xa0\ -\xa7\xa1\xd1\x93\xf4\x74\x34\xb1\xf8\x84\x8f\xe5\x5c\x2b\xb2\xb1\ -\x32\x3f\x86\x7e\xdf\x3b\xb0\x82\xa2\x52\x6b\xfb\xd1\x96\x22\xfb\ -\x71\x37\xef\xec\x60\xc3\x15\x4e\xee\x89\x4d\xab\x2a\xd3\xd3\xc2\ -\xa3\xde\xf3\xb5\x59\x5a\x98\x60\x17\xae\x3d\xea\xd4\xd6\x54\x21\ -\xcc\xfd\x63\x12\x8f\x7f\xce\xae\x9a\x60\x3f\xe2\x48\x8f\x04\x48\ -\x48\x88\x35\x8c\xb2\x1e\x3c\xee\xef\x43\x17\x43\x60\x7f\xc5\xc4\ -\xc4\xc8\x10\xbd\x87\x74\x1c\xf2\xbb\xce\xae\xad\xab\xaf\xb6\xb6\ -\x1c\xa4\xd4\x15\xc4\xc0\xb1\x7c\x48\x5f\x3c\x7f\xca\x3e\x72\x4f\ -\x6f\xa6\xbe\xae\x66\x18\xc4\x22\x83\x0b\xd7\x1f\x31\x1c\xc7\x58\ -\x61\xe9\x19\xb9\x24\x63\xc3\x7e\xfe\x39\x9f\x0a\xa7\x52\x28\x52\ -\x1c\xe6\x7b\x8f\x5e\x29\xdd\xb2\xd6\xcb\xee\xbb\x81\xed\x7b\xd4\ -\x87\x26\x5b\x00\x91\xfd\x6b\x42\x4a\x46\x2b\xf8\x4f\xcd\xe6\x66\ -\xe6\x54\x3b\x1b\x8b\x57\x87\xfd\xae\xab\x8d\xb7\xb7\xde\x08\xcc\ -\xb9\x31\x5b\xa8\x2f\x42\x04\x93\x14\x82\x42\x5f\x1f\x49\xfb\x98\ -\x3b\x8d\x4c\x22\xb5\xee\xdc\xb8\x98\xf6\xd3\xbe\x88\x13\x92\xbf\ -\x56\x9a\x1e\x3d\x73\x2b\x8d\xb7\x0d\xc5\x47\xac\x97\xc4\x27\xe0\ -\xc1\x93\xb0\x4b\x78\x44\x43\xb6\x8f\xee\x00\x7a\x87\xd8\x1f\x07\ -\x37\x77\xb8\xcb\x04\xdb\x15\x23\x87\x0f\x3a\xd9\x2b\x01\x8f\x9e\ -\xbe\x3a\x83\x33\x47\x0e\xce\xc3\xdd\x61\xae\xa0\x87\x0d\x7c\x16\ -\x75\xa2\x57\x02\x8a\x4b\xcb\x2d\xe2\xde\xa7\x2d\x41\xef\x52\x92\ -\x12\xb5\x38\xf3\x5f\x41\x1c\x01\x27\xcf\xdd\x49\xc0\x1b\x04\xa3\ -\xae\x9b\xd3\xe8\xe5\x8f\x43\x22\xfd\x7a\x2d\x20\x3c\xea\xdd\x16\ -\xde\x06\x71\x71\xb1\x46\xde\xef\x11\x96\x66\xa7\x7e\x44\x40\xc8\ -\xcb\xd8\x07\x1f\x33\x3f\xb9\x56\x54\xd5\x88\x71\x6e\xbd\x0a\x3d\ -\x6f\xd5\x92\x99\xfa\xe4\xd0\x88\xb8\xbf\xf1\x41\x70\x33\x5b\x7a\ -\xaa\xe1\xeb\x37\x49\x67\x82\x43\xa3\x97\x78\x7a\x38\x61\x4e\xe3\ -\xac\xb9\xed\x8d\x8d\x2d\x7a\x27\xcf\xde\x49\xe0\xb3\x22\x8a\xb4\ -\x54\xc5\xf7\x98\xa9\x28\x2b\xa6\xf3\x7e\xef\x39\x72\x89\xa9\xa3\ -\xad\x2e\x71\x00\xbc\x6b\x65\x55\x6d\x33\x18\x84\xb4\x9a\x32\xbd\ -\x73\xe5\xd2\x99\x44\x2a\x55\x0a\x6b\x6d\x6f\x97\xe1\x13\xd0\xd0\ -\xd8\xa4\x2a\xc8\x14\x07\xc7\x88\x16\xcc\x72\x71\xc6\xdf\xc1\xc1\ -\x35\xb9\x39\xdb\x49\x18\x1b\xea\x62\xe5\x15\xd5\x0d\xc7\xfe\x73\ -\x5b\x02\xb9\xed\x93\xe7\xef\x72\xad\x8e\x2a\x2d\x55\xc6\x27\xa0\ -\xa3\xa3\x53\x4c\x50\x00\x4a\x17\xba\xb6\xaf\x19\x30\xd9\x67\xf4\ -\x7e\xe4\xf4\xcd\xba\x31\x36\x43\xa4\x11\x73\x8e\xb0\xeb\x01\xf5\ -\x90\x88\x68\x7c\xcb\x34\x30\xec\xfe\xe3\x97\x59\x66\x03\x0d\xee\ -\x74\xbb\xc9\x9f\x4b\xbe\x0e\x45\x30\xa5\x6b\x7f\x57\xc3\xd2\xf5\ -\x39\x29\xc7\xe6\x65\xd4\x2e\x58\xe6\x29\x21\x2e\x26\x6b\x3e\xd0\ -\x90\x33\x1e\x00\x41\xb6\xfb\xc4\x31\xeb\xd1\x7b\x62\x6a\xe6\x5e\ -\x2a\x45\x1a\xbb\x7c\x2b\x30\xbd\xae\xbe\xa1\x13\x32\xab\xb3\x1c\ -\x30\x08\x7b\xf9\x19\x17\xe0\x77\xfe\xee\x3b\x04\x6e\x11\xf2\x42\ -\x50\x1a\xb5\x21\x3f\x84\xa2\x1b\x7a\x87\xf0\x7a\x83\x37\xa2\x01\ -\xac\xe9\x0f\xf7\x26\x90\x83\x3c\xb5\xd5\xcf\x41\x40\xcf\x30\x32\ -\xd0\x09\x2a\xfd\x5a\x69\xd6\xd8\xd8\xac\xc4\x81\xa7\x28\xcf\x82\ -\xcc\x24\x1f\x60\xaa\xfa\x3f\x67\xd1\xac\xc2\xc1\x84\xfd\xb4\x5e\ -\x78\xcf\x71\x77\xc4\x99\x65\xe5\x16\x4c\x1b\x31\xcc\x8c\xcb\xbc\ -\xbc\xb2\x06\x1b\x64\xda\xff\x0e\xfe\x2d\x29\x21\x51\xff\x30\x28\ -\xe2\x9a\x96\x9a\x32\xb1\xbf\x7e\x5f\x2c\x3c\x22\x21\x9a\xb3\x45\ -\x24\xf0\x96\xb0\x12\x8d\x7f\x33\xc9\x98\xf8\x94\x63\xde\x9e\x6e\ -\xdc\xef\xac\x1c\x46\xae\xf5\x70\x33\xee\x1d\xb9\xf7\xf8\x65\xf5\ -\xce\x0d\x8b\x10\x3f\xce\x77\xf0\xcb\x18\x9b\x1e\xc5\x83\x9c\xdc\ -\x42\x75\xde\x6f\x70\x2b\xc5\xb5\xb5\x0d\x5a\xda\x1a\xaa\x71\x57\ -\x6e\x05\x66\x8f\xb2\xb6\xe0\x32\xaf\xad\x6b\xc0\x10\xc4\x21\xff\ -\x8c\x9f\xa9\x6f\x68\x1a\x06\xfb\xbd\xa6\xa9\xa9\x45\xad\xb0\xf8\ -\x8b\x41\xbf\xbe\xdf\xe4\x83\xf3\xcc\x07\xc4\xbf\xb2\x47\x02\xe8\ -\x8a\xb4\x6a\x14\xd0\xbe\x21\x91\xc1\x94\x4b\x37\x1f\x27\x12\xe1\ -\x64\xb7\xad\x5b\xc8\x37\x36\x33\x87\xa1\x3b\x5f\xce\xe5\x73\x8f\ -\x04\x8c\xb2\x1a\xbc\xf1\x55\x74\xc2\x39\x3b\x9b\x21\x5d\x7e\x8b\ -\x8c\x2d\x59\x30\x95\xc0\x62\xb5\xb2\xf7\x1f\xbf\x4c\xd8\xb4\xda\ -\xab\xeb\xc2\x36\x63\xda\x9a\xaa\x6f\x7a\x1c\x93\x2d\x87\x98\x9c\ -\xdf\xb4\xeb\x94\x9f\xa4\xb8\xb8\xb8\x95\xe5\x40\x4e\x5b\x58\x64\ -\x7c\xea\xcb\xc8\x78\x53\x49\x09\x71\x2e\xf8\xba\x1b\x10\x9a\xe6\ -\x33\xc7\xdd\xa1\x57\x41\x7f\x2f\x98\x34\x30\xdd\xb6\x69\xf7\xa9\ -\x8d\x04\x36\x81\x34\xc5\x65\xec\x11\x40\x19\x0c\xd8\x92\x68\x7c\ -\x0c\xe4\x16\x03\xc0\x2b\x37\xf5\x1a\x55\xa4\x7e\xcc\x5d\xa4\xad\ -\xa1\x26\xe9\x3d\xdb\x1d\xdb\xb8\xeb\xe4\x75\x45\x05\x5a\xdd\xea\ -\xa5\xb3\x38\x7d\x37\xef\x3f\x7b\xb7\x7a\x99\xe7\x5c\x91\x41\xff\ -\x47\x48\xa1\x8f\x9c\xfc\xbc\x2e\x14\xb7\x6f\xdb\x72\xec\xdc\xb5\ -\x00\x39\x48\xfc\x31\x46\x61\x49\x45\x51\xf1\x17\x55\x25\x45\x5a\ -\xd6\x4f\x09\xb0\x1e\x66\xe6\x76\xe6\xd2\xfd\x13\x0e\x76\x23\x34\ -\x5a\x98\x4c\x19\x27\xfb\x11\xf5\x00\x9e\x1f\x46\xc6\x24\x4c\x80\ -\xa4\x5b\x8b\x2f\xa2\x89\xc2\x45\xc2\xe8\x53\x41\xf1\xe8\x98\xb8\ -\x94\x15\x59\xb9\x0c\x67\x61\x9e\x17\x05\x2c\x2f\x4f\xb7\x09\x3d\ -\xc9\x80\x7e\x49\xcc\xff\x1e\xa1\xb4\xe1\x59\x58\xec\xbe\xf6\xf6\ -\x0e\x89\x6e\x01\x4e\x49\xe1\x83\xa9\xb1\xbe\x3f\x84\x85\x27\x6a\ -\x2a\xf4\x14\xd8\x0b\x42\x76\x5e\xc1\xf8\xd0\xf0\xb8\xdd\xda\x5a\ -\xaa\xb1\x90\xb3\x3c\xfb\xbf\x2c\x00\x45\x81\x3b\x0f\x9f\xdf\x4a\ -\xfd\x90\xe3\x21\xd8\x87\x20\xd9\x94\x49\x63\x17\x82\xcd\xb0\xf0\ -\xb6\xf8\x84\x0f\x0b\x01\xfb\xbd\x15\x5c\x24\x44\xf8\xca\x2d\x6b\ -\xbd\xd5\x20\x2d\x6c\xfb\x9f\x2d\x20\xe6\x6d\xca\x9f\x08\x1f\x76\ -\x4f\x0a\xb4\xc2\xbc\x20\x5b\x04\xaf\xd6\xc1\xdb\x8e\x83\x56\x11\ -\xc5\x08\x45\x74\x82\xa3\x47\x5a\x1c\xfc\xed\x0b\x40\x36\x7d\xfc\ -\xec\xed\xe4\xaf\xe5\x55\xc6\x82\x83\x20\xcf\xda\x64\x67\x33\x74\ -\x9f\x30\x06\x28\x05\x3e\x70\xf2\x5a\x2e\xc4\x03\xba\xb0\x7e\xb8\ -\xd5\xc3\x7f\xbb\x09\xb5\x30\x59\xf2\xfb\x8f\x5d\x61\xa0\x67\x37\ -\xef\x6c\x61\x72\x41\x94\xf2\x78\x59\x63\xd3\xaa\x05\xda\x00\x4a\ -\x8a\x9b\x5b\x98\x7d\xba\x8f\x20\xb0\x7b\xab\x5c\x45\x65\xad\x41\ -\x69\x59\xb9\x65\x6d\x5d\xa3\xa1\x18\x99\xc4\x6c\xef\xe8\x60\x2a\ -\xd1\x69\x19\xba\xda\x1a\x91\x5c\x47\x8d\xec\x1d\xd5\xb0\x85\x29\ -\x8f\x08\x52\xee\x9d\xff\x26\x08\x79\x9f\x49\xe3\x47\xad\x06\xf8\ -\x75\x55\xb0\x4f\x5d\x8d\x9e\xf4\x23\xca\x02\x3c\xe8\x17\x1b\x9f\ -\x72\x36\x3a\x2e\xc5\x1e\xe5\xa8\x70\x7f\x50\xb9\x11\xa3\xc9\xcb\ -\x00\x1a\x56\x40\xd0\x02\x83\x0d\x42\xe8\x17\x8b\x7e\x9b\xca\x66\ -\xb6\xb0\xb2\x2d\xcc\x0c\x4f\x93\x43\x5e\xc6\x1c\x44\x05\x41\x61\ -\x4c\x11\x1a\x83\x5f\xc9\x8f\x28\x40\xa3\xc9\x32\x84\xb5\x0f\x35\ -\x37\xbe\x22\x6a\x0e\x04\x6d\xcd\xdb\xfe\xcf\x62\x21\xf4\x69\xca\ -\xca\x50\xb0\x89\x8e\x36\x9c\xc0\x02\xf0\x11\x4c\xba\x03\x7b\x9f\ -\x94\x91\x99\x99\xcb\xc0\xc0\xbb\x19\x59\x0f\x1b\x84\x2a\x31\x70\ -\x19\x31\x0c\xf2\x37\x42\xf4\x9b\x64\x03\x94\xb0\x90\x01\xa8\xce\ -\x16\x25\x00\xee\x85\xf8\x8f\x1e\x37\xca\xa3\x05\xdb\x1c\xc6\x58\ -\x6d\x03\xc5\x4a\x85\x14\x53\xdc\x01\x18\xfb\xb7\xb6\xb6\x12\x27\ -\x3a\x8e\xc2\x16\xcd\x9b\xc2\xed\x2b\x2e\xfd\x5a\x7d\xf1\xfa\x63\ -\x12\x84\xdf\x4c\x4d\x75\xa5\xf7\x03\x8d\xf5\x13\x60\x21\x57\x5a\ -\x5b\x59\x44\x5e\x1e\xb2\xb2\x14\xe4\x05\xa5\xc8\x4c\x66\xab\x9c\ -\x28\xa5\x1a\x9b\x9a\x95\xe0\x68\xb5\x50\x89\xf6\x7b\xca\xa7\xa4\ -\xe7\xcc\x78\x13\x9f\xea\x2b\xe8\x6e\xd1\x7f\x51\x78\xdb\xaa\xaa\ -\x6b\x4d\x4f\x5d\xbc\x9f\x04\x32\xc9\x0b\x3c\x5d\x51\xb9\x83\xdb\ -\xd7\xda\xda\x86\x9d\xbd\xe2\xff\xa9\xe4\x4b\xb9\xee\x8c\xc9\x8e\ -\xb3\x21\x2b\xe0\x14\xd7\xb2\x73\x0b\xd6\x80\x1e\xc4\xc6\x26\x26\ -\x77\x2c\x93\xc9\x62\x07\x3e\x7b\xdd\x02\x9b\x53\x4b\x36\x33\xd1\ -\xbf\x9f\x94\x9a\xe5\x29\x4a\xb9\xa0\xe7\xd1\x47\x66\x7b\x38\x4d\ -\x13\xd5\x8f\x5c\x2e\x72\xbd\x02\xe5\xe5\xd5\x82\x25\x2d\xff\x27\ -\x61\x31\xef\x92\x3e\x5a\x83\x4b\x85\x14\x71\x24\x1f\x0f\xb0\xeb\ -\xd6\x23\xa7\x6f\x10\x15\x68\x72\x6d\x7b\xb6\xf8\x4a\xe1\x31\x06\ -\xcc\x48\x02\x92\x80\x7d\x80\x17\x61\x43\x0c\x51\x7c\xc2\x9e\x85\ -\xc5\x64\x15\x7d\x2e\xab\xea\x43\x93\x25\x2e\x9e\x3f\xd5\x96\xec\ -\xe1\xee\x38\xa7\xae\xae\x51\x03\xc1\x04\x61\x0a\xa6\x67\xe4\x4e\ -\xdd\x7d\xf8\x62\x29\xda\x15\x3d\x5d\xcd\x70\x3c\x2b\x81\xe8\xbc\ -\x37\x21\x39\x63\x3e\xef\x58\x0d\x35\xe5\x84\x85\x73\xdd\xed\x91\ -\x67\xe2\x75\xcf\xe0\x24\x60\xf3\xeb\x64\xd6\xf8\xce\xc6\x94\xe9\ -\x7d\x04\x61\x39\x1b\x92\xb8\x0f\x06\x7a\x7d\x8b\xe7\xcf\x9c\xe4\ -\xca\xdb\xf7\x34\x34\xe6\x04\x91\x4d\xac\x01\x07\xa3\x04\xe9\x54\ -\x2c\x89\x48\x68\xd7\xd1\xd6\x78\x0d\xf1\xe8\x20\x9e\xc4\x93\x51\ -\xbe\x85\x8a\xad\xd5\x35\xf5\x3a\x37\xee\x3d\x7d\x58\xf2\xa5\xc2\ -\x5c\x08\xf6\x57\x15\xac\xa9\x72\x9d\x24\x81\xd0\x39\x6a\xc4\xe0\ -\x23\xe3\xc7\x8e\xd8\x2c\x18\x71\x51\xd5\x12\x32\xd3\x2a\x29\x49\ -\x71\x99\xcd\x6b\xbc\x31\x2a\x45\x4a\x58\x6d\xbc\x02\xf8\xeb\x6f\ -\x5b\xe7\xd3\xad\x4c\xe4\xea\x64\xbb\x18\xfd\x5a\x5a\x58\x34\xb8\ -\xab\x9b\xe0\xee\x38\x47\x44\xbf\xdb\x12\x9f\x98\xbe\x59\x49\x51\ -\xa1\x91\x4a\x95\x7c\x41\xe6\xa9\x8c\x31\xf0\x92\x10\xda\xb5\xdc\ -\xfc\x22\x7b\xb0\xbf\x09\x5f\xca\x2a\xcd\xea\x1a\x1a\xd5\x11\xce\ -\x41\xee\x52\xb1\x8f\x5c\x9e\x96\x86\xea\x5b\x93\x01\xfd\x02\xe8\ -\x0a\xb4\x9c\xef\xdd\x8d\x07\x4f\xc2\xa2\x60\xeb\x65\x56\x2c\xfe\ -\x43\xa8\xf2\xef\x12\x33\xea\xc0\x5d\xe6\x3a\xd8\x59\xf9\xa1\xd4\ -\x5b\xa0\xc8\xa6\x88\xee\x04\x89\x48\x92\x6d\x61\xb1\x50\xa5\x15\ -\x9b\x3c\x69\x0c\xe6\x39\xdd\x89\xb3\x6f\xc9\x69\xd9\x84\xc4\xd4\ -\x2c\xaa\x50\x2c\x84\x76\x12\x01\xb1\x9f\x01\x63\xb0\xab\x6a\x60\ -\x62\x23\xcd\x4c\x0c\x30\x75\x55\x25\xa1\x63\x5a\x98\x2d\x79\xa8\ -\xbc\xeb\x60\x37\x7c\x3b\x6f\xfb\x97\xaf\x95\x56\xfe\x81\x61\x51\ -\x10\x43\xc4\xa6\xb9\x3a\x60\x00\x5f\xba\xcd\x85\xac\xa4\x05\xa0\ -\xca\x30\xf2\xef\x0a\xf1\xa0\x84\x29\x28\xde\x01\xf7\x86\x24\xba\ -\xb2\x2b\x4e\xfc\x67\x21\x4c\x6e\x09\x13\x4e\x7e\xd6\xbd\x80\x17\ -\x37\x54\x94\x15\x09\x1e\xdf\x92\x7e\x3e\x8a\x4f\xfc\x50\x51\x51\ -\x55\xa3\x04\xa0\x72\xd1\x6f\x5b\x00\x95\x22\x5d\x0e\x2e\x91\x54\ -\x59\x55\x23\x7a\x10\x9b\x68\x0e\xc9\x74\xc9\xe7\xe2\x72\x6b\x22\ -\x81\x44\x09\x7f\x1d\x7f\x1c\x2e\xa7\xfc\xe8\x91\x43\x21\x58\x99\ -\x09\x9d\x82\x3c\xd2\x93\x90\x48\x39\x0d\x35\xa5\x44\x94\x84\xfe\ -\xb6\x05\xa0\x32\x2b\x78\xa5\xcc\x37\xef\xd2\x8c\xc6\xd8\x58\x62\ -\x52\x52\xdd\x63\xe2\xf0\xa1\xc6\x90\x09\x1b\xab\x17\x14\x95\xac\ -\xac\xa9\x6b\xc0\xa6\xbb\x39\x40\x6e\x2f\x0f\x59\xef\x0b\x80\x16\ -\xf5\x9c\xc8\x2c\x48\xb7\x1e\x3c\xfb\x04\xee\x55\x17\xee\xc2\xf4\ -\x5e\xa7\x7c\x3f\x4a\x8b\xe6\x4d\xb6\x3c\xe4\x77\x3d\xef\xf2\xad\ -\xc7\x34\xf0\x26\x62\xb0\x20\x21\x5e\x0c\xc3\x74\xb4\xd5\x31\xc9\ -\xb2\xca\xfa\x9b\xf7\x83\xeb\xcb\x2b\x6b\x38\xb0\x66\xde\x4c\x97\ -\x6e\x63\x23\x5e\xbf\x67\x64\xe5\x16\xf4\x9b\xe6\x6a\xef\xd5\x87\ -\x26\x97\xff\xdb\x17\x00\x36\xde\x00\x09\x8d\x32\xa4\xfc\xce\x90\ -\xa5\x2d\xcf\xce\x2b\x74\xd4\xd3\xd5\xca\x07\x08\x41\x55\x52\xec\ -\x53\x0d\xb0\x41\x16\x14\x56\x42\xa5\x65\x9a\x1c\xb5\xc8\xd5\xc9\ -\x6e\xfd\xad\x07\x21\x21\x14\x8a\x94\xf8\x80\xfe\x3a\xfc\x99\xfe\ -\x87\xdc\xca\xe7\xe1\x6f\x74\x00\x07\x9d\x40\xff\x53\xfe\xa9\xa4\ -\xbb\xa7\x64\x64\xa0\x13\x8c\x7e\x2c\x56\x1b\x35\x22\xfa\xfd\xc5\ -\x9c\xbc\xc2\xc9\x48\x71\x0d\xf0\x4e\x54\xaa\x74\xbe\x32\x5d\x21\ -\x04\xa2\xf7\xca\x97\x91\x6f\xaf\x01\xe2\x14\xf7\x98\xec\x20\x58\ -\x06\xaa\x84\x85\x29\x9a\x0f\xec\x7f\xdb\x65\x82\xed\xca\x1e\xe5\ -\xc4\xbf\x8a\x62\xe3\x53\xd7\x31\x0a\x4b\xf6\xd3\x15\xe4\x89\xbe\ -\x3e\xd3\xb9\x55\xb6\xb6\xb6\x76\xdd\xab\xb7\x83\x7c\x37\xec\x3c\ -\xe9\xdb\x85\xa1\xd0\x82\xb9\xf3\x22\x63\x12\xf2\x42\x5e\xc6\xea\ -\x41\x1c\x38\x0e\x8b\x5c\xd5\xe3\xa4\xfe\x57\xd1\x40\x63\xbd\x1b\ -\x99\xd9\x0c\x97\xe2\xd2\x0a\xd3\x8c\x6c\x86\x9c\xe9\x00\x3d\x3c\ -\x97\xc0\x7c\xe6\xba\xe3\x48\x14\xc3\xef\xc9\xe7\x92\xaf\x99\x37\ -\xee\x05\xcb\x4b\x4b\x4b\x36\x2c\xf5\x9a\x36\xb2\xaf\x96\x5a\x6c\ -\xaf\xaa\x12\xbf\x8a\x64\xa8\x94\x32\xef\x39\x6e\x36\xa8\x26\xfd\ -\x28\xf8\xd5\x19\x00\x81\x86\x60\x52\xb2\x4c\x16\x4b\x16\xb0\x53\ -\x23\x20\x4c\x4e\x8d\x1a\xd5\xaf\xc5\xc4\xc4\x9a\xca\x2b\xaa\x07\ -\xfc\x31\xc5\x71\xa6\x7e\x3f\xad\xf0\xef\xf1\xfd\x2f\x9a\x21\xe5\ -\x2e\x6b\x8c\x93\x5e\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\ -\x82\ -" - -qt_resource_name = b"\ -\x00\x06\ -\x07\x03\x7d\xc3\ -\x00\x69\ -\x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\ -\x00\x03\ -\x00\x00\x70\x37\ -\x00\x69\ -\x00\x6d\x00\x67\ -\x00\x07\ -\x05\xd4\x57\xa7\ -\x00\x6f\ -\x00\x67\x00\x61\x00\x2e\x00\x70\x00\x6e\x00\x67\ -" - -qt_resource_struct = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ -\x00\x00\x00\x12\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\ -\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -" - - -def qInitResources(): - QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - - -def qCleanupResources(): - QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - - -qInitResources() diff --git a/src/VERSION b/src/VERSION index 3e153f5..f0bb29e 100644 --- a/src/VERSION +++ b/src/VERSION @@ -1 +1 @@ -1.1.1b +1.3.0 diff --git a/src/about-dialog.ui b/src/about-dialog.ui index ab0cac1..df40da5 100644 --- a/src/about-dialog.ui +++ b/src/about-dialog.ui @@ -65,7 +65,7 @@ - Version 1.1.0 + Version 1.3.0 diff --git a/src/cfg/ogagent.cfg b/src/cfg/ogagent.cfg index 3fa38ab..0291e88 100644 --- a/src/cfg/ogagent.cfg +++ b/src/cfg/ogagent.cfg @@ -11,7 +11,10 @@ remote=https://192.168.2.10/opengnsys/rest # Alternate OpenGnsys Service (comment out to enable this option) #altremote=https://10.0.2.2/opengnsys/rest -# Log Level, if ommited, will be set to INFO +# Execution level (permitted operations): status, halt, full +level=full + +# Log Level, if omitted, will be set to INFO log=DEBUG # Module specific diff --git a/src/opengnsys/RESTApi.py b/src/opengnsys/RESTApi.py index d785dfa..8773cf5 100644 --- a/src/opengnsys/RESTApi.py +++ b/src/opengnsys/RESTApi.py @@ -32,7 +32,7 @@ # pylint: disable-msg=E1101,W0703 -from __future__ import unicode_literals + import requests import logging diff --git a/src/opengnsys/__init__.py b/src/opengnsys/__init__.py index 0197f32..e268ca7 100644 --- a/src/opengnsys/__init__.py +++ b/src/opengnsys/__init__.py @@ -29,19 +29,15 @@ """ @author: Adolfo Gómez, dkmaster at dkmon dot com """ -from __future__ import unicode_literals + # On centos, old six release does not includes byte2int, nor six.PY2 import six -import modules -from RESTApi import REST, RESTError +from . import modules +from .RESTApi import REST, RESTError -try: - with open('../VERSION', 'r') as v: - VERSION = v.read().strip() -except IOError: - VERSION = '1.1.1b' +VERSION='1.3.0' __title__ = 'OpenGnsys Agent' __version__ = VERSION diff --git a/src/opengnsys/config.py b/src/opengnsys/config.py index d1f3ede..7ffe046 100644 --- a/src/opengnsys/config.py +++ b/src/opengnsys/config.py @@ -26,24 +26,25 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -''' +""" @author: Adolfo Gómez, dkmaster at dkmon dot com -''' +""" # pylint: disable=unused-wildcard-import, wildcard-import -from __future__ import unicode_literals -from ConfigParser import SafeConfigParser + +from configparser import SafeConfigParser config = None + def readConfig(client=False): - ''' + """ Reads configuration file If client is False, will read ogagent.cfg as configuration If client is True, will read ogclient.cfg as configuration This is this way so we can protect ogagent.cfg against reading for non admin users on all platforms. - ''' + """ cfg = SafeConfigParser() if client is True: fname = 'ogclient.cfg' @@ -55,4 +56,3 @@ def readConfig(client=False): return None return cfg - \ No newline at end of file diff --git a/src/opengnsys/httpserver.py b/src/opengnsys/httpserver.py index a06ae6b..69c4317 100644 --- a/src/opengnsys/httpserver.py +++ b/src/opengnsys/httpserver.py @@ -25,27 +25,24 @@ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -''' +""" @author: Adolfo Gómez, dkmaster at dkmon dot com -''' -# pylint: disable=unused-wildcard-import,wildcard-import -from __future__ import unicode_literals, print_function +""" -# Pydev can't parse "six.moves.xxxx" because it is loaded lazy -import six + +import json +import ssl +import threading from six.moves.socketserver import ThreadingMixIn # @UnresolvedImport from six.moves.BaseHTTPServer import BaseHTTPRequestHandler # @UnresolvedImport from six.moves.BaseHTTPServer import HTTPServer # @UnresolvedImport from six.moves.urllib.parse import unquote # @UnresolvedImport -import json -import threading -import ssl - from .utils import exceptionToMessage from .certs import createSelfSignedCert from .log import logger + class HTTPServerHandler(BaseHTTPRequestHandler): service = None protocol_version = 'HTTP/1.0' @@ -56,22 +53,22 @@ class HTTPServerHandler(BaseHTTPRequestHandler): self.send_response(code) self.send_header('Content-type', 'application/json') self.end_headers() - self.wfile.write(json.dumps({'error': message})) + self.wfile.write(str.encode(json.dumps({'error': message}))) return def sendJsonResponse(self, data): self.send_response(200) data = json.dumps(data) self.send_header('Content-type', 'application/json') - self.send_header('Content-Length', len(data)) + self.send_header('Content-Length', str(len(data))) self.end_headers() # Send the html message - self.wfile.write(data) - - - # parseURL + self.wfile.write(str.encode(data)) + def parseUrl(self): - # Very simple path & params splitter + """ + Very simple path & params splitter + """ path = self.path.split('?')[0][1:].split('/') try: @@ -81,16 +78,16 @@ class HTTPServerHandler(BaseHTTPRequestHandler): for v in self.service.modules: if v.name == path[0]: # Case Sensitive!!!! - return (v, path[1:], params) + return v, path[1:], params - return (None, path, params) + return None, path, params - def notifyMessage(self, module, path, getParams, postParams): - ''' + def notifyMessage(self, module, path, get_params, post_params): + """ Locates witch module will process the message based on path (first folder on url path) - ''' + """ try: - data = module.processServerMessage(path, getParams, postParams, self) + data = module.processServerMessage(path, get_params, post_params, self) self.sendJsonResponse(data) except Exception as e: logger.exception() @@ -102,19 +99,19 @@ class HTTPServerHandler(BaseHTTPRequestHandler): self.notifyMessage(module, path, params, None) def do_POST(self): - module, path, getParams = self.parseUrl() + module, path, get_params = self.parseUrl() + post_params = None # Tries to get JSON content (UTF-8 encoded) try: - length = int(self.headers.getheader('content-length')) + length = int(self.headers.get('content-length')) content = self.rfile.read(length).decode('utf-8') - logger.debug('length: {}, content >>{}<<'.format(length, content)) - postParams = json.loads(content) + logger.debug('length: {0}, content >>{1}<<'.format(length, content)) + post_params = json.loads(content) except Exception as e: self.sendJsonError(500, exceptionToMessage(e)) - self.notifyMessage(module, path, getParams, postParams) - + self.notifyMessage(module, path, get_params, post_params) def log_error(self, fmt, *args): logger.error('HTTP ' + fmt % args) @@ -126,6 +123,7 @@ class HTTPServerHandler(BaseHTTPRequestHandler): class HTTPThreadingServer(ThreadingMixIn, HTTPServer): pass + class HTTPServerThread(threading.Thread): def __init__(self, address, service): super(self.__class__, self).__init__() @@ -146,5 +144,3 @@ class HTTPServerThread(threading.Thread): def run(self): self.server.serve_forever() - - diff --git a/src/opengnsys/ipc.py b/src/opengnsys/ipc.py index a92473e..fbfee9c 100644 --- a/src/opengnsys/ipc.py +++ b/src/opengnsys/ipc.py @@ -25,16 +25,16 @@ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -''' +""" @author: Adolfo Gómez, dkmaster at dkmon dot com -''' -from __future__ import unicode_literals +""" + +import json +import queue import socket import threading -import six import traceback -import json from opengnsys.utils import toUnicode from opengnsys.log import logger @@ -59,7 +59,7 @@ from opengnsys.log import logger # BYTE # 0 1-2 3 4 ... # MSG_ID DATA_LENGTH (little endian) Data (can be 0 length) -# With a previos "MAGIC" header in fron of each message +# With a previous "MAGIC" header in front of each message # Client messages MSG_LOGOFF = 0xA1 # Request log off from an user @@ -84,7 +84,7 @@ REV_DICT = { REQ_MESSAGE: 'REQ_MESSAGE' } -MAGIC = b'\x4F\x47\x41\x00' # OGA in hexa with a padded 0 to the right +MAGIC = b'\x4F\x47\x41\x00' # OGA in hex with a padded 0 to the right # States for client processor @@ -99,10 +99,10 @@ class ClientProcessor(threading.Thread): self.parent = parent self.clientSocket = clientSocket self.running = False - self.messages = six.moves.queue.Queue(32) # @UndefinedVariable + self.messages = queue.Queue(32) def stop(self): - logger.debug('Stoping client processor') + logger.debug('Stopping client processor') self.running = False def processRequest(self, msg, data): @@ -117,6 +117,7 @@ class ClientProcessor(threading.Thread): state = None recv_msg = None recv_data = None + msg_len = 0 while self.running: try: counter = 1024 @@ -127,7 +128,7 @@ class ClientProcessor(threading.Thread): # Client disconnected self.running = False break - buf = six.byte2int(b) # Empty buffer, this is set as non-blocking + buf = int.from_bytes(b, 'big') # Empty buffer, this is set as non-blocking if state is None: if buf in (REQ_MESSAGE, REQ_LOGIN, REQ_LOGOUT): logger.debug('State set to {}'.format(buf)) @@ -152,7 +153,7 @@ class ClientProcessor(threading.Thread): recv_data = b'' continue elif state == ST_RECEIVING: - recv_data += six.int2byte(buf) + recv_data += bytes([buf]) msg_len -= 1 if msg_len == 0: self.processRequest(recv_msg, recv_data) @@ -173,7 +174,7 @@ class ClientProcessor(threading.Thread): try: msg = self.messages.get(block=True, timeout=1) - except six.moves.queue.Empty: # No message got in time @UndefinedVariable + except queue.Empty: # No message got in time @UndefinedVariable continue logger.debug('Got message {}={}'.format(msg, REV_DICT.get(msg[0]))) @@ -181,7 +182,7 @@ class ClientProcessor(threading.Thread): try: m = msg[1] if msg[1] is not None else b'' l = len(m) - data = MAGIC + six.int2byte(msg[0]) + six.int2byte(l & 0xFF) + six.int2byte(l >> 8) + m + data = MAGIC + bytes([msg[0]]) + bytes([l & 0xFF]) + bytes([l >> 8]) + m try: self.clientSocket.sendall(data) except socket.error as e: @@ -220,20 +221,20 @@ class ServerIPC(threading.Thread): for t in self.threads: t.join() - def sendMessage(self, msgId, msgData): - ''' + def sendMessage(self, msg_id, msg_data): + """ Notify message to all listening threads - ''' - logger.debug('Sending message {}({}),{} to all clients'.format(msgId, REV_DICT.get(msgId), msgData)) + """ + logger.debug('Sending message {}({}),{} to all clients'.format(msg_id, REV_DICT.get(msg_id), msg_data)) # Convert to bytes so length is correctly calculated - if isinstance(msgData, six.text_type): - msgData = msgData.encode('utf8') + if isinstance(msg_data, str): + msg_data = str.encode(msg_data) for t in self.threads: - if t.isAlive(): + if t.is_alive(): logger.debug('Sending to {}'.format(t)) - t.messages.put((msgId, msgData)) + t.messages.put((msg_id, msg_data)) def sendLoggofMessage(self): self.sendMessage(MSG_LOGOFF, '') @@ -242,18 +243,18 @@ class ServerIPC(threading.Thread): self.sendMessage(MSG_MESSAGE, message) def sendPopupMessage(self, title, message): - self.sendMessage(MSG_POPUP, {'title':title, 'message':message}) + self.sendMessage(MSG_POPUP, {'title': title, 'message': message}) def sendScriptMessage(self, script): self.sendMessage(MSG_SCRIPT, script) def cleanupFinishedThreads(self): - ''' + """ Cleans up current threads list - ''' + """ aliveThreads = [] for t in self.threads: - if t.isAlive(): + if t.is_alive(): logger.debug('Thread {} is alive'.format(t)) aliveThreads.append(t) self.threads[:] = aliveThreads @@ -262,7 +263,7 @@ class ServerIPC(threading.Thread): self.running = True self.serverSocket.bind(('localhost', self.port)) - self.serverSocket.setblocking(1) + self.serverSocket.setblocking(True) self.serverSocket.listen(4) while True: @@ -289,7 +290,7 @@ class ClientIPC(threading.Thread): self.port = listenPort self.running = False self.clientSocket = None - self.messages = six.moves.queue.Queue(32) # @UndefinedVariable + self.messages = queue.Queue(32) # @UndefinedVariable self.connect() @@ -300,7 +301,7 @@ class ClientIPC(threading.Thread): while self.running: try: return self.messages.get(timeout=1) - except six.moves.queue.Empty: # @UndefinedVariable + except queue.Empty: continue return None @@ -310,11 +311,11 @@ class ClientIPC(threading.Thread): if data is None: data = b'' - if isinstance(data, six.text_type): # Convert to bytes if necessary - data = data.encode('utf-8') + if isinstance(data, str): + data = str.encode(data) l = len(data) - msg = six.int2byte(msg) + six.int2byte(l & 0xFF) + six.int2byte(l >> 8) + data + msg = bytes([msg]) + bytes([l & 0xFF]) + bytes([l >> 8]) + data self.clientSocket.sendall(msg) def sendLogin(self, user_data): @@ -324,20 +325,20 @@ class ClientIPC(threading.Thread): self.sendRequestMessage(REQ_LOGOUT, username) def sendMessage(self, module, message, data=None): - ''' + """ Sends a message "message" with data (data will be encoded as json, so ensure that it is serializable) @param module: Module that will receive this message @param message: Message to send. This message is "customized", and understand by modules @param data: Data to be send as message companion - ''' + """ msg = '\0'.join((module, message, json.dumps(data))) self.sendRequestMessage(REQ_MESSAGE, msg) def messageReceived(self): - ''' + """ Override this method to automatically get notified on new message received. Message is at self.messages queue - ''' + """ pass def receiveBytes(self, number): @@ -420,4 +421,3 @@ class ClientIPC(threading.Thread): self.clientSocket.close() except Exception: pass # If can't close, nothing happens, just end thread - diff --git a/src/opengnsys/linux/OGAgentService.py b/src/opengnsys/linux/OGAgentService.py index 29a238d..76bfbb8 100644 --- a/src/opengnsys/linux/OGAgentService.py +++ b/src/opengnsys/linux/OGAgentService.py @@ -26,10 +26,10 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -''' +""" @author: Adolfo Gómez, dkmaster at dkmon dot com -''' -from __future__ import unicode_literals +""" + from opengnsys.service import CommonService from opengnsys.service import IPC_PORT @@ -45,7 +45,7 @@ import json try: from prctl import set_proctitle # @UnresolvedImport -except Exception: # Platform may not include prctl, so in case it's not available, we let the "name" as is +except ImportError: # Platform may not include prctl, so in case it's not available, we let the "name" as is def set_proctitle(_): pass @@ -63,7 +63,6 @@ class OGAgentSvc(Daemon, CommonService): # Call modules initialization # They are called in sequence, no threading is done at this point, so ensure modules onActivate always returns - # ********************* # * Main Service loop * @@ -93,6 +92,7 @@ def usage(): sys.stderr.write("usage: {} start|stop|restart|fg|login 'username'|logout 'username'|message 'module' 'message' 'json'\n".format(sys.argv[0])) sys.exit(2) + if __name__ == '__main__': logger.setLevel('INFO') @@ -105,7 +105,6 @@ if __name__ == '__main__': sys.exit(0) except Exception as e: logger.error(e) - if len(sys.argv) == 3 and sys.argv[1] in ('login', 'logout'): logger.debug('Running client opengnsys') diff --git a/src/opengnsys/linux/__init__.py b/src/opengnsys/linux/__init__.py index 3a98c78..97ac9be 100644 --- a/src/opengnsys/linux/__init__.py +++ b/src/opengnsys/linux/__init__.py @@ -26,7 +26,6 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -''' +""" @author: Adolfo Gómez, dkmaster at dkmon dot com -''' -from __future__ import unicode_literals +""" diff --git a/src/opengnsys/linux/daemon.py b/src/opengnsys/linux/daemon.py index 3753808..a2e8960 100644 --- a/src/opengnsys/linux/daemon.py +++ b/src/opengnsys/linux/daemon.py @@ -25,18 +25,16 @@ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -''' +""" @author: : http://www.jejik.com/authors/sander_marechal/ @see: : http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/ -''' +""" -from __future__ import unicode_literals -import sys -import os -import time import atexit +import os +import sys +import time from opengnsys.log import logger - from signal import SIGTERM @@ -89,7 +87,7 @@ class Daemon: sys.stderr.flush() si = open(self.stdin, 'r') so = open(self.stdout, 'a+') - se = open(self.stderr, 'a+', 0) + se = open(self.stderr, 'a+') os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) diff --git a/src/opengnsys/linux/log.py b/src/opengnsys/linux/log.py index dc54e19..396a2db 100644 --- a/src/opengnsys/linux/log.py +++ b/src/opengnsys/linux/log.py @@ -26,18 +26,16 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -''' +""" @author: Adolfo Gómez, dkmaster at dkmon dot com -''' -from __future__ import unicode_literals +""" import logging import os import tempfile -import six -# Valid logging levels, from UDS Broker (uds.core.utils.log) -OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in six.moves.xrange(6)) # @UndefinedVariable +# Logging levels +OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in range(6)) class LocalLogger(object): @@ -46,7 +44,7 @@ class LocalLogger(object): # service wil get c:\windows\temp, while user will get c:\users\XXX\temp # Try to open logger at /var/log path # If it fails (access denied normally), will try to open one at user's home folder, and if - # agaim it fails, open it at the tmpPath + # again it fails, open it at the tmpPath for logDir in ('/var/log', os.path.expanduser('~'), tempfile.gettempdir()): try: diff --git a/src/opengnsys/linux/operations.py b/src/opengnsys/linux/operations.py index a3e119f..621bc4d 100644 --- a/src/opengnsys/linux/operations.py +++ b/src/opengnsys/linux/operations.py @@ -29,7 +29,7 @@ ''' @author: Adolfo Gómez, dkmaster at dkmon dot com ''' -from __future__ import unicode_literals + import socket import platform @@ -104,7 +104,7 @@ def _getInterfaces(): 0x8912, # SIOCGIFCONF struct.pack(str('iL'), space, names.buffer_info()[0]) ))[0] - namestr = names.tostring() + namestr = names.tobytes() # return namestr, outbytes return [namestr[i:i + offset].split(b'\0', 1)[0].decode('utf-8') for i in range(0, outbytes, length)] diff --git a/src/opengnsys/linux/renamer/__init__.py b/src/opengnsys/linux/renamer/__init__.py index 27198dc..7b3bc69 100644 --- a/src/opengnsys/linux/renamer/__init__.py +++ b/src/opengnsys/linux/renamer/__init__.py @@ -29,7 +29,7 @@ ''' @author: Adolfo Gómez, dkmaster at dkmon dot com ''' -from __future__ import unicode_literals + import platform import os diff --git a/src/opengnsys/linux/renamer/debian.py b/src/opengnsys/linux/renamer/debian.py index be55e00..2e6e2a1 100644 --- a/src/opengnsys/linux/renamer/debian.py +++ b/src/opengnsys/linux/renamer/debian.py @@ -29,7 +29,7 @@ ''' @author: Adolfo Gómez, dkmaster at dkmon dot com ''' -from __future__ import unicode_literals + from opengnsys.linux.renamer import renamers from opengnsys.log import logger diff --git a/src/opengnsys/linux/renamer/opensuse.py b/src/opengnsys/linux/renamer/opensuse.py index a2d29a5..3f05e4e 100644 --- a/src/opengnsys/linux/renamer/opensuse.py +++ b/src/opengnsys/linux/renamer/opensuse.py @@ -28,7 +28,7 @@ ''' @author: Adolfo Gómez, dkmaster at dkmon dot com ''' -from __future__ import unicode_literals + from opengnsys.linux.renamer import renamers from opengnsys.log import logger diff --git a/src/opengnsys/linux/renamer/redhat.py b/src/opengnsys/linux/renamer/redhat.py index 8821a81..2605c22 100644 --- a/src/opengnsys/linux/renamer/redhat.py +++ b/src/opengnsys/linux/renamer/redhat.py @@ -28,7 +28,7 @@ ''' @author: Adolfo Gómez, dkmaster at dkmon dot com ''' -from __future__ import unicode_literals + from opengnsys.linux.renamer import renamers from opengnsys.log import logger diff --git a/src/opengnsys/loader.py b/src/opengnsys/loader.py index 23988fc..ca053b6 100644 --- a/src/opengnsys/loader.py +++ b/src/opengnsys/loader.py @@ -33,7 +33,7 @@ # This is a simple module loader, so we can add "external opengnsys" modules as addons # Modules under "opengsnsys/modules" are always autoloaded -from __future__ import unicode_literals + import pkgutil import os.path diff --git a/src/opengnsys/log.py b/src/opengnsys/log.py index e34c087..5c74783 100644 --- a/src/opengnsys/log.py +++ b/src/opengnsys/log.py @@ -26,22 +26,21 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -''' +""" @author: Adolfo Gómez, dkmaster at dkmon dot com -''' -from __future__ import unicode_literals +""" + -import traceback import sys -import six +import traceback if sys.platform == 'win32': - from opengnsys.windows.log import LocalLogger # @UnusedImport + from opengnsys.windows.log import LocalLogger else: - from opengnsys.linux.log import LocalLogger # @Reimport + from opengnsys.linux.log import LocalLogger # Valid logging levels, from UDS Broker (uds.core.utils.log) -OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in six.moves.xrange(6)) # @UndefinedVariable +OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in range(6)) _levelName = { 'OTHER': OTHER, @@ -52,17 +51,18 @@ _levelName = { 'FATAL': FATAL } + class Logger(object): def __init__(self): self.logLevel = INFO self.logger = LocalLogger() def setLevel(self, level): - ''' + """ Sets log level filter (minimum level required for a log message to be processed) :param level: Any message with a level below this will be filtered out - ''' - if isinstance(level, six.string_types): + """ + if isinstance(level, str): level = _levelName.get(level, INFO) self.logLevel = level # Ensures level is an integer or fails diff --git a/src/opengnsys/macos/__init__.py b/src/opengnsys/macos/__init__.py index ee5ba4c..988217f 100644 --- a/src/opengnsys/macos/__init__.py +++ b/src/opengnsys/macos/__init__.py @@ -29,4 +29,4 @@ ''' @author: Ramón M. Gómez, ramongomez at us dot es ''' -from __future__ import unicode_literals + diff --git a/src/opengnsys/macos/operations.py b/src/opengnsys/macos/operations.py index 286b6d8..15b6b11 100644 --- a/src/opengnsys/macos/operations.py +++ b/src/opengnsys/macos/operations.py @@ -29,7 +29,7 @@ ''' @author: Adolfo Gómez, dkmaster at dkmon dot com ''' -from __future__ import unicode_literals + import socket import platform diff --git a/src/opengnsys/modules/client/OpenGnSys/__init__.py b/src/opengnsys/modules/client/OpenGnSys/__init__.py index adeb0a6..51a5137 100644 --- a/src/opengnsys/modules/client/OpenGnSys/__init__.py +++ b/src/opengnsys/modules/client/OpenGnSys/__init__.py @@ -28,7 +28,7 @@ """ @author: Ramón M. Gómez, ramongomez at us dot es """ -from __future__ import unicode_literals + from opengnsys.workers import ClientWorker diff --git a/src/opengnsys/modules/server/OpenGnSys/__init__.py b/src/opengnsys/modules/server/OpenGnSys/__init__.py index 838ffda..73182b6 100644 --- a/src/opengnsys/modules/server/OpenGnSys/__init__.py +++ b/src/opengnsys/modules/server/OpenGnSys/__init__.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (c) 2014 Virtual Cable S.L. @@ -28,22 +29,24 @@ """ @author: Ramón M. Gómez, ramongomez at us dot es """ -from __future__ import unicode_literals -import threading + +import base64 import os -import platform -import time import random import shutil import string -import urllib +import threading +import time +import urllib.error +import urllib.parse +import urllib.request -from opengnsys.workers import ServerWorker -from opengnsys import REST, RESTError -from opengnsys import operations +from configparser import NoOptionError +from opengnsys import REST, operations, VERSION from opengnsys.log import logger from opengnsys.scriptThread import ScriptExecutorThread +from opengnsys.workers import ServerWorker # Check authorization header decorator @@ -53,9 +56,12 @@ def check_secret(fnc): """ def wrapper(*args, **kwargs): try: - this, path, get_params, post_params, server = args # @UnusedVariable - if this.random == server.headers['Authorization']: - fnc(*args, **kwargs) + this, path, get_params, post_params, server = args + # Accept "status" operation with no arguments or any function with Authorization header + if fnc.__name__ == 'process_status' and not get_params: + return fnc(*args, **kwargs) + elif this.random == server.headers['Authorization']: + return fnc(*args, **kwargs) else: raise Exception('Unauthorized operation') except Exception as e: @@ -65,6 +71,26 @@ def check_secret(fnc): return wrapper +# Check if operation is permitted +def execution_level(level): + def check_permitted(fnc): + def wrapper(*args, **kwargs): + levels = ['status', 'halt', 'full'] + this = args[0] + try: + if levels.index(level) <= levels.index(this.exec_level): + return fnc(*args, **kwargs) + else: + raise Exception('Unauthorized operation') + except Exception as e: + logger.error(e) + raise Exception(e) + + return wrapper + + return check_permitted + + # Error handler decorator. def catch_background_error(fnc): def wrapper(*args, **kwargs): @@ -77,31 +103,43 @@ def catch_background_error(fnc): class OpenGnSysWorker(ServerWorker): - name = 'opengnsys' + name = 'opengnsys' # Module name interface = None # Bound interface for OpenGnsys REST = None # REST object - logged_in = False # User session flag + user = [] # User sessions session_type = '' # User session type - locked = {} random = None # Random string for secure connections length = 32 # Random string length + exec_level = None # Execution level (permitted operations) def onActivation(self): """ Sends OGAgent activation notification to OpenGnsys server """ - t = 0 + e = None # Error info + t = 0 # Count of time # Generate random secret to send on activation self.random = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(self.length)) # Ensure cfg has required configuration variables or an exception will be thrown - url = self.service.config.get('opengnsys', 'remote') + try: + url = self.service.config.get('opengnsys', 'remote') + except NoOptionError as e: + logger.error("Configuration error: {}".format(e)) + raise e self.REST = REST(url) + # Execution level ('full' by default) + try: + self.exec_level = self.service.config.get('opengnsys', 'level') + except NoOptionError: + self.exec_level = 'full' # Get network interfaces until they are active or timeout (5 minutes) for t in range(0, 300): try: - self.interface = list(operations.getNetworkInfo())[0] # Get first network interface + # Get the first network interface + self.interface = list(operations.getNetworkInfo())[0] except Exception as e: # Wait 1 sec. and retry + logger.warn (e) time.sleep(1) finally: # Exit loop if interface is active @@ -112,29 +150,36 @@ class OpenGnSysWorker(ServerWorker): # Raise error after timeout if not self.interface: raise e + # Loop to send initialization message - for t in range(0, 100): + init_retries = 100 + for t in range(0, init_retries): try: try: self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip, 'secret': self.random, 'ostype': operations.os_type, - 'osversion': operations.os_version}) + 'osversion': operations.os_version, + 'agent_version': VERSION}) break - except: + except Exception as e: + logger.warn (str (e)) # Trying to initialize on alternative server, if defined # (used in "exam mode" from the University of Seville) self.REST = REST(self.service.config.get('opengnsys', 'altremote')) self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip, 'secret': self.random, 'ostype': operations.os_type, - 'osversion': operations.os_version, 'alt_url': True}) + 'osversion': operations.os_version, 'alt_url': True, + 'agent_version': VERSION}) break - except: + except Exception as e: + logger.warn (str (e)) time.sleep(3) # Raise error after timeout - if 0 < t < 100: + if t < init_retries-1: logger.debug('Successful connection after {} tries'.format(t)) - elif t == 100: + elif t == init_retries-1: raise Exception('Initialization error: Cannot connect to remote server') + # Delete marking files for f in ['ogboot.me', 'ogboot.firstboot', 'ogboot.secondboot']: try: @@ -165,7 +210,7 @@ class OpenGnSysWorker(ServerWorker): """ user, language, self.session_type = tuple(data.split(',')) logger.debug('Received login for {0} using {2} with language {1}'.format(user, language, self.session_type)) - self.logged_in = True + self.user.append(user) self.REST.sendMessage('ogagent/loggedin', {'ip': self.interface.ip, 'user': user, 'language': language, 'session': self.session_type, 'ostype': operations.os_type, 'osversion': operations.os_version}) @@ -175,7 +220,10 @@ class OpenGnSysWorker(ServerWorker): Sends session logout notification to OpenGnsys server """ logger.debug('Received logout for {}'.format(user)) - self.logged_in = False + try: + self.user.pop() + except IndexError: + pass self.REST.sendMessage('ogagent/loggedout', {'ip': self.interface.ip, 'user': user}) def process_ogclient(self, path, get_params, post_params, server): @@ -206,34 +254,34 @@ class OpenGnSysWorker(ServerWorker): raise Exception('Message processor for "{}" not found'.format(path[0])) return operation(path[1:], get_params, post_params) + # Warning: the order of the decorators matters + @execution_level('status') + @check_secret def process_status(self, path, get_params, post_params, server): """ Returns client status (OS type or execution status) and login status :param path: - :param get_params: + :param get_params: optional parameter "detail" to show extended status :param post_params: :param server: - :return: JSON object {"status": "status_code", "loggedin": boolean} + :return: JSON object {"status": "status_code", "loggedin": boolean, ...} """ - res = {'status': '', 'loggedin': self.logged_in, 'session': self.session_type} - if platform.system() == 'Linux': # GNU/Linux - # Check if it's OpenGnsys Client. - if os.path.exists('/scripts/oginit'): - # Check if OpenGnsys Client is busy. - if self.locked: - res['status'] = 'BSY' - else: - res['status'] = 'OPG' - else: - # Check if there is an active session. - res['status'] = 'LNX' - elif platform.system() == 'Windows': # Windows - # Check if there is an active session. - res['status'] = 'WIN' - elif platform.system() == 'Darwin': # Mac OS X ?? - res['status'] = 'OSX' + st = {'linux': 'LNX', 'macos': 'OSX', 'windows': 'WIN'} + try: + # Standard status + res = {'status': st[operations.os_type.lower()], 'loggedin': len(self.user) > 0, + 'session': self.session_type} + # Detailed status + if get_params.get('detail', 'false') == 'true': + res.update({'agent_version': VERSION, 'os_version': operations.os_version, 'sys_load': os.getloadavg()}) + if res['loggedin']: + res.update({'sessions': len(self.user), 'current_user': self.user[-1]}) + except KeyError: + # Unknown operating system + res = {'status': 'UNK'} return res + @execution_level('halt') @check_secret def process_reboot(self, path, get_params, post_params, server): """ @@ -252,6 +300,7 @@ class OpenGnSysWorker(ServerWorker): threading.Thread(target=rebt).start() return {'op': 'launched'} + @execution_level('halt') @check_secret def process_poweroff(self, path, get_params, post_params, server): """ @@ -271,6 +320,7 @@ class OpenGnSysWorker(ServerWorker): threading.Thread(target=pwoff).start() return {'op': 'launched'} + @execution_level('full') @check_secret def process_script(self, path, get_params, post_params, server): """ @@ -283,7 +333,7 @@ class OpenGnSysWorker(ServerWorker): """ logger.debug('Processing script request') # Decoding script (Windows scripts need a subprocess call per line) - script = urllib.unquote(post_params.get('script').decode('base64')).decode('utf8') + script = urllib.parse.unquote(base64.b64decode(post_params.get('script')).decode('utf-8')) if operations.os_type == 'Windows': script = 'import subprocess; {0}'.format( ';'.join(['subprocess.check_output({0},shell=True)'.format(repr(c)) for c in script.split('\n')])) @@ -297,6 +347,7 @@ class OpenGnSysWorker(ServerWorker): self.sendClientMessage('script', {'code': script}) return {'op': 'launched'} + @execution_level('full') @check_secret def process_logoff(self, path, get_params, post_params, server): """ @@ -307,6 +358,7 @@ class OpenGnSysWorker(ServerWorker): self.sendClientMessage('logoff', {}) return {'op': 'sent to client'} + @execution_level('full') @check_secret def process_popup(self, path, get_params, post_params, server): """ diff --git a/src/opengnsys/operations.py b/src/opengnsys/operations.py index be777dd..1ec9872 100644 --- a/src/opengnsys/operations.py +++ b/src/opengnsys/operations.py @@ -31,7 +31,7 @@ """ # pylint: disable=unused-wildcard-import,wildcard-import -from __future__ import unicode_literals + import sys # Importing platform operations and getting operating system data. diff --git a/src/opengnsys/service.py b/src/opengnsys/service.py index 4a44028..e7da465 100644 --- a/src/opengnsys/service.py +++ b/src/opengnsys/service.py @@ -26,23 +26,21 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -''' +""" @author: Adolfo Gómez, dkmaster at dkmon dot com -''' -from __future__ import unicode_literals +""" -from .log import logger -from .config import readConfig -from .utils import exceptionToMessage +import json +import socket +import time from . import ipc from . import httpserver +from .config import readConfig from .loader import loadModules +from .log import logger +from .utils import exceptionToMessage -import socket -import time -import json -import six IPC_PORT = 10398 @@ -58,21 +56,17 @@ class CommonService(object): logger.info('Initializing OpenGnsys Agent') # Read configuration file before proceding & ensures minimal config is there - self.config = readConfig() - - # Get opengnsys section as dict + # Get opengnsys section as dict cfg = dict(self.config.items('opengnsys')) # Set up log level logger.setLevel(cfg.get('log', 'INFO')) - logger.debug('Loaded configuration from opengnsys.cfg:') for section in self.config.sections(): logger.debug('Section {} = {}'.format(section, self.config.items(section))) - - + if logger.logger.isWindows(): # Logs will also go to windows event log for services logger.logger.serviceLogger = True @@ -90,15 +84,16 @@ class CommonService(object): logger.debug('Modules: {}'.format(list(v.name for v in self.modules))) def stop(self): - ''' + """ Requests service termination - ''' + """ self.isAlive = False # ******************************** # * Internal messages processors * # ******************************** - def notifyLogin(self, username): + def notifyLogin(self, data): + username = data.decode('utf-8') for v in self.modules: try: logger.debug('Notifying login of user {} to module {}'.format(username, v.name)) @@ -106,7 +101,8 @@ class CommonService(object): except Exception as e: logger.error('Got exception {} processing login message on {}'.format(e, v.name)) - def notifyLogout(self, username): + def notifyLogout(self, data): + username = data.decode('utf-8') for v in self.modules: try: logger.debug('Notifying logout of user {} to module {}'.format(username, v.name)) @@ -115,7 +111,7 @@ class CommonService(object): logger.error('Got exception {} processing logout message on {}'.format(e, v.name)) def notifyMessage(self, data): - module, message, data = data.split('\0') + module, message, data = data.decode('utf-8').split('\0') for v in self.modules: if v.name == module: # Case Sensitive!!!! try: @@ -126,13 +122,12 @@ class CommonService(object): logger.error('Got exception {} processing generic message on {}'.format(e, v.name)) logger.error('Module {} not found, messsage {} not sent'.format(module, message)) - def clientMessageProcessor(self, msg, data): - ''' + """ Callback, invoked from IPC, on its own thread (not the main thread). This thread will "block" communication with agent untill finished, but this should be no problem - ''' + """ logger.debug('Got message {}'.format(msg)) if msg == ipc.REQ_LOGIN: @@ -143,14 +138,9 @@ class CommonService(object): self.notifyMessage(data) def initialize(self): - # ****************************************** - # * Initialize listeners, modules, etc... - # ****************************************** - - if six.PY3 is False: - import threading - threading._DummyThread._Thread__stop = lambda x: 42 - + """ + Initialize listeners, modules, etc... + """ logger.debug('Starting IPC listener at {}'.format(IPC_PORT)) self.ipc = ipc.ServerIPC(self.ipcport, clientMessageProcessor=self.clientMessageProcessor) self.ipc.start() @@ -203,47 +193,47 @@ class CommonService(object): # Methods that CAN BE overridden by agents # **************************************** def doWait(self, miliseconds): - ''' + """ Invoked to wait a bit CAN be OVERRIDDEN - ''' + """ time.sleep(float(miliseconds) / 1000) def notifyStop(self): - ''' + """ Overridden to log stop - ''' + """ logger.info('Service is being stopped') # *************************************************** # * Helpers, convenient methods to facilitate comms * # *************************************************** def sendClientMessage(self, toModule, message, data): - ''' + """ Sends a message to the clients using IPC The data is converted to json, so ensure that it is serializable. All IPC is asynchronous, so if you expect a response, this will be sent by client using another message - + @param toModule: Module that will receive this message @param message: Message to send - @param data: data to send - ''' + @param data: data to send + """ self.ipc.sendMessageMessage('\0'.join((toModule, message, json.dumps(data)))) def sendScriptMessage(self, script): - ''' + """ Sends an script to be executed by client - ''' + """ self.ipc.sendScriptMessage(script) def sendLogoffMessage(self): - ''' + """ Sends a logoff message to client - ''' + """ self.ipc.sendLoggofMessage() def sendPopupMessage(self, title, message): - ''' - Sends a poup box to be displayed by client - ''' + """ + Sends a popup box to be displayed by client + """ self.ipc.sendPopupMessage(title, message) diff --git a/src/opengnsys/utils.py b/src/opengnsys/utils.py index 9480a6a..36bef63 100644 --- a/src/opengnsys/utils.py +++ b/src/opengnsys/utils.py @@ -29,7 +29,7 @@ ''' @author: Adolfo Gómez, dkmaster at dkmon dot com ''' -from __future__ import unicode_literals + import sys import six diff --git a/src/opengnsys/workers/client_worker.py b/src/opengnsys/workers/client_worker.py index 6a08380..3e81685 100644 --- a/src/opengnsys/workers/client_worker.py +++ b/src/opengnsys/workers/client_worker.py @@ -25,25 +25,25 @@ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -''' +""" @author: Adolfo Gómez, dkmaster at dkmon dot com -''' +""" # pylint: disable=unused-wildcard-import,wildcard-import -from __future__ import unicode_literals + class ClientWorker(object): - ''' + """ A ServerWorker is a server module that "works" for service - Most method are invoked inside their own thread, except onActivation & onDeactivation. + Most method are invoked inside their own thread, except onActivation & onDeactivation. This two methods are invoked inside main service thread, take that into account when creating them - + * You must provide a module name (override name on your class), so we can identify the module by a "valid" name. A valid name is like a valid python variable (do not use spaces, etc...) * The name of the module is used as REST message destination id: https://sampleserver:8888/[name]/.... Remember that module names and REST path are case sensitive!!! - - ''' + + """ name = None service = None @@ -51,15 +51,15 @@ class ClientWorker(object): self.service = service def activate(self): - ''' + """ Convenient method to wrap onActivation, so we can include easyly custom common logic for activation in a future - ''' + """ self.onActivation() def deactivate(self): - ''' + """ Convenient method to wrap onActivation, so we can include easyly custom common logic for deactivation in a future - ''' + """ self.onDeactivation() def processMessage(self, message, params): @@ -83,32 +83,31 @@ class ClientWorker(object): return operation(params) def onActivation(self): - ''' + """ Invoked by Service for activation. This MUST be overridden by modules! This method is invoked inside main thread, so if it "hangs", complete service will hang This should be no problem, but be advised about this - ''' + """ pass def onDeactivation(self): - ''' + """ Invoked by Service before unloading service This MUST be overridden by modules! This method is invoked inside main thread, so if it "hangs", complete service will hang This should be no problem, but be advised about this - ''' + """ pass # ************************************* # * Helper, convenient helper methods * # ************************************* def sendServerMessage(self, message, data): - ''' + """ Sends a message to connected ipc clients By convenience, it uses the "current" moduel name as destination module name also. If you need to send a message to a different module, you can use self.service.sendClientMessage(module, message, data) instead og this helmer - ''' + """ self.service.ipc.sendMessage(self.name, message, data) - \ No newline at end of file diff --git a/src/opengnsys/workers/server_worker.py b/src/opengnsys/workers/server_worker.py index 141d657..b002655 100644 --- a/src/opengnsys/workers/server_worker.py +++ b/src/opengnsys/workers/server_worker.py @@ -25,25 +25,25 @@ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -''' +""" @author: Adolfo Gómez, dkmaster at dkmon dot com -''' +""" # pylint: disable=unused-wildcard-import,wildcard-import -from __future__ import unicode_literals + class ServerWorker(object): - ''' + """ A ServerWorker is a server module that "works" for service - Most method are invoked inside their own thread, except onActivation & onDeactivation. + Most method are invoked inside their own thread, except onActivation & onDeactivation. This two methods are invoked inside main service thread, take that into account when creating them - + * You must provide a module name (override name on your class), so we can identify the module by a "valid" name. A valid name is like a valid python variable (do not use spaces, etc...) * The name of the module is used as REST message destination id: https://sampleserver:8888/[name]/.... Remember that module names and REST path are case sensitive!!! - - ''' + + """ name = None service = None locked = False @@ -52,43 +52,43 @@ class ServerWorker(object): self.service = service def activate(self): - ''' + """ Convenient method to wrap onActivation, so we can include easyly custom common logic for activation in a future - ''' + """ self.onActivation() def deactivate(self): - ''' + """ Convenient method to wrap onActivation, so we can include easyly custom common logic for deactivation in a future - ''' + """ self.onDeactivation() def process(self, getParams, postParams, server): - ''' + """ This method is invoked on a message received with an empty path (that means a message with only the module name, like in "http://example.com/Sample" Override it if you expect messages with that pattern - + Overriden method must return data that can be serialized to json (i.e. Ojects are not serializable to json, basic type are) - ''' + """ raise NotImplementedError('Generic message processor is not supported') def processServerMessage(self, path, getParams, postParams, server): - ''' + """ This method can be overriden to provide your own message proccessor, or better you can implement a method that is called exactly as "process_" + path[0] (module name has been removed from path array) and this default processMessage will invoke it * Example: Imagine this invocation url (no matter if GET or POST): http://example.com:9999/Sample/mazinger/Z The HTTP Server will remove "Sample" from path, parse arguments and invoke this method as this: module.processMessage(["mazinger","Z"], getParams, postParams) - + This method will process "mazinguer", and look for a "self" method that is called "process_mazinger", and invoke it this way: return self.process_mazinger(["Z"], getParams, postParams) - + In the case path is empty (that is, the path is composed only by the module name, like in "http://example.com/Sample", the "process" method will be invoked directly - + The methods must return data that can be serialized to json (i.e. Ojects are not serializable to json, basic type are) - ''' + """ if self.locked is True: raise Exception('system is busy') @@ -103,20 +103,20 @@ class ServerWorker(object): def processClientMessage(self, message, data): - ''' + """ Invoked by Service when a client message is received (A message from user space Agent) - + This method can be overriden to provide your own message proccessor, or better you can implement a method that is called exactly "process_client_" + message (module name has been removed from path) and this default processMessage will invoke it * Example: We got a message from OGAgent "Mazinger", with json params module.processClientMessage("mazinger", jsonParams) - + This method will process "mazinguer", and look for a "self" method that is called "process_client_mazinger", and invoke it this way: self.process_client_mazinger(jsonParams) - + The methods returns nothing (client communications are done asynchronously) - ''' + """ try: operation = getattr(self, 'process_client_' + message) except Exception: @@ -128,52 +128,52 @@ class ServerWorker(object): def onActivation(self): - ''' + """ Invoked by Service for activation. This MUST be overridden by modules! This method is invoked inside main thread, so if it "hangs", complete service will hang This should be no problem, but be advised about this - ''' + """ pass def onDeactivation(self): - ''' + """ Invoked by Service before unloading service This MUST be overridden by modules! This method is invoked inside main thread, so if it "hangs", complete service will hang This should be no problem, but be advised about this - ''' + """ pass def onLogin(self, user): - ''' + """ Invoked by Service when an user login is detected This CAN be overridden by modules This method is invoked whenever the client (user space agent) notifies the server (Service) that a user has logged in. This method is run on its own thread - ''' + """ pass def onLogout(self, user): - ''' + """ Invoked by Service when an user login is detected This CAN be overridden by modules This method is invoked whenever the client (user space agent) notifies the server (Service) that a user has logged in. This method is run on its own thread - ''' + """ pass # ************************************* # * Helper, convenient helper methods * # ************************************* def sendClientMessage(self, message, data): - ''' + """ Sends a message to connected ipc clients By convenience, it uses the "current" moduel name as destination module name also. If you need to send a message to a different module, you can use self.service.sendClientMessage(module, message, data) instead og this helmer - ''' + """ self.service.sendClientMessage(self.name, message, data) def sendScriptMessage(self, script): diff --git a/src/prototypes/threaded_server.py b/src/prototypes/threaded_server.py index ed5583b..b8d7751 100644 --- a/src/prototypes/threaded_server.py +++ b/src/prototypes/threaded_server.py @@ -3,7 +3,7 @@ Created on Jul 9, 2015 @author: dkmaster ''' -from __future__ import unicode_literals, print_function + # Pydev can't parse "six.moves.xxxx" because it is loaded lazy from six.moves.socketserver import ThreadingMixIn # @UnresolvedImport diff --git a/src/requirements.txt b/src/requirements.txt new file mode 100644 index 0000000..abedc7c --- /dev/null +++ b/src/requirements.txt @@ -0,0 +1,3 @@ +netifaces +requests +urllib3 diff --git a/src/setup.py b/src/setup.py index 7f5c04f..a3b7f43 100644 --- a/src/setup.py +++ b/src/setup.py @@ -60,12 +60,8 @@ from distutils.core import setup import py2exe import sys -# Reading version file: -try: - with open('VERSION', 'r') as v: - VERSION = v.read().rstrip() -except IOError: - VERSION = '1.1.0' +# update.sh changes this value +VERSION='1.3.0' sys.argv.append('py2exe') @@ -99,7 +95,7 @@ class Target: # thus don't need to run in a console. -udsservice = Target( +ogaservice = Target( description='OpenGnsys Agent Service', modules=['opengnsys.windows.OGAgentService'], icon_resources=[(0, 'img\\oga.ico'), (1, 'img\\oga.ico')], @@ -112,7 +108,7 @@ HIDDEN_BY_SIX = ['SocketServer', 'SimpleHTTPServer', 'urllib'] setup( windows=[ { - 'script': 'OGAgentUser.py', + 'script': 'OGAgentUser-qt4.py', 'icon_resources': [(0, 'img\\oga.ico'), (1, 'img\\oga.ico')] }, ], @@ -121,7 +117,7 @@ setup( 'script': 'OGAServiceHelper.py' } ], - service=[udsservice], + service=[ogaservice], data_files=[('', [get_requests_cert_file()]), ('cfg', ['cfg/ogagent.cfg', 'cfg/ogclient.cfg'])], options={ 'py2exe': { diff --git a/src/test_modules/client/Sample1/__init__.py b/src/test_modules/client/Sample1/__init__.py index db174f0..a6dcf49 100644 --- a/src/test_modules/client/Sample1/__init__.py +++ b/src/test_modules/client/Sample1/__init__.py @@ -29,7 +29,7 @@ ''' @author: Adolfo Gómez, dkmaster at dkmon dot com ''' -from __future__ import unicode_literals + from opengnsys.workers import ClientWorker diff --git a/src/test_modules/server/Sample1/__init__.py b/src/test_modules/server/Sample1/__init__.py index 189957e..0359b8a 100644 --- a/src/test_modules/server/Sample1/__init__.py +++ b/src/test_modules/server/Sample1/__init__.py @@ -1,2 +1,2 @@ # Module must be imported on package, so we can initialize and load it -from sample1 import Sample1 \ No newline at end of file +from .sample1 import Sample1 \ No newline at end of file diff --git a/src/test_modules/server/Sample1/sample1.py b/src/test_modules/server/Sample1/sample1.py index 61e405e..08cb90f 100644 --- a/src/test_modules/server/Sample1/sample1.py +++ b/src/test_modules/server/Sample1/sample1.py @@ -29,7 +29,7 @@ ''' @author: Adolfo Gómez, dkmaster at dkmon dot com ''' -from __future__ import unicode_literals + from opengnsys.workers import ServerWorker diff --git a/src/test_modules/server/Sample1/sample_pkg/__init__.py b/src/test_modules/server/Sample1/sample_pkg/__init__.py index 1936572..571a11c 100644 --- a/src/test_modules/server/Sample1/sample_pkg/__init__.py +++ b/src/test_modules/server/Sample1/sample_pkg/__init__.py @@ -29,7 +29,7 @@ ''' @author: Adolfo Gómez, dkmaster at dkmon dot com ''' -from __future__ import unicode_literals + def test(): diff --git a/src/test_rest_server.py b/src/test_rest_server.py index ad1aede..f1bdd37 100644 --- a/src/test_rest_server.py +++ b/src/test_rest_server.py @@ -30,7 +30,7 @@ @author: Adolfo Gómez, dkmaster at dkmon dot com ''' # pylint: disable=unused-wildcard-import,wildcard-import -from __future__ import unicode_literals, print_function + # Pydev can't parse "six.moves.xxxx" because it is loaded lazy from six.moves.socketserver import ThreadingMixIn # @UnresolvedImport diff --git a/src/update.sh b/src/update.sh index e6e02ab..3f39fed 100755 --- a/src/update.sh +++ b/src/update.sh @@ -1,6 +1,7 @@ #!/bin/bash # # Copyright (c) 2014 Virtual Cable S.L. +# Copyright (c) 2024 Qindel Formación y Servicios S.L. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -27,16 +28,32 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -function process { - pyuic4 about-dialog.ui -o about_dialog_ui.py -x - pyuic4 message-dialog.ui -o message_dialog_ui.py -x -} +function update_version { + if [[ -r VERSION ]]; then + V="$(cat VERSION)" + sed -i "s/Version [^<]*/Version $V/" about-dialog.ui + sed -i "s/^VERSION='.*'$/VERSION='$V'/" setup.py + sed -i "s/^VERSION='.*'$/VERSION='$V'/" opengnsys/__init__.py + else + echo 'src/VERSION: No such file or directory' + exit 1 + fi +} + +function process_ui { + pyuic6 about-dialog.ui -o about_dialog_ui.py -x + pyuic6 message-dialog.ui -o message_dialog_ui.py -x +} + +function process_resources { + ## requires a virtualenv with pyside6 + ## you can create it by doing 'mkvirtualenv -p python3 ogpyside6' + ## this will obviously go away in the future, but we need to merge the py3/qt6 change now + ~/.virtualenvs/ogpyside6/bin/pyside6-rcc -o OGAgent_rc.py OGAgent.qrc + sed -i -e '/^from PySide6 import QtCore/s/PySide6/PyQt6/' OGAgent_rc.py +} cd $(dirname "$0") -[ -r VERSION ] && sed -i "s/Version [^<]*/Version $(cat VERSION)/" about-dialog.ui -pyrcc4 -py3 OGAgent.qrc -o OGAgent_rc.py - - -# process current directory ui's -process - +update_version +process_ui +process_resources diff --git a/windows/build.bat b/windows/build.bat index 2c44474..0e0d514 100644 --- a/windows/build.bat +++ b/windows/build.bat @@ -2,5 +2,6 @@ C: CD \ogagent\src python setup.py CD .. +RENAME bin\OGAgentUser-qt4.exe OGAgentUser.exe "C:\Program Files\NSIS\makensis.exe" ogagent.nsi