Compare commits

..

86 Commits
main ... oglive

Author SHA1 Message Date
Ramón M. Gómez eb2c5f8768 #908: Fast-forward branch. 2020-05-20 14:23:02 +02:00
Ramón M. Gómez a0bd02cdc4 #930: Fast-forward the branch 2020-01-15 10:39:27 +01:00
Ramón M. Gómez a3f2b23e80 #930: OGAgent for ogLive can launch the Browser. 2020-01-15 09:48:26 +01:00
Juan Manuel Bardallo fb1405778b Bug fixed when out and err are None in operation exec_command 2020-01-15 09:46:55 +01:00
Ramón M. Gómez 53f167e585 #908: Trying to fix a bug when obteining execution outputs. 2020-01-15 09:46:55 +01:00
Ramón M. Gómez 3c08c36716 #761: OGAGent checks for dobule slash before connecting to REST URL. 2020-01-15 09:45:41 +01:00
Ramón M. Gómez 4d89f8218e #750: Simple REST route to get the list of running commands. 2020-01-15 09:44:19 +01:00
Ramón M. Gómez 15eb6949ce #750: Using PEP 8 Style Guide for Python in new module; implementing basic {{{getconfig}}} operation. 2020-01-15 09:43:38 +01:00
Ramón M. Gómez e140d8d037 #750: Process to build OGAgent for ogLive package. 2020-01-15 09:43:38 +01:00
Ramón M. Gómez 0fbb893e75 #750: Adapting route {{GET /command}}} parameters. 2020-01-15 09:42:43 +01:00
Ramón M. Gómez d1b88b05ee #750: Using more descriptive status; new route {{{POST /command}}} to launch command/script in a callback thread that returns all data to a server route. 2020-01-15 09:40:07 +01:00
Ramón M. Gómez dc8c12bf6e #750: Using PEP 8 Style Guide for Python in new module; implementing basic {{{getconfig}}} operation. 2020-01-15 09:32:59 +01:00
Ramón M. Gómez b8d05a72c6 #750: Process to build OGAgent for ogLive package. 2020-01-15 09:29:07 +01:00
Ramón M. Gómez bb549c7878 #930: Correct some typos. 2020-01-15 09:24:46 +01:00
Ramón M. Gómez a648e3fbbe #930: Remove duplicate code and fix some typos. 2020-01-15 09:24:46 +01:00
Ramón M. Gómez d8ba7b2bf0 #930: OGAgent for ogLive can launch the Browser. 2020-01-15 09:24:46 +01:00
Ramón M. Gómez b9ed5ca8f6 #908 OGAgent for ogLive launches the Browser with an animation while getting initial configuration. 2020-01-15 09:24:46 +01:00
Juan Manuel Bardallo 8055b1358a Bug fixed when out and err are None in operation exec_command 2020-01-15 09:24:46 +01:00
Ramón M. Gómez 2ac6daa3f5 #908: Trying to fix a bug when obteining execution outputs. 2020-01-15 09:24:46 +01:00
Ramón M. Gómez 16c3792567 #761: OGAgent launchs client's default menu on activation process. 2020-01-15 09:24:46 +01:00
Ramón M. Gómez 8ef3e73b9e #908 OGAgent for ogLive code clean up. 2020-01-15 09:24:46 +01:00
Ramón M. Gómez bdfcb0d1fe #761: OGAGent checks for dobule slash before connecting to REST URL. 2020-01-15 09:24:46 +01:00
Ramón M. Gómez 198088d7f4 #750: Using PEP 8 Style Guide for Python in new module; implementing basic {{{getconfig}}} operation. 2020-01-14 14:10:42 +01:00
Ramón M. Gómez ecf49815c1 #750: Process to build OGAgent for ogLive package. 2020-01-14 12:02:30 +01:00
Ramón M. Gómez ae813486ce #750: Renaming server REST route {{{GET /done}}} to {{{GET /command_done}}} to log commands executed on clients. 2020-01-14 11:58:38 +01:00
Ramón M. Gómez 85f395ee78 #750: Adapting route {{GET /command}}} parameters. 2020-01-14 11:58:38 +01:00
Ramón M. Gómez 633c90c26e #750: Simple REST route to get the list of running commands. 2020-01-14 11:57:34 +01:00
Ramón M. Gómez ddd54eea4b #750: Using more descriptive status; new route {{{POST /command}}} to launch command/script in a callback thread that returns all data to a server route. 2020-01-14 11:56:33 +01:00
Ramón M. Gómez f0c847c35f #750: Route {{{GET /getconfig}}} renamed as {{{GET /config}}}; route {{{GET /hardware}}} returns data in JSON format; new basic route {{{GET /software?disk=NDisk&part=NPart}}} 2020-01-14 11:53:59 +01:00
Ramón M. Gómez 863e038f24 #750: New route {{{GET /hardware}}}. 2020-01-14 11:53:59 +01:00
Ramón M. Gómez bb1ff4dc1d #750: OGAgent for ogLive looks for {{{oglive}}} environ variable; route {{{GET /getconfig}}} returns data in JSON format. 2020-01-14 11:53:59 +01:00
Ramón M. Gómez be334a5c17 #750: Using PEP 8 Style Guide for Python in new module; implementing basic {{{getconfig}}} operation. 2020-01-14 11:53:09 +01:00
Ramón M. Gómez 983213c9df #750: Process to build OGAgent for ogLive package. 2020-01-14 11:05:44 +01:00
Ramón M. Gómez 7b07f5c983 #930: OGAgent `GET /script` route accepts a new optional `send_config` parameter to send back the client configuration after command execution. 2019-12-12 12:07:22 +01:00
Ramón M. Gómez 37c8fb71d4 #930: Correct some typos. 2019-12-12 11:23:17 +01:00
Ramón M. Gómez c3cd292d0d #930: Remove duplicate code and fix some typos. 2019-12-12 11:23:17 +01:00
Ramón M. Gómez 582ab7873b #930: OGAgent for ogLive can launch the Browser. 2019-12-12 11:20:05 +01:00
Ramón M. Gómez 3642196f14 #908 OGAgent for ogLive launches the Browser with an animation while getting initial configuration. 2019-12-12 11:17:20 +01:00
Juan Manuel Bardallo f3f6b350bc Bug fixed when out and err are None in operation exec_command 2019-12-12 11:17:20 +01:00
Ramón M. Gómez 10c30f9fca #908: Trying to fix a bug when obteining execution outputs. 2019-12-12 11:17:20 +01:00
Ramón M. Gómez 7ad4f2f559 #761: OGAgent launchs client's default menu on activation process. 2019-12-12 11:17:20 +01:00
Ramón M. Gómez f2c21d888a #908 OGAgent for ogLive code clean up. 2019-12-12 11:17:20 +01:00
Ramón M. Gómez 65c74510f7 #761: OGAGent checks for dobule slash before connecting to REST URL. 2019-12-12 11:10:05 +01:00
Ramón M. Gómez 1cfe9dc9db #750: Simple REST route to get the list of running commands. 2019-12-12 11:08:35 +01:00
Ramón M. Gómez 3c5a6993d7 #750: Using PEP 8 Style Guide for Python in new module; implementing basic {{{getconfig}}} operation. 2019-12-12 10:42:23 +01:00
Ramón M. Gómez 079b250bb1 #750: Process to build OGAgent for ogLive package. 2019-12-12 10:14:13 +01:00
Ramón M. Gómez 0e7fbadeb9 #750: Renaming server REST route {{{GET /done}}} to {{{GET /command_done}}} to log commands executed on clients. 2019-12-12 10:05:29 +01:00
Ramón M. Gómez f01a01aee4 #750: Adapting route {{GET /command}}} parameters. 2019-12-12 10:05:29 +01:00
Ramón M. Gómez dcc3b83968 #750: Simple REST route to get the list of running commands. 2019-12-12 10:02:13 +01:00
Ramón M. Gómez dfd2c05275 #750: Using more descriptive status; new route {{{POST /command}}} to launch command/script in a callback thread that returns all data to a server route. 2019-12-12 09:57:41 +01:00
Ramón M. Gómez ae066d2d6c #750: Route {{{GET /getconfig}}} renamed as {{{GET /config}}}; route {{{GET /hardware}}} returns data in JSON format; new basic route {{{GET /software?disk=NDisk&part=NPart}}} 2019-12-12 09:53:34 +01:00
Ramón M. Gómez 8d427bb0d0 #750: New route {{{GET /hardware}}}. 2019-12-12 09:53:34 +01:00
Ramón M. Gómez 8e4fedd75e #750: OGAgent for ogLive looks for {{{oglive}}} environ variable; route {{{GET /getconfig}}} returns data in JSON format. 2019-12-12 09:53:34 +01:00
Ramón M. Gómez 81a9b65d79 #750: Using PEP 8 Style Guide for Python in new module; implementing basic {{{getconfig}}} operation. 2019-12-12 09:50:15 +01:00
Ramón M. Gómez 88d3c4642b #750: Process to build OGAgent for ogLive package. 2019-12-12 09:28:18 +01:00
Ramón M. Gómez 85057befc1 #930: Correct some typos. 2019-12-02 18:43:52 +01:00
Ramón M. Gómez 888f92a60e Merge branch 'master' into ogagent-oglive 2019-12-02 18:15:13 +01:00
Ramón M. Gómez 40402bd267 #930: Remove duplicate code and fix some typos. 2019-09-30 14:22:21 +02:00
Ramón M. Gómez b3bdeb414e #930: OGAgent for ogLive can launch the Browser. 2019-09-30 13:27:04 +02:00
Ramón M. Gómez 1954ef5cfa #908 OGAgent for ogLive launches the Browser with an animation while getting initial configuration. 2019-09-30 11:53:50 +02:00
Juan Manuel Bardallo d0a7766b08 Bug fixed when out and err are None in operation exec_command 2019-09-30 11:35:00 +02:00
Ramón M. Gómez a5117cbf55 #908: Trying to fix a bug when obteining execution outputs. 2019-09-30 11:33:35 +02:00
Ramón M. Gómez f24fea2a9c #761: OGAgent launchs client's default menu on activation process. 2019-09-30 11:32:36 +02:00
Ramón M. Gómez 221bcd14cc #908 OGAgent for ogLive code clean up. 2019-09-30 11:26:01 +02:00
Ramón M. Gómez 01adfee493 #761: OGAGent checks for dobule slash before connecting to REST URL. 2019-09-30 11:20:51 +02:00
Ramón M. Gómez cb1fb02df5 #750: Fast forward branch 2019-09-30 11:04:46 +02:00
Ramón M. Gómez 46d3308a5f #750: Renaming server REST route {{{GET /done}}} to {{{GET /command_done}}} to log commands executed on clients. 2019-09-30 10:58:04 +02:00
Ramón M. Gómez a0ce0fc9ea #750: Adapting route {{GET /command}}} parameters. 2019-09-30 10:58:04 +02:00
Ramón M. Gómez aab7ec58aa #750: Simple REST route to get the list of running commands. 2019-09-30 10:57:24 +02:00
Ramón M. Gómez 2af37100e0 #750: Using more descriptive status; new route {{{POST /command}}} to launch command/script in a callback thread that returns all data to a server route. 2019-09-30 10:54:04 +02:00
Ramón M. Gómez 9e81e9020c #750: Route {{{GET /getconfig}}} renamed as {{{GET /config}}}; route {{{GET /hardware}}} returns data in JSON format; new basic route {{{GET /software?disk=NDisk&part=NPart}}} 2019-09-30 10:49:25 +02:00
Ramón M. Gómez a5905575e2 #750: New route {{{GET /hardware}}}. 2019-09-30 10:49:25 +02:00
Ramón M. Gómez 149d1bdc4f #750: OGAgent for ogLive looks for {{{oglive}}} environ variable; route {{{GET /getconfig}}} returns data in JSON format. 2019-09-30 10:49:25 +02:00
Ramón M. Gómez 70ce2377da #750: Using PEP 8 Style Guide for Python in new module; implementing basic {{{getconfig}}} operation. 2019-09-30 10:47:52 +02:00
Ramón M. Gómez cf4de0c2d2 #750: Process to build OGAgent for ogLive package. 2019-09-30 10:40:22 +02:00
Ramón M. Gómez 1ff2475ee3 #750: OGAgent activation and deactivation now compatible with new web console. 2019-04-09 10:52:05 +02:00
Ramón M. Gómez 417f9497dd #750: Fast-forward. 2018-11-26 13:09:55 +01:00
Ramón M. Gómez da9bd96ec8 #750: Renaming server REST route {{{GET /done}}} to {{{GET /command_done}}} to log commands executed on clients. 2018-07-05 13:50:17 +02:00
Ramón M. Gómez 91bdf9040d #750: Adapting route {{GET /command}}} parameters. 2018-07-04 12:15:50 +02:00
Ramón M. Gómez 3806f120c6 #750: Simple REST route to get the list of running commands. 2018-07-03 17:55:25 +02:00
Ramón M. Gómez 0c8032d137 #750: Using more descriptive status; new route {{{POST /command}}} to launch command/script in a callback thread that returns all data to a server route. 2018-06-30 17:13:45 +02:00
Ramón M. Gómez a108f36cff #750: Route {{{GET /getconfig}}} renamed as {{{GET /config}}}; route {{{GET /hardware}}} returns data in JSON format; new basic route {{{GET /software?disk=NDisk&part=NPart}}} 2018-06-21 19:23:10 +02:00
Ramón M. Gómez e5ba6cfc10 #750: New route {{{GET /hardware}}}. 2018-06-20 20:25:30 +02:00
Ramón M. Gómez 4272d559e7 #750: OGAgent for ogLive looks for {{{oglive}}} environ variable; route {{{GET /getconfig}}} returns data in JSON format. 2018-06-20 19:48:49 +02:00
Ramón M. Gómez 0a085de592 #750: Using PEP 8 Style Guide for Python in new module; implementing basic {{{getconfig}}} operation. 2018-06-18 20:47:35 +02:00
Ramón M. Gómez e20838c641 #750: Process to build OGAgent for ogLive package. 2018-06-18 13:54:44 +02:00
118 changed files with 3389 additions and 4707 deletions

30
.gitignore vendored
View File

