From 11f7a07988d510e03e22ccda50240ea3514e6183 Mon Sep 17 00:00:00 2001 From: ramon Date: Thu, 7 Apr 2016 11:27:41 +0000 Subject: [PATCH] =?UTF-8?q?#718:=20Integrar=20c=C3=B3digo=20fuente=20de=20?= =?UTF-8?q?agente=20OGAgent=20en=20rama=20de=20desarrollo.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git-svn-id: https://opengnsys.es/svn/branches/version1.1@4865 a21b9725-9963-47de-94b9-378ad31fedc9 --- linux/Makefile | 72 +++ linux/build-packages.sh | 34 ++ linux/debian/changelog | 5 + linux/debian/compat | 1 + linux/debian/control | 15 + linux/debian/copyright | 26 ++ linux/debian/docs | 1 + linux/debian/ogagent.init | 23 + linux/debian/ogagent.links | 2 + linux/debian/ogagent.postinst | 21 + linux/debian/ogagent.postinst.debhelper | 5 + linux/debian/ogagent.postrm | 10 + linux/debian/ogagent.postrm.debhelper | 12 + linux/debian/ogagent.substvars | 2 + linux/debian/rules | 44 ++ linux/debian/source/format | 1 + linux/desktop/OGAgentTool.desktop | 12 + linux/ogagent-template.spec | 65 +++ .../org.openuds.pkexec.UDSActorConfig.policy | 20 + linux/readme.txt | 3 + linux/scripts/OGAgentTool | 6 + linux/scripts/OGAgentTool-startup | 10 + linux/scripts/ogagent | 6 + notas.txt | 11 + requires.txt | 3 + src/OGAServiceHelper.py | 57 +++ src/OGAgent.manifest | 17 + src/OGAgent.qrc | 5 + src/OGAgentUser.py | 338 ++++++++++++++ src/OGAgent_rc.py | 289 ++++++++++++ src/about-dialog.ui | 240 ++++++++++ src/about_dialog_ui.py | 163 +++++++ src/cfg/ogagent.cfg | 21 + src/cfg/ogclient.cfg | 11 + src/img/oga-48x48.ico | Bin 0 -> 9662 bytes src/img/oga-512.png | Bin 0 -> 44217 bytes src/img/oga.ico | Bin 0 -> 9662 bytes src/img/oga.png | Bin 0 -> 3906 bytes src/license.txt | 27 ++ src/message-dialog.ui | 89 ++++ src/message_dialog_ui.py | 69 +++ src/opengnsys/RESTApi.py | 160 +++++++ src/opengnsys/__init__.py | 57 +++ src/opengnsys/certs.py | 101 +++++ src/opengnsys/config.py | 59 +++ src/opengnsys/httpserver.py | 150 +++++++ src/opengnsys/ipc.py | 416 ++++++++++++++++++ src/opengnsys/linux/OGAgentService.py | 147 +++++++ src/opengnsys/linux/__init__.py | 32 ++ src/opengnsys/linux/daemon.py | 182 ++++++++ src/opengnsys/linux/log.py | 80 ++++ src/opengnsys/linux/operations.py | 271 ++++++++++++ src/opengnsys/linux/renamer/__init__.py | 61 +++ src/opengnsys/linux/renamer/debian.py | 68 +++ src/opengnsys/linux/renamer/opensuse.py | 66 +++ src/opengnsys/linux/renamer/redhat.py | 74 ++++ src/opengnsys/loader.py | 111 +++++ src/opengnsys/log.py | 103 +++++ src/opengnsys/modules/__init__.py | 0 .../modules/client/OpenGnSys/__init__.py | 55 +++ src/opengnsys/modules/client/__init__.py | 0 .../modules/server/OpenGnSys/__init__.py | 198 +++++++++ src/opengnsys/modules/server/__init__.py | 0 src/opengnsys/operations.py | 40 ++ src/opengnsys/scriptThread.py | 51 +++ src/opengnsys/service.py | 243 ++++++++++ src/opengnsys/utils.py | 72 +++ src/opengnsys/windows/OGAgentService.py | 124 ++++++ src/opengnsys/windows/__init__.py | 39 ++ src/opengnsys/windows/log.py | 77 ++++ src/opengnsys/windows/operations.py | 231 ++++++++++ src/opengnsys/workers/__init__.py | 2 + src/opengnsys/workers/client_worker.py | 114 +++++ src/opengnsys/workers/server_worker.py | 183 ++++++++ src/prototypes/threaded_server.py | 170 +++++++ src/setup.py | 132 ++++++ src/test_modules/__init__.py | 0 src/test_modules/client/Sample1/__init__.py | 38 ++ src/test_modules/client/__init__.py | 0 src/test_modules/server/Sample1/__init__.py | 2 + src/test_modules/server/Sample1/sample1.py | 40 ++ .../server/Sample1/sample_pkg/__init__.py | 36 ++ src/test_modules/server/__init__.py | 0 src/test_rest_server.py | 210 +++++++++ src/update.sh | 40 ++ windows/build-windows.sh | 4 + windows/build.bat | 6 + windows/ogagent.nsi | 184 ++++++++ windows/py2exe-wine-linux.sh | 72 +++ 89 files changed, 6237 insertions(+) create mode 100644 linux/Makefile create mode 100755 linux/build-packages.sh create mode 100644 linux/debian/changelog create mode 100644 linux/debian/compat create mode 100644 linux/debian/control create mode 100644 linux/debian/copyright create mode 100644 linux/debian/docs create mode 100644 linux/debian/ogagent.init create mode 100644 linux/debian/ogagent.links create mode 100644 linux/debian/ogagent.postinst create mode 100644 linux/debian/ogagent.postinst.debhelper create mode 100644 linux/debian/ogagent.postrm create mode 100644 linux/debian/ogagent.postrm.debhelper create mode 100644 linux/debian/ogagent.substvars create mode 100755 linux/debian/rules create mode 100644 linux/debian/source/format create mode 100644 linux/desktop/OGAgentTool.desktop create mode 100644 linux/ogagent-template.spec create mode 100644 linux/policy/org.openuds.pkexec.UDSActorConfig.policy create mode 100644 linux/readme.txt create mode 100644 linux/scripts/OGAgentTool create mode 100644 linux/scripts/OGAgentTool-startup create mode 100644 linux/scripts/ogagent create mode 100644 notas.txt create mode 100644 requires.txt create mode 100644 src/OGAServiceHelper.py create mode 100644 src/OGAgent.manifest create mode 100644 src/OGAgent.qrc create mode 100644 src/OGAgentUser.py create mode 100644 src/OGAgent_rc.py create mode 100644 src/about-dialog.ui create mode 100644 src/about_dialog_ui.py create mode 100644 src/cfg/ogagent.cfg create mode 100644 src/cfg/ogclient.cfg create mode 100644 src/img/oga-48x48.ico create mode 100644 src/img/oga-512.png create mode 100644 src/img/oga.ico create mode 100644 src/img/oga.png create mode 100644 src/license.txt create mode 100644 src/message-dialog.ui create mode 100644 src/message_dialog_ui.py create mode 100644 src/opengnsys/RESTApi.py create mode 100644 src/opengnsys/__init__.py create mode 100644 src/opengnsys/certs.py create mode 100644 src/opengnsys/config.py create mode 100644 src/opengnsys/httpserver.py create mode 100644 src/opengnsys/ipc.py create mode 100644 src/opengnsys/linux/OGAgentService.py create mode 100644 src/opengnsys/linux/__init__.py create mode 100644 src/opengnsys/linux/daemon.py create mode 100644 src/opengnsys/linux/log.py create mode 100644 src/opengnsys/linux/operations.py create mode 100644 src/opengnsys/linux/renamer/__init__.py create mode 100644 src/opengnsys/linux/renamer/debian.py create mode 100644 src/opengnsys/linux/renamer/opensuse.py create mode 100644 src/opengnsys/linux/renamer/redhat.py create mode 100644 src/opengnsys/loader.py create mode 100644 src/opengnsys/log.py create mode 100644 src/opengnsys/modules/__init__.py create mode 100644 src/opengnsys/modules/client/OpenGnSys/__init__.py create mode 100644 src/opengnsys/modules/client/__init__.py create mode 100644 src/opengnsys/modules/server/OpenGnSys/__init__.py create mode 100644 src/opengnsys/modules/server/__init__.py create mode 100644 src/opengnsys/operations.py create mode 100644 src/opengnsys/scriptThread.py create mode 100644 src/opengnsys/service.py create mode 100644 src/opengnsys/utils.py create mode 100644 src/opengnsys/windows/OGAgentService.py create mode 100644 src/opengnsys/windows/__init__.py create mode 100644 src/opengnsys/windows/log.py create mode 100644 src/opengnsys/windows/operations.py create mode 100644 src/opengnsys/workers/__init__.py create mode 100644 src/opengnsys/workers/client_worker.py create mode 100644 src/opengnsys/workers/server_worker.py create mode 100644 src/prototypes/threaded_server.py create mode 100644 src/setup.py create mode 100644 src/test_modules/__init__.py create mode 100644 src/test_modules/client/Sample1/__init__.py create mode 100644 src/test_modules/client/__init__.py create mode 100644 src/test_modules/server/Sample1/__init__.py create mode 100644 src/test_modules/server/Sample1/sample1.py create mode 100644 src/test_modules/server/Sample1/sample_pkg/__init__.py create mode 100644 src/test_modules/server/__init__.py create mode 100644 src/test_rest_server.py create mode 100755 src/update.sh create mode 100755 windows/build-windows.sh create mode 100644 windows/build.bat create mode 100644 windows/ogagent.nsi create mode 100755 windows/py2exe-wine-linux.sh diff --git a/linux/Makefile b/linux/Makefile new file mode 100644 index 0000000..6ada91f --- /dev/null +++ b/linux/Makefile @@ -0,0 +1,72 @@ +#!/usr/bin/make -f +# -*- makefile -*- + +# Directories +SOURCEDIR := ../src +LIBDIR := $(DESTDIR)/usr/share/OGAgent +BINDIR := $(DESTDIR)/usr/bin +SBINDIR = $(DESTDIR)/usr/sbin +APPSDIR := $(DESTDIR)/usr/share/applications +CFGDIR := $(DESTDIR)/etc/ogagent +INITDIR := $(DESTDIR)/etc/init.d +XDGAUTOSTARTDIR := $(DESTDIR)/etc/xdg/autostart +KDEAUTOSTARTDIR := $(DESTDIR)/usr/share/autostart + +PYC := $(shell find $(SOURCEDIR) -name '*.py[co]') +CACHES := $(shell find $(SOURCEDIR) -name '__pycache__') + +clean: + rm -rf $(PYC) $(CACHES) $(DESTDIR) +install-ogagent: + rm -rf $(DESTDIR) + mkdir -p $(LIBDIR) + mkdir -p $(BINDIR) + mkdir -p $(SBINDIR) + mkdir -p $(APPSDIR) + mkdir -p $(CFGDIR) + mkdir -p $(XDGAUTOSTARTDIR) + mkdir -p $(KDEAUTOSTARTDIR) + + mkdir $(LIBDIR)/img + + # Cleans up .pyc and cache folders + rm -f $(PYC) $(CACHES) + + cp -r $(SOURCEDIR)/opengnsys $(LIBDIR)/opengnsys + cp -r $(SOURCEDIR)/cfg $(LIBDIR)/cfg + cp $(SOURCEDIR)/img/oga.png $(LIBDIR)/img + + cp $(SOURCEDIR)/OGAgentUser.py $(LIBDIR) + # QT Dialogs & resources + cp $(SOURCEDIR)/*_ui.py $(LIBDIR) + cp $(SOURCEDIR)/OGAgent_rc.py $(LIBDIR) + + # Autostart elements for gnome/kde + cp desktop/OGAgentTool.desktop $(XDGAUTOSTARTDIR) + cp desktop/OGAgentTool.desktop $(KDEAUTOSTARTDIR) + + # scripts + cp scripts/ogagent $(BINDIR) + cp scripts/OGAgentTool-startup $(BINDIR) + cp scripts/OGAgentTool $(BINDIR) + + # Fix permissions + chmod 755 $(BINDIR)/ogagent + chmod 755 $(BINDIR)/OGAgentTool-startup + chmod 755 $(LIBDIR)/OGAgentUser.py + chmod 600 $(LIBDIR)/cfg/ogagent.cfg + + # If for red hat based, copy init.d +ifeq ($(DISTRO),rh) + mkdir -p $(INITDIR) + cp debian/ogagent.init $(INITDIR)/ogagent + chmod +x $(INITDIR)/ogagent + ln -fs /usr/share/OGAgent/cfg/ogagent.cfg $(CFGDIR) + ln -fs /usr/share/OGAgent/cfg/ogclient.cfg $(CFGDIR) +endif + + # chmod 0755 $(BINDIR)/ogagent +uninstall: + rm -rf $(LIBDIR) + # rm -f $(BINDIR)/ogagent + rm -rf $(CFGDIR) diff --git a/linux/build-packages.sh b/linux/build-packages.sh new file mode 100755 index 0000000..a42fd1c --- /dev/null +++ b/linux/build-packages.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +VERSION=1.0.0 +RELEASE=1 + +top=`pwd` + +# Debian based +dpkg-buildpackage -b -d + +cat ogagent-template.spec | + sed -e s/"version 0.0.0"/"version ${VERSION}"/g | + sed -e s/"release 1"/"release ${RELEASE}"/g > ogagent-$VERSION.spec + +# Now fix dependencies for opensuse +cat ogagent-template.spec | + sed -e s/"version 0.0.0"/"version ${VERSION}"/g | + sed -e s/"name ogagent"/"name ogagent-opensuse"/g | + sed -e s/"PyQt4"/"python-qt4"/g | + sed -e s/"libXScrnSaver"/"libXss1"/g > 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 diff --git a/linux/debian/changelog b/linux/debian/changelog new file mode 100644 index 0000000..e09ad98 --- /dev/null +++ b/linux/debian/changelog @@ -0,0 +1,5 @@ +ogagent (1.0.0) stable; urgency=medium + + * Initial release for OpenGnSys Agent + + -- Adolfo Gómez García Tue, 18 Jul 2015 03:18:22 +0200 diff --git a/linux/debian/compat b/linux/debian/compat new file mode 100644 index 0000000..f11c82a --- /dev/null +++ b/linux/debian/compat @@ -0,0 +1 @@ +9 \ No newline at end of file diff --git a/linux/debian/control b/linux/debian/control new file mode 100644 index 0000000..275b89f --- /dev/null +++ b/linux/debian/control @@ -0,0 +1,15 @@ +Source: ogagent +Section: admin +Priority: optional +Maintainer: Adolfo Gómez García +Build-Depends: debhelper (>= 7), po-debconf +Standards-Version: 3.9.2 +Homepage: http://www.opengnsys.es + +Package: ogagent +Section: admin +Priority: optional +Architecture: all +Depends: policykit-1(>=0.100), python-requests (>=0.8.2), python-qt4 (>=4.9), python-six(>=1.1), python-prctl(>=1.1.1), python (>=2.7), libxss1, ${misc:Depends} +Description: Agent for OpenGnSys + 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 new file mode 100644 index 0000000..cef2d43 --- /dev/null +++ b/linux/debian/copyright @@ -0,0 +1,26 @@ +Format-Specification: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=135 +Name: udsactor +Maintainer: Adolfo Gómez García +Source: http://www.udsenterprise.com/ + +Copyright: 2014 Virtual Cable S.L.U. +License: BSD-3-clause + +License: GPL-2+ +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., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +. +On Debian systems, the full text of the GNU General Public +License version 2 can be found in the file +`/usr/share/common-licenses/GPL-2'. \ No newline at end of file diff --git a/linux/debian/docs b/linux/debian/docs new file mode 100644 index 0000000..b2b2a78 --- /dev/null +++ b/linux/debian/docs @@ -0,0 +1 @@ +readme.txt diff --git a/linux/debian/ogagent.init b/linux/debian/ogagent.init new file mode 100644 index 0000000..f78341f --- /dev/null +++ b/linux/debian/ogagent.init @@ -0,0 +1,23 @@ +#!/bin/sh -e +### BEGIN INIT INFO +# Provides: ogagent +# Required-Start: $local_fs $remote_fs $network $syslog $named +# Required-Stop: $local_fs $remote_fs $network $syslog $named +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: OpenGnSys Agent Service +### END INIT INFO +# + +# . /lib/lsb/init-functions + +case "$1" in + start|stop|restart) + /usr/bin/ogagent $1 + ;; + force-reload) + /usr/bin/ogagent restart + ;; + *) echo "Usage: $0 {start|stop|restart|force-reload}" >&2; exit 1 ;; +esac + diff --git a/linux/debian/ogagent.links b/linux/debian/ogagent.links new file mode 100644 index 0000000..9b970d7 --- /dev/null +++ b/linux/debian/ogagent.links @@ -0,0 +1,2 @@ +/usr/share/OGAgent/cfg/ogagent.cfg /etc/ogagent/ogagent.cfg +/usr/share/OGAgent/cfg/ogclient.cfg /etc/ogagent/ogclient.cfg diff --git a/linux/debian/ogagent.postinst b/linux/debian/ogagent.postinst new file mode 100644 index 0000000..b59cfa6 --- /dev/null +++ b/linux/debian/ogagent.postinst @@ -0,0 +1,21 @@ +#!/bin/sh + +. /usr/share/debconf/confmodule + +set -e +case "$1" in + configure) + chmod 600 /usr/share/OGAgent/cfg/ogagent.cfg + ;; + abort-upgrade|abort-remove|abort-deconfigure) + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +#DEBHELPER# + +exit 0 diff --git a/linux/debian/ogagent.postinst.debhelper b/linux/debian/ogagent.postinst.debhelper new file mode 100644 index 0000000..e75924d --- /dev/null +++ b/linux/debian/ogagent.postinst.debhelper @@ -0,0 +1,5 @@ +# 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 b/linux/debian/ogagent.postrm new file mode 100644 index 0000000..a46fa48 --- /dev/null +++ b/linux/debian/ogagent.postrm @@ -0,0 +1,10 @@ +#!/bin/sh -e + +. /usr/share/debconf/confmodule + +set -e + +if [ "$1" = "purge" ] ; then + rm -rf /usr/share/OGAgent || true > /dev/null 2>&1 +fi + diff --git a/linux/debian/ogagent.postrm.debhelper b/linux/debian/ogagent.postrm.debhelper new file mode 100644 index 0000000..3167f1f --- /dev/null +++ b/linux/debian/ogagent.postrm.debhelper @@ -0,0 +1,12 @@ +# 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 new file mode 100644 index 0000000..978fc8b --- /dev/null +++ b/linux/debian/ogagent.substvars @@ -0,0 +1,2 @@ +misc:Depends= +misc:Pre-Depends= diff --git a/linux/debian/rules b/linux/debian/rules new file mode 100755 index 0000000..fbe82e6 --- /dev/null +++ b/linux/debian/rules @@ -0,0 +1,44 @@ +#!/usr/bin/make -f +# -*- makefile -*- +configure: configure-stamp +configure-stamp: + dh_testdir + touch configure-stamp +build: build-arch build-indep +build-arch: build-stamp +build-indep: build-stamp +build-stamp: configure-stamp + dh_testdir + $(MAKE) + touch $@ +clean: + dh_testdir + dh_testroot + rm -f build-stamp configure-stamp + dh_clean +install: build + dh_testdir + dh_testroot + dh_prep + dh_installdirs + $(MAKE) DESTDIR=$(CURDIR)/debian/ogagent install-ogagent +binary-arch: build install + # emptyness +binary-indep: build install + dh_testdir + dh_testroot + dh_installchangelogs + dh_installdocs + dh_installdebconf + dh_installinit --no-start + dh_python2=python + dh_compress + dh_link + dh_fixperms + dh_installdeb + dh_shlibdeps + dh_gencontrol + dh_md5sums + dh_builddeb +binary: binary-indep +.PHONY: build clean binary-indep binary install configure diff --git a/linux/debian/source/format b/linux/debian/source/format new file mode 100644 index 0000000..9f67427 --- /dev/null +++ b/linux/debian/source/format @@ -0,0 +1 @@ +3.0 (native) \ No newline at end of file diff --git a/linux/desktop/OGAgentTool.desktop b/linux/desktop/OGAgentTool.desktop new file mode 100644 index 0000000..28ff094 --- /dev/null +++ b/linux/desktop/OGAgentTool.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Name=OpenGnSys Agent Tools +Comment=OpenGnSys Userspace tools +Exec=/usr/bin/OGAgentTool-startup +Icon=/usr/share/OGAgent/img/oga.png +Terminal=false +Type=Application +NoDisplay=true +X-KDE-autostart-after=panel +X-KDE-StartupNotify=false +X-DBUS-StartupType=Unique +X-KDE-UniqueApplet=true diff --git a/linux/ogagent-template.spec b/linux/ogagent-template.spec new file mode 100644 index 0000000..072955e --- /dev/null +++ b/linux/ogagent-template.spec @@ -0,0 +1,65 @@ +%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 & tools +License: BSD3 +Group: Admin +Requires: python-six python-requests PyQt4 libXScrnSaver +Vendor: Virtual Cable S.L.U. +URL: http://www.udsenterprise.com +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/UDSActor +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 diff --git a/linux/policy/org.openuds.pkexec.UDSActorConfig.policy b/linux/policy/org.openuds.pkexec.UDSActorConfig.policy new file mode 100644 index 0000000..9afd775 --- /dev/null +++ b/linux/policy/org.openuds.pkexec.UDSActorConfig.policy @@ -0,0 +1,20 @@ + + + + + + + Run UDS Actor Configuration Program + Authentication is required to run UDS Actor Configuration + + no + no + auth_admin_keep + + /usr/sbin/UDSActorConfig + TRUE + + + \ No newline at end of file diff --git a/linux/readme.txt b/linux/readme.txt new file mode 100644 index 0000000..85a6443 --- /dev/null +++ b/linux/readme.txt @@ -0,0 +1,3 @@ +OGAgent is the agent intended for OpengGnSys interaction. + +Please, visit http://www.opengnsys.es for more information diff --git a/linux/scripts/OGAgentTool b/linux/scripts/OGAgentTool new file mode 100644 index 0000000..5b30052 --- /dev/null +++ b/linux/scripts/OGAgentTool @@ -0,0 +1,6 @@ +#!/bin/sh + +FOLDER=/usr/share/OGAgent + +cd $FOLDER +python OGAgentUser.py $@ diff --git a/linux/scripts/OGAgentTool-startup b/linux/scripts/OGAgentTool-startup new file mode 100644 index 0000000..bb3a848 --- /dev/null +++ b/linux/scripts/OGAgentTool-startup @@ -0,0 +1,10 @@ +#!/bin/sh + +# Simple hack to wait for systray to be present +# Exec tool if not already runned by session manager +ps -ef | grep "$USER" | grep -v grep | grep -v OGAgentTool-startup | grep 'OGAgentTool' -q +# If not already running +if [ $? -eq 1 ]; then + sleep 5 + exec /usr/bin/OGAgentTool +fi \ No newline at end of file diff --git a/linux/scripts/ogagent b/linux/scripts/ogagent new file mode 100644 index 0000000..1bcc29b --- /dev/null +++ b/linux/scripts/ogagent @@ -0,0 +1,6 @@ +#!/bin/sh + +FOLDER=/usr/share/OGAgent + +cd $FOLDER +python -m opengnsys.linux.OGAgentService $@ diff --git a/notas.txt b/notas.txt new file mode 100644 index 0000000..22118d6 --- /dev/null +++ b/notas.txt @@ -0,0 +1,11 @@ +* ¿Como sabemos la direccion del servidor? +* Seguridad. Como hacer el agente seguro (registro con OpenGnsys). Mensajes bidireccionales +* Mensajes. (Moderm) + - Definir mensajes de cliente --> servidor + - Definir mensajes servidor --> cliente +* Logotipo OpenGnsys +* Licencia BSD (relicenciamiento basado en OpenUDS) +* Clientes Windows y Linux inicialmente, parte usuarios? +* "Modern" vs "Legacy" agents +* Particularidades +* ¿Posibles extensiones para "consola" og? diff --git a/requires.txt b/requires.txt new file mode 100644 index 0000000..07ce387 --- /dev/null +++ b/requires.txt @@ -0,0 +1,3 @@ +six +requests + diff --git a/src/OGAServiceHelper.py b/src/OGAServiceHelper.py new file mode 100644 index 0000000..79a6c81 --- /dev/null +++ b/src/OGAServiceHelper.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 win32service +import win32serviceutil + +svc_name = "UDSActor" + +try: + hscm = win32service.OpenSCManager(None, None, win32service.SC_MANAGER_ALL_ACCESS) + + try: + hs = win32serviceutil.SmartOpenService(hscm, svc_name, win32service.SERVICE_ALL_ACCESS) + service_failure_actions = { + 'ResetPeriod': 864000, # Time in ms after which to reset the failure count to zero. + 'RebootMsg': u'', # Not using reboot option + 'Command': u'', # Not using run-command option + 'Actions': [ + (win32service.SC_ACTION_RESTART, 5000), # action, delay in ms + (win32service.SC_ACTION_RESTART, 5000) + ] + } + win32service.ChangeServiceConfig2(hs, win32service.SERVICE_CONFIG_FAILURE_ACTIONS, service_failure_actions) + finally: + win32service.CloseServiceHandle(hs) +finally: + win32service.CloseServiceHandle(hscm) diff --git a/src/OGAgent.manifest b/src/OGAgent.manifest new file mode 100644 index 0000000..0e5ff97 --- /dev/null +++ b/src/OGAgent.manifest @@ -0,0 +1,17 @@ + + + + Description + + + + + + + + diff --git a/src/OGAgent.qrc b/src/OGAgent.qrc new file mode 100644 index 0000000..5917766 --- /dev/null +++ b/src/OGAgent.qrc @@ -0,0 +1,5 @@ + + + img/oga.png + + diff --git a/src/OGAgentUser.py b/src/OGAgentUser.py new file mode 100644 index 0000000..501133c --- /dev/null +++ b/src/OGAgentUser.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 sys +from PyQt4 import QtGui +from PyQt4 import QtCore + +import time +import signal +import json +import six + +from opengnsys import ipc +from opengnsys import utils +from opengnsys.log import logger +from opengnsys.service import IPC_PORT +from opengnsys import operations +from about_dialog_ui import Ui_OGAAboutDialog +from message_dialog_ui import Ui_OGAMessageDialog +from opengnsys.scriptThread import ScriptExecutorThread +from opengnsys import VERSION +from opengnsys.config import readConfig +from opengnsys.loader import loadModules + +trayIcon = None + + +def sigTerm(sigNo, stackFrame): + if trayIcon: + trayIcon.quit() + + +# About dialog +class OGAAboutDialog(QtGui.QDialog): + def __init__(self, parent=None): + QtGui.QDialog.__init__(self, parent) + self.ui = Ui_OGAAboutDialog() + self.ui.setupUi(self) + self.ui.VersionLabel.setText("Version " + VERSION) + + def closeDialog(self): + self.hide() + + +class OGAMessageDialog(QtGui.QDialog): + def __init__(self, parent=None): + QtGui.QDialog.__init__(self, parent) + self.ui = Ui_OGAMessageDialog() + self.ui.setupUi(self) + + def message(self, message): + self.ui.message.setText(message) + self.show() + + def closeDialog(self): + self.hide() + + +class MessagesProcessor(QtCore.QThread): + + logoff = QtCore.pyqtSignal(name='logoff') + message = QtCore.pyqtSignal(tuple, name='message') + script = QtCore.pyqtSignal(QtCore.QString, name='script') + exit = QtCore.pyqtSignal(name='exit') + + def __init__(self, port): + super(self.__class__, self).__init__() + # Retries connection for a while + for _ in range(10): + try: + self.ipc = ipc.ClientIPC(port) + self.ipc.start() + break + except Exception: + logger.debug('IPC Server is not reachable') + self.ipc = None + time.sleep(2) + + self.running = False + + def stop(self): + self.running = False + if self.ipc: + self.ipc.stop() + + def isAlive(self): + return self.ipc is not None + + def sendLogin(self, userName): + if self.ipc: + self.ipc.sendLogin(userName) + + def sendLogout(self, userName): + if self.ipc: + self.ipc.sendLogout(userName) + + def run(self): + if self.ipc is None: + return + self.running = True + + # Wait a bit so we ensure IPC thread is running... + time.sleep(2) + + while self.running and self.ipc.running: + try: + 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') + self.message.emit((module, message, data)) + elif msgId == ipc.MSG_LOGOFF: + self.logoff.emit() + elif msgId == ipc.MSG_SCRIPT: + self.script.emit(QtCore.QString.fromUtf8(data)) + 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??)') + + if self.ipc.running is False and self.running is True: + logger.warn('Lost connection with Service, closing program') + + self.exit.emit() + + +class OGASystemTray(QtGui.QSystemTrayIcon): + def __init__(self, app_, parent=None): + self.app = app_ + + self.config = readConfig(client=True) + + # 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) + self.setContextMenu(self.menu) + self.ipc = MessagesProcessor(self.ipcport) + + if self.ipc.isAlive() is False: + raise Exception('No connection to service, exiting.') + + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.timerFnc) + + + self.stopped = False + + self.ipc.message.connect(self.message) + self.ipc.exit.connect(self.quit) + self.ipc.script.connect(self.executeScript) + self.ipc.logoff.connect(self.logoff) + + self.aboutDlg = OGAAboutDialog() + self.msgDlg = OGAMessageDialog() + + self.timer.start(1000) # Launch idle checking every 1 seconds + + self.ipc.start() + + def initialize(self): + # Load modules and activate them + # Also, sends "login" event to service + self.modules = loadModules(self, client=True) + logger.debug('Modules: {}'.format(list(v.name for v in self.modules))) + + # Send init to all modules + validMods = [] + for mod in self.modules: + try: + logger.debug('Activating module {}'.format(mod.name)) + mod.activate() + validMods.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 + + # If this is running, it's because he have logged in, inform service of this fact + self.ipc.sendLogin(operations.getCurrentUser()) + + def deinitialize(self): + for mod in reversed(self.modules): # Deinitialize reversed of initialization + try: + logger.debug('Deactivating module {}'.format(mod.name)) + mod.deactivate() + except Exception as e: + logger.exception() + logger.error("Deactivation of {} failed: {}".format(mod.name, utils.exceptionToMessage(e))) + + def timerFnc(self): + pass + + def message(self, msg): + ''' + Processes the message sent asynchronously, msg is an QString + ''' + try: + logger.debug('msg: {}, {}'.format(type(msg), msg)) + module, message, data = msg + except Exception as e: + logger.error('Got exception {} processing message {}'.format(e, msg)) + return + + for v in self.modules: + if v.name == module: # Case Sensitive!!!! + try: + logger.debug('Notifying message {} to module {} with json data {}'.format(message, v.name, data)) + v.processMessage(message, json.loads(data)) + return + except Exception as e: + logger.error('Got exception {} processing generic message on {}'.format(e, v.name)) + + logger.error('Module {} not found, messsage {} not sent'.format(module, message)) + + def executeScript(self, script): + logger.debug('Executing script') + script = six.text_type(script.toUtf8()).decode('base64') + th = ScriptExecutorThread(script) + th.start() + + def logoff(self): + logger.debug('Logoff invoked') + operations.logoff() # Invoke log off + + def about(self): + self.aboutDlg.exec_() + + def quit(self): + logger.debug('Quit invoked') + if self.stopped is False: + self.stopped = True + try: + self.deinitialize() + except Exception: + logger.exception() + logger.error('Got exception deinitializing modules') + + try: + # If we close Client, send Logoff to Broker + self.ipc.sendLogout(operations.getCurrentUser()) + self.timer.stop() + self.ipc.stop() + except Exception: + # May we have lost connection with server, simply exit in that case + pass + + try: + # operations.logoff() # Uncomment this after testing to logoff user + pass + except Exception: + pass + + self.app.quit() + +if __name__ == '__main__': + app = QtGui.QApplication(sys.argv) + + if not QtGui.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) + + try: + trayIcon = OGASystemTray(app) + except Exception as e: + logger.exception() + logger.error('OGA Service is not running, or it can\'t contact with OGA Server. User Tools stopped: {}'.format(utils.exceptionToMessage(e))) + sys.exit(1) + + try: + trayIcon.initialize() # Initialize modules, etc.. + except Exception as e: + logger.exception() + logger.error('Exception initializing OpenGnsys User Agent {}'.format(utils.exceptionToMessage(e))) + trayIcon.quit() + sys.exit(1) + + trayIcon.show() + + # Catch kill and logout user :) + signal.signal(signal.SIGTERM, sigTerm) + + res = app.exec_() + + logger.debug('Exiting') + trayIcon.quit() + + sys.exit(res) diff --git a/src/OGAgent_rc.py b/src/OGAgent_rc.py new file mode 100644 index 0000000..867ca2a --- /dev/null +++ b/src/OGAgent_rc.py @@ -0,0 +1,289 @@ +# -*- 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/about-dialog.ui b/src/about-dialog.ui new file mode 100644 index 0000000..5a6a8d3 --- /dev/null +++ b/src/about-dialog.ui @@ -0,0 +1,240 @@ + + + OGAAboutDialog + + + + 0 + 0 + 466 + 402 + + + + + Verdana + 9 + + + + About OGAgent + + + + + + true + + + + 9 + + + 9 + + + + + <html><head/><body><p><img src=":/images/img/oga.png"/>OpenGnsys Agent Tools</p></body></html> + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + <html><head/><body><p><span style=" font-family:'Sans Serif'; font-size:9pt; font-weight:600;">OpenGnsys Agent</span></p></body></html> + + + + + + + Version 1.0.0 + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + 0 + + + + &About + + + + 6 + + + 9 + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Verdana'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif';"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Sans Serif'; font-weight:600;">(c) 2014, Virtual Cable S.L.U.</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif'; font-style:italic;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a href="http://www.udsenterprise.com"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt; text-decoration: underline; color:#0000ff;">http://www.opengnsys.es</span></a></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a href="http://www.openuds.org"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt; text-decoration: underline; color:#0000ff;">http://www.udsenterprise.com</span></a></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif';"><br /></p></body></html> + + + true + + + + + + + + A&uthors + + + + 6 + + + 9 + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Verdana'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Sans Serif';">Adolfo Gómez García &lt;agomez@virtualcable.es&gt;</span></p></body></html> + + + true + + + + + + + + &License Agreement + + + + 6 + + + 9 + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Verdana'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;">Copyright (c) 2014 Virtual Cable S.L.</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'MS Shell Dlg 2'; font-size:8pt;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;">All rights reserved.</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'MS Shell Dlg 2'; font-size:8pt;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;">Redistribution and use in source and binary forms, with or without modification,</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;">are permitted provided that the following conditions are met:</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'MS Shell Dlg 2'; font-size:8pt;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;"> * Redistributions of source code must retain the above copyright notice,</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;"> this list of conditions and the following disclaimer.</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;"> * Redistributions in binary form must reproduce the above copyright notice,</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;"> this list of conditions and the following disclaimer in the documentation</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;"> and/or other materials provided with the distribution.</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;"> * Neither the name of Virtual Cable S.L. nor the names of its contributors</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;"> may be used to endorse or promote products derived from this software</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;"> without specific prior written permission.</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'MS Shell Dlg 2'; font-size:8pt;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;">THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS &quot;AS IS&quot;</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;">AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;">IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;">DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;">FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;">DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;">SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;">CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;">OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;">OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'MS Shell Dlg 2'; font-size:8pt;"><br /></p></body></html> + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + + + + buttonBox + clicked(QAbstractButton*) + OGAAboutDialog + closeDialog() + + + 432 + 381 + + + 282 + 362 + + + + + + closeDialog() + + diff --git a/src/about_dialog_ui.py b/src/about_dialog_ui.py new file mode 100644 index 0000000..054cc06 --- /dev/null +++ b/src/about_dialog_ui.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'about-dialog.ui' +# +# Created by: PyQt4 UI code generator 4.11.4 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) + +class Ui_OGAAboutDialog(object): + def setupUi(self, OGAAboutDialog): + OGAAboutDialog.setObjectName(_fromUtf8("OGAAboutDialog")) + OGAAboutDialog.resize(466, 402) + font = QtGui.QFont() + font.setFamily(_fromUtf8("Verdana")) + font.setPointSize(9) + OGAAboutDialog.setFont(font) + OGAAboutDialog.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) + OGAAboutDialog.setModal(True) + self.vboxlayout = QtGui.QVBoxLayout(OGAAboutDialog) + self.vboxlayout.setMargin(9) + self.vboxlayout.setSpacing(9) + self.vboxlayout.setObjectName(_fromUtf8("vboxlayout")) + self.LogoLabel = QtGui.QLabel(OGAAboutDialog) + self.LogoLabel.setObjectName(_fromUtf8("LogoLabel")) + self.vboxlayout.addWidget(self.LogoLabel) + spacerItem = QtGui.QSpacerItem(20, 5, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) + self.vboxlayout.addItem(spacerItem) + self.TitleLabel = QtGui.QLabel(OGAAboutDialog) + self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) + self.vboxlayout.addWidget(self.TitleLabel) + self.VersionLabel = QtGui.QLabel(OGAAboutDialog) + self.VersionLabel.setObjectName(_fromUtf8("VersionLabel")) + self.vboxlayout.addWidget(self.VersionLabel) + spacerItem1 = QtGui.QSpacerItem(20, 5, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) + self.vboxlayout.addItem(spacerItem1) + self.tabWidget = QtGui.QTabWidget(OGAAboutDialog) + self.tabWidget.setObjectName(_fromUtf8("tabWidget")) + self.aboutTab = QtGui.QWidget() + self.aboutTab.setObjectName(_fromUtf8("aboutTab")) + self.vboxlayout1 = QtGui.QVBoxLayout(self.aboutTab) + self.vboxlayout1.setMargin(9) + self.vboxlayout1.setSpacing(6) + self.vboxlayout1.setObjectName(_fromUtf8("vboxlayout1")) + self.aboutBrowser = QtGui.QTextBrowser(self.aboutTab) + self.aboutBrowser.setOpenExternalLinks(True) + self.aboutBrowser.setObjectName(_fromUtf8("aboutBrowser")) + self.vboxlayout1.addWidget(self.aboutBrowser) + self.tabWidget.addTab(self.aboutTab, _fromUtf8("")) + self.authorsTab = QtGui.QWidget() + self.authorsTab.setObjectName(_fromUtf8("authorsTab")) + self.vboxlayout2 = QtGui.QVBoxLayout(self.authorsTab) + self.vboxlayout2.setMargin(9) + self.vboxlayout2.setSpacing(6) + self.vboxlayout2.setObjectName(_fromUtf8("vboxlayout2")) + self.authorsBrowser = QtGui.QTextBrowser(self.authorsTab) + self.authorsBrowser.setOpenExternalLinks(True) + self.authorsBrowser.setObjectName(_fromUtf8("authorsBrowser")) + self.vboxlayout2.addWidget(self.authorsBrowser) + self.tabWidget.addTab(self.authorsTab, _fromUtf8("")) + self.licenseTab = QtGui.QWidget() + self.licenseTab.setObjectName(_fromUtf8("licenseTab")) + self.vboxlayout3 = QtGui.QVBoxLayout(self.licenseTab) + self.vboxlayout3.setMargin(9) + self.vboxlayout3.setSpacing(6) + self.vboxlayout3.setObjectName(_fromUtf8("vboxlayout3")) + self.licenseBrowser = QtGui.QTextBrowser(self.licenseTab) + self.licenseBrowser.setObjectName(_fromUtf8("licenseBrowser")) + self.vboxlayout3.addWidget(self.licenseBrowser) + self.tabWidget.addTab(self.licenseTab, _fromUtf8("")) + self.vboxlayout.addWidget(self.tabWidget) + self.buttonBox = QtGui.QDialogButtonBox(OGAAboutDialog) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Close) + self.buttonBox.setObjectName(_fromUtf8("buttonBox")) + self.vboxlayout.addWidget(self.buttonBox) + + self.retranslateUi(OGAAboutDialog) + self.tabWidget.setCurrentIndex(0) + QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("clicked(QAbstractButton*)")), OGAAboutDialog.closeDialog) + QtCore.QMetaObject.connectSlotsByName(OGAAboutDialog) + + def retranslateUi(self, OGAAboutDialog): + OGAAboutDialog.setWindowTitle(_translate("OGAAboutDialog", "About OGAgent", None)) + self.LogoLabel.setText(_translate("OGAAboutDialog", "

