diff --git a/linux/debian/changelog b/linux/debian/changelog index 37b61ba..9a3204e 100644 --- a/linux/debian/changelog +++ b/linux/debian/changelog @@ -1,3 +1,16 @@ +ogagent (1.3.7-1) stable; urgency=medium + + * CloningEngine: RESTfully keep a list of long-running jobs + + -- OpenGnsys developers 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 Thu, 19 Sep 2024 13:28:17 +0200 + ogagent (1.3.5-1) stable; urgency=medium * Don't unconditionally load modules--dynamically load everything diff --git a/ogcore-mock.py b/ogcore-mock.py index 5982355..765ba5e 100644 --- a/ogcore-mock.py +++ b/ogcore-mock.py @@ -3,6 +3,7 @@ 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 @@ -14,30 +15,28 @@ logging.basicConfig(level=logging.INFO) @app.route('/opengnsys/rest/ogagent/', methods=['POST']) def og_agent(cucu): - c = request - logging.info(f"{request.get_json()}") + logging.info(f'{request.get_json()}') return jsonify({}) -## agente oglive +## agente oglive: modulo ogAdmClient -@app.route('/opengnsys/rest/__ogAdmClient/InclusionCliente', methods=['POST']) +@app.route('/opengnsys/rest/ogAdmClient/InclusionCliente', methods=['POST']) def inclusion_cliente(): - c = request - logging.info(f"{request.get_json()}") + 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})") + 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"); + 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"); @@ -79,21 +78,20 @@ def _recorreProcedimientos(parametros, fileexe, idp): return 1 -@app.route('/opengnsys/rest/__ogAdmClient/AutoexecCliente', methods=['POST']) +@app.route('/opengnsys/rest/ogAdmClient/AutoexecCliente', methods=['POST']) def autoexec_client(): - c = request - logging.info(f"{request.get_json()}") + 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})") + logging.info(f'iph ({iph}) exe ({exe})') fileautoexec = '/tmp/Sautoexec-{}'.format(iph) - logging.info ("fileautoexec ({})".format (fileautoexec)); + logging.info ('fileautoexec ({})'.format (fileautoexec)); try: fileexe = open (fileautoexec, 'w') except Exception as e: - logging.error ("cannot create temporary file: {}".format (e)) + logging.error ('cannot create temporary file: {}'.format (e)) return jsonify({}) if (_recorreProcedimientos ('', fileexe, exe)): @@ -104,15 +102,16 @@ def autoexec_client(): fileexe.close() return res -@app.route('/opengnsys/rest/__ogAdmClient/enviaArchivo', methods=['POST']) +@app.route('/opengnsys/rest/ogAdmClient/enviaArchivo', methods=['POST']) def envia_archivo(): - c = request - logging.info(f"{request.get_json()}") + 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})") + logging.info(f'nfl ({nfl})') - return jsonify({'contents': subprocess.run (['cat', nfl], capture_output=True).stdout.decode('utf-8')}) + 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 @@ -130,19 +129,18 @@ def buscaComandos(ido): ## 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']) +@app.route('/opengnsys/rest/ogAdmClient/ComandosPendientes', methods=['POST']) def comandos_pendientes(): - c = request - logging.info(f"{request.get_json()}") + 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})") + 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") + abort(404, 'Client does not exist') param = buscaComandos(ido) ## Existen comandos pendientes, buscamos solo uno if param is None: @@ -152,46 +150,62 @@ def comandos_pendientes(): return jsonify(param) -@app.route('/opengnsys/rest/__ogAdmClient/DisponibilidadComandos', methods=['POST']) +@app.route('/opengnsys/rest/ogAdmClient/DisponibilidadComandos', methods=['POST']) def disponibilidad_comandos(): - c = request - logging.info(f"{request.get_json()}") + 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})") + 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") + 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/', methods=['POST']) -def cucu(cucu): - c = request - j = c.get_json(force=True) - logging.info(f"{request.get_json()} {j}") - if 'cucu' not in j: - abort(400, "missing parameter 'cucu'") +@app.route('/opengnsys/rest/ogAdmClient/', 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 recibe_archivo(): + logging.info(f'{request.get_json()}') + return jsonify({'anything':'anything'}) ## if we return {}, then we trigger "if not {}" which happens to be true + +@app.route('/opengnsys/rest/CloningEngine/', methods=['GET', 'POST']) +def ce_cucu(cucu): + abort (404) + - return jsonify({'cucu': j['cucu']}) @app.errorhandler(404) def _page_not_found(e): - return render_template_string('''not found''') + if type(e.description) is dict: + return jsonify (e.description), e.code + else: + return render_template_string('''not found'''), e.code @app.errorhandler(500) def _internal_server_error(e): - return render_template_string('''err''') + return render_template_string('''err'''), e.code @app.errorhandler(Exception) def _exception(e): print(e) - return render_template_string('''exception''') + return render_template_string('''exception'''), e.code if __name__ == '__main__': app.run(host = '192.168.1.249', port = 443, debug=True) diff --git a/src/VERSION b/src/VERSION index 80e78df..3336003 100644 --- a/src/VERSION +++ b/src/VERSION @@ -1 +1 @@ -1.3.5 +1.3.7 diff --git a/src/cfg/ogagent.cfg b/src/cfg/ogagent.cfg index 96f4da0..f9b012b 100644 --- a/src/cfg/ogagent.cfg +++ b/src/cfg/ogagent.cfg @@ -7,7 +7,7 @@ port=8000 #path=test_modules/server,more_modules/server # Remote OpenGnsys Service -remote=https://192.168.2.10/opengnsys/rest +remote=https://192.168.2.1/opengnsys/rest # Alternate OpenGnsys Service (comment out to enable this option) #altremote=https://10.0.2.2/opengnsys/rest @@ -22,12 +22,16 @@ log=DEBUG # This section will be passes on activation to module [ogAdmClient] #path=test_modules/server,more_modules/server -## this URL will probably be left equal to the other one, but let's see -remote=https://192.168.2.10/opengnsys/rest -log=DEBUG -#servidorAdm=192.168.2.1 -#puerto=2008 +remote=https://192.168.2.1/opengnsys/rest +log=DEBUG +pathinterface=/opt/opengnsys/interfaceAdm +urlMenu=https://192.168.2.1/opengnsys/varios/menubrowser.php +urlMsg=http://localhost/cgi-bin/httpd-log.sh + +[CloningEngine] +remote=https://192.168.2.1/opengnsys/rest +log=DEBUG pathinterface=/opt/opengnsys/interfaceAdm urlMenu=https://192.168.2.1/opengnsys/varios/menubrowser.php urlMsg=http://localhost/cgi-bin/httpd-log.sh diff --git a/src/opengnsys/RESTApi.py b/src/opengnsys/RESTApi.py index 8773cf5..3e3468f 100644 --- a/src/opengnsys/RESTApi.py +++ b/src/opengnsys/RESTApi.py @@ -142,6 +142,10 @@ class REST(object): else: 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 except requests.exceptions.RequestException as e: raise ConnectionError(e) diff --git a/src/opengnsys/httpserver.py b/src/opengnsys/httpserver.py index 47c36ec..bcbb238 100644 --- a/src/opengnsys/httpserver.py +++ b/src/opengnsys/httpserver.py @@ -88,11 +88,32 @@ class HTTPServerHandler(BaseHTTPRequestHandler): Locates witch module will process the message based on path (first folder on url path) """ try: + if module is None: + raise Exception ({ '_httpcode': 404, '_msg': f'Module {path[0]} not found' }) data = module.processServerMessage(path, get_params, post_params, self) self.sendJsonResponse(data) except Exception as e: logger.exception() - self.sendJsonError(500, exceptionToMessage(e)) + n_args = len (e.args) + if 0 == n_args: + logger.error ('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.error ('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.warning ('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.error ('Message processor for "{}" returned exception dict "{}" with no HTTP code'.format(path[0], str(e))) + self.sendJsonError (500, exceptionToMessage(e)) + else: + logger.error ('Message processor for "{}" returned non-string and non-dict exception "{}"'.format(path[0], str(e))) + self.sendJsonError (500, exceptionToMessage(e)) + ## not reached def do_GET(self): module, path, params = self.parseUrl() diff --git a/src/opengnsys/loader.py b/src/opengnsys/loader.py index a1c0211..746babc 100644 --- a/src/opengnsys/loader.py +++ b/src/opengnsys/loader.py @@ -43,6 +43,8 @@ from opengnsys.workers import ServerWorker from opengnsys.workers import ClientWorker from .log import logger +PY3_12 = sys.version_info[0:2] >= (3, 12) + def loadModules(controller, client=False): ''' @@ -89,7 +91,11 @@ def loadModules(controller, client=False): for (module_loader, name, ispkg) in pkgutil.iter_modules(paths, modPath + '.'): if ispkg: logger.debug('Found module package {}'.format(name)) - module_loader.find_module(name).load_module(name) + if PY3_12: + 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: diff --git a/src/opengnsys/log.py b/src/opengnsys/log.py index eb1f309..7950b71 100644 --- a/src/opengnsys/log.py +++ b/src/opengnsys/log.py @@ -79,6 +79,9 @@ class Logger(object): def warn(self, message): self.log(WARN, message) + def warning(self, message): + self.log(WARN, message) + def info(self, message): self.log(INFO, message) diff --git a/src/opengnsys/modules/server/CloningEngine/__init__.py b/src/opengnsys/modules/server/CloningEngine/__init__.py new file mode 100644 index 0000000..a7c44cb --- /dev/null +++ b/src/opengnsys/modules/server/CloningEngine/__init__.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 +# -*- 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 +""" + +import base64 +import os +import time +import random +from pathlib import Path + +from opengnsys.log import logger +from opengnsys.workers import ogLiveWorker, ThreadWithResult + +class CloningEngineWorker (ogLiveWorker): + name = 'CloningEngine' # Module name + REST = None # REST object + + def onActivation (self): + super().onActivation() + logger.info ('onActivation ok') + + def onDeactivation (self): + logger.debug ('onDeactivation') + + ## 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): + 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] ## XXX + + return cmd + + def InventariandoSoftware (self, dsk, par, sw, 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') + 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() + + ## Envía fichero de inventario al servidor + sft_dst = f'/tmp/Ssft-{self.IPlocal}-{par}' ## Nombre que tendra el archivo en el Servidor + logger.debug ('sending recibeArchivo to server') + res = self.enviaMensajeServidor ('recibeArchivo', { 'nfl': sft_dst, 'contents': base64.b64encode (sft_src_contents).decode ('utf-8') }) + logger.debug (res) + if not res: + herror = 12 ## Error de envío de fichero por la red + raise Exception ('Ha ocurrido algún problema al enviar un archivo por la red') + self.muestraMensaje (19) + + if not sw: + cmd = { + 'nfn': 'RESPUESTA_InventarioSoftware', + 'par': par, + 'sft': sft_dst, + } + return self.respuestaEjecucionComando (cmd, herror, 0) + + return {'true':'true'} ## XXX + + 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) + + if self.InventariandoSoftware (dsk, par, False, 'InventarioSoftware'): ## Crea inventario Software previamente + self.muestraMensaje (2) + 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') + + 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ó + } + 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: + self.interfaceAdmin (nfn, [dsk, par, nci, ipr, ptc]) + 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': cfg, ## Configuración de discos + } + return self.respuestaEjecucionComando (cmd, herror, ids) + + 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))) + self.thread_list[job_id] = { + 'thread': ThreadWithResult (target=f, args=args), + 'starttime': time.time(), + 'running': True, + 'result': None + } + self.thread_list[job_id]['thread'].start() + return { 'job_id': job_id } + + def process_status (self, path, get_params, post_params, server): + ## join finished threads + for k in self.thread_list: + logger.debug (f'considering thread ({k})') + elem = self.thread_list[k] + if 'thread' in elem: + 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'] + + ## return status of threads + thr_status = {} + for k in self.thread_list: + thr_status[k] = { + 'running': self.thread_list[k]['running'], + 'result': self.thread_list[k]['result'], + } + return thr_status + + 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' }) + + 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' }) diff --git a/src/opengnsys/modules/server/OpenGnSys/__init__.py b/src/opengnsys/modules/server/OpenGnSys/__init__.py index 0506b93..a6b2493 100644 --- a/src/opengnsys/modules/server/OpenGnSys/__init__.py +++ b/src/opengnsys/modules/server/OpenGnSys/__init__.py @@ -106,20 +106,25 @@ class OpenGnSysWorker(ServerWorker): """ Sends OGAgent activation notification to OpenGnsys server """ + if os.path.exists ('/scripts/oginit'): + ## 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 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 try: - url = self.service.config.get('opengnsys', 'remote') + url = self.service.config.get(self.name, 'remote') except NoOptionError as e: logger.error("Configuration error: {}".format(e)) raise e self.REST = REST(url) # Execution level ('full' by default) try: - self.exec_level = self.service.config.get('opengnsys', 'level') + 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) @@ -156,7 +161,7 @@ class OpenGnSysWorker(ServerWorker): logger.warn (str (e)) # Trying to initialize on alternative server, if defined # (used in "exam mode" from the University of Seville) - self.REST = REST(self.service.config.get('opengnsys', 'altremote')) + self.REST = REST(self.service.config.get(self.name, 'altremote')) self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip, 'secret': self.random, 'ostype': operations.os_type, 'osversion': operations.os_version, 'alt_url': True, @@ -184,6 +189,8 @@ class OpenGnSysWorker(ServerWorker): if os.path.isfile(new_hosts_file): shutil.copyfile(new_hosts_file, hosts_file) + logger.info ('onActivation ok') + def onDeactivation(self): """ Sends OGAgent stopping notification to OpenGnsys server diff --git a/src/opengnsys/modules/server/ogAdmClient/__init__.py b/src/opengnsys/modules/server/ogAdmClient/__init__.py index b107431..3c617ea 100644 --- a/src/opengnsys/modules/server/ogAdmClient/__init__.py +++ b/src/opengnsys/modules/server/ogAdmClient/__init__.py @@ -32,154 +32,182 @@ @author: Natalia Serrano, nserrano at qindel dot com """ - import base64 -import os -import random -import shutil -import string -import threading -import time +#import threading +#import time import subprocess -import urllib.error -import urllib.parse -import urllib.request -from configparser import NoOptionError -from opengnsys import REST, operations, VERSION +#from opengnsys import operations from opengnsys.log import logger -from opengnsys.workers import ServerWorker +from opengnsys.workers import ogLiveWorker # Check authorization header decorator -def check_secret(fnc): +def check_secret (fnc): """ Decorator to check for received secret key and raise exception if it isn't valid. """ - def wrapper(*args, **kwargs): - return fnc(*args, **kwargs) + def wrapper (*args, **kwargs): + return fnc (*args, **kwargs) #try: # this, path, get_params, post_params, server = args # # Accept "status" operation with no arguments or any function with Authorization header # if fnc.__name__ == 'process_status' and not get_params: - # return fnc(*args, **kwargs) + # return fnc (*args, **kwargs) # elif this.random == server.headers['Authorization']: - # return fnc(*args, **kwargs) + # return fnc (*args, **kwargs) # else: - # raise Exception('Unauthorized operation') + # raise Exception ('Unauthorized operation') #except Exception as e: - # logger.error(str(e)) - # raise Exception(e) + # logger.error (str (e)) + # raise Exception (e) return wrapper -class ogAdmClientWorker(ServerWorker): +class ogAdmClientWorker (ogLiveWorker): name = 'ogAdmClient' # Module name - interface = None # Bound interface for OpenGnsys + #interface = None # Bound interface for OpenGnsys (el otro modulo lo usa para obtener .ip y .mac REST = None # REST object - random = None # Random string for secure connections - length = 32 # Random string length - pathinterface = None - IPlocal = None - idordenador = None - nombreordenador = None - cache = None - idproautoexec = None - idcentro = None - idaula = None + 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 onDeactivation(self): + def onDeactivation (self): """ Sends OGAgent stopping notification to OpenGnsys server """ - logger.debug('onDeactivation') + logger.debug ('onDeactivation') - def processClientMessage(self, message, data): - logger.debug('Got OpenGnsys message from client: {}, data {}'.format(message, data)) + #def processClientMessage (self, message, data): + # logger.debug ('Got OpenGnsys message from client: {}, data {}'.format (message, data)) - def onLogin(self, data): - logger.warn('in onLogin, should not happen') + #def onLogin (self, data): + # logger.warning ('in onLogin, should not happen') - def onLogout(self, user): - logger.warn('in onLogout, should not happen') - - def process_ogclient(self, path, get_params, post_params, server): - """ - This method can be overridden to provide your own message processor, or better you can - implement a method that is called exactly as "process_" + path[0] (module name has been removed from path - array) and this default processMessage will invoke it - * Example: - Imagine this invocation url (no matter if GET or POST): http://example.com:9999/Sample/mazinger/Z - The HTTP Server will remove "Sample" from path, parse arguments and invoke this method as this: - module.processMessage(["mazinger","Z"], get_params, post_params) - - This method will process "mazinger", and look for a "self" method that is called "process_mazinger", - and invoke it this way: - return self.process_mazinger(["Z"], get_params, post_params) - - In the case path is empty (that is, the path is composed only by the module name, like in - "http://example.com/Sample", the "process" method will be invoked directly - - The methods must return data that can be serialized to json (i.e. Objects are not serializable to json, - basic type are) - """ - if not path: - return "ok" - try: - operation = getattr(self, 'ogclient_' + path[0]) - except Exception: - raise Exception('Message processor for "{}" not found'.format(path[0])) - return operation(path[1:], get_params, post_params) + #def onLogout (self, user): + # logger.warning ('in onLogout, should not happen') @check_secret - def process_status(self, path, get_params, post_params, server): - return {'ogAdmClient': 'in process_status'} + def process_status (self, path, get_params, post_params, server): + return {self.name: 'in process_status'} ## XXX + #@check_secret + #def process_reboot (self, path, get_params, post_params, server): + # """ + # Launches a system reboot operation + # :param path: + # :param get_params: + # :param post_params: + # :param server: authorization header + # :return: JSON object {"op": "launched"} + # """ + # logger.debug ('Received reboot operation') + + # # Rebooting thread + # def rebt(): + # operations.reboot() + # threading.Thread (target=rebt).start() + # return {'op': 'launched'} + + #@check_secret + #def process_poweroff (self, path, get_params, post_params, server): + # """ + # Launches a system power off operation + # :param path: + # :param get_params: + # :param post_params: + # :param server: authorization header + # :return: JSON object {"op": "launched"} + # """ + # logger.debug ('Received poweroff operation') + + # # Powering off thread + # def pwoff(): + # time.sleep (2) + # operations.poweroff() + # threading.Thread (target=pwoff).start() + # return {'op': 'launched'} + + ## curl --insecure -X POST --data '{"nfn": "popup", "title": "my title", "message": "my message"}' https://192.168.1.249:8000/ogAdmClient/popup @check_secret - def process_reboot(self, path, get_params, post_params, server): - """ - Launches a system reboot operation - :param path: - :param get_params: - :param post_params: - :param server: authorization header - :return: JSON object {"op": "launched"} - """ - logger.debug('Received reboot operation') - - # Rebooting thread - def rebt(): - operations.reboot() - threading.Thread(target=rebt).start() - return {'op': 'launched'} - - @check_secret - def process_poweroff(self, path, get_params, post_params, server): - """ - Launches a system power off operation - :param path: - :param get_params: - :param post_params: - :param server: authorization header - :return: JSON object {"op": "launched"} - """ - logger.debug('Received poweroff operation') - - # Powering off thread - def pwoff(): - time.sleep(2) - operations.poweroff() - threading.Thread(target=pwoff).start() - return {'op': 'launched'} - - @check_secret - def process_logoff(self, path, get_params, post_params, server): - logger.warn('in process_logoff, should not happen') - - @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))) + 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 "" ## type(post_params) "" return {'debug':'test'} @@ -191,95 +219,25 @@ class ogAdmClientWorker(ServerWorker): - #def process_client_popup(self, params): - # logger.warn('in process_client_popup') - ## process_* are invoked from opengnsys/httpserver.py:99 "data = module.processServerMessage(path, get_params, post_params, self)" (via opengnsys/workers/server_worker.py) - ## process_client_* are invoked from opengnsys/service.py:123 "v.processClientMessage(message, json.loads(data))" (via opengnsys/workers/server_worker.py) + ## process_* are invoked from opengnsys/httpserver.py:99 "data = module.processServerMessage (path, get_params, post_params, self)" (via opengnsys/workers/server_worker.py) + ## process_client_* are invoked from opengnsys/service.py:123 "v.processClientMessage (message, json.loads (data))" (via opengnsys/workers/server_worker.py) - - def interfaceAdmin (self, method, parametros=[]): - exe = '{}/{}'.format (self.pathinterface, method) - devel_bash_prefix = ''' - PATH=/opt/opengnsys/scripts/:$PATH; - 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 ("{}", capture_output=True)'.format (proc)) - return subprocess.run (proc, capture_output=True).stdout.strip().decode ('utf-8') - - def tomaIPlocal(self): - try: - self.IPlocal = self.interfaceAdmin ('getIpAddress'); - logger.info ('local IP is "{}"'.format (self.IPlocal)) - except Exception as e: - logger.error (e) - logger.error ('No se ha podido recuperar la dirección IP del cliente') - return False - return True - - def LeeConfiguracion(self): - parametroscfg = self.interfaceAdmin ('getConfiguration') ## Configuración de los Sistemas Operativos del cliente - logger.debug ('parametroscfg ({})'.format (parametroscfg)) - return (parametroscfg) - - 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 (path, obj) - - if (type(res) is not dict): - #logger.error ('No se ha podido establecer conexión con el Servidor de Administración') ## Error de conexión con el servidor - logger.debug (f'res ({res})') - logger.error ('Error al enviar trama ***send() fallo') - return False - - return res - - def ejecutaArchivo(self,fn): + def ejecutaArchivo (self,fn): logger.debug ('fn ({})'.format (fn)) - ## TODO need to understand this code (ogAdmClient.c:2111) before translating it to python - ## in a function called "ejecutaArchivo" I'd expect a file to be run, however there's only a call to gestionaTrama() that I don't know where it leads to - #char* buffer,*lineas[MAXIMAS_LINEAS]; - #int i,numlin; - #char modulo[] = "ejecutaArchivo()"; - - #buffer = leeArchivo(filecmd); - #if (buffer): - # numlin = splitCadena(lineas, buffer, '@'); - # initParametros(ptrTrama,0); - # for (i = 0; i < numlin; i++) { - # if(strlen(lineas[i])>0){ - # strcpy(ptrTrama->parametros,lineas[i]); - # if(!gestionaTrama(ptrTrama)){ // Análisis de la trama - # errorLog(modulo,39,FALSE); - # //return(FALSE); - # } - # } - # } - #liberaMemoria(buffer); - - ## let's test something, assuming that in the "file" there's not just some bash, but a sequence of parameters such as "nfn=Function\rparam1=foo\rparam2=bar" + ## 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 + 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 + for l in buffer.split ('@'): + if not len (l): continue logger.debug ('line ({})'.format (l)) ## 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('=') + for param in l.split ("\r"): + k, v = param.split ('=') post_params[k] = v logger.debug ('post_params "{}"'.format (post_params)) @@ -297,13 +255,18 @@ class ogAdmClientWorker(ServerWorker): logger.error ('Ha ocurrido algún problema al procesar la trama recibida') break - def inclusionCliente(self): + 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': cfg }) logger.debug ('res ({})'.format (res)) ## RESPUESTA_InclusionCliente - if (type(res) is not dict or 0 == res['res']) : + 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 @@ -320,34 +283,26 @@ class ogAdmClientWorker(ServerWorker): return True - def cuestionCache(self): + def cuestionCache (self): return True ## ogAdmClient.c:425 - #>>>>>>>>>>>>>>>>>>>>>>>>>> - #try: - # self.interfaceAdmin ('procesaCache', [ self.cache ]); - #except Exception as e: - # logger.error ('Ha habido algún problerma al procesar la caché') - # return False - # - #return True - def autoexecCliente(self): + def autoexecCliente (self): res = self.enviaMensajeServidor ('AutoexecCliente', { 'exe': self.idproautoexec }) logger.debug ('res ({})'.format (res)) - if (type(res) is not dict): + 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']) : + 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): + 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 @@ -356,16 +311,16 @@ class ogAdmClientWorker(ServerWorker): 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')) + fd.write (base64.b64decode (res['contents']).decode ('utf-8')) - self.ejecutaArchivo (fileautoexec); + self.ejecutaArchivo (fileautoexec) return True - def comandosPendientes(self): + def comandosPendientes (self): while (True): res = self.enviaMensajeServidor ('ComandosPendientes') ## receives just one command - if (type(res) is not dict): + 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 @@ -375,7 +330,7 @@ class ogAdmClientWorker(ServerWorker): break ## TODO manage the rest of cases... we might have to do something similar to ejecutaArchivo - #if(!gestionaTrama(ptrTrama)){ // Análisis de la trama + #if (!gestionaTrama (ptrTrama)){ // Análisis de la trama # logger.error ('Ha ocurrido algún problema al procesar la trama recibida') # return False #} @@ -384,31 +339,11 @@ class ogAdmClientWorker(ServerWorker): return True - def cargaPaginaWeb(url=None): - if (not url): url = self.urlMenu - os.system ('pkill -9 browser'); - - #p = subprocess.Popen (['/opt/opengnsys/bin/browser', '-qws', url]) - p = subprocess.Popen (['/usr/bin/xeyes']) - try: - p.wait (2) ## if the process dies before 2 seconds... - logger.error ('Error al ejecutar la llamada a la interface de administración') - logger.error ('Error en la creación del proceso hijo') - logger.error ('return code "{}"'.format (p.returncode)) - return False - except subprocess.TimeoutExpired: - pass - - return True - - def muestraMenu(self): - self.cargaPaginaWeb() - - def procesaComandos(self): + def procesaComandos (self): res = self.enviaMensajeServidor ('DisponibilidadComandos', { 'tpc': 'OPG' }) ## Activar disponibilidad logger.debug ('res ({})'.format (res)) - if (type(res) is not dict): + 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 @@ -417,44 +352,22 @@ class ogAdmClientWorker(ServerWorker): ## 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' + #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' + #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' + #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): - """ - Sends OGAgent activation notification to OpenGnsys server - """ - # Generate random secret to send on activation - self.random = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(self.length)) - # Ensure cfg has required configuration variables or an exception will be thrown - try: - url = self.service.config.get('ogAdmClient', 'remote') - loglevel = self.service.config.get('ogAdmClient', 'log') - #servidorAdm = self.service.config.get('ogAdmClient', 'servidorAdm') - #puerto = self.service.config.get('ogAdmClient', 'puerto') - self.pathinterface = self.service.config.get('ogAdmClient', 'pathinterface') - urlMenu = self.service.config.get('ogAdmClient', 'urlMenu') - #urlMsg = self.service.config.get('ogAdmClient', 'urlMsg') - logger.setLevel(loglevel) - except NoOptionError as e: - logger.error("Configuration error: {}".format(e)) - raise e - self.REST = REST(url) - - if (not self.tomaIPlocal()): - raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo') - + def onActivation (self): + super().onActivation() logger.info ('Inicio de sesion') logger.info ('Abriendo sesión en el servidor de Administración') if (not self.inclusionCliente()): @@ -462,16 +375,16 @@ class ogAdmClientWorker(ServerWorker): logger.info ('Cliente iniciado') logger.info ('Procesando caché') - if (not self.cuestionCache()): + 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): + if self.idproautoexec > 0: logger.info ('Ejecución de archivo Autoexec') - if (not self.autoexecCliente()): + 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()): + 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') @@ -479,62 +392,44 @@ class ogAdmClientWorker(ServerWorker): self.muestraMenu() self.procesaComandos() - def process_NoComandosPtes(self, path, get_params, post_params, server): - logger.warn('in process_NoComandosPtes') + logger.info ('onActivation ok') - def process_Actualizar(self, path, get_params, post_params, server): - logger.warn('in process_Actualizar') + ## curl --insecure https://192.168.1.249:8000/ogAdmClient/Actualizar + def process_Actualizar (self, path, get_params, post_params, server): + logger.warning ('in process_Actualizar') - def process_Purgar(self, path, get_params, post_params, server): - logger.warn('in process_Purgar') + def process_Purgar (self, path, get_params, post_params, server): + logger.warning ('in process_Purgar') - def process_ConsolaRemota(self, path, get_params, post_params, server): - logger.warn('in process_ConsolaRemota') + def process_ConsolaRemota (self, path, get_params, post_params, server): + logger.warning ('in process_ConsolaRemota') - def process_Sondeo(self, path, get_params, post_params, server): - logger.warn('in process_Sondeo') + def process_Sondeo (self, path, get_params, post_params, server): + logger.warning ('in process_Sondeo') - def process_Arrancar(self, path, get_params, post_params, server): - logger.warn('in process_Arrancar') + def process_Arrancar (self, path, get_params, post_params, server): + logger.warning ('in process_Arrancar') - def process_Apagar(self, path, get_params, post_params, server): - logger.warn('in process_Apagar') + def process_Apagar (self, path, get_params, post_params, server): + logger.warning ('in process_Apagar') - def process_Reiniciar(self, path, get_params, post_params, server): - logger.warn('in process_Reiniciar') + def process_Reiniciar (self, path, get_params, post_params, server): + logger.warning ('in process_Reiniciar') - def process_IniciarSesion(self, path, get_params, post_params, server): - logger.warn('in process_IniciarSesion') + def process_IniciarSesion (self, path, get_params, post_params, server): + logger.warning ('in process_IniciarSesion') - def process_CrearImagen(self, path, get_params, post_params, server): - logger.warn('in process_CrearImagen') + def process_Configurar (self, path, get_params, post_params, server): + logger.warning ('in process_Configurar') - def process_CrearImagenBasica(self, path, get_params, post_params, server): - logger.warn('in process_CrearImagenBasica') + def process_EjecutarScript (self, path, get_params, post_params, server): + logger.warning ('in process_EjecutarScript') - def process_CrearSoftIncremental(self, path, get_params, post_params, server): - logger.warn('in process_CrearSoftIncremental') + def process_InventarioHardware (self, path, get_params, post_params, server): + logger.warning ('in process_InventarioHardware') - def process_RestaurarImagen(self, path, get_params, post_params, server): - logger.warn('in process_RestaurarImagen') + def process_InventarioSoftware (self, path, get_params, post_params, server): + logger.warning ('in process_InventarioSoftware') - def process_RestaurarImagenBasica(self, path, get_params, post_params, server): - logger.warn('in process_RestaurarImagenBasica') - - def process_RestaurarSoftIncremental(self, path, get_params, post_params, server): - logger.warn('in process_RestaurarSoftIncremental') - - def process_Configurar(self, path, get_params, post_params, server): - logger.warn('in process_Configurar') - - def process_EjecutarScript(self, path, get_params, post_params, server): - logger.warn('in process_EjecutarScript') - - def process_InventarioHardware(self, path, get_params, post_params, server): - logger.warn('in process_InventarioHardware') - - def process_InventarioSoftware(self, path, get_params, post_params, server): - logger.warn('in process_InventarioSoftware') - - def process_EjecutaComandosPendientes(self, path, get_params, post_params, server): - logger.warn('in process_EjecutaComandosPendientes') + def process_EjecutaComandosPendientes (self, path, get_params, post_params, server): + logger.warning ('in process_EjecutaComandosPendientes') diff --git a/src/opengnsys/workers/__init__.py b/src/opengnsys/workers/__init__.py index f2bcd7d..e74f9f5 100644 --- a/src/opengnsys/workers/__init__.py +++ b/src/opengnsys/workers/__init__.py @@ -1,2 +1,3 @@ from .server_worker import ServerWorker from .client_worker import ClientWorker +from .oglive_worker import ogLiveWorker, ThreadWithResult diff --git a/src/opengnsys/workers/oglive_worker.py b/src/opengnsys/workers/oglive_worker.py new file mode 100644 index 0000000..e09b74e --- /dev/null +++ b/src/opengnsys/workers/oglive_worker.py @@ -0,0 +1,177 @@ +# -*- 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 subprocess +import threading + +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: + 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 + +class ogLiveWorker(ServerWorker): + thread_list = {} + + def interfaceAdmin (self, method, parametros=[]): + exe = '{}/{}'.format (self.pathinterface, method) + ## for development only. Will be removed when the referenced bash code (/opt/opengnsys/lib/engine/bin/*.lib) is translated into python + devel_bash_prefix = ''' + PATH=/opt/opengnsys/scripts/:$PATH; + 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', '{} set -x; bash -x {} {}; set +x'.format (devel_bash_prefix, exe, ' '.join (parametros))] + else: + proc = ['bash', '-c', '{} set -x; bash -x {}; set +x'.format (devel_bash_prefix, exe)] + logger.debug ('subprocess.run ("{}", capture_output=True)'.format (proc)) + p = subprocess.run (proc, capture_output=True) + ## DEBUG + logger.info (f'stdout follows:') + for l in p.stdout.strip().decode ('utf-8').splitlines(): + logger.info (f' {l}') + logger.info (f'stderr follows:') + for l in p.stderr.strip().decode ('utf-8').splitlines(): + logger.info (f' {l}') + ## /DEBUG + if 0 != p.returncode: + cmd_txt = ' '.join (proc) + logger.error (f'command ({cmd_txt}) failed, stderr follows:') + for l in p.stderr.strip().decode ('utf-8').splitlines(): + logger.error (f' {l}') + raise Exception (f'command ({cmd_txt}) failed, see log for details') + return p.stdout.strip().decode ('utf-8') + + 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 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 ('No se ha podido establecer conexión con el Servidor de Administración') ## Error de conexión con el servidor + logger.debug (f'res ({res})') + logger.error ('Error al enviar trama ***send() fallo') + return False + + return res + + def cargaPaginaWeb (self, url=None): + if (not url): url = self.urlMenu + os.system ('pkill -9 browser') + + p = subprocess.Popen (['/opt/opengnsys/bin/browser', '-qws', url]) + try: + p.wait (2) ## if the process dies before 2 seconds... + logger.error ('Error al ejecutar la llamada a la interface de administración') + logger.error ('Error en la creación del proceso hijo') + logger.error ('return code "{}"'.format (p.returncode)) + return False + except subprocess.TimeoutExpired: + pass + + 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 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.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 + + 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') + except NoOptionError as e: + logger.error ("Configuration error: {}".format (e)) + raise e + logger.setLevel (loglevel) + self.REST = REST (url) + + if not self.tomaIPlocal(): + raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo') diff --git a/src/opengnsys/workers/server_worker.py b/src/opengnsys/workers/server_worker.py index b002655..57c913f 100644 --- a/src/opengnsys/workers/server_worker.py +++ b/src/opengnsys/workers/server_worker.py @@ -96,8 +96,8 @@ class ServerWorker(object): return self.process(getParams, postParams, server) try: operation = getattr(self, 'process_' + path[0]) - except Exception: - raise Exception('Message processor for "{}" not found'.format(path[0])) + except: + raise Exception ({ '_httpcode': 404, '_msg': f'{path[0]}: method not found' }) return operation(path[1:], getParams, postParams, server)