@ -1,30 +0,0 @@
.DS_Store
.editorconfig
.idea
**/*.swp
__pycache__/
linux/debian/.debhelper/
linux/debian/files
linux/debian/ogagent/
linux/debian/ogagent.debhelper.log
linux/debian/ogagent.postinst.debhelper
linux/debian/ogagent.postrm.debhelper
linux/configure-stamp
linux/build-stamp
macos/build
windows/vc_redist.x64.exe
ogagent_*_all.deb
ogagent_*_amd64.buildinfo
ogagent_*_amd64.changes
ogagent_*_amd64.build
OGAgentInstaller-*.pkg
OGAgentSetup-*.exe
bin
src/build
src/dist
src/about_dialog_ui.py
src/message_dialog_ui.py
src/dist
src/build
windows/VERSION
windows/VC_redist.x64.exe

View File

@ -1,344 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [5.7.0] - 2025-05-27
### Changed
- Use TLS again
## [5.6.0] - 2025-05-21
### Changed
- Launch QT6 browser
- Change URLs using dbus
## [5.5.0] - 2025-05-19
### Changed
- Revert to the QT4 browser again
## [5.4.0] - 2025-05-19
### Changed
- Disabled TLS on request
## [5.3.0] - 2025-05-16
### Changed
- Execute 'launch_browser' rather than 'browser'
## [5.2.0] - 2025-05-14
### Added
- Log duration of user sessions
## [5.1.1] - 2025-05-06
### Fixed
- Fixed URL for notifying stop to ogcore
## [5.1.0] - 2025-05-06
### Added
- Added powershell helper script for logging out from windows
## [5.0.0] - 2025-05-06
### Added
- Use TLS
## [4.0.0] - 2025-04-24
### Added
- Authn/authz to the oglive agent
## [3.3.0] - 2025-04-14
### Added
- Log stuff to a new json log
## [3.2.0] - 2025-04-10
### Added
- Operating system: periodically ping ogcore
## [3.1.0] - 2025-04-07
### Added
- Oglive: periodically ping ogcore
## [3.0.0] - 2025-03-31
### Changed
- Ignore module provided in the URLs to the API
## [2.0.0] - 2025-03-26
### Changed
- EjecutarScript/ConsolaRemota: expect "scp" parameter encoded in base64
## [1.7.1] - 2025-03-25
### Fixed
- Make cfg2obj more robust
## [1.7.0] - 2025-03-21
### Removed
- Delete the new "ptt" parameter. It's not needed.
## [1.6.0] - 2025-03-12
### Changed
- Don't invoke bash code for some functionalities
## [1.5.0] - 2025-03-12
### Changed
- Accept new "ptt" parameter in /ogAdmCli/Configurar
### Removed
- No longer recognise the unused "che" parameter in /ogAdmCli/Configurar
## [1.4.9] - 2025-02-20
### Changed
- Notify ogcore when agent shuts down within oglive
## [1.4.8] - 2025-02-18
### Changed
- Optionally return disk config in /status
## [1.4.7] - 2025-02-04
### Changed
- Merge server modules
### Added
- Track the progress of children
## [1.4.6] - 2025-01-14
### Changed
- Point to the new menu browser
## [1.4.5] - 2024-11-29
### Added
- Kill long running jobs in oglive
## [1.4.5~pre8] - 2024-11-27
### Added
- Add Configurar() to the CloningEngine module
## [1.4.5~pre7] - 2024-11-20
### Changed
- Use old browser again
## [1.4.5~pre6] - 2024-11-20
### Changed
- Do not use envvars for the operating-system module
## [1.4.5~pre5] - 2024-11-18
### Fixed
- Avoid some KeyErrors
## [1.4.5~pre4] - 2024-11-15
### Fixed
- Don't die when ogcore returns HTTP 4xx or 5xx
### Changed
- Get ogcore IP and port from the environment
## [1.4.5~pre3] - 2024-11-06
- Kill long running jobs in oglive (not-yet-working draft)
## [1.4.5~pre2] - 2024-11-06
### Fixed
- Remove race condition due to several monitoring threads
### Changed
- Include job_id in asynchronous responses
### Removed
- Remove vim swapfiles from the package contents
## [1.4.5~pre1] - 2024-11-06
### Changed
- CrearImagen: return inventory inline
## [1.4.4] - 2024-10-17
### Fixed
- Use logger.debug() to prevent the windows agent from dying
### Changed
- Make status() call synchronous
## [1.4.3] - 2024-10-17
### Changed
- Use new OGBrowser
## [1.4.2] - 2024-10-15
### Added
- Have ogAdmClient/status return information about network and disks
## [1.4.1] - 2024-10-11
### Fixed
- Bugfix: move data structure to the right class
## [1.4.0] - 2024-10-11
- Add more functionality
### Changed
- Begin using semantic versioning
## [1.3.8] - 2024-10-01
### Added
- Add more functionality to the ogAdmClient module
## [1.3.7] - 2024-09-27
### Added
- CloningEngine: RESTfully keep a list of long-running jobs
## [1.3.6] - 2024-09-19
### Added
- Add more functionality to the ogAdmClient module
- Add CloningEngine module
## [1.3.5] - 2024-08-29
### Changed
- Don't unconditionally load modules--dynamically load everything
### Removed
- Remove old, unused code
## [1.3.4] - 2024-07-30
### Added
- Implement JobMgr
## [1.3.1] - 2024-06-26
### Changed
- Migrate the update script from shell to python
- pyinstaller: include the 'img' subdir
- take icons from 'img'
## [1.3.0-2] - 2024-04-25
### Fixed
- Add missing dependency on zenity
## [1.3.0] - 2024-04-25
### Changed
- Upgrade to Qt 6
## [1.2.0] - 2020-05-4
### Changed
- Python 3 and Qt 5 compatibility
## [1.1.1b] - 2020-02-7
### Changed
- Use python-distro to detect the distribution version
## [1.1.1] - 2019-05-23
### Changed
- Set connection timeout
- Compatibility with "Exam Mode" from the University of Seville
## [1.1.0a] - 2019-05-22
### Fixed
- Fix a bug when activating the agent with some network devices
## [1.1.0] - 2016-10-13
### Changed
- Functional OpenGnsys Agent interacting with OpenGnsys Server 1.1.0
## [1.0.0] - 2015-07-18
### Added
- Initial release for OpenGnsys Agent

View File

@ -1,22 +1,25 @@
OGAgent: agente OpenGnsys para sistemas operativos INSTALL.es.txt OGAgent: agente OpenGnsys para sistemas operativos INSTALL.es.txt
==================================================================== ====================================================================
Requisitos de creación Requisitos de creación
---------------------- ----------------------
Sistema operativo Linux con los siguientes paquetes instalados: Sisitema operativo Linux con los siguientes paquetes instalados:
- GNU C++, Python, librerías PyQt6 - Subversion
- GNU C++, Python, librerías PyQt4
- Creación de instalador Exe (Wine 32 bits, Wine Gecko, Wine Mono, Samba Winbind, Cabextrct) - Creación de instalador Exe (Wine 32 bits, Wine Gecko, Wine Mono, Samba Winbind, Cabextrct)
- Creación de paquetes Deb (debhelper, dpkg-dev) - Creación de paquetes Deb (debhelper, dpkg-dev)
- Creación de paquetes RPM (rpm-build)
- Creación de paquetes Pkg (xar, bomutils) - Creación de paquetes Pkg (xar, bomutils)
Crear instaladores de OGAgent Crear instaladores de OGAgent
----------------------------- -----------------------------
- Paso previo: actualizar componentes gráficos de PyQt para OGAgent: - Paso previo: actaulizar componentes gráficos de PyQt para OGAgnet:
src/update.sh src/update.sh
- Crear paquetes Deb distribuciones debian/ubuntu - Crear paquetes Deb y RPM para distribuciones Linux (requiere permisos de "root"):
linux/build-packages.sh sudo linux/build-packages.sh
- Crear paquete Pkg para sistemas operativos macOS X: - Crear paquete Pkg para sistemas operativos macOS X:
sudo macos/build-pkg.sh sudo macos/build-pkg.sh
@ -24,8 +27,8 @@ Crear instaladores de OGAgent
- Crear el programa instalador para sistemas operativos Windows: - Crear el programa instalador para sistemas operativos Windows:
windows/build-windows.sh windows/build-windows.sh
- Subir los nuevos ficheros .deb, .pkg y .exe generados al directorio - Subir los nuevos ficheros .deb, .rpm, .pkg y .exe generados al directorio
/opt/opengnsys/www/descargas del servidor OpenGnsys. /opt/opengnsys/www/descargas del servidor OpenGnsys.
Instalar OGAgent en cliente modelo Instalar OGAgent en cliente modelo
@ -40,6 +43,20 @@ Instalar OGAgent en cliente modelo
- Iniciar el servicio (se iniciará automáticamente en el proceso de arranque): - Iniciar el servicio (se iniciará automáticamente en el proceso de arranque):
sudo service ogagent start sudo service ogagent start
- Red Hat, Fedora y derivados (como root):
- Descargar e instalar el agente:
yum install ogagent-Version.noarch.rpm (Red Hat/CentOS)
dnf install ogagent-Version.noarch.rpm (Fedora)
- Configurar el agente:
sed -i "0,/remote=/ s,remote=.*,remote=https://IPServidorOpenGnsys/opengnsys/rest/," /usr/share/OGAgent/cfg/ogagent.cfg
- Puede ser necesario corregir permisos antes de iniciar el servicio:
chmod +x /etc/init.d/ogagent
- Iniciar el servicio (se iniciará automáticamente en el proceso de arranque):
service ogagent start
- OpenSuSE:
(en preparación)
- Windows (como usuario administrador): - Windows (como usuario administrador):
- Descargar e instalar el agente ejecutando: - Descargar e instalar el agente ejecutando:
OGAgentSetup-Version.exe OGAgentSetup-Version.exe
@ -66,3 +83,5 @@ Postconfiguración para clientes clonados
- Ejecutar manualmente o configurar automáticamente OGAgent en los clientes clonados - Ejecutar manualmente o configurar automáticamente OGAgent en los clientes clonados
en el script de postconfiguración tras restuarar imagen: en el script de postconfiguración tras restuarar imagen:
ogConfigureOgagent NDisco Npart ogConfigureOgagent NDisco Npart

11
Jenkinsfile vendored
View File

@ -1,11 +0,0 @@
pipeline {
agent any
stages {
stage('Build') {
steps {
echo 'Jenkinsfile for ogagent'
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View File

@ -1,47 +0,0 @@
## Crear tarea programada para matar el agente de Windows al cerrar sesión
1. Abrir el task scheduler y pinchar en Create task:
![image info](./sched01.png)
2. Rellenar el nombre y luego pinchar en Change user or group:
![image info](./sched02.png)
3. Pinchar en Advanced:
![image info](./sched03.png)
4. Pinchar en Find now:
![image info](./sched04.png)
5. Seleccionar Administrator y luego Ok, y luego Ok:
![image info](./sched05.png)
6. De vuelta en la pantalla de crear tarea, ir a la pestaña Triggers y pinchar en New:
![image info](./sched06.png)
7. Seleccionar "On disconnect from user session", seleccionar "Connection from local computer", y luego Ok:
![image info](./sched07.png)
8. Ir a la pestaña "Actions" y pinchar en New:
![image info](./sched08.png)
9. Pinchar en Browse, seleccionar C:\Program Files (x86)\OGAgent\stop-agent.ps1, y luego Ok:
![image info](./sched09.png)
10. Ir a la pestaña Conditions y desmarcar las condiciones relativas a Power:
![image info](./sched10.png)
11. Pinchar Ok y la tarea programada queda creada.
En caso de usar remotepc, hay que repetir todos estos pasos, es decir, crear una
tarea programada nueva, seleccionando "Connection from remote computer" en lugar
de "Connection from local computer".

View File

@ -39,6 +39,7 @@ install-ogagent:
cp $(SOURCEDIR)/OGAgentUser.py $(LIBDIR) cp $(SOURCEDIR)/OGAgentUser.py $(LIBDIR)
# QT Dialogs & resources # QT Dialogs & resources
cp $(SOURCEDIR)/*_ui.py $(LIBDIR) cp $(SOURCEDIR)/*_ui.py $(LIBDIR)
cp $(SOURCEDIR)/OGAgent_rc.py $(LIBDIR)
# Version file # Version file
cp $(SOURCEDIR)/VERSION $(LIBDIR) cp $(SOURCEDIR)/VERSION $(LIBDIR)
@ -67,6 +68,4 @@ ifeq ($(DISTRO),rh)
endif endif
uninstall: uninstall:
rm -rf $(LIBDIR) rm -rf $(LIBDIR) $(CFGDIR) $(BINDIR)/ogagent $(INITDIR)/ogagent
# rm -f $(BINDIR)/ogagent
rm -rf $(CFGDIR)

View File

@ -1,6 +1,37 @@
#!/bin/bash #!/bin/bash
cd $(dirname "$0") cd $(dirname "$0")
top=$(pwd)
VERSION="$(cat ../src/VERSION 2>/dev/null)" || VERSION="1.1.1"
RELEASE="1"
# Debian based # Debian based
debuild -D --build=binary --post-clean --lintian-opts --profile debian dpkg-buildpackage -b -d
# Fix version number.
sed -e "s/version 0.0.0/version ${VERSION}/g" \
-e "s/release 1/release ${RELEASE}/g" ogagent-template.spec > ogagent-$VERSION.spec
# Now fix dependencies for opensuse
sed -e "s/name ogagent/name ogagent-opensuse/g" \
-e "s/version 0.0.0/version ${VERSION}/g" \
-e "s/release 1/release ${RELEASE}/g" \
-e "s/chkconfig//g" \
-e "s/initscripts/insserv/g" \
-e "s/PyQt4/python-qt4/g" \
-e "s/libXScrnSaver/libXss1/g" ogagent-template.spec > ogagent-opensuse-$VERSION.spec
# Right now, ogagent-xrdp-1.7.0.spec is not needed
for pkg in ogagent-$VERSION.spec ogagent-opensuse-$VERSION.spec; do
rm -rf rpm
for folder in SOURCES BUILD RPMS SPECS SRPMS; do
mkdir -p rpm/$folder
done
rpmbuild -v -bb --clean --buildroot=$top/rpm/BUILD/$pkg-root --target noarch $pkg 2>&1
done
#rm ogagent-$VERSION

View File

@ -1,291 +1,3 @@
ogagent (5.7.0-1) stable; urgency=medium
* Use TLS again
-- OpenGnsys developers <info@opengnsys.es> Wed, 21 May 2025 17:39:13 +0200
ogagent (5.6.0-1) stable; urgency=medium
* Execute 'launch_browser' rather than 'browser'
* Change URLs using dbus
-- OpenGnsys developers <info@opengnsys.es> Wed, 21 May 2025 15:06:52 +0200
ogagent (5.5.0-1) stable; urgency=medium
* Return to the QT4 browser again
-- OpenGnsys developers <info@opengnsys.es> Mon, 19 May 2025 10:57:37 +0200
ogagent (5.4.0-1) stable; urgency=medium
* Disable TLS on request
-- OpenGnsys developers <info@opengnsys.es> Mon, 19 May 2025 09:46:42 +0200
ogagent (5.3.0-1) stable; urgency=medium
* Execute 'launch_browser' rather than 'browser'
-- OpenGnsys developers <info@opengnsys.es> Wed, 14 May 2025 10:50:15 +0200
ogagent (5.2.0-1) stable; urgency=medium
* Log length of user sessions
-- OpenGnsys developers <info@opengnsys.es> Mon, 12 May 2025 11:38:27 +0200
ogagent (5.1.1-1) stable; urgency=medium
* Fix URL for notifying stop to ogcore
-- OpenGnsys developers <info@opengnsys.es> Tue, 06 May 2025 13:31:48 +0200
ogagent (5.1.0-1) stable; urgency=medium
* Include powershell helper script for logging out of windows
-- OpenGnsys developers <info@opengnsys.es> Tue, 06 May 2025 13:30:59 +0200
ogagent (5.0.0-1) stable; urgency=medium
* Use TLS
-- OpenGnsys developers <info@opengnsys.es> Fri, 25 Apr 2025 13:09:49 +0200
ogagent (4.0.0-1) stable; urgency=medium
* Handle authn/authz in the oglive agent
-- OpenGnsys developers <info@opengnsys.es> Thu, 24 Apr 2025 13:28:57 +0200
ogagent (3.3.0-1) stable; urgency=medium
* Create an additional json log file
-- OpenGnsys developers <info@opengnsys.es> Mon, 14 Apr 2025 13:50:32 +0200
ogagent (3.2.0-1) stable; urgency=medium
* Operating system: periodically ping ogcore
-- OpenGnsys developers <info@opengnsys.es> Thu, 10 Apr 2025 11:37:35 +0200
ogagent (3.1.0-1) stable; urgency=medium
* Oglive: periodically ping ogcore
-- OpenGnsys developers <info@opengnsys.es> Mon, 07 Apr 2025 11:50:05 +0200
ogagent (3.0.0-1) stable; urgency=medium
* Ignore module provided in the URLs to the API
-- OpenGnsys developers <info@opengnsys.es> Mon, 31 Mar 2025 10:16:07 +0200
ogagent (2.0.0-1) stable; urgency=medium
* EjecutarScript/ConsolaRemota: expect "scp" parameter encoded in base64
-- OpenGnsys developers <info@opengnsys.es> Wed, 26 Mar 2025 10:40:14 +0100
ogagent (1.7.1-1) stable; urgency=medium
* Make cfg2obj more robust
-- OpenGnsys developers <info@opengnsys.es> Tue, 25 Mar 2025 13:31:33 +0100
ogagent (1.7.0-1) stable; urgency=medium
* Delete the new "ptt" parameter. It's not needed.
-- OpenGnsys developers <info@opengnsys.es> Fri, 21 Mar 2025 14:19:56 +0100
ogagent (1.6.0-1) stable; urgency=medium
* Don't invoke bash code for some functionalities
-- OpenGnsys developers <info@opengnsys.es> Wed, 12 Mar 2025 11:59:36 +0100
ogagent (1.5.0-1) stable; urgency=medium
* Accept new "ptt" parameter in /ogAdmCli/Configurar
* No longer recognise the unused "che" parameter in /ogAdmCli/Configurar
-- OpenGnsys developers <info@opengnsys.es> Wed, 12 Mar 2025 11:45:37 +0100
ogagent (1.4.9-1) stable; urgency=medium
* Notify ogcore when agent shuts down within oglive
-- OpenGnsys developers <info@opengnsys.es> Thu, 20 Feb 2025 11:58:29 +0100
ogagent (1.4.8-1) stable; urgency=medium
* Optionally return disk config in /status
-- OpenGnsys developers <info@opengnsys.es> Tue, 18 Feb 2025 13:48:54 +0100
ogagent (1.4.7-1) stable; urgency=medium
* Merge server modules
* Track the progress of children
-- OpenGnsys developers <info@opengnsys.es> Tue, 04 Feb 2025 14:12:19 +0100
ogagent (1.4.6-1) stable; urgency=medium
* Point to the new menu browser
-- OpenGnsys developers <info@opengnsys.es> Tue, 14 Jan 2025 12:00:24 +0100
ogagent (1.4.5-1) stable; urgency=medium
* Kill long running jobs in oglive
-- OpenGnsys developers <info@opengnsys.es> Fri, 29 Nov 2024 10:22:36 +0100
ogagent (1.4.5~pre8-1) stable; urgency=medium
* Add Configurar() to the CloningEngine module
-- OpenGnsys developers <info@opengnsys.es> Wed, 27 Nov 2024 20:02:42 +0100
ogagent (1.4.5~pre7-1) stable; urgency=medium
* Use old browser again
-- OpenGnsys developers <info@opengnsys.es> Wed, 20 Nov 2024 14:24:44 +0100
ogagent (1.4.5~pre6-1) stable; urgency=medium
* Do not use envvars for the operating-system module
-- OpenGnsys developers <info@opengnsys.es> Wed, 20 Nov 2024 13:45:21 +0100
ogagent (1.4.5~pre5-1) stable; urgency=medium
* Avoid some KeyErrors
-- OpenGnsys developers <info@opengnsys.es> Mon, 18 Nov 2024 12:14:27 +0100
ogagent (1.4.5~pre4-1) stable; urgency=medium
* Don't die when ogcore returns HTTP 4xx or 5xx
* Get ogcore IP and port from the environment
-- OpenGnsys developers <info@opengnsys.es> Fri, 15 Nov 2024 11:43:01 +0100
ogagent (1.4.5~pre3-1) stable; urgency=medium
* Kill long running jobs in oglive (not-yet-working draft)
-- OpenGnsys developers <info@opengnsys.es> Wed, 06 Nov 2024 14:11:32 +0100
ogagent (1.4.5~pre2-1) stable; urgency=medium
* Remove race condition due to several monitoring threads
* Include job_id in asynchronous responses
* Remove vim swapfiles from the package contents
-- OpenGnsys developers <info@opengnsys.es> Wed, 06 Nov 2024 13:24:03 +0100
ogagent (1.4.5~pre1-1) stable; urgency=medium
* CrearImagen: return inventory inline
-- OpenGnsys developers <info@opengnsys.es> Wed, 06 Nov 2024 12:41:14 +0100
ogagent (1.4.4-1) stable; urgency=medium
* Use logger.debug() to prevent the windows agent from dying
* Make status() call synchronous
-- OpenGnsys developers <info@opengnsys.es> Thu, 17 Oct 2024 19:13:58 +0200
ogagent (1.4.3-1) stable; urgency=medium
* Use new OGBrowser
-- OpenGnsys developers <info@opengnsys.es> Thu, 17 Oct 2024 09:43:08 +0200
ogagent (1.4.2-1) stable; urgency=medium
* Have ogAdmClient/status return information about network and disks
-- OpenGnsys developers <info@opengnsys.es> Tue, 15 Oct 2024 10:35:16 +0200
ogagent (1.4.1-1) stable; urgency=medium
* Bugfix: move data structure to the right class
-- OpenGnsys developers <info@opengnsys.es> Fri, 11 Oct 2024 13:51:55 +0200
ogagent (1.4.0-1) stable; urgency=medium
* Add more functionality
* Begin using semantic versioning
-- OpenGnsys developers <info@opengnsys.es> Fri, 11 Oct 2024 13:06:51 +0200
ogagent (1.3.8-1) stable; urgency=medium
* Add more functionality to the ogAdmClient module
-- OpenGnsys developers <info@opengnsys.es> Tue, 01 Oct 2024 13:41:48 +0200
ogagent (1.3.7-1) stable; urgency=medium
* CloningEngine: RESTfully keep a list of long-running jobs
-- OpenGnsys developers <info@opengnsys.es> Fri, 27 Sep 2024 18:03:16 +0200
ogagent (1.3.6-1) stable; urgency=medium
* Add more functionality to the ogAdmClient module
* Add CloningEngine module
-- OpenGnsys developers <info@opengnsys.es> Thu, 19 Sep 2024 13:28:17 +0200
ogagent (1.3.5-1) stable; urgency=medium
* Don't unconditionally load modules--dynamically load everything
* Remove old, unused code
-- OpenGnsys developers <info@opengnsys.es> Thu, 29 Aug 2024 10:47:12 +0200
ogagent (1.3.4-1) stable; urgency=medium
* Implement JobMgr
-- OpenGnsys developers <info@opengnsys.es> Tue, 30 Jul 2024 13:39:55 +0200
ogagent (1.3.1-1) stable; urgency=medium
* Migrate the update script from shell to python
* pyinstaller: include the 'img' subdir
* take icons from 'img'
-- OpenGnsys developers <info@opengnsys.es> Wed, 26 Jun 2024 15:16:57 +0200
ogagent (1.3.0-2) stable; urgency=medium
* Add missing dependency on zenity
-- OpenGnsys developers <info@opengnsys.es> Thu, 25 Apr 2024 15:53:16 +0200
ogagent (1.3.0-1) stable; urgency=medium
* Upgrade to Qt 6
-- OpenGnsys developers <info@opengnsys.es> Thu, 25 Apr 2024 12:50:20 +0200
ogagent (1.2.0) unstable; urgency=medium
* Python 3 and Qt 5 compatibility
-- OpenGnsys developers <info@opengnsys.es> Mon, 4 May 2020 18:00:00 +0100
ogagent (1.1.1b) stable; urgency=medium ogagent (1.1.1b) stable; urgency=medium
* Use python-distro to detect the distribution version * Use python-distro to detect the distribution version

View File

@ -1 +1 @@
10 9

View File

@ -1,7 +1,7 @@
Source: ogagent Source: ogagent
Section: admin Section: admin
Priority: optional Priority: optional
Maintainer: OpenGnsys developers <info@opengnsys.es> Maintainer: Ramón M. Gómez <ramongomez@us.es>
Build-Depends: debhelper (>= 7), po-debconf Build-Depends: debhelper (>= 7), po-debconf
Standards-Version: 3.9.2 Standards-Version: 3.9.2
Homepage: https://opengnsys.es/ Homepage: https://opengnsys.es/
@ -11,7 +11,8 @@ Section: admin
Priority: optional Priority: optional
Architecture: all Architecture: all
Depends: Depends:
policykit-1 (>= 0.100), python3 (>=3.4) | python (>= 3.4), python3-pyqt6, python3-requests, policykit-1 (>= 0.100), python2 (>=2.7) | python (>= 2.7), python-qt4 (>= 4.9),, python-requests (>= 0.8.2),
python3-six, python3-prctl, python3-distro, libxss1, zenity, ${misc:Depends} python-six (>= 1.1), python-prctl (>= 1.1.1), python-distro, libxss1, ${misc:Depends}
Suggests: gnome-shell-extension-top-icons-plus
Description: OpenGnsys Agent for Operating Systems Description: OpenGnsys Agent for Operating Systems
This package provides the required components to allow this machine to work on an environment managed by OpenGnsys. This package provides the required components to allow this machine to work on an environment managed by OpenGnsys.

View File

@ -1,6 +1,6 @@
Format-Specification: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=135 Format-Specification: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=135
Name: ogagent Name: ogagent
Maintainer: OpenGnsys developers Maintainer: Ramón M. Gómez
Source: https://opengnsys.es Source: https://opengnsys.es
Copyright: 2014 Virtual Cable S.L.U. Copyright: 2014 Virtual Cable S.L.U.

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
misc:Depends=
misc:Pre-Depends=

View File

@ -22,7 +22,6 @@ install: build
dh_prep dh_prep
dh_installdirs dh_installdirs
$(MAKE) DESTDIR=$(CURDIR)/debian/ogagent install-ogagent $(MAKE) DESTDIR=$(CURDIR)/debian/ogagent install-ogagent
find $(CURDIR) -name '*.swp' -exec rm -f '{}' ';'
binary-arch: build install binary-arch: build install
# emptyness # emptyness
binary-indep: build install binary-indep: build install
@ -32,7 +31,7 @@ binary-indep: build install
dh_installdocs dh_installdocs
dh_installdebconf dh_installdebconf
dh_installinit --no-start dh_installinit --no-start
dh_python3=python dh_python2=python
dh_compress dh_compress
dh_link dh_link
dh_fixperms dh_fixperms

0
linux/desktop/OGAgentTool.desktop 100644 → 100755
View File

View File

@ -0,0 +1,83 @@
%define _topdir %(echo $PWD)/rpm
%define name ogagent
%define version 0.0.0
%define release 1
%define buildroot %{_topdir}/%{name}-%{version}-%{release}-root
BuildRoot: %{buildroot}
Name: %{name}
Version: %{version}
Release: %{release}
Summary: OpenGnsys Agent for Operating Systems
License: BSD3
Group: Admin
Requires: chkconfig initscripts python-six python-requests python-distro PyQt4 libXScrnSaver
Vendor: OpenGnsys Project
URL: https://opengnsys.es/
Provides: ogagent
%define _rpmdir ../
%define _rpmfilename %%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm
%install
curdir=`pwd`
cd ../..
make DESTDIR=$RPM_BUILD_ROOT DISTRO=rh install-ogagent
cd $curdir
%clean
rm -rf $RPM_BUILD_ROOT
curdir=`pwd`
cd ../..
make DESTDIR=$RPM_BUILD_ROOT DISTRO=rh clean
cd $curdir
%post
systemctl enable ogagent.service > /dev/null 2>&1
%preun
systemctl disable ogagent.service > /dev/null 2>&1
systemctl stop ogagent.service > /dev/null 2>&1
%postun
# $1 == 0 on uninstall, == 1 on upgrade for preun and postun (just a reminder for me... :) )
if [ $1 -eq 0 ]; then
rm -rf /etc/ogagent
rm /var/log/ogagent.log
fi
# And, posibly, the .pyc leaved behind on /usr/share/OGAgent
rm -rf /usr/share/OGAgent > /dev/null 2>&1
%description
This package provides the required components to allow this machine to work on an environment managed by OpenGnsys.
%files
%defattr(-,root,root)
/etc/ogagent
/etc/xdg/autostart/OGAgentTool.desktop
/etc/init.d/ogagent
/usr/bin/OGAgentTool-startup
/usr/bin/ogagent
/usr/bin/OGAgentTool
/usr/share/OGAgent/*
/usr/share/autostart/OGAgentTool.desktop
%changelog
* Fri Feb 07 2020 Ramón M. Gómez <ramongomez@us.es> - 1.1.1b-1
- Use python-distro to detect the distribution version
* Thu May 23 2019 Ramón M. Gómez <ramongomez@us.es> - 1.1.1-1
- Set connection timeout
- Compatibility with "Exam Mode" from the University of Seville
* Wed May 22 2019 Ramón M. Gómez <ramongomez@us.es> - 1.1.0a-1
- Fix a bug when activating the agent with some network devices
* Tue Oct 13 2016 Ramón M. Gómez <ramongomez@us.es> - 1.1.0-1
- Functional OpenGnsys Agent interacting with OpenGnsys Server 1.1.0
* Tue Jul 18 2015 Adolfo Gómez García <agomez@virtualcable.es> - 1.0.0-1
- Initial release for OpenGnsys Agent

View File

@ -1,10 +1,10 @@
#!/bin/sh #!/bin/sh
for p in python python3; do for p in python python2; do
[ "$(command -v $p)" ] && [ -z "$PYTHON" ] && [ $($p -c 'import sys; print(sys.version_info[0])') -eq 3 ] && PYTHON=$p [ -z "$PYTHON" ] && [ $($p -c 'import sys; print(sys.version_info[0])') -eq 2 ] && PYTHON=$p
done done
if [ -z "$PYTHON" ]; then if [ -z "$PYTHON" ]; then
echo "ERROR: OGAgent needs Python 3" &>2 echo "ERROR: OGAgent needs Python 2" &>2
exit 1 exit 1
fi fi

View File

@ -1,10 +1,10 @@
#!/bin/sh #!/bin/sh
for p in python python3; do for p in python python2; do
[ "$(command -v $p)" ] && [ -z "$PYTHON" ] && [ $($p -c 'import sys; print(sys.version_info[0])') -eq 3 ] && PYTHON=$p [ -z "$PYTHON" ] && [ $($p -c 'import sys; print(sys.version_info[0])') -eq 2 ] && PYTHON=$p
done done
if [ -z "$PYTHON" ]; then if [ -z "$PYTHON" ]; then
echo "ERROR: OGAgent needs Python 3" &>2 echo "ERROR: OGAgent needs Python 2" &>2
exit 1 exit 1
fi fi

View File

@ -1,9 +1,9 @@
#!/bin/bash #!/bin/bash
# Create macOS installation packages. # Create macOS installation packages.
# Based on bomutils tutorial: http://bomutils.dyndns.org/tutorial.html # Based on bomutils tutorail: http://bomutils.dyndns.org/tutorial.html
cd $(dirname $0) cd $(dirname $0)
[ -r ../src/VERSION ] && VERSION="$(cat ../src/VERSION)" || (echo "Can't get version from ../src/VERSION" 1>&2; exit 1) [ -r ../src/VERSION ] && VERSION="$(cat ../src/VERSION)" || VERSION="1.1.0"
AUTHOR="OpenGnsys Project" AUTHOR="OpenGnsys Project"
# Create empty directories. # Create empty directories.
@ -12,7 +12,7 @@ mkdir -p build && cd build
mkdir -p flat/base.pkg flat/Resources/en.lproj mkdir -p flat/base.pkg flat/Resources/en.lproj
mkdir -p root/Applications mkdir -p root/Applications
# Copy application and script files # Copy application and script files.
cp -a ../../src root/Applications/OGAgent.app cp -a ../../src root/Applications/OGAgent.app
cp -a ../scripts . cp -a ../scripts .
@ -84,3 +84,4 @@ EOT
# Create new Xar application archive. # Create new Xar application archive.
rm -f ../../../OGAgentInstaller-$VERSION.pkg rm -f ../../../OGAgentInstaller-$VERSION.pkg
( cd flat && xar --compression none -cf "../../../OGAgentInstaller-$VERSION.pkg" * ) ( cd flat && xar --compression none -cf "../../../OGAgentInstaller-$VERSION.pkg" * )

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC -//Apple Computer//DTD PLIST 1.0//EN http://www.apple.com/DTDs/PropertyList-1.0.dtd>
<plist version="1.0">
<dict>
<key>Label</key>
<string>es.opengnsys.agent.user</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin</string>
</dict>
<key>WorkingDirectory</key>
<string>/Applications/OGAgent.app</string>
<key>ProgramArguments</key>
<array>
<string>/Applications/OGAgent.app/OGAgentUser.py</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>

View File

@ -2,10 +2,10 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>Label</key> <key>Label</key>
<string>es.opengnsys.agent.system</string> <string>es.opengnsys.ogagent</string>
<key>ProgramArguments</key> <key>ProgramArguments</key>
<array> <array>
<string>/usr/local/bin/ogagent</string> <string>/usr/bin/ogagent</string>
<string>start</string> <string>start</string>
</array> </array>
<key>RunAtLoad</key> <key>RunAtLoad</key>

View File

@ -1,832 +0,0 @@
#!/usr/bin/env python3
# encoding: utf8
"""
This program is taken from https://github.com/brona/iproute2mac
When doing 'brew install iproute2mac', we get an old version (1.4.2) that doesn't support 'ip -json'
The alternative installation method recomended in the project's README file is 'curl; chmod; mv'
Therefore we make the decision of shipping this ip.py (version 1.5.0) along opengnsys, which is pretty much the same as curling it
"""
"""
iproute2mac
CLI wrapper for basic network utilites on Mac OS X.
Homepage: https://github.com/brona/iproute2mac
The MIT License (MIT)
Copyright (c) 2015 Bronislav Robenek <brona@robenek.me>
"""
import ipaddress
import json
import os
import random
import re
import socket
import string
import subprocess
import sys
import types
# Version
VERSION = "1.5.0"
# Utilities
SUDO = "/usr/bin/sudo"
IFCONFIG = "/sbin/ifconfig"
ROUTE = "/sbin/route"
NETSTAT = "/usr/sbin/netstat"
NDP = "/usr/sbin/ndp"
ARP = "/usr/sbin/arp"
NETWORKSETUP = "/usr/sbin/networksetup"
# Helper functions
def perror(*args):
sys.stderr.write(*args)
sys.stderr.write("\n")
def execute_cmd(cmd):
print("Executing: %s" % cmd)
status, output = subprocess.getstatusoutput(cmd)
if status == 0: # unix/linux commands 0 true, 1 false
print(output)
return True
else:
perror(output)
return False
def json_dump(data, pretty):
if pretty:
print(json.dumps(data, indent=4))
else:
print(json.dumps(data, separators=(",", ":")))
return True
# Classful to CIDR conversion with "default" being passed through
def cidr_from_netstat_dst(target):
if target == "default":
return target
dots = target.count(".")
if target.find("/") == -1:
addr = target
netmask = (dots + 1) * 8
else:
[addr, netmask] = target.split("/")
addr = addr + ".0" * (3 - dots)
return addr + "/" + str(netmask)
# Convert hexadecimal netmask in prefix length
def netmask_to_length(mask):
return int(mask, 16).bit_count()
def any_startswith(words, test):
for word in words:
if word.startswith(test):
return True
return False
# Handles passsing return value, error messages and program exit on error
def help_msg(help_func):
def wrapper(func):
def inner(*args, **kwargs):
if not func(*args, **kwargs):
specific = eval(help_func)
if specific:
if isinstance(specific, types.FunctionType):
if args and kwargs:
specific(*args, **kwargs)
else:
specific()
return False
else:
raise Exception("Function expected for: " + help_func)
else:
raise Exception(
"Function variant not defined: " + help_func
)
return True
return inner
return wrapper
# Generate random MAC address with XenSource Inc. OUI
# http://www.linux-kvm.com/sites/default/files/macgen.py
def randomMAC():
mac = [
0x00,
0x16,
0x3E,
random.randint(0x00, 0x7F),
random.randint(0x00, 0xFF),
random.randint(0x00, 0xFF),
]
return ":".join(["%02x" % x for x in mac])
# Decode ifconfig output
def parse_ifconfig(res, af, address):
links = []
count = 1
for r in res.split("\n"):
if re.match(r"^\w+:", r):
if count > 1:
links.append(link)
(ifname, flags, mtu, ifindex) = re.findall(r"^(\w+): flags=\d+<(.*)> mtu (\d+) index (\d+)", r)[0]
flags = flags.split(",")
link = {
"ifindex": int(ifindex),
"ifname": ifname,
"flags": flags,
"mtu": int(mtu),
"operstate": "UNKNOWN",
"link_type": "unknown"
}
if "LOOPBACK" in flags:
link["link_type"] = "loopback"
link["address"] = "00:00:00:00:00:00"
link["broadcast"] = "00:00:00:00:00:00"
elif "POINTOPOINT" in flags:
link["link_type"] = "none"
count = count + 1
else:
if re.match(r"^\s+ether ", r):
link["link_type"] = "ether"
link["address"] = re.findall(r"(\w\w:\w\w:\w\w:\w\w:\w\w:\w\w)", r)[0]
link["broadcast"] = "ff:ff:ff:ff:ff:ff"
elif address and re.match(r"^\s+inet ", r) and af != 6:
(local, netmask) = re.findall(r"inet (\d+\.\d+\.\d+\.\d+) netmask (0x[0-9a-f]+)", r)[0]
addr = {
"family": "inet",
"local": local,
"prefixlen": netmask_to_length(netmask),
}
if re.match(r"^.*broadcast", r):
addr["broadcast"] = re.findall(r"broadcast (\d+\.\d+\.\d+\.\d+)", r)[0]
link["addr_info"] = link.get("addr_info", []) + [addr]
elif address and re.match(r"^\s+inet6 ", r) and af != 4:
(local, prefixlen) = re.findall(r"inet6 ((?:[a-f0-9:]+:+)+[a-f0-9]+)%*\w* +prefixlen (\d+)", r)[0]
link["addr_info"] = link.get("addr_info", []) + [{
"family": "inet6",
"local": local,
"prefixlen": int(prefixlen)
}]
elif re.match(r"^\s+status: ", r):
match re.findall(r"status: (\w+)", r)[0]:
case "active":
link["operstate"] = "UP"
case "inactive":
link["operstate"] = "DOWN"
if count > 1:
links.append(link)
return links
def link_addr_show(argv, af, json_print, pretty_json, address):
if len(argv) > 0 and argv[0] == "dev":
argv.pop(0)
if len(argv) > 0:
param = argv[0]
else:
param = "-a"
status, res = subprocess.getstatusoutput(
IFCONFIG + " -v " + param + " 2>/dev/null"
)
if status: # unix status
if res == "":
perror(param + " not found")
else:
perror(res)
return False
links = parse_ifconfig(res, af, address)
if json_print:
return json_dump(links, pretty_json)
for l in links:
print("%d: %s: <%s> mtu %d status %s" % (
l["ifindex"], l["ifname"], ",".join(l["flags"]), l["mtu"], l["operstate"]
))
print(
" link/" + l["link_type"] +
((" " + l["address"]) if "address" in l else "") +
((" brd " + l["broadcast"]) if "broadcast" in l else "")
)
for a in l.get("addr_info", []):
print(
" %s %s/%d" % (a["family"], a["local"], a["prefixlen"]) +
((" brd " + a["broadcast"]) if "broadcast" in a else "")
)
return True
# Help
def do_help(argv=None, af=None, json_print=None, pretty_json=None):
perror("Usage: ip [ OPTIONS ] OBJECT { COMMAND | help }")
perror("where OBJECT := { link | addr | route | neigh }")
perror(" OPTIONS := { -V[ersion] | -j[son] | -p[retty] |")
perror(" -4 | -6 }")
perror("iproute2mac")
perror("Homepage: https://github.com/brona/iproute2mac")
perror(
"This is CLI wrapper for basic network utilities on Mac OS X"
" inspired with iproute2 on Linux systems."
)
perror(
"Provided functionality is limited and command output is not"
" fully compatible with iproute2."
)
perror(
"For advanced usage use netstat, ifconfig, ndp, arp, route "
" and networksetup directly."
)
exit(255)
def do_help_route():
perror("Usage: ip route list")
perror(" ip route get ADDRESS")
perror(" ip route { add | del | replace } ROUTE")
perror(" ip route flush cache")
perror(" ip route flush table main")
perror("ROUTE := NODE_SPEC [ INFO_SPEC ]")
perror("NODE_SPEC := [ TYPE ] PREFIX")
perror("INFO_SPEC := NH")
perror("TYPE := { blackhole }")
perror("NH := { via ADDRESS | gw ADDRESS | nexthop ADDRESS | dev STRING }")
exit(255)
def do_help_addr():
perror("Usage: ip addr show [ dev STRING ]")
perror(" ip addr { add | del } PREFIX dev STRING")
exit(255)
def do_help_link():
perror("Usage: ip link show [ DEVICE ]")
perror(" ip link set dev DEVICE")
perror(" [ { up | down } ]")
perror(" [ address { LLADDR | factory | random } ]")
perror(" [ mtu MTU ]")
exit(255)
def do_help_neigh():
perror("Usage: ip neighbour show [ [ to ] PREFIX ] [ dev DEV ]")
perror(" ip neighbour flush [ dev DEV ]")
exit(255)
# Route Module
@help_msg("do_help_route")
def do_route(argv, af, json_print, pretty_json):
if not argv or (
any_startswith(["show", "lst", "list"], argv[0]) and len(argv) == 1
):
return do_route_list(af, json_print, pretty_json)
elif "get".startswith(argv[0]) and len(argv) == 2:
argv.pop(0)
return do_route_get(argv, af, json_print, pretty_json)
elif "add".startswith(argv[0]) and len(argv) >= 3:
argv.pop(0)
return do_route_add(argv, af)
elif "delete".startswith(argv[0]) and len(argv) >= 2:
argv.pop(0)
return do_route_del(argv, af)
elif "replace".startswith(argv[0]) and len(argv) >= 3:
argv.pop(0)
return do_route_del(argv, af) and do_route_add(argv, af)
elif "flush".startswith(argv[0]) and len(argv) >= 1:
argv.pop(0)
return do_route_flush(argv, af)
else:
return False
return True
def do_route_list(af, json_print, pretty_json):
# ip route prints IPv6 or IPv4, never both
inet = "inet6" if af == 6 else "inet"
status, res = subprocess.getstatusoutput(
NETSTAT + " -nr -f " + inet + " 2>/dev/null"
)
if status:
perror(res)
return False
res = res.split("\n")
res = res[4:] # Removes first 4 lines
routes = []
for r in res:
ra = r.split()
target = ra[0]
gw = ra[1]
flags = ra[2]
# macOS Mojave and earlier vs Catalina
dev = ra[5] if len(ra) >= 6 else ra[3]
if flags.find("W") != -1:
continue
if af == 6:
target = re.sub(r"%[^ ]+/", "/", target)
else:
target = cidr_from_netstat_dst(target)
if flags.find("B") != -1:
routes.append({"type": "blackhole", "dst": target, "flags": []})
continue
if re.match(r"link.+", gw):
routes.append({"dst": target, "dev": dev, "scope": "link", "flags": []})
else:
routes.append({"dst": target, "gateway": gw, "dev": dev, "flags": []})
if json_print:
return json_dump(routes, pretty_json)
for route in routes:
if "type" in route:
print("%s %s" % (route["type"], route["dst"]))
elif "scope" in route:
print("%s dev %s scope %s" % (route["dst"], route["dev"], route["scope"]))
elif "gateway" in route:
print("%s via %s dev %s" % (route["dst"], route["gateway"], route["dev"]))
return True
def do_route_add(argv, af):
options = ""
if argv[0] == "blackhole":
argv.pop(0)
if len(argv) != 1:
return False
argv.append("via")
argv.append("::1" if ":" in argv[0] or af == 6 else "127.0.0.1")
options = "-blackhole"
if len(argv) not in (3, 5):
return False
if len(argv) == 5:
perror(
"iproute2mac: Ignoring last 2 arguments, not implemented: {} {}".format(
argv[3], argv[4]
)
)
if argv[1] in ["via", "nexthop", "gw"]:
gw = argv[2]
elif argv[1] in ["dev"]:
gw = "-interface " + argv[2]
else:
do_help_route()
prefix = argv[0]
inet = "-inet6 " if ":" in prefix or af == 6 else ""
return execute_cmd(
SUDO + " " + ROUTE + " add " + inet + prefix + " " + gw + " " + options
)
def do_route_del(argv, af):
options = ""
if argv[0] == "blackhole":
argv.pop(0)
if len(argv) != 1:
return False
if ":" in argv[0] or af == 6:
options = " ::1 -blackhole"
else:
options = " 127.0.0.1 -blackhole"
prefix = argv[0]
inet = "-inet6 " if ":" in prefix or af == 6 else ""
return execute_cmd(
SUDO + " " + ROUTE + " delete " + inet + prefix + options
)
def do_route_flush(argv, af):
if not argv:
perror('"ip route flush" requires arguments.')
perror("")
return False
# https://github.com/brona/iproute2mac/issues/38
# http://linux-ip.net/html/tools-ip-route.html
if argv[0] == "cache":
print("iproute2mac: There is no route cache to flush in MacOS,")
print(" returning 0 status code for compatibility.")
return True
elif len(argv) == 2 and argv[0] == "table" and argv[1] == "main":
family = "-inet6" if af == 6 else "-inet"
print("iproute2mac: Flushing all routes")
return execute_cmd(SUDO + " " + ROUTE + " -n flush " + family)
else:
return False
def do_route_get(argv, af, json_print, pretty_json):
target = argv[0]
inet = ""
if ":" in target or af == 6:
inet = "-inet6 "
family = socket.AF_INET6
else:
family = socket.AF_INET
status, res = subprocess.getstatusoutput(
ROUTE + " -n get " + inet + target
)
if status: # unix status or not in table
perror(res)
return False
if res.find("not in table") >= 0:
perror(res)
exit(1)
res = dict(
re.findall(
r"^\W*((?:route to|destination|gateway|interface)): (.+)$",
res,
re.MULTILINE,
)
)
route = {"dst": res["route to"], "dev": res["interface"]}
if "gateway" in res:
route["gateway"] = res["gateway"]
try:
s = socket.socket(family, socket.SOCK_DGRAM)
s.connect((route["dst"], 7))
route["prefsrc"] = src_ip = s.getsockname()[0]
s.close()
except:
pass
route["flags"] = []
route["uid"] = os.getuid()
route["cache"] = []
if json_print:
return json_dump([route], pretty_json)
print(
route["dst"] +
((" via " + route["gateway"]) if "gateway" in route else "") +
" dev " + route["dev"] +
((" src " + route["prefsrc"]) if "prefsrc" in route else "") +
" uid " + str(route["uid"])
)
return True
# Addr Module
@help_msg("do_help_addr")
def do_addr(argv, af, json_print, pretty_json):
if not argv:
argv.append("show")
if any_startswith(["show", "lst", "list"], argv[0]):
argv.pop(0)
return do_addr_show(argv, af, json_print, pretty_json)
elif "add".startswith(argv[0]) and len(argv) >= 3:
argv.pop(0)
return do_addr_add(argv, af)
elif "delete".startswith(argv[0]) and len(argv) >= 3:
argv.pop(0)
return do_addr_del(argv, af)
else:
return False
return True
def do_addr_show(argv, af, json_print, pretty_json):
return link_addr_show(argv, af, json_print, pretty_json, True)
def do_addr_add(argv, af):
if len(argv) < 2:
return False
dst = ""
if argv[1] == "peer":
argv.pop(1)
dst = argv.pop(1)
if argv[1] == "dev":
argv.pop(1)
else:
return False
try:
addr = argv[0]
dev = argv[1]
except IndexError:
perror("dev not found")
exit(1)
inet = ""
if ":" in addr or af == 6:
af = 6
inet = " inet6"
return execute_cmd(
SUDO + " " + IFCONFIG + " " + dev + inet + " add " + addr + " " + dst
)
def do_addr_del(argv, af):
if len(argv) < 2:
return False
if argv[1] == "dev":
argv.pop(1)
try:
addr = argv[0]
dev = argv[1]
except IndexError:
perror("dev not found")
exit(1)
inet = "inet"
if ":" in addr or af == 6:
af = 6
inet = "inet6"
return execute_cmd(
SUDO + " " + IFCONFIG + " " + dev + " " + inet + " " + addr + " remove"
)
# Link module
@help_msg("do_help_link")
def do_link(argv, af, json_print, pretty_json):
if not argv:
argv.append("show")
if any_startswith(["show", "lst", "list"], argv[0]):
argv.pop(0)
return do_link_show(argv, af, json_print, pretty_json)
elif "set".startswith(argv[0]):
argv.pop(0)
return do_link_set(argv, af)
else:
return False
return True
def do_link_show(argv, af, json_print, pretty_json):
return link_addr_show(argv, af, json_print, pretty_json, False)
def do_link_set(argv, af):
if not argv:
return False
elif argv[0] == "dev":
argv.pop(0)
if len(argv) < 2:
return False
dev = argv[0]
IFCONFIG_DEV_CMD = SUDO + " " + IFCONFIG + " " + dev
try:
args = iter(argv)
for arg in args:
if arg == "up":
if not execute_cmd(IFCONFIG_DEV_CMD + " up"):
return False
elif arg == "down":
if not execute_cmd(IFCONFIG_DEV_CMD + " down"):
return False
elif arg in ["address", "addr", "lladdr"]:
addr = next(args)
if addr in ["random", "rand"]:
addr = randomMAC()
elif addr == "factory":
(status, res) = subprocess.getstatusoutput(
NETWORKSETUP + " -listallhardwareports"
)
if status != 0:
return False
details = re.findall(
r"^(?:Device|Ethernet Address): (.+)$",
res,
re.MULTILINE,
)
addr = details[details.index(dev) + 1]
if not execute_cmd(IFCONFIG_DEV_CMD + " lladdr " + addr):
return False
elif arg == "mtu":
mtu = int(next(args))
if not execute_cmd(IFCONFIG_DEV_CMD + " mtu " + str(mtu)):
return False
except Exception:
return False
return True
# Neigh module
@help_msg("do_help_neigh")
def do_neigh(argv, af, json_print, pretty_json):
if not argv:
argv.append("show")
if any_startswith(["show", "list", "lst"], argv[0]) and len(argv) <= 5:
argv.pop(0)
return do_neigh_show(argv, af, json_print, pretty_json)
elif "flush".startswith(argv[0]):
argv.pop(0)
return do_neigh_flush(argv, af)
else:
return False
def do_neigh_show(argv, af, json_print, pretty_json):
prefix = None
dev = None
try:
while argv:
arg = argv.pop(0)
if arg == "to":
prefix = argv.pop(0)
elif arg == "dev":
dev = argv.pop(0)
elif prefix is None:
prefix = arg
else:
return False
if prefix:
prefix = ipaddress.ip_network(prefix, strict=False)
except Exception:
return False
nd_ll_states = {
"R": "REACHABLE",
"S": "STALE",
"D": "DELAY",
"P": "PROBE",
"I": "INCOMPLETE",
"N": "INCOMPLETE",
"W": "INCOMPLETE",
}
neighs = []
if af != 4:
res = subprocess.run(
[NDP, "-an"], capture_output=True, text=True, check=True
)
for row in res.stdout.splitlines()[1:]:
cols = row.split()
entry = {"dst": re.sub(r"%.+$", "", cols[0])}
if cols[1] != "(incomplete)":
entry["lladdr"] = cols[1]
entry["dev"] = cols[2]
if dev and entry["dev"] != dev:
continue
if prefix and ipaddress.ip_address(entry["dst"]) not in prefix:
continue
if cols[1] == "(incomplete)" and cols[4] != "R":
entry["status"] = ["INCOMPLETE"]
else:
entry["status"] = [nd_ll_states[cols[4]]]
entry["router"] = len(cols) >= 6 and cols[5] == "R"
neighs.append(entry)
if af != 6:
args = [ARP, "-anl"]
if dev:
args += ["-i", dev]
res = subprocess.run(args, capture_output=True, text=True, check=True)
for row in res.stdout.splitlines()[1:]:
cols = row.split()
entry = {"dst": cols[0]}
if cols[1] != "(incomplete)":
entry["lladdr"] = cols[1]
entry["dev"] = cols[4]
if dev and entry["dev"] != dev:
continue
if prefix and ipaddress.ip_address(entry["dst"]) not in prefix:
continue
if cols[1] == "(incomplete)":
entry["status"] = ["INCOMPLETE"]
else:
entry["status"] = ["REACHABLE"]
entry["router"] = False
neighs.append(entry)
if json_print:
return json_dump(neighs, pretty_json)
for nb in neighs:
print(
nb["dst"] +
("", " dev " + nb["dev"], "")[dev == None] +
("", " router")[nb["router"]] +
" %s" % (nb["status"][0])
)
return True
def do_neigh_flush(argv, af):
if len(argv) != 2:
perror("Flush requires arguments.")
exit(1)
if argv[0] != "dev":
return False
dev = argv[1]
if af != 4:
print(
"iproute2mac: NDP doesn't support filtering by interface,"
"flushing all IPv6 entries."
)
execute_cmd(SUDO + " " + NDP + " -cn")
if af != 6:
execute_cmd(SUDO + " " + ARP + " -a -d -i " + dev)
return True
# Match iproute2 commands
# https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/ip.c#n86
cmds = [
("address", do_addr),
("route", do_route),
("neighbor", do_neigh),
("neighbour", do_neigh),
("link", do_link),
("help", do_help),
]
@help_msg("do_help")
def main(argv):
af = -1 # default / both
json_print = False
pretty_json = False
while argv and argv[0].startswith("-"):
# Turn --opt into -opt
argv[0] = argv[0][1:] if argv[0][1] == "-" else argv[0]
# Process options
if argv[0] == "-6":
af = 6
argv.pop(0)
elif argv[0] == "-4":
af = 4
argv.pop(0)
elif argv[0].startswith("-color"):
perror("iproute2mac: Color option is not implemented")
argv.pop(0)
elif "-json".startswith(argv[0]):
json_print = True
argv.pop(0)
elif "-pretty".startswith(argv[0]):
pretty_json = True
argv.pop(0)
elif "-Version".startswith(argv[0]):
print("iproute2mac, v" + VERSION)
exit(0)
elif "-help".startswith(argv[0]):
return False
else:
perror('Option "{}" is unknown, try "ip help".'.format(argv[0]))
exit(255)
if not argv:
return False
for cmd, cmd_func in cmds:
if cmd.startswith(argv[0]):
argv.pop(0)
# Functions return true or terminate with exit(255)
# See help_msg and do_help*
return cmd_func(argv, af, json_print, pretty_json)
perror('Object "{}" is unknown, try "ip help".'.format(argv[0]))
exit(1)
if __name__ == "__main__":
main(sys.argv[1:])

View File

@ -3,4 +3,4 @@
FOLDER=/Applications/OGAgent.app FOLDER=/Applications/OGAgent.app
cd $FOLDER cd $FOLDER
/usr/local/bin/python3 -m opengnsys.linux.OGAgentService $@ python -m opengnsys.linux.OGAgentService $@

View File

@ -1,11 +1,17 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# Directories
SRCDIR=$(dirname "$0") SRCDIR=$(dirname "$0")
BINDIR=/usr/local/bin BINDIR=/usr/bin
LAUNCH_AGENTS_DIR=/Library/LaunchAgents INITDIR=/Library/LaunchDaemons
LAUNCH_DAEMONS_DIR=/Library/LaunchDaemons
# Check if it needs to install Python dependencies:
if ! which pip &>/dev/null; then
easy_install pip
pip install netifaces requests six
fi
# Copying files.
cp $SRCDIR/ogagent $BINDIR cp $SRCDIR/ogagent $BINDIR
cp $SRCDIR/ip.py $BINDIR/ip ## override 'ip' from iproute2mac-1.4.2 with a more recent one cp $SRCDIR/es.opengnsys.ogagent.plist $INITDIR
cp $SRCDIR/es.opengnsys.agent.system.plist $LAUNCH_DAEMONS_DIR
cp $SRCDIR/es.opengnsys.agent.user.plist $LAUNCH_AGENTS_DIR

View File

@ -1,238 +0,0 @@
from flask import Flask, request, jsonify, render_template_string, abort
import os
import logging
import json
import subprocess
import base64
## FLASK_APP=/path/to/ogcore-mock.py FLASK_ENV=development FLASK_RUN_CERT=adhoc sudo --preserve-env flask run --host 192.168.1.249 --port 443
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
## agente lin/win/mac
@app.route('/opengnsys/rest/ogagent/<cucu>', methods=['POST'])
def og_agent(cucu):
logging.info(f'{request.get_json()}')
return jsonify({})
## agente oglive: modulo ogAdmClient
@app.route('/opengnsys/rest/ogAdmClient/InclusionCliente', methods=['POST'])
def inclusion_cliente():
logging.info(f'{request.get_json()}')
#procesoInclusionCliente() or { return (jsonify { 'res': 0 }) }
j = request.get_json(force=True)
iph = j['iph'] ## Toma ip
cfg = j['cfg'] ## Toma configuracion
logging.info(f'iph ({iph}) cfg ({cfg})')
# dbi->query (sprintf "SELECT ordenadores.*,aulas.idaula,centros.idcentro FROM ordenadores INNER JOIN aulas ON aulas.idaula=ordenadores.idaula INNER JOIN centros ON centros.idcentro=aulas.idcentro WHERE ordenadores.ip = '%s'", iph);
# if (!dbi_result_next_row(result)) { log_error ('client does not exist in database') }
# log_debug (sprintf 'Client %s requesting inclusion', iph);
idordenador = 42 #dbi_result_get_uint(result, "idordenador")
nombreordenador = 'hal9000' #dbi_result_get_string(result, "nombreordenador");
cache = 42 #dbi_result_get_uint(result, "cache");
idproautoexec = 42 #dbi_result_get_uint(result, "idproautoexec");
idaula = 42 #dbi_result_get_uint(result, "idaula");
idcentro = 42 #dbi_result_get_uint(result, "idcentro");
# resul = actualizaConfiguracion(dbi, cfg, idordenador); ## Actualiza la configuración del ordenador
# if (!resul) { log_error ('Cannot add client to database') }
# if (!registraCliente(iph)) { log_error ('client table is full') } ## Incluyendo al cliente en la tabla de sokets
return jsonify({
'res': 1, ## int, Confirmación proceso correcto
'ido': idordenador, ## int
'npc': nombreordenador, ## string
'che': cache, ## int
'exe': idproautoexec, ## int
'ida': idaula, ## int
'idc': idcentro ## int
})
def _recorreProcedimientos(parametros, fileexe, idp):
#char idprocedimiento[LONPRM];
#int procedimientoid, lsize;
#const char *msglog, *param;
#dbi_result result;
#dbi->query (sprintf 'SELECT procedimientoid,parametros FROM procedimientos_acciones WHERE idprocedimiento=%s ORDER BY orden', $idp);
#if (!result) { log_error ('failed to query database'); return 0; }
if 1: #while (dbi_result_next_row(result)) {
procedimientoid = 0 ## dbi_result_get_uint(result, "procedimientoid");
if (procedimientoid > 0): ## Procedimiento recursivo
if (not _recorreProcedimientos (parametros, fileexe, procedimientoid)):
return 0
else:
#param = '@'.join (["nfn=EjecutarScript\rscp=uptime", "nfn=EjecutarScript\rscp=cat /proc/uptime"]) ## dbi_result_get_string(result, "parametros");
param = '@'.join (["nfn=popup\rtitle=my title\rmessage=my message"])
parametros = '{}@'.format (param)
fileexe.write (parametros)
#}
return 1
@app.route('/opengnsys/rest/ogAdmClient/AutoexecCliente', methods=['POST'])
def autoexec_client():
logging.info(f'{request.get_json()}')
j = request.get_json(force=True)
iph = j['iph'] ## Toma dirección IP del cliente
exe = j['exe'] ## Toma identificador del procedimiento inicial
logging.info(f'iph ({iph}) exe ({exe})')
fileautoexec = '/tmp/Sautoexec-{}'.format(iph)
logging.info ('fileautoexec ({})'.format (fileautoexec));
try:
fileexe = open (fileautoexec, 'w')
except Exception as e:
logging.error ('cannot create temporary file: {}'.format (e))
return jsonify({})
if (_recorreProcedimientos ('', fileexe, exe)):
res = jsonify ({ 'res': 1, 'nfl': fileautoexec })
else:
res = jsonify ({ 'res': 0 })
fileexe.close()
return res
@app.route('/opengnsys/rest/ogAdmClient/enviaArchivo', methods=['POST'])
def envia_archivo():
logging.info(f'{request.get_json()}')
j = request.get_json(force=True)
nfl = j['nfl'] ## Toma nombre completo del archivo
logging.info(f'nfl ({nfl})')
contents = subprocess.run (['cat', nfl], capture_output=True).stdout
b64 = base64.b64encode (contents).decode ('utf-8')
return jsonify({'contents': b64})
def clienteExistente(iph):
## esto queda totalmente del lado del servidor, no lo implemento en python
return 42
def buscaComandos(ido):
#dbi->query (sprintf "SELECT sesion, parametros FROM acciones WHERE idordenador=%s AND estado='%d' ORDER BY idaccion", ido, ACCION_INICIADA);
#dbi_result_next_row(result) ## cogemos solo una fila
#if not row { return; }
return
#ids = 42 #dbi_result_get_uint(result, "sesion");
#param = "nfn=popup\rtitle=my title\rmessage=my message" #dbi_result_get_string(result, "parametros");
## convertirlo a json, aqui lo pongo a capon
#return jsonify ({ 'nfn': 'popup', 'title': 'my title', 'message': 'my message', 'ids': ids })
@app.route('/opengnsys/rest/ogAdmClient/ComandosPendientes', methods=['POST'])
def comandos_pendientes():
logging.info(f'{request.get_json()}')
j = request.get_json(force=True)
iph = j['iph'] ## Toma dirección IP
ido = j['ido'] ## Toma identificador del ordenador
logging.info(f'iph ({iph}) ido ({ido})')
idx = clienteExistente(iph) ## Busca índice del cliente
if not idx:
## que devuelvo?? pongamos un 404...
abort(404, 'Client does not exist')
param = buscaComandos(ido) ## Existen comandos pendientes, buscamos solo uno
if param is None:
return jsonify({'nfn': 'NoComandosPtes'})
#strcpy(tbsockets[idx].estado, CLIENTE_OCUPADO); ## esto queda totalmente del lado del servidor, no lo implemento en python
return jsonify(param)
@app.route('/opengnsys/rest/ogAdmClient/DisponibilidadComandos', methods=['POST'])
def disponibilidad_comandos():
logging.info(f'{request.get_json()}')
j = request.get_json(force=True)
iph = j['iph']
tpc = j['tpc']
logging.info(f'iph ({iph}) tpc ({tpc})')
idx = clienteExistente(iph) ## Busca índice del cliente
if not idx:
## que devuelvo?? pongamos un 404...
abort(404, 'Client does not exist')
#strcpy(tbsockets[idx].estado, tpc); ## esto queda totalmente del lado del servidor, no lo implemento en python
return jsonify({})
@app.route('/opengnsys/rest/ogAdmClient/recibeArchivo', methods=['POST'])
def oac_recibe_archivo():
logging.info(f'{request.get_json()}')
j = request.get_json(force=True)
nfl = j['nfl']
contents = j['contents']
logging.info(f'nfl ({nfl}) contents ({contents})')
dec = base64.b64decode (contents).decode ('utf-8')
logging.info(f'dec ({dec})')
return jsonify({'anything':'anything'}) ## if we return {}, then we trigger "if not {}" which happens to be true
@app.route('/opengnsys/rest/clients/status/webhook', methods=['POST'])
def oac_callback():
logging.info(f'{request.get_json()}')
return jsonify({'anything':'anything'})
@app.route('/opengnsys/rest/ogAdmClient/<cucu>', methods=['GET', 'POST'])
def oac_cucu(cucu):
#j = request.get_json(force=True)
#logging.info(f'{request.get_json()} {j}')
#if 'cucu' not in j:
# abort(400, 'missing parameter 'cucu'')
#return jsonify({'cucu': j['cucu']})
abort (404)
## agente oglive: modulo CloningEngine
@app.route('/opengnsys/rest/CloningEngine/recibeArchivo', methods=['POST'])
def ce_recibe_archivo():
logging.info(f'{request.get_json()}')
j = request.get_json(force=True)
nfl = j['nfl']
contents = j['contents']
logging.info(f'nfl ({nfl}) contents ({contents})')
dec = base64.b64decode (contents).decode ('utf-8')
logging.info(f'dec ({dec})')
return jsonify({'anything':'anything'}) ## if we return {}, then we trigger "if not {}" which happens to be true
@app.route('/opengnsys/rest/CloningEngine/callback', methods=['POST'])
def ce_callback():
logging.info(f'{request.get_json()}')
return jsonify({'anything':'anything'})
@app.route('/opengnsys/rest/CloningEngine/<cucu>', methods=['GET', 'POST'])
def ce_cucu(cucu):
abort (404)
@app.errorhandler(404)
def _page_not_found(e):
if type(e.description) is dict:
return jsonify (e.description), e.code
else:
return render_template_string('''<!DOCTYPE html><html>not found</html>'''), e.code
@app.errorhandler(500)
def _internal_server_error(e):
return render_template_string('''<!DOCTYPE html><html>err</html>'''), e.code
@app.errorhandler(Exception)
def _exception(e):
print(e)
return render_template_string('''<!DOCTYPE html><html>exception</html>'''), e.code
if __name__ == '__main__':
app.run(host = '192.168.1.249', port = 443, debug=True)

42
oglive/Makefile 100644
View File

@ -0,0 +1,42 @@
#!/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
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 $(LIBDIR)/img
# Cleans up .pyc and cache folders
rm -f $(PYC) $(CACHES)
cp -r $(SOURCEDIR)/opengnsys $(LIBDIR)/opengnsys
cp -r $(SOURCEDIR)/cfg $(LIBDIR)/cfg
ln -fs $(LIBDIR)/cfg/ogagent.cfg $(CFGDIR)
ln -fs $(LIBDIR)/cfg/ogclient.cfg $(CFGDIR)
cp scripts/ogagent $(BINDIR)
chmod 755 $(BINDIR)/ogagent
uninstall:
rm -rf $(LIBDIR)
rm -f $(BINDIR)/ogagent
rm -rf $(CFGDIR)

View File

@ -0,0 +1,7 @@
#!/bin/bash
cd $(dirname "$0")
# Build package
dpkg-buildpackage -b -d

View File

@ -0,0 +1,6 @@
ogagent-oglive (1.1.1) unstable; urgency=medium
* Initial release.
-- Ramón M. Gómez <ramongomez@us.es> Mon, 18 Jun 2018 13:00:00 +0200

View File

@ -0,0 +1 @@
9

View File

@ -0,0 +1,15 @@
Source: ogagent-oglive
Section: admin
Priority: optional
Maintainer: Ramón M. Gómez <ramongomez@us.es>
Build-Depends: debhelper (>= 7), po-debconf
Standards-Version: 3.9.2
Homepage: https://opengnsys.es
Package: ogagent-oglive
Section: admin
Priority: optional
Architecture: all
Depends: python-requests (>=0.8.2), python-six(>=1.1), python-prctl(>=1.1.1), python (>=2.7), libxss1, ${misc:Depends}
Description: OpenGnsys Agent for ogLive client
This package provides the required components to allow this machine to work on an environment managed by OpenGnsys.

View File

@ -0,0 +1,26 @@
Format-Specification: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=135
Name: ogagent
Maintainer: Ramón M. Gómez
Source: https://opengnsys.es
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'.

View File

@ -0,0 +1 @@
readme.txt

View File

@ -0,0 +1,2 @@
/usr/share/OGAgent/cfg/ogagent.cfg /etc/ogagent/ogagent.cfg
/usr/share/OGAgent/cfg/ogclient.cfg /etc/ogagent/ogclient.cfg

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
misc:Depends=
misc:Pre-Depends=

View File

@ -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

View File

@ -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-oglive 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

View File

@ -0,0 +1 @@
3.0 (native)

View File

@ -0,0 +1,3 @@
OGAgent is the agent intended for OpengGnsys interaction.
Please, visit https://opengnsys.es for more information

View File

@ -0,0 +1,6 @@
#!/bin/sh
FOLDER=/usr/share/OGAgent
cd $FOLDER
python -m opengnsys.linux.OGAgentService $@

View File

@ -1,297 +0,0 @@
openapi: 3.0.3
info:
title: OgAgent API
description: OgAgent API
version: 0.0.1
paths:
/opengnsys/status:
post:
summary: Get status of the agent
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/StatusReq'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/StatusRes'
/opengnsys/poweroff:
post:
summary: Power agent off
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/EmptyObj'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/LaunchedRes'
/opengnsys/reboot:
post:
summary: Reboot agent
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/EmptyObj'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/LaunchedRes'
/opengnsys/script:
post:
summary: Run script on agent
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ScriptReq'
required: true
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/ScriptRes'
/opengnsys/terminatescript:
post:
summary: Terminate running script on agent
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TerminateScriptReq'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/EmptyObj'
/opengnsys/preparescripts:
post:
summary: Prepare list of scripts running on agent
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/EmptyObj'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/EmptyObj'
/opengnsys/getscripts:
post:
summary: Get the list of scripts running on agent
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/EmptyObj'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/GetScriptsRes'
/opengnsys/logoff:
post:
summary: Log remote user off
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/EmptyObj'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/LogoffRes'
/opengnsys/popup:
post:
summary: Show message on agent
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Popup'
required: true
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/LaunchedRes'
components:
schemas:
EmptyObj:
type: object
additionalProperties: false
Jobid:
type: string
example:
- "deadbeef"
StatusReq:
type: object
properties:
detail:
type: boolean
StatusRes:
type: object
required:
- status
properties:
status:
type: string
enum:
- "LNX"
- "OSX"
- "WIN"
loggedin:
type: boolean
session:
type: string
example:
- "x11"
agent_version:
type: string
os_version:
type: string
sys_load:
type: number
LaunchedRes:
type: object
required:
- op
properties:
op:
type: string
enum:
- "launched"
ScriptReq:
type: object
required:
- script
properties:
script:
type: string
example:
- "uptime\nwho"
- "Start-Process notepad.exe"
client:
type: boolean
default: false
ScriptRes:
type: object
required:
- op
properties:
op:
type: string
enum:
- "launched"
jobid:
$ref: '#/components/schemas/Jobid'
TerminateScriptReq:
type: object
required:
- jobid
properties:
jobid:
$ref: '#/components/schemas/Jobid'
RunningScript:
type: object
required:
- jobid
- pid
- starttime
- script
- client
- status
- stdout
- stderr
properties:
jobid:
$ref: '#/components/schemas/Jobid'
pid:
type: integer
starttime:
type: string
example: "2024-12-31 23:59:59.123456+0000"
script:
type: string
client:
type: boolean
status:
type: string
enum:
- "running"
- "finished"
stdout:
type: string
stderr:
type: string
rc:
type: integer
GetScriptsRes:
type: array
items:
$ref: '#/components/schemas/RunningScript'
LogoffRes:
type: object
required:
- op
properties:
op:
type: string
enum:
- "sent to client"
Popup:
type: object
properties:
title:
type: string
message:
type: string

3
requires.txt 100644
View File

@ -0,0 +1,3 @@
six
requests

View File

@ -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)

View File

@ -1,9 +1,10 @@
<<<<<<< HEAD
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity <assemblyIdentity
type="win32" type="win32"
name="UDSActorService" name="OGAgentService"
version="1.6.0.0" version="1.1.2.0"
processorArchitecture="x86" processorArchitecture="x86"
/> />
<description>Description</description> <description>Description</description>

5
src/OGAgent.qrc 100644
View File

@ -0,0 +1,5 @@
<RCC>
<qresource prefix="images">
<file>img/oga.png</file>
</qresource>
</RCC>

View File

@ -1,102 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
## generated on windows using:
## pyi-makespec.exe --windowed --icon img\oga.ico --manifest OGAgent.manifest OGAgentUser.py opengnsys\windows\OGAgentService.py
## move OGAgentUser.spec OGAgent.spec
ogausr_a = Analysis(
['OGAgentUser.py'],
pathex=[],
binaries=[],
datas=[
# ('cfg', 'cfg'), ## add the entire directory
('img', 'img'), ## add the entire directory
],
hiddenimports=['win32timezone', 'socketserver', 'http.server', 'urllib', 'opengnsys.modules.client.OpenGnSys', 'opengnsys.modules.server.ogAdmClient', 'opengnsys.modules.server.OpenGnSys'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
ogasvc_a = Analysis(
['opengnsys\\windows\\OGAgentService.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=['win32timezone', 'socketserver', 'http.server', 'urllib', 'opengnsys.modules.client.OpenGnSys', 'opengnsys.modules.server.ogAdmClient', 'opengnsys.modules.server.OpenGnSys'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
MERGE(
(ogausr_a, 'OGAgentUser', 'OGAgentUser'), ## class, py name, exe name
(ogasvc_a, 'OGAgentService', 'OGAgentService')
)
ogausr_pyz = PYZ(ogausr_a.pure)
ogasvc_pyz = PYZ(ogasvc_a.pure)
ogausr_exe = EXE(
ogausr_pyz,
ogausr_a.scripts,
[],
exclude_binaries=True,
name='OGAgentUser',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=['img\\oga.ico'],
)
ogasvc_exe = EXE(
ogasvc_pyz,
ogasvc_a.scripts,
[],
exclude_binaries=True,
name='OGAgentService',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=['img\\oga.ico'],
manifest='OGAgent.manifest',
)
dist_name = 'OGAgent'
coll = COLLECT(
ogausr_exe,
ogausr_a.binaries,
ogausr_a.datas,
ogasvc_exe,
ogasvc_a.binaries,
ogasvc_a.datas,
strip=False,
upx=True,
upx_exclude=[],
name=dist_name,
)
import shutil
shutil.copytree ('cfg', '{}/{}/cfg'. format(DISTPATH, dist_name))
shutil.copy ('stop-agent.ps1', '{}/{}/stop-agent.ps1'.format(DISTPATH, dist_name))

133
src/OGAgentUser.py 100755 → 100644
View File

@ -1,4 +1,4 @@
#!/usr/bin/env python3 #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2014 Virtual Cable S.L. # Copyright (c) 2014 Virtual Cable S.L.
@ -29,22 +29,28 @@
""" """
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
""" """
import atexit from __future__ import unicode_literals
import base64
import json
import sys import sys
import time import time
import os import json
from PyQt6 import QtCore, QtGui, QtWidgets import six
import atexit
from PyQt4 import QtCore, QtGui # @UnresolvedImport
from opengnsys import VERSION, ipc, operations, utils
from opengnsys.log import logger
from opengnsys.service import IPC_PORT
from about_dialog_ui import Ui_OGAAboutDialog from about_dialog_ui import Ui_OGAAboutDialog
from message_dialog_ui import Ui_OGAMessageDialog from message_dialog_ui import Ui_OGAMessageDialog
from opengnsys import VERSION, ipc, operations, utils from opengnsys.scriptThread import ScriptExecutorThread
from opengnsys.config import readConfig from opengnsys.config import readConfig
from opengnsys.loader import loadModules from opengnsys.loader import loadModules
from opengnsys.log import logger
from opengnsys.jobmgr import JobMgr # Set default characters encoding to UTF-8
from opengnsys.service import IPC_PORT reload(sys)
if hasattr(sys, 'setdefaultencoding'):
sys.setdefaultencoding('utf-8')
trayIcon = None trayIcon = None
@ -55,9 +61,9 @@ def sigAtExit():
# About dialog # About dialog
class OGAAboutDialog(QtWidgets.QDialog): class OGAAboutDialog(QtGui.QDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
QtWidgets.QDialog.__init__(self, parent) QtGui.QDialog.__init__(self, parent)
self.ui = Ui_OGAAboutDialog() self.ui = Ui_OGAAboutDialog()
self.ui.setupUi(self) self.ui.setupUi(self)
self.ui.VersionLabel.setText("Version " + VERSION) self.ui.VersionLabel.setText("Version " + VERSION)
@ -66,9 +72,9 @@ class OGAAboutDialog(QtWidgets.QDialog):
self.hide() self.hide()
class OGAMessageDialog(QtWidgets.QDialog): class OGAMessageDialog(QtGui.QDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
QtWidgets.QDialog.__init__(self, parent) QtGui.QDialog.__init__(self, parent)
self.ui = Ui_OGAMessageDialog() self.ui = Ui_OGAMessageDialog()
self.ui.setupUi(self) self.ui.setupUi(self)
@ -83,7 +89,7 @@ class OGAMessageDialog(QtWidgets.QDialog):
class MessagesProcessor(QtCore.QThread): class MessagesProcessor(QtCore.QThread):
logoff = QtCore.pyqtSignal(name='logoff') logoff = QtCore.pyqtSignal(name='logoff')
message = QtCore.pyqtSignal(tuple, name='message') message = QtCore.pyqtSignal(tuple, name='message')
script = QtCore.pyqtSignal(str, name='script') script = QtCore.pyqtSignal(QtCore.QString, name='script')
exit = QtCore.pyqtSignal(name='exit') exit = QtCore.pyqtSignal(name='exit')
def __init__(self, port): def __init__(self, port):
@ -109,17 +115,13 @@ class MessagesProcessor(QtCore.QThread):
def isAlive(self): def isAlive(self):
return self.ipc is not None return self.ipc is not None
def sendLogin(self, user_data): def sendLogin(self, userName, language):
if self.ipc: if self.ipc:
self.ipc.sendLogin(user_data) self.ipc.sendLogin(userName, language)
def sendLogout(self, username): def sendLogout(self, userName):
if self.ipc: if self.ipc:
self.ipc.sendLogout(username) self.ipc.sendLogout(userName)
def sendMessage(self, module, message, data):
if self.ipc:
self.ipc.sendMessage(module, message, data)
def run(self): def run(self):
if self.ipc is None: if self.ipc is None:
@ -134,17 +136,20 @@ class MessagesProcessor(QtCore.QThread):
msg = self.ipc.getMessage() msg = self.ipc.getMessage()
if msg is None: if msg is None:
break break
msg_id, data = msg msgId, data = msg
logger.debug('Got Message on User Space: {}:{}'.format(msg_id, data)) logger.debug('Got Message on User Space: {}:{}'.format(msgId, data))
if msg_id == ipc.MSG_MESSAGE: if msgId == ipc.MSG_MESSAGE:
module, message, data = data.decode('utf-8').split('\0') module, message, data = data.split('\0')
self.message.emit((module, message, data)) self.message.emit((module, message, data))
elif msg_id == ipc.MSG_LOGOFF: elif msgId == ipc.MSG_LOGOFF:
self.logoff.emit() self.logoff.emit()
elif msg_id == ipc.MSG_SCRIPT: elif msgId == ipc.MSG_SCRIPT:
self.script.emit(data.decode('utf-8')) self.script.emit(QtCore.QString.fromUtf8(data))
except Exception as e: except Exception as e:
logger.error('Got error on IPC thread {}'.format(utils.exceptionToMessage(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: if self.ipc.running is False and self.running is True:
logger.warn('Lost connection with Service, closing program') logger.warn('Lost connection with Service, closing program')
@ -152,26 +157,27 @@ class MessagesProcessor(QtCore.QThread):
self.exit.emit() self.exit.emit()
class OGASystemTray(QtWidgets.QSystemTrayIcon): class OGASystemTray(QtGui.QSystemTrayIcon):
jobmgr = JobMgr()
def __init__(self, app_, parent=None): def __init__(self, app_, parent=None):
self.app = app_ self.app = app_
self.config = readConfig(client=True) self.config = readConfig(client=True)
self.modules = None
# Get opengnsys section as dict # Get opengnsys section as dict
cfg = dict(self.config.items('opengnsys')) cfg = dict(self.config.items('opengnsys'))
# Set up log level # Set up log level
logger.setLevel(cfg.get('log', 'INFO')) logger.setLevel(cfg.get('log', 'INFO'))
self.ipcport = int(cfg.get('ipc_port', IPC_PORT)) self.ipcport = int(cfg.get('ipc_port', IPC_PORT))
QtCore.QDir.addSearchPath('images', os.path.join(os.path.dirname(__file__), 'img')) # style = app.style()
icon = QtGui.QIcon('images:oga.png') # icon = QtGui.QIcon(style.standardPixmap(QtGui.QStyle.SP_ComputerIcon))
icon = QtGui.QIcon(':/images/img/oga.png')
QtWidgets.QSystemTrayIcon.__init__(self, icon, parent) QtGui.QSystemTrayIcon.__init__(self, icon, parent)
self.menu = QtWidgets.QMenu(parent) self.menu = QtGui.QMenu(parent)
exit_action = self.menu.addAction("About") exitAction = self.menu.addAction("About")
exit_action.triggered.connect(self.about) exitAction.triggered.connect(self.about)
self.setContextMenu(self.menu) self.setContextMenu(self.menu)
self.ipc = MessagesProcessor(self.ipcport) self.ipc = MessagesProcessor(self.ipcport)
@ -202,19 +208,20 @@ class OGASystemTray(QtWidgets.QSystemTrayIcon):
logger.debug('Modules: {}'.format(list(v.name for v in self.modules))) logger.debug('Modules: {}'.format(list(v.name for v in self.modules)))
# Send init to all modules # Send init to all modules
valid_mods = [] validMods = []
for mod in self.modules: for mod in self.modules:
try: try:
logger.debug('Activating module {}'.format(mod.name)) logger.debug('Activating module {}'.format(mod.name))
mod.activate() mod.activate()
valid_mods.append(mod) validMods.append(mod)
except Exception as e: except Exception as e:
logger.exception() logger.exception()
logger.debug ("Activation of {} failed: {}".format(mod.name, utils.exceptionToMessage(e))) logger.error("Activation of {} failed: {}".format(mod.name, utils.exceptionToMessage(e)))
self.modules[:] = valid_mods # copy instead of assignment
self.modules[:] = validMods # copy instead of assignment
# If this is running, it's because he have logged in, inform service of this fact # If this is running, it's because he have logged in, inform service of this fact
self.ipc.sendLogin((operations.getCurrentUser(), operations.getSessionLanguage(), self.ipc.sendLogin(operations.getCurrentUser(), operations.getSessionLanguage())
operations.get_session_type()))
def deinitialize(self): def deinitialize(self):
for mod in reversed(self.modules): # Deinitialize reversed of initialization for mod in reversed(self.modules): # Deinitialize reversed of initialization
@ -223,7 +230,7 @@ class OGASystemTray(QtWidgets.QSystemTrayIcon):
mod.deactivate() mod.deactivate()
except Exception as e: except Exception as e:
logger.exception() logger.exception()
logger.debug ("Deactivation of {} failed: {}".format(mod.name, utils.exceptionToMessage(e))) logger.error("Deactivation of {} failed: {}".format(mod.name, utils.exceptionToMessage(e)))
def timerFnc(self): def timerFnc(self):
pass pass
@ -236,7 +243,7 @@ class OGASystemTray(QtWidgets.QSystemTrayIcon):
logger.debug('msg: {}, {}'.format(type(msg), msg)) logger.debug('msg: {}, {}'.format(type(msg), msg))
module, message, data = msg module, message, data = msg
except Exception as e: except Exception as e:
logger.debug ('Got exception {} processing message {}'.format(e, msg)) logger.error('Got exception {} processing message {}'.format(e, msg))
return return
for v in self.modules: for v in self.modules:
@ -246,22 +253,22 @@ class OGASystemTray(QtWidgets.QSystemTrayIcon):
v.processMessage(message, json.loads(data)) v.processMessage(message, json.loads(data))
return return
except Exception as e: except Exception as e:
logger.debug ('Got exception {} processing generic message on {}'.format(e, v.name)) logger.error('Got exception {} processing generic message on {}'.format(e, v.name))
logger.debug ('Module {} not found, messsage {} not sent'.format(module, message)) logger.error('Module {} not found, messsage {} not sent'.format(module, message))
## when is this run??
def executeScript(self, script): def executeScript(self, script):
script = base64.b64decode(script.encode('ascii')) logger.debug('Executing script')
logger.debug('Executing received script "{}"'.format(script)) script = six.text_type(script.toUtf8()).decode('base64')
self.jobmgr.launch_job (script, True) th = ScriptExecutorThread(script)
th.start()
def logoff(self): def logoff(self):
logger.debug('Logoff invoked') logger.debug('Logoff invoked')
operations.logoff() # Invoke log off operations.logoff() # Invoke log off
def about(self): def about(self):
self.aboutDlg.exec() self.aboutDlg.exec_()
def cleanup(self): def cleanup(self):
logger.debug('Quit invoked') logger.debug('Quit invoked')
@ -271,7 +278,7 @@ class OGASystemTray(QtWidgets.QSystemTrayIcon):
self.deinitialize() self.deinitialize()
except Exception: except Exception:
logger.exception() logger.exception()
logger.debug ('Got exception deinitializing modules') logger.error('Got exception deinitializing modules')
try: try:
# If we close Client, send Logoff to Broker # If we close Client, send Logoff to Broker
@ -282,7 +289,7 @@ class OGASystemTray(QtWidgets.QSystemTrayIcon):
except Exception: except Exception:
# May we have lost connection with server, simply log and exit in that case # May we have lost connection with server, simply log and exit in that case
logger.exception() logger.exception()
logger.debug ('Got an exception, processing quit') logger.exception("Got an exception, processing quit")
try: try:
# operations.logoff() # Uncomment this after testing to logoff user # operations.logoff() # Uncomment this after testing to logoff user
@ -303,20 +310,20 @@ class OGASystemTray(QtWidgets.QSystemTrayIcon):
if __name__ == '__main__': if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv) app = QtGui.QApplication(sys.argv)
if not QtWidgets.QSystemTrayIcon.isSystemTrayAvailable(): if not QtGui.QSystemTrayIcon.isSystemTrayAvailable():
# QtGui.QMessageBox.critical(None, "Systray", "I couldn't detect any system tray on this system.") # QtGui.QMessageBox.critical(None, "Systray", "I couldn't detect any system tray on this system.")
sys.exit(1) sys.exit(1)
# This is important so our app won't close on messages windows (alerts, etc...) # This is important so our app won't close on messages windows (alerts, etc...)
QtWidgets.QApplication.setQuitOnLastWindowClosed(False) QtGui.QApplication.setQuitOnLastWindowClosed(False)
try: try:
trayIcon = OGASystemTray(app) trayIcon = OGASystemTray(app)
except Exception as e: except Exception as e:
logger.exception() logger.exception()
logger.debug ('OGA Service is not running, or it can\'t contact with OGA Server. User Tools stopped: {}'.format( logger.error('OGA Service is not running, or it can\'t contact with OGA Server. User Tools stopped: {}'.format(
utils.exceptionToMessage(e))) utils.exceptionToMessage(e)))
sys.exit(1) sys.exit(1)
@ -324,7 +331,7 @@ if __name__ == '__main__':
trayIcon.initialize() # Initialize modules, etc.. trayIcon.initialize() # Initialize modules, etc..
except Exception as e: except Exception as e:
logger.exception() logger.exception()
logger.debug ('Exception initializing OpenGnsys User Agent {}'.format(utils.exceptionToMessage(e))) logger.error('Exception initializing OpenGnsys User Agent {}'.format(utils.exceptionToMessage(e)))
trayIcon.quit() trayIcon.quit()
sys.exit(1) sys.exit(1)
@ -334,7 +341,7 @@ if __name__ == '__main__':
# Catch kill and logout user :) # Catch kill and logout user :)
atexit.register(sigAtExit) atexit.register(sigAtExit)
res = app.exec() res = app.exec_()
logger.debug('Exiting') logger.debug('Exiting')
trayIcon.quit() trayIcon.quit()

292
src/OGAgent_rc.py 100644
View File

@ -0,0 +1,292 @@
# -*- 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()

View File

@ -1 +1 @@
5.7.0 1.1.1b

View File

@ -65,7 +65,7 @@
<item> <item>
<widget class="QLabel" name="VersionLabel"> <widget class="QLabel" name="VersionLabel">
<property name="text"> <property name="text">
<string>Version 0</string> <string>Version 1.1.0</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -217,6 +217,9 @@ p, li { white-space: pre-wrap; }
</item> </item>
</layout> </layout>
</widget> </widget>
<resources>
<include location="OGAgent.qrc"/>
</resources>
<connections> <connections>
<connection> <connection>
<sender>buttonBox</sender> <sender>buttonBox</sender>

View File

@ -4,42 +4,20 @@ address=0.0.0.0
port=8000 port=8000
# This is a comma separated list of paths where to look for modules to load # This is a comma separated list of paths where to look for modules to load
#path=test_modules/server,more_modules/server path=test_modules/server
# Remote OpenGnsys Service # Remote OpenGnsys Service
remote=https://192.168.2.1/opengnsys/rest remote=https://192.168.2.10/opengnsys/rest
# Alternate OpenGnsys Service (comment out to enable this option) # Alternate OpenGnsys Service (comment out to enable this option)
#altremote=https://10.0.2.2/opengnsys/rest #altremote=https://10.0.2.2/opengnsys/rest
# Execution level (permitted operations): status, halt, full # Log Level, if ommited, will be set to INFO
level=full
# Log Level, if omitted, will be set to INFO
log=DEBUG log=DEBUG
imgname=
# TLS
# The agent will look for these files in /opt/opengnsys/etc, /usr/share/OGAgent,
# windows "Program Files (x86)" and the current working directory
ca=ca.crt
crt=ogagent.crt
key=ogagent.key
# Module specific # Module specific
# The sections must match the module name # The sections must match the module name
# This section will be passes on activation to module # This section will be passes on activation to module
[ogAdmClient] #[Sample1]
#path=test_modules/server,more_modules/server #value1=Mariete
#value2=Yo
remote={}://{}/opengnsys/rest #remote=https://172.27.0.1:9999/rest
log=DEBUG
pathinterface=/opt/opengnsys/interfaceAdm
urlMenu={}://{}/menu-browser
urlMsg=http://localhost/cgi-bin/httpd-log.sh
# TLS
ca=/opt/opengnsys/etc/ca.crt
crt=/opt/opengnsys/etc/ogagent.crt
key=/opt/opengnsys/etc/ogagent.key

View File

@ -32,9 +32,8 @@
# pylint: disable-msg=E1101,W0703 # pylint: disable-msg=E1101,W0703
from __future__ import unicode_literals
import os
import requests import requests
import logging import logging
import json import json
@ -44,8 +43,8 @@ from .log import logger
from .utils import exceptionToMessage from .utils import exceptionToMessage
VERIFY_CERT = False # Do not check server certificate
TIMEOUT = 5 # Connection timout, in seconds TIMEOUT = 5 # Connection timout, in seconds
VERIFY_TLS=True
class RESTError(Exception): class RESTError(Exception):
@ -59,12 +58,8 @@ class ConnectionError(RESTError):
# Disable warnings log messages # Disable warnings log messages
try: try:
import urllib3 # @UnusedImport import urllib3 # @UnusedImport
requests_log = logging.getLogger ('urllib3')
requests_log.setLevel (logging.INFO)
except Exception: except Exception:
from requests.packages import urllib3 # @Reimport from requests.packages import urllib3 # @Reimport
requests_log = logging.getLogger ('requests.packages.urllib3')
requests_log.setLevel (logging.INFO)
try: try:
urllib3.disable_warnings() # @UndefinedVariable urllib3.disable_warnings() # @UndefinedVariable
@ -89,14 +84,13 @@ class REST(object):
the deserialized JSON result or raises an exception in case of error the deserialized JSON result or raises an exception in case of error
""" """
def __init__(self, url, ca_file=None, crt_file=None, key_file=None): def __init__(self, url):
""" """
Initializes the REST helper Initializes the REST helper
url is the full url of the REST API Base, as for example "https://example.com/rest/v1". 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. @param url The url of the REST API Base. The trailing '/' can be included or omitted, as desired.
""" """
self.endpoint = url self.endpoint = url
global VERIFY_TLS
if self.endpoint[-1] != '/': if self.endpoint[-1] != '/':
self.endpoint += '/' self.endpoint += '/'
@ -107,52 +101,6 @@ class REST(object):
except Exception: except Exception:
self.newerRequestLib = False # I no version, guess this must be an old requests self.newerRequestLib = False # I no version, guess this must be an old requests
if not self.newerRequestLib:
logger.debug ('TLS not available: python requests library is old')
self.use_tls = url.startswith ('https')
if self.use_tls:
if not ca_file or not crt_file or not key_file:
raise Exception ('missing TLS parameters in REST constructor')
certs_dirs = ['/opt/opengnsys/etc', '/usr/share/OGAgent']
pf = os.environ.get ('PROGRAMFILES(X86)')
if pf: certs_dirs.append (os.path.join (pf, 'OGAgent'))
certs_dirs.append (os.getcwd())
certs_dir = None
for sp in certs_dirs:
if os.path.exists (sp):
logger.debug (f'Looking for TLS files in ({sp})')
certs_dir = sp
break
if not certs_dir:
logger.debug ("Don't know where to look for TLS files")
errs = 1
else:
errs = 0
for f in [ca_file, crt_file, key_file]:
if os.path.exists (f'{certs_dir}/{f}'):
logger.debug (f'{certs_dir}/{f}: found')
else:
logger.error (f'{f}: No such file or directory')
errs += 1
if errs:
self.verify_tls = False
logger.debug ('HTTP client: using insecure TLS to talk to ogcore due to missing files')
else:
self.ca_file = f'{certs_dir}/{ca_file}'
self.crt_file = f'{certs_dir}/{crt_file}'
self.key_file = f'{certs_dir}/{key_file}'
self.verify_tls = VERIFY_TLS
if self.verify_tls:
logger.debug ('HTTP client: using TLS to talk to ogcore')
else:
logger.debug ('HTTP client: using insecure TLS as requested to talk to ogcore')
else:
logger.debug ('HTTP client: not using TLS to talk to ogcore')
# Disable logging requests messages except for errors, ... # Disable logging requests messages except for errors, ...
logging.getLogger("requests").setLevel(logging.CRITICAL) logging.getLogger("requests").setLevel(logging.CRITICAL)
# Tries to disable all warnings # Tries to disable all warnings
@ -183,39 +131,20 @@ class REST(object):
logger.debug('Requesting using GET (no data provided) {}'.format(url)) logger.debug('Requesting using GET (no data provided) {}'.format(url))
# Old requests version does not support verify, but it do not checks ssl certificate by default # Old requests version does not support verify, but it do not checks ssl certificate by default
if self.newerRequestLib: if self.newerRequestLib:
if self.use_tls: r = requests.get(url, verify=VERIFY_CERT, timeout=TIMEOUT)
if self.verify_tls:
r = requests.get(url, cert=(self.crt_file, self.key_file), verify=self.ca_file, timeout=TIMEOUT)
else:
logger.warning ('using insecure TLS for GET')
r = requests.get(url, verify=False, timeout=TIMEOUT)
else:
r = requests.get(url, timeout=TIMEOUT)
else: else:
r = requests.get(url) r = requests.get(url)
else: # POST else: # POST
logger.debug('Requesting using POST {}, data: {}'.format(url, data)) logger.debug('Requesting using POST {}, data: {}'.format(url, data))
if self.newerRequestLib: if self.newerRequestLib:
if self.use_tls: r = requests.post(url, data=data, headers={'content-type': 'application/json'},
if self.verify_tls: verify=VERIFY_CERT, timeout=TIMEOUT)
r = requests.post(url, data=data, headers={'content-type': 'application/json'}, cert=(self.crt_file, self.key_file), verify=self.ca_file, timeout=TIMEOUT)
else:
logger.warning ('using insecure TLS for POST')
r = requests.post(url, data=data, headers={'content-type': 'application/json'}, verify=False, timeout=TIMEOUT)
else:
r = requests.post(url, data=data, headers={'content-type': 'application/json'}, timeout=TIMEOUT)
else: else:
r = requests.post(url, data=data, headers={'content-type': 'application/json'}) r = requests.post(url, data=data, headers={'content-type': 'application/json'})
r.raise_for_status()
ct = r.headers['Content-Type']
if 'application/json' != ct:
raise Exception (f'response content-type is not "application/json" but "{ct}"')
r = json.loads(r.content) # Using instead of r.json() to make compatible with old requests lib versions r = json.loads(r.content) # Using instead of r.json() to make compatible with old requests lib versions
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
code = e.response.status_code raise ConnectionError(e)
logger.warning (f'request failed, HTTP code "{code}"')
return None
except Exception as e: except Exception as e:
raise ConnectionError(exceptionToMessage(e)) raise ConnectionError(exceptionToMessage(e))
@ -227,17 +156,12 @@ class REST(object):
@param data: if None or omitted, message will be a GET, else it will send a POST @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" @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)) logger.debug('Invoking post message {} with data {}'.format(msg, data))
if processData and data is not None: if processData and data is not None:
data = json.dumps(data) data = json.dumps(data)
url = self._getUrl(msg) url = self._getUrl(msg)
#logger.debug('Requesting {}'.format(url)) logger.debug('Requesting {}'.format(url))
try: return self._request(url, data)
res = self._request(url, data)
return res
except:
logger.exception()
return None

View File

@ -29,15 +29,19 @@
""" """
@author: Adolfo Gómez, dkmaster at dkmon dot com @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 # On centos, old six release does not includes byte2int, nor six.PY2
import six import six
from . import modules import modules
from .RESTApi import REST, RESTError from RESTApi import REST, RESTError
VERSION='0' try:
with open('../VERSION', 'r') as v:
VERSION = v.read().strip()
except IOError:
VERSION = '1.1.1b'
__title__ = 'OpenGnsys Agent' __title__ = 'OpenGnsys Agent'
__version__ = VERSION __version__ = VERSION

View File

@ -26,26 +26,25 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # 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. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
""" '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
""" '''
# pylint: disable=unused-wildcard-import, wildcard-import # pylint: disable=unused-wildcard-import, wildcard-import
from __future__ import unicode_literals
from ConfigParser import SafeConfigParser
from configparser import ConfigParser
config = None config = None
def readConfig(client=False): def readConfig(client=False):
""" '''
Reads configuration file Reads configuration file
If client is False, will read ogagent.cfg as configuration If client is False, will read ogagent.cfg as configuration
If client is True, will read ogclient.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. This is this way so we can protect ogagent.cfg against reading for non admin users on all platforms.
""" '''
cfg = ConfigParser() cfg = SafeConfigParser()
if client is True: if client is True:
fname = 'ogclient.cfg' fname = 'ogclient.cfg'
else: else:
@ -56,3 +55,4 @@ def readConfig(client=False):
return None return None
return cfg return cfg

View File

@ -25,26 +25,27 @@
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # 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 # 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. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
""" '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @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 os import six
import json
import ssl
import threading
from six.moves.socketserver import ThreadingMixIn # @UnresolvedImport from six.moves.socketserver import ThreadingMixIn # @UnresolvedImport
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler # @UnresolvedImport from six.moves.BaseHTTPServer import BaseHTTPRequestHandler # @UnresolvedImport
from six.moves.BaseHTTPServer import HTTPServer # @UnresolvedImport from six.moves.BaseHTTPServer import HTTPServer # @UnresolvedImport
from six.moves.urllib.parse import unquote # @UnresolvedImport from six.moves.urllib.parse import unquote # @UnresolvedImport
import json
import threading
import ssl
from .utils import exceptionToMessage from .utils import exceptionToMessage
from .certs import createSelfSignedCert from .certs import createSelfSignedCert
from .log import logger from .log import logger
VERIFY_TLS=True
class HTTPServerHandler(BaseHTTPRequestHandler): class HTTPServerHandler(BaseHTTPRequestHandler):
service = None service = None
protocol_version = 'HTTP/1.0' protocol_version = 'HTTP/1.0'
@ -55,23 +56,22 @@ class HTTPServerHandler(BaseHTTPRequestHandler):
self.send_response(code) self.send_response(code)
self.send_header('Content-type', 'application/json') self.send_header('Content-type', 'application/json')
self.end_headers() self.end_headers()
self.wfile.write(str.encode(json.dumps({'error': message}))) self.wfile.write(json.dumps({'error': message}))
return return
def sendJsonResponse(self, data): def sendJsonResponse(self, data):
try: self.send_response(200) self.send_response(200)
except Exception as e: logger.warn ('exception: "{}"'.format(str(e)))
data = json.dumps(data) data = json.dumps(data)
self.send_header('Content-type', 'application/json') self.send_header('Content-type', 'application/json')
self.send_header('Content-Length', str(len(data))) self.send_header('Content-Length', len(data))
self.end_headers() self.end_headers()
# Send the html message # Send the html message
self.wfile.write(str.encode(data)) self.wfile.write(data)
# parseURL
def parseUrl(self): def parseUrl(self):
""" # Very simple path & params splitter
Very simple path & params splitter
"""
path = self.path.split('?')[0][1:].split('/') path = self.path.split('?')[0][1:].split('/')
try: try:
@ -79,46 +79,22 @@ class HTTPServerHandler(BaseHTTPRequestHandler):
except Exception: except Exception:
params = {} params = {}
## quick override because universities do not actually want the module to be extracted out of the URL
module = 'ogAdmClient' if os.path.exists ('/scripts/oginit') else 'opengnsys'
for v in self.service.modules: for v in self.service.modules:
if v.name == module: # Case Sensitive!!!! if v.name == path[0]: # Case Sensitive!!!!
return v, path[1:], params return (v, path[1:], params)
return None, path, params return (None, path, params)
def notifyMessage(self, module, path, get_params, post_params): def notifyMessage(self, module, path, getParams, postParams):
""" '''
Locates witch module will process the message based on path (first folder on url path) Locates witch module will process the message based on path (first folder on url path)
""" '''
try: try:
if module is None: data = module.processServerMessage(path, getParams, postParams, self)
raise Exception ({ '_httpcode': 404, '_msg': f'Module {path[0]} not found' })
data = module.processServerMessage(path, get_params, post_params, self)
self.sendJsonResponse(data) self.sendJsonResponse(data)
except Exception as e: except Exception as e:
logger.exception() logger.exception()
n_args = len (e.args) self.sendJsonError(500, exceptionToMessage(e))
if 0 == n_args:
logger.debug ('Empty exception raised from message processor for "{}"'.format(path[0]))
self.sendJsonError(500, exceptionToMessage(e))
else:
arg0 = e.args[0]
if type (arg0) is str:
logger.debug ('Message processor for "{}" returned exception string "{}"'.format(path[0], str(e)))
self.sendJsonError (500, exceptionToMessage(e))
elif type (arg0) is dict:
if '_httpcode' in arg0:
logger.debug ('Message processor for "{}" returned HTTP code "{}" with exception string "{}"'.format(path[0], str(arg0['_httpcode']), str(arg0['_msg'])))
self.sendJsonError (arg0['_httpcode'], arg0['_msg'])
else:
logger.debug ('Message processor for "{}" returned exception dict "{}" with no HTTP code'.format(path[0], str(e)))
self.sendJsonError (500, exceptionToMessage(e))
else:
logger.debug ('Message processor for "{}" returned non-string and non-dict exception "{}", type "{}"'.format(path[0], str(e), type(e)))
self.sendJsonError (500, exceptionToMessage(e))
## not reached
def do_GET(self): def do_GET(self):
module, path, params = self.parseUrl() module, path, params = self.parseUrl()
@ -126,74 +102,40 @@ class HTTPServerHandler(BaseHTTPRequestHandler):
self.notifyMessage(module, path, params, None) self.notifyMessage(module, path, params, None)
def do_POST(self): def do_POST(self):
module, path, get_params = self.parseUrl() module, path, getParams = self.parseUrl()
post_params = None
# Tries to get JSON content (UTF-8 encoded) # Tries to get JSON content (UTF-8 encoded)
try: try:
length = int(self.headers.get('content-length')) length = int(self.headers.getheader('content-length'))
content = self.rfile.read(length).decode('utf-8') content = self.rfile.read(length).decode('utf-8')
logger.debug('length: {0}, content >>{1}<<'.format(length, content)) logger.debug('length: {}, content >>{}<<'.format(length, content))
post_params = json.loads(content) postParams = json.loads(content)
except Exception as e: except Exception as e:
self.sendJsonError(500, exceptionToMessage(e)) self.sendJsonError(500, exceptionToMessage(e))
self.notifyMessage(module, path, get_params, post_params) self.notifyMessage(module, path, getParams, postParams)
def log_error(self, fmt, *args): def log_error(self, fmt, *args):
logger.error('HTTP ' + fmt % args) logger.error('HTTP ' + fmt % args)
def log_message(self, fmt, *args): def log_message(self, fmt, *args):
logger.debug('HTTP ' + fmt % args) logger.info('HTTP ' + fmt % args)
class HTTPThreadingServer(ThreadingMixIn, HTTPServer): class HTTPThreadingServer(ThreadingMixIn, HTTPServer):
pass pass
class HTTPServerThread(threading.Thread): class HTTPServerThread(threading.Thread):
def __init__(self, address, service): def __init__(self, address, service):
super(self.__class__, self).__init__() super(self.__class__, self).__init__()
global VERIFY_TLS
HTTPServerHandler.service = service # Keep tracking of service so we can intercact with it HTTPServerHandler.service = service # Keep tracking of service so we can intercact with it
self.certFile = createSelfSignedCert()
self.server = HTTPThreadingServer(address, HTTPServerHandler) self.server = HTTPThreadingServer(address, HTTPServerHandler)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) self.server.socket = ssl.wrap_socket(self.server.socket, certfile=self.certFile, server_side=True)
pf = os.environ.get ('PROGRAMFILES(X86)')
if pf: pf = os.path.join (pf, 'OGAgent')
if os.path.exists ('/opt/opengnsys/etc/ogagent.crt') and os.path.exists ('/opt/opengnsys/etc/ogagent.key') and os.path.exists ('/opt/opengnsys/etc/ca.crt'):
logger.debug ('HTTP server: using certificate/CA from /opt/opengnsys/etc')
context.load_cert_chain (certfile='/opt/opengnsys/etc/ogagent.crt', keyfile='/opt/opengnsys/etc/ogagent.key')
context.load_verify_locations (cafile='/opt/opengnsys/etc/ca.crt')
elif os.path.exists (os.path.join (pf, 'ogagent.crt')) and os.path.exists (os.path.join (pf, 'ogagent.key')) and os.path.exists (os.path.join (pf, 'ca.crt')):
logger.debug (f'HTTP server: using certificate/CA from the installation path ({pf})')
context.load_cert_chain (certfile=os.path.join (pf, 'ogagent.crt'), keyfile=os.path.join (pf, 'ogagent.key'))
context.load_verify_locations (cafile=os.path.join (pf, 'ca.crt'))
elif os.path.exists ('./ogagent.crt') and os.path.exists ('./ogagent.key') and os.path.exists ('./ca.crt'):
cwd = os.getcwd()
logger.debug (f'HTTP server: using certificate/CA from the current working directory ({cwd})')
context.load_cert_chain (certfile=f'{cwd}/ogagent.crt', keyfile=f'{cwd}/ogagent.key')
context.load_verify_locations (cafile=f'{cwd}/ca.crt')
else:
logger.debug ('HTTP server: using a self-signed certificate')
self.certFile = createSelfSignedCert()
context.load_cert_chain (certfile=self.certFile)
VERIFY_TLS = False
if VERIFY_TLS:
context.verify_mode = ssl.CERT_REQUIRED
context.verify_flags &= ssl.VERIFY_X509_STRICT
else:
context.verify_mode = ssl.CERT_NONE
context.verify_flags &= ~ssl.VERIFY_X509_STRICT
s = context.cert_store_stats()
if 'x509_ca' in s: logger.debug (f'HTTP server: {s['x509_ca']} CAs loaded')
if 'x509' in s: logger.debug (f'HTTP server: {s['x509']} certs loaded')
self.server.socket = context.wrap_socket(self.server.socket, server_side=True)
logger.debug('Initialized HTTPS Server thread on {}'.format(address)) logger.debug('Initialized HTTPS Server thread on {}'.format(address))
def getServerUrl(self): def getServerUrl(self):
@ -204,3 +146,5 @@ class HTTPServerThread(threading.Thread):
def run(self): def run(self):
self.server.serve_forever() self.server.serve_forever()

View File

@ -25,16 +25,16 @@
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # 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 # 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. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
""" '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
""" '''
from __future__ import unicode_literals
import json
import queue
import socket import socket
import threading import threading
import six
import traceback import traceback
import json
from opengnsys.utils import toUnicode from opengnsys.utils import toUnicode
from opengnsys.log import logger from opengnsys.log import logger
@ -59,7 +59,7 @@ from opengnsys.log import logger
# BYTE # BYTE
# 0 1-2 3 4 ... # 0 1-2 3 4 ...
# MSG_ID DATA_LENGTH (little endian) Data (can be 0 length) # MSG_ID DATA_LENGTH (little endian) Data (can be 0 length)
# With a previous "MAGIC" header in front of each message # With a previos "MAGIC" header in fron of each message
# Client messages # Client messages
MSG_LOGOFF = 0xA1 # Request log off from an user MSG_LOGOFF = 0xA1 # Request log off from an user
@ -84,7 +84,7 @@ REV_DICT = {
REQ_MESSAGE: 'REQ_MESSAGE' REQ_MESSAGE: 'REQ_MESSAGE'
} }
MAGIC = b'\x4F\x47\x41\x00' # OGA in hex with a padded 0 to the right MAGIC = b'\x4F\x47\x41\x00' # OGA in hexa with a padded 0 to the right
# States for client processor # States for client processor
@ -99,10 +99,10 @@ class ClientProcessor(threading.Thread):
self.parent = parent self.parent = parent
self.clientSocket = clientSocket self.clientSocket = clientSocket
self.running = False self.running = False
self.messages = queue.Queue(32) self.messages = six.moves.queue.Queue(32) # @UndefinedVariable
def stop(self): def stop(self):
logger.debug('Stopping client processor') logger.debug('Stoping client processor')
self.running = False self.running = False
def processRequest(self, msg, data): def processRequest(self, msg, data):
@ -117,7 +117,6 @@ class ClientProcessor(threading.Thread):
state = None state = None
recv_msg = None recv_msg = None
recv_data = None recv_data = None
msg_len = 0
while self.running: while self.running:
try: try:
counter = 1024 counter = 1024
@ -128,7 +127,7 @@ class ClientProcessor(threading.Thread):
# Client disconnected # Client disconnected
self.running = False self.running = False
break break
buf = int.from_bytes(b, 'big') # Empty buffer, this is set as non-blocking buf = six.byte2int(b) # Empty buffer, this is set as non-blocking
if state is None: if state is None:
if buf in (REQ_MESSAGE, REQ_LOGIN, REQ_LOGOUT): if buf in (REQ_MESSAGE, REQ_LOGIN, REQ_LOGOUT):
logger.debug('State set to {}'.format(buf)) logger.debug('State set to {}'.format(buf))
@ -153,7 +152,7 @@ class ClientProcessor(threading.Thread):
recv_data = b'' recv_data = b''
continue continue
elif state == ST_RECEIVING: elif state == ST_RECEIVING:
recv_data += bytes([buf]) recv_data += six.int2byte(buf)
msg_len -= 1 msg_len -= 1
if msg_len == 0: if msg_len == 0:
self.processRequest(recv_msg, recv_data) self.processRequest(recv_msg, recv_data)
@ -174,7 +173,7 @@ class ClientProcessor(threading.Thread):
try: try:
msg = self.messages.get(block=True, timeout=1) msg = self.messages.get(block=True, timeout=1)
except queue.Empty: # No message got in time @UndefinedVariable except six.moves.queue.Empty: # No message got in time @UndefinedVariable
continue continue
logger.debug('Got message {}={}'.format(msg, REV_DICT.get(msg[0]))) logger.debug('Got message {}={}'.format(msg, REV_DICT.get(msg[0])))
@ -182,7 +181,7 @@ class ClientProcessor(threading.Thread):
try: try:
m = msg[1] if msg[1] is not None else b'' m = msg[1] if msg[1] is not None else b''
l = len(m) l = len(m)
data = MAGIC + bytes([msg[0]]) + bytes([l & 0xFF]) + bytes([l >> 8]) + m data = MAGIC + six.int2byte(msg[0]) + six.int2byte(l & 0xFF) + six.int2byte(l >> 8) + m
try: try:
self.clientSocket.sendall(data) self.clientSocket.sendall(data)
except socket.error as e: except socket.error as e:
@ -221,20 +220,20 @@ class ServerIPC(threading.Thread):
for t in self.threads: for t in self.threads:
t.join() t.join()
def sendMessage(self, msg_id, msg_data): def sendMessage(self, msgId, msgData):
""" '''
Notify message to all listening threads Notify message to all listening threads
""" '''
logger.debug('Sending message {}({}),{} to all clients'.format(msg_id, REV_DICT.get(msg_id), msg_data)) logger.debug('Sending message {}({}),{} to all clients'.format(msgId, REV_DICT.get(msgId), msgData))
# Convert to bytes so length is correctly calculated # Convert to bytes so length is correctly calculated
if isinstance(msg_data, str): if isinstance(msgData, six.text_type):
msg_data = str.encode(msg_data) msgData = msgData.encode('utf8')
for t in self.threads: for t in self.threads:
if t.is_alive(): if t.isAlive():
logger.debug('Sending to {}'.format(t)) logger.debug('Sending to {}'.format(t))
t.messages.put((msg_id, msg_data)) t.messages.put((msgId, msgData))
def sendLoggofMessage(self): def sendLoggofMessage(self):
self.sendMessage(MSG_LOGOFF, '') self.sendMessage(MSG_LOGOFF, '')
@ -243,18 +242,18 @@ class ServerIPC(threading.Thread):
self.sendMessage(MSG_MESSAGE, message) self.sendMessage(MSG_MESSAGE, message)
def sendPopupMessage(self, title, message): def sendPopupMessage(self, title, message):
self.sendMessage(MSG_POPUP, {'title': title, 'message': message}) self.sendMessage(MSG_POPUP, {'title':title, 'message':message})
def sendScriptMessage(self, script): def sendScriptMessage(self, script):
self.sendMessage(MSG_SCRIPT, script) self.sendMessage(MSG_SCRIPT, script)
def cleanupFinishedThreads(self): def cleanupFinishedThreads(self):
""" '''
Cleans up current threads list Cleans up current threads list
""" '''
aliveThreads = [] aliveThreads = []
for t in self.threads: for t in self.threads:
if t.is_alive(): if t.isAlive():
logger.debug('Thread {} is alive'.format(t)) logger.debug('Thread {} is alive'.format(t))
aliveThreads.append(t) aliveThreads.append(t)
self.threads[:] = aliveThreads self.threads[:] = aliveThreads
@ -263,7 +262,7 @@ class ServerIPC(threading.Thread):
self.running = True self.running = True
self.serverSocket.bind(('localhost', self.port)) self.serverSocket.bind(('localhost', self.port))
self.serverSocket.setblocking(True) self.serverSocket.setblocking(1)
self.serverSocket.listen(4) self.serverSocket.listen(4)
while True: while True:
@ -290,7 +289,7 @@ class ClientIPC(threading.Thread):
self.port = listenPort self.port = listenPort
self.running = False self.running = False
self.clientSocket = None self.clientSocket = None
self.messages = queue.Queue(32) # @UndefinedVariable self.messages = six.moves.queue.Queue(32) # @UndefinedVariable
self.connect() self.connect()
@ -301,7 +300,7 @@ class ClientIPC(threading.Thread):
while self.running: while self.running:
try: try:
return self.messages.get(timeout=1) return self.messages.get(timeout=1)
except queue.Empty: except six.moves.queue.Empty: # @UndefinedVariable
continue continue
return None return None
@ -311,34 +310,34 @@ class ClientIPC(threading.Thread):
if data is None: if data is None:
data = b'' data = b''
if isinstance(data, str): if isinstance(data, six.text_type): # Convert to bytes if necessary
data = str.encode(data) data = data.encode('utf-8')
l = len(data) l = len(data)
msg = bytes([msg]) + bytes([l & 0xFF]) + bytes([l >> 8]) + data msg = six.int2byte(msg) + six.int2byte(l & 0xFF) + six.int2byte(l >> 8) + data
self.clientSocket.sendall(msg) self.clientSocket.sendall(msg)
def sendLogin(self, user_data): def sendLogin(self, username, language):
self.sendRequestMessage(REQ_LOGIN, ','.join(user_data)) self.sendRequestMessage(REQ_LOGIN, username+','+language)
def sendLogout(self, username): def sendLogout(self, username):
self.sendRequestMessage(REQ_LOGOUT, username) self.sendRequestMessage(REQ_LOGOUT, username)
def sendMessage(self, module, message, data=None): 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) 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 module: Module that will receive this message
@param message: Message to send. This message is "customized", and understand by modules @param message: Message to send. This message is "customized", and understand by modules
@param data: Data to be send as message companion @param data: Data to be send as message companion
""" '''
msg = '\0'.join((module, message, json.dumps(data))) msg = '\0'.join((module, message, json.dumps(data)))
self.sendRequestMessage(REQ_MESSAGE, msg) self.sendRequestMessage(REQ_MESSAGE, msg)
def messageReceived(self): def messageReceived(self):
""" '''
Override this method to automatically get notified on new message Override this method to automatically get notified on new message
received. Message is at self.messages queue received. Message is at self.messages queue
""" '''
pass pass
def receiveBytes(self, number): def receiveBytes(self, number):
@ -421,3 +420,4 @@ class ClientIPC(threading.Thread):
self.clientSocket.close() self.clientSocket.close()
except Exception: except Exception:
pass # If can't close, nothing happens, just end thread pass # If can't close, nothing happens, just end thread

View File

@ -1,62 +0,0 @@
import threading
import subprocess
import hashlib
import time
from datetime import datetime, timezone
from opengnsys import operations
from opengnsys.log import logger
def job_readstdout(job):
for l in iter(job['p'].stdout.readline, b''):
job['stdout'] += l.decode ('utf-8', 'ignore')
def job_readstderr(job):
for l in iter(job['p'].stderr.readline, b''):
job['stderr'] += l.decode ('utf-8', 'ignore')
class JobMgr():
jobs = {}
def launch_job(self, script, is_client):
logger.debug ('in launch_job(), is_client "{}"'.format(is_client))
args = operations.build_popen_args (script)
logger.debug ('args "{}"'.format (args))
now = datetime.now (tz=timezone.utc)
ts = now.strftime ('%Y-%m-%d %H:%M:%S.%f%z') ## '%s' doesn't work on windows
jobid = hashlib.sha256 (now.isoformat().encode('UTF-8') + script.encode ('UTF-8')).hexdigest()[0:12]
p = subprocess.Popen (args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.jobs[jobid] = { 'p': p, 'pid': p.pid, 'starttime': ts, 'script': script, 'client': is_client, 'status': 'running', 'stdout': '', 'stderr': '' }
self.jobs[jobid]['t1'] = threading.Thread (target=job_readstdout, args=(self.jobs[jobid],))
self.jobs[jobid]['t2'] = threading.Thread (target=job_readstderr, args=(self.jobs[jobid],))
self.jobs[jobid]['t1'].start()
self.jobs[jobid]['t2'].start()
logger.debug ('jobs "{}"'.format (self.jobs))
return jobid
def prepare_jobs(self):
## can't return self.jobs because the Popen object at self.jobs[id]['p'] is not serializable. So, need to create a new dict to return
st = []
for jobid in self.jobs:
j = self.jobs[jobid]
entry = dict ((k, j[k]) for k in ['pid', 'starttime', 'script', 'client', 'status', 'stdout', 'stderr'])
entry['jobid'] = jobid
if j['p'].poll() is not None: ## process finished
entry['rc'] = j['p'].returncode
entry['status'] = 'finished'
st.append (entry)
return st
def terminate_job(self, jobid):
if jobid not in self.jobs: return {}
p = self.jobs[jobid]['p']
p.terminate()
time.sleep (1)
if p.poll() is not None:
return { 'terminated': True }
p.kill()
time.sleep (1)
if p.poll() is not None:
return { 'killed': True }
return { 'killed': False }

View File

@ -26,10 +26,10 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # 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. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
""" '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
""" '''
from __future__ import unicode_literals
from opengnsys.service import CommonService from opengnsys.service import CommonService
from opengnsys.service import IPC_PORT from opengnsys.service import IPC_PORT
@ -45,7 +45,7 @@ import json
try: try:
from prctl import set_proctitle # @UnresolvedImport from prctl import set_proctitle # @UnresolvedImport
except ImportError: # Platform may not include prctl, so in case it's not available, we let the "name" as is except Exception: # Platform may not include prctl, so in case it's not available, we let the "name" as is
def set_proctitle(_): def set_proctitle(_):
pass pass
@ -63,6 +63,7 @@ class OGAgentSvc(Daemon, CommonService):
# Call modules initialization # Call modules initialization
# They are called in sequence, no threading is done at this point, so ensure modules onActivate always returns # They are called in sequence, no threading is done at this point, so ensure modules onActivate always returns
# ********************* # *********************
# * Main Service loop * # * Main Service loop *
@ -92,7 +93,6 @@ def usage():
sys.stderr.write("usage: {} start|stop|restart|fg|login 'username'|logout 'username'|message 'module' 'message' 'json'\n".format(sys.argv[0])) sys.stderr.write("usage: {} start|stop|restart|fg|login 'username'|logout 'username'|message 'module' 'message' 'json'\n".format(sys.argv[0]))
sys.exit(2) sys.exit(2)
if __name__ == '__main__': if __name__ == '__main__':
logger.setLevel('INFO') logger.setLevel('INFO')
@ -105,6 +105,7 @@ if __name__ == '__main__':
sys.exit(0) sys.exit(0)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
if len(sys.argv) == 3 and sys.argv[1] in ('login', 'logout'): if len(sys.argv) == 3 and sys.argv[1] in ('login', 'logout'):
logger.debug('Running client opengnsys') logger.debug('Running client opengnsys')

View File

@ -26,6 +26,7 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # 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. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
""" '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
""" '''
from __future__ import unicode_literals

View File

@ -25,16 +25,18 @@
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # 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 # 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. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
""" '''
@author: : http://www.jejik.com/authors/sander_marechal/ @author: : http://www.jejik.com/authors/sander_marechal/
@see: : http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/ @see: : http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/
""" '''
import atexit from __future__ import unicode_literals
import os
import sys import sys
import os
import time import time
import atexit
from opengnsys.log import logger from opengnsys.log import logger
from signal import SIGTERM from signal import SIGTERM
@ -87,7 +89,7 @@ class Daemon:
sys.stderr.flush() sys.stderr.flush()
si = open(self.stdin, 'r') si = open(self.stdin, 'r')
so = open(self.stdout, 'a+') so = open(self.stdout, 'a+')
se = open(self.stderr, 'a+') se = open(self.stderr, 'a+', 0)
os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno()) os.dup2(se.fileno(), sys.stderr.fileno())

View File

@ -26,18 +26,18 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # 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. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
""" '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
""" '''
from __future__ import unicode_literals
import logging import logging
import os import os
import tempfile import tempfile
import six
from ..log_format import JsonFormatter # Valid logging levels, from UDS Broker (uds.core.utils.log)
OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in six.moves.xrange(6)) # @UndefinedVariable
# Logging levels
OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in range(6))
class LocalLogger(object): class LocalLogger(object):
@ -46,29 +46,19 @@ class LocalLogger(object):
# service wil get c:\windows\temp, while user will get c:\users\XXX\temp # service wil get c:\windows\temp, while user will get c:\users\XXX\temp
# Try to open logger at /var/log path # 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 # If it fails (access denied normally), will try to open one at user's home folder, and if
# again it fails, open it at the tmpPath # agaim it fails, open it at the tmpPath
for logDir in ('/var/log', os.path.expanduser('~'), tempfile.gettempdir()): for logDir in ('/var/log', os.path.expanduser('~'), tempfile.gettempdir()):
try: try:
fname1 = os.path.join (logDir, 'opengnsys.log') fname = os.path.join(logDir, 'opengnsys.log')
fmt1 = logging.Formatter (fmt='%(levelname)s %(asctime)s (%(threadName)s) (%(funcName)s) %(message)s') logging.basicConfig(
fh1 = logging.FileHandler (filename=fname1, mode='a') filename=fname,
fh1.setFormatter (fmt1) filemode='a',
fh1.setLevel (logging.DEBUG) format='%(levelname)s %(asctime)s %(message)s',
level=logging.DEBUG
fname2 = os.path.join (logDir, 'opengnsys.json.log') )
fmt2 = JsonFormatter ({"timestamp": "asctime", "severity": "levelname", "threadName": "threadName", "function": "funcName", "message": "message"}, time_format='%Y-%m-%d %H:%M:%S', msec_format='') self.logger = logging.getLogger('opengnsys')
fh2 = logging.FileHandler (filename=fname2, mode='a') os.chmod(fname, 0o0600)
fh2.setFormatter (fmt2)
fh2.setLevel (logging.DEBUG)
self.logger = logging.getLogger ('opengnsys')
self.logger.setLevel (logging.DEBUG)
self.logger.addHandler (fh1)
self.logger.addHandler (fh2)
os.chmod (fname1, 0o0600)
os.chmod (fname2, 0o0600)
return return
except Exception: except Exception:
pass pass
@ -81,7 +71,7 @@ class LocalLogger(object):
# our loglevels are 10000 (other), 20000 (debug), .... # our loglevels are 10000 (other), 20000 (debug), ....
# logging levels are 10 (debug), 20 (info) # logging levels are 10 (debug), 20 (info)
# OTHER = logging.NOTSET # OTHER = logging.NOTSET
self.logger.log(int(level / 1000) - 10, message, stacklevel=4) self.logger.log(int(level / 1000) - 10, message)
def isWindows(self): def isWindows(self):
return False return False

View File

@ -29,7 +29,7 @@
''' '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
from __future__ import unicode_literals
import socket import socket
import platform import platform
@ -44,6 +44,7 @@ import array
import six import six
import distro import distro
from opengnsys import utils from opengnsys import utils
from .renamer import rename
def _getMacAddr(ifname): def _getMacAddr(ifname):
@ -103,7 +104,7 @@ def _getInterfaces():
0x8912, # SIOCGIFCONF 0x8912, # SIOCGIFCONF
struct.pack(str('iL'), space, names.buffer_info()[0]) struct.pack(str('iL'), space, names.buffer_info()[0])
))[0] ))[0]
namestr = names.tobytes() namestr = names.tostring()
# return namestr, outbytes # return namestr, outbytes
return [namestr[i:i + offset].split(b'\0', 1)[0].decode('utf-8') for i in range(0, outbytes, length)] return [namestr[i:i + offset].split(b'\0', 1)[0].decode('utf-8') for i in range(0, outbytes, length)]
@ -113,6 +114,13 @@ def _getIpAndMac(ifname):
return (ip, mac) return (ip, mac)
def getComputerName():
'''
Returns computer name, with no domain
'''
return socket.gethostname().split('.')[0]
def getNetworkInfo(): def getNetworkInfo():
''' '''
Obtains a list of network interfaces Obtains a list of network interfaces
@ -127,6 +135,10 @@ def getNetworkInfo():
yield utils.Bunch(name=ifname, mac=mac, ip=ip) yield utils.Bunch(name=ifname, mac=mac, ip=ip)
def getDomainName():
return ''
def getLinuxVersion(): def getLinuxVersion():
""" """
Returns the version of the Linux distribution Returns the version of the Linux distribution
@ -143,11 +155,7 @@ def reboot(flags=0):
import threading import threading
threading._DummyThread._Thread__stop = lambda x: 42 threading._DummyThread._Thread__stop = lambda x: 42
# Check for OpenGnsys Client or GNU/Linux distribution. subprocess.call(['/sbin/reboot'])
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): def poweroff(flags=0):
@ -159,11 +167,7 @@ def poweroff(flags=0):
import threading import threading
threading._DummyThread._Thread__stop = lambda x: 42 threading._DummyThread._Thread__stop = lambda x: 42
# Check for OpenGnsys Client or GNU/Linux distribution. subprocess.call(['/sbin/poweroff'])
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(): def logoff():
@ -179,6 +183,83 @@ def logoff():
subprocess.call(['/usr/bin/pkill', '-u', os.environ['USER']]) 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(): def getCurrentUser():
''' '''
Returns current logged in user Returns current logged in user
@ -193,14 +274,6 @@ def getSessionLanguage():
return locale.getdefaultlocale()[0] return locale.getdefaultlocale()[0]
def get_session_type():
"""
Returns the user's session type (xrdp, wayland, x11, tty,...)
:return: string
"""
return 'xrdp' if 'XRDP_SESSION' in os.environ else os.environ.get('XDG_SESSION_TYPE', 'unknown').lower()
def showPopup(title, message): def showPopup(title, message):
''' '''
Displays a message box on user's session (during 1 min). Displays a message box on user's session (during 1 min).
@ -214,7 +287,3 @@ def get_etc_path():
Returns etc directory path. Returns etc directory path.
""" """
return os.sep + 'etc' return os.sep + 'etc'
def build_popen_args(script):
return ['/bin/sh', '-c', script]

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -33,9 +33,8 @@
# This is a simple module loader, so we can add "external opengnsys" modules as addons # This is a simple module loader, so we can add "external opengnsys" modules as addons
# Modules under "opengsnsys/modules" are always autoloaded # Modules under "opengsnsys/modules" are always autoloaded
from __future__ import unicode_literals
import sys
import pkgutil import pkgutil
import os.path import os.path
@ -43,8 +42,6 @@ from opengnsys.workers import ServerWorker
from opengnsys.workers import ClientWorker from opengnsys.workers import ClientWorker
from .log import logger from .log import logger
PY3_12 = sys.version_info[0:2] >= (3, 12)
def loadModules(controller, client=False): def loadModules(controller, client=False):
''' '''
@ -58,10 +55,12 @@ def loadModules(controller, client=False):
ogModules = [] ogModules = []
if client is False: if client is False:
from opengnsys.modules.server import OpenGnSys # @UnusedImport
from .modules import server # @UnusedImport, just used to ensure opengnsys modules are initialized from .modules import server # @UnusedImport, just used to ensure opengnsys modules are initialized
modPath = 'opengnsys.modules.server' modPath = 'opengnsys.modules.server'
modType = ServerWorker modType = ServerWorker
else: else:
from opengnsys.modules.client import OpenGnSys # @UnusedImport @Reimport
from .modules import client # @UnusedImport, just used to ensure opengnsys modules are initialized from .modules import client # @UnusedImport, just used to ensure opengnsys modules are initialized
modPath = 'opengnsys.modules.client' modPath = 'opengnsys.modules.client'
modType = ClientWorker modType = ClientWorker
@ -91,11 +90,7 @@ def loadModules(controller, client=False):
for (module_loader, name, ispkg) in pkgutil.iter_modules(paths, modPath + '.'): for (module_loader, name, ispkg) in pkgutil.iter_modules(paths, modPath + '.'):
if ispkg: if ispkg:
logger.debug('Found module package {}'.format(name)) logger.debug('Found module package {}'.format(name))
if PY3_12: module_loader.find_module(name).load_module(name)
loader = module_loader.find_spec(name).loader
else:
loader = module_loader.find_module(name)
loader.load_module(name)
if controller.config.has_option('opengnsys', 'path') is True: if controller.config.has_option('opengnsys', 'path') is True:
@ -103,14 +98,14 @@ def loadModules(controller, client=False):
else: else:
paths = () paths = ()
paths += (os.path.dirname(sys.modules[modPath].__file__),) # paths += (os.path.dirname(sys.modules[modPath].__file__),)
# Load modules
logger.debug('Loading modules from {}'.format(paths)) logger.debug('Loading modules from {}'.format(paths))
# Load modules
doLoad(paths) doLoad(paths)
# Add to list of available modules # Add to list of available modules
logger.debug('Adding {} classes'.format('server' if modType == ServerWorker else 'client'))
recursiveAdd(modType) recursiveAdd(modType)
return ogModules return ogModules

View File

@ -26,21 +26,22 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # 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. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
""" '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
""" '''
from __future__ import unicode_literals
import sys
import traceback import traceback
import sys
import six
if sys.platform == 'win32': if sys.platform == 'win32':
from opengnsys.windows.log import LocalLogger from opengnsys.windows.log import LocalLogger # @UnusedImport
else: else:
from opengnsys.linux.log import LocalLogger from opengnsys.linux.log import LocalLogger # @Reimport
# Valid logging levels, from UDS Broker (uds.core.utils.log) # Valid logging levels, from UDS Broker (uds.core.utils.log)
OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in range(6)) OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in six.moves.xrange(6)) # @UndefinedVariable
_levelName = { _levelName = {
'OTHER': OTHER, 'OTHER': OTHER,
@ -51,18 +52,17 @@ _levelName = {
'FATAL': FATAL 'FATAL': FATAL
} }
class Logger(object): class Logger(object):
def __init__(self): def __init__(self):
self.logLevel = INFO self.logLevel = INFO
self.logger = LocalLogger() self.logger = LocalLogger()
def setLevel(self, level): def setLevel(self, level):
""" '''
Sets log level filter (minimum level required for a log message to be processed) 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 :param level: Any message with a level below this will be filtered out
""" '''
if isinstance(level, str): if isinstance(level, six.string_types):
level = _levelName.get(level, INFO) level = _levelName.get(level, INFO)
self.logLevel = level # Ensures level is an integer or fails self.logLevel = level # Ensures level is an integer or fails
@ -79,9 +79,6 @@ class Logger(object):
def warn(self, message): def warn(self, message):
self.log(WARN, message) self.log(WARN, message)
def warning(self, message):
self.log(WARN, message)
def info(self, message): def info(self, message):
self.log(INFO, message) self.log(INFO, message)
@ -97,7 +94,7 @@ class Logger(object):
except Exception: except Exception:
tb = '(could not get traceback!)' tb = '(could not get traceback!)'
self.log(DEBUG, 'traceback follows: "{}"'.format(tb)) self.log(DEBUG, tb)
def flush(self): def flush(self):
pass pass

View File

@ -1,55 +0,0 @@
import json
import logging
class JsonFormatter(logging.Formatter):
"""
Formatter that outputs JSON strings after parsing the LogRecord.
@param dict fmt_dict: Key: logging format attribute pairs. Defaults to {"message": "message"}.
@param str time_format: time.strftime() format string. Default: "%Y-%m-%dT%H:%M:%S"
@param str msec_format: Microsecond formatting. Appended at the end. Default: "%s.%03dZ"
"""
def __init__(self, fmt_dict: dict = None, time_format: str = "%Y-%m-%dT%H:%M:%S", msec_format: str = "%s.%03dZ"):
self.fmt_dict = fmt_dict if fmt_dict is not None else {"message": "message"}
self.default_time_format = time_format
self.default_msec_format = msec_format
self.datefmt = None
def usesTime(self) -> bool:
"""
Overwritten to look for the attribute in the format dict values instead of the fmt string.
"""
return "asctime" in self.fmt_dict.values()
def formatMessage(self, record) -> dict:
"""
Overwritten to return a dictionary of the relevant LogRecord attributes instead of a string.
KeyError is raised if an unknown attribute is provided in the fmt_dict.
"""
return {fmt_key: record.__dict__[fmt_val] for fmt_key, fmt_val in self.fmt_dict.items()}
def format(self, record) -> str:
"""
Mostly the same as the parent's class method, the difference being that a dict is manipulated and dumped as JSON
instead of a string.
"""
record.message = record.getMessage()
if self.usesTime():
record.asctime = self.formatTime(record, self.datefmt)
message_dict = self.formatMessage(record)
if record.exc_info:
# Cache the traceback text to avoid converting it multiple times
# (it's constant anyway)
if not record.exc_text:
record.exc_text = self.formatException(record.exc_info)
if record.exc_text:
message_dict["exc_info"] = record.exc_text
if record.stack_info:
message_dict["stack_info"] = self.formatStack(record.stack_info)
return json.dumps(message_dict, default=str)

View File

@ -29,4 +29,4 @@
''' '''
@author: Ramón M. Gómez, ramongomez at us dot es @author: Ramón M. Gómez, ramongomez at us dot es
''' '''
from __future__ import unicode_literals

View File

@ -29,7 +29,7 @@
''' '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
from __future__ import unicode_literals
import socket import socket
import platform import platform
@ -42,69 +42,45 @@ import subprocess
import struct import struct
import array import array
import six import six
import json
from opengnsys import utils from opengnsys import utils
import netifaces
ip_a_s = None
## make sure /usr/local/bin is in PATH
## we need that to run /usr/local/bin/ip, which uses 'env' and pulls from PATH anyway
def check_path():
path = os.getenv ('PATH', '')
usr_local_bin = '/usr/local/bin'
if usr_local_bin not in path.split (os.pathsep):
os.environ['PATH'] = usr_local_bin + os.pathsep + path
def _getMacAddr(ifname): def _getMacAddr(ifname):
''' '''
Returns the mac address of an interface Returns the mac address of an interface
Mac is returned as unicode utf-8 encoded Mac is returned as unicode utf-8 encoded
''' '''
for interface in ip_a_s: if isinstance(ifname, list):
if interface.get ('ifname') != ifname: continue return dict([(name, _getMacAddr(name)) for name in ifname])
return interface.get ('address') if isinstance(ifname, six.text_type):
return None ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7)
try:
return netifaces.ifaddresses(ifname)[18][0]['addr']
except Exception:
return None
def _getIpAddr(ifname): def _getIpAddr(ifname):
''' '''
Returns the first IP address of an interface Returns the IP address of an interface
IPv4 is preferred over IPv6
IP is returned as unicode utf-8 encoded IP is returned as unicode utf-8 encoded
''' '''
if isinstance(ifname, list):
## loop and return the first IPv4 address found return dict([(name, _getIpAddr(name)) for name in ifname])
for interface in ip_a_s: if isinstance(ifname, six.text_type):
if interface.get ('ifname') != ifname: continue ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7)
for addr_info in interface.get ('addr_info', []): try:
ip_address = addr_info.get ('local') return netifaces.ifaddresses(ifname)[2][0]['addr']
try: except Exception:
ip_address.index ('.') return None
return ip_address
except: pass
## if nothing found, loop again and return the first IP found, which will be an IPv6
for interface in ip_a_s:
if interface.get ('ifname') != ifname: continue
for addr_info in interface.get ('addr_info', []):
return addr_info.get ('local')
return None
def _getInterfaces(): def _getInterfaces():
''' '''
Returns a list of interfaces names Returns a list of interfaces names
''' '''
global ip_a_s return netifaces.interfaces()
check_path()
result = subprocess.run (['/usr/local/bin/ip', '-json', 'address', 'show'], capture_output=True, text=True)
if result.returncode != 0: raise Exception (f'Command "ip" failed with exit code {result.returncode}')
ip_a_s = json.loads (result.stdout)
return [i.get('ifname') for i in ip_a_s]
def _getIpAndMac(ifname): def _getIpAndMac(ifname):
@ -112,6 +88,13 @@ def _getIpAndMac(ifname):
return (ip, mac) return (ip, mac)
def getComputerName():
'''
Returns computer name, with no domain
'''
return socket.gethostname().split('.')[0]
def getNetworkInfo(): def getNetworkInfo():
''' '''
Obtains a list of network interfaces Obtains a list of network interfaces
@ -126,6 +109,10 @@ def getNetworkInfo():
yield utils.Bunch(name=ifname, mac=mac, ip=ip) yield utils.Bunch(name=ifname, mac=mac, ip=ip)
def getDomainName():
return ''
def getMacosVersion(): def getMacosVersion():
return 'macOS {}'.format(platform.mac_ver()[0]) return 'macOS {}'.format(platform.mac_ver()[0])
@ -165,10 +152,87 @@ def logoff():
import threading import threading
threading._DummyThread._Thread__stop = lambda x: 42 threading._DummyThread._Thread__stop = lambda x: 42
# Exec logout using AppleScript # Exec logout using AppleSctipt
subprocess.call('/usr/bin/osascript -e \'tell app "System Events" to «event aevtrlgo»\'', shell=True) subprocess.call('/usr/bin/osascript -e \'tell app "System Events" to «event aevtrlgo»\'', shell=True)
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(): def getCurrentUser():
''' '''
Returns current logged in user Returns current logged in user
@ -180,19 +244,7 @@ def getSessionLanguage():
''' '''
Returns the user's session language Returns the user's session language
''' '''
lang = locale.getdefaultlocale()[0] return locale.getdefaultlocale()[0]
if lang is None:
return 'C'
else:
return lang
def get_session_type():
"""
Minimal implementation of this required function
:return: string
"""
return 'unknown'
def showPopup(title, message): def showPopup(title, message):
@ -209,7 +261,3 @@ def get_etc_path():
Returns etc directory path. Returns etc directory path.
""" """
return os.sep + 'etc' return os.sep + 'etc'
def build_popen_args(script):
return ['/bin/sh', '-c', script]

View File

@ -28,47 +28,41 @@
""" """
@author: Ramón M. Gómez, ramongomez at us dot es @author: Ramón M. Gómez, ramongomez at us dot es
""" """
from __future__ import unicode_literals
from opengnsys.workers import ClientWorker from opengnsys.workers import ClientWorker
from opengnsys import operations from opengnsys import operations
from opengnsys.log import logger from opengnsys.log import logger
from opengnsys.jobmgr import JobMgr from opengnsys.scriptThread import ScriptExecutorThread
class OpenGnSysWorker(ClientWorker): class OpenGnSysWorker(ClientWorker):
name = 'opengnsys' name = 'opengnsys'
jobmgr = JobMgr()
def onActivation(self): @staticmethod
def onActivation():
logger.debug('Activate invoked') logger.debug('Activate invoked')
def onDeactivation(self): @staticmethod
def onDeactivation():
logger.debug('Deactivate invoked') logger.debug('Deactivate invoked')
def process_script(self, json_params): # Processes script execution
script = json_params['code'] @staticmethod
logger.debug('Processing message: script({})'.format(script)) def process_script(json_params):
self.jobmgr.launch_job (script, True) logger.debug('Processed message: script({})'.format(json_params))
thr = ScriptExecutorThread(json_params['code'])
thr.start()
#self.sendServerMessage('script', {'op', 'launched'}) #self.sendServerMessage('script', {'op', 'launched'})
def process_terminatescript(self, json_params): @staticmethod
jobid = json_params['jobid'] def process_logoff(json_params):
logger.debug('Processing terminatescript request, jobid "{}"'.format (jobid))
self.jobmgr.terminate_job (jobid)
def process_preparescripts(self, json_params):
logger.debug('Processing preparescripts request')
st = self.jobmgr.prepare_jobs()
logger.debug('Sending preparescripts to server with data "{}"'.format(st))
self.sendServerMessage('preparescripts', st)
def process_logoff(self, json_params):
logger.debug('Processed message: logoff({})'.format(json_params)) logger.debug('Processed message: logoff({})'.format(json_params))
operations.logoff() operations.logoff()
def process_popup(self, json_params): @staticmethod
def process_popup(json_params):
logger.debug('Processed message: popup({})'.format(json_params)) logger.debug('Processed message: popup({})'.format(json_params))
ret = operations.showPopup(json_params['title'], json_params['message']) ret = operations.showPopup(json_params['title'], json_params['message'])
#self.sendServerMessage('popup', {'op', ret}) #self.sendServerMessage('popup', {'op', ret})

View File

@ -1,4 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2014 Virtual Cable S.L. # Copyright (c) 2014 Virtual Cable S.L.
@ -29,23 +28,23 @@
""" """
@author: Ramón M. Gómez, ramongomez at us dot es @author: Ramón M. Gómez, ramongomez at us dot es
""" """
from __future__ import unicode_literals
import base64
import os import os
import random import random
import shutil import shutil
import signal
import string import string
import subprocess
import threading import threading
import time import time
import urllib.error import urllib
import urllib.parse
import urllib.request
from configparser import NoOptionError from opengnsys import REST
from opengnsys import REST, operations, VERSION from opengnsys import operations
from opengnsys.log import logger from opengnsys.log import logger
from opengnsys.jobmgr import JobMgr
from opengnsys.workers import ServerWorker from opengnsys.workers import ServerWorker
from six.moves.urllib import parse
# Check authorization header decorator # Check authorization header decorator
@ -53,104 +52,145 @@ def check_secret(fnc):
""" """
Decorator to check for received secret key and raise exception if it isn't valid. Decorator to check for received secret key and raise exception if it isn't valid.
""" """
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
try: try:
this, path, get_params, post_params, server = args this, path, get_params, post_params, server = args # @UnusedVariable
# Accept "status" operation with no arguments or any function with Authorization header if this.random == server.headers['Authorization']:
if fnc.__name__ == 'process_status' and not get_params: fnc(*args, **kwargs)
return fnc(*args, **kwargs)
elif this.random == server.headers['Authorization']:
return fnc(*args, **kwargs)
else: else:
raise Exception('Unauthorized operation') raise Exception('Unauthorized operation')
except Exception as e: except Exception as e:
logger.debug (str(e)) logger.error(e)
raise Exception(e) raise Exception(e)
return wrapper return wrapper
# Check if operation is permitted # Error handler decorator.
def execution_level(level): def catch_background_error(fnc):
def check_permitted(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
def check_locked_partition(sync=False):
"""
Decorator to check if a partition is locked
"""
def outer(fnc):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
levels = ['status', 'halt', 'full'] part_id = 'None'
this = args[0]
try: try:
if levels.index(level) <= levels.index(this.exec_level): this, path, get_params, post_params, server = args # @UnusedVariable
return fnc(*args, **kwargs) part_id = post_params['disk'] + post_params['part']
if this.locked.get(part_id, False):
this.locked[part_id] = True
fnc(*args, **kwargs)
else: else:
raise Exception('Unauthorized operation') return 'partition locked'
except Exception as e: except Exception as e:
logger.debug (str(e)) this.locked[part_id] = False
raise Exception(e) return 'error {}'.format(e)
finally:
if sync is True:
this.locked[part_id] = False
logger.debug('Lock status: {} {}'.format(fnc, this.locked))
return wrapper return wrapper
return check_permitted return outer
class OpenGnSysWorker(ServerWorker): class OpenGnSysWorker(ServerWorker):
name = 'opengnsys' # Module name name = 'opengnsys'
interface = None # Bound interface for OpenGnsys interface = None # Bound interface for OpenGnsys
REST = None # REST object REST = None # REST object
user = [] # User sessions logged_in = False # User session flag
session_type = '' # User session type browser = {} # Browser info
commands = [] # Running commands
random = None # Random string for secure connections random = None # Random string for secure connections
length = 32 # Random string length length = 32 # Random string length
exec_level = None # Execution level (permitted operations)
jobmgr = JobMgr()
## pings ogcore def _launch_browser(self, url):
def mon (self): """
n = 0 Launches the Browser with specified URL
while True: :param url: URL to show
time.sleep (1) """
n += 1 logger.debug('Launching browser with URL: {}'.format(url))
if not n % 10: # Trying to kill an old browser
body = { try:
"iph": self.interface.ip, os.kill(self.browser['process'].pid, signal.SIGKILL)
"timestamp": int (time.time()), except OSError:
} logger.warn('Cannot kill the old browser process')
logger.debug (f'about to send ping ({body})') except KeyError:
self.REST.sendMessage ('clients/status/webhook', body) # There is no previous browser
pass
self.browser['url'] = url
self.browser['process'] = subprocess.Popen(['browser', '-qws', url])
def _task_command(self, route, code, op_id, send_config=False):
"""
Task to execute a command and return results to a server URI
:param route: server callback REST route to return results
:param code: code to execute
:param op_id: operation id.
:param send_config: indicate if client will send configuration data after command execution
"""
menu_url = ''
# Show execution tacking log, if OGAgent runs on ogLive
os_type = operations.os_type.lower()
if os_type == 'oglive':
menu_url = self.browser['url']
self._launch_browser('http://localhost/cgi-bin/httpd-log.sh')
# Execute the code
(stat, out, err) = operations.exec_command(code)
# Remove command from the list
for c in self.commands:
if c.getName() == op_id:
self.commands.remove(c)
# Remove the REST API prefix, if needed
if route.startswith(self.REST.endpoint):
route = route[len(self.REST.endpoint):]
# Send back exit status and outputs (base64-encoded)
self.REST.sendMessage(route, {'mac': self.interface.mac, 'ip': self.interface.ip, 'trace': op_id,
'status': stat, 'output': out.encode('base64'), 'error': err.encode('base64')})
# Show latest menu, if OGAgent runs on ogLive
if os_type == 'oglive':
# Send configuration data, if needed
if send_config:
self.REST.sendMessage('ogagent/config', {'mac': self.interface.mac, 'ip': self.interface.ip,
'config': operations.get_configuration()})
self._launch_browser(menu_url)
def onActivation(self): def onActivation(self):
""" """
Sends OGAgent activation notification to OpenGnsys server Sends OGAgent activation notification to OpenGnsys server
""" """
if os.path.exists ('/scripts/oginit'): t = 0
## estamos en oglive, este modulo no debe cargarse
## esta lógica la saco de src/opengnsys/linux/operations.py, donde hay un if similar
raise Exception ('Refusing to load within an ogLive image')
e = None # Error info
t = 0 # Count of time
# Generate random secret to send on activation # Generate random secret to send on activation
self.random = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(self.length)) self.random = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(self.length))
# Ensure cfg has required configuration variables or an exception will be thrown # Ensure cfg has required configuration variables or an exception will be thrown
try: url = self.service.config.get('opengnsys', 'remote')
url = self.service.config.get(self.name, 'remote') if operations.os_type == 'ogLive' and 'oglive' in os.environ:
ca_file = self.service.config.get(self.name, 'ca') # Replacing server IP if it's running on ogLive client
crt_file = self.service.config.get(self.name, 'crt') logger.debug('Activating on ogLive client, new server is {}'.format(os.environ['oglive']))
key_file = self.service.config.get(self.name, 'key') url = parse.urlsplit(url)._replace(netloc=os.environ['oglive']).geturl()
except NoOptionError as e: if not url.endswith(os.path.sep):
logger.error("Configuration error: {}".format(e)) url += os.path.sep
raise e self.REST = REST(url)
self.REST = REST (url, ca_file=ca_file, crt_file=crt_file, key_file=key_file)
# Execution level ('full' by default)
try:
self.exec_level = self.service.config.get(self.name, 'level')
except NoOptionError:
self.exec_level = 'full'
# Get network interfaces until they are active or timeout (5 minutes) # Get network interfaces until they are active or timeout (5 minutes)
for t in range(0, 300): for t in range(0, 300):
try: try:
# Get the first network interface self.interface = list(operations.getNetworkInfo())[0] # Get first network interface
self.interface = list(operations.getNetworkInfo())[0]
except Exception as e: except Exception as e:
# Wait 1 sec. and retry # Wait 1 sec. and retry
logger.warn (e)
time.sleep(1) time.sleep(1)
finally: finally:
# Exit loop if interface is active # Exit loop if interface is active
@ -160,91 +200,113 @@ class OpenGnSysWorker(ServerWorker):
break break
# Raise error after timeout # Raise error after timeout
if not self.interface: if not self.interface:
## UnboundLocalError: cannot access local variable 'e' where it is not associated with a value
raise e raise e
# Loop to send initialization message # Loop to send initialization message
init_retries = 100 for t in range(0, 100):
for t in range(0, init_retries):
try: try:
try: try:
self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip, self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip,
'secret': self.random, 'ostype': operations.os_type, 'secret': self.random, 'ostype': operations.os_type,
'osversion': operations.os_version, 'osversion': operations.os_version})
'agent_version': VERSION})
break break
except Exception as e: except:
logger.warn (str (e))
# Trying to initialize on alternative server, if defined # Trying to initialize on alternative server, if defined
# (used in "exam mode" from the University of Seville) # (used in "exam mode" from the University of Seville)
self.REST = REST(self.service.config.get(self.name, 'altremote'), ca_file=ca_file, crt_file=crt_file, key_file=key_file) self.REST = REST(self.service.config.get('opengnsys', 'altremote'))
self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip, self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip,
'secret': self.random, 'ostype': operations.os_type, 'secret': self.random, 'ostype': operations.os_type,
'osversion': operations.os_version, 'alt_url': True, 'osversion': operations.os_version, 'alt_url': True})
'agent_version': VERSION})
break break
except Exception as e: except:
logger.warn (str (e))
time.sleep(3) time.sleep(3)
# Raise error after timeout # Raise error after timeout
if t < init_retries-1: if 0 < t < 100:
logger.debug('Successful connection after {} tries'.format(t)) logger.debug('Successful connection after {} tries'.format(t))
elif t == init_retries-1: elif t == 100:
raise Exception('Initialization error: Cannot connect to remote server') raise Exception('Initialization error: Cannot connect to remote server')
# Completing OGAgent initialization process
# Delete marking files os_type = operations.os_type.lower()
for f in ['ogboot.me', 'ogboot.firstboot', 'ogboot.secondboot']: if os_type == 'oglive':
try: # # Following code may be separated into a different function to launch the browser while getting the disk
os.remove(os.sep + f) # # configuration
except OSError: message = """
pass <html>
# Copy file "HostsFile.FirstOctetOfIPAddress" to "HostsFile", if it exists <head></head>
# (used in "exam mode" from the University of Seville) <style>
hosts_file = os.path.join(operations.get_etc_path(), 'hosts') #bar { width: 20px; height: 10px; position: relative; background: darkslategrey; }
new_hosts_file = hosts_file + '.' + self.interface.ip.split('.')[0] </style>
if os.path.isfile(new_hosts_file): <body>
shutil.copyfile(new_hosts_file, hosts_file) <h1 style="margin: 5em 0 0 5em; font-size: 250%; color: darkslategrey;">
<span id="opengnsys"><span style="font-weight: lighter;">Open</span>Gnsys 3</div>
threading.Thread (name='monitoring_thread', target=self.mon, daemon=True).start() <div id="bar"></span>
</h1>
logger.debug ('onActivation ok') <script>
var elem = document.getElementById("bar");
var max = document.getElementById("opengnsys").offsetWidth;
var pos = 0;
var inc = true;
var id = setInterval(frame, 5);
function frame() {
if (inc) {
if (pos == max - 20) { inc = false; } else { pos++; }
} else {
if (pos == 0) { inc = true; } else { pos--; }
}
elem.style.left = pos + 'px';
}
</script>
</body>
</html>
"""
f = open('/tmp/init.html', 'w')
f.write(message)
f.close()
# Launching the Browser
self._launch_browser('/tmp/init.html')
config = operations.get_configuration()
self.REST.sendMessage('ogagent/config', {'mac': self.interface.mac, 'ip': self.interface.ip,
'config': config})
else:
# Delete marking files
for f in ['ogboot.me', 'ogboot.firstboot', 'ogboot.secondboot']:
try:
os.remove(os.sep + f)
except OSError:
pass
# Copy file "HostsFile.FirstOctetOfIPAddress" to "HostsFile", if it exists
# (used in "exam mode" from the University of Seville)
hosts_file = os.path.join(operations.get_etc_path(), 'hosts')
new_hosts_file = hosts_file + '.' + self.interface.ip.split('.')[0]
if os.path.isfile(new_hosts_file):
shutil.copyfile(new_hosts_file, hosts_file)
def onDeactivation(self): def onDeactivation(self):
""" """
Sends OGAgent stopping notification to OpenGnsys server Sends OGAgent stopping notification to OpenGnsys server
""" """
now = time.time()
for elem in self.user:
sess_len = now - elem['login_ts']
logger.debug ('Session of logged in user {} took {} seconds'.format (elem['username'], int (sess_len)))
logger.debug('onDeactivation') logger.debug('onDeactivation')
self.REST.sendMessage('ogagent/stopped', {'mac': self.interface.mac, 'ip': self.interface.ip, self.REST.sendMessage('ogagent/stopped', {'mac': self.interface.mac, 'ip': self.interface.ip,
'ostype': operations.os_type, 'osversion': operations.os_version}) 'ostype': operations.os_type, 'osversion': operations.os_version})
def processClientMessage(self, message, data):
logger.debug('Got OpenGnsys message from client: {}, data {}'.format(message, data))
def onLogin(self, data): def onLogin(self, data):
""" """
Sends session login notification to OpenGnsys server Sends session login notification to OpenGnsys server
""" """
user, language, self.session_type = tuple(data.split(',')) user, sep, language = data.partition(',')
logger.debug('Received login for {0} using {2} with language {1}'.format(user, language, self.session_type)) logger.debug('Received login for {} with language {}'.format(user, language))
self.user.append ({'username': user, 'login_ts': time.time() }) self.logged_in = True
self.REST.sendMessage('ogagent/loggedin', {'ip': self.interface.ip, 'user': user, 'language': language, self.REST.sendMessage('ogagent/loggedin', {'ip': self.interface.ip, 'user': user, 'language': language,
'session': self.session_type,
'ostype': operations.os_type, 'osversion': operations.os_version}) 'ostype': operations.os_type, 'osversion': operations.os_version})
def onLogout(self, user): def onLogout(self, user):
""" """
Sends session logout notification to OpenGnsys server Sends session logout notification to OpenGnsys server
""" """
sess_len = 0 logger.debug('Received logout for {}'.format(user))
for elem in self.user: self.logged_in = False
if user != elem['username']: continue
sess_len = time.time() - elem['login_ts']
logger.debug ('Received logout for {}, session length {} seconds'.format (user, int (sess_len)))
try:
self.user.pop()
except IndexError:
pass
self.REST.sendMessage('ogagent/loggedout', {'ip': self.interface.ip, 'user': user}) self.REST.sendMessage('ogagent/loggedout', {'ip': self.interface.ip, 'user': user})
def process_ogclient(self, path, get_params, post_params, server): def process_ogclient(self, path, get_params, post_params, server):
@ -275,34 +337,25 @@ class OpenGnSysWorker(ServerWorker):
raise Exception('Message processor for "{}" not found'.format(path[0])) raise Exception('Message processor for "{}" not found'.format(path[0]))
return operation(path[1:], get_params, post_params) return operation(path[1:], get_params, post_params)
# Warning: the order of the decorators matters
@execution_level('status')
@check_secret
def process_status(self, path, get_params, post_params, server): def process_status(self, path, get_params, post_params, server):
""" """
Returns client status (OS type or execution status) and login status Returns client status (OS type or execution status) and login status
:param path: :param path:
:param get_params: optional parameter "detail" to show extended status :param get_params:
:param post_params: :param post_params:
:param server: :param server:
:return: JSON object {"status": "status_code", "loggedin": boolean, ...} :return: JSON object {"status": "status_code", "loggedin": boolean}
""" """
st = {'linux': 'LNX', 'macos': 'OSX', 'windows': 'WIN'} res = {'loggedin': self.logged_in}
try: try:
# Standard status res['status'] = operations.os_type.lower()
res = {'status': st[operations.os_type.lower()], 'loggedin': len(self.user) > 0,
'session': self.session_type}
# Detailed status
if get_params.get('detail', 'false') == 'true':
res.update({'agent_version': VERSION, 'os_version': operations.os_version, 'sys_load': os.getloadavg()})
if res['loggedin']:
res.update({'sessions': len(self.user), 'current_user': self.user[-1]['username']})
except KeyError: except KeyError:
# Unknown operating system res['status'] = ''
res = {'status': 'UNK'} # Check if OpenGnsys Client is busy
if res['status'] == 'oglive' and len(self.commands) > 0:
res['status'] = 'busy'
return res return res
@execution_level('halt')
@check_secret @check_secret
def process_reboot(self, path, get_params, post_params, server): def process_reboot(self, path, get_params, post_params, server):
""" """
@ -318,10 +371,10 @@ class OpenGnSysWorker(ServerWorker):
# Rebooting thread # Rebooting thread
def rebt(): def rebt():
operations.reboot() operations.reboot()
threading.Thread(target=rebt).start() threading.Thread(target=rebt).start()
return {'op': 'launched'} return {'op': 'launched'}
@execution_level('halt')
@check_secret @check_secret
def process_poweroff(self, path, get_params, post_params, server): def process_poweroff(self, path, get_params, post_params, server):
""" """
@ -338,88 +391,189 @@ class OpenGnSysWorker(ServerWorker):
def pwoff(): def pwoff():
time.sleep(2) time.sleep(2)
operations.poweroff() operations.poweroff()
threading.Thread(target=pwoff).start() threading.Thread(target=pwoff).start()
return {'op': 'launched'} return {'op': 'launched'}
@execution_level('full')
@check_secret @check_secret
def process_script(self, path, get_params, post_params, server): def process_script(self, path, get_params, post_params, server):
""" """
Processes an script execution (script should be encoded in base64) Processes an script execution (script should be encoded in base64)
:param path: :param path:
:param get_params: :param get_params:
:param post_params: JSON object {"script": "commands"} :param post_params: object with format:
:param server: authorization header id: operation id.
:return: JSON object {"op": "launched"} script: command code
redirect_url: callback REST route
send_config: flag to send client's configuration after command execution (optional)
:param server: headers data
:rtype: JSON object with launching status
""" """
logger.debug('Processing script request') logger.debug('Processing script operation with params: {}'.format(post_params))
# Decoding script # Processing data
script = urllib.parse.unquote(base64.b64decode(post_params.get('script')).decode('utf-8')) try:
logger.debug('received script "{}"'.format(script)) # Decoding script (Windows scripts need a subprocess call per line)
script = urllib.unquote(post_params.get('script').decode('base64')).decode('utf8')
if operations.os_type == 'Windows':
script = 'import subprocess; {0}'.format(
';'.join(['subprocess.check_output({0},shell=True)'.format(repr(c)) for c in script.split('\n')]))
else:
script = 'import subprocess; subprocess.check_output("""{0}""",shell=True)'.format(script)
op_id = post_params.get('id')
route = post_params.get('redirect_uri')
send_config = (post_params.get('send_config', 'false') == 'true')
# Checking if the thread id. exists
for c in self.commands:
if c.getName() == str(op_id):
raise Exception('Task id. already exists: {}'.format(op_id))
if post_params.get('client', 'false') == 'false':
# Launching a new thread
thr = threading.Thread(name=op_id, target=self._task_command, args=(route, script, op_id, send_config))
thr.start()
self.commands.append(thr)
else:
# Executing as normal user
self.sendClientMessage('script', {'code': script})
except Exception as e:
logger.error('Got exception {}'.format(e))
return {'error': e}
return {'op': 'launched'}
if post_params.get('client', 'false') == 'false':
jobid = self.jobmgr.launch_job (script, False)
return {'op': 'launched', 'jobid': jobid}
else: ## post_params.get('client') is not 'false'
## send script as-is
self.sendClientMessage('script', {'code': script})
#return {'op': 'launched', 'jobid': jobid} ## TODO obtain jobid generated at the client (can it be done?)
return {'op': 'launched'}
@execution_level('full')
@check_secret
def process_terminatescript(self, path, get_params, post_params, server):
jobid = post_params.get('jobid', None)
logger.debug('Processing terminate_script request, jobid "{}"'.format (jobid))
if jobid is None:
return {}
self.sendClientMessage('terminatescript', {'jobid': jobid})
self.jobmgr.terminate_job (jobid)
return {}
@execution_level('full')
@check_secret
def process_preparescripts(self, path, get_params, post_params, server):
logger.debug('Processing preparescripts request')
self.st = self.jobmgr.prepare_jobs()
logger.debug('Sending preparescripts to client')
self.sendClientMessage('preparescripts', None)
return {}
def process_client_preparescripts(self, params):
logger.debug('Processing preparescripts message from client')
for p in params:
#logger.debug ('p "{}"'.format(p))
self.st.append (p)
@execution_level('full')
@check_secret
def process_getscripts(self, path, get_params, post_params, server):
logger.debug('Processing getscripts request')
return self.st
@execution_level('full')
@check_secret @check_secret
def process_logoff(self, path, get_params, post_params, server): def process_logoff(self, path, get_params, post_params, server):
""" """
Closes user session Closes user session
""" """
logger.debug('Received logoff operation') logger.debug('Received logoff operation')
# Sending log off message to OGAgent client # Send log off message to OGAgent client
self.sendClientMessage('logoff', {}) self.sendClientMessage('logoff', {})
return {'op': 'sent to client'} return {'op': 'sent to client'}
@execution_level('full')
@check_secret @check_secret
def process_popup(self, path, get_params, post_params, server): def process_popup(self, path, get_params, post_params, server):
""" """
Shows a message popup on the user's session Shows a message popup on the user's session
""" """
logger.debug('Received message operation') logger.debug('Received message operation')
# Sending popup message to OGAgent client # Send popup message to OGAgent client
self.sendClientMessage('popup', post_params) self.sendClientMessage('popup', post_params)
return {'op': 'launched'} return {'op': 'launched'}
def process_client_popup(self, params): def process_client_popup(self, params):
self.REST.sendMessage('popup_done', params) self.REST.sendMessage('popup_done', params)
@check_secret
def process_config(self, path, get_params, post_params, server):
"""
Returns client configuration
:param path:
:param get_params:
:param post_params:
:param server:
:return: object
"""
serial_no = '' # Serial number
storage = [] # Storage configuration
warnings = 0 # Number of warnings
logger.debug('Received getconfig operation')
# Processing data
for row in operations.get_configuration().split(';'):
cols = row.split(':')
if len(cols) == 1:
if cols[0] != '':
# Serial number
serial_no = cols[0]
else:
# Skip blank rows
pass
elif len(cols) == 7:
disk, part_no, part_type, fs, op_sys, size, usage = cols
try:
if int(part_no) == 0:
# Disk information
storage.append({'disk': int(disk), 'parttable': int(part_type), 'size': int(size)})
else:
# Partition information
storage.append({'disk': int(disk), 'partition': int(part_no), 'parttype': part_type,
'filesystem': fs, 'operatingsystem': op_sys, 'size': int(size),
'usage': int(usage)})
except ValueError:
logger.warn('Configuration parameter error: {}'.format(cols))
warnings += 1
else:
# Logging warnings
logger.warn('Configuration data error: {}'.format(cols))
warnings += 1
# Returning configuration data and count of warnings
return {'serial': serial_no, 'storage': storage, 'warnings': warnings}
@check_secret
def process_execinfo(self, path, get_params, post_params, server):
"""
Returns running commands information
:param path:
:param get_params:
:param post_params:
:param server:
:return: object
"""
data = []
logger.debug('Received execinfo operation')
# Returning the arguments of all running threads
for c in self.commands:
if c.is_alive():
data.append(c.__dict__['_Thread__args'])
return data
@check_secret
def process_stopcmd(self, path, get_params, post_params, server):
"""
Stops a running process identified by its trace id.
:param path:
:param get_params:
:param post_params: JSON object {"trace": trace_id}
:param server: authorization header
:return: JSON object: {"stopped": trace_id}
"""
logger.debug('Received stopcmd operation with params {}:'.format(post_params))
# Find operation id. and stop the thread
op_id = post_params.get('trace')
for c in self.commands:
if c.is_alive() and c.getName() == str(op_id):
c._Thread__stop()
return {"stopped": op_id}
return {}
@check_secret
def process_hardware(self, path, get_params, post_params, server):
"""
Returns client's hardware profile
:param path:
:param get_params:
:param post_params:
:param server:
:return: array of component data objects
"""
data = []
logger.debug('Received hardware operation')
# Processing data
try:
for comp in operations.get_hardware():
data.append({'component': comp.split('=')[0], 'value': comp.split('=')[1]})
except:
pass
# Return list of hardware components
return data
@check_secret
def process_software(self, path, get_params, post_params, server):
"""
Returns software profile installed on an operating system
:param path:
:param get_params:
:param post_params:
:param server:
:return:
"""
logger.debug('Received software operation with params: {}'.format(post_params))
return operations.get_software(post_params.get('disk'), post_params.get('part'))

View File

@ -1,875 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014 Virtual Cable S.L.
# Copyright (c) 2024-2025 Qindel Formación y Servicios 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: Ramón M. Gómez, ramongomez at us dot es
@author: Natalia Serrano, nserrano at qindel dot com
"""
import base64
import os
import signal
import string
import random
import subprocess
from pathlib import Path
from urllib.parse import unquote
from opengnsys import VERSION
from opengnsys.log import logger
from opengnsys.workers import ogLiveWorker
# Check authorization header decorator
def check_secret (fnc):
"""
Decorator to check for received secret key and raise exception if it isn't valid.
"""
def wrapper (*args, **kwargs):
try:
this, path, get_params, post_params, server = args
if not server: ## this happens on startup, eg. onActivation->autoexecCliente->ejecutaArchivo->popup->check_secret
return fnc (*args, **kwargs)
if this.random == server.headers['Authorization']:
return fnc (*args, **kwargs)
else:
raise Exception ('Unauthorized operation')
except Exception as e:
logger.error (str (e))
raise Exception (e)
return wrapper
# Check if operation is permitted
def execution_level(level):
def check_permitted(fnc):
def wrapper(*args, **kwargs):
levels = ['status', 'halt', 'full']
this = args[0]
try:
if levels.index(level) <= levels.index(this.exec_level):
return fnc(*args, **kwargs)
else:
raise Exception('Unauthorized operation')
except Exception as e:
logger.debug (str(e))
raise Exception(e)
return wrapper
return check_permitted
class ogAdmClientWorker (ogLiveWorker):
name = 'ogAdmClient' # Module name
REST = None # REST object
def onDeactivation (self):
"""
Sends OGAgent stopping notification to OpenGnsys server
"""
logger.debug ('onDeactivation')
self.REST.sendMessage ('ogagent/stopped', {'mac': self.mac, 'ip': self.IPlocal, 'idcentro': self.idcentro, 'idaula': self.idaula,
'idordenador': self.idordenador, 'nombreordenador': self.nombreordenador})
def InventariandoSoftware (self, dsk, par, nfn):
sft_src = f'/tmp/CSft-{self.IPlocal}-{par}'
try:
self.interfaceAdmin (nfn, [dsk, par, sft_src])
herror = 0
except:
herror = 1
if herror:
logger.warning ('Error al ejecutar el comando')
b64 = ''
self.muestraMensaje (20)
else:
if not os.path.exists (sft_src):
raise Exception (f'interfaceAdmin({nfn}) returned success but did not create file ({sft_src})')
sft_src_contents = Path (sft_src).read_bytes()
b64 = base64.b64encode (sft_src_contents).decode ('utf-8')
self.muestraMensaje (19)
cmd = {
'nfn': 'RESPUESTA_InventarioSoftware',
'dsk': dsk, ## not in the original C code, around ogAdmClient.c:1944
'par': par,
'contents': b64,
}
return self.respuestaEjecucionComando (cmd, herror, 0)
def ejecutaArchivo (self,fn):
logger.debug ('fn ({})'.format (fn))
## in the "file" there's not just some bash, but a sequence of parameters such as "nfn=Function\rparam1=foo\rparam2=bar"
buffer = subprocess.run (['cat', fn], capture_output=True).stdout.strip().decode ('utf-8')
logger.debug ('buffer ({})'.format (buffer.replace ('\r', '\\r'))) ## change \r so as not to mess with the log
if buffer:
for l in buffer.split ('@'):
if not len (l): continue
logger.debug ('line ({})'.format (l.replace ('\r', '\\r'))) ## change \r so as not to mess with the log
## at this point, an option would be fire up a curl to localhost, but we can also parse the params and locally call the desired function:
post_params = {}
for param in l.split ("\r"):
k, v = param.split ('=')
post_params[k] = v
logger.debug ('post_params "{}"'.format (post_params))
func_name = post_params.pop ('nfn', None)
if func_name is None:
logger.error ('Ha ocurrido algún problema al procesar la trama recibida')
break
func = getattr (self, 'process_' + func_name)
## func is already a ref to self.func, so we don't have to call self.func(...) or func(self, ...)
logger.debug ('calling function "{}" with post_params "{}"'.format (func_name, post_params))
output = func ([], {}, post_params, None)
logger.debug ('output "{}"'.format (output))
if not output:
logger.error ('Ha ocurrido algún problema al procesar la trama recibida')
break
def inclusionCliente (self):
cfg = self.LeeConfiguracion()
if not cfg:
logger.warning ('No se ha podido recuperar la configuración de las particiones del disco')
logger.warning ('Ha ocurrido algún problema en el proceso de inclusión del cliente')
logger.error ('LeeConfiguracion() failed')
return False
res = self.enviaMensajeServidor ('InclusionCliente', { 'cfg': self.cfg2obj (cfg), 'secret': self.random, 'agent_version': VERSION })
logger.debug ('res ({})'.format (res))
## RESPUESTA_InclusionCliente
if (type (res) is not dict or 0 == res['res']) :
logger.error ('Ha ocurrido algún problema en el proceso de inclusión del cliente')
return False
if (not res['ido'] or not res['npc']):
logger.error ('Se han recibido parámetros con valores no válidos')
return False
self.idordenador = res['ido'] ## Identificador del ordenador
self.nombreordenador = res['npc'] ## Nombre del ordenador
self.cache = res['che'] ## Tamaño de la caché reservada al cliente
self.idproautoexec = res['exe'] ## Procedimento de inicio (Autoexec)
self.idcentro = res['idc'] ## Identificador de la Unidad Organizativa
self.idaula = res['ida'] ## Identificador del aula
return True
def cuestionCache (self):
return True ## ogAdmClient.c:425
def autoexecCliente (self):
res = self.enviaMensajeServidor ('AutoexecCliente', { 'exe': self.idproautoexec })
logger.debug ('res ({})'.format (res))
if (type (res) is not dict):
logger.error ('Ha ocurrido algún problema al enviar una petición de comandos o tareas pendientes al Servidor de Administración')
logger.error ('Ha ocurrido algún problema al recibir una petición de comandos o tareas pendientes desde el Servidor de Administración')
return False
## RESPUESTA_AutoexecCliente
if (type (res) is not dict or 0 == res['res']) :
logger.error ('Ha ocurrido algún problema al procesar la trama recibida')
return False
logger.info (res)
res = self.enviaMensajeServidor ('enviaArchivo', { 'nfl': res['nfl'] })
if (type (res) is not dict):
logger.error ('Ha ocurrido algún problema al enviar una petición de comandos o tareas pendientes al Servidor de Administración')
logger.error ('Ha ocurrido algún problema al recibir un archivo por la red')
return False
logger.debug (f'res ({res})')
fileautoexec = '/tmp/_autoexec_{}'.format (self.IPlocal)
logger.debug ('fileautoexec ({})'.format (fileautoexec))
with open (fileautoexec, 'w') as fd:
fd.write (base64.b64decode (res['contents']).decode ('utf-8'))
self.ejecutaArchivo (fileautoexec)
return True
def comandosPendientes (self):
while (True):
res = self.enviaMensajeServidor ('ComandosPendientes') ## receives just one command
if (type (res) is not dict):
logger.error ('Ha ocurrido algún problema al enviar una petición de comandos o tareas pendientes al Servidor de Administración')
logger.error ('Ha ocurrido algún problema al recibir una petición de comandos o tareas pendientes desde el Servidor de Administración')
return False
logger.info (res)
if ('NoComandosPtes' == res['nfn']):
break
## TODO manage the rest of cases... we might have to do something similar to ejecutaArchivo
#if (!gestionaTrama (ptrTrama)){ // Análisis de la trama
# logger.error ('Ha ocurrido algún problema al procesar la trama recibida')
# return False
#}
## ATM let's just return false to avoid a possible infinite loop
return False
return True
def procesaComandos (self):
res = self.enviaMensajeServidor ('DisponibilidadComandos', { 'tpc': 'OPG' }) ## Activar disponibilidad
logger.debug ('res ({})'.format (res))
if (type (res) is not dict):
logger.error ('Ha ocurrido algún problema al enviar una petición de comandos interactivos al Servidor de Administración')
return False
logger.info ('Disponibilidad de comandos activada') ## Disponibilidad de cliente activada
## we now return true and the outer agent code gets to wait for requests from outside
## TODO thing is, ogAdmClient always calls comandosPendientes() after every received request. How do we do that here?
#
#ptrTrama=recibeMensaje (&socket_c);
#if (!ptrTrama){
# errorLog (modulo,46,FALSE); 'Ha ocurrido algún problema al recibir un comando interactivo desde el Servidor de Administración'
# return;
#}
#close (socket_c);
#if (!gestionaTrama (ptrTrama)){ // Análisis de la trama
# errorLog (modulo,39,FALSE); 'Ha ocurrido algún problema al procesar la trama recibida'
# return;
#}
#if (!comandosPendientes (ptrTrama)){
# errorLog (modulo,42,FALSE); 'Ha ocurrido algún problema al enviar una petición de comandos o tareas pendientes al Servidor de Administración'
#}
def onActivation (self):
super().onActivation()
self.exec_level = 'full'
self.random = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(32))
logger.info ('Inicio de sesion')
logger.info ('Abriendo sesión en el servidor de Administración')
if (not self.inclusionCliente()):
raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo')
logger.info ('Cliente iniciado')
logger.info ('Procesando caché')
if not self.cuestionCache():
raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo')
if self.idproautoexec > 0:
logger.info ('Ejecución de archivo Autoexec')
if not self.autoexecCliente():
raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo')
logger.info ('Procesa comandos pendientes')
if not self.comandosPendientes():
raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo')
logger.info ('Acciones pendientes procesadas')
self.muestraMenu()
self.procesaComandos()
logger.info ('onActivation ok')
def do_CrearImagen (self, post_params):
for k in ['dsk', 'par', 'cpt', 'idi', 'nci', 'ipr', 'nfn', 'ids']:
if k not in post_params:
logger.error (f'required parameter ({k}) not in POST params')
return {}
dsk = post_params['dsk'] ## Disco
par = post_params['par'] ## Número de partición
cpt = post_params['cpt'] ## Código de la partición
idi = post_params['idi'] ## Identificador de la imagen
nci = post_params['nci'] ## Nombre canónico de la imagen
ipr = post_params['ipr'] ## Ip del repositorio
nfn = post_params['nfn']
ids = post_params['ids']
self.muestraMensaje (7)
try:
res = self.InventariandoSoftware (dsk, par, 'InventarioSoftware') ## Crea inventario Software previamente
except:
logger.warning ('Error al ejecutar el comando')
return {}
if res['contents']:
self.muestraMensaje (2)
inv_sft = res['contents']
try:
self.interfaceAdmin (nfn, [dsk, par, nci, ipr])
self.muestraMensaje (9)
herror = 0
except:
logger.warning ('Error al ejecutar el comando')
self.muestraMensaje (10)
herror = 1
else:
logger.warning ('Error al ejecutar el comando')
herror = 1
inv_sft = ''
self.muestraMenu()
cmd = {
'nfn': 'RESPUESTA_CrearImagen',
'idi': idi, ## Identificador de la imagen
'dsk': dsk, ## Número de disco
'par': par, ## Número de partición de donde se creó
'cpt': cpt, ## Tipo o código de partición
'ipr': ipr, ## Ip del repositorio donde se alojó
'inv_sft': inv_sft,
}
return self.respuestaEjecucionComando (cmd, herror, ids)
def do_RestaurarImagen (self, post_params):
for k in ['dsk', 'par', 'idi', 'ipr', 'nci', 'ifs', 'ptc', 'nfn', 'ids']:
if k not in post_params:
logger.error (f'required parameter ({k}) not in POST params')
return {}
dsk = post_params['dsk']
par = post_params['par']
idi = post_params['idi']
ipr = post_params['ipr']
nci = post_params['nci']
ifs = post_params['ifs']
ptc = post_params['ptc'] ## Protocolo de clonación: Unicast, Multicast, Torrent
nfn = post_params['nfn']
ids = post_params['ids']
self.muestraMensaje (3)
try:
## the ptc.split() is useless right now, since interfaceAdmin() does ' '.join(params) in order to spawn a shell
## however we're going to need it in the future (when everything gets translated into python), plus it's harmless now. So let's do it
#self.interfaceAdmin (nfn, [dsk, par, nci, ipr, ptc])
self.interfaceAdmin (nfn, [dsk, par, nci, ipr] + ptc.split())
self.muestraMensaje (11)
herror = 0
except:
logger.warning ('Error al ejecutar el comando')
self.muestraMensaje (12)
herror = 1
cfg = self.LeeConfiguracion()
if not cfg:
logger.warning ('No se ha podido recuperar la configuración de las particiones del disco')
self.muestraMenu()
cmd = {
'nfn': 'RESPUESTA_RestaurarImagen',
'idi': idi, ## Identificador de la imagen
'dsk': dsk, ## Número de disco
'par': par, ## Número de partición
'ifs': ifs, ## Identificador del perfil software
'cfg': self.cfg2obj(cfg), ## Configuración de discos
}
return self.respuestaEjecucionComando (cmd, herror, ids)
def do_Configurar (self, post_params):
for k in ['nfn', 'dsk', 'cfg', 'ids']:
if k not in post_params:
logger.error (f'required parameter ({k}) not in POST params')
return {}
nfn = post_params['nfn']
dsk = post_params['dsk']
cfg = post_params['cfg']
ids = post_params['ids']
self.muestraMensaje (4)
params = []
disk_info = cfg.pop (0)
logger.debug (f'disk_info ({disk_info})')
for k in ['dis']:
params.append (f'{k}={disk_info[k]}')
disk_info_str = '*'.join (params)
partitions = []
for entry in cfg:
logger.debug (f'entry ({entry})')
params = []
for k in ['par', 'cpt', 'sfi', 'tam', 'ope']:
params.append (f'{k}={entry[k]}')
partitions.append ('*'.join (params))
part_info_str = '%'.join (partitions)
cfg_str = f'{disk_info_str}!{part_info_str}%'
try:
self.interfaceAdmin (nfn, ['ignored', cfg_str])
self.muestraMensaje (14)
herror = 0
except:
logger.warning ('Error al ejecutar el comando')
self.muestraMensaje (13)
herror = 1
cfg = self.LeeConfiguracion()
if not cfg:
logger.warning ('No se ha podido recuperar la configuración de las particiones del disco')
return {}
cmd = {
'nfn': 'RESPUESTA_Configurar',
'cfg': self.cfg2obj (cfg),
}
self.muestraMenu()
return self.respuestaEjecucionComando (cmd, herror, ids)
def do_InventarioHardware (self, post_params):
for k in ['nfn', 'ids']:
if k not in post_params:
logger.error (f'required parameter ({k}) not in POST params')
return {}
nfn = post_params['nfn']
ids = post_params['ids']
self.muestraMensaje (6)
hrdsrc = f'/tmp/Chrd-{self.IPlocal}' ## Nombre que tendra el archivo de inventario
hrddst = f'/tmp/Shrd-{self.IPlocal}' ## Nombre que tendra el archivo en el Servidor
try:
self.interfaceAdmin (nfn, [hrdsrc])
hrdsrc_contents = Path (hrdsrc).read_bytes()
logger.debug (f'hrdsrc_contents 1 ({hrdsrc_contents})')
herror = 0
except:
logger.warning ('Error al ejecutar el comando')
self.muestraMensaje (18)
herror = 1
if herror:
hrddst = ''
else:
logger.debug (f'hrdsrc_contents 2 ({hrdsrc_contents})')
## Envía fichero de inventario al servidor
res = self.enviaMensajeServidor ('recibeArchivo', { 'nfl': hrddst, 'contents': base64.b64encode (hrdsrc_contents).decode ('utf-8') })
logger.debug (res)
if not res:
logger.error ('Ha ocurrido algún problema al enviar un archivo por la red')
herror = 12 ## Error de envío de fichero por la red
self.muestraMensaje (17)
## Envia respuesta de ejecución de la función de interface
cmd = {
'nfn': 'RESPUESTA_InventarioHardware',
'hrd': hrddst,
}
self.muestraMenu()
return self.respuestaEjecucionComando (cmd, herror, ids)
def do_InventarioSoftware (self, post_params):
for k in ['nfn', 'dsk', 'par', 'ids']:
if k not in post_params:
logger.error (f'required parameter ({k}) not in POST params')
return {}
nfn = post_params['nfn']
dsk = post_params['dsk']
par = post_params['par']
ids = post_params['ids']
self.muestraMensaje (7)
try:
cmd = self.InventariandoSoftware (dsk, par, 'InventarioSoftware')
herror = 0
except:
logger.warning ('Error al ejecutar el comando')
cmd = { 'nfn': 'RESPUESTA_InventarioSoftware' }
herror = 1
self.muestraMenu()
return self.respuestaEjecucionComando (cmd, herror, ids)
def do_Actualizar (self, post_params):
self.muestraMensaje (1)
#if !comandosPendientes: error 84 'Ha ocurrido algún problema al reiniciar la sesión del cliente'
cfg = self.LeeConfiguracion()
if not cfg:
logger.warning ('No se ha podido recuperar la configuración de las particiones del disco')
logger.error ('LeeConfiguracion() failed')
return {}
cmd = {
'nfn': 'RESPUESTA_Actualizar',
'cfg': self.cfg2obj (cfg),
}
self.muestraMenu()
return self.respuestaEjecucionComando (cmd, 0)
def do_Comando (self, post_params):
for k in ['nfn', 'ids']:
if k not in post_params:
logger.error (f'required parameter ({k}) not in POST params')
return {}
nfn = post_params['nfn']
ids = post_params['ids']
try:
self.interfaceAdmin (nfn)
herror = 0
except:
logger.warning ('Error al ejecutar el comando')
herror = 1
cmd = {
'nfn': 'RESPUESTA_Comando',
}
return self.respuestaEjecucionComando (cmd, herror, ids)
def do_ConsolaRemota (self, post_params):
for k in ['nfn', 'scp']:
if k not in post_params:
logger.error (f'required parameter ({k}) not in POST params')
return {}
nfn = post_params['nfn']
scp = base64.b64decode (unquote (post_params['scp'])).decode ('utf-8')
filescript = f'/tmp/_script_{self.IPlocal}'
ecosrc = f'/tmp/_econsola_{self.IPlocal}'
ecodst = f'/tmp/_Seconsola_{self.IPlocal}' ## Nombre que tendra el archivo en el Servidor
with open (filescript, 'w') as fd:
fd.write (scp)
try:
self.interfaceAdmin (nfn, [filescript, ecosrc])
ecosrc_contents = Path (ecosrc).read_bytes()
except:
logger.error ('Error al ejecutar el comando')
return {}
logger.debug ('sending recibeArchivo to server')
res = self.enviaMensajeServidor ('recibeArchivo', { 'nfl': ecodst, 'contents': base64.b64encode (ecosrc_contents).decode ('utf-8') })
logger.debug (res)
if not res:
logger.error ('Ha ocurrido algún problema al enviar un archivo por la red')
return {}
def do_Apagar (self, post_params):
for k in ['nfn', 'ids']:
if k not in post_params:
logger.error (f'required parameter ({k}) not in POST params')
return {}
nfn = post_params['nfn']
ids = post_params['ids']
try:
self.interfaceAdmin (nfn)
herror = 0
except:
logger.warning ('Error al ejecutar el comando')
herror = 1
cmd = {
'nfn': 'RESPUESTA_Apagar',
}
return self.respuestaEjecucionComando (cmd, herror, ids)
def do_Reiniciar (self, post_params):
for k in ['nfn', 'ids']:
if k not in post_params:
logger.error (f'required parameter ({k}) not in POST params')
return {}
nfn = post_params['nfn']
ids = post_params['ids']
try:
self.interfaceAdmin (nfn)
herror = 0
except:
logger.warning ('Error al ejecutar el comando')
herror = 1
cmd = {
'nfn': 'RESPUESTA_Reiniciar',
}
return self.respuestaEjecucionComando (cmd, herror, ids)
def do_IniciarSesion (self, post_params):
for k in ['nfn', 'dsk', 'par', 'ids']:
if k not in post_params:
logger.error (f'required parameter ({k}) not in POST params')
return {}
nfn = post_params['nfn']
dsk = post_params['dsk']
par = post_params['par']
ids = post_params['ids']
try:
self.interfaceAdmin (nfn, [dsk, par])
herror = 0
except:
logger.warning ('Error al ejecutar el comando')
herror = 1
cmd = {
'nfn': 'RESPUESTA_IniciarSesion',
}
return self.respuestaEjecucionComando (cmd, herror, ids)
def do_EjecutarScript (self, post_params):
for k in ['nfn', 'scp', 'ids']:
if k not in post_params:
logger.error (f'required parameter ({k}) not in POST params')
return {}
nfn = post_params['nfn']
scp = base64.b64decode (unquote (post_params['scp'])).decode ('utf-8')
ids = post_params['ids']
self.muestraMensaje (8)
filescript = f'/tmp/_script_{self.IPlocal}' ## Nombre del archivo de script
with open (filescript, 'w') as fd:
fd.write (scp)
try:
self.interfaceAdmin (nfn, [filescript])
self.muestraMensaje (22)
herror = 0
except:
logger.warning ('Error al ejecutar el comando')
self.muestraMensaje (21)
herror = 1
## Toma configuración de particiones
cfg = self.LeeConfiguracion()
if not cfg:
logger.warning ('No se ha podido recuperar la configuración de las particiones del disco')
herror = 36
#herror=ejecutarCodigoBash(scp); ## ogAdmClient.c:2004
cmd = {
'nfn': 'RESPUESTA_EjecutarScript',
'cfg': self.cfg2obj (cfg),
}
self.muestraMenu()
return self.respuestaEjecucionComando (cmd, herror, ids)
@execution_level('status')
def process_status (self, path, get_params, post_params, server):
logger.debug ('in process_status, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
full_config = 'full-config' in post_params and post_params['full-config']
thr_status = {}
for k in self.thread_list:
thr_status[k] = {
'running': self.thread_list[k]['running'],
'result': self.thread_list[k]['result'],
}
ret = {
'nfn': 'RESPUESTA_status',
'mac': self.mac,
'st': 'OGL',
'ip': self.IPlocal,
'threads': thr_status,
}
if full_config:
cfg = self.LeeConfiguracion()
ret['cfg'] = self.cfg2obj (cfg)
return ret
@check_secret
def process_popup (self, path, get_params, post_params, server):
logger.debug ('in process_popup, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
logger.debug ('type(post_params) "{}"'.format (type (post_params)))
## in process_popup, should not happen, path "[]" get_params "{}" post_params "{'title': 'mi titulo', 'message': 'mi mensaje'}" server "<opengnsys.httpserver.HTTPServerHandler object at 0x7fa788cb8fa0>"
## type(post_params) "<class 'dict'>"
return {'debug':'test'}
@execution_level('full')
@check_secret
def process_Actualizar (self, path, get_params, post_params, server):
logger.debug ('in process_Actualizar, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
return self._long_running_job ('Actualizar', self.do_Actualizar, args=(post_params,))
@execution_level('full')
@check_secret
def process_Purgar (self, path, get_params, post_params, server):
logger.debug ('in process_Purgar, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
os.kill (os.getpid(), signal.SIGTERM)
return {}
#exit (0) ## ogAdmClient.c:905
@execution_level('full')
@check_secret
def process_Comando (self, path, get_params, post_params, server):
logger.debug ('in process_Comando, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
return self._long_running_job ('Comando', self.do_Comando, args=(post_params,))
def process_Sondeo (self, path, get_params, post_params, server):
logger.debug ('in process_Sondeo, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
return {} ## ogAdmClient.c:920
@execution_level('full')
@check_secret
def process_ConsolaRemota (self, path, get_params, post_params, server):
logger.debug ('in process_ConsolaRemota, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
return self._long_running_job ('ConsolaRemota', self.do_ConsolaRemota, args=(post_params,))
@execution_level('full')
@check_secret
def process_Arrancar (self, path, get_params, post_params, server):
logger.debug ('in process_Arrancar, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
for k in ['ids']:
if k not in post_params:
logger.error (f'required parameter ({k}) not in POST params')
return {}
ids = post_params['ids']
cmd = {
'nfn': 'RESPUESTA_Arrancar',
'tpc': 'OPG',
}
return self.respuestaEjecucionComando (cmd, 0, ids)
@execution_level('halt')
@check_secret
def process_Apagar (self, path, get_params, post_params, server):
logger.debug ('in process_Apagar, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
return self._long_running_job ('Apagar', self.do_Apagar, args=(post_params,))
@execution_level('halt')
@check_secret
def process_Reiniciar (self, path, get_params, post_params, server):
logger.debug ('in process_Reiniciar, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
return self._long_running_job ('Reiniciar', self.do_Reiniciar, args=(post_params,))
@execution_level('full')
@check_secret
def process_IniciarSesion (self, path, get_params, post_params, server):
logger.debug ('in process_IniciarSesion, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
return self._long_running_job ('IniciarSesion', self.do_IniciarSesion, args=(post_params,))
@execution_level('full')
@check_secret
def process_EjecutarScript (self, path, get_params, post_params, server):
logger.debug ('in process_EjecutarScript, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
return self._long_running_job ('EjecutarScript', self.do_EjecutarScript, args=(post_params,))
@execution_level('full')
@check_secret
def process_EjecutaComandosPendientes (self, path, get_params, post_params, server):
logger.debug ('in process_EjecutaComandosPendientes, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
return {'true':'true'} ## ogAdmClient.c:2138
@execution_level('full')
@check_secret
def process_CrearImagen (self, path, get_params, post_params, server):
logger.debug ('in process_CrearImagen, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
logger.debug ('type(post_params) "{}"'.format (type (post_params)))
return self._long_running_job ('CrearImagen', self.do_CrearImagen, args=(post_params,))
#def process_CrearImagenBasica (self, path, get_params, post_params, server):
# logger.debug ('in process_CrearImagenBasica, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
# logger.warning ('this method has been removed')
# raise Exception ({ '_httpcode': 404, '_msg': 'This method has been removed' })
#def process_CrearSoftIncremental (self, path, get_params, post_params, server):
# logger.debug ('in process_CrearSoftIncremental, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
# logger.warning ('this method has been removed')
# raise Exception ({ '_httpcode': 404, '_msg': 'This method has been removed' })
@execution_level('full')
@check_secret
def process_RestaurarImagen (self, path, get_params, post_params, server):
logger.debug ('in process_RestaurarImagen, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
logger.debug ('type(post_params) "{}"'.format (type (post_params)))
return self._long_running_job ('RestaurarImagen', self.do_RestaurarImagen, args=(post_params,))
#def process_RestaurarImagenBasica (self, path, get_params, post_params, server):
# logger.debug ('in process_RestaurarImagenBasica, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
# logger.warning ('this method has been removed')
# raise Exception ({ '_httpcode': 404, '_msg': 'This method has been removed' })
#def process_RestaurarSoftIncremental (self, path, get_params, post_params, server):
# logger.warning ('in process_RestaurarSoftIncremental')
# logger.debug ('in process_RestaurarSoftIncremental, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
# logger.warning ('this method has been removed')
# raise Exception ({ '_httpcode': 404, '_msg': 'This method has been removed' })
## una partición + cache en disco de 30 Gb:
@execution_level('full')
@check_secret
def process_Configurar (self, path, get_params, post_params, server):
logger.debug ('in process_Configurar, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
return self._long_running_job ('Configurar', self.do_Configurar, args=(post_params,))
@execution_level('full')
@check_secret
def process_InventarioHardware (self, path, get_params, post_params, server):
logger.debug ('in process_InventarioHardware, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
return self._long_running_job ('InventarioHardware', self.do_InventarioHardware, args=(post_params,))
@execution_level('full')
@check_secret
def process_InventarioSoftware (self, path, get_params, post_params, server):
logger.debug ('in process_InventarioSoftware, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
return self._long_running_job ('InventarioSoftware', self.do_InventarioSoftware, args=(post_params,))
@execution_level('full')
@check_secret
def process_KillJob (self, path, get_params, post_params, server):
logger.debug ('in process_KillJob, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
jid = post_params['job_id']
r = self.killer (jid)
logger.debug (f'r bef ({r})')
r.update ({ 'nfn':'RESPUESTA_KillJob', 'job':jid })
logger.debug (f'r aft ({r})')
return r

View File

@ -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

View File

@ -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:
for i in range(10):
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().
"""

View File

@ -0,0 +1,258 @@
# -*- 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: Ramón M. Gómez, ramongomez at us dot es
"""
from __future__ import unicode_literals
import socket
import platform
import os
import fcntl
import subprocess
import struct
import array
import six
import chardet
from opengnsys import utils
from opengnsys.log import logger
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 _exec_ogcommand(ogcmd):
"""
Loads OpenGnsys environment variables, executes the command and returns the result
"""
ret = subprocess.check_output(ogcmd, shell=True)
return ret
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 get_oglive_version():
"""
Returns ogLive Kernel version and architecture
:return: kernel version
"""
kv = platform.os.uname()
return kv[2] + ', ' + kv[4]
def reboot():
"""
Simple reboot using OpenGnsys script
"""
# Workaround for dummy thread
if six.PY3 is False:
import threading
threading._DummyThread._Thread__stop = lambda x: 42
_exec_ogcommand('/opt/opengnsys/scripts/reboot')
def poweroff():
"""
Simple power off using OpenGnsys script
"""
# Workaround for dummy thread
if six.PY3 is False:
import threading
threading._DummyThread._Thread__stop = lambda x: 42
_exec_ogcommand('/opt/opengnsys/scripts/poweroff')
def get_etc_path():
"""
Returns etc directory path.
"""
return os.sep + 'etc'
def get_configuration():
"""
Returns client's configuration
Warning: this operation may take some time
:return:
"""
try:
_exec_ogcommand('/opt/opengnsys/interfaceAdm/getConfiguration')
# Returns content of configuration file
cfgdata = open('/tmp/getconfig', 'r').read().strip()
except IOError:
cfgdata = ''
return cfgdata
def exec_command(cmd):
"""
Executing a shell command
:param cmd:
:return: object with components:
output: standard output
error: error output
exit: exit code
"""
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = proc.communicate()
try:
if out is not None:
encoding = chardet.detect(out)["encoding"]
if encoding is not None:
out = out.decode(encoding).encode("utf8")
if err is not None:
encoding = chardet.detect(err)["encoding"]
if encoding is not None:
err = err.decode(encoding).encode("utf8")
except Exception as e:
logger.debug("ERROR EXEC COMMAND: {}".format(str(e)))
stat = proc.returncode
return stat, out, err
def get_hardware():
"""
Returns client's hardware list
:return:
"""
try:
filepath = _exec_ogcommand('/opt/opengnsys/scripts/listHardwareInfo').strip()
# Returns content of configuration file, skipping the header line and newline characters
with open(filepath, 'r') as f:
harddata = map(str.strip, f.readlines()[1:])
except IOError:
harddata = ''
return harddata
def get_software(disk, part):
"""
Returns software list installed on an operating system
:param disk:
:param part:
:return:
"""
try:
filepath = _exec_ogcommand('/opt/opengnsys/scripts/listSoftwareInfo {} {}'.format(disk, part)).strip()
# Returns content of configuration file, skipping the header line and newline characters
with open(filepath, 'r') as f:
softdata = map(str.strip, f.readlines())
except IOError:
softdata = ''
return softdata

View File

@ -27,12 +27,13 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
""" """
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Ramón M. Gómez, ramongomez at us dot es
""" """
# pylint: disable=unused-wildcard-import,wildcard-import # pylint: disable=unused-wildcard-import,wildcard-import
from __future__ import unicode_literals
import sys import sys
import os
# Importing platform operations and getting operating system data. # Importing platform operations and getting operating system data.
if sys.platform == 'win32': if sys.platform == 'win32':
@ -45,6 +46,11 @@ else:
os_type = 'MacOS' os_type = 'MacOS'
os_version = getMacosVersion().replace(',', '') os_version = getMacosVersion().replace(',', '')
else: else:
from .linux.operations import * # @UnusedWildImport if os.path.exists('/scripts/oginit'):
os_type = 'Linux' from .oglive.operations import * # @UnusedWildImport
os_version = getLinuxVersion() os_type = 'ogLive'
os_version = get_oglive_version().replace(',', '')
else:
from .linux.operations import * # @UnusedWildImport
os_type = 'Linux'
os_version = getLinuxVersion()

View File

@ -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))

View File

@ -26,21 +26,23 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # 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. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
""" '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
""" '''
from __future__ import unicode_literals
import json from .log import logger
import socket from .config import readConfig
import time from .utils import exceptionToMessage
from . import ipc from . import ipc
from . import httpserver from . import httpserver
from .config import readConfig
from .loader import loadModules from .loader import loadModules
from .log import logger
from .utils import exceptionToMessage
import socket
import time
import json
import six
IPC_PORT = 10398 IPC_PORT = 10398
@ -56,17 +58,21 @@ class CommonService(object):
logger.info('Initializing OpenGnsys Agent') logger.info('Initializing OpenGnsys Agent')
# Read configuration file before proceding & ensures minimal config is there # Read configuration file before proceding & ensures minimal config is there
self.config = readConfig() self.config = readConfig()
# Get opengnsys section as dict
# Get opengnsys section as dict
cfg = dict(self.config.items('opengnsys')) cfg = dict(self.config.items('opengnsys'))
# Set up log level # Set up log level
logger.setLevel(cfg.get('log', 'INFO')) logger.setLevel(cfg.get('log', 'INFO'))
logger.debug('Loaded configuration from opengnsys.cfg:') logger.debug('Loaded configuration from opengnsys.cfg:')
for section in self.config.sections(): for section in self.config.sections():
logger.debug('Section {} = {}'.format(section, self.config.items(section))) logger.debug('Section {} = {}'.format(section, self.config.items(section)))
if logger.logger.isWindows(): if logger.logger.isWindows():
# Logs will also go to windows event log for services # Logs will also go to windows event log for services
logger.logger.serviceLogger = True logger.logger.serviceLogger = True
@ -84,16 +90,15 @@ class CommonService(object):
logger.debug('Modules: {}'.format(list(v.name for v in self.modules))) logger.debug('Modules: {}'.format(list(v.name for v in self.modules)))
def stop(self): def stop(self):
""" '''
Requests service termination Requests service termination
""" '''
self.isAlive = False self.isAlive = False
# ******************************** # ********************************
# * Internal messages processors * # * Internal messages processors *
# ******************************** # ********************************
def notifyLogin(self, data): def notifyLogin(self, username):
username = data.decode('utf-8')
for v in self.modules: for v in self.modules:
try: try:
logger.debug('Notifying login of user {} to module {}'.format(username, v.name)) logger.debug('Notifying login of user {} to module {}'.format(username, v.name))
@ -101,8 +106,7 @@ class CommonService(object):
except Exception as e: except Exception as e:
logger.error('Got exception {} processing login message on {}'.format(e, v.name)) logger.error('Got exception {} processing login message on {}'.format(e, v.name))
def notifyLogout(self, data): def notifyLogout(self, username):
username = data.decode('utf-8')
for v in self.modules: for v in self.modules:
try: try:
logger.debug('Notifying logout of user {} to module {}'.format(username, v.name)) logger.debug('Notifying logout of user {} to module {}'.format(username, v.name))
@ -111,7 +115,7 @@ class CommonService(object):
logger.error('Got exception {} processing logout message on {}'.format(e, v.name)) logger.error('Got exception {} processing logout message on {}'.format(e, v.name))
def notifyMessage(self, data): def notifyMessage(self, data):
module, message, data = data.decode('utf-8').split('\0') module, message, data = data.split('\0')
for v in self.modules: for v in self.modules:
if v.name == module: # Case Sensitive!!!! if v.name == module: # Case Sensitive!!!!
try: try:
@ -122,12 +126,13 @@ class CommonService(object):
logger.error('Got exception {} processing generic message on {}'.format(e, v.name)) logger.error('Got exception {} processing generic message on {}'.format(e, v.name))
logger.error('Module {} not found, messsage {} not sent'.format(module, message)) logger.error('Module {} not found, messsage {} not sent'.format(module, message))
def clientMessageProcessor(self, msg, data): def clientMessageProcessor(self, msg, data):
""" '''
Callback, invoked from IPC, on its own thread (not the main thread). 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 This thread will "block" communication with agent untill finished, but this should be no problem
""" '''
logger.debug('Got message {}'.format(msg)) logger.debug('Got message {}'.format(msg))
if msg == ipc.REQ_LOGIN: if msg == ipc.REQ_LOGIN:
@ -138,9 +143,14 @@ class CommonService(object):
self.notifyMessage(data) self.notifyMessage(data)
def initialize(self): def initialize(self):
""" # ******************************************
Initialize listeners, modules, etc... # * 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)) logger.debug('Starting IPC listener at {}'.format(IPC_PORT))
self.ipc = ipc.ServerIPC(self.ipcport, clientMessageProcessor=self.clientMessageProcessor) self.ipc = ipc.ServerIPC(self.ipcport, clientMessageProcessor=self.clientMessageProcessor)
self.ipc.start() self.ipc.start()
@ -158,7 +168,7 @@ class CommonService(object):
validMods.append(mod) validMods.append(mod)
except Exception as e: except Exception as e:
logger.exception() logger.exception()
logger.debug("Activation of {} failed: {}".format(mod.name, exceptionToMessage(e))) logger.error("Activation of {} failed: {}".format(mod.name, exceptionToMessage(e)))
self.modules[:] = validMods # copy instead of assignment self.modules[:] = validMods # copy instead of assignment
@ -193,47 +203,47 @@ class CommonService(object):
# Methods that CAN BE overridden by agents # Methods that CAN BE overridden by agents
# **************************************** # ****************************************
def doWait(self, miliseconds): def doWait(self, miliseconds):
""" '''
Invoked to wait a bit Invoked to wait a bit
CAN be OVERRIDDEN CAN be OVERRIDDEN
""" '''
time.sleep(float(miliseconds) / 1000) time.sleep(float(miliseconds) / 1000)
def notifyStop(self): def notifyStop(self):
""" '''
Overridden to log stop Overridden to log stop
""" '''
logger.info('Service is being stopped') logger.info('Service is being stopped')
# *************************************************** # ***************************************************
# * Helpers, convenient methods to facilitate comms * # * Helpers, convenient methods to facilitate comms *
# *************************************************** # ***************************************************
def sendClientMessage(self, toModule, message, data): def sendClientMessage(self, toModule, message, data):
""" '''
Sends a message to the clients using IPC Sends a message to the clients using IPC
The data is converted to json, so ensure that it is serializable. 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 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 toModule: Module that will receive this message
@param message: Message to send @param message: Message to send
@param data: data to send @param data: data to send
""" '''
self.ipc.sendMessageMessage('\0'.join((toModule, message, json.dumps(data)))) self.ipc.sendMessageMessage('\0'.join((toModule, message, json.dumps(data))))
def sendScriptMessage(self, script): def sendScriptMessage(self, script):
""" '''
Sends an script to be executed by client Sends an script to be executed by client
""" '''
self.ipc.sendScriptMessage(script) self.ipc.sendScriptMessage(script)
def sendLogoffMessage(self): def sendLogoffMessage(self):
""" '''
Sends a logoff message to client Sends a logoff message to client
""" '''
self.ipc.sendLoggofMessage() self.ipc.sendLoggofMessage()
def sendPopupMessage(self, title, message): def sendPopupMessage(self, title, message):
""" '''
Sends a popup box to be displayed by client Sends a poup box to be displayed by client
""" '''
self.ipc.sendPopupMessage(title, message) self.ipc.sendPopupMessage(title, message)

View File

@ -29,7 +29,7 @@
''' '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
from __future__ import unicode_literals
import sys import sys
import six import six
@ -61,7 +61,7 @@ def exceptionToMessage(e):
if isinstance(arg, Exception): if isinstance(arg, Exception):
msg = msg + exceptionToMessage(arg) msg = msg + exceptionToMessage(arg)
else: else:
msg = msg + str(arg) + '. ' msg = msg + toUnicode(arg) + '. '
return msg return msg

View File

@ -120,9 +120,5 @@ class OGAgentSvc(win32serviceutil.ServiceFramework, CommonService):
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) == 1:
servicemanager.Initialize() win32serviceutil.HandleCommandLine(OGAgentSvc)
servicemanager.PrepareToHostSingle(OGAgentSvc)
servicemanager.StartServiceCtrlDispatcher()
else:
win32serviceutil.HandleCommandLine(OGAgentSvc)

View File

@ -36,8 +36,6 @@ import logging
import os import os
import tempfile import tempfile
from ..log_format import JsonFormatter
# Valid logging levels, from UDS Broker (uds.core.utils.log) # Valid logging levels, from UDS Broker (uds.core.utils.log)
OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in range(6)) OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in range(6))
@ -45,25 +43,14 @@ OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in range(6))
class LocalLogger(object): class LocalLogger(object):
def __init__(self): def __init__(self):
# tempdir is different for "user application" and "service" # tempdir is different for "user application" and "service"
# service wil get c:\windows\temp, while user will get c:\users\XXX\appdata\local\temp # service wil get c:\windows\temp, while user will get c:\users\XXX\temp
logging.basicConfig(
fname1 = os.path.join (tempfile.gettempdir(), 'opengnsys.log') filename=os.path.join(tempfile.gettempdir(), 'opengnsys.log'),
fmt1 = logging.Formatter (fmt='%(levelname)s %(asctime)s (%(threadName)s) (%(funcName)s) %(message)s') filemode='a',
fh1 = logging.FileHandler (filename=fname1, mode='a') format='%(levelname)s %(asctime)s %(message)s',
fh1.setFormatter (fmt1) level=logging.DEBUG
fh1.setLevel (logging.DEBUG) )
fname2 = os.path.join (tempfile.gettempdir(), 'opengnsys.json.log')
fmt2 = JsonFormatter ({"timestamp": "asctime", "severity": "levelname", "threadName": "threadName", "function": "funcName", "message": "message"}, time_format='%Y-%m-%d %H:%M:%S', msec_format='')
fh2 = logging.FileHandler (filename=fname2, mode='a')
fh2.setFormatter (fmt2)
fh2.setLevel (logging.DEBUG)
self.logger = logging.getLogger('opengnsys') self.logger = logging.getLogger('opengnsys')
self.logger.setLevel (logging.DEBUG)
self.logger.addHandler (fh1)
self.logger.addHandler (fh2)
self.serviceLogger = False self.serviceLogger = False
def log(self, level, message): def log(self, level, message):
@ -71,7 +58,7 @@ class LocalLogger(object):
# our loglevels are 10000 (other), 20000 (debug), .... # our loglevels are 10000 (other), 20000 (debug), ....
# logging levels are 10 (debug), 20 (info) # logging levels are 10 (debug), 20 (info)
# OTHER = logging.NOTSET # OTHER = logging.NOTSET
self.logger.log(int(level / 1000 - 10), message, stacklevel=4) self.logger.log(level / 1000 - 10, message)
if level < INFO or self.serviceLogger is False: # Only information and above will be on event log if level < INFO or self.serviceLogger is False: # Only information and above will be on event log
return return

View File

@ -35,7 +35,6 @@ import os
import locale import locale
import subprocess import subprocess
import ctypes import ctypes
import base64
from ctypes.wintypes import DWORD, LPCWSTR from ctypes.wintypes import DWORD, LPCWSTR
import win32com.client # @UnresolvedImport, pylint: disable=import-error import win32com.client # @UnresolvedImport, pylint: disable=import-error
import win32net # @UnresolvedImport, pylint: disable=import-error import win32net # @UnresolvedImport, pylint: disable=import-error
@ -47,6 +46,15 @@ from opengnsys import utils
from opengnsys.log import logger 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(): def getNetworkInfo():
''' '''
Obtains a list of network interfaces Obtains a list of network interfaces
@ -56,7 +64,7 @@ def getNetworkInfo():
ip: ip of the interface ip: ip of the interface
''' '''
obj = win32com.client.Dispatch("WbemScripting.SWbemLocator") obj = win32com.client.Dispatch("WbemScripting.SWbemLocator")
wmobj = obj.ConnectServer("localhost", "root\\cimv2") wmobj = obj.ConnectServer("localhost", "root\cimv2")
adapters = wmobj.ExecQuery("Select * from Win32_NetworkAdapterConfiguration where IpEnabled=True") adapters = wmobj.ExecQuery("Select * from Win32_NetworkAdapterConfiguration where IpEnabled=True")
try: try:
for obj in adapters: for obj in adapters:
@ -73,16 +81,33 @@ def getNetworkInfo():
return 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(): def getWindowsVersion():
''' '''
Returns Windows version. Returns Windows version.
''' '''
import winreg import _winreg
reg = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion') reg = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion')
try: try:
data = '{} {}'.format(winreg.QueryValueEx(reg, 'ProductName')[0], winreg.QueryValueEx(reg, 'ReleaseId')[0]) data = '{} {}'.format(_winreg.QueryValueEx(reg, 'ProductName')[0], _winreg.QueryValueEx(reg, 'ReleaseId')[0])
except Exception: except Exception:
data = '{} {}'.format(winreg.QueryValueEx(reg, 'ProductName')[0], winreg.QueryValueEx(reg, 'CurrentBuildNumber')[0]) data = '{} {}'.format(_winreg.QueryValueEx(reg, 'ProductName')[0], _winreg.QueryValueEx(reg, 'CurrentBuildNumber')[0])
reg.Close() reg.Close()
return data return data
@ -90,8 +115,8 @@ def getWindowsVersion():
EWX_LOGOFF = 0x00000000 EWX_LOGOFF = 0x00000000
EWX_SHUTDOWN = 0x00000001 EWX_SHUTDOWN = 0x00000001
EWX_REBOOT = 0x00000002 EWX_REBOOT = 0x00000002
#EWX_FORCE = 0x00000004 EWX_FORCE = 0x00000004
#EWX_POWEROFF = 0x00000008 EWX_POWEROFF = 0x00000008
EWX_FORCEIFHUNG = 0x00000010 EWX_FORCEIFHUNG = 0x00000010
@ -112,6 +137,109 @@ def logoff():
win32api.ExitWindowsEx(EWX_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('{} {}'.format(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(): def getCurrentUser():
''' '''
Returns current logged in username Returns current logged in username
@ -126,14 +254,6 @@ def getSessionLanguage():
return locale.getdefaultlocale()[0] return locale.getdefaultlocale()[0]
def get_session_type():
"""
returns the user's session type (Local session, RDP,...)
:return: string
"""
return os.environ.get('SESSIONNAME').lower()
def showPopup(title, message): def showPopup(title, message):
''' '''
Displays a message box on user's session (during 1 min). Displays a message box on user's session (during 1 min).
@ -147,10 +267,3 @@ def get_etc_path():
Returns etc directory path. Returns etc directory path.
""" """
return os.path.join('C:', os.sep, 'Windows', 'System32', 'drivers', 'etc') return os.path.join('C:', os.sep, 'Windows', 'System32', 'drivers', 'etc')
def build_popen_args(script):
## turn the script into utf16le, then to b64 again, and feed the blob to powershell
u16 = script.encode ('utf-16le') ## utf16
b64 = base64.b64encode (u16).decode ('utf-8') ## b64 (which returns bytes, so we need an additional decode(utf8))
return ['powershell', '-WindowStyle', 'Hidden', '-EncodedCommand', b64]

View File

@ -1,3 +1,2 @@
from .server_worker import ServerWorker from .server_worker import ServerWorker
from .client_worker import ClientWorker from .client_worker import ClientWorker
from .oglive_worker import ogLiveWorker, ThreadWithResult

View File

@ -25,25 +25,25 @@
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # 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 # 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. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
""" '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
""" '''
# pylint: disable=unused-wildcard-import,wildcard-import # pylint: disable=unused-wildcard-import,wildcard-import
from __future__ import unicode_literals
class ClientWorker(object): class ClientWorker(object):
""" '''
A ServerWorker is a server module that "works" for service A ServerWorker is a server module that "works" for service
Most method are invoked inside their own thread, except onActivation & onDeactivation. Most method are invoked inside their own thread, except onActivation & onDeactivation.
This two methods are invoked inside main service thread, take that into account when creating them 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. * 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...) 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: * The name of the module is used as REST message destination id:
https://sampleserver:8888/[name]/.... https://sampleserver:8888/[name]/....
Remember that module names and REST path are case sensitive!!! Remember that module names and REST path are case sensitive!!!
""" '''
name = None name = None
service = None service = None
@ -51,15 +51,15 @@ class ClientWorker(object):
self.service = service self.service = service
def activate(self): def activate(self):
""" '''
Convenient method to wrap onActivation, so we can include easyly custom common logic for activation in a future Convenient method to wrap onActivation, so we can include easyly custom common logic for activation in a future
""" '''
self.onActivation() self.onActivation()
def deactivate(self): def deactivate(self):
""" '''
Convenient method to wrap onActivation, so we can include easyly custom common logic for deactivation in a future Convenient method to wrap onActivation, so we can include easyly custom common logic for deactivation in a future
""" '''
self.onDeactivation() self.onDeactivation()
def processMessage(self, message, params): def processMessage(self, message, params):
@ -83,31 +83,32 @@ class ClientWorker(object):
return operation(params) return operation(params)
def onActivation(self): def onActivation(self):
""" '''
Invoked by Service for activation. Invoked by Service for activation.
This MUST be overridden by modules! This MUST be overridden by modules!
This method is invoked inside main thread, so if it "hangs", complete service will hang 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 This should be no problem, but be advised about this
""" '''
pass pass
def onDeactivation(self): def onDeactivation(self):
""" '''
Invoked by Service before unloading service Invoked by Service before unloading service
This MUST be overridden by modules! This MUST be overridden by modules!
This method is invoked inside main thread, so if it "hangs", complete service will hang 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 This should be no problem, but be advised about this
""" '''
pass pass
# ************************************* # *************************************
# * Helper, convenient helper methods * # * Helper, convenient helper methods *
# ************************************* # *************************************
def sendServerMessage(self, message, data): def sendServerMessage(self, message, data):
""" '''
Sends a message to connected ipc clients Sends a message to connected ipc clients
By convenience, it uses the "current" moduel name as destination module name also. 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 If you need to send a message to a different module, you can use self.service.sendClientMessage(module, message, data) instead
og this helmer og this helmer
""" '''
self.service.ipc.sendMessage(self.name, message, data) self.service.ipc.sendMessage(self.name, message, data)

View File

@ -1,529 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2024 Qindel Formación y Servicios S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# 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: Natalia Serrano, nserrano at qindel dot com
"""
# pylint: disable=unused-wildcard-import,wildcard-import
import os
import re
import time
try: import dbus ## don't fail on windows (the worker will later refuse to load anyway)
except: pass
import random
import subprocess
import threading
import signal
from configparser import NoOptionError
from opengnsys import REST
from opengnsys.log import logger
from .server_worker import ServerWorker
## https://stackoverflow.com/questions/6893968/how-to-get-the-return-value-from-a-thread
class ThreadWithResult (threading.Thread):
def run (self):
try:
self.result = None
if self._target is not None:
## the first arg in self._args is the queue
self.pid_q = self._args[0]
self.stdout_q = self._args[1]
self._args = self._args[2:]
try:
self.result = self._target (*self._args, **self._kwargs)
except Exception as e:
self.result = { 'res': 2, 'der': f'got exception: ({e})' } ## res=2 as defined in ogAdmClient.c:2048
finally:
# Avoid a refcycle if the thread is running a function with an argument that has a member that points to the thread.
del self._target, self._args, self._kwargs, self.pid_q, self.stdout_q
class ogLiveWorker(ServerWorker):
thread_list = {}
thread_lock = threading.Lock()
tbErroresScripts = [
"Se han generado errores desconocidos. No se puede continuar la ejecución de este módulo", ## 0
"001-Formato de ejecución incorrecto.",
"002-Fichero o dispositivo no encontrado",
"003-Error en partición de disco",
"004-Partición o fichero bloqueado",
"005-Error al crear o restaurar una imagen",
"006-Sin sistema operativo",
"007-Programa o función BOOLEAN no ejecutable",
"008-Error en la creación del archivo de eco para consola remota",
"009-Error en la lectura del archivo temporal de intercambio",
"010-Error al ejecutar la llamada a la interface de administración",
"011-La información retornada por la interface de administración excede de la longitud permitida",
"012-Error en el envío de fichero por la red",
"013-Error en la creación del proceso hijo",
"014-Error de escritura en destino",
"015-Sin Cache en el Cliente",
"016-No hay espacio en la cache para almacenar fichero-imagen",
"017-Error al Reducir el Sistema Archivos",
"018-Error al Expandir el Sistema Archivos",
"019-Valor fuera de rango o no válido.",
"020-Sistema de archivos desconocido o no se puede montar",
"021-Error en partición de caché local",
"022-El disco indicado no contiene una particion GPT",
"023-Error no definido",
"024-Error no definido",
"025-Error no definido",
"026-Error no definido",
"027-Error no definido",
"028-Error no definido",
"029-Error no definido",
"030-Error al restaurar imagen - Imagen mas grande que particion",
"031-Error al realizar el comando updateCache",
"032-Error al formatear",
"033-Archivo de imagen corrupto o de otra versión de partclone",
"034-Error no definido",
"035-Error no definido",
"036-Error no definido",
"037-Error no definido",
"038-Error no definido",
"039-Error no definido",
"040-Error imprevisto no definido",
"041-Error no definido",
"042-Error no definido",
"043-Error no definido",
"044-Error no definido",
"045-Error no definido",
"046-Error no definido",
"047-Error no definido",
"048-Error no definido",
"049-Error no definido",
"050-Error en la generación de sintaxis de transferenica unicast",
"051-Error en envio UNICAST de una particion",
"052-Error en envio UNICAST de un fichero",
"053-Error en la recepcion UNICAST de una particion",
"054-Error en la recepcion UNICAST de un fichero",
"055-Error en la generacion de sintaxis de transferenica Multicast",
"056-Error en envio MULTICAST de un fichero",
"057-Error en la recepcion MULTICAST de un fichero",
"058-Error en envio MULTICAST de una particion",
"059-Error en la recepcion MULTICAST de una particion",
"060-Error en la conexion de una sesion UNICAST|MULTICAST con el MASTER",
"061-Error no definido",
"062-Error no definido",
"063-Error no definido",
"064-Error no definido",
"065-Error no definido",
"066-Error no definido",
"067-Error no definido",
"068-Error no definido",
"069-Error no definido",
"070-Error al montar una imagen sincronizada.",
"071-Imagen no sincronizable (es monolitica).",
"072-Error al desmontar la imagen.",
"073-No se detectan diferencias entre la imagen basica y la particion.",
"074-Error al sincronizar, puede afectar la creacion/restauracion de la imagen.",
"Error desconocido",
]
def notifier (self, job_id, result):
result['job_id'] = job_id
self.REST.sendMessage ('clients/status/webhook', result)
def killer (self, job_id):
logger.debug (f'killer() called, job_id ({job_id})')
if job_id not in self.thread_list: return { 'res': 2, 'der': 'Unknown job' }
with self.thread_lock:
if 'thread' not in self.thread_list[job_id]: return { 'res': 2, 'der': 'Job is not running' }
t = self.thread_list[job_id]['thread']
pid = self.thread_list[job_id]['child_pid']
logger.debug (f'pid ({pid})')
try_times = 8
sig = signal.SIGTERM
msg = f'could not kill pid ({pid}) after ({try_times}) tries'
success = 2 ## mimic cmd['res'] in respuestaEjecucionComando(): "1" means success, "2" means failed
while True:
t.join (0.05)
if not t.is_alive():
msg = 'job terminated'
success = 1
logger.debug (msg)
self.thread_list[job_id]['child_pid'] = None
break
## race condition: if the subprocess finishes just here, then we already checked that t.is_alive() is true, but os.path.exists(/proc/pid) will be false below. msg will be 'nothing to kill'.
## this is fine in the first iteration of the loop, before we send any signals. In the rest of iterations, after some signals were sent, msg should be 'job terminated' instead.
if pid:
if os.path.exists (f'/proc/{pid}'):
logger.debug (f'sending signal ({sig}) to pid ({pid})')
## if the process finishes just here, nothing happens: the signal is sent to the void
os.kill (pid, sig)
#subprocess.run (['kill', '--signal', str(sig), str(pid)])
else:
msg = f'pid ({pid}) is gone, nothing to kill'
success = 1
logger.debug (msg)
self.thread_list[job_id]['child_pid'] = None
break
else:
msg = 'no PID to kill'
logger.debug (msg)
if not try_times: break
if 4 == try_times: sig = signal.SIGKILL ## change signal after a few tries
try_times -= 1
time.sleep (0.4)
return { 'res':success, 'der':msg }
def _extract_progress (self, job_id, ary=[]):
progress = None
for i in ary:
if m := re.search (r'^\[([0-9]+)\]', i): ## look for strings like '[10]', '[60]'
#logger.debug (f"matched regex, m.groups ({m.groups()})")
progress = float (m.groups()[0]) / 100
return progress
## monitors child threads, waits for them to finish
## pings ogcore
def mon (self):
n = 0
while True:
with self.thread_lock:
for k in self.thread_list:
elem = self.thread_list[k]
if 'thread' not in elem: continue
#logger.debug (f'considering thread ({k})')
if self.pid_q:
if not self.pid_q.empty():
elem['child_pid'] = self.pid_q.get()
logger.debug (f'queue not empty, got pid ({elem["child_pid"]})')
if self.stdout_q:
partial = ''
while not self.stdout_q.empty():
partial += self.stdout_q.get()
lines = partial.splitlines()
if len (lines):
p = self._extract_progress (k, lines)
if p:
m = { "job_id": k, "progress": p }
self.REST.sendMessage ('clients/status/webhook', { "job_id": k, "progress": p })
elem['thread'].join (0.05)
if not elem['thread'].is_alive():
logger.debug (f'is no longer alive, k ({k}) thread ({elem["thread"]})')
elem['running'] = False
elem['result'] = elem['thread'].result
del elem['thread']
self.notifier (k, elem['result'])
time.sleep (1)
n += 1
if not n % 10:
alive_threads = []
for k in self.thread_list:
elem = self.thread_list[k]
if 'thread' not in elem: continue
alive_threads.append (k)
if alive_threads:
s = ','.join (alive_threads)
logger.debug (f'alive threads: {s}')
body = {
'iph': self.IPlocal,
'timestamp': int (time.time()),
}
#logger.debug (f'about to send ping ({body})')
self.REST.sendMessage ('clients/status/webhook', body)
def interfaceAdmin (self, method, parametros=[]):
if method in ['Apagar', 'CambiarAcceso', 'Configurar', 'CrearImagen', 'EjecutarScript', 'getConfiguration', 'getIpAddress', 'IniciarSesion', 'InventarioHardware', 'InventarioSoftware', 'Reiniciar', 'RestaurarImagen']:
## python
logger.debug (f'({method}) is a python method')
exe = '{}/{}.py'.format (self.pathinterface, method)
proc = [exe]+parametros
else: ## ConsolaRemota procesaCache
## bash
logger.debug (f'({method}) is a bash method')
exe = '{}/{}'.format (self.pathinterface, method)
LANG = os.environ.get ('LANG', 'en_GB.UTF-8').replace ('UTF_8', 'UTF-8')
devel_bash_prefix = f'''
PATH=/opt/opengnsys/scripts/:$PATH;
source /opt/opengnsys/etc/lang.{LANG}.conf;
for I in /opt/opengnsys/lib/engine/bin/*.lib; do source $I; done;
for i in $(declare -F |cut -f3 -d" "); do export -f $i; done;
'''
if parametros:
proc = ['bash', '-c', '{} {} {}'.format (devel_bash_prefix, exe, ' '.join (parametros))]
else:
proc = ['bash', '-c', '{} {}'.format (devel_bash_prefix, exe)]
logger.debug ('subprocess.run ("{}")'.format (' '.join (proc)))
p = subprocess.Popen (proc, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if self.pid_q:
self.pid_q.put (p.pid)
else:
## esto sucede por ejemplo cuando arranca el agente, que estamos en interfaceAdmin() en el mismo hilo, sin _long_running_job ni hilo separado
#logger.debug ('no queue--not writing any PID to it')
pass
sout = serr = ''
while p.poll() is None:
for l in iter (p.stdout.readline, b''):
partial = l.decode ('utf-8', 'ignore')
if self.stdout_q: self.stdout_q.put (partial)
sout += partial
for l in iter (p.stderr.readline, b''):
partial = l.decode ('utf-8', 'ignore')
serr += partial
time.sleep (1)
sout = sout.strip()
serr = serr.strip()
## DEBUG
logger.debug (f'stdout follows:')
for l in sout.splitlines():
logger.debug (f' {l}')
#logger.debug (f'stderr follows:')
#for l in serr.splitlines():
# logger.debug (f' {l}')
## /DEBUG
if 0 != p.returncode:
cmd_txt = ' '.join (proc)
logger.error (f'command ({cmd_txt}) failed, stderr follows:')
for l in serr.splitlines():
logger.error (f' {l}')
raise Exception (f'command ({cmd_txt}) failed, see log for details')
return sout
def tomaIPlocal (self):
try:
self.IPlocal = self.interfaceAdmin ('getIpAddress')
except Exception as e:
logger.error (e)
logger.error ('No se ha podido recuperar la dirección IP del cliente')
return False
logger.info ('local IP is "{}"'.format (self.IPlocal))
return True
def tomaMAClocal (self):
## tomaIPlocal() calls interfaceAdm('getIpAddress')
## getIpAddress runs 'ip addr show' and returns the IP address of every network interface except "lo"
## (ie. breaks badly if there's more than one network interface)
## let's make the same assumptions here
mac = subprocess.run (["ip -json link show |jq -r '.[] |select (.ifname != \"lo\") |.address'"], shell=True, capture_output=True, text=True)
self.mac = mac.stdout.strip()
return True
def enviaMensajeServidor (self, path, obj={}):
obj['iph'] = self.IPlocal ## Ip del ordenador
obj['ido'] = self.idordenador ## Identificador del ordenador
obj['npc'] = self.nombreordenador ## Nombre del ordenador
obj['idc'] = self.idcentro ## Identificador del centro
obj['ida'] = self.idaula ## Identificador del aula
res = self.REST.sendMessage ('/'.join ([self.name, path]), obj)
if (type (res) is not dict):
logger.error (f'response is not a dict ({res})')
return False
return res
## en C, esto envia una trama de respuesta al servidor. Devuelve un boolean
## en python, simplemente termina de construir la respuesta y la devuelve; no envía nada por la red. El caller la usa en return() para enviar implícitamente la respuesta
def respuestaEjecucionComando (self, cmd, herror, ids=None):
if ids: ## Existe seguimiento
cmd['ids'] = ids ## Añade identificador de la sesión
if 0 == herror: ## el comando terminó con resultado satisfactorio
cmd['res'] = 1
cmd['der'] = ''
else: ## el comando tuvo algún error
cmd['res'] = 2
cmd['der'] = self.tbErroresScripts[herror]
return cmd
def cargaPaginaWeb (self, url=None):
if (not url): url = self.urlMenu
dbus_address = os.environ.get ('DBUS_SESSION_BUS_ADDRESS')
if not dbus_address: logger.warning ('env var DBUS_SESSION_BUS_ADDRESS not set, cargaPaginaWeb() will likely not work')
b = dbus.SystemBus()
dest = 'es.opengnsys.OGBrowser.browser'
path = '/'
interface = None
method = 'setURL'
signature = 's'
try:
b.call_blocking (dest, path, interface, method, 's', [url])
except Exception as e:
if 'ServiceUnknown' in str(e):
## browser not running
subprocess.Popen (['/usr/bin/launch_browser', url])
else:
logger.error (f'Error al cambiar URL: ({e})')
return False
return True
def muestraMenu (self):
self.cargaPaginaWeb()
def muestraMensaje (self, idx):
self.cargaPaginaWeb (f'{self.urlMsg}?idx={idx}')
def LeeConfiguracion (self):
try:
parametroscfg = self.interfaceAdmin ('getConfiguration') ## Configuración de los Sistemas Operativos del cliente
except Exception as e:
logger.error (e)
logger.error ('No se ha podido recuperar la dirección IP del cliente')
return None
#logger.debug ('parametroscfg ({})'.format (parametroscfg))
return parametroscfg
def cfg2obj (self, cfg):
obj = []
ptrPar = cfg.split ('\n')
for line in ptrPar:
elem = {}
ptrCfg = line.split ('\t')
for item in ptrCfg:
if '=' not in item:
logger.warning (f'invalid item ({item})')
continue
k, v = item.split ('=', maxsplit=1)
elem[k] = v
obj.append (elem)
return obj
def onActivation (self):
if not os.path.exists ('/scripts/oginit'):
## no estamos en oglive, este modulo no debe cargarse
## esta lógica la saco de src/opengnsys/linux/operations.py, donde hay un if similar
raise Exception ('Refusing to load within an operating system')
self.pathinterface = None
self.IPlocal = None ## Ip del ordenador
self.mac = None ## MAC del ordenador
self.idordenador = None ## Identificador del ordenador
self.nombreordenador = None ## Nombre del ordenador
self.cache = None
self.idproautoexec = None
self.idcentro = None ## Identificador del centro
self.idaula = None ## Identificador del aula
self.pid_q = None ## for passing PIDs around
self.stdout_q = None ## for passing stdout
self.progress_jobs = {}
ogcore_scheme = os.environ.get ('OGAGENTCFG_OGCORE_SCHEME', 'https')
ogcore_ip = os.environ.get ('OGAGENTCFG_OGCORE_IP', '192.168.2.1')
ogcore_port = os.environ.get ('OGAGENTCFG_OGCORE_PORT', '8443')
urlmenu_scheme = os.environ.get ('OGAGENTCFG_URLMENU_SCHEME', 'https')
urlmenu_ip = os.environ.get ('OGAGENTCFG_URLMENU_IP', '192.168.2.1')
urlmenu_port = os.environ.get ('OGAGENTCFG_URLMENU_PORT', '8443')
ogcore_ip_port = ':'.join (map (str, filter (None, [ogcore_ip, ogcore_port ])))
urlmenu_ip_port = ':'.join (map (str, filter (None, [urlmenu_ip, urlmenu_port])))
try:
url = self.service.config.get (self.name, 'remote')
loglevel = self.service.config.get (self.name, 'log')
self.pathinterface = self.service.config.get (self.name, 'pathinterface')
self.urlMenu = self.service.config.get (self.name, 'urlMenu')
self.urlMsg = self.service.config.get (self.name, 'urlMsg')
ca_file = self.service.config.get (self.name, 'ca')
crt_file = self.service.config.get (self.name, 'crt')
key_file = self.service.config.get (self.name, 'key')
url = url.format (ogcore_scheme, ogcore_ip_port)
self.urlMenu = self.urlMenu.format (urlmenu_scheme, urlmenu_ip_port)
except NoOptionError as e:
logger.error ("Configuration error: {}".format (e))
raise e
logger.setLevel (loglevel)
self.REST = REST (url, ca_file=ca_file, crt_file=crt_file, key_file=key_file)
if not self.tomaIPlocal():
raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo')
if not self.tomaMAClocal():
raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo')
threading.Thread (name='monitoring_thread', target=self.mon, daemon=True).start()
def _long_running_job (self, name, f, args):
any_job_running = False
for k in self.thread_list:
if self.thread_list[k]['running']:
any_job_running = True
break
if any_job_running:
logger.info ('some job is already running, refusing to launch another one')
return { 'job_id': None, 'message': 'some job is already running, refusing to launch another one' }
job_id = '{}-{}'.format (name, ''.join (random.choice ('0123456789abcdef') for _ in range (8)))
import queue
self.pid_q = queue.Queue() ## a single queue works for us because we never have more than one long_running_job at the same time
self.stdout_q = queue.Queue()
self.thread_list[job_id] = {
'thread': ThreadWithResult (target=f, args=(self.pid_q, self.stdout_q) + args),
'starttime': time.time(),
'child_pid': None,
'running': True,
'result': None
}
self.thread_list[job_id]['thread'].start()
return { 'job_id': job_id }
## para matar threads tengo lo siguiente:
## - aqui en _long_running_job meto una cola en self.pid_q
## - (self.pid_q fue inicializado a None al instanciar el objeto, para evitar error "objeto no tiene 'pid_q'")
## - en el thread_list también tengo un child_pid para almacenar el pid de los procesos hijos
## - al crear el ThreadWithResult le paso la cola, y luego en run() la recojo y la meto en el self.pid_q del thread
## - en interfaceAdmin() al hacer subprocess.Popen(), recojo el pid y lo escribo en la queue
## - en mon() recojo pids de la queue y los meto en thread_list 'child_pid'
## - algunas funciones llaman a interfaceAdmin más de una vez, y escriben más de un pid en la cola, y en mon() voy recogiendo y actualizando
## - por ejemplo EjecutarScript llama a interfaceAdmin() y luego llama a LeeConfiguracion() el cual llama a interfaceAdmin() otra vez
## - y cuando nos llamen a KillJob, terminamos en killer() el cual coge el 'child_pid' y zas
## - pero a lo mejor el child ya terminó
## - o a lo mejor el KillJob nos llegó demasiado pronto y todavía no hubo ningún child
##
## $ curl --insecure -X POST --data '{"nfn":"EjecutarScript","scp":"cd /usr; sleep 30; pwd; ls","ids":"0"}' https://192.168.2.199:8000/ogAdmClient/EjecutarScript
## {"job_id": "EjecutarScript-333feb3f"}
## $ curl --insecure -X POST --data '{"job_id":"EjecutarScript-333feb3f"}' https://192.168.2.199:8000/ogAdmClient/KillJob
##
## funciona bien, excepto que el PID no muere xD, ni siquiera haciendo subprocess.run('kill')
## para mostrar el progreso de los jobs reutilizo la misma infra
## una cola self.stdout_q
## en interfaceAdmin escribo la stdout parcial que ya venia recogiendo
## mon() lo recoge y le hace un POST a ogcore

View File

@ -25,25 +25,25 @@
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # 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 # 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. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
""" '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
""" '''
# pylint: disable=unused-wildcard-import,wildcard-import # pylint: disable=unused-wildcard-import,wildcard-import
from __future__ import unicode_literals
class ServerWorker(object): class ServerWorker(object):
""" '''
A ServerWorker is a server module that "works" for service A ServerWorker is a server module that "works" for service
Most method are invoked inside their own thread, except onActivation & onDeactivation. Most method are invoked inside their own thread, except onActivation & onDeactivation.
This two methods are invoked inside main service thread, take that into account when creating them 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. * 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...) 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: * The name of the module is used as REST message destination id:
https://sampleserver:8888/[name]/.... https://sampleserver:8888/[name]/....
Remember that module names and REST path are case sensitive!!! Remember that module names and REST path are case sensitive!!!
""" '''
name = None name = None
service = None service = None
locked = False locked = False
@ -52,43 +52,43 @@ class ServerWorker(object):
self.service = service self.service = service
def activate(self): def activate(self):
""" '''
Convenient method to wrap onActivation, so we can include easyly custom common logic for activation in a future Convenient method to wrap onActivation, so we can include easyly custom common logic for activation in a future
""" '''
self.onActivation() self.onActivation()
def deactivate(self): def deactivate(self):
""" '''
Convenient method to wrap onActivation, so we can include easyly custom common logic for deactivation in a future Convenient method to wrap onActivation, so we can include easyly custom common logic for deactivation in a future
""" '''
self.onDeactivation() self.onDeactivation()
def process(self, getParams, postParams, server): def process(self, getParams, postParams, server):
""" '''
This method is invoked on a message received with an empty path (that means a message with only the module name, like in "http://example.com/Sample" 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 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) 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') raise NotImplementedError('Generic message processor is not supported')
def processServerMessage(self, path, getParams, postParams, server): def processServerMessage(self, path, getParams, postParams, server):
""" '''
This method can be overriden to provide your own message proccessor, or better you can 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 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: * Example:
Imagine this invocation url (no matter if GET or POST): http://example.com:9999/Sample/mazinger/Z 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: The HTTP Server will remove "Sample" from path, parse arguments and invoke this method as this:
module.processMessage(["mazinger","Z"], getParams, postParams) 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: 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) 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 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 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) 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: if self.locked is True:
raise Exception('system is busy') raise Exception('system is busy')
@ -96,27 +96,27 @@ class ServerWorker(object):
return self.process(getParams, postParams, server) return self.process(getParams, postParams, server)
try: try:
operation = getattr(self, 'process_' + path[0]) operation = getattr(self, 'process_' + path[0])
except: except Exception:
raise Exception ({ '_httpcode': 404, '_msg': f'{path[0]}: method not found' }) raise Exception('Message processor for "{}" not found'.format(path[0]))
return operation(path[1:], getParams, postParams, server) return operation(path[1:], getParams, postParams, server)
def processClientMessage(self, message, data): def processClientMessage(self, message, data):
""" '''
Invoked by Service when a client message is received (A message from user space Agent) 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 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 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: * Example:
We got a message from OGAgent "Mazinger", with json params We got a message from OGAgent "Mazinger", with json params
module.processClientMessage("mazinger", jsonParams) 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: 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) self.process_client_mazinger(jsonParams)
The methods returns nothing (client communications are done asynchronously) The methods returns nothing (client communications are done asynchronously)
""" '''
try: try:
operation = getattr(self, 'process_client_' + message) operation = getattr(self, 'process_client_' + message)
except Exception: except Exception:
@ -128,52 +128,52 @@ class ServerWorker(object):
def onActivation(self): def onActivation(self):
""" '''
Invoked by Service for activation. Invoked by Service for activation.
This MUST be overridden by modules! This MUST be overridden by modules!
This method is invoked inside main thread, so if it "hangs", complete service will hang 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 This should be no problem, but be advised about this
""" '''
pass pass
def onDeactivation(self): def onDeactivation(self):
""" '''
Invoked by Service before unloading service Invoked by Service before unloading service
This MUST be overridden by modules! This MUST be overridden by modules!
This method is invoked inside main thread, so if it "hangs", complete service will hang 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 This should be no problem, but be advised about this
""" '''
pass pass
def onLogin(self, user): def onLogin(self, user):
""" '''
Invoked by Service when an user login is detected Invoked by Service when an user login is detected
This CAN be overridden by modules 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 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 This method is run on its own thread
""" '''
pass pass
def onLogout(self, user): def onLogout(self, user):
""" '''
Invoked by Service when an user login is detected Invoked by Service when an user login is detected
This CAN be overridden by modules 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 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 This method is run on its own thread
""" '''
pass pass
# ************************************* # *************************************
# * Helper, convenient helper methods * # * Helper, convenient helper methods *
# ************************************* # *************************************
def sendClientMessage(self, message, data): def sendClientMessage(self, message, data):
""" '''
Sends a message to connected ipc clients Sends a message to connected ipc clients
By convenience, it uses the "current" moduel name as destination module name also. 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 If you need to send a message to a different module, you can use self.service.sendClientMessage(module, message, data) instead
og this helmer og this helmer
""" '''
self.service.sendClientMessage(self.name, message, data) self.service.sendClientMessage(self.name, message, data)
def sendScriptMessage(self, script): def sendScriptMessage(self, script):

Some files were not shown because too many files have changed in this diff Show More