OpenGnsys Agent Tools

", None)) + self.TitleLabel.setText(_translate("OGAAboutDialog", "

OpenGnsys Agent

", None)) + self.VersionLabel.setText(_translate("OGAAboutDialog", "Version 1.0.0", None)) + self.aboutBrowser.setHtml(_translate("OGAAboutDialog", "\n" +"\n" +"


\n" +"

(c) 2014, Virtual Cable S.L.U.

\n" +"


\n" +"

http://www.opengnsys.es

\n" +"

http://www.udsenterprise.com

\n" +"


", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.aboutTab), _translate("OGAAboutDialog", "&About", None)) + self.authorsBrowser.setHtml(_translate("OGAAboutDialog", "\n" +"\n" +"

Adolfo Gómez García <agomez@virtualcable.es>

", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.authorsTab), _translate("OGAAboutDialog", "A&uthors", None)) + self.licenseBrowser.setHtml(_translate("OGAAboutDialog", "\n" +"\n" +"

Copyright (c) 2014 Virtual Cable S.L.

\n" +"


\n" +"

All rights reserved.

\n" +"


\n" +"

Redistribution and use in source and binary forms, with or without modification,

\n" +"

are permitted provided that the following conditions are met:

\n" +"


\n" +"

* Redistributions of source code must retain the above copyright notice,

