source: ogAgent-Git/src/opengnsys/workers/oglive_worker.py @ 85dd23f

browser-nuevodecorare-oglive-methodsexec-ogbrowserfix-urlfixes-winlgromero-filebeatlog-sess-lenmainno-ptt-paramno-tlsogcore1oggitoglogoglog2override-moduleping1ping2ping3ping4report-progresssched-tasktlstls-again
Last change on this file since 85dd23f was 1d89f6d, checked in by Natalia Serrano <natalia.serrano@…>, 7 months ago

refs #973 run new browser

  • Property mode set to 100644
File size: 14.8 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (c) 2024 Qindel Formación y Servicios S.L.
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without modification,
7# are permitted provided that the following conditions are met:
8#
9#    * Redistributions of source code must retain the above copyright notice,
10#      this list of conditions and the following disclaimer.
11#    * Redistributions in binary form must reproduce the above copyright notice,
12#      this list of conditions and the following disclaimer in the documentation
13#      and/or other materials provided with the distribution.
14#    * Neither the name of Virtual Cable S.L. nor the names of its contributors
15#      may be used to endorse or promote products derived from this software
16#      without specific prior written permission.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28"""
29@author: Natalia Serrano, nserrano at qindel dot com
30"""
31# pylint: disable=unused-wildcard-import,wildcard-import
32
33import os
34import time
35import random
36import subprocess
37import threading
38
39from configparser import NoOptionError
40from opengnsys import REST
41from opengnsys.log import logger
42from .server_worker import ServerWorker
43
44## https://stackoverflow.com/questions/6893968/how-to-get-the-return-value-from-a-thread
45class ThreadWithResult (threading.Thread):
46    def run (self):
47        try:
48            self.result = None
49            if self._target is not None:
50                try:
51                    self.result = self._target (*self._args, **self._kwargs)
52                except Exception as e:
53                    self.result = { 'res': 2, 'der': f'got exception: ({e})' }    ## res=2 as defined in ogAdmClient.c:2048
54        finally:
55            # Avoid a refcycle if the thread is running a function with an argument that has a member that points to the thread.
56            del self._target, self._args, self._kwargs
57
58class ogLiveWorker(ServerWorker):
59    thread_list = {}
60
61    tbErroresScripts = [
62        "Se han generado errores desconocidos. No se puede continuar la ejecución de este módulo",        ## 0
63                "001-Formato de ejecución incorrecto.",
64                "002-Fichero o dispositivo no encontrado",
65                "003-Error en partición de disco",
66                "004-Partición o fichero bloqueado",
67                "005-Error al crear o restaurar una imagen",
68                "006-Sin sistema operativo",
69                "007-Programa o función BOOLEAN no ejecutable",
70                "008-Error en la creación del archivo de eco para consola remota",
71                "009-Error en la lectura del archivo temporal de intercambio",
72                "010-Error al ejecutar la llamada a la interface de administración",
73                "011-La información retornada por la interface de administración excede de la longitud permitida",
74                "012-Error en el envío de fichero por la red",
75                "013-Error en la creación del proceso hijo",
76                "014-Error de escritura en destino",
77                "015-Sin Cache en el Cliente",
78                "016-No hay espacio en la cache para almacenar fichero-imagen",
79                "017-Error al Reducir el Sistema Archivos",
80                "018-Error al Expandir el Sistema Archivos",
81                "019-Valor fuera de rango o no válido.",
82                "020-Sistema de archivos desconocido o no se puede montar",
83                "021-Error en partición de caché local",
84                "022-El disco indicado no contiene una particion GPT",
85                "023-Error no definido",
86                "024-Error no definido",
87                "025-Error no definido",
88                "026-Error no definido",
89                "027-Error no definido",
90                "028-Error no definido",
91                "029-Error no definido",
92                "030-Error al restaurar imagen - Imagen mas grande que particion",
93                "031-Error al realizar el comando updateCache",
94                "032-Error al formatear",
95                "033-Archivo de imagen corrupto o de otra versión de partclone",
96                "034-Error no definido",
97                "035-Error no definido",
98                "036-Error no definido",
99                "037-Error no definido",
100                "038-Error no definido",
101                "039-Error no definido",
102                "040-Error imprevisto no definido",
103                "041-Error no definido",
104                "042-Error no definido",
105                "043-Error no definido",
106                "044-Error no definido",
107                "045-Error no definido",
108                "046-Error no definido",
109                "047-Error no definido",
110                "048-Error no definido",
111                "049-Error no definido",
112                "050-Error en la generación de sintaxis de transferenica unicast",
113                "051-Error en envio UNICAST de una particion",
114                "052-Error en envio UNICAST de un fichero",
115                "053-Error en la recepcion UNICAST de una particion",
116                "054-Error en la recepcion UNICAST de un fichero",
117                "055-Error en la generacion de sintaxis de transferenica Multicast",
118                "056-Error en envio MULTICAST de un fichero",
119                "057-Error en la recepcion MULTICAST de un fichero",
120                "058-Error en envio MULTICAST de una particion",
121                "059-Error en la recepcion MULTICAST de una particion",
122                "060-Error en la conexion de una sesion UNICAST|MULTICAST con el MASTER",
123                "061-Error no definido",
124                "062-Error no definido",
125                "063-Error no definido",
126                "064-Error no definido",
127                "065-Error no definido",
128                "066-Error no definido",
129                "067-Error no definido",
130                "068-Error no definido",
131                "069-Error no definido",
132                "070-Error al montar una imagen sincronizada.",
133                "071-Imagen no sincronizable (es monolitica).",
134                "072-Error al desmontar la imagen.",
135                "073-No se detectan diferencias entre la imagen basica y la particion.",
136                "074-Error al sincronizar, puede afectar la creacion/restauracion de la imagen.",
137                "Error desconocido",
138        ]
139
140    def notifier (self, result):
141        logger.debug (f'notifier() called, result ({result})')
142        res = self.REST.sendMessage ('/'.join ([self.name, 'callback']), result)
143
144    def mon (self):
145        while True:
146            #print ('mon(): iterating')
147            for k in self.thread_list:
148                elem = self.thread_list[k]
149                if 'thread' not in elem: continue
150                logger.debug (f'considering thread ({k})')
151                try: elem['thread'].join (0.05)
152                except RuntimeError: pass       ## race condition: a thread is created and this code runs before it is start()ed
153                if not elem['thread'].is_alive():
154                    logger.debug (f'is no longer alive, k ({k}) thread ({elem["thread"]})')
155                    elem['running'] = False
156                    elem['result'] = elem['thread'].result
157                    del elem['thread']
158                    self.notifier (elem['result'])
159
160            time.sleep (1)
161
162    def interfaceAdmin (self, method, parametros=[]):
163        exe = '{}/{}'.format (self.pathinterface, method)
164        ## for development only. Will be removed when the referenced bash code (/opt/opengnsys/lib/engine/bin/*.lib) is translated into python
165        devel_bash_prefix = '''
166            PATH=/opt/opengnsys/scripts/:$PATH;
167            for I in /opt/opengnsys/lib/engine/bin/*.lib; do source $I; done;
168            for i in $(declare -F |cut -f3 -d" "); do export -f $i; done;
169        '''
170
171        if parametros:
172            proc = ['bash', '-c', '{} bash -x {} {}'.format (devel_bash_prefix, exe, ' '.join (parametros))]
173        else:
174            proc = ['bash', '-c', '{} bash -x {}'.format (devel_bash_prefix, exe)]
175        logger.debug ('subprocess.run ("{}", capture_output=True)'.format (proc))
176        p = subprocess.run (proc, capture_output=True)
177        ## DEBUG
178        logger.info (f'stdout follows:')
179        for l in p.stdout.strip().decode ('utf-8').splitlines():
180            logger.info (f'  {l}')
181        logger.info (f'stderr follows:')
182        for l in p.stderr.strip().decode ('utf-8').splitlines():
183            logger.info (f'  {l}')
184        ## /DEBUG
185        if 0 != p.returncode:
186            cmd_txt = ' '.join (proc)
187            logger.error (f'command ({cmd_txt}) failed, stderr follows:')
188            for l in p.stderr.strip().decode ('utf-8').splitlines():
189                logger.error (f'  {l}')
190            raise Exception (f'command ({cmd_txt}) failed, see log for details')
191        return p.stdout.strip().decode ('utf-8')
192
193    def tomaIPlocal (self):
194        try:
195            self.IPlocal = self.interfaceAdmin ('getIpAddress')
196        except Exception as e:
197            logger.error (e)
198            logger.error ('No se ha podido recuperar la dirección IP del cliente')
199            return False
200        logger.info ('local IP is "{}"'.format (self.IPlocal))
201        return True
202
203    def tomaMAClocal (self):
204        ## tomaIPlocal() calls interfaceAdm('getIpAddress')
205        ## getIpAddress runs 'ip addr show' and returns the IP address of every network interface except "lo"
206        ## (ie. breaks badly if there's more than one network interface)
207        ## let's make the same assumptions here
208        mac = subprocess.run (["ip -json link show |jq -r '.[] |select (.ifname != \"lo\") |.address'"], shell=True, capture_output=True, text=True)
209        self.mac = mac.stdout.strip()
210        return True
211
212    def enviaMensajeServidor (self, path, obj={}):
213        obj['iph'] = self.IPlocal          ## Ip del ordenador
214        obj['ido'] = self.idordenador      ## Identificador del ordenador
215        obj['npc'] = self.nombreordenador  ## Nombre del ordenador
216        obj['idc'] = self.idcentro         ## Identificador del centro
217        obj['ida'] = self.idaula           ## Identificador del aula
218
219        res = self.REST.sendMessage ('/'.join ([self.name, path]), obj)
220
221        if (type (res) is not dict):
222            #logger.error ('No se ha podido establecer conexión con el Servidor de Administración')   ## Error de conexión con el servidor
223            logger.debug (f'res ({res})')
224            logger.error ('Error al enviar trama ***send() fallo')
225            return False
226
227        return res
228
229    ## en C, esto envia una trama de respuesta al servidor. Devuelve un boolean
230    ## 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
231    def respuestaEjecucionComando (self, cmd, herror, ids=None):
232        if ids:                 ## Existe seguimiento
233            cmd['ids'] = ids    ## Añade identificador de la sesión
234
235        if 0 == herror:  ## el comando terminó con resultado satisfactorio
236            cmd['res'] = 1
237            cmd['der'] = ''
238        else:            ## el comando tuvo algún error
239            cmd['res'] = 2
240            cmd['der'] = self.tbErroresScripts[herror]           ## XXX
241
242        return cmd
243
244    def cargaPaginaWeb (self, url=None):
245        if (not url): url = self.urlMenu
246        os.system ('pkill -9 OGBrowser')
247
248        p = subprocess.Popen (['/usr/bin/OGBrowser', '-qws', url])
249        try:
250            p.wait (2)       ## if the process dies before 2 seconds...
251            logger.error ('Error al ejecutar la llamada a la interface de administración')
252            logger.error ('Error en la creación del proceso hijo')
253            logger.error ('return code "{}"'.format (p.returncode))
254            return False
255        except subprocess.TimeoutExpired:
256            pass
257
258        return True
259
260    def muestraMenu (self):
261        self.cargaPaginaWeb()
262
263    def muestraMensaje (self, idx):
264        self.cargaPaginaWeb (f'{self.urlMsg}?idx={idx}')
265
266    def LeeConfiguracion (self):
267        try:
268            parametroscfg = self.interfaceAdmin ('getConfiguration')   ## Configuración de los Sistemas Operativos del cliente
269        except Exception as e:
270            logger.error (e)
271            logger.error ('No se ha podido recuperar la dirección IP del cliente')
272            return None
273        logger.debug ('parametroscfg ({})'.format (parametroscfg))
274        return parametroscfg
275
276    def cfg2obj (self, cfg):
277        obj = []
278        ptrPar = cfg.split ('\n')
279        for line in ptrPar:
280            elem = {}
281            ptrCfg = line.split ('\t')
282
283            for item in ptrCfg:
284                k, v = item.split ('=')
285                elem[k] = v
286
287            obj.append (elem)
288
289        return obj
290
291    def onActivation (self):
292        if not os.path.exists ('/scripts/oginit'):
293            ## no estamos en oglive, este modulo no debe cargarse
294            ## esta lógica la saco de src/opengnsys/linux/operations.py, donde hay un if similar
295            raise Exception ('Refusing to load within an operating system')
296
297        self.pathinterface   = None
298        self.IPlocal         = None     ## Ip del ordenador
299        self.mac             = None     ## MAC del ordenador
300        self.idordenador     = None     ## Identificador del ordenador
301        self.nombreordenador = None     ## Nombre del ordenador
302        self.cache           = None
303        self.idproautoexec   = None
304        self.idcentro        = None     ## Identificador del centro
305        self.idaula          = None     ## Identificador del aula
306
307        try:
308            url                = self.service.config.get (self.name, 'remote')
309            loglevel           = self.service.config.get (self.name, 'log')
310            self.pathinterface = self.service.config.get (self.name, 'pathinterface')
311            self.urlMenu       = self.service.config.get (self.name, 'urlMenu')
312            self.urlMsg        = self.service.config.get (self.name, 'urlMsg')
313        except NoOptionError as e:
314            logger.error ("Configuration error: {}".format (e))
315            raise e
316        logger.setLevel (loglevel)
317        self.REST = REST (url)
318
319        if not self.tomaIPlocal():
320            raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo')
321
322        if not self.tomaMAClocal():
323            raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo')
324
325        threading.Thread (name='monitoring_thread', target=self.mon, daemon=True).start()
326
327    def _long_running_job (self, name, f, args):
328        any_job_running = False
329        for k in self.thread_list:
330            if self.thread_list[k]['running']:
331                any_job_running = True
332                break
333        if any_job_running:
334            logger.info ('some job is already running, refusing to launch another one')
335            return { 'job_id': None, 'message': 'some job is already running, refusing to launch another one' }
336
337        job_id = '{}-{}'.format (name, ''.join (random.choice ('0123456789abcdef') for _ in range (8)))
338        self.thread_list[job_id] = {
339            'thread': ThreadWithResult (target=f, args=args),
340            'starttime': time.time(),
341            'running': True,
342            'result': None
343        }
344        self.thread_list[job_id]['thread'].start()
345        return { 'job_id': job_id }
Note: See TracBrowser for help on using the repository browser.