# -*- 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 time import random 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 = {} 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, result): logger.debug (f'notifier() called, result ({result})') res = self.REST.sendMessage ('/'.join ([self.name, 'callback']), result) def mon (self): while True: #print ('mon(): iterating') for k in self.thread_list: elem = self.thread_list[k] if 'thread' not in elem: continue logger.debug (f'considering thread ({k})') try: elem['thread'].join (0.05) except RuntimeError: pass ## race condition: a thread is created and this code runs before it is start()ed 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 (elem['result']) time.sleep (1) 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', '{} bash -x {} {}'.format (devel_bash_prefix, exe, ' '.join (parametros))] else: proc = ['bash', '-c', '{} bash -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 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 ('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 ## 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] ## XXX return cmd def cargaPaginaWeb (self, url=None): if (not url): url = self.urlMenu os.system ('pkill -9 OGBrowser') p = subprocess.Popen (['/usr/bin/OGBrowser', '-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 cfg2obj (self, cfg): obj = [] ptrPar = cfg.split ('\n') for line in ptrPar: elem = {} ptrCfg = line.split ('\t') for item in ptrCfg: k, v = item.split ('=') 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 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') 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))) 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 }