\n" +"

this list of conditions and the following disclaimer.

\n" +"

* Redistributions in binary form must reproduce the above copyright notice,

\n" +"

this list of conditions and the following disclaimer in the documentation

\n" +"

and/or other materials provided with the distribution.

\n" +"

* Neither the name of Virtual Cable S.L. nor the names of its contributors

\n" +"

may be used to endorse or promote products derived from this software

\n" +"

without specific prior written permission.

\n" +"


\n" +"

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"

\n" +"

AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE

\n" +"

IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE

\n" +"

DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE

\n" +"

FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL

\n" +"

DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR

\n" +"

SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER

\n" +"

CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,

\n" +"

OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE

\n" +"

OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

\n" +"


", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.licenseTab), _translate("OGAAboutDialog", "&License Agreement", None)) + +import OGAgent_rc + +if __name__ == "__main__": + import sys + app = QtGui.QApplication(sys.argv) + OGAAboutDialog = QtGui.QDialog() + ui = Ui_OGAAboutDialog() + ui.setupUi(OGAAboutDialog) + OGAAboutDialog.show() + sys.exit(app.exec_()) + diff --git a/src/cfg/ogagent.cfg b/src/cfg/ogagent.cfg new file mode 100644 index 0000000..11b1483 --- /dev/null +++ b/src/cfg/ogagent.cfg @@ -0,0 +1,21 @@ +[opengnsys] +# Listen address & port of REST +address=0.0.0.0 +port=8000 + +# This is a comma separated list of paths where to look for modules to load +path=test_modules/server + +# Remote OpenGnsys Service +remote=https://192.168.2.10/opengnsys/rest + +# Log Level, if ommited, will be set to INFO +log=DEBUG + +# Module specific +# The sections must match the module name +# This section will be passes on activation to module +[Sample1] +value1=Mariete +value2=Yo +remote=https://172.27.0.1:9999/rest diff --git a/src/cfg/ogclient.cfg b/src/cfg/ogclient.cfg new file mode 100644 index 0000000..e1a9b07 --- /dev/null +++ b/src/cfg/ogclient.cfg @@ -0,0 +1,11 @@ +[opengnsys] +# Log Level, if ommited, will be set to INFO +log=DEBUG + +# Module specific +# The sections must match the module name +# This section will be passes on activation to module +[Sample1] +value1=Mariete +value2=Yo +remote=https://172.27.0.1:9999/rest \ No newline at end of file diff --git a/src/img/oga-48x48.ico b/src/img/oga-48x48.ico new file mode 100644 index 0000000000000000000000000000000000000000..37ded487de6e9c0f4026d1964212acd72433d5f1 GIT binary patch literal 9662 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 literal 0 HcmV?d00001 diff --git a/src/img/oga.ico b/src/img/oga.ico new file mode 100644 index 0000000000000000000000000000000000000000..37ded487de6e9c0f4026d1964212acd72433d5f1 GIT binary patch literal 9662 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 QUH||907*qoM6N<$f?Q&xHvj+t literal 0 HcmV?d00001 diff --git a/src/license.txt b/src/license.txt new file mode 100644 index 0000000..bdb7d25 --- /dev/null +++ b/src/license.txt @@ -0,0 +1,27 @@ +Copyright (c) 2012-2015 Virtual Cable S.L. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of Virtual Cable S.L. nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +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. diff --git a/src/message-dialog.ui b/src/message-dialog.ui new file mode 100644 index 0000000..7513703 --- /dev/null +++ b/src/message-dialog.ui @@ -0,0 +1,89 @@ + + + OGAMessageDialog + + + + 0 + 0 + 339 + 188 + + + + + 0 + 0 + + + + + Verdana + 10 + + + + UDS Actor + + + + + 10 + 10 + 321 + 171 + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 15 + + + + + + + + QDialogButtonBox::Ok + + + + + + + + + + buttonBox + clicked(QAbstractButton*) + OGAMessageDialog + closeDialog() + + + 203 + 161 + + + 337 + 125 + + + + + + closeDialog() + + diff --git a/src/message_dialog_ui.py b/src/message_dialog_ui.py new file mode 100644 index 0000000..ebd4d34 --- /dev/null +++ b/src/message_dialog_ui.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'message-dialog.ui' +# +# Created by: PyQt4 UI code generator 4.11.4 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) + +class Ui_OGAMessageDialog(object): + def setupUi(self, OGAMessageDialog): + OGAMessageDialog.setObjectName(_fromUtf8("OGAMessageDialog")) + OGAMessageDialog.resize(339, 188) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(OGAMessageDialog.sizePolicy().hasHeightForWidth()) + OGAMessageDialog.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setFamily(_fromUtf8("Verdana")) + font.setPointSize(10) + OGAMessageDialog.setFont(font) + self.verticalLayoutWidget = QtGui.QWidget(OGAMessageDialog) + self.verticalLayoutWidget.setGeometry(QtCore.QRect(10, 10, 321, 171)) + self.verticalLayoutWidget.setObjectName(_fromUtf8("verticalLayoutWidget")) + self.verticalLayout = QtGui.QVBoxLayout(self.verticalLayoutWidget) + self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + self.message = QtGui.QTextBrowser(self.verticalLayoutWidget) + self.message.setObjectName(_fromUtf8("message")) + self.verticalLayout.addWidget(self.message) + spacerItem = QtGui.QSpacerItem(20, 15, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) + self.verticalLayout.addItem(spacerItem) + self.buttonBox = QtGui.QDialogButtonBox(self.verticalLayoutWidget) + self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Ok) + self.buttonBox.setObjectName(_fromUtf8("buttonBox")) + self.verticalLayout.addWidget(self.buttonBox) + + self.retranslateUi(OGAMessageDialog) + QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("clicked(QAbstractButton*)")), OGAMessageDialog.closeDialog) + QtCore.QMetaObject.connectSlotsByName(OGAMessageDialog) + + def retranslateUi(self, OGAMessageDialog): + OGAMessageDialog.setWindowTitle(_translate("OGAMessageDialog", "UDS Actor", None)) + + +if __name__ == "__main__": + import sys + app = QtGui.QApplication(sys.argv) + OGAMessageDialog = QtGui.QDialog() + ui = Ui_OGAMessageDialog() + ui.setupUi(OGAMessageDialog) + OGAMessageDialog.show() + sys.exit(app.exec_()) + diff --git a/src/opengnsys/RESTApi.py b/src/opengnsys/RESTApi.py new file mode 100644 index 0000000..5caaf8c --- /dev/null +++ b/src/opengnsys/RESTApi.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 201 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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-msg=E1101,W0703 + +from __future__ import unicode_literals + +import requests +import logging +import json +import warnings + +from .log import logger + +from .utils import exceptionToMessage + +VERIFY_CERT = False + + +class RESTError(Exception): + ERRCODE = 0 + + +class ConnectionError(RESTError): + ERRCODE = -1 + + +# Disable warnings log messages +try: + import urllib3 # @UnusedImport +except Exception: + from requests.packages import urllib3 # @Reimport + +try: + urllib3.disable_warnings() # @UndefinedVariable + warnings.simplefilter("ignore") +except Exception: + pass # In fact, isn't too important, but wil log warns to logging file + +class REST(object): + ''' + Simple interface to remote REST apis. + The constructor expects the "base url" as parameter, that is, the url that will be common on all REST requests + Remember that this is a helper for "easy of use". You can provide your owns using requests lib for example. + Examples: + v = REST('https://example.com/rest/v1/') (Can omit trailing / if desired) + v.sendMessage('hello?param1=1¶m2=2') + This will generate a GET message to https://example.com/rest/v1/hello?param1=1¶m2=2, and return the deserialized JSON result or an exception + v.sendMessage('hello?param1=1¶m2=2', {'name': 'mario' }) + This will generate a POST message to https://example.com/rest/v1/hello?param1=1¶m2=2, with json encoded body {'name': 'mario' }, and also returns + the deserialized JSON result or raises an exception in case of error + ''' + def __init__(self, url): + ''' + Initializes the REST helper + url is the full url of the REST API Base, as for example "https://example.com/rest/v1". + @param url The url of the REST API Base. The trailing '/' can be included or omitted, as desired. + ''' + self.endpoint = url + + if self.endpoint[-1] != '/': + self.endpoint += '/' + + # Some OSs ships very old python requests lib implementations, workaround them... + try: + self.newerRequestLib = requests.__version__.split('.')[0] >= '1' + except Exception: + self.newerRequestLib = False # I no version, guess this must be an old requests + + # Disable logging requests messages except for errors, ... + logging.getLogger("requests").setLevel(logging.CRITICAL) + # Tries to disable all warnings + try: + warnings.simplefilter("ignore") # Disables all warnings + except Exception: + pass + + def _getUrl(self, method): + ''' + Internal method + Composes the URL based on "method" + @param method: Method to append to base url for composition + ''' + url = self.endpoint + method + + return url + + def _request(self, url, data=None): + ''' + Launches the request + @param url: The url to obtain + @param data: if None, the request will be sent as a GET request. If != None, the request will be sent as a POST, with data serialized as JSON in the body. + ''' + try: + if data is None: + logger.debug('Requesting using GET (no data provided) {}'.format(url)) + # Old requests version does not support verify, but they do not checks ssl certificate by default + if self.newerRequestLib: + r = requests.get(url, verify=VERIFY_CERT) + else: + r = requests.get(url) + else: # POST + logger.debug('Requesting using POST {}, data: {}'.format(url, data)) + if self.newerRequestLib: + r = requests.post(url, data=data, headers={'content-type': 'application/json'}, verify=VERIFY_CERT) + else: + r = requests.post(url, data=data, headers={'content-type': 'application/json'}) + + r = json.loads(r.content) # Using instead of r.json() to make compatible with oooold rquests lib versions + except requests.exceptions.RequestException as e: + raise ConnectionError(e) + except Exception as e: + raise ConnectionError(exceptionToMessage(e)) + + return r + + def sendMessage(self, msg, data=None, processData=True): + ''' + Sends a message to remote REST server + @param data: if None or omitted, message will be a GET, else it will send a POST + @param processData: if True, data will be serialized to json before sending, else, data will be sent as "raw" + ''' + logger.debug('Invoking post message {} with data {}'.format(msg, data)) + + if processData and data is not None: + data = json.dumps(data) + + url = self._getUrl(msg) + logger.debug('Requesting {}'.format(url)) + + return self._request(url, data) diff --git a/src/opengnsys/__init__.py b/src/opengnsys/__init__.py new file mode 100644 index 0000000..43c033d --- /dev/null +++ b/src/opengnsys/__init__.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 + +# On centos, old six release does not includes byte2int, nor six.PY2 +import six + +import modules +from RESTApi import REST, RESTError + +VERSION = '1.0.0' + +__title__ = 'OpenGnSys Agent' +__version__ = VERSION +__build__ = 0x010750 +__author__ = 'Adolfo Gómez' +__license__ = "BSD 3-clause" +__copyright__ = "Copyright VirtualCable S.L.U." + + +if not hasattr(six, 'byte2int'): + if six.PY3: + import operator + six.byte2int = operator.itemgetter(0) + else: + def _byte2int(bs): + return ord(bs[0]) + six.byte2int = _byte2int diff --git a/src/opengnsys/certs.py b/src/opengnsys/certs.py new file mode 100644 index 0000000..e4c070e --- /dev/null +++ b/src/opengnsys/certs.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 tempfile import gettempdir +from os.path import exists, join + +CERTFILE = 'OGAgent.pem' + + +def createSelfSignedCert(force=False): + + certFile = join(gettempdir(), CERTFILE) + + if exists(certFile) and not force: + return certFile + + certData = '''-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCb50K3mIznNklz +yVAD7xSQOSJQ6+NPXj7U9/4zLZ+TvmbQ7RqUUsxbfxHbeRnoYTWV2nKk4+tHqmvz +ujLSS/loFhTSMqtrLn7rowSYJoQhKOUkAiQlWkqCfItWgL5pJopDpNHFul9Rn3ds +PMWQTiGeUNR4Y3RnBhr1Q1BsqAzf4m6zFUmgLPPmVLdF4uJ3Tuz8TSy2gWLs5aSr +5do4WamwUfYjRSVMJECmwjUM4rQ8SQgg0sHBeBuDUGNBvBQFac1G7qUcMReeu8Zr +DUtMsXma/l4rA8NB5CRmTrQbTBF4l+jb2BDFebDqDUK1Oqs9X35yOQfDOAFYHiix +PX0IsXOZAgMBAAECggEBAJi3000RrIUZUp6Ph0gzPMuCjDEEwWiQA7CPNX1gpb8O +dp0WhkDhUroWIaICYPSXtOwUTtVjRqivMoxPy1Thg3EIoGC/rdeSdlXRHMEGicwJ +yVyalFnatr5Xzg5wkxVh4XMd0zeDt7e3JD7s0QLo5lm1CEzd77qz6lhzFic5/1KX +bzdULtTlq60dazg2hEbcS4OmM1UMCtRVDAsOIUIZPL0M9j1C1d1iEdYnh2xshKeG +/GOfo95xsgdMlGjtv3hUT5ryKVoEsu+36rGb4VfhPfUvvoVbRx5QZpW+QvxaYh5E +Fi0JEROozFwG31Y++8El7J3yQko8cFBa1lYYUwwpNAECgYEAykT+GiM2YxJ4uVF1 +OoKiE9BD53i0IG5j87lGPnWqzEwYBwnqjEKDTou+uzMGz3MDV56UEFNho7wUWh28 +LpEkjJB9QgbsugjxIBr4JoL/rYk036e/6+U8I95lvYWrzb+rBMIkRDYI7kbQD/mQ +piYUpuCkTymNAu2RisK6bBzJslkCgYEAxVE23OQvkCeOV8hJNPZGpJ1mDS+TiOow +oOScMZmZpail181eYbAfMsCr7ri812lSj98NvA2GNVLpddil6LtS1cQ5p36lFBtV +xQUMZiFz4qVbEak+izL+vPaev/mXXsOcibAIQ+qI/0txFpNhJjpaaSy6vRCBYFmc +8pgSoBnBI0ECgYAUKCn2atnpp5aWSTLYgNosBU4vDA1PShD14dnJMaqyr0aZtPhF +v/8b3btFJoGgPMLxgWEZ+2U4ju6sSFhPf7FXvLJu2QfQRkHZRDbEh7t5DLpTK4Fp +va9vl6Ml7uM/HsGpOLuqfIQJUs87OFCc7iCSvMJDDU37I7ekT2GKkpfbCQKBgBrE +0NeY0WcSJrp7/oqD2sOcYurpCG/rrZs2SIZmGzUhMxaa0vIXzbO59dlWELB8pmnE +Tf20K//x9qA5OxDe0PcVPukdQlH+/1zSOYNliG44FqnHtyd1TJ/gKVtMBiAiE4uO +aSClod5Yosf4SJbCFd/s5Iyfv52NqsAyp1w3Aj/BAoGAVCnEiGUfyHlIR+UH4zZW +GXJMeqdZLfcEIszMxLePkml4gUQhoq9oIs/Kw+L1DDxUwzkXN4BNTlFbOSu9gzK1 +dhuIUGfS6RPL88U+ivC3A0y2jT43oUMqe3hiRt360UQ1GXzp2dMnR9odSRB1wHoO +IOjEBZ8341/c9ZHc5PCGAG8= +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIJAIrEIthCfxUCMA0GCSqGSIb3DQEBCwUAMIGNMQswCQYD +VQQGEwJFUzEPMA0GA1UECAwGTWFkcmlkMREwDwYDVQQHDAhBbGNvcmNvbjEMMAoG +A1UECgwDVURTMQ4wDAYDVQQLDAVBY3RvcjESMBAGA1UEAwwJVURTIEFjdG9yMSgw +JgYJKoZIhvcNAQkBFhlzdXBwb3J0QHVkc2VudGVycHJpc2UuY29tMB4XDTE0MTAy +NjIzNDEyNFoXDTI0MTAyMzIzNDEyNFowgY0xCzAJBgNVBAYTAkVTMQ8wDQYDVQQI +DAZNYWRyaWQxETAPBgNVBAcMCEFsY29yY29uMQwwCgYDVQQKDANVRFMxDjAMBgNV +BAsMBUFjdG9yMRIwEAYDVQQDDAlVRFMgQWN0b3IxKDAmBgkqhkiG9w0BCQEWGXN1 +cHBvcnRAdWRzZW50ZXJwcmlzZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCb50K3mIznNklzyVAD7xSQOSJQ6+NPXj7U9/4zLZ+TvmbQ7RqUUsxb +fxHbeRnoYTWV2nKk4+tHqmvzujLSS/loFhTSMqtrLn7rowSYJoQhKOUkAiQlWkqC +fItWgL5pJopDpNHFul9Rn3dsPMWQTiGeUNR4Y3RnBhr1Q1BsqAzf4m6zFUmgLPPm +VLdF4uJ3Tuz8TSy2gWLs5aSr5do4WamwUfYjRSVMJECmwjUM4rQ8SQgg0sHBeBuD +UGNBvBQFac1G7qUcMReeu8ZrDUtMsXma/l4rA8NB5CRmTrQbTBF4l+jb2BDFebDq +DUK1Oqs9X35yOQfDOAFYHiixPX0IsXOZAgMBAAGjUDBOMB0GA1UdDgQWBBRShS90 +5lJTNvYPIEqP3GxWwG5iiDAfBgNVHSMEGDAWgBRShS905lJTNvYPIEqP3GxWwG5i +iDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAU0Sp4gXhQmRVzq+7+ +vRFUkQuPj4Ga/d9r5Wrbg3hck3+5pwe9/7APoq0P/M0DBhQpiJKjrD6ydUevC+Y/ +43ZOJPhMlNw0o6TdQxOkX6FDwQanLLs7sfvJvqtVzYn3nuRFKT3dvl7Zg44QMw2M +ay42q59fAcpB4LaDx/i7gOYSS5eca3lYW7j7YSr/+ozXK2KlgUkuCUHN95lOq+dF +trmV9mjzM4CNPZqKSE7kpHRywgrXGPCO000NvEGSYf82AtgRSFKiU8NWLQSEPdcB +k//2dsQZw2cRZ8DrC2B6Tb3M+3+CA6wVyqfqZh1SZva3LfGvq/C+u+ItguzPqNpI +xtvM +-----END CERTIFICATE-----''' + with open(certFile, "wt") as f: + f.write(certData) + + return certFile diff --git a/src/opengnsys/config.py b/src/opengnsys/config.py new file mode 100644 index 0000000..c86c697 --- /dev/null +++ b/src/opengnsys/config.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 + +from ConfigParser import SafeConfigParser +from .log import logger + +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' + else: + fname = 'ogagent.cfg' + + if len(cfg.read('cfg/{}'.format(fname))) == 0: + # No configuration found + return None + + return cfg + \ No newline at end of file diff --git a/src/opengnsys/httpserver.py b/src/opengnsys/httpserver.py new file mode 100644 index 0000000..4cd95c8 --- /dev/null +++ b/src/opengnsys/httpserver.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2015 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 +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' + server_version = 'OpenGnsys Agent Server' + sys_version = '' + + def sendJsonError(self, code, message): + self.send_response(code) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(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.end_headers() + # Send the html message + self.wfile.write(data) + + + # parseURL + def parseUrl(self): + # Very simple path & params splitter + path = self.path.split('?')[0][1:].split('/') + + try: + params = dict((v[0], unquote(v[1])) for v in (v.split('=') for v in self.path.split('?')[1].split('&'))) + except Exception: + params = {} + + for v in self.service.modules: + if v.name == path[0]: # Case Sensitive!!!! + return (v, path[1:], params) + + return (None, path, params) + + def notifyMessage(self, module, path, getParams, postParams): + ''' + Locates witch module will process the message based on path (first folder on url path) + ''' + try: + data = module.processServerMessage(path, getParams, postParams) + self.sendJsonResponse(data) + except Exception as e: + logger.exception() + self.sendJsonError(500, exceptionToMessage(e)) + + def do_GET(self): + module, path, params = self.parseUrl() + + self.notifyMessage(module, path, params, None) + + def do_POST(self): + module, path, getParams = self.parseUrl() + + # Tries to get json content + try: + length = int(self.headers.getheader('content-length')) + content = self.rfile.read(length) + logger.debug('length: {}, content >>{}<<'.format(length, content)) + postParams = json.loads(content) + except Exception as e: + self.sendJsonError(500, exceptionToMessage(e)) + + self.notifyMessage(module, path, getParams, postParams) + + + def log_error(self, fmt, *args): + logger.error('HTTP ' + fmt % args) + + def log_message(self, fmt, *args): + logger.info('HTTP ' + fmt % args) + + +class HTTPThreadingServer(ThreadingMixIn, HTTPServer): + pass + +class HTTPServerThread(threading.Thread): + def __init__(self, address, service): + super(self.__class__, self).__init__() + + HTTPServerHandler.service = service # Keep tracking of service so we can intercact with it + + self.certFile = createSelfSignedCert() + self.server = HTTPThreadingServer(address, HTTPServerHandler) + self.server.socket = ssl.wrap_socket(self.server.socket, certfile=self.certFile, server_side=True) + + logger.debug('Initialized HTTPS Server thread on {}'.format(address)) + + def getServerUrl(self): + return 'https://{}:{}/'.format(self.server.server_address[0], self.server.server_address[1]) + + def stop(self): + self.server.shutdown() + + def run(self): + self.server.serve_forever() + + \ No newline at end of file diff --git a/src/opengnsys/ipc.py b/src/opengnsys/ipc.py new file mode 100644 index 0000000..de9faf2 --- /dev/null +++ b/src/opengnsys/ipc.py @@ -0,0 +1,416 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 socket +import threading +import six +import traceback +import json + +from opengnsys.utils import toUnicode +from opengnsys.log import logger + +# The IPC Server will wait for connections from clients +# Clients will open socket, and wait for data from server +# The messages sent (from server) will be the following (subject to future changes): +# Message_id Data Action +# ------------ -------- -------------------------- +# MSG_LOGOFF None Logout user from session +# MSG_MESSAGE message,level Display a message with level (INFO, WARN, ERROR, FATAL) # TODO: Include level, right now only has message +# MSG_SCRIPT python script Execute an specific python script INSIDE CLIENT environment (this messages is not sent right now) +# The messages received (sent from client) will be the following: +# Message_id Data Action +# ------------ -------- -------------------------- +# REQ_LOGOUT Logout user from session +# REQ_INFORMATION None Request information from ipc server (maybe configuration parameters in a near future) +# REQ_LOGIN python script Execute an specific python script INSIDE CLIENT environment (this messages is not sent right now) +# +# All messages are in the form: +# 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 + +# Client messages +MSG_LOGOFF = 0xA1 # Request log off from an user +MSG_MESSAGE = 0xB2 +MSG_SCRIPT = 0xC3 + +# Request messages +REQ_MESSAGE = 0xD4 +REQ_LOGIN = 0xE5 +REQ_LOGOUT = 0xF6 + +# Reverse msgs dict for debugging +REV_DICT = { + MSG_LOGOFF: 'MSG_LOGOFF', + MSG_MESSAGE: 'MSG_MESSAGE', + MSG_SCRIPT: 'MSG_SCRIPT', + REQ_LOGIN: 'REQ_LOGIN', + REQ_LOGOUT: 'REQ_LOGOUT', + REQ_MESSAGE: 'REQ_MESSAGE' +} + +MAGIC = b'\x4F\x47\x41\x00' # OGA in hexa with a padded 0 to the right + + +# States for client processor +ST_SECOND_BYTE = 0x01 +ST_RECEIVING = 0x02 +ST_PROCESS_MESSAGE = 0x02 + + +class ClientProcessor(threading.Thread): + def __init__(self, parent, clientSocket): + super(self.__class__, self).__init__() + self.parent = parent + self.clientSocket = clientSocket + self.running = False + self.messages = six.moves.queue.Queue(32) # @UndefinedVariable + + def stop(self): + logger.debug('Stoping client processor') + self.running = False + + def processRequest(self, msg, data): + logger.debug('Got Client message {}={}'.format(msg, REV_DICT.get(msg))) + if self.parent.clientMessageProcessor is not None: + self.parent.clientMessageProcessor(msg, data) + + def run(self): + self.running = True + self.clientSocket.setblocking(0) + + state = None + recv_msg = None + recv_data = None + while self.running: + try: + counter = 1024 + while counter > 0: # So we process at least the incoming queue every XX bytes readed + counter -= 1 + b = self.clientSocket.recv(1) + if b == b'': + # Client disconnected + self.running = False + break + buf = six.byte2int(b) # 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)) + state = buf + recv_msg = buf + continue # Get next byte + else: + logger.debug('Got unexpected data {}'.format(buf)) + elif state in (REQ_MESSAGE, REQ_LOGIN, REQ_LOGOUT): + logger.debug('First length byte is {}'.format(buf)) + msg_len = buf + state = ST_SECOND_BYTE + continue + elif state == ST_SECOND_BYTE: + msg_len += buf << 8 + logger.debug('Second length byte is {}, len is {}'.format(buf, msg_len)) + if msg_len == 0: + self.processRequest(recv_msg, None) + state = None + break + state = ST_RECEIVING + recv_data = b'' + continue + elif state == ST_RECEIVING: + recv_data += six.int2byte(buf) + msg_len -= 1 + if msg_len == 0: + self.processRequest(recv_msg, recv_data) + recv_data = None + state = None + break + else: + logger.debug('Got invalid message from request: {}, state: {}'.format(buf, state)) + except socket.error as e: + # If no data is present, no problem at all, pass to check messages + pass + except Exception as e: + tb = traceback.format_exc() + logger.error('Error: {}, trace: {}'.format(e, tb)) + + if self.running is False: + break + + try: + msg = self.messages.get(block=True, timeout=1) + except six.moves.queue.Empty: # No message got in time @UndefinedVariable + continue + + logger.debug('Got message {}={}'.format(msg, REV_DICT.get(msg[0]))) + + 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 + try: + self.clientSocket.sendall(data) + except socket.error as e: + # Send data error + logger.debug('Socket connection is no more available: {}'.format(e.args)) + self.running = False + except Exception as e: + logger.error('Invalid message in queue: {}'.format(e)) + + logger.debug('Client processor stopped') + try: + self.clientSocket.close() + except Exception: + pass # If can't close, nothing happens, just end thread + + +class ServerIPC(threading.Thread): + + def __init__(self, listenPort, clientMessageProcessor=None): + super(self.__class__, self).__init__() + self.port = listenPort + self.running = False + self.serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.threads = [] + self.clientMessageProcessor = clientMessageProcessor + + def stop(self): + logger.debug('Stopping Server IPC') + self.running = False + for t in self.threads: + t.stop() + socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect(('localhost', self.port)) + self.serverSocket.close() + + for t in self.threads: + t.join() + + def sendMessage(self, msgId, msgData): + ''' + Notify message to all listening threads + ''' + logger.debug('Sending message {}({}),{} to all clients'.format(msgId, REV_DICT.get(msgId), msgData)) + + # Convert to bytes so length is correctly calculated + if isinstance(msgData, six.text_type): + msgData = msgData.encode('utf8') + + for t in self.threads: + if t.isAlive(): + logger.debug('Sending to {}'.format(t)) + t.messages.put((msgId, msgData)) + + def sendLoggofMessage(self): + self.sendMessage(MSG_LOGOFF, '') + + def sendMessageMessage(self, message): + self.sendMessage(MSG_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(): + logger.debug('Thread {} is alive'.format(t)) + aliveThreads.append(t) + self.threads[:] = aliveThreads + + def run(self): + self.running = True + + self.serverSocket.bind(('localhost', self.port)) + self.serverSocket.setblocking(1) + self.serverSocket.listen(4) + + while True: + try: + (clientSocket, address) = self.serverSocket.accept() + # Stop processing if thread is mean to stop + if self.running is False: + break + logger.debug('Got connection from {}'.format(address)) + + self.cleanupFinishedThreads() # House keeping + + logger.debug('Starting new thread, current: {}'.format(self.threads)) + t = ClientProcessor(self, clientSocket) + self.threads.append(t) + t.start() + except Exception as e: + logger.error('Got an exception on Server ipc thread: {}'.format(e)) + + +class ClientIPC(threading.Thread): + def __init__(self, listenPort): + super(ClientIPC, self).__init__() + self.port = listenPort + self.running = False + self.clientSocket = None + self.messages = six.moves.queue.Queue(32) # @UndefinedVariable + + self.connect() + + def stop(self): + self.running = False + + def getMessage(self): + while self.running: + try: + return self.messages.get(timeout=1) + except six.moves.queue.Empty: # @UndefinedVariable + continue + + return None + + def sendRequestMessage(self, msg, data=None): + logger.debug('Sending request for msg: {}({}), {}'.format(msg, REV_DICT.get(msg), data)) + if data is None: + data = b'' + + if isinstance(data, six.text_type): # Convert to bytes if necessary + data = data.encode('utf-8') + + l = len(data) + msg = six.int2byte(msg) + six.int2byte(l & 0xFF) + six.int2byte(l >> 8) + data + self.clientSocket.sendall(msg) + + def sendLogin(self, username): + self.sendRequestMessage(REQ_LOGIN, username) + + def sendLogout(self, username): + 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): + msg = b'' + while self.running and len(msg) < number: + try: + buf = self.clientSocket.recv(number - len(msg)) + if buf == b'': + logger.debug('Buf {}, msg {}({})'.format(buf, msg, REV_DICT.get(msg))) + self.running = False + break + msg += buf + except socket.timeout: + pass + + if self.running is False: + logger.debug('Not running, returning None') + return None + return msg + + def connect(self): + self.clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.clientSocket.connect(('localhost', self.port)) + self.clientSocket.settimeout(2) # Static, custom socket timeout of 2 seconds for local connection (no network) + + def run(self): + self.running = True + + while self.running: + try: + msg = b'' + # We look for magic message header + while self.running: # Wait for MAGIC + try: + buf = self.clientSocket.recv(len(MAGIC) - len(msg)) + if buf == b'': + self.running = False + break + msg += buf + if len(msg) != len(MAGIC): + continue # Do not have message + if msg != MAGIC: # Skip first byte an continue searchong + msg = msg[1:] + continue + break + except socket.timeout: # Timeout is here so we can get stop thread + continue + + if self.running is False: + break + + # Now we get message basic data (msg + datalen) + msg = bytearray(self.receiveBytes(3)) + + # We have the magic header, here comes the message itself + if msg is None: + continue + + msgId = msg[0] + dataLen = msg[1] + (msg[2] << 8) + if msgId not in (MSG_LOGOFF, MSG_MESSAGE, MSG_SCRIPT): + raise Exception('Invalid message id: {}'.format(msgId)) + + data = self.receiveBytes(dataLen) + if data is None: + continue + + self.messages.put((msgId, data)) + self.messageReceived() + + except socket.error as e: + logger.error('Communication with server got an error: {}'.format(toUnicode(e.strerror))) + self.running = False + return + except Exception as e: + tb = traceback.format_exc() + logger.error('Error: {}, trace: {}'.format(e, tb)) + + try: + 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 new file mode 100644 index 0000000..29a238d --- /dev/null +++ b/src/opengnsys/linux/OGAgentService.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 + +from opengnsys.service import CommonService +from opengnsys.service import IPC_PORT +from opengnsys import ipc + +from opengnsys.log import logger + +from opengnsys.linux.daemon import Daemon + +import sys +import signal +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 + def set_proctitle(_): + pass + + +class OGAgentSvc(Daemon, CommonService): + def __init__(self, args=None): + Daemon.__init__(self, '/var/run/opengnsys-agent.pid') + CommonService.__init__(self) + + def run(self): + logger.debug('** Running Daemon **') + set_proctitle('OGAgent') + + self.initialize() + + # Call modules initialization + # They are called in sequence, no threading is done at this point, so ensure modules onActivate always returns + + + # ********************* + # * Main Service loop * + # ********************* + # Counter used to check ip changes only once every 10 seconds, for + # example + try: + while self.isAlive: + # In milliseconds, will break + self.doWait(1000) + except (KeyboardInterrupt, SystemExit) as e: + logger.error('Requested exit of main loop') + except Exception as e: + logger.exception() + logger.error('Caught exception on main loop: {}'.format(e)) + + self.terminate() + + self.notifyStop() + + def signal_handler(self, signal, frame): + self.isAlive = False + sys.stderr.write("signal handler: {}".format(signal)) + + +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') + + if len(sys.argv) == 5 and sys.argv[1] == 'message': + logger.debug('Running client opengnsys') + client = None + try: + client = ipc.ClientIPC(IPC_PORT) + client.sendMessage(sys.argv[2], sys.argv[3], json.loads(sys.argv[4])) + 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') + client = None + try: + client = ipc.ClientIPC(IPC_PORT) + if 'login' == sys.argv[1]: + client.sendLogin(sys.argv[2]) + sys.exit(0) + elif 'logout' == sys.argv[1]: + client.sendLogout(sys.argv[2]) + sys.exit(0) + else: + usage() + except Exception as e: + logger.error(e) + elif len(sys.argv) != 2: + usage() + + logger.debug('Executing actor') + daemon = OGAgentSvc() + + signal.signal(signal.SIGTERM, daemon.signal_handler) + signal.signal(signal.SIGINT, daemon.signal_handler) + + if len(sys.argv) == 2: + if 'start' == sys.argv[1]: + daemon.start() + elif 'stop' == sys.argv[1]: + daemon.stop() + elif 'restart' == sys.argv[1]: + daemon.restart() + elif 'fg' == sys.argv[1]: + daemon.run() + else: + usage() + sys.exit(0) + else: + usage() diff --git a/src/opengnsys/linux/__init__.py b/src/opengnsys/linux/__init__.py new file mode 100644 index 0000000..3a98c78 --- /dev/null +++ b/src/opengnsys/linux/__init__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 diff --git a/src/opengnsys/linux/daemon.py b/src/opengnsys/linux/daemon.py new file mode 100644 index 0000000..26b2b9e --- /dev/null +++ b/src/opengnsys/linux/daemon.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 +from opengnsys.log import logger + +from signal import SIGTERM + + +class Daemon: + """ + A generic daemon class. + + Usage: subclass the Daemon class and override the run() method + """ + def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.pidfile = pidfile + + def daemonize(self): + """ + do the UNIX double-fork magic, see Stevens' "Advanced + Programming in the UNIX Environment" for details (ISBN 0201563177) + http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 + """ + try: + pid = os.fork() + if pid > 0: + # exit first parent + sys.exit(0) + except OSError as e: + logger.error("fork #1 error: {}".format(e)) + sys.stderr.write("fork #1 failed: {}\n".format(e)) + sys.exit(1) + + # decouple from parent environment + os.chdir("/") + os.setsid() + os.umask(0) + + # do second fork + try: + pid = os.fork() + if pid > 0: + # exit from second parent + sys.exit(0) + except OSError as e: + logger.error("fork #2 error: {}".format(e)) + sys.stderr.write("fork #2 failed: {}\n".format(e)) + sys.exit(1) + + # redirect standard file descriptors + sys.stdout.flush() + sys.stderr.flush() + si = open(self.stdin, 'r') + so = open(self.stdout, 'a+') + se = open(self.stderr, 'a+', 0) + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + # write pidfile + atexit.register(self.delpid) + pid = str(os.getpid()) + with open(self.pidfile, 'w+') as f: + f.write("{}\n".format(pid)) + + def delpid(self): + try: + os.remove(self.pidfile) + except Exception: + # Not found/not permissions or whatever... + pass + + def start(self): + """ + Start the daemon + """ + logger.debug('Starting daemon') + # Check for a pidfile to see if the daemon already runs + try: + pf = open(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + + if pid: + message = "pidfile {} already exist. Daemon already running?\n".format(pid) + logger.error(message) + sys.stderr.write(message) + sys.exit(1) + + # Start the daemon + self.daemonize() + try: + self.run() + except Exception as e: + logger.error('Exception running process: {}'.format(e)) + + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + + def stop(self): + """ + Stop the daemon + """ + # Get the pid from the pidfile + try: + pf = open(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + + if pid is None: + message = "pidfile {} does not exist. Daemon not running?\n".format(self.pidfile) + logger.info(message) + # sys.stderr.write(message) + return # not an error in a restart + + # Try killing the daemon process + try: + while True: + os.kill(pid, SIGTERM) + time.sleep(1) + except OSError as err: + if err.errno == 3: # No such process + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + else: + sys.stderr.write(err) + sys.exit(1) + + def restart(self): + """ + Restart the daemon + """ + self.stop() + self.start() + + # Overridables + def run(self): + """ + You should override this method when you subclass Daemon. It will be called after the process has been + daemonized by start() or restart(). + """ diff --git a/src/opengnsys/linux/log.py b/src/opengnsys/linux/log.py new file mode 100644 index 0000000..dc54e19 --- /dev/null +++ b/src/opengnsys/linux/log.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 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 + + +class LocalLogger(object): + def __init__(self): + # tempdir is different for "user application" and "service" + # 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 + + for logDir in ('/var/log', os.path.expanduser('~'), tempfile.gettempdir()): + try: + fname = os.path.join(logDir, 'opengnsys.log') + logging.basicConfig( + filename=fname, + filemode='a', + format='%(levelname)s %(asctime)s %(message)s', + level=logging.DEBUG + ) + self.logger = logging.getLogger('opengnsys') + os.chmod(fname, 0o0600) + return + except Exception: + pass + + # Logger can't be set + self.logger = None + + def log(self, level, message): + # Debug messages are logged to a file + # our loglevels are 10000 (other), 20000 (debug), .... + # logging levels are 10 (debug), 20 (info) + # OTHER = logging.NOTSET + self.logger.log(int(level / 1000) - 10, message) + + def isWindows(self): + return False + + def isLinux(self): + return True diff --git a/src/opengnsys/linux/operations.py b/src/opengnsys/linux/operations.py new file mode 100644 index 0000000..b00c259 --- /dev/null +++ b/src/opengnsys/linux/operations.py @@ -0,0 +1,271 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 socket +import platform +import fcntl +import os +import ctypes # @UnusedImport +import ctypes.util +import subprocess +import struct +import array +import six +from opengnsys import utils +from .renamer import rename + + +def _getMacAddr(ifname): + ''' + Returns the mac address of an interface + Mac is returned as unicode utf-8 encoded + ''' + if isinstance(ifname, list): + return dict([(name, _getMacAddr(name)) for name in ifname]) + if isinstance(ifname, six.text_type): + ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7) + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + info = bytearray(fcntl.ioctl(s.fileno(), 0x8927, struct.pack(str('256s'), ifname[:15]))) + return six.text_type(''.join(['%02x:' % char for char in info[18:24]])[:-1]) + except Exception: + return None + + +def _getIpAddr(ifname): + ''' + Returns the ip address of an interface + Ip is returned as unicode utf-8 encoded + ''' + if isinstance(ifname, list): + return dict([(name, _getIpAddr(name)) for name in ifname]) + if isinstance(ifname, six.text_type): + ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7) + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + return six.text_type(socket.inet_ntoa(fcntl.ioctl( + s.fileno(), + 0x8915, # SIOCGIFADDR + struct.pack(str('256s'), ifname[:15]) + )[20:24])) + except Exception: + return None + + +def _getInterfaces(): + ''' + Returns a list of interfaces names coded in utf-8 + ''' + max_possible = 128 # arbitrary. raise if needed. + space = max_possible * 16 + if platform.architecture()[0] == '32bit': + offset, length = 32, 32 + elif platform.architecture()[0] == '64bit': + offset, length = 16, 40 + else: + raise OSError('Unknown arquitecture {0}'.format(platform.architecture()[0])) + + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + names = array.array(str('B'), b'\0' * space) + outbytes = struct.unpack(str('iL'), fcntl.ioctl( + s.fileno(), + 0x8912, # SIOCGIFCONF + struct.pack(str('iL'), space, names.buffer_info()[0]) + ))[0] + namestr = names.tostring() + # return namestr, outbytes + return [namestr[i:i + offset].split(b'\0', 1)[0].decode('utf-8') for i in range(0, outbytes, length)] + + +def _getIpAndMac(ifname): + ip, mac = _getIpAddr(ifname), _getMacAddr(ifname) + return (ip, mac) + + +def getComputerName(): + ''' + Returns computer name, with no domain + ''' + return socket.gethostname().split('.')[0] + + +def getNetworkInfo(): + ''' + Obtains a list of network interfaces + @return: A "generator" of elements, that are dict-as-object, with this elements: + name: Name of the interface + mac: mac of the interface + ip: ip of the interface + ''' + for ifname in _getInterfaces(): + ip, mac = _getIpAndMac(ifname) + if mac != '00:00:00:00:00:00': # Skips local interfaces + yield utils.Bunch(name=ifname, mac=mac, ip=ip) + + +def getDomainName(): + return '' + + +def getLinuxVersion(): + lv = platform.linux_distribution() + return lv[0] + ', ' + lv[1] + + +def reboot(flags=0): + ''' + Simple reboot using os command + ''' + # Workaround for dummy thread + if six.PY3 is False: + import threading + threading._DummyThread._Thread__stop = lambda x: 42 + + # Check for OpenGnsys Client or GNU/Linux distribution. + if os.path.exists('/scripts/oginit'): + subprocess.call('source /opt/opengnsys/etc/preinit/loadenviron.sh; /opt/opengnsys/scripts/reboot', shell=True) + else: + subprocess.call(['/sbin/reboot']) + +def poweroff(flags=0): + ''' + Simple poweroff using os command + ''' + # Workaround for dummy thread + if six.PY3 is False: + import threading + threading._DummyThread._Thread__stop = lambda x: 42 + + # Check for OpenGnsys Client or GNU/Linux distribution. + if os.path.exists('/scripts/oginit'): + subprocess.call('source /opt/opengnsys/etc/preinit/loadenviron.sh; /opt/opengnsys/scripts/poweroff', shell=True) + else: + subprocess.call(['/sbin/poweroff']) + + + +def logoff(): + ''' + Kills all curent user processes, which must send a logogof + caveat: If the user has other sessions, will also disconnect from them + ''' + # Workaround for dummy thread + if six.PY3 is False: + import threading + threading._DummyThread._Thread__stop = lambda x: 42 + + subprocess.call(['/usr/bin/pkill', '-u', os.environ['USER']]) + + +def renameComputer(newName): + rename(newName) + + +def joinDomain(domain, ou, account, password, executeInOneStep=False): + pass + + +def changeUserPassword(user, oldPassword, newPassword): + ''' + Simple password change for user using command line + ''' + os.system('echo "{1}\n{1}" | /usr/bin/passwd {0} 2> /dev/null'.format(user, newPassword)) + + +class XScreenSaverInfo(ctypes.Structure): + _fields_ = [('window', ctypes.c_long), + ('state', ctypes.c_int), + ('kind', ctypes.c_int), + ('til_or_since', ctypes.c_ulong), + ('idle', ctypes.c_ulong), + ('eventMask', ctypes.c_ulong)] + +# Initialize xlib & xss +try: + xlibPath = ctypes.util.find_library('X11') + xssPath = ctypes.util.find_library('Xss') + xlib = ctypes.cdll.LoadLibrary(xlibPath) + xss = ctypes.cdll.LoadLibrary(xssPath) + + # Fix result type to XScreenSaverInfo Structure + xss.XScreenSaverQueryExtension.restype = ctypes.c_int + xss.XScreenSaverAllocInfo.restype = ctypes.POINTER(XScreenSaverInfo) # Result in a XScreenSaverInfo structure +except Exception: # Libraries not accesible, not found or whatever.. + xlib = xss = None + + +def initIdleDuration(atLeastSeconds): + ''' + On linux we set the screensaver to at least required seconds, or we never will get "idle" + ''' + # Workaround for dummy thread + if six.PY3 is False: + import threading + threading._DummyThread._Thread__stop = lambda x: 42 + + subprocess.call(['/usr/bin/xset', 's', '{}'.format(atLeastSeconds + 30)]) + # And now reset it + subprocess.call(['/usr/bin/xset', 's', 'reset']) + + +def getIdleDuration(): + ''' + Returns idle duration, in seconds + ''' + if xlib is None or xss is None: + return 0 # Libraries not available + + # production code might want to not hardcode the offset 16... + display = xlib.XOpenDisplay(None) + + event_base = ctypes.c_int() + error_base = ctypes.c_int() + + available = xss.XScreenSaverQueryExtension(display, ctypes.byref(event_base), ctypes.byref(error_base)) + if available != 1: + return 0 # No screen saver is available, no way of getting idle + + info = xss.XScreenSaverAllocInfo() + xss.XScreenSaverQueryInfo(display, xlib.XDefaultRootWindow(display), info) + + if info.contents.state != 0: + return 3600 * 100 * 1000 # If screen saver is active, return a high enough value + + return info.contents.idle / 1000.0 + + +def getCurrentUser(): + ''' + Returns current logged in user + ''' + return os.environ['USER'] diff --git a/src/opengnsys/linux/renamer/__init__.py b/src/opengnsys/linux/renamer/__init__.py new file mode 100644 index 0000000..27198dc --- /dev/null +++ b/src/opengnsys/linux/renamer/__init__.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 platform +import os +import sys +import pkgutil + +from opengnsys.log import logger + +renamers = {} + + +# Renamers now are for IPv4 only addresses +def rename(newName): + distribution = platform.linux_distribution()[0].lower().strip() + if distribution in renamers: + return renamers[distribution](newName) + + # Try Debian renamer, simplest one + logger.info('Renamer for platform "{0}" not found, tryin debian renamer'.format(distribution)) + return renamers['debian'](newName) + + +# Do load of packages +def _init(): + pkgpath = os.path.dirname(sys.modules[__name__].__file__) + for _, name, _ in pkgutil.iter_modules([pkgpath]): + __import__(__name__ + '.' + name, globals(), locals()) + +_init() \ No newline at end of file diff --git a/src/opengnsys/linux/renamer/debian.py b/src/opengnsys/linux/renamer/debian.py new file mode 100644 index 0000000..be55e00 --- /dev/null +++ b/src/opengnsys/linux/renamer/debian.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 + +from opengnsys.linux.renamer import renamers +from opengnsys.log import logger + +import os + + +def rename(newName): + ''' + Debian renamer + Expects new host name on newName + Host does not needs to be rebooted after renaming + ''' + logger.debug('using Debian renamer') + + with open('/etc/hostname', 'w') as hostname: + hostname.write(newName) + + # Force system new name + os.system('/bin/hostname %s' % newName) + + # add name to "hosts" + with open('/etc/hosts', 'r') as hosts: + lines = hosts.readlines() + with open('/etc/hosts', 'w') as hosts: + hosts.write("127.0.1.1\t%s\n" % newName) + for l in lines: + if l[:9] == '127.0.1.1': # Skips existing 127.0.1.1. if it already exists + continue + hosts.write(l) + + return True + +# All names in lower case +renamers['debian'] = rename +renamers['ubuntu'] = rename diff --git a/src/opengnsys/linux/renamer/opensuse.py b/src/opengnsys/linux/renamer/opensuse.py new file mode 100644 index 0000000..a2d29a5 --- /dev/null +++ b/src/opengnsys/linux/renamer/opensuse.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 + +from opengnsys.linux.renamer import renamers +from opengnsys.log import logger + +import os + + +def rename(newName): + ''' + RH, Centos, Fedora Renamer + Expects new host name on newName + Host does not needs to be rebooted after renaming + ''' + logger.debug('using SUSE renamer') + + with open('/etc/hostname', 'w') as hostname: + hostname.write(newName) + + # Force system new name + os.system('/bin/hostname %s' % newName) + + # add name to "hosts" + with open('/etc/hosts', 'r') as hosts: + lines = hosts.readlines() + with open('/etc/hosts', 'w') as hosts: + hosts.write("127.0.1.1\t{}\n".format(newName)) + for l in lines: + if l[:9] != '127.0.1.1': # Skips existing 127.0.1.1. if it already exists + hosts.write(l) + + return True + +# All names in lower case +renamers['opensuse'] = rename +renamers['suse'] = rename diff --git a/src/opengnsys/linux/renamer/redhat.py b/src/opengnsys/linux/renamer/redhat.py new file mode 100644 index 0000000..8821a81 --- /dev/null +++ b/src/opengnsys/linux/renamer/redhat.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 + +from opengnsys.linux.renamer import renamers +from opengnsys.log import logger + +import os + + +def rename(newName): + ''' + RH, Centos, Fedora Renamer + Expects new host name on newName + Host does not needs to be rebooted after renaming + ''' + logger.debug('using RH renamer') + + with open('/etc/hostname', 'w') as hostname: + hostname.write(newName) + + # Force system new name + os.system('/bin/hostname %s' % newName) + + # add name to "hosts" + with open('/etc/hosts', 'r') as hosts: + lines = hosts.readlines() + with open('/etc/hosts', 'w') as hosts: + hosts.write("127.0.1.1\t{}\n".format(newName)) + for l in lines: + if l[:9] != '127.0.1.1': # Skips existing 127.0.1.1. if it already exists + hosts.write(l) + + with open('/etc/sysconfig/network', 'r') as net: + lines = net.readlines() + with open('/etc/sysconfig/network', 'w') as net: + net.write('HOSTNAME={}\n'.format(newName)) + for l in lines: + if l[:8] != 'HOSTNAME': + net.write(l) + + return True + +# All names in lower case +renamers['centos linux'] = rename +renamers['fedora'] = rename diff --git a/src/opengnsys/loader.py b/src/opengnsys/loader.py new file mode 100644 index 0000000..23988fc --- /dev/null +++ b/src/opengnsys/loader.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 + +# 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 + +from opengnsys.workers import ServerWorker +from opengnsys.workers import ClientWorker +from .log import logger + + +def loadModules(controller, client=False): + ''' + Load own provided modules plus the modules that are in the configuration path. + The loading order is not defined (they are loaded as found, because modules MUST be "standalone" modules + @param service: The service that: + * Holds the configuration + * Will be used to initialize modules. + ''' + + ogModules = [] + + if client is False: + from opengnsys.modules.server import OpenGnSys # @UnusedImport + from .modules import server # @UnusedImport, just used to ensure opengnsys modules are initialized + modPath = 'opengnsys.modules.server' + modType = ServerWorker + else: + from opengnsys.modules.client import OpenGnSys # @UnusedImport @Reimport + from .modules import client # @UnusedImport, just used to ensure opengnsys modules are initialized + modPath = 'opengnsys.modules.client' + modType = ClientWorker + + def addCls(cls): + logger.debug('Found module class {}'.format(cls)) + try: + if cls.name is None: + # Error, cls has no name + # Log the issue and + logger.error('Class {} has no name attribute'.format(cls)) + return + ogModules.append(cls(controller)) + except Exception as e: + logger.error('Error loading module {}'.format(e)) + + def recursiveAdd(p): + subcls = p.__subclasses__() + + if len(subcls) == 0: + addCls(p) + else: + for c in subcls: + recursiveAdd(c) + + def doLoad(paths): + for (module_loader, name, ispkg) in pkgutil.iter_modules(paths, modPath + '.'): + if ispkg: + logger.debug('Found module package {}'.format(name)) + module_loader.find_module(name).load_module(name) + + + if controller.config.has_option('opengnsys', 'path') is True: + paths = tuple(os.path.abspath(v) for v in controller.config.get('opengnsys', 'path').split(',')) + else: + paths = () + + # paths += (os.path.dirname(sys.modules[modPath].__file__),) + + logger.debug('Loading modules from {}'.format(paths)) + + # Load modules + doLoad(paths) + + # Add to list of available modules + recursiveAdd(modType) + + return ogModules diff --git a/src/opengnsys/log.py b/src/opengnsys/log.py new file mode 100644 index 0000000..e34c087 --- /dev/null +++ b/src/opengnsys/log.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 traceback +import sys +import six + +if sys.platform == 'win32': + from opengnsys.windows.log import LocalLogger # @UnusedImport +else: + from opengnsys.linux.log import LocalLogger # @Reimport + +# 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 + +_levelName = { + 'OTHER': OTHER, + 'DEBUG': DEBUG, + 'INFO': INFO, + 'WARN': WARN, + 'ERROR': ERROR, + '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): + level = _levelName.get(level, INFO) + + self.logLevel = level # Ensures level is an integer or fails + + def log(self, level, message): + if level < self.logLevel: # Skip not wanted messages + return + + self.logger.log(level, message) + + def debug(self, message): + self.log(DEBUG, message) + + def warn(self, message): + self.log(WARN, message) + + def info(self, message): + self.log(INFO, message) + + def error(self, message): + self.log(ERROR, message) + + def fatal(self, message): + self.log(FATAL, message) + + def exception(self): + try: + tb = traceback.format_exc() + except Exception: + tb = '(could not get traceback!)' + + self.log(DEBUG, tb) + + def flush(self): + pass + + +logger = Logger() diff --git a/src/opengnsys/modules/__init__.py b/src/opengnsys/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/opengnsys/modules/client/OpenGnSys/__init__.py b/src/opengnsys/modules/client/OpenGnSys/__init__.py new file mode 100644 index 0000000..6e287be --- /dev/null +++ b/src/opengnsys/modules/client/OpenGnSys/__init__.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 + +from opengnsys.workers import ClientWorker + +from opengnsys import operations +from opengnsys.log import logger + +class OpenGnSysWorker(ClientWorker): + name = 'opengnsys' + + def onActivation(self): + logger.debug('Activate invoked') + + def onDeactivation(self): + logger.debug('Deactivate invoked') + + # Processes message "doit" (sample) + def process_doit(self, jsonParams): + logger.debug('Processed message doit with params {}'.format(jsonParams)) + self.sendServerMessage('doit', {'data':1}) + + def process_logoff(self, jsonParams): + logger.debug('Processed logoff message with params {}'.format(jsonParams)) + operations.logoff() + diff --git a/src/opengnsys/modules/client/__init__.py b/src/opengnsys/modules/client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/opengnsys/modules/server/OpenGnSys/__init__.py b/src/opengnsys/modules/server/OpenGnSys/__init__.py new file mode 100644 index 0000000..f6f32e3 --- /dev/null +++ b/src/opengnsys/modules/server/OpenGnSys/__init__.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 + +from opengnsys.workers import ServerWorker + +from opengnsys import REST, RESTError +from opengnsys import operations +from opengnsys.log import logger +from opengnsys.scriptThread import ScriptExecutorThread + +import subprocess +import threading +import thread +import os +import platform +import time + +# Error handler decorator. +def catchBackgroundError(fnc): + def wrapper(*args, **kwargs): + this = args[0] + try: + fnc(*args, **kwargs) + except Exception as e: + this.REST.sendMessage('error?id={}'.format(kwargs.get('requestId', 'error')), {'error': '{}'.format(e)}) + return wrapper + +class OpenGnSysWorker(ServerWorker): + name = 'opengnsys' + interface = None # Binded interface for OpenGnsys + loggedin = False # + locked = {} + + def onActivation(self): + self.cmd = None + # Ensure cfg has required configuration variables or an exception will be thrown + + self.REST = REST(self.service.config.get('opengnsys', 'remote')) + + # Get network interfaces + self.interface = list(operations.getNetworkInfo())[0] # Get first network interface + + # Send an initialize message + #self.REST.sendMessage('initialize/{}/{}'.format(self.interface.mac, self.interface.ip)) + + # Send an POST message + self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip}) + + def onDeactivation(self): + #self.REST.sendMessage('deinitialize/{}/{}'.format(self.interface.mac, self.interface.ip)) + logger.debug('onDeactivation') + self.REST.sendMessage('ogagent/stopped', {'mac': self.interface.mac, 'ip': self.interface.ip}) + + # Processes message "doit" (sample) + #def process_doit(self, path, getParams, postParams): + # # Send a sample message to client + # logger.debug('Processing doit') + # self.sendClientMessage('doit', {'param1': 'test', 'param2': 'test2'}) + # return 'Processed message for {}, {}, {}'.format(path, getParams, postParams) + + def process_script(self, path, getParams, postParams): + ''' + Processes an script execution (script is encoded in base64) + ''' + logger.debug('Processing script request') + script = postParams.get('script') + if postParams.get('client', 'false') == 'false': + thr = ScriptExecutorThread(script=script.decode('base64')) + thr.start() + else: + self.sendScriptMessage(script) + + return 'ok' + + def processClientMessage(self, message, data): + logger.debug('Got OpenGnsys message from client: {}, data {}'.format(message, data)) + + def process_client_doit(self, params): + self.REST.sendMessage('doit_done', params) + + def onLogin(self, user): + logger.debug('Received login for {}'.format(user)) + self.loggedin = True + self.REST.sendMessage('ogagent/loggedin', {'ip': self.interface.ip, "user": user}) + + def onLogout(self, user): + logger.debug('Received logout for {}'.format(user)) + self.loggedin = False + self.REST.sendMessage('ogagent/loggedout', {'ip': self.interface.ip, "user": user}) + + def process_ogclient(self, path, getParams, postParams): + ''' + 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 "mazinger", 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 len(path) == 0: + return "ok" + try: + operation = getattr(self, 'ogclient_' + path[0]) + except Exception: + raise Exception('Message processor for "{}" not found'.format(path[0])) + + return operation(path[1:], getParams, postParams) + + ###### EN PRUEBAS ###### + def process_status(self, path, getParams, postParams): + ''' + Returns client status. + ''' + res = { 'status': '', 'loggedin': self.loggedin } + 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' + return res + + def process_reboot(self, path, getParams, postParams): + ''' + Launches a system reboot operation. + ''' + logger.debug('Received reboot operation') + def rebt(): + operations.reboot() + threading.Thread(target=rebt).start() + return {'op': 'launched'} + + def process_poweroff(self, path, getParams, postParams): + ''' + Launches a system power off operation. + ''' + logger.debug('Received poweroff operation') + def pwoff(): + time.sleep(2) + operations.poweroff() + threading.Thread(target=pwoff).start() + return {'op': 'launched'} + + def process_logoff(self, path, getParams, postParams): + ''' + Closes user session. + ''' + logger.debug('Received logoff operation') + self.sendClientMessage('logoff', {}) + return 'Logoff operation was sended to client' + diff --git a/src/opengnsys/modules/server/__init__.py b/src/opengnsys/modules/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/opengnsys/operations.py b/src/opengnsys/operations.py new file mode 100644 index 0000000..fc241ab --- /dev/null +++ b/src/opengnsys/operations.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 + +import sys +if sys.platform == 'win32': + from .windows.operations import * # @UnusedWildImport +else: + from .linux.operations import * # @UnusedWildImport diff --git a/src/opengnsys/scriptThread.py b/src/opengnsys/scriptThread.py new file mode 100644 index 0000000..2a6779b --- /dev/null +++ b/src/opengnsys/scriptThread.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 201 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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-msg=E1101,W0703 + +from opengnsys.log import logger + +import threading +import six + + +class ScriptExecutorThread(threading.Thread): + def __init__(self, script): + super(ScriptExecutorThread, self).__init__() + self.script = script + + def run(self): + try: + logger.debug('Executing script: {}'.format(self.script)) + six.exec_(self.script, globals(), None) + except Exception as e: + logger.error('Error executing script: {}'.format(e)) diff --git a/src/opengnsys/service.py b/src/opengnsys/service.py new file mode 100644 index 0000000..b937f21 --- /dev/null +++ b/src/opengnsys/service.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 + +from .log import logger +from .config import readConfig +from .utils import exceptionToMessage + +from . import ipc +from . import httpserver +from .loader import loadModules + +import socket +import time +import json +import six + +IPC_PORT = 10398 + + +class CommonService(object): + isAlive = True + ipc = None + httpServer = None + modules = None + + def __init__(self): + logger.info('----------------------------------------') + logger.info('Initializing OpenGnsys Agent') + + # Read configuration file before proceding & ensures minimal config is there + + self.config = readConfig() + + # 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 + + self.address = (cfg.get('address', '0.0.0.0'), int(cfg.get('port', '10997'))) + self.ipcport = int(cfg.get('ipc_port', IPC_PORT)) + + self.timeout = int(cfg.get('timeout', '20')) + + logger.debug('Socket timeout: {}'.format(self.timeout)) + socket.setdefaulttimeout(self.timeout) + + # Now load modules + self.modules = loadModules(self) + 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): + for v in self.modules: + try: + logger.debug('Notifying login of user {} to module {}'.format(username, v.name)) + v.onLogin(username) + except Exception as e: + logger.error('Got exception {} processing login message on {}'.format(e, v.name)) + + def notifyLogout(self, username): + for v in self.modules: + try: + logger.debug('Notifying logout of user {} to module {}'.format(username, v.name)) + v.onLogout(username) + except Exception as e: + logger.error('Got exception {} processing logout message on {}'.format(e, v.name)) + + def notifyMessage(self, data): + module, message, data = data.split('\0') + for v in self.modules: + if v.name == module: # Case Sensitive!!!! + try: + logger.debug('Notifying message {} to module {} with json data {}'.format(message, v.name, data)) + v.processClientMessage(message, json.loads(data)) + return + except Exception as e: + 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: + self.notifyLogin(data) + elif msg == ipc.REQ_LOGOUT: + self.notifyLogout(data) + elif msg == ipc.REQ_MESSAGE: + self.notifyMessage(data) + + def initialize(self): + # ****************************************** + # * Initialize listeners, modules, etc... + # ****************************************** + + if six.PY3 is False: + import threading + threading._DummyThread._Thread__stop = lambda x: 42 + + logger.debug('Starting IPC listener at {}'.format(IPC_PORT)) + self.ipc = ipc.ServerIPC(self.ipcport, clientMessageProcessor=self.clientMessageProcessor) + self.ipc.start() + + # And http threaded server + self.httpServer = httpserver.HTTPServerThread(self.address, self) + self.httpServer.start() + + # And lastly invoke modules activation + validMods = [] + for mod in self.modules: + try: + logger.debug('Activating module {}'.format(mod.name)) + mod.activate() + validMods.append(mod) + except Exception as e: + logger.exception() + logger.error("Activation of {} failed: {}".format(mod.name, exceptionToMessage(e))) + + self.modules[:] = validMods # copy instead of assignment + + logger.debug('Modules after activation: {}'.format(list(v.name for v in self.modules))) + + def terminate(self): + # First invoke deactivate on modules + for mod in reversed(self.modules): + try: + logger.debug('Deactivating module {}'.format(mod.name)) + mod.deactivate() + except Exception as e: + logger.exception() + logger.error("Deactivation of {} failed: {}".format(mod.name, exceptionToMessage(e))) + + # Remove IPC threads + if self.ipc is not None: + try: + self.ipc.stop() + except Exception: + logger.error('Couln\'t stop ipc server') + + if self.httpServer is not None: + try: + self.httpServer.stop() + except Exception: + logger.error('Couln\'t stop RESTApi server') + + self.notifyStop() + + # **************************************** + # 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 + ''' + 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() diff --git a/src/opengnsys/utils.py b/src/opengnsys/utils.py new file mode 100644 index 0000000..9480a6a --- /dev/null +++ b/src/opengnsys/utils.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 sys +import six + +if sys.platform == 'win32': + _fromEncoding = 'windows-1250' +else: + _fromEncoding = 'utf-8' + + +def toUnicode(msg): + try: + if not isinstance(msg, six.text_type): + if isinstance(msg, six.binary_type): + return msg.decode(_fromEncoding, 'ignore') + return six.text_type(msg) + else: + return msg + except Exception: + try: + return six.text_type(msg) + except Exception: + return '' + + +def exceptionToMessage(e): + msg = '' + for arg in e.args: + if isinstance(arg, Exception): + msg = msg + exceptionToMessage(arg) + else: + msg = msg + toUnicode(arg) + '. ' + return msg + + +class Bunch(dict): + def __init__(self, **kw): + dict.__init__(self, kw) + self.__dict__ = self + diff --git a/src/opengnsys/windows/OGAgentService.py b/src/opengnsys/windows/OGAgentService.py new file mode 100644 index 0000000..2873607 --- /dev/null +++ b/src/opengnsys/windows/OGAgentService.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 +# pylint: disable=unused-wildcard-import, wildcard-import + +import win32serviceutil # @UnresolvedImport, pylint: disable=import-error +import win32service # @UnresolvedImport, pylint: disable=import-error +import win32security # @UnresolvedImport, pylint: disable=import-error +import win32net # @UnresolvedImport, pylint: disable=import-error +import win32event # @UnresolvedImport, pylint: disable=import-error +import win32com.client # @UnresolvedImport, @UnusedImport, pylint: disable=import-error +import pythoncom # @UnresolvedImport, pylint: disable=import-error +import servicemanager # @UnresolvedImport, pylint: disable=import-error +import os + +from opengnsys import operations +from opengnsys.service import CommonService + +from opengnsys.log import logger + +class OGAgentSvc(win32serviceutil.ServiceFramework, CommonService): + ''' + This class represents a Windows Service for managing actor interactions + with UDS Broker and Machine + ''' + _svc_name_ = "OGAgent" + _svc_display_name_ = "OpenGnSys Agent Service" + _svc_description_ = "OpenGnSys Agent for machines" + # 'System Event Notification' is the SENS service + _svc_deps_ = ['EventLog'] + + def __init__(self, args): + win32serviceutil.ServiceFramework.__init__(self, args) + CommonService.__init__(self) + self.hWaitStop = win32event.CreateEvent(None, 1, 0, None) + self._user = None + + def SvcStop(self): + self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) + self.isAlive = False + win32event.SetEvent(self.hWaitStop) + + SvcShutdown = SvcStop + + def notifyStop(self): + servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, + servicemanager.PYS_SERVICE_STOPPED, + (self._svc_name_, '')) + + def doWait(self, miliseconds): + win32event.WaitForSingleObject(self.hWaitStop, miliseconds) + + def SvcDoRun(self): + ''' + Main service loop + ''' + try: + logger.debug('running SvcDoRun') + servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, + servicemanager.PYS_SERVICE_STARTED, + (self._svc_name_, '')) + + # call the CoInitialize to allow the registration to run in an other + # thread + logger.debug('Initializing com...') + pythoncom.CoInitialize() + + # Initialize remaining service data + self.initialize() + except Exception: # Any init exception wil be caught, service must be then restarted + logger.exception() + logger.debug('Exiting service with failure status') + os._exit(-1) # pylint: disable=protected-access + + # ********************* + # * Main Service loop * + # ********************* + try: + while self.isAlive: + # Pumps & processes any waiting messages + pythoncom.PumpWaitingMessages() + win32event.WaitForSingleObject(self.hWaitStop, 1000) + except Exception as e: + logger.error('Caught exception on main loop: {}'.format(e)) + + logger.debug('Exited main loop, deregistering SENS') + + self.terminate() # Ends IPC servers + + self.notifyStop() + + +if __name__ == '__main__': + + win32serviceutil.HandleCommandLine(OGAgentSvc) diff --git a/src/opengnsys/windows/__init__.py b/src/opengnsys/windows/__init__.py new file mode 100644 index 0000000..e662942 --- /dev/null +++ b/src/opengnsys/windows/__init__.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 os +import sys + +# Change to application directory. +os.chdir(os.path.dirname(sys.argv[0])) + diff --git a/src/opengnsys/windows/log.py b/src/opengnsys/windows/log.py new file mode 100644 index 0000000..79ca877 --- /dev/null +++ b/src/opengnsys/windows/log.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 servicemanager # @UnresolvedImport, pylint: disable=import-error +import logging +import os +import tempfile + +# Valid logging levels, from UDS Broker (uds.core.utils.log) +OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in xrange(6)) + + +class LocalLogger(object): + def __init__(self): + # tempdir is different for "user application" and "service" + # service wil get c:\windows\temp, while user will get c:\users\XXX\temp + logging.basicConfig( + filename=os.path.join(tempfile.gettempdir(), 'opengnsys.log'), + filemode='a', + format='%(levelname)s %(asctime)s %(message)s', + level=logging.DEBUG + ) + self.logger = logging.getLogger('opengnsys') + self.serviceLogger = False + + def log(self, level, message): + # Debug messages are logged to a file + # our loglevels are 10000 (other), 20000 (debug), .... + # logging levels are 10 (debug), 20 (info) + # OTHER = logging.NOTSET + self.logger.log(level / 1000 - 10, message) + + if level < INFO or self.serviceLogger is False: # Only information and above will be on event log + return + + if level < WARN: # Info + servicemanager.LogInfoMsg(message) + elif level < ERROR: # WARN + servicemanager.LogWarningMsg(message) + else: # Error & Fatal + servicemanager.LogErrorMsg(message) + + def isWindows(self): + return True + + def isLinux(self): + return False diff --git a/src/opengnsys/windows/operations.py b/src/opengnsys/windows/operations.py new file mode 100644 index 0000000..7cfea99 --- /dev/null +++ b/src/opengnsys/windows/operations.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 win32com.client # @UnresolvedImport, pylint: disable=import-error +import win32net # @UnresolvedImport, pylint: disable=import-error +import win32security # @UnresolvedImport, pylint: disable=import-error +import win32api # @UnresolvedImport, pylint: disable=import-error +import win32con # @UnresolvedImport, pylint: disable=import-error +import ctypes +from ctypes.wintypes import DWORD, LPCWSTR +import os + +from opengnsys import utils +from opengnsys.log import logger + + +def getErrorMessage(res=0): + msg = win32api.FormatMessage(res) + return msg.decode('windows-1250', 'ignore') + + +def getComputerName(): + return win32api.GetComputerNameEx(win32con.ComputerNamePhysicalDnsHostname) + + +def getNetworkInfo(): + ''' + Obtains a list of network interfaces + @return: A "generator" of elements, that are dict-as-object, with this elements: + name: Name of the interface + mac: mac of the interface + ip: ip of the interface + ''' + obj = win32com.client.Dispatch("WbemScripting.SWbemLocator") + wmobj = obj.ConnectServer("localhost", "root\cimv2") + adapters = wmobj.ExecQuery("Select * from Win32_NetworkAdapterConfiguration where IpEnabled=True") + try: + for obj in adapters: + for ip in obj.IPAddress: + if ':' in ip: # Is IPV6, skip this + continue + if ip is None or ip == '' or ip.startswith('169.254') or ip.startswith('0.'): # If single link ip, or no ip + continue + # logger.debug('Net config found: {}=({}, {})'.format(obj.Caption, obj.MACAddress, ip)) + yield utils.Bunch(name=obj.Caption, mac=obj.MACAddress, ip=ip) + except Exception: + return + + +def getDomainName(): + ''' + Will return the domain name if we belong a domain, else None + (if part of a network group, will also return None) + ''' + # Status: + # 0 = Unknown + # 1 = Unjoined + # 2 = Workgroup + # 3 = Domain + domain, status = win32net.NetGetJoinInformation() + if status != 3: + domain = None + + return domain + + +def getWindowsVersion(): + return win32api.GetVersionEx() + +EWX_LOGOFF = 0x00000000 +EWX_SHUTDOWN = 0x00000001 +EWX_REBOOT = 0x00000002 +EWX_FORCE = 0x00000004 +EWX_POWEROFF = 0x00000008 +EWX_FORCEIFHUNG = 0x00000010 + + +def reboot(flags=EWX_FORCEIFHUNG | EWX_REBOOT): + hproc = win32api.GetCurrentProcess() + htok = win32security.OpenProcessToken(hproc, win32security.TOKEN_ADJUST_PRIVILEGES | win32security.TOKEN_QUERY) + privs = ((win32security.LookupPrivilegeValue(None, win32security.SE_SHUTDOWN_NAME), win32security.SE_PRIVILEGE_ENABLED),) + win32security.AdjustTokenPrivileges(htok, 0, privs) + win32api.ExitWindowsEx(flags, 0) + +def poweroff(flags=0): + ''' + Simple poweroff command. + ''' + reboot(flags=EWX_FORCEIFHUNG | EWX_POWEROFF) + +def logoff(): + win32api.ExitWindowsEx(EWX_LOGOFF) + + +def renameComputer(newName): + # Needs admin privileges to work + if ctypes.windll.kernel32.SetComputerNameExW(DWORD(win32con.ComputerNamePhysicalDnsHostname), LPCWSTR(newName)) == 0: # @UndefinedVariable + # win32api.FormatMessage -> returns error string + # win32api.GetLastError -> returns error code + # (just put this comment here to remember to log this when logger is available) + error = getErrorMessage() + computerName = win32api.GetComputerNameEx(win32con.ComputerNamePhysicalDnsHostname) + raise Exception('Error renaming computer from {} to {}: {}'.format(computerName, newName, error)) + +NETSETUP_JOIN_DOMAIN = 0x00000001 +NETSETUP_ACCT_CREATE = 0x00000002 +NETSETUP_ACCT_DELETE = 0x00000004 +NETSETUP_WIN9X_UPGRADE = 0x00000010 +NETSETUP_DOMAIN_JOIN_IF_JOINED = 0x00000020 +NETSETUP_JOIN_UNSECURE = 0x00000040 +NETSETUP_MACHINE_PWD_PASSED = 0x00000080 +NETSETUP_JOIN_WITH_NEW_NAME = 0x00000400 +NETSETUP_DEFER_SPN_SET = 0x1000000 + + +def joinDomain(domain, ou, account, password, executeInOneStep=False): + ''' + Joins machine to a windows domain + :param domain: Domain to join to + :param ou: Ou that will hold machine + :param account: Account used to join domain + :param password: Password of account used to join domain + :param executeInOneStep: If true, means that this machine has been renamed and wants to add NETSETUP_JOIN_WITH_NEW_NAME to request so we can do rename/join in one step. + ''' + # If account do not have domain, include it + if '@' not in account and '\\' not in account: + if '.' in domain: + account = account + '@' + domain + else: + account = domain + '\\' + account + + # Do log + flags = NETSETUP_ACCT_CREATE | NETSETUP_DOMAIN_JOIN_IF_JOINED | NETSETUP_JOIN_DOMAIN + + if executeInOneStep: + flags |= NETSETUP_JOIN_WITH_NEW_NAME + + flags = DWORD(flags) + + domain = LPCWSTR(domain) + + # Must be in format "ou=.., ..., dc=...," + ou = LPCWSTR(ou) if ou is not None and ou != '' else None + account = LPCWSTR(account) + password = LPCWSTR(password) + + res = ctypes.windll.netapi32.NetJoinDomain(None, domain, ou, account, password, flags) + # Machine found in another ou, use it and warn this on log + if res == 2224: + flags = DWORD(NETSETUP_DOMAIN_JOIN_IF_JOINED | NETSETUP_JOIN_DOMAIN) + res = ctypes.windll.netapi32.NetJoinDomain(None, domain, None, account, password, flags) + if res != 0: + # Log the error + error = getErrorMessage(res) + if res == 1355: + error = "DC Is not reachable" + print res, error + raise Exception('Error joining domain {}, with credentials {}/*****{}: {}, {}'.format(domain.value, account.value, ', under OU {}'.format(ou.value) if ou.value is not None else '', res, error)) + + +def changeUserPassword(user, oldPassword, newPassword): + computerName = LPCWSTR(getComputerName()) + user = LPCWSTR(user) + oldPassword = LPCWSTR(oldPassword) + newPassword = LPCWSTR(newPassword) + + res = ctypes.windll.netapi32.NetUserChangePassword(computerName, user, oldPassword, newPassword) + + if res != 0: + # Log the error, and raise exception to parent + error = getErrorMessage() + raise Exception('Error changing password for user {}: {}'.format(user.value, error)) + + +class LASTINPUTINFO(ctypes.Structure): + _fields_ = [ + ('cbSize', ctypes.c_uint), + ('dwTime', ctypes.c_uint), + ] + + +def initIdleDuration(atLeastSeconds): + ''' + In windows, there is no need to set screensaver + ''' + pass + + +def getIdleDuration(): + lastInputInfo = LASTINPUTINFO() + lastInputInfo.cbSize = ctypes.sizeof(lastInputInfo) + ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lastInputInfo)) + millis = ctypes.windll.kernel32.GetTickCount() - lastInputInfo.dwTime # @UndefinedVariable + return millis / 1000.0 + + +def getCurrentUser(): + ''' + Returns current logged in username + ''' + return os.environ['USERNAME'] diff --git a/src/opengnsys/workers/__init__.py b/src/opengnsys/workers/__init__.py new file mode 100644 index 0000000..f2bcd7d --- /dev/null +++ b/src/opengnsys/workers/__init__.py @@ -0,0 +1,2 @@ +from .server_worker import ServerWorker +from .client_worker import ClientWorker diff --git a/src/opengnsys/workers/client_worker.py b/src/opengnsys/workers/client_worker.py new file mode 100644 index 0000000..6a08380 --- /dev/null +++ b/src/opengnsys/workers/client_worker.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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. + 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 + + def __init__(self, service): + 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): + ''' + This method can be overriden to provide your own message proccessor, or better you can + implement a method that is called "process_" + message and this default processMessage will invoke it + * Example: + We got a message from OGAgent "Mazinger", with json params + module.processMessage("mazinger", jsonParams) + + 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(jsonParams) + + The methods must return data that can be serialized to json (i.e. Ojects are not serializable to json, basic type are) + ''' + try: + operation = getattr(self, 'process_' + message) + except Exception: + raise Exception('Message processor for "{}" not found'.format(message)) + + 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 new file mode 100644 index 0000000..f15144f --- /dev/null +++ b/src/opengnsys/workers/server_worker.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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. + 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 + + def __init__(self, service): + 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): + ''' + 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): + ''' + 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') + + if len(path) == 0: + return self.process(getParams, postParams) + try: + operation = getattr(self, 'process_' + path[0]) + except Exception: + raise Exception('Message processor for "{}" not found'.format(path[0])) + + return operation(path[1:], getParams, postParams) + + + 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: + raise Exception('Message processor for "{}" not found'.format(message)) + + operation(data) + + # raise NotImplementedError('Got a client message but no proccessor is implemented') + + + 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): + self.service.sendScriptMessage(script) + + def sendLogoffMessage(self): + self.service.sendLogoffMessage() diff --git a/src/prototypes/threaded_server.py b/src/prototypes/threaded_server.py new file mode 100644 index 0000000..ed5583b --- /dev/null +++ b/src/prototypes/threaded_server.py @@ -0,0 +1,170 @@ +''' +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 +from six.moves.SimpleHTTPServer import SimpleHTTPRequestHandler # @UnresolvedImport +from six.moves.BaseHTTPServer import HTTPServer # @UnresolvedImport +from six.moves.urllib.parse import unquote # @UnresolvedImport + +import json +import threading +import ssl + +import os.path +import tempfile + +# For testing +# -------------------- +CERTFILE = 'UDSActor.pem' + + +def createSelfSignedCert(force=False): + + certFile = os.path.join(tempfile.gettempdir(), CERTFILE) + + if os.path.exists(certFile) and not force: + return certFile + + certData = '''-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCb50K3mIznNklz +yVAD7xSQOSJQ6+NPXj7U9/4zLZ+TvmbQ7RqUUsxbfxHbeRnoYTWV2nKk4+tHqmvz +ujLSS/loFhTSMqtrLn7rowSYJoQhKOUkAiQlWkqCfItWgL5pJopDpNHFul9Rn3ds +PMWQTiGeUNR4Y3RnBhr1Q1BsqAzf4m6zFUmgLPPmVLdF4uJ3Tuz8TSy2gWLs5aSr +5do4WamwUfYjRSVMJECmwjUM4rQ8SQgg0sHBeBuDUGNBvBQFac1G7qUcMReeu8Zr +DUtMsXma/l4rA8NB5CRmTrQbTBF4l+jb2BDFebDqDUK1Oqs9X35yOQfDOAFYHiix +PX0IsXOZAgMBAAECggEBAJi3000RrIUZUp6Ph0gzPMuCjDEEwWiQA7CPNX1gpb8O +dp0WhkDhUroWIaICYPSXtOwUTtVjRqivMoxPy1Thg3EIoGC/rdeSdlXRHMEGicwJ +yVyalFnatr5Xzg5wkxVh4XMd0zeDt7e3JD7s0QLo5lm1CEzd77qz6lhzFic5/1KX +bzdULtTlq60dazg2hEbcS4OmM1UMCtRVDAsOIUIZPL0M9j1C1d1iEdYnh2xshKeG +/GOfo95xsgdMlGjtv3hUT5ryKVoEsu+36rGb4VfhPfUvvoVbRx5QZpW+QvxaYh5E +Fi0JEROozFwG31Y++8El7J3yQko8cFBa1lYYUwwpNAECgYEAykT+GiM2YxJ4uVF1 +OoKiE9BD53i0IG5j87lGPnWqzEwYBwnqjEKDTou+uzMGz3MDV56UEFNho7wUWh28 +LpEkjJB9QgbsugjxIBr4JoL/rYk036e/6+U8I95lvYWrzb+rBMIkRDYI7kbQD/mQ +piYUpuCkTymNAu2RisK6bBzJslkCgYEAxVE23OQvkCeOV8hJNPZGpJ1mDS+TiOow +oOScMZmZpail181eYbAfMsCr7ri812lSj98NvA2GNVLpddil6LtS1cQ5p36lFBtV +xQUMZiFz4qVbEak+izL+vPaev/mXXsOcibAIQ+qI/0txFpNhJjpaaSy6vRCBYFmc +8pgSoBnBI0ECgYAUKCn2atnpp5aWSTLYgNosBU4vDA1PShD14dnJMaqyr0aZtPhF +v/8b3btFJoGgPMLxgWEZ+2U4ju6sSFhPf7FXvLJu2QfQRkHZRDbEh7t5DLpTK4Fp +va9vl6Ml7uM/HsGpOLuqfIQJUs87OFCc7iCSvMJDDU37I7ekT2GKkpfbCQKBgBrE +0NeY0WcSJrp7/oqD2sOcYurpCG/rrZs2SIZmGzUhMxaa0vIXzbO59dlWELB8pmnE +Tf20K//x9qA5OxDe0PcVPukdQlH+/1zSOYNliG44FqnHtyd1TJ/gKVtMBiAiE4uO +aSClod5Yosf4SJbCFd/s5Iyfv52NqsAyp1w3Aj/BAoGAVCnEiGUfyHlIR+UH4zZW +GXJMeqdZLfcEIszMxLePkml4gUQhoq9oIs/Kw+L1DDxUwzkXN4BNTlFbOSu9gzK1 +dhuIUGfS6RPL88U+ivC3A0y2jT43oUMqe3hiRt360UQ1GXzp2dMnR9odSRB1wHoO +IOjEBZ8341/c9ZHc5PCGAG8= +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIJAIrEIthCfxUCMA0GCSqGSIb3DQEBCwUAMIGNMQswCQYD +VQQGEwJFUzEPMA0GA1UECAwGTWFkcmlkMREwDwYDVQQHDAhBbGNvcmNvbjEMMAoG +A1UECgwDVURTMQ4wDAYDVQQLDAVBY3RvcjESMBAGA1UEAwwJVURTIEFjdG9yMSgw +JgYJKoZIhvcNAQkBFhlzdXBwb3J0QHVkc2VudGVycHJpc2UuY29tMB4XDTE0MTAy +NjIzNDEyNFoXDTI0MTAyMzIzNDEyNFowgY0xCzAJBgNVBAYTAkVTMQ8wDQYDVQQI +DAZNYWRyaWQxETAPBgNVBAcMCEFsY29yY29uMQwwCgYDVQQKDANVRFMxDjAMBgNV +BAsMBUFjdG9yMRIwEAYDVQQDDAlVRFMgQWN0b3IxKDAmBgkqhkiG9w0BCQEWGXN1 +cHBvcnRAdWRzZW50ZXJwcmlzZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCb50K3mIznNklzyVAD7xSQOSJQ6+NPXj7U9/4zLZ+TvmbQ7RqUUsxb +fxHbeRnoYTWV2nKk4+tHqmvzujLSS/loFhTSMqtrLn7rowSYJoQhKOUkAiQlWkqC +fItWgL5pJopDpNHFul9Rn3dsPMWQTiGeUNR4Y3RnBhr1Q1BsqAzf4m6zFUmgLPPm +VLdF4uJ3Tuz8TSy2gWLs5aSr5do4WamwUfYjRSVMJECmwjUM4rQ8SQgg0sHBeBuD +UGNBvBQFac1G7qUcMReeu8ZrDUtMsXma/l4rA8NB5CRmTrQbTBF4l+jb2BDFebDq +DUK1Oqs9X35yOQfDOAFYHiixPX0IsXOZAgMBAAGjUDBOMB0GA1UdDgQWBBRShS90 +5lJTNvYPIEqP3GxWwG5iiDAfBgNVHSMEGDAWgBRShS905lJTNvYPIEqP3GxWwG5i +iDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAU0Sp4gXhQmRVzq+7+ +vRFUkQuPj4Ga/d9r5Wrbg3hck3+5pwe9/7APoq0P/M0DBhQpiJKjrD6ydUevC+Y/ +43ZOJPhMlNw0o6TdQxOkX6FDwQanLLs7sfvJvqtVzYn3nuRFKT3dvl7Zg44QMw2M +ay42q59fAcpB4LaDx/i7gOYSS5eca3lYW7j7YSr/+ozXK2KlgUkuCUHN95lOq+dF +trmV9mjzM4CNPZqKSE7kpHRywgrXGPCO000NvEGSYf82AtgRSFKiU8NWLQSEPdcB +k//2dsQZw2cRZ8DrC2B6Tb3M+3+CA6wVyqfqZh1SZva3LfGvq/C+u+ItguzPqNpI +xtvM +-----END CERTIFICATE-----''' + with open(certFile, "wt") as f: + f.write(certData) + + return certFile +# -------------- + + +class HTTPServerHandler(SimpleHTTPRequestHandler): + service = None + protocol_version = 'HTTP/1.1' + server_version = 'OpenGnsys Agent Server' + sys_version = '' + + def sendJsonError(self, code, message): + self.send_response(code) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(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.end_headers() + # Send the html message + self.wfile.write(data) + + + # parseURL + def parseUrl(self): + # Very simple path & params splitter + path = self.path.split('?')[0][1:].split('/') + + try: + params = dict((v[0], unquote(v[1])) for v in (v.split('=') for v in self.path.split('?')[1].split('&'))) + except Exception: + params = {} + + return (path, params) + + + def do_GET(self): + path, params = self.parseUrl() + + self.sendJsonResponse({'path': path, 'params': params}) + + def do_POST(self): + path, getParams = self.parseUrl() + + # Now post parameters, that are in JSON format + + + + + +class HTTPThreadingServer(ThreadingMixIn, HTTPServer): + pass + +class HTTPServerThread(threading.Thread): + def __init__(self, address, service): + super(self.__class__, self).__init__() + + HTTPServerHandler.service = service + + self.certFile = createSelfSignedCert() + self.server = HTTPThreadingServer(address, HTTPServerHandler) + self.server.socket = ssl.wrap_socket(self.server.socket, certfile=self.certFile, server_side=True) + + def getServerUrl(self): + return 'https://{}:{}/{}'.format(self.server.server_address[0], self.server.server_address[1], HTTPServerHandler.uuid) + + def stop(self): + self.server.shutdown() + + def run(self): + self.server.serve_forever() + + +if __name__ == '__main__': + thr = HTTPServerThread(('0.0.0.0', 8000), None) + print('Server started: {}'.format(thr)) + thr.start() + + \ No newline at end of file diff --git a/src/setup.py b/src/setup.py new file mode 100644 index 0000000..7d6e80c --- /dev/null +++ b/src/setup.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 +''' + +VERSION = '1.0.0' + +# ModuleFinder can't handle runtime changes to __path__, but win32com uses them +try: + # py2exe 0.6.4 introduced a replacement modulefinder. + # This means we have to add package paths there, not to the built-in + # one. If this new modulefinder gets integrated into Python, then + # we might be able to revert this some day. + # if this doesn't work, try import modulefinder + try: + import py2exe.mf as modulefinder + except ImportError: + import modulefinder + import win32com, sys + for p in win32com.__path__[1:]: + modulefinder.AddPackagePath("win32com", p) + for extra in ["win32com.shell"]: # ,"win32com.mapi" + __import__(extra) + m = sys.modules[extra] + for p in m.__path__[1:]: + modulefinder.AddPackagePath(extra, p) +except ImportError: + # no build path setup, no worries. + pass + +from distutils.core import setup +import py2exe +import sys +import os + +sys.argv.append('py2exe') + +def get_requests_cert_file(): + """Add Python requests .pem file for installers.""" + import requests + f = os.path.join(os.path.dirname(requests.__file__), 'cacert.pem') + return f + + +class Target: + + def __init__(self, **kw): + self.__dict__.update(kw) + # for the versioninfo resources + self.version = VERSION + self.name = 'OGAgentService' + self.description = 'OpenGnsys Agent Service' + self.author = 'Adolfo Gomez' + self.url = 'http://www.opengnsys.es' + self.company_name = "VirtualCable S.L.U." + self.copyright = "(c) 2014 VirtualCable S.L.U." + self.name = "OpenGnsys Agent" + +# Now you need to pass arguments to setup +# windows is a list of scripts that have their own UI and +# thus don't need to run in a console. + + +udsservice = Target( + description='OpenGnsys Agent Service', + modules=['opengnsys.windows.OGAgentService'], + icon_resources=[(0, 'img\\oga.ico'), (1, 'img\\oga.ico')], + cmdline_style='pywin32' +) + +# Some test_modules are hidden to py2exe by six, we ensure that they appear on "includes" +HIDDEN_BY_SIX = ['SocketServer', 'SimpleHTTPServer', 'urllib'] + +setup( + windows=[ + { + 'script': 'OGAgentUser.py', + 'icon_resources': [(0, 'img\\oga.ico'), (1, 'img\\oga.ico')] + }, + ], + console=[ + { + 'script': 'OGAServiceHelper.py' + } + ], + service=[udsservice], + data_files=[('', [get_requests_cert_file()]),('cfg', ['cfg/ogagent.cfg', 'cfg/ogclient.cfg'])], + options={ + 'py2exe': { + 'bundle_files': 3, + 'compressed': True, + 'optimize': 2, + 'includes': [ 'sip', 'PyQt4', 'win32com.shell', 'requests'] + HIDDEN_BY_SIX, + 'excludes': [ 'doctest', 'unittest' ], + 'dll_excludes': ['msvcp90.dll'], + 'dist_dir': '..\\bin', + } + }, + name='OpenGnsys Agent', + version=VERSION, + description='OpenGnsys Agent', + author='Adolfo Gomez', + author_email='agomez@virtualcable.es', + zipfile='OGAgent.zip', +) diff --git a/src/test_modules/__init__.py b/src/test_modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/test_modules/client/Sample1/__init__.py b/src/test_modules/client/Sample1/__init__.py new file mode 100644 index 0000000..db174f0 --- /dev/null +++ b/src/test_modules/client/Sample1/__init__.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 + +from opengnsys.workers import ClientWorker + +class Sample1(ClientWorker): + name = 'Sample1' + diff --git a/src/test_modules/client/__init__.py b/src/test_modules/client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/test_modules/server/Sample1/__init__.py b/src/test_modules/server/Sample1/__init__.py new file mode 100644 index 0000000..189957e --- /dev/null +++ b/src/test_modules/server/Sample1/__init__.py @@ -0,0 +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 diff --git a/src/test_modules/server/Sample1/sample1.py b/src/test_modules/server/Sample1/sample1.py new file mode 100644 index 0000000..61e405e --- /dev/null +++ b/src/test_modules/server/Sample1/sample1.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 + +from opengnsys.workers import ServerWorker + +from .sample_pkg import test + +class Sample1(ServerWorker): + name='Sample1' + diff --git a/src/test_modules/server/Sample1/sample_pkg/__init__.py b/src/test_modules/server/Sample1/sample_pkg/__init__.py new file mode 100644 index 0000000..1936572 --- /dev/null +++ b/src/test_modules/server/Sample1/sample_pkg/__init__.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 + + +def test(): + return 'Test' \ No newline at end of file diff --git a/src/test_modules/server/__init__.py b/src/test_modules/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/test_rest_server.py b/src/test_rest_server.py new file mode 100644 index 0000000..ad1aede --- /dev/null +++ b/src/test_rest_server.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2015 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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 +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 + +import logging +from tempfile import gettempdir +from os.path import exists, join + +logger = logging.getLogger(__name__) + + +CERTFILE = 'OGTestServer.pem' + + +def createSelfSignedCert(force=False): + + certFile = join(gettempdir(), CERTFILE) + + if exists(certFile) and not force: + return certFile + + certData = '''-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCb50K3mIznNklz +yVAD7xSQOSJQ6+NPXj7U9/4zLZ+TvmbQ7RqUUsxbfxHbeRnoYTWV2nKk4+tHqmvz +ujLSS/loFhTSMqtrLn7rowSYJoQhKOUkAiQlWkqCfItWgL5pJopDpNHFul9Rn3ds +PMWQTiGeUNR4Y3RnBhr1Q1BsqAzf4m6zFUmgLPPmVLdF4uJ3Tuz8TSy2gWLs5aSr +5do4WamwUfYjRSVMJECmwjUM4rQ8SQgg0sHBeBuDUGNBvBQFac1G7qUcMReeu8Zr +DUtMsXma/l4rA8NB5CRmTrQbTBF4l+jb2BDFebDqDUK1Oqs9X35yOQfDOAFYHiix +PX0IsXOZAgMBAAECggEBAJi3000RrIUZUp6Ph0gzPMuCjDEEwWiQA7CPNX1gpb8O +dp0WhkDhUroWIaICYPSXtOwUTtVjRqivMoxPy1Thg3EIoGC/rdeSdlXRHMEGicwJ +yVyalFnatr5Xzg5wkxVh4XMd0zeDt7e3JD7s0QLo5lm1CEzd77qz6lhzFic5/1KX +bzdULtTlq60dazg2hEbcS4OmM1UMCtRVDAsOIUIZPL0M9j1C1d1iEdYnh2xshKeG +/GOfo95xsgdMlGjtv3hUT5ryKVoEsu+36rGb4VfhPfUvvoVbRx5QZpW+QvxaYh5E +Fi0JEROozFwG31Y++8El7J3yQko8cFBa1lYYUwwpNAECgYEAykT+GiM2YxJ4uVF1 +OoKiE9BD53i0IG5j87lGPnWqzEwYBwnqjEKDTou+uzMGz3MDV56UEFNho7wUWh28 +LpEkjJB9QgbsugjxIBr4JoL/rYk036e/6+U8I95lvYWrzb+rBMIkRDYI7kbQD/mQ +piYUpuCkTymNAu2RisK6bBzJslkCgYEAxVE23OQvkCeOV8hJNPZGpJ1mDS+TiOow +oOScMZmZpail181eYbAfMsCr7ri812lSj98NvA2GNVLpddil6LtS1cQ5p36lFBtV +xQUMZiFz4qVbEak+izL+vPaev/mXXsOcibAIQ+qI/0txFpNhJjpaaSy6vRCBYFmc +8pgSoBnBI0ECgYAUKCn2atnpp5aWSTLYgNosBU4vDA1PShD14dnJMaqyr0aZtPhF +v/8b3btFJoGgPMLxgWEZ+2U4ju6sSFhPf7FXvLJu2QfQRkHZRDbEh7t5DLpTK4Fp +va9vl6Ml7uM/HsGpOLuqfIQJUs87OFCc7iCSvMJDDU37I7ekT2GKkpfbCQKBgBrE +0NeY0WcSJrp7/oqD2sOcYurpCG/rrZs2SIZmGzUhMxaa0vIXzbO59dlWELB8pmnE +Tf20K//x9qA5OxDe0PcVPukdQlH+/1zSOYNliG44FqnHtyd1TJ/gKVtMBiAiE4uO +aSClod5Yosf4SJbCFd/s5Iyfv52NqsAyp1w3Aj/BAoGAVCnEiGUfyHlIR+UH4zZW +GXJMeqdZLfcEIszMxLePkml4gUQhoq9oIs/Kw+L1DDxUwzkXN4BNTlFbOSu9gzK1 +dhuIUGfS6RPL88U+ivC3A0y2jT43oUMqe3hiRt360UQ1GXzp2dMnR9odSRB1wHoO +IOjEBZ8341/c9ZHc5PCGAG8= +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIJAIrEIthCfxUCMA0GCSqGSIb3DQEBCwUAMIGNMQswCQYD +VQQGEwJFUzEPMA0GA1UECAwGTWFkcmlkMREwDwYDVQQHDAhBbGNvcmNvbjEMMAoG +A1UECgwDVURTMQ4wDAYDVQQLDAVBY3RvcjESMBAGA1UEAwwJVURTIEFjdG9yMSgw +JgYJKoZIhvcNAQkBFhlzdXBwb3J0QHVkc2VudGVycHJpc2UuY29tMB4XDTE0MTAy +NjIzNDEyNFoXDTI0MTAyMzIzNDEyNFowgY0xCzAJBgNVBAYTAkVTMQ8wDQYDVQQI +DAZNYWRyaWQxETAPBgNVBAcMCEFsY29yY29uMQwwCgYDVQQKDANVRFMxDjAMBgNV +BAsMBUFjdG9yMRIwEAYDVQQDDAlVRFMgQWN0b3IxKDAmBgkqhkiG9w0BCQEWGXN1 +cHBvcnRAdWRzZW50ZXJwcmlzZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCb50K3mIznNklzyVAD7xSQOSJQ6+NPXj7U9/4zLZ+TvmbQ7RqUUsxb +fxHbeRnoYTWV2nKk4+tHqmvzujLSS/loFhTSMqtrLn7rowSYJoQhKOUkAiQlWkqC +fItWgL5pJopDpNHFul9Rn3dsPMWQTiGeUNR4Y3RnBhr1Q1BsqAzf4m6zFUmgLPPm +VLdF4uJ3Tuz8TSy2gWLs5aSr5do4WamwUfYjRSVMJECmwjUM4rQ8SQgg0sHBeBuD +UGNBvBQFac1G7qUcMReeu8ZrDUtMsXma/l4rA8NB5CRmTrQbTBF4l+jb2BDFebDq +DUK1Oqs9X35yOQfDOAFYHiixPX0IsXOZAgMBAAGjUDBOMB0GA1UdDgQWBBRShS90 +5lJTNvYPIEqP3GxWwG5iiDAfBgNVHSMEGDAWgBRShS905lJTNvYPIEqP3GxWwG5i +iDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAU0Sp4gXhQmRVzq+7+ +vRFUkQuPj4Ga/d9r5Wrbg3hck3+5pwe9/7APoq0P/M0DBhQpiJKjrD6ydUevC+Y/ +43ZOJPhMlNw0o6TdQxOkX6FDwQanLLs7sfvJvqtVzYn3nuRFKT3dvl7Zg44QMw2M +ay42q59fAcpB4LaDx/i7gOYSS5eca3lYW7j7YSr/+ozXK2KlgUkuCUHN95lOq+dF +trmV9mjzM4CNPZqKSE7kpHRywgrXGPCO000NvEGSYf82AtgRSFKiU8NWLQSEPdcB +k//2dsQZw2cRZ8DrC2B6Tb3M+3+CA6wVyqfqZh1SZva3LfGvq/C+u+ItguzPqNpI +xtvM +-----END CERTIFICATE-----''' + with open(certFile, "wt") as f: + f.write(certData) + + return certFile + +class HTTPServerHandler(BaseHTTPRequestHandler): + service = None + protocol_version = 'HTTP/1.0' + server_version = 'OpenGnsys Test REST Server' + sys_version = '' + + def sendJsonError(self, code, message): + self.send_response(code) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(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.end_headers() + # Send the html message + self.wfile.write(data) + + + # parseURL + def parseUrl(self): + # Very simple path & params splitter + path = self.path.split('?')[0][1:].split('/') + + try: + params = dict((v[0], unquote(v[1])) for v in (v.split('=') for v in self.path.split('?')[1].split('&'))) + except Exception: + params = {} + + return (path, params) + + + def do_GET(self): + path, params = self.parseUrl() + + self.sendJsonResponse({'path': path, 'params': params}) + + def do_POST(self): + path, getParams = self.parseUrl() + + # Now post parameters, that are in JSON format + self.sendJsonResponse({'path': path, 'params': getParams}) + + def log_error(self, fmt, *args): + logger.error('HTTP ' + fmt % args) + + def log_message(self, fmt, *args): + logger.info('HTTP ' + fmt % args) + + +class HTTPThreadingServer(ThreadingMixIn, HTTPServer): + pass + +class HTTPServerThread(threading.Thread): + def __init__(self, address, service): + super(self.__class__, self).__init__() + + HTTPServerHandler.service = service + + self.certFile = createSelfSignedCert() + self.server = HTTPThreadingServer(address, HTTPServerHandler) + self.server.socket = ssl.wrap_socket(self.server.socket, certfile=self.certFile, server_side=True) + + logger.info('Initialized HTTPS Server thread on {}'.format(address)) + + def getServerUrl(self): + return 'https://{}:{}/{}'.format(self.server.server_address[0], self.server.server_address[1], HTTPServerHandler.uuid) + + def stop(self): + self.server.shutdown() + + def run(self): + self.server.serve_forever() + + + +if __name__ == '__main__': + logging.basicConfig( + filename='/tmp/restserver.log', + filemode='w', + format='%(levelname)s %(asctime)s %(message)s', + level=logging.DEBUG + ) + + thr = HTTPServerThread(('0.0.0.0', 9999), None) + print('Server started: {}'.format(thr)) + thr.run() + \ No newline at end of file diff --git a/src/update.sh b/src/update.sh new file mode 100755 index 0000000..78147fa --- /dev/null +++ b/src/update.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# 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. + + +function process { + pyuic4 about-dialog.ui -o about_dialog_ui.py -x + pyuic4 message-dialog.ui -o message_dialog_ui.py -x +} + +pyrcc4 -py3 OGAgent.qrc -o OGAgent_rc.py + + +# process current directory ui's +process + diff --git a/windows/build-windows.sh b/windows/build-windows.sh new file mode 100755 index 0000000..a861b09 --- /dev/null +++ b/windows/build-windows.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export WINEARCH=win32 +export WINEPREFIX=$(realpath $(dirname $0)/wine) +wine cmd /c c:\\ogagent\\build.bat diff --git a/windows/build.bat b/windows/build.bat new file mode 100644 index 0000000..2c44474 --- /dev/null +++ b/windows/build.bat @@ -0,0 +1,6 @@ +C: +CD \ogagent\src +python setup.py +CD .. +"C:\Program Files\NSIS\makensis.exe" ogagent.nsi + diff --git a/windows/ogagent.nsi b/windows/ogagent.nsi new file mode 100644 index 0000000..7e7cd71 --- /dev/null +++ b/windows/ogagent.nsi @@ -0,0 +1,184 @@ +# We need http://nsis.sourceforge.net/NSIS_Simple_Firewall_Plugin +# Copy inside the two x86_xxxxx folders inside nsis plugins folder +Name "OpenGnSys Agent" + +# OpenGnsys Actor version +!define OGA_VERSION 1.0.0 + +# General Symbol Definitions +!define REGKEY "SOFTWARE\OGAgent" +!define VERSION ${OGA_VERSION}.0 +!define COMPANY "Virtual Cable S.L.U." +!define URL http://www.udsenterprise.com + +# MultiUser Symbol Definitions +!define MULTIUSER_EXECUTIONLEVEL Admin +#!define MULTIUSER_INSTALLMODE_DEFAULT_CURRENTUSER +!define MULTIUSER_INSTALLMODE_COMMANDLINE +!define MULTIUSER_INSTALLMODE_INSTDIR OGAgent +!define MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_KEY "${REGKEY}" +!define MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUE "Path" + +# MUI Symbol Definitions +#!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\orange-install.ico" +!define MUI_ICON "src\img\oga.ico" +!define MUI_FINISHPAGE_NOAUTOCLOSE +!define MUI_UNICON "src\img\oga.ico" +!define MUI_UNFINISHPAGE_NOAUTOCLOSE +!define MUI_LANGDLL_REGISTRY_ROOT HKLM +!define MUI_LANGDLL_REGISTRY_KEY ${REGKEY} +!define MUI_LANGDLL_REGISTRY_VALUENAME InstallerLanguage + +# Included files +!include MultiUser.nsh +!include Sections.nsh +!include MUI2.nsh + +# Reserved Files +!insertmacro MUI_RESERVEFILE_LANGDLL + +# Variables +Var StartMenuGroup + +# Installer pages +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_LICENSE src\license.txt +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +# Installer languages +!insertmacro MUI_LANGUAGE English +!insertmacro MUI_LANGUAGE Spanish +!insertmacro MUI_LANGUAGE French +!insertmacro MUI_LANGUAGE German + +# Installer attributes +BrandingText "OpenGnSys" +OutFile OGAgentSetup-${OGA_VERSION}.exe +InstallDir OGAgent +CRCCheck on +XPStyle on +ShowInstDetails hide +VIProductVersion "${VERSION}.0.0" +VIAddVersionKey /LANG=${LANG_ENGLISH} ProductName "OGAgent" +VIAddVersionKey /LANG=${LANG_ENGLISH} ProductVersion "${VERSION}" +VIAddVersionKey /LANG=${LANG_ENGLISH} CompanyName "${COMPANY}" +VIAddVersionKey /LANG=${LANG_ENGLISH} CompanyWebsite "${URL}" +VIAddVersionKey /LANG=${LANG_ENGLISH} FileVersion "${VERSION}" +VIAddVersionKey /LANG=${LANG_ENGLISH} FileDescription "OpenGnSys Agent installer" +VIAddVersionKey /LANG=${LANG_ENGLISH} LegalCopyright "(c) 2015 Virtual Cable S.L.U." +InstallDirRegKey HKLM "${REGKEY}" Path +ShowUninstDetails show + +# Installer sections +Section -Main SEC0000 + SetShellVarContext all + SetOutPath $INSTDIR + SetOverwrite on + File /r bin\*.* + File vcredist_x86.exe + WriteRegStr HKLM "${REGKEY}\Components" Main 1 +SectionEnd + +Section -post SEC0001 + SetShellVarContext current + WriteRegStr HKLM "${REGKEY}" Path $INSTDIR + SetOutPath $INSTDIR + WriteUninstaller $INSTDIR\OGAgentUninstaller.exe + SetOutPath $SMPROGRAMS\$StartMenuGroup + CreateShortcut "$SMPROGRAMS\$StartMenuGroup\$(^UninstallLink).lnk" $INSTDIR\OGAgentUninstaller.exe + WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" DisplayName "$(^Name)" + WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" DisplayVersion "${VERSION}" + WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" Publisher "${COMPANY}" + WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" URLInfoAbout "${URL}" + WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" DisplayIcon $INSTDIR\OGAgentUninstaller.exe + WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" UninstallString $INSTDIR\OGAgentUninstaller.exe + WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" OGAgentTool $INSTDIR\OGAgentUser.exe + WriteRegDWORD HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" NoModify 1 + WriteRegDWORD HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" NoRepair 1 + ExecWait '"$INSTDIR\vcredist_x86.exe" /passive /norestart' + # Add the application to the firewall exception list - All Networks - All IP Version - Enabled + # SimpleFC::AddApplication "OpenGnSys Agent Service" "$INSTDIR\OGAgentService.exe" 0 2 "" 1 + # SimpleFC::AdvAddRule [name] [description] [protocol] [direction] + # [status] [profile] [action] [application] [service_name] [icmp_types_and_codes] + # [group] [local_ports] [remote_ports] [local_address] [remote_address] + # + SimpleFC::AdvAddRule "OpenGnSys Agent Firewall rules" "Firewall rules for OpenGnSys Agent interaction with broker." "6" "1" \ + "1" "7" "1" "$INSTDIR\OGAgentService.exe" "" "" \ + "" "" "" "" "" + Pop $0 ; return error(1)/success(0) + # Install service + nsExec::Exec /OEM "$INSTDIR\OGAgentService.exe --startup auto install" # Add service after installation + # Update recovery options + nsExec::Exec /OEM "$INSTDIR\OGAServiceHelper.exe" +SectionEnd + +# Macro for selecting uninstaller sections +!macro SELECT_UNSECTION SECTION_NAME UNSECTION_ID + Push $R0 + ReadRegStr $R0 HKLM "${REGKEY}\Components" "${SECTION_NAME}" + StrCmp $R0 1 0 next${UNSECTION_ID} + !insertmacro SelectSection "${UNSECTION_ID}" + GoTo done${UNSECTION_ID} +next${UNSECTION_ID}: + !insertmacro UnselectSection "${UNSECTION_ID}" +done${UNSECTION_ID}: + Pop $R0 +!macroend + +# Uninstaller sections +Section /o -un.Main UNSEC0000 + nsExec::Exec /OEM "$INSTDIR\OGAgentService.exe stop" # Stops the service prior uninstall + nsExec::Exec /OEM "$INSTDIR\OGAgentService.exe remove" # Removes the service prior uninstall + Delete /REBOOTOK "$INSTDIR\*.*" + DeleteRegValue HKLM "${REGKEY}\Components" Main + DeleteRegValue HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" OGAgentTool +SectionEnd + +Section -un.post UNSEC0001 + # Remove application from the firewall exception list + # SimpleFC::RemoveApplication "$INSTDIR\OGAgentService.exe" + SimpleFC::AdvRemoveRule "OpenGnSys Agent Firewall rules" + Pop $0 ; return error(1)/success(0) + + SetShellVarContext current + StrCpy $StartMenuGroup "OpenGnSys Agent" + DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" + Delete /REBOOTOK "$SMPROGRAMS\$StartMenuGroup\$(^UninstallLink).lnk" + Delete /REBOOTOK $INSTDIR\OGAgentUninstaller.exe + DeleteRegValue HKLM "${REGKEY}" Path + DeleteRegKey /IfEmpty HKLM "${REGKEY}\Components" + DeleteRegKey /IfEmpty HKLM "${REGKEY}" + RmDir /REBOOTOK $SMPROGRAMS\$StartMenuGroup + SetShellVarContext all + RmDir /REBOOTOK $INSTDIR + SetRebootFlag true + MessageBox MB_OK|MB_USERICON "Your system needs to reboot to complete uninstallation." + Reboot # Reboot is needed after uninstalling, so new installs works fine +SectionEnd + +# Installer functions +Function .onInit + InitPluginsDir + StrCpy $StartMenuGroup "OpenGnSys Agent" + + !insertmacro MUI_LANGDLL_DISPLAY + !insertmacro MULTIUSER_INIT +FunctionEnd + +# Uninstaller functions +Function un.onInit + StrCpy $StartMenuGroup "OpenGnSys Agent" + !insertmacro MUI_UNGETLANGUAGE + !insertmacro MULTIUSER_UNINIT + !insertmacro SELECT_UNSECTION Main ${UNSEC0000} +FunctionEnd + +# Installer Language Strings +LangString ^UninstallLink ${LANG_ENGLISH} "Uninstall $(^Name)" +LangString ^UninstallLink ${LANG_SPANISH} "Desinstalar $(^Name)" +LangString ^UninstallLink ${LANG_FRENCH} "D�sinstaller $(^Name)" +LangString ^UninstallLink ${LANG_GERMAN} "deinstallieren $(^Name)" diff --git a/windows/py2exe-wine-linux.sh b/windows/py2exe-wine-linux.sh new file mode 100755 index 0000000..7094550 --- /dev/null +++ b/windows/py2exe-wine-linux.sh @@ -0,0 +1,72 @@ +#!/bin/sh + +# We need: +# * Wine (32 bit) +# * winetricks + +export WINEARCH=win32 +WINE=wine + +download() { + mkdir downloads + # Get needed software + cd downloads + wget -nd https://www.python.org/ftp/python/2.7.10/python-2.7.10.msi + wget -nd http://download.microsoft.com/download/7/9/6/796EF2E4-801B-4FC4-AB28-B59FBF6D907B/VCForPython27.msi + wget -nd https://bootstrap.pypa.io/get-pip.py + wget -nd http://sourceforge.net/projects/pywin32/files/pywin32/Build%20219/pywin32-219.win32-py2.7.exe/download -O pywin32-install.exe + wget -nd http://sourceforge.net/projects/py2exe/files/py2exe/0.6.9/py2exe-0.6.9.win32-py2.7.exe/download -O py2exe-install.exe + wget -nd http://prdownloads.sourceforge.net/nsis/nsis-3.0b1-setup.exe?download -O nsis-install.exe + wget -nd http://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.11.4/PyQt4-4.11.4-gpl-Py2.7-Qt4.8.7-x32.exe/download -O pyqt-install.exe + wget -nd http://nsis.sourceforge.net/mediawiki/images/d/d7/NSIS_Simple_Firewall_Plugin_1.20.zip + cd .. +} + +install_python() { + WINEPREFIX=`pwd`/wine + export WINEPREFIX + echo "Setting up wine prefix (using winetricks)" + winetricks + + cd downloads + echo "Installing python" + $WINE msiexec /qn /i python-2.7.10.msi + echo "Installing vc for python" + $WINE msiexec /qn /i VCForPython27.msi + + echo "Installing pywin32 (needs X)" + $WINE pywin32-install.exe + echo "Installing py2exe (needs X)" + $WINE py2exe-install.exe + echo "Installing pyqt" + $WINE pyqt-install.exe + echo "Installing nsis (needs X?)" + $WINE nsis-install.exe + + cd .. +} + +setup_pip() { + echo "Seting up pip..." + #mkdir $WINEPREFIX/drive_c/temp + #cp downloads/get-pip.py $WINEPREFIX/drive_c/temp + #cd $WINEPREFIX/drive_c/temp + #$WINE c:\\Python27\\python.exe get-pip.py + wine c:\\Python27\\python -m pip install --upgrade pip +} + +install_packages() { + echo "Installing required packages" + wine c:\\Python27\\python -m pip install requests pycrypto six + # Copy nsis required NSIS_Simple_Firewall_Plugin_1 + echo "Copying simple firewall plugin for nsis installer" + unzip -o downloads/NSIS_Simple_Firewall_Plugin_1.20.zip SimpleFC.dll -d $WINEPREFIX/drive_c/Program\ Files/NSIS/Plugins/x86-ansi/ + unzip -o downloads/NSIS_Simple_Firewall_Plugin_1.20.zip SimpleFC.dll -d $WINEPREFIX/drive_c/Program\ Files/NSIS/Plugins/x86-unicode/ +} + +download +install_python +setup_pip +install_packages + +