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

browser-nuevodecorare-oglive-methodsexec-ogbrowserfix-urllog-sess-lenmainno-ptt-paramno-tlsogagentuser-sigtermoggitoggit-notlsoglogoglog2override-moduleping1ping2ping3ping4report-progresssched-tasktlstls-again
Last change on this file since f8d6706 was f8d6706, checked in by Natalia Serrano <natalia.serrano@…>, 4 months ago

refs #1461 keep track of RestaurarImagen? unicast

  • Property mode set to 100644
File size: 24.3 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 re
35import time
36import random
37import subprocess
38import threading
39import signal
40
41from configparser import NoOptionError
42from opengnsys import REST
43from opengnsys.log import logger
44from .server_worker import ServerWorker
45
46## https://stackoverflow.com/questions/6893968/how-to-get-the-return-value-from-a-thread
47class ThreadWithResult (threading.Thread):
48    def run (self):
49        try:
50            self.result = None
51            if self._target is not None:
52                ## the first arg in self._args is the queue
53                self.pid_q    = self._args[0]
54                self.stdout_q = self._args[1]
55                self._args    = self._args[2:]
56                try:
57                    self.result = self._target (*self._args, **self._kwargs)
58                except Exception as e:
59                    self.result = { 'res': 2, 'der': f'got exception: ({e})' }    ## res=2 as defined in ogAdmClient.c:2048
60        finally:
61            # Avoid a refcycle if the thread is running a function with an argument that has a member that points to the thread.
62            del self._target, self._args, self._kwargs, self.pid_q, self.stdout_q
63
64class ogLiveWorker(ServerWorker):
65    thread_list = {}
66    thread_lock = threading.Lock()
67
68    tbErroresScripts = [
69        "Se han generado errores desconocidos. No se puede continuar la ejecución de este módulo",        ## 0
70                "001-Formato de ejecución incorrecto.",
71                "002-Fichero o dispositivo no encontrado",
72                "003-Error en partición de disco",
73                "004-Partición o fichero bloqueado",
74                "005-Error al crear o restaurar una imagen",
75                "006-Sin sistema operativo",
76                "007-Programa o función BOOLEAN no ejecutable",
77                "008-Error en la creación del archivo de eco para consola remota",
78                "009-Error en la lectura del archivo temporal de intercambio",
79                "010-Error al ejecutar la llamada a la interface de administración",
80                "011-La información retornada por la interface de administración excede de la longitud permitida",
81                "012-Error en el envío de fichero por la red",
82                "013-Error en la creación del proceso hijo",
83                "014-Error de escritura en destino",
84                "015-Sin Cache en el Cliente",
85                "016-No hay espacio en la cache para almacenar fichero-imagen",
86                "017-Error al Reducir el Sistema Archivos",
87                "018-Error al Expandir el Sistema Archivos",
88                "019-Valor fuera de rango o no válido.",
89                "020-Sistema de archivos desconocido o no se puede montar",
90                "021-Error en partición de caché local",
91                "022-El disco indicado no contiene una particion GPT",
92                "023-Error no definido",
93                "024-Error no definido",
94                "025-Error no definido",
95                "026-Error no definido",
96                "027-Error no definido",
97                "028-Error no definido",
98                "029-Error no definido",
99                "030-Error al restaurar imagen - Imagen mas grande que particion",
100                "031-Error al realizar el comando updateCache",
101                "032-Error al formatear",
102                "033-Archivo de imagen corrupto o de otra versión de partclone",
103                "034-Error no definido",
104                "035-Error no definido",
105                "036-Error no definido",
106                "037-Error no definido",
107                "038-Error no definido",
108                "039-Error no definido",
109                "040-Error imprevisto no definido",
110                "041-Error no definido",
111                "042-Error no definido",
112                "043-Error no definido",
113                "044-Error no definido",
114                "045-Error no definido",
115                "046-Error no definido",
116                "047-Error no definido",
117                "048-Error no definido",
118                "049-Error no definido",
119                "050-Error en la generación de sintaxis de transferenica unicast",
120                "051-Error en envio UNICAST de una particion",
121                "052-Error en envio UNICAST de un fichero",
122                "053-Error en la recepcion UNICAST de una particion",
123                "054-Error en la recepcion UNICAST de un fichero",
124                "055-Error en la generacion de sintaxis de transferenica Multicast",
125                "056-Error en envio MULTICAST de un fichero",
126                "057-Error en la recepcion MULTICAST de un fichero",
127                "058-Error en envio MULTICAST de una particion",
128                "059-Error en la recepcion MULTICAST de una particion",
129                "060-Error en la conexion de una sesion UNICAST|MULTICAST con el MASTER",
130                "061-Error no definido",
131                "062-Error no definido",
132                "063-Error no definido",
133                "064-Error no definido",
134                "065-Error no definido",
135                "066-Error no definido",
136                "067-Error no definido",
137                "068-Error no definido",
138                "069-Error no definido",
139                "070-Error al montar una imagen sincronizada.",
140                "071-Imagen no sincronizable (es monolitica).",
141                "072-Error al desmontar la imagen.",
142                "073-No se detectan diferencias entre la imagen basica y la particion.",
143                "074-Error al sincronizar, puede afectar la creacion/restauracion de la imagen.",
144                "Error desconocido",
145        ]
146
147    def notifier (self, job_id, result):
148        result['job_id'] = job_id
149        self.REST.sendMessage ('clients/status/webhook', result)
150
151    def killer (self, job_id):
152        logger.debug (f'killer() called, job_id ({job_id})')
153        if job_id not in self.thread_list: return { 'res': 2, 'der': 'Unknown job' }
154
155        with self.thread_lock:
156            if 'thread' not in self.thread_list[job_id]: return { 'res': 2, 'der': 'Job is not running' }
157            t   = self.thread_list[job_id]['thread']
158            pid = self.thread_list[job_id]['child_pid']
159            logger.debug (f'pid ({pid})')
160            try_times = 8
161            sig = signal.SIGTERM
162            msg = f'could not kill pid ({pid}) after ({try_times}) tries'
163            success = 2    ## mimic cmd['res'] in respuestaEjecucionComando(): "1" means success, "2" means failed
164            while True:
165                t.join (0.05)
166                if not t.is_alive():
167                    msg = 'job terminated'
168                    success = 1
169                    logger.debug (msg)
170                    self.thread_list[job_id]['child_pid'] = None
171                    break
172                ## 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'.
173                ## 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.
174                if pid:
175                    if os.path.exists (f'/proc/{pid}'):
176                        logger.debug (f'sending signal ({sig}) to pid ({pid})')
177                        ## if the process finishes just here, nothing happens: the signal is sent to the void
178                        os.kill (pid, sig)
179                        #subprocess.run (['kill', '--signal', str(sig), str(pid)])
180                    else:
181                        msg = f'pid ({pid}) is gone, nothing to kill'
182                        success = 1
183                        logger.debug (msg)
184                        self.thread_list[job_id]['child_pid'] = None
185                        break
186                else:
187                    msg = 'no PID to kill'
188                    logger.debug (msg)
189
190                if not try_times: break
191                if 4 == try_times: sig = signal.SIGKILL   ## change signal after a few tries
192                try_times -= 1
193                time.sleep (0.4)
194
195        return { 'res':success, 'der':msg }
196
197    def _extract_progress (self, job_id, ary=[]):
198        is_create = is_restore = False
199        if job_id not in self.progress_jobs: self.progress_jobs[job_id] = { 'restore_with_cache': False }
200        if 'CrearImagen-' in job_id:
201            is_create = True
202        elif 'RestaurarImagen-' in job_id:
203            is_restore = True
204        else:
205            return None
206
207        progress = None
208        for i in ary:
209            if is_restore:
210                ## RestaurarImagen:
211                ## - si nos traemos la imagen a cache por unicast, se llama en última instancia a rsync:
212                ##     - '         32,77K   0%    0,00kB/s    0:00:00'
213                ##     - '         11,57M   0%    8,77MB/s    0:06:16'
214                ##     - '          1,03G  30%   11,22MB/s    0:03:25'
215                ##     - '          3,39G 100%   11,22MB/s    0:04:48 (xfr#1, to-chk=0/1)'
216                if m := re.search (r'\d+,\d+[KMGT]\s+(\d+)%.*[kM]B/s', i):
217                    progress = float (m.groups()[0]) / 100
218                    progress /= 2
219                    self.progress_jobs[job_id]['restore_with_cache'] = True
220
221                ## - si nos traemos la imagen a cache por multicast:
222                #elif regex:
223                    #TODO
224                    #progress =/ 2
225                    #self.progress_jobs[job_id]['restore_with_cache'] = True
226
227                ## - si nos traemos la imagen a cache por torrent:
228                #elif regex:
229                    #TODO
230                    #progress =/ 2
231                    #self.progress_jobs[job_id]['restore_with_cache'] = True
232
233                ## - tanto si nos la hemos traído a cache como si no, pasamos la imagen a la partición:
234                ##     - 'Current block:     720646, Total block:    1750078, Complete:  41,18%'
235                ##     - 'Elapsed: 00:00:20, Remaining: 00:00:15, Completed:  57,06%,   9,81GB/min,'
236                ##     - 'Current block:    1606658, Total block:    1750078, Complete: 100.00%'
237                ##     - 'Elapsed: 00:00:36, Remaining: 00:00:00, Completed: 100.00%, Rate:   9,56GB/min,'
238                elif m := re.search (r'Current block:.*Complete:\s+(\d+[,.]\d+)%', i):
239                    progress = float (m.groups()[0].replace (',', '.')) / 100
240                    if self.progress_jobs[job_id]['restore_with_cache']:
241                        progress /= 2
242                        progress += 0.5
243            elif is_create:
244                pass
245        if progress and progress > 1: progress = 1
246        return progress
247
248    def mon (self):
249        while True:
250            with self.thread_lock:
251                for k in self.thread_list:
252                    elem = self.thread_list[k]
253                    if 'thread' not in elem: continue
254                    logger.debug (f'considering thread ({k})')
255
256                    if self.pid_q:
257                        if not self.pid_q.empty():
258                            elem['child_pid'] = self.pid_q.get()
259                            logger.debug (f'queue not empty, got pid ({elem["child_pid"]})')
260
261                    if self.stdout_q:
262                        partial = ''
263                        while not self.stdout_q.empty():
264                            partial += self.stdout_q.get()
265                        lines = partial.splitlines()
266                        if len (lines):
267                            p = self._extract_progress (k, lines)
268                            if p:
269                                m = { "job_id": k, "progress": p }
270                                logger.debug (f'would sendMessage ({m})')
271                                #self.REST.sendMessage ('clients/status/webhook', { "job_id": "EjecutarScript-333feb3f", "progress": 0.91337824 })
272
273                    elem['thread'].join (0.05)
274                    if not elem['thread'].is_alive():
275                        logger.debug (f'is no longer alive, k ({k}) thread ({elem["thread"]})')
276                        elem['running'] = False
277                        elem['result'] = elem['thread'].result
278                        del elem['thread']
279                        self.notifier (k, elem['result'])
280
281            time.sleep (1)
282
283    def interfaceAdmin (self, method, parametros=[]):
284        exe = '{}/{}'.format (self.pathinterface, method)
285        ## for development only. Will be removed when the referenced bash code (/opt/opengnsys/lib/engine/bin/*.lib) is translated into python
286        devel_bash_prefix = '''
287            PATH=/opt/opengnsys/scripts/:$PATH;
288            for I in /opt/opengnsys/lib/engine/bin/*.lib; do source $I; done;
289            for i in $(declare -F |cut -f3 -d" "); do export -f $i; done;
290        '''
291
292        if parametros:
293            proc = ['bash', '-c', '{} {} {}'.format (devel_bash_prefix, exe, ' '.join (parametros))]
294        else:
295            proc = ['bash', '-c', '{} {}'.format (devel_bash_prefix, exe)]
296        logger.debug ('subprocess.run ("{}", capture_output=True)'.format (proc))
297
298        p = subprocess.Popen (proc, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
299        if self.pid_q:
300            self.pid_q.put (p.pid)
301        else:
302            ## esto sucede por ejemplo cuando arranca el agente, que estamos en interfaceAdmin() en el mismo hilo, sin _long_running_job ni hilo separado
303            logger.debug ('no queue--not writing any PID to it')
304
305        sout = serr = ''
306        while p.poll() is None:
307            for l in iter (p.stdout.readline, b''):
308                partial = l.decode ('utf-8', 'ignore')
309                if self.stdout_q: self.stdout_q.put (partial)
310                sout += partial
311            for l in iter (p.stderr.readline, b''):
312                partial = l.decode ('utf-8', 'ignore')
313                serr += partial
314            time.sleep (1)
315        sout = sout.strip()
316        serr = serr.strip()
317
318        ## DEBUG
319        logger.info (f'stdout follows:')
320        for l in sout.splitlines():
321            logger.info (f'  {l}')
322        logger.info (f'stderr follows:')
323        for l in serr.splitlines():
324            logger.info (f'  {l}')
325        ## /DEBUG
326        if 0 != p.returncode:
327            cmd_txt = ' '.join (proc)
328            logger.error (f'command ({cmd_txt}) failed, stderr follows:')
329            for l in serr.splitlines():
330                logger.error (f'  {l}')
331            raise Exception (f'command ({cmd_txt}) failed, see log for details')
332        return sout
333
334    def tomaIPlocal (self):
335        try:
336            self.IPlocal = self.interfaceAdmin ('getIpAddress')
337        except Exception as e:
338            logger.error (e)
339            logger.error ('No se ha podido recuperar la dirección IP del cliente')
340            return False
341        logger.info ('local IP is "{}"'.format (self.IPlocal))
342        return True
343
344    def tomaMAClocal (self):
345        ## tomaIPlocal() calls interfaceAdm('getIpAddress')
346        ## getIpAddress runs 'ip addr show' and returns the IP address of every network interface except "lo"
347        ## (ie. breaks badly if there's more than one network interface)
348        ## let's make the same assumptions here
349        mac = subprocess.run (["ip -json link show |jq -r '.[] |select (.ifname != \"lo\") |.address'"], shell=True, capture_output=True, text=True)
350        self.mac = mac.stdout.strip()
351        return True
352
353    def enviaMensajeServidor (self, path, obj={}):
354        obj['iph'] = self.IPlocal          ## Ip del ordenador
355        obj['ido'] = self.idordenador      ## Identificador del ordenador
356        obj['npc'] = self.nombreordenador  ## Nombre del ordenador
357        obj['idc'] = self.idcentro         ## Identificador del centro
358        obj['ida'] = self.idaula           ## Identificador del aula
359
360        res = self.REST.sendMessage ('/'.join ([self.name, path]), obj)
361
362        if (type (res) is not dict):
363            #logger.error ('No se ha podido establecer conexión con el Servidor de Administración')   ## Error de conexión con el servidor
364            logger.debug (f'res ({res})')
365            logger.error ('Error al enviar trama ***send() fallo')
366            return False
367
368        return res
369
370    ## en C, esto envia una trama de respuesta al servidor. Devuelve un boolean
371    ## 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
372    def respuestaEjecucionComando (self, cmd, herror, ids=None):
373        if ids:                 ## Existe seguimiento
374            cmd['ids'] = ids    ## Añade identificador de la sesión
375
376        if 0 == herror:  ## el comando terminó con resultado satisfactorio
377            cmd['res'] = 1
378            cmd['der'] = ''
379        else:            ## el comando tuvo algún error
380            cmd['res'] = 2
381            cmd['der'] = self.tbErroresScripts[herror]
382
383        return cmd
384
385    def cargaPaginaWeb (self, url=None):
386        if (not url): url = self.urlMenu
387        os.system ('pkill -9 browser')
388
389        p = subprocess.Popen (['/usr/bin/browser', '-qws', url])
390        try:
391            p.wait (2)       ## if the process dies before 2 seconds...
392            logger.error ('Error al ejecutar la llamada a la interface de administración')
393            logger.error ('Error en la creación del proceso hijo')
394            logger.error ('return code "{}"'.format (p.returncode))
395            return False
396        except subprocess.TimeoutExpired:
397            pass
398
399        return True
400
401    def muestraMenu (self):
402        self.cargaPaginaWeb()
403
404    def muestraMensaje (self, idx):
405        self.cargaPaginaWeb (f'{self.urlMsg}?idx={idx}')
406
407    def LeeConfiguracion (self):
408        try:
409            parametroscfg = self.interfaceAdmin ('getConfiguration')   ## Configuración de los Sistemas Operativos del cliente
410        except Exception as e:
411            logger.error (e)
412            logger.error ('No se ha podido recuperar la dirección IP del cliente')
413            return None
414        logger.debug ('parametroscfg ({})'.format (parametroscfg))
415        return parametroscfg
416
417    def cfg2obj (self, cfg):
418        obj = []
419        ptrPar = cfg.split ('\n')
420        for line in ptrPar:
421            elem = {}
422            ptrCfg = line.split ('\t')
423
424            for item in ptrCfg:
425                k, v = item.split ('=')
426                elem[k] = v
427
428            obj.append (elem)
429
430        return obj
431
432    def onActivation (self):
433        if not os.path.exists ('/scripts/oginit'):
434            ## no estamos en oglive, este modulo no debe cargarse
435            ## esta lógica la saco de src/opengnsys/linux/operations.py, donde hay un if similar
436            raise Exception ('Refusing to load within an operating system')
437
438        self.pathinterface   = None
439        self.IPlocal         = None     ## Ip del ordenador
440        self.mac             = None     ## MAC del ordenador
441        self.idordenador     = None     ## Identificador del ordenador
442        self.nombreordenador = None     ## Nombre del ordenador
443        self.cache           = None
444        self.idproautoexec   = None
445        self.idcentro        = None     ## Identificador del centro
446        self.idaula          = None     ## Identificador del aula
447        self.pid_q           = None     ## for passing PIDs around
448        self.stdout_q        = None     ## for passing stdout
449        self.progress_jobs   = {}
450
451        ogcore_scheme   = os.environ.get ('OGAGENTCFG_OGCORE_SCHEME',  'https')
452        ogcore_ip       = os.environ.get ('OGAGENTCFG_OGCORE_IP',      '192.168.2.1')
453        ogcore_port     = os.environ.get ('OGAGENTCFG_OGCORE_PORT',    '8443')
454        urlmenu_scheme  = os.environ.get ('OGAGENTCFG_URLMENU_SCHEME', 'https')
455        urlmenu_ip      = os.environ.get ('OGAGENTCFG_URLMENU_IP',     '192.168.2.1')
456        urlmenu_port    = os.environ.get ('OGAGENTCFG_URLMENU_PORT',   '8443')
457        ogcore_ip_port  = ':'.join (map (str, filter (None, [ogcore_ip,  ogcore_port ])))
458        urlmenu_ip_port = ':'.join (map (str, filter (None, [urlmenu_ip, urlmenu_port])))
459        try:
460            url                = self.service.config.get (self.name, 'remote')
461            loglevel           = self.service.config.get (self.name, 'log')
462            self.pathinterface = self.service.config.get (self.name, 'pathinterface')
463            self.urlMenu       = self.service.config.get (self.name, 'urlMenu')
464            self.urlMsg        = self.service.config.get (self.name, 'urlMsg')
465
466            url          = url.format          (ogcore_scheme,  ogcore_ip_port)
467            self.urlMenu = self.urlMenu.format (urlmenu_scheme, urlmenu_ip_port)
468        except NoOptionError as e:
469            logger.error ("Configuration error: {}".format (e))
470            raise e
471        logger.setLevel (loglevel)
472        self.REST = REST (url)
473
474        if not self.tomaIPlocal():
475            raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo')
476
477        if not self.tomaMAClocal():
478            raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo')
479
480        threading.Thread (name='monitoring_thread', target=self.mon, daemon=True).start()
481
482    def _long_running_job (self, name, f, args):
483        any_job_running = False
484        for k in self.thread_list:
485            if self.thread_list[k]['running']:
486                any_job_running = True
487                break
488        if any_job_running:
489            logger.info ('some job is already running, refusing to launch another one')
490            return { 'job_id': None, 'message': 'some job is already running, refusing to launch another one' }
491
492        job_id = '{}-{}'.format (name, ''.join (random.choice ('0123456789abcdef') for _ in range (8)))
493        import queue
494        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
495        self.stdout_q = queue.Queue()
496        self.thread_list[job_id] = {
497            'thread': ThreadWithResult (target=f, args=(self.pid_q, self.stdout_q) + args),
498            'starttime': time.time(),
499            'child_pid': None,
500            'running': True,
501            'result': None
502        }
503        self.thread_list[job_id]['thread'].start()
504        return { 'job_id': job_id }
505
506## para matar threads tengo lo siguiente:
507## - aqui en _long_running_job meto una cola en self.pid_q
508## - (self.pid_q fue inicializado a None al instanciar el objeto, para evitar error "objeto no tiene 'pid_q'")
509## - en el thread_list también tengo un child_pid para almacenar el pid de los procesos hijos
510## - al crear el ThreadWithResult le paso la cola, y luego en run() la recojo y la meto en el self.pid_q del thread
511## - en interfaceAdmin() al hacer subprocess.Popen(), recojo el pid y lo escribo en la queue
512## - en mon() recojo pids de la queue y los meto en thread_list 'child_pid'
513##   - 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
514##     - por ejemplo EjecutarScript llama a interfaceAdmin() y luego llama a LeeConfiguracion() el cual llama a interfaceAdmin() otra vez
515## - y cuando nos llamen a KillJob, terminamos en killer() el cual coge el 'child_pid' y zas
516##   - pero a lo mejor el child ya terminó
517##   - o a lo mejor el KillJob nos llegó demasiado pronto y todavía no hubo ningún child
518##
519## $ curl --insecure -X POST --data '{"nfn":"EjecutarScript","scp":"cd /usr; sleep 30; pwd; ls","ids":"0"}' https://192.168.2.199:8000/ogAdmClient/EjecutarScript
520## {"job_id": "EjecutarScript-333feb3f"}
521## $ curl --insecure -X POST --data '{"job_id":"EjecutarScript-333feb3f"}' https://192.168.2.199:8000/ogAdmClient/KillJob
522##
523## funciona bien, excepto que el PID no muere xD, ni siquiera haciendo subprocess.run('kill')
524
525## para mostrar el progreso de los jobs reutilizo la misma infra
526## una cola self.stdout_q
527## en interfaceAdmin escribo la stdout parcial que ya venia recogiendo
528## mon() lo recoge y le hace un POST a ogcore
Note: See TracBrowser for help on using the repository browser.