From b3399b9d5da6cebfa66ef482462fa9aa116a5553 Mon Sep 17 00:00:00 2001 From: ggil Date: Fri, 26 Jul 2024 13:10:07 +0200 Subject: [PATCH 01/70] refs #521 - Add sendfileUFTP.py --- bin/sendFileUFTP.py | 132 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 bin/sendFileUFTP.py diff --git a/bin/sendFileUFTP.py b/bin/sendFileUFTP.py new file mode 100644 index 0000000..81dbda6 --- /dev/null +++ b/bin/sendFileUFTP.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script envía mediante UFTP la imagen recibida como primer parámetro, al puerto e IP (Multicast o Unicast) especificados en el segundo parámetro, + a la velocidad de transferencia tambíén especificada en el segundo parámetro (la sintaxis de este parámetro es "puerto:ip:bitrate"). +Previamente, los clientes deben haberse puesto a escuchar en la IP Multicast correspondiente (tanto para envíos Multicast como para envíos Unicast). + +- Parámetros: +-------------- +sys.argv[1] - Nombre completo de la imagen a enviar (con o sin ruta) + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/images/image1.img + +sys.argv[2] - Parámetros Multicast/Unicast (en formato "puerto:ip:bitrate") + - Ejemplo1: 9000:239.194.17.2:100M + - Ejemplo2: 9000:192.168.56.101:1G + +- Sintaxis: +./sendFileUFTP.py image_name|/image_path/image_name port:ip:bitrate + +- Ejemplos: +./sendFileUFTP.py image1.img 9000:239.194.17.2:100M +./sendFileUFTP.py /opt/opengnsys/images/image1.img 9000:192.168.56.101:1G +""" + +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess + +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/images/' +#REPO_IFACE = subprocess.getoutput('/opt/opengnsys/bin/getRepoIface') # Para poder ejecutar esto tengo que ejecutar el script con sudo, pero no sé si es necesario (de momento no lo estoy usando) +#print(REPO_IFACE) +log_file = '/opt/opengnsys/images/uftp.log' + +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda (cuando se ejecuta el script con el parámetro "help"). + """ + help_text = f""" + Sintaxis: {script_name} image_name|/image_path/image_name port:ip:bitrate + Ejemplo1: {script_name} image1.img 9000:239.194.17.2:100M + Ejemplo2: {script_name} /opt/opengnsys/images/image1.img 9000:192.168.56.101:1G + """ + print(help_text) + + +# Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: +if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + + +# Si se ejecuta la función con más o menos de 2 parámetros, se muestra un mensaje de error, y se sale del script: +if len(sys.argv) != 3: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 2 parámetros (image_name|/image_path/image_name port:ip:bitrate)") + sys.exit(1) + + +def build_file_path(): + """ Construye la ruta completa al archivo a enviar + (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + if not param_path.startswith(repo_path): + file_path = os.path.join(repo_path, param_path) + else: + file_path = param_path + return file_path + + +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Obtenemos la ruta completa al archivo a enviar: + file_path = build_file_path() + + # Si el fichero no es accesible, devolvermos un error, y salimos del script: + if not os.path.isfile(file_path): + print(f"{script_name} Error: Fichero \"{file_path}\" no accesible") + sys.exit(2) + + # Si en el segundo parámetro no hay 3 elementos (separados por ":"), devolvermos un error, y salimos del script: + params = sys.argv[2].split(':') + if len(params) != 3: + print(f"{script_name} Error: Datos Multicast incorrectos: \"{sys.argv[2]}\" (se debe especificar \"puerto:ip:bitrate\")") + sys.exit(3) + + # Almacenamos los elementos del segundo parámetro en variables: + port, ip, bitrate = params + + # Calculamos el valor de la variable "bitrate", en base a la letra especificada (que luego eliminamos de la variable): + bitrate = bitrate.lower() + if "m" in bitrate: + bitrate = int(bitrate.strip("m")) * 1024 + elif "g" in bitrate: + bitrate = int(bitrate.strip("g")) * 1024 * 1024 + else: + bitrate = int(bitrate.strip("k")) + + # Creamos una lista con el comando a enviar (esto es requerido por la función "subprocess.run"), e impimimos el comando con espacios: + splitted_cmd = f"uftp -M {ip} -p {port} -L {log_file} -Y aes256-cbc -h sha256 -e rsa -c -K 1024 -R {bitrate} {file_path}".split() + + print(f"Sending command: {' '.join(splitted_cmd)}") + + # Ejecutamos el comando en el sistema, e imprimimos el resultado: + try: + result = subprocess.run(splitted_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"ReturnCode: {result.returncode}") + except subprocess.CalledProcessError as error: + print(f"ReturnCode: {error.returncode}") + print(f"Error Output: {error.stderr.decode()}") + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- -- 2.40.1 From 5c37a4600afa69fb7822e77a220291fdbe1bb140 Mon Sep 17 00:00:00 2001 From: ggil Date: Mon, 29 Jul 2024 09:59:17 +0200 Subject: [PATCH 02/70] refs #521 - Modify sendFileUFTP.py --- bin/sendFileUFTP.py | 75 +++++++++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/bin/sendFileUFTP.py b/bin/sendFileUFTP.py index 81dbda6..1514fa9 100644 --- a/bin/sendFileUFTP.py +++ b/bin/sendFileUFTP.py @@ -6,8 +6,10 @@ Este script envía mediante UFTP la imagen recibida como primer parámetro, al p a la velocidad de transferencia tambíén especificada en el segundo parámetro (la sintaxis de este parámetro es "puerto:ip:bitrate"). Previamente, los clientes deben haberse puesto a escuchar en la IP Multicast correspondiente (tanto para envíos Multicast como para envíos Unicast). -- Parámetros: --------------- +Paquetes APT requeridos: "uftp" + + Parámetros +------------ sys.argv[1] - Nombre completo de la imagen a enviar (con o sin ruta) - Ejemplo1: image1.img - Ejemplo2: /opt/opengnsys/images/image1.img @@ -16,33 +18,42 @@ sys.argv[2] - Parámetros Multicast/Unicast (en formato "puerto:ip:bitrate") - Ejemplo1: 9000:239.194.17.2:100M - Ejemplo2: 9000:192.168.56.101:1G -- Sintaxis: + Sintaxis +---------- ./sendFileUFTP.py image_name|/image_path/image_name port:ip:bitrate -- Ejemplos: + Ejemplos + --------- ./sendFileUFTP.py image1.img 9000:239.194.17.2:100M ./sendFileUFTP.py /opt/opengnsys/images/image1.img 9000:192.168.56.101:1G """ +# -------------------------------------------------------------------------------------------- +# IMPORTS # -------------------------------------------------------------------------------------------- import os import sys import subprocess + +# -------------------------------------------------------------------------------------------- +# VARIABLES # -------------------------------------------------------------------------------------------- script_name = os.path.basename(__file__) repo_path = '/opt/opengnsys/images/' -#REPO_IFACE = subprocess.getoutput('/opt/opengnsys/bin/getRepoIface') # Para poder ejecutar esto tengo que ejecutar el script con sudo, pero no sé si es necesario (de momento no lo estoy usando) -#print(REPO_IFACE) log_file = '/opt/opengnsys/images/uftp.log' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS # -------------------------------------------------------------------------------------------- def show_help(): - """ Imprime la ayuda (cuando se ejecuta el script con el parámetro "help"). + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". """ help_text = f""" Sintaxis: {script_name} image_name|/image_path/image_name port:ip:bitrate @@ -52,18 +63,6 @@ def show_help(): print(help_text) -# Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: -if len(sys.argv) == 2 and sys.argv[1] == "help": - show_help() - sys.exit(0) - - -# Si se ejecuta la función con más o menos de 2 parámetros, se muestra un mensaje de error, y se sale del script: -if len(sys.argv) != 3: - print(f"{script_name} Error: Formato incorrecto: Se debe especificar 2 parámetros (image_name|/image_path/image_name port:ip:bitrate)") - sys.exit(1) - - def build_file_path(): """ Construye la ruta completa al archivo a enviar (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). @@ -76,12 +75,39 @@ def build_file_path(): return file_path +def calculate_bitrate(bitrate): + """ Calcula el valor de la variable "bitrate", en base a la letra especificada ("K", "M" o "G"). + Luego elimina la letra y retorna el valor con tipo de datos "int". + """ + bitrate = bitrate.lower() + if "m" in bitrate: + bitrate = int(bitrate.strip("m")) * 1024 + elif "g" in bitrate: + bitrate = int(bitrate.strip("g")) * 1024 * 1024 + else: + bitrate = int(bitrate.strip("k")) + return bitrate + + + +# -------------------------------------------------------------------------------------------- +# MAIN # -------------------------------------------------------------------------------------------- def main(): """ """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + + # Si se ejecuta la función con más o menos de 2 parámetros, se muestra un mensaje de error, y se sale del script: + if len(sys.argv) != 3: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 2 parámetros (image_name|/image_path/image_name port:ip:bitrate)") + sys.exit(1) + # Obtenemos la ruta completa al archivo a enviar: file_path = build_file_path() @@ -100,15 +126,9 @@ def main(): port, ip, bitrate = params # Calculamos el valor de la variable "bitrate", en base a la letra especificada (que luego eliminamos de la variable): - bitrate = bitrate.lower() - if "m" in bitrate: - bitrate = int(bitrate.strip("m")) * 1024 - elif "g" in bitrate: - bitrate = int(bitrate.strip("g")) * 1024 * 1024 - else: - bitrate = int(bitrate.strip("k")) + bitrate = calculate_bitrate(bitrate) - # Creamos una lista con el comando a enviar (esto es requerido por la función "subprocess.run"), e impimimos el comando con espacios: + # Creamos una lista con el comando a enviar (esto es requerido por la función "subprocess.run"), e imprimimos el comando con espacios: splitted_cmd = f"uftp -M {ip} -p {port} -L {log_file} -Y aes256-cbc -h sha256 -e rsa -c -K 1024 -R {bitrate} {file_path}".split() print(f"Sending command: {' '.join(splitted_cmd)}") @@ -124,6 +144,7 @@ def main(): print(f"Se ha producido un error inesperado: {error}") + # -------------------------------------------------------------------------------------------- if __name__ == "__main__": -- 2.40.1 From 98742f31a64e722d43e39504f9d8db6fbfbc82f2 Mon Sep 17 00:00:00 2001 From: ggil Date: Mon, 29 Jul 2024 10:30:33 +0200 Subject: [PATCH 03/70] refs #521 - Modify sendFileUFTP.py --- bin/sendFileUFTP.py | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/bin/sendFileUFTP.py b/bin/sendFileUFTP.py index 1514fa9..5f18752 100644 --- a/bin/sendFileUFTP.py +++ b/bin/sendFileUFTP.py @@ -63,6 +63,26 @@ def show_help(): print(help_text) +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta la función con más o menos de 2 parámetros, se muestra un mensaje de error, y se sale del script: + if len(sys.argv) != 3: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 2 parámetros (image_name|/image_path/image_name port:ip:bitrate)") + sys.exit(1) + # Si en el segundo parámetro no hay 3 elementos (separados por ":"), devolvermos un error, y salimos del script: + param_list = sys.argv[2].split(':') + if len(param_list) != 3: + print(f"{script_name} Error: Datos Multicast incorrectos: \"{sys.argv[2]}\" (se debe especificar \"puerto:ip:bitrate\")") + sys.exit(3) + + def build_file_path(): """ Construye la ruta completa al archivo a enviar (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). @@ -98,16 +118,9 @@ def calculate_bitrate(bitrate): def main(): """ """ - # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: - if len(sys.argv) == 2 and sys.argv[1] == "help": - show_help() - sys.exit(0) - - # Si se ejecuta la función con más o menos de 2 parámetros, se muestra un mensaje de error, y se sale del script: - if len(sys.argv) != 3: - print(f"{script_name} Error: Formato incorrecto: Se debe especificar 2 parámetros (image_name|/image_path/image_name port:ip:bitrate)") - sys.exit(1) - + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + # Obtenemos la ruta completa al archivo a enviar: file_path = build_file_path() @@ -116,14 +129,9 @@ def main(): print(f"{script_name} Error: Fichero \"{file_path}\" no accesible") sys.exit(2) - # Si en el segundo parámetro no hay 3 elementos (separados por ":"), devolvermos un error, y salimos del script: - params = sys.argv[2].split(':') - if len(params) != 3: - print(f"{script_name} Error: Datos Multicast incorrectos: \"{sys.argv[2]}\" (se debe especificar \"puerto:ip:bitrate\")") - sys.exit(3) - # Almacenamos los elementos del segundo parámetro en variables: - port, ip, bitrate = params + param_list = sys.argv[2].split(':') + port, ip, bitrate = param_list # Calculamos el valor de la variable "bitrate", en base a la letra especificada (que luego eliminamos de la variable): bitrate = calculate_bitrate(bitrate) -- 2.40.1 From 66f51cc1e2aee7b49334be55aed60a1291100447 Mon Sep 17 00:00:00 2001 From: ggil Date: Wed, 7 Aug 2024 16:01:40 +0200 Subject: [PATCH 04/70] =?UTF-8?q?refs=20#583=20-=20A=C3=B1adido=20sendFile?= =?UTF-8?q?Mcast.py,=20y=20reestructurados=20los=20directorios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +- py_scripts/sendFileMcast.py | 164 ++++++++++++++++++++++++++++ {bin => py_scripts}/sendFileUFTP.py | 15 ++- 3 files changed, 174 insertions(+), 10 deletions(-) create mode 100644 py_scripts/sendFileMcast.py rename {bin => py_scripts}/sendFileUFTP.py (94%) diff --git a/README.md b/README.md index a206438..639ee04 100644 --- a/README.md +++ b/README.md @@ -4,5 +4,6 @@ OpenGnsys Repository Manager README Este repositorio GIT contiene la estructura de datos del repositorio de datos de OpenGnsys. -- bin: binarios y scripts de gestión del repositorio. -- etc: ficheros o plantillas de configuración del repositorio. \ No newline at end of file +- bin: Binarios y scripts de gestión del repositorio. +- etc: Ficheros o plantillas de configuración del repositorio. +- py_scripts: Scripts en Python 3, algunos de los cuales son traducciones de los scripts bash situados en "bin". \ No newline at end of file diff --git a/py_scripts/sendFileMcast.py b/py_scripts/sendFileMcast.py new file mode 100644 index 0000000..72b267c --- /dev/null +++ b/py_scripts/sendFileMcast.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script envía mediante UDPCast la imagen recibida como primer parámetro, con los datos Multicast especificados en el segundo parámetro. +En principio, debería hacer lo mismo que el script bash original (cuyo nombre es "sendFileMcast", a secas). + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a enviar (con o sin ruta) + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/images/image1.img + +sys.argv[2] - Parámetros Multicast (en formato "Port:Duplex:IP:Mpbs:Nclients:Timeout") + - Ejemplo: 9000:full:239.194.17.2:70M:20:120 + + Sintaxis +---------- +./sendFileMcast.py image_name|/image_path/image_name Port:Duplex:IP:Mpbs:Nclients:Timeout + + Ejemplos + --------- +./sendFileMcast.py image1.img 9000:full:239.194.17.2:70M:20:120 +./sendFileMcast.py /opt/opengnsys/images/image1.img 9000:full:239.194.17.2:70M:20:120 +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/images/' +bin_path = '/opt/opengnsys/bin/' +repo_iface = subprocess.getoutput('/opt/opengnsys/bin/getRepoIface') + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} image_name|/image_path/image_name Port:Duplex:IP:Mpbs:Nclients:Timeout + Ejemplo1: {script_name} image1.img 9000:full-duplex:239.194.17.2:70M:20:120 + Ejemplo2: {script_name} /opt/opengnsys/images/image1.img 9000:full-duplex:239.194.17.2:70M:20:120 + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta la función con más o menos de 2 parámetros, se muestra un mensaje de error, y se sale del script: + elif len(sys.argv) != 3: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 2 parámetros (image_name|/image_path/image_name Port:Duplex:IP:Mpbs:Nclients:Timeout)") + sys.exit(1) + # Si en el segundo parámetro no hay 6 elementos (separados por ":"), se muestra un mensaje de error, y se sale del script: + param_list = sys.argv[2].split(':') + if len(param_list) != 6: + print(f"{script_name} Error: Datos Multicast incorrectos: \"{sys.argv[2]}\" (se debe especificar \"Port:Duplex:IP:Mpbs:Nclients:Timeout\")") + sys.exit(3) + + +def build_file_path(): + """ Construye la ruta completa al archivo a enviar + (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + if not param_path.startswith(repo_path): + file_path = os.path.join(repo_path, param_path) + else: + file_path = param_path + return file_path + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo a enviar: + file_path = build_file_path() + + # Si el fichero no es accesible, devolvermos un error, y salimos del script: + if not os.path.isfile(file_path): + print(f"{script_name} Error: Fichero \"{file_path}\" no accesible") + sys.exit(2) + + # Almacenamos los elementos del segundo parámetro en variables (su formato es "Port:Duplex:IP:Mpbs:Nclients:Timeout"): + param_list = sys.argv[2].split(':') + port, method, ip, bitrate, nclients, maxtime = param_list + + # Retocamos las variables "method" y "bitrate": + method = '--half-duplex' if 'half' in method.lower() else '--full-duplex' + bitrate = bitrate.lower() + + # Creamos la variable "cerror" (no sé que hace, pero estaba en el script original): + cerror = "8x8/128" + + # Creamos una lista con el comando a enviar (esto es requerido por la función "subprocess.run"). + # NOTA: Se desabilita el uso de mbuffer, ya que esta versión del upd-sender no la admite (ya estaba así en el script original). + mbuffer = "" # which mbuffer &> /dev/null && MBUFFER="--pipe 'mbuffer -m 20M'" + splitted_cmd = [ + os.path.join(bin_path, 'udp-sender'), + mbuffer, + '--nokbd', + '--retries-until-drop', '65', + '--portbase', port, + method, + '--interface', repo_iface, + '--mcast-data-address', ip, + '--fec', cerror, + '--max-bitrate', bitrate, + '--ttl', '16', + '--min-clients', nclients, + '--max-wait', maxtime, + '--file', file_path + ] + + # Imprimimos el comando con espacios (como realmente se enviará): + print(f"Sending command: {' '.join(splitted_cmd)}") + + # Ejecutamos el comando en el sistema, e imprimimos el resultado: + try: + result = subprocess.run(splitted_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"ReturnCode: {result.returncode}") + except subprocess.CalledProcessError as error: + print(f"ReturnCode: {error.returncode}") + print(f"Error Output: {error.stderr.decode()}") + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/sendFileUFTP.py b/py_scripts/sendFileUFTP.py similarity index 94% rename from bin/sendFileUFTP.py rename to py_scripts/sendFileUFTP.py index 5f18752..7fe1906 100644 --- a/bin/sendFileUFTP.py +++ b/py_scripts/sendFileUFTP.py @@ -3,7 +3,7 @@ """ Este script envía mediante UFTP la imagen recibida como primer parámetro, al puerto e IP (Multicast o Unicast) especificados en el segundo parámetro, - a la velocidad de transferencia tambíén especificada en el segundo parámetro (la sintaxis de este parámetro es "puerto:ip:bitrate"). + a la velocidad de transferencia tambíén especificada en el segundo parámetro (la sintaxis de este parámetro es "Port:IP:Bitrate"). Previamente, los clientes deben haberse puesto a escuchar en la IP Multicast correspondiente (tanto para envíos Multicast como para envíos Unicast). Paquetes APT requeridos: "uftp" @@ -14,13 +14,13 @@ sys.argv[1] - Nombre completo de la imagen a enviar (con o sin ruta) - Ejemplo1: image1.img - Ejemplo2: /opt/opengnsys/images/image1.img -sys.argv[2] - Parámetros Multicast/Unicast (en formato "puerto:ip:bitrate") +sys.argv[2] - Parámetros Multicast/Unicast (en formato "Port:IP:Bitrate") - Ejemplo1: 9000:239.194.17.2:100M - Ejemplo2: 9000:192.168.56.101:1G Sintaxis ---------- -./sendFileUFTP.py image_name|/image_path/image_name port:ip:bitrate +./sendFileUFTP.py image_name|/image_path/image_name Port:IP:Bitrate Ejemplos --------- @@ -46,7 +46,6 @@ repo_path = '/opt/opengnsys/images/' log_file = '/opt/opengnsys/images/uftp.log' - # -------------------------------------------------------------------------------------------- # FUNCTIONS # -------------------------------------------------------------------------------------------- @@ -56,7 +55,7 @@ def show_help(): """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". """ help_text = f""" - Sintaxis: {script_name} image_name|/image_path/image_name port:ip:bitrate + Sintaxis: {script_name} image_name|/image_path/image_name Port:IP:Bitrate Ejemplo1: {script_name} image1.img 9000:239.194.17.2:100M Ejemplo2: {script_name} /opt/opengnsys/images/image1.img 9000:192.168.56.101:1G """ @@ -73,10 +72,10 @@ def check_params(): show_help() sys.exit(0) # Si se ejecuta la función con más o menos de 2 parámetros, se muestra un mensaje de error, y se sale del script: - if len(sys.argv) != 3: + elif len(sys.argv) != 3: print(f"{script_name} Error: Formato incorrecto: Se debe especificar 2 parámetros (image_name|/image_path/image_name port:ip:bitrate)") sys.exit(1) - # Si en el segundo parámetro no hay 3 elementos (separados por ":"), devolvermos un error, y salimos del script: + # Si en el segundo parámetro no hay 3 elementos (separados por ":"), se muestra un mensaje de error, y se sale del script: param_list = sys.argv[2].split(':') if len(param_list) != 3: print(f"{script_name} Error: Datos Multicast incorrectos: \"{sys.argv[2]}\" (se debe especificar \"puerto:ip:bitrate\")") @@ -129,7 +128,7 @@ def main(): print(f"{script_name} Error: Fichero \"{file_path}\" no accesible") sys.exit(2) - # Almacenamos los elementos del segundo parámetro en variables: + # Almacenamos los elementos del segundo parámetro en variables (su formato es "puerto:ip:bitrate"): param_list = sys.argv[2].split(':') port, ip, bitrate = param_list -- 2.40.1 From 28ef5b9b3bd3a354116d2ebec80badbdc0e78947 Mon Sep 17 00:00:00 2001 From: ggil Date: Thu, 8 Aug 2024 13:28:40 +0200 Subject: [PATCH 05/70] refs #631 - getRepoIface.py added --- README.md | 6 +-- py_scripts/getRepoIface.py | 97 +++++++++++++++++++++++++++++++++++++ py_scripts/sendFileMcast.py | 21 +++++++- 3 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 py_scripts/getRepoIface.py diff --git a/README.md b/README.md index 639ee04..367fd0d 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,6 @@ OpenGnsys Repository Manager README Este repositorio GIT contiene la estructura de datos del repositorio de datos de OpenGnsys. -- bin: Binarios y scripts de gestión del repositorio. -- etc: Ficheros o plantillas de configuración del repositorio. -- py_scripts: Scripts en Python 3, algunos de los cuales son traducciones de los scripts bash situados en "bin". \ No newline at end of file +- bin -------- Binarios y scripts de gestión del repositorio. +- etc -------- Ficheros o plantillas de configuración del repositorio. +- py_scripts - Scripts en Python 3, algunos de los cuales son traducciones de los scripts bash situados en "bin". \ No newline at end of file diff --git a/py_scripts/getRepoIface.py b/py_scripts/getRepoIface.py new file mode 100644 index 0000000..18d9385 --- /dev/null +++ b/py_scripts/getRepoIface.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 + +""" +Este script obtiene y devuelve la interfaz de red asociada a la IP especificada en el archivo "/opt/opengnsys/etc/ogAdmRepo.cfg" (en la clave "IPlocal"). +En principio, debería hacer lo mismo que el script bash original (cuyo nombre es "getRepoIface", a secas). + +No recibe ningún parámetro, y siempre es llamado por otros scripts, que necesitan dicha interfaz (por ejemplo, "sendFileMcast"). +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import socket +import fcntl +import struct + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +config_file = '/opt/opengnsys/etc/ogAdmRepo.cfg' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def get_IPlocal(): + """ Obtiene el valor asociado a la variable "IPlocal", desde el archivo '/opt/opengnsys/etc/ogAdmRepo.cfg'. + Retorna la IP encontrada, o un error (si no la encuentra). + """ + IPlocal = None + with open(config_file, 'r') as file: + for line in file: + if line.startswith('IPlocal'): + IPlocal = line.split('=')[1].strip() + return IPlocal + if IPlocal is None: + return "IP no encontrada en el archivo de configuración" + + +def get_ip_address(ifname): + """ Obtiene y retorna la IP asociada a la interfaz especificada como parámetro. + """ + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + return socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, struct.pack('256s', ifname[:15].encode('utf-8')))[20:24]) + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Obtenemos la IP especificada en el archivo '/opt/opengnsys/etc/ogAdmRepo.cfg' (como valor de "IPlocal"): + IPlocal = get_IPlocal() + + # Si no se ha encontrado, imprimimos un error y salimos del script: + if "IP no encontrada" in IPlocal: + print("IP no encontrada en el archivo de configuración") + sys.exit(0) + + # Obtenemos una lista de tuplas, que contiene los índices y los nombres de las interfaces + # (en formato "[(1, 'lo'), (2, 'enp0s3'), (3, 'enp0s8')]"): + interfaces = socket.if_nameindex() + + # Iteramos los índices y nombres contenidos en "interfaces", obtenemos la IP asociada a cada interfaz, + # y las comparamos a la variable "IPlocal", para obtener el nombre de la interfaz asociada a dicha IP: + for ifindex, ifname in interfaces: + try: + ip_address = get_ip_address(ifname) + if ip_address == IPlocal: + interface_name = ifname + break + except IOError: + continue + + # Si hemos obtenido la interfaz la imprimimos, y si no imprimimos un error y salimos del script: + if interface_name: + print(interface_name) + else: + print("No se encontró la interfaz asociada a IPlocal") + sys.exit(1) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/py_scripts/sendFileMcast.py b/py_scripts/sendFileMcast.py index 72b267c..ba7e06c 100644 --- a/py_scripts/sendFileMcast.py +++ b/py_scripts/sendFileMcast.py @@ -40,7 +40,7 @@ import subprocess script_name = os.path.basename(__file__) repo_path = '/opt/opengnsys/images/' bin_path = '/opt/opengnsys/bin/' -repo_iface = subprocess.getoutput('/opt/opengnsys/bin/getRepoIface') +repo_iface_script = '/opt/opengnsys/py_scripts/getRepoIface.py' # -------------------------------------------------------------------------------------------- @@ -91,6 +91,22 @@ def build_file_path(): return file_path +def get_repo_iface(): + """ Obtiene y retorna la interfaz del repositorio, ejecutando el script "getRepoIface.py". + Como se ve, es necesario que el script se ejecute como sudo, o dará error. + """ + try: + result = subprocess.run(['sudo', 'python3', repo_iface_script], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + repo_iface = result.stdout.decode().strip() # Es necesario poner "strip", o dará error. + return repo_iface + except subprocess.CalledProcessError as error: + print(f"Error Output: {error.stderr.decode()}") + sys.exit(3) + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + sys.exit(4) + + # -------------------------------------------------------------------------------------------- # MAIN # -------------------------------------------------------------------------------------------- @@ -121,6 +137,9 @@ def main(): # Creamos la variable "cerror" (no sé que hace, pero estaba en el script original): cerror = "8x8/128" + # Obtenemos y almacenamos la interfaz del repositorio, mediante el script "getRepoIface.py": + repo_iface = get_repo_iface() + # Creamos una lista con el comando a enviar (esto es requerido por la función "subprocess.run"). # NOTA: Se desabilita el uso de mbuffer, ya que esta versión del upd-sender no la admite (ya estaba así en el script original). mbuffer = "" # which mbuffer &> /dev/null && MBUFFER="--pipe 'mbuffer -m 20M'" -- 2.40.1 From 2011b2390e78915d8adf7baa13b9fa01e83357ca Mon Sep 17 00:00:00 2001 From: ggil Date: Mon, 12 Aug 2024 17:28:25 +0200 Subject: [PATCH 06/70] refs #626 - Add API proposal to README.md --- README.md | 419 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 414 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 367fd0d..fb49f24 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,418 @@ -OpenGnsys Repository Manager README +OpenGnsys Repository Manager ======================================= -Este repositorio GIT contiene la estructura de datos del repositorio de datos de OpenGnsys. +Este repositorio GIT contiene la estructura de datos del repositorio de imágenes de OpenGnsys. -- bin -------- Binarios y scripts de gestión del repositorio. -- etc -------- Ficheros o plantillas de configuración del repositorio. -- py_scripts - Scripts en Python 3, algunos de los cuales son traducciones de los scripts bash situados en "bin". \ No newline at end of file +- **bin** -------------- Binarios y scripts de gestión del repositorio. +- **etc** -------------- Ficheros y plantillas de configuración del repositorio. +- **py_scripts** --- Scripts en Python 3, algunos de los cuales son traducciones de los scripts bash situados en "bin". + +--- + +## API de ogRepository + +La API de ogRepository proporciona una interfaz para facilitar la administración de las imágenes almacenadas en los repositorios de imágenes, permitiendo eliminarlas, enviarlas a clientes ogLive (con diferentes protocolos de transmisión), importarlas desde otros repositorios, etc. + +El presente documento detalla los endpoints de la API, con sus respectivos parámetros de entrada, así como las acciones que llevan a cabo. + +--- +### Tabla de Contenido: + +1. [Obtener Información de todas las Imágenes](#obtener-información-de-todas-las-imágenes) - `GET /ogrepository/v1/images` +2. [Obtener Información de una Imagen concreta](#obtener-información-de-una-imagen-concreta) - `GET /ogrepository/v1/images/{name}` +3. [Actualizar Información del Repositorio](#actualizar-información-del-repositorio) - `PUT /ogrepository/v1/images` +4. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/{name}` +5. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/images/import-image` +6. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/images/{protocol}` +7. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/images/{protocol}` +8. [Crear archivo ".torrent"](#crear-archivo-torrent) - `POST /ogrepository/v1/images/create-torrent` +9. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/images/{protocol}` +10. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/images/check-image` +11. [Exportar una Imagen](#exportar-una-imagen) - `POST /ogrepository/v1/images/export-image` +12. [Definir Imagen Global](#definir-imagen-global) - `PUT /ogrepository/v1/images/set-global` +13. [Definir Imagen Local](#definir-imagen-local) - `PUT /ogrepository/v1/images/set-local` +14. [Ver Estado de Transmisiones Multicast-P2P](#ver-estado-de-transmisiones-multicast-p2p) - +15. [Cancelar Transmisión Multicast-P2P](#cancelar-transmisión-multicast-p2p) - + +--- +### Obtener Información de todas las Imágenes + +Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (que corresponde a todas las imágenes monolíticas almacenadas en el repositorio). +Se debe crear un script que devuelva dicha información, porque actualmente no hay ninguno. + +**URL:** `/ogrepository/v1/images` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images +``` + +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al consultar y/o devolver la información de las imágenes. +- **Código 200 OK:** La información de las imágenes se obtuvo exitosamente. + - **Contenido:** Información de imágenes en formato JSON. + ```json + { + "directory": "/opt/opengnsys/images", + "images": [ + { + "name": "UbuntuSATA", + "type": "img", + "clientname": "Ubuntu_SATA", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "EXTFS", + "datasize": 8704000000 + }, + { + "name": "Windows10", + "type": "img", + "clientname": "Windows_10", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "NTFS", + "datasize": 23654400000 + } + ], + "ous": [] + } + ``` + - **name**: Nombre de la imagen, sin extensión. + - **type**: Extensión de la imagen. + - **clientname**: Nombre asignado al modelo del que se ha obtenido la imagen. + - **clonator**: Programa utilizado para la clonación. + - **compressor**: Programa utilizado para la compresión. + - **filesystem**: Sistema de archivos utilizado en la partición clonada. + - **datasize**: Tamaño de la imagen una vez restaurada, en bytes (tamaño de los datos). + +--- +### Obtener Información de una Imagen concreta + +Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (correspondiente a la imagen especificada). +Se debe crear un script que devuelva dicha información, porque actualmente no hay ninguno. + +**URL:** `/ogrepository/v1/images/{name}` +**Método HTTP:** GET + +**Parámetros de la URL:** +- `{name}`: Nombre de la imagen. + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/Windows10 +``` + +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al consultar y/o devolver la información de las imágenes. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La información de la imagen se obtuvo exitosamente. + - **Contenido:** Información de la imagen en formato JSON. + ```json + { + "name": "Windows10", + "type": "img", + "clientname": "Windows_10", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "NTFS", + "datasize": 23654400000 + } + ``` + - **name**: Nombre de la imagen, sin extensión. + - **type**: Extensión de la imagen. + - **clientname**: Nombre asignado al modelo del que se ha obtenido la imagen. + - **clonator**: Programa utilizado para la clonación. + - **compressor**: Programa utilizado para la compresión. + - **filesystem**: Sistema de archivos utilizado en la partición clonada. + - **datasize**: Tamaño de la imagen una vez restaurada, en bytes (tamaño de los datos). + +--- +### Actualizar Información del Repositorio + +Se actualizará la información de las imágenes almacenadas en el repositorio, reflejándola en el archivo "**/opt/opengnsys/etc/repoinfo.json**". +Se puede hacer con el script "**checkrepo**", que actualmente se ejecuta a cada minuto por crontab (indirectamente, porque es llamado por el script "**deletepreimage**", que es el que realmente se ejecuta por crontab). +Creemos que este endpoint debe ser llamado por ogCore u ogLive cada vez que se haya creado una imagen, y ejecutado desde el propio ogRepository cada vez que se elimine una imagen. + +**URL:** `/ogrepository/v1/images` +**Método HTTP:** PUT + +**Ejemplo de Solicitud:** + +```bash +curl -X PUT -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al actualizar la información de las imágenes. +- **Código 200 OK:** La actualización se realizó exitosamente. + +--- +### Eliminar una Imagen + +Se eliminará la imagen especificada como parámetro. +Se puede hacer con el script "**deleteimage**", que actualmente no se utiliza (lo que se hace ahora es eliminar todas las imágenes marcadas con ".delete", mediante el script "deletepreimage", que se ejecuta por crontab a cada minuto). +Además, el script "deleteimage" debería llamar al script "**checkrepo**", para actualizar la información del repositorio una vez eliminada la imagen. + +**NOTA**: En el pliego se solicita una función "papelera", para lo que habría que modificar los scripts existentes (y posiblemente crear otros endpoints, como "recuperar imagen de la papelera", por ejemplo). + +**URL:** `/ogrepository/v1/images/{name}` +**Método HTTP:** DELETE + +**Ejemplo de Solicitud:** + +```bash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/Windows10 +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al eliminar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se eliminó exitosamente. + +--- +### Importar una Imagen + +Se importará una imagen de un repositorio remoto al repositorio local. +Se puede hacer con el script "**importimage**", que actualmente no se utiliza. + +**URL:** `/ogrepository/v1/images/import-image` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **user**: Usuario con el que acceder al repositorio remoto (por defecto, usuario local). +- **repo**: IP o hostname del repositorio remoto. +- **image**: Nombre de la imagen a importar. + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"user":"user_name", "repo":"192.168.56.100", "image":"Windows10"}' http://example.com/ogrepository/v1/images/import-image +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al importar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen y/o el equipo remoto especificados. +- **Código 200 OK:** La imagen se ha importado exitosamente. + +--- +### Enviar una Imagen mediante UDPcast + +Se enviará una imagen por Multicast, mediante la aplicación UDPcast. +Se puede hacer con el script "**sendFileMcast**", que a su vez llama al binario "**udp-sender**", que es quien realmente realiza el envío. + +**URL:** `/ogrepository/v1/images/{protocol}` +**Método HTTP:** POST + +**Parámetros de la URL:** +- `{protocol}`: Protocolo que se utilizará para enviar la imagen (en este caso, "udpcast"). + +**Cuerpo de la Solicitud (JSON):** +- **image**: Nombre completo de la imagen a enviar, con extensión (con o sin ruta). +- **port**: Puerto Multicast. +- **method**: Modalidad half-duplex o full-duplex. +- **ip**: IP Multicast. +- **bitrate**: Velocidad de transmisión (en Mbps). +- **nclients**: Número mínimo de clientes. +- **maxtime**: Tiempo máximo de espera. + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "port":"9000", "method":"full", "ip":"239.194.17.2", "bitrate":"70M", "nclients":"20", "maxtime":"120"}' http://example.com/ogrepository/v1/images/udpcast +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se ha enviado exitosamente. + +--- +### Enviar una Imagen mediante UFTP + +Se enviará una imagen por Unicast o Multicast, mediante el protocolo "UFTP". +Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente los clientes ogLive destino se pongan en escucha con un daemon "UFTPD" (ejecutando el script "**listenUFTPD.py**"). +**NOTA**: Los envíos mediante "UFTP" funcionan al revés que los envíos mediante "UDPcast" (con este último, primero se debe ejecutar un comando en el servidor, y luego en los clientes). + +**URL:** `/ogrepository/v1/images/{protocol}` +**Método HTTP:** POST + +**Parámetros de la URL:** +- `{protocol}`: Protocolo que se utilizará para enviar la imagen (en este caso, "uftp"). + +**Cuerpo de la Solicitud (JSON):** +- **image**: Nombre completo de la imagen a enviar, con extensión (con o sin ruta). +- **port**: Puerto Multicast. +- **ip**: IP Unicast/Multicast. +- **bitrate**: Velocidad de transmisión (con "K" para Kbps, "M" para Mbps o "G" para Gbps). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "port":"9000", "ip":"239.194.17.2", "bitrate":"1G"}' http://example.com/ogrepository/v1/images/uftp +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se ha enviado exitosamente. + +--- +### Crear archivo .torrent + +Se creará un archivo ".torrent" para la imagen especificada como parámetro. +Se debe crear un script que realice dicha tarea, porque actualmente se hace mediante el script "**torrent-creator**", que se ejecuta por crontab a cada minuto (y crea un archivo ".torrent" por cada imagen que no tenga uno asociado). +**NOTA**: Puede que sea preferible que esta acción la realice el propio ogLive al crear una imagen, ya que también tiene las herramientas para hacerlo. + +**URL:** `/ogrepository/v1/images/create-torrent` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **image**: Nombre completo de la imagen a la que asociar un archivo torrent. + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img"}' http://example.com/ogrepository/v1/images/create-torrent +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al crear el archivo torrent. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** El archivo torrent se ha creado exitosamente. + +--- +### Enviar una Imagen mediante P2P + +Se debe hacer tracking de los torrents almacenados en ogRepository, e iniciar la transferencia de la imagen especificada (además, los clientes deben disponer del torrent asociado, y añadirlo a su cliente torrent). +No tengo claro cómo se haría con los scripts existentes (que utilizan "bttrack" y "ctorrent"), pero si usáramos "opentracker" y "transmission", se debería crear nuevos scripts. + +**URL:** `/ogrepository/v1/images/{protocol}` +**Método HTTP:** POST + +**Parámetros de la URL:** +- `{protocol}`: Protocolo que se utilizará para enviar la imagen (en este caso, "p2p"). + +**Cuerpo de la Solicitud (JSON):** +- **image**: Nombre completo de la imagen a enviar, con extensión (con o sin ruta). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img"}' http://example.com/ogrepository/v1/images/p2p +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se ha enviado exitosamente. + +--- +### Chequear Integridad de Imagen + +Se comprobará la integridad de todos los ficheros asociados a la imagen especificada como parámetro. +Para esto, entiendo que se debe crear un script que compare los ficheros asociados a la imagen (especialmente los archivos "**.sum**" y "**.full.sum**"), con la información almacenada en el archivo "**/opt/opengnsys/etc/repoinfo.json**". + +**URL:** `/ogrepository/v1/images/check-image` +**Método HTTP:** GET + +**Cuerpo de la Solicitud (JSON):** +- **image**: Nombre completo de la imagen a chequear, con extensión (con o sin ruta). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img"}' http://example.com/ogrepository/v1/images/check-image +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al chequear la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se ha chequeado exitosamente. +- **Código 200 KO:** La imagen se ha chequeado correctamente, pero no ha pasado el test. + +--- +### Exportar una Imagen + +Se exportará una imagen del repositorio local a un repositorio remoto. +Se debe crear un script que realice dicha tarea (o se puede utilizar el script "**importimage**", que realiza la acción contraria, pero que actualmente no se utiliza). + +**NOTA**: Aunque no se indica en el pliego, entendemos que también será necesario especificar credenciales de acceso al repositorio remoto como parámetros de entrada. + +**URL:** `/ogrepository/v1/images/export-image` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **repo**: IP o hostname del repositorio remoto. +- **ou**: Unidad Organizativa del repositorio remoto. +- **image**: Nombre de la imagen a exportar. + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"repo":"192.168.56.200", "ou":"OU_Ejemplo", "image":"Windows10"}' http://example.com/ogrepository/v1/images/export-image +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al exportar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen y/o el equipo remoto especificados. +- **Código 200 OK:** La imagen se ha exportado exitosamente. + +--- +### Definir Imagen Global + +Se marcará como "global" la imagen especificada como parámetro. +En principio, esto requerirá agregar una nueva clave al archivo "**/opt/opengnsys/etc/repoinfo.json**" (por ejemplo, "scope"), modificando el script "**checkrepo**", para que realice dicha modificación. +También debe crearse un script que realice la definición de la imagen especificada (modificando el valor de la nueva clave del archivo "**/opt/opengnsys/etc/repoinfo.json**", de "local" a "global", por ejemplo). +Además, deberá llamarse a un script que exporte dicha imagen a los demás repositorios gestionados por el servidor de administración (que aun no está creado). + +**URL:** `/ogrepository/v1/images/set-global` +**Método HTTP:** PUT + +**Cuerpo de la Solicitud (JSON):** +- **image**: Nombre completo de la imagen a definir como global, con extensión (con o sin ruta). + +**Ejemplo de Solicitud:** + +```bash +curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img"}' http://example.com/ogrepository/v1/images/set-global +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al definir la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La definición se realizó exitosamente. + +--- +### Definir Imagen Local + +Se marcará como "local" la imagen especificada como parámetro, que previamente habría sido marcada como "global" (ya que de forma predeterminada, todas las imágenes estarán marcadas como "local"). +Como comentábamos en el endpoint precedentte, esto requerirá agregar una nueva clave al archivo "**/opt/opengnsys/etc/repoinfo.json**" (por ejemplo, "scope"), modificando el script "**checkrepo**", para que realice dicha modificación. +También debe crearse un script que realice la definición de la imagen especificada (modificando el valor de la nueva clave del archivo "**/opt/opengnsys/etc/repoinfo.json**", de "global" a "local", por ejemplo). +Este endpoint deberá ser llamado en todos los repositorios gestionados por el mismo servidor de administración (para que todos hagan la modificación). + +**URL:** `/ogrepository/v1/images/set-local` +**Método HTTP:** PUT + +**Cuerpo de la Solicitud (JSON):** +- **image**: Nombre completo de la imagen a definir como local, con extensión (con o sin ruta). + +**Ejemplo de Solicitud:** + +```bash +curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img"}' http://example.com/ogrepository/v1/images/set-local +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al definir la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La definición se realizó exitosamente. + +--- +### Ver Estado de Transmisiones Multicast-P2P + +Se devolverá información del estado de las transmisiones existentes, con un identificador de cada sesión multicast o P2P, y la imagen asociada. +Se debe estudiar como realizar esta tarea para cada uno de los protocolos de transmisión, ya que cada uno tiene sus particularidades, y habitualmente no tienen comandos asociados para comprobar el estado de las transmisiones. +Y tampoco está claro que protocolo se utilizará para transimisiones Multicast (¿"UDPcast", "UFTP", o ambos?), ni qué programas se utilizarán para P2P (¿"ctorrent/bttrack" u "opentracker/Transmission"?). +**NOTA**: Posiblemente deba crearse un endpoint específico para cada uno de los protocolos que se utilicen. + +--- +### Cancelar Transmisión Multicast-P2P + +Se cancelará la transmisión Multicast o P2P cuyo identificador se especifique como parámetro. +Aunque cancelar una transmisión Multicast o P2P es una tarea sencilla (independientemente del protocolo o programa que se utilice), en principio deberá crearse un script para cada uno de ellos. +Y la definición del endpoint depende de como se defina el endpoint anterior ("**Ver Estado de Transmisiones Multicast-P2P**"), ya que será el que determine cómo se especifica el identificador de la transmisión. + +--- \ No newline at end of file -- 2.40.1 From 8e99fda66a8eeaf1320a5090aca0ec18036c1e63 Mon Sep 17 00:00:00 2001 From: ggil Date: Tue, 13 Aug 2024 14:42:59 +0200 Subject: [PATCH 07/70] refs #626 - README.md edited --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fb49f24..f841876 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ OpenGnsys Repository Manager Este repositorio GIT contiene la estructura de datos del repositorio de imágenes de OpenGnsys. -- **bin** -------------- Binarios y scripts de gestión del repositorio. -- **etc** -------------- Ficheros y plantillas de configuración del repositorio. +- **bin** ----------- Binarios y scripts de gestión del repositorio. +- **etc** ----------- Ficheros y plantillas de configuración del repositorio. - **py_scripts** --- Scripts en Python 3, algunos de los cuales son traducciones de los scripts bash situados en "bin". --- @@ -307,7 +307,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d ### Chequear Integridad de Imagen Se comprobará la integridad de todos los ficheros asociados a la imagen especificada como parámetro. -Para esto, entiendo que se debe crear un script que compare los ficheros asociados a la imagen (especialmente los archivos "**.sum**" y "**.full.sum**"), con la información almacenada en el archivo "**/opt/opengnsys/etc/repoinfo.json**". +Para esto, entiendo que se debe crear un script que compare el contenido de los ficheros "**.sum**" y "**.full.sum**" con una nueva extracción del checksum de la imagen, pero no veo como comprobar la integridad de todos los archivos asociados. **URL:** `/ogrepository/v1/images/check-image` **Método HTTP:** GET -- 2.40.1 From 1ec5d4ba3e6f9061bd7b6df6191e1baaa9d1867e Mon Sep 17 00:00:00 2001 From: ggil Date: Tue, 13 Aug 2024 14:53:22 +0200 Subject: [PATCH 08/70] refs #626 - README.md edited --- README.md | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index f841876..2e588eb 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará --- ### Obtener Información de todas las Imágenes -Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (que corresponde a todas las imágenes monolíticas almacenadas en el repositorio). +Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (que corresponde a todas las imágenes monolíticas almacenadas en el repositorio). Se debe crear un script que devuelva dicha información, porque actualmente no hay ninguno. **URL:** `/ogrepository/v1/images` @@ -91,7 +91,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag --- ### Obtener Información de una Imagen concreta -Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (correspondiente a la imagen especificada). +Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (correspondiente a la imagen especificada). Se debe crear un script que devuelva dicha información, porque actualmente no hay ninguno. **URL:** `/ogrepository/v1/images/{name}` @@ -133,8 +133,8 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag --- ### Actualizar Información del Repositorio -Se actualizará la información de las imágenes almacenadas en el repositorio, reflejándola en el archivo "**/opt/opengnsys/etc/repoinfo.json**". -Se puede hacer con el script "**checkrepo**", que actualmente se ejecuta a cada minuto por crontab (indirectamente, porque es llamado por el script "**deletepreimage**", que es el que realmente se ejecuta por crontab). +Se actualizará la información de las imágenes almacenadas en el repositorio, reflejándola en el archivo "**/opt/opengnsys/etc/repoinfo.json**". +Se puede hacer con el script "**checkrepo**", que actualmente se ejecuta a cada minuto por crontab (indirectamente, porque es llamado por el script "**deletepreimage**", que es el que realmente se ejecuta por crontab). Creemos que este endpoint debe ser llamado por ogCore u ogLive cada vez que se haya creado una imagen, y ejecutado desde el propio ogRepository cada vez que se elimine una imagen. **URL:** `/ogrepository/v1/images` @@ -152,8 +152,8 @@ curl -X PUT -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag --- ### Eliminar una Imagen -Se eliminará la imagen especificada como parámetro. -Se puede hacer con el script "**deleteimage**", que actualmente no se utiliza (lo que se hace ahora es eliminar todas las imágenes marcadas con ".delete", mediante el script "deletepreimage", que se ejecuta por crontab a cada minuto). +Se eliminará la imagen especificada como parámetro. +Se puede hacer con el script "**deleteimage**", que actualmente no se utiliza (lo que se hace ahora es eliminar todas las imágenes marcadas con ".delete", mediante el script "deletepreimage", que se ejecuta por crontab a cada minuto). Además, el script "deleteimage" debería llamar al script "**checkrepo**", para actualizar la información del repositorio una vez eliminada la imagen. **NOTA**: En el pliego se solicita una función "papelera", para lo que habría que modificar los scripts existentes (y posiblemente crear otros endpoints, como "recuperar imagen de la papelera", por ejemplo). @@ -174,7 +174,7 @@ curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/i --- ### Importar una Imagen -Se importará una imagen de un repositorio remoto al repositorio local. +Se importará una imagen de un repositorio remoto al repositorio local. Se puede hacer con el script "**importimage**", que actualmente no se utiliza. **URL:** `/ogrepository/v1/images/import-image` @@ -198,7 +198,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d --- ### Enviar una Imagen mediante UDPcast -Se enviará una imagen por Multicast, mediante la aplicación UDPcast. +Se enviará una imagen por Multicast, mediante la aplicación UDPcast. Se puede hacer con el script "**sendFileMcast**", que a su vez llama al binario "**udp-sender**", que es quien realmente realiza el envío. **URL:** `/ogrepository/v1/images/{protocol}` @@ -229,8 +229,8 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d --- ### Enviar una Imagen mediante UFTP -Se enviará una imagen por Unicast o Multicast, mediante el protocolo "UFTP". -Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente los clientes ogLive destino se pongan en escucha con un daemon "UFTPD" (ejecutando el script "**listenUFTPD.py**"). +Se enviará una imagen por Unicast o Multicast, mediante el protocolo "UFTP". +Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente los clientes ogLive destino se pongan en escucha con un daemon "UFTPD" (ejecutando el script "**listenUFTPD.py**"). **NOTA**: Los envíos mediante "UFTP" funcionan al revés que los envíos mediante "UDPcast" (con este último, primero se debe ejecutar un comando en el servidor, y luego en los clientes). **URL:** `/ogrepository/v1/images/{protocol}` @@ -258,8 +258,8 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d --- ### Crear archivo .torrent -Se creará un archivo ".torrent" para la imagen especificada como parámetro. -Se debe crear un script que realice dicha tarea, porque actualmente se hace mediante el script "**torrent-creator**", que se ejecuta por crontab a cada minuto (y crea un archivo ".torrent" por cada imagen que no tenga uno asociado). +Se creará un archivo ".torrent" para la imagen especificada como parámetro. +Se debe crear un script que realice dicha tarea, porque actualmente se hace mediante el script "**torrent-creator**", que se ejecuta por crontab a cada minuto (y crea un archivo ".torrent" por cada imagen que no tenga uno asociado). **NOTA**: Puede que sea preferible que esta acción la realice el propio ogLive al crear una imagen, ya que también tiene las herramientas para hacerlo. **URL:** `/ogrepository/v1/images/create-torrent` @@ -281,7 +281,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d --- ### Enviar una Imagen mediante P2P -Se debe hacer tracking de los torrents almacenados en ogRepository, e iniciar la transferencia de la imagen especificada (además, los clientes deben disponer del torrent asociado, y añadirlo a su cliente torrent). +Se debe hacer tracking de los torrents almacenados en ogRepository, e iniciar la transferencia de la imagen especificada (además, los clientes deben disponer del torrent asociado, y añadirlo a su cliente torrent). No tengo claro cómo se haría con los scripts existentes (que utilizan "bttrack" y "ctorrent"), pero si usáramos "opentracker" y "transmission", se debería crear nuevos scripts. **URL:** `/ogrepository/v1/images/{protocol}` @@ -306,7 +306,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d --- ### Chequear Integridad de Imagen -Se comprobará la integridad de todos los ficheros asociados a la imagen especificada como parámetro. +Se comprobará la integridad de todos los ficheros asociados a la imagen especificada como parámetro. Para esto, entiendo que se debe crear un script que compare el contenido de los ficheros "**.sum**" y "**.full.sum**" con una nueva extracción del checksum de la imagen, pero no veo como comprobar la integridad de todos los archivos asociados. **URL:** `/ogrepository/v1/images/check-image` @@ -329,7 +329,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d --- ### Exportar una Imagen -Se exportará una imagen del repositorio local a un repositorio remoto. +Se exportará una imagen del repositorio local a un repositorio remoto. Se debe crear un script que realice dicha tarea (o se puede utilizar el script "**importimage**", que realiza la acción contraria, pero que actualmente no se utiliza). **NOTA**: Aunque no se indica en el pliego, entendemos que también será necesario especificar credenciales de acceso al repositorio remoto como parámetros de entrada. @@ -355,9 +355,9 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d --- ### Definir Imagen Global -Se marcará como "global" la imagen especificada como parámetro. -En principio, esto requerirá agregar una nueva clave al archivo "**/opt/opengnsys/etc/repoinfo.json**" (por ejemplo, "scope"), modificando el script "**checkrepo**", para que realice dicha modificación. -También debe crearse un script que realice la definición de la imagen especificada (modificando el valor de la nueva clave del archivo "**/opt/opengnsys/etc/repoinfo.json**", de "local" a "global", por ejemplo). +Se marcará como "global" la imagen especificada como parámetro. +En principio, esto requerirá agregar una nueva clave al archivo "**/opt/opengnsys/etc/repoinfo.json**" (por ejemplo, "scope"), modificando el script "**checkrepo**", para que realice dicha modificación. +También debe crearse un script que realice la definición de la imagen especificada (modificando el valor de la nueva clave del archivo "**/opt/opengnsys/etc/repoinfo.json**", de "local" a "global", por ejemplo). Además, deberá llamarse a un script que exporte dicha imagen a los demás repositorios gestionados por el servidor de administración (que aun no está creado). **URL:** `/ogrepository/v1/images/set-global` @@ -379,9 +379,9 @@ curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d --- ### Definir Imagen Local -Se marcará como "local" la imagen especificada como parámetro, que previamente habría sido marcada como "global" (ya que de forma predeterminada, todas las imágenes estarán marcadas como "local"). -Como comentábamos en el endpoint precedentte, esto requerirá agregar una nueva clave al archivo "**/opt/opengnsys/etc/repoinfo.json**" (por ejemplo, "scope"), modificando el script "**checkrepo**", para que realice dicha modificación. -También debe crearse un script que realice la definición de la imagen especificada (modificando el valor de la nueva clave del archivo "**/opt/opengnsys/etc/repoinfo.json**", de "global" a "local", por ejemplo). +Se marcará como "local" la imagen especificada como parámetro, que previamente habría sido marcada como "global" (ya que de forma predeterminada, todas las imágenes estarán marcadas como "local"). +Como comentábamos en el endpoint precedentte, esto requerirá agregar una nueva clave al archivo "**/opt/opengnsys/etc/repoinfo.json**" (por ejemplo, "scope"), modificando el script "**checkrepo**", para que realice dicha modificación. +También debe crearse un script que realice la definición de la imagen especificada (modificando el valor de la nueva clave del archivo "**/opt/opengnsys/etc/repoinfo.json**", de "global" a "local", por ejemplo). Este endpoint deberá ser llamado en todos los repositorios gestionados por el mismo servidor de administración (para que todos hagan la modificación). **URL:** `/ogrepository/v1/images/set-local` @@ -403,16 +403,16 @@ curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d --- ### Ver Estado de Transmisiones Multicast-P2P -Se devolverá información del estado de las transmisiones existentes, con un identificador de cada sesión multicast o P2P, y la imagen asociada. -Se debe estudiar como realizar esta tarea para cada uno de los protocolos de transmisión, ya que cada uno tiene sus particularidades, y habitualmente no tienen comandos asociados para comprobar el estado de las transmisiones. -Y tampoco está claro que protocolo se utilizará para transimisiones Multicast (¿"UDPcast", "UFTP", o ambos?), ni qué programas se utilizarán para P2P (¿"ctorrent/bttrack" u "opentracker/Transmission"?). +Se devolverá información del estado de las transmisiones existentes, con un identificador de cada sesión multicast o P2P, y la imagen asociada. +Se debe estudiar como realizar esta tarea para cada uno de los protocolos de transmisión, ya que cada uno tiene sus particularidades, y habitualmente no tienen comandos asociados para comprobar el estado de las transmisiones. +Y tampoco está claro que protocolo se utilizará para transimisiones Multicast (¿"UDPcast", "UFTP", o ambos?), ni qué programas se utilizarán para P2P (¿"ctorrent/bttrack" u "opentracker/Transmission"?). **NOTA**: Posiblemente deba crearse un endpoint específico para cada uno de los protocolos que se utilicen. --- ### Cancelar Transmisión Multicast-P2P Se cancelará la transmisión Multicast o P2P cuyo identificador se especifique como parámetro. -Aunque cancelar una transmisión Multicast o P2P es una tarea sencilla (independientemente del protocolo o programa que se utilice), en principio deberá crearse un script para cada uno de ellos. +Aunque cancelar una transmisión Multicast o P2P es una tarea sencilla (independientemente del protocolo o programa que se utilice), en principio deberá crearse un script para cada uno de ellos. Y la definición del endpoint depende de como se defina el endpoint anterior ("**Ver Estado de Transmisiones Multicast-P2P**"), ya que será el que determine cómo se especifica el identificador de la transmisión. --- \ No newline at end of file -- 2.40.1 From 2467dd30e793bb32ee4c5527c582ec33b28d1b06 Mon Sep 17 00:00:00 2001 From: ggil Date: Wed, 14 Aug 2024 17:25:58 +0200 Subject: [PATCH 09/70] refs #631 - Add updateRepoInfo.py script --- README.md | 4 +- py_scripts/getRepoIface.py | 1 + py_scripts/updateRepoInfo.py | 282 +++++++++++++++++++++++++++++++++++ 3 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 py_scripts/updateRepoInfo.py diff --git a/README.md b/README.md index 2e588eb..fa164fe 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -OpenGnsys Repository Manager -======================================= +ogRepository - OpenGnsys Repository Manager +=========================================== Este repositorio GIT contiene la estructura de datos del repositorio de imágenes de OpenGnsys. diff --git a/py_scripts/getRepoIface.py b/py_scripts/getRepoIface.py index 18d9385..f0d105b 100644 --- a/py_scripts/getRepoIface.py +++ b/py_scripts/getRepoIface.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- """ Este script obtiene y devuelve la interfaz de red asociada a la IP especificada en el archivo "/opt/opengnsys/etc/ogAdmRepo.cfg" (en la clave "IPlocal"). diff --git a/py_scripts/updateRepoInfo.py b/py_scripts/updateRepoInfo.py new file mode 100644 index 0000000..0b5221a --- /dev/null +++ b/py_scripts/updateRepoInfo.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script actualiza la información de las imágenes del repositorio, reflejándola en el archivo "/opt/opengnsys/etc/repoinfo.json", + añadiendo información de las imágenes nuevas, y borrando la información de las imágenes que fueron eliminadas. +La información es obtenida desde archivos ".info", que originalmente eran eliminados, + pero que en esta versión son renombrados a ".info.checked", obteniendo la misma funcionalidad. + +En principio, debería hacer lo mismo que el script bash original (cuyo nombre es "checkrepo"), salvo por el detalle que acabamos de comentar. + +No recibe ningún parámetro, y debería ser llamado por el propio ogRepoitory cada vez que se elimine una imagen (desde el script "deleteImage.py"), + y por ogCore u ogLive cada vez que se cree una imagen. +""" + + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import json +import subprocess +import shutil + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +repo_path = '/opt/opengnsys/images' +info_file = '/opt/opengnsys/etc/repoinfo.json' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def create_empty_json(): + """ Esta función crea el archivo "repoinfo.json", con la estructura básica que debe contener. + Evidentemente, solo es llamada cuando no existe el archivo. + """ + # Creamos un diccionario con la estructura básica que debe tener el archivo json: + json_data = {"directory": repo_path, "images": [], "ous": []} + + # Abrimos el archivo json en modo escritura (creándolo si no existe, como es el caso), + # y le añadimos la estructura almacenada en el diccionario "json_data": + with open(info_file, 'w') as file: + json.dump(json_data, file, indent=2) + + + +def check_files(): + """ Esta función recorre el directorio de imágenes, para buscar archivos con extensiones ".img" o ".dsk". + Llama a la función "add_to_json" para que esta añada información de las imágenes no bloqueadas (sin archivo ".lock"), + que además tengan un archivo ".info" asociado (siempre que este no se haya modificado antes que la propia imagen). + Originalmente eliminaba los archivos ".info" después de extraer la información, pero ahora los renombra (agrega ".checked"). + """ + # Iteramos recursivamente todos los archivos y directorios de "/opt/opengnsys/images", + # y luego iteramos todos los archivos encontrados (en una iteración anidada): + for root, dirs, files in os.walk(repo_path): + for file in files: + # Si la imagen actual tiene extensión ".img" o ".dsk", construimos la ruta completa ("img_path"): + if file.endswith((".img", ".dsk")): + img_path = os.path.join(root, file) + # Si existe un archivo ".lock" asociado a la imagen actual, pasamos a la siguiente: + if os.path.exists(f"{img_path}.lock"): + continue + # Comprobamos si existe un archivo ".info" asociado a la imagen actual: + info_file = f"{img_path}.info" + if os.path.exists(info_file): + # Si la fecha de modificación del archivo ".info" es anterior a la de la imagen, lo eliminamos (porque estará desactualizado): + if os.path.getmtime(info_file) < os.path.getmtime(img_path): + os.remove(info_file) + print(f"Warning: Deleted outdated file {info_file}") + # En caso contrario, almacenamos el contenido del archivo ".info" (del tipo "PARTCLONE:LZOP:EXTFS:8500000:Ubuntu_20") en la variable "data": + else: + with open(info_file, 'r') as file: + data = file.read() + # Llamamos a la función "add_to_json", para que inserte la información de la imagen en el archivo json + # (pasándole el nombre de la imagen, la extensión, y los datos extraídos del archivo ".info"): + img_name = os.path.relpath(img_path, repo_path) + add_to_json(os.path.splitext(img_name)[0], os.path.splitext(img_name)[1][1:], data) + + # Renombramos el archivo ".info" a ".info.checked", para que ya no se añada la información que contiene + # (originalmente se eliminaba el archivo, pero creo que es mejor mantenerlo): + os.rename(info_file, f"{info_file}.checked") + + + +def check_dirs(): + """ Esta función recorre el directorio de imágenes, para buscar archivos con nombre "ogimg.info". + Llama a la función "add_to_json" para que esta añada información de las imágenes no bloqueadas (sin archivo ".lock"), + que además tengan un archivo "ogimg.info" asociado (pero no sé que tipo de imágenes son esas). + """ + # Iteramos recursivamente todos los archivos y directorios de "/opt/opengnsys/images", + # y luego iteramos todos los archivos encontrados (en una iteración anidada): + for root, dirs, files in os.walk(repo_path): + for file in files: + # Si el nombre del archivo actual es "ogimg.info", construimos su ruta completa ("info_path"), y la ruta del directorio ("img_path"): + if file == "ogimg.info": + info_path = os.path.join(root, file) + img_path = os.path.dirname(info_path) + # Si existe un archivo ".lock" asociado a la imagen actual, o si la ruta coincide con "repo_path", pasamos a la siguiente: + if img_path == repo_path or os.path.exists(f"{img_path}.lock"): + continue + # Almacenamos el contenido del archivo "ogimg.info", en la variable "lines": + with open(info_path, 'r') as file: + lines = file.readlines() + # Iteramos las lineas obtenidas, para extraer el tipo de sistema de archivos ("fstype") y el tamaño de los datos ("sizedata"), + # y con ello construimos una cadena "rsync::{fstype}:{sizedata}:" (que almacenamos en "data"): + for line in lines: + if line.startswith("# fstype"): + fstype = line.split("=")[1].strip() + elif line.startswith("# sizedata"): + sizedata = line.split("=")[1].strip() + data = f"rsync::{fstype}:{sizedata}:" + # Llamamos a la función "add_to_json", para que inserte la información de la imagen en el archivo json + # (pasándole el nombre de la imagen, el tipo "dir"", y los datos extraídos del archivo "ogimg.info"): + img_name = os.path.relpath(img_path, repo_path) + add_to_json(img_name, "dir", data) + + + +def add_to_json(image_name, image_type, data): + """ Esta función añade al archivo "repoinfo.json" la información de las imágenes que aun no ha sido introducida en él (imágenes nuevas, básicamente). + El procedimiento es diferente para las imágenes "normales" y para las imágenes basadas en OU. + """ + # Almacenamos el contenido de la variable "data" (del tipo "PARTCLONE:LZOP:EXTFS:8500000:Ubuntu_20") en variables separadas: + clonator, compressor, fstype, datasize, client = data.split(":") + + # Si la imagen está dentro de una OU (directorio), extraemos el nombre de la OU y de la imagen: + ou_name = None + if '/' in image_name: + ou_name = os.path.dirname(image_name) + image_name = os.path.basename(image_name) + + # Creamos un diccionario con los datos de la imagen, que luego insertaremos en el json: + json_data = { + "name": image_name, + "type": image_type.lower(), + "clientname": client.strip('\n'), # Eliminamos el salto de linea + "clonator": clonator.lower(), + "compressor": compressor.lower(), + "filesystem": fstype.upper(), + "datasize": int(datasize) * 1024 # Convertimos el valor a bytes (desde KB) + } + # Almacenamos el contenido del archivo "repoinfo.json" en la variable "info_data": + with open(info_file, 'r') as file: + info_data = json.load(file) + + # Comprobamos si las claves "info_data" (o sea, del archivo json) son las correctas: + if set(info_data.keys()) == {"directory", "images", "ous"}: + + # Actualizamos la información de las imágenes "normales" (no basadas en OU): + if ou_name is None: + img_index = next((i for i, img in enumerate(info_data["images"]) if img["name"] == image_name), None) + if img_index is not None: + # Update if image data changes + if info_data["images"][img_index] != json_data: + info_data["images"][img_index] = json_data + else: + # Append a new entry + info_data["images"].append(json_data) + + # Actualizamos la información de las imágenes basadas en OU: + else: + ou_index = next((i for i, ou in enumerate(info_data["ous"]) if ou["subdir"] == ou_name), None) + if ou_index is None: + # Append a new OU entry + info_data["ous"].append({"subdir": ou_name, "images": [json_data]}) + else: + img_index = next((i for i, img in enumerate(info_data["ous"][ou_index]["images"]) if img["name"] == image_name), None) + if img_index is not None: + # Update if image data changes + if info_data["ous"][ou_index]["images"][img_index] != json_data: + info_data["ous"][ou_index]["images"][img_index] = json_data + else: + # Append a new entry + info_data["ous"][ou_index]["images"].append(json_data) + + # Si las claves de "info_data" no son las correctas, creamos toda la estructura: + else: + if ou_name is None: + info_data = {"directory": repo_path, "images": [json_data], "ous": []} + else: + info_data = {"directory": repo_path, "images": [], "ous": [{"subdir": ou_name, "images": [json_data]}]} + + # Sobreescribimos el archivo "repoinfo.json", con el contenido de "info_data" (que estará actualizado): + with open(info_file, 'w') as file: + json.dump(info_data, file, indent=2) + + + +def remove_from_json(): + """ Esta función carga el contenido del archivo "repoinfo.json", y comprueba la existencia de las imágenes especificadas allí. + Elimina las claves correspondientes a imágenes inexistentes, y posteriormente sobreescribe el archivo "repoinfo.json". + """ + # Almacenamos el contenido del archivo "repoinfo.json", en la variable "json_data": + with open(info_file, 'r') as file: + json_data = json.load(file) + + # Iteramos las imágenes de la clave "images" de "json_data", construimos sus rutas y comprobamos su existencia + # (si no existen, las eliminamos de "json_data"): + for i, image in enumerate(json_data.get("images", [])): + img_name = image["name"] + img_type = image["type"] + if img_type != "dir": + img_name = f"{img_name}.{img_type}" + img_path = os.path.join(repo_path, img_name) + if not os.path.exists(img_path) and not os.path.exists(f"{img_path}.lock"): + json_data["images"].pop(i) + + # Iteramos las imágenes de la clave "ous" de "json_data", construimos sus rutas y comprobamos su existencia + # (si no existen, las eliminamos de "json_data"): + for j, ou in enumerate(json_data.get("ous", [])): + ou_name = ou["subdir"] + ou_path = os.path.join(repo_path, ou_name) + if not os.path.exists(ou_path): + json_data["ous"].pop(j) + else: + for i, image in enumerate(ou.get("images", [])): + img_name = image["name"] + img_type = image["type"] + if img_type != "dir": + img_name = f"{img_name}.{img_type}" + img_path = os.path.join(ou_path, img_name) + if not os.path.exists(img_path) and not os.path.exists(f"{img_path}.lock"): + ou["images"].pop(i) + + # Sobreescribimos el archivo "repoinfo.json", con el contenido de "json_data" + # (una vez eliminadas las imágenes inexistentes): + with open(info_file, 'w') as file: + json.dump(json_data, file, indent=2) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Comprobamos si tenemos permisos de escritura sobre el directorio que contiene el archivo "repoinfo.json" + # ("/opt/opengnsys/etc"), y en caso contrario lanzamos una excepción: + if not os.access(os.path.dirname(info_file), os.W_OK): + raise PermissionError(f"Cannot access {info_file}") + + # Comprobamos si existe el archivo "repoinfo.json", y en caso contrario lo creamos: + if not os.path.exists(info_file): + print("Creating empty json file...") + create_empty_json() + + # Comprobamos si tenemos permisos de escritura sobre el archivo, y en caso contrario lanzamos un error: + if not os.access(info_file, os.W_OK): + raise PermissionError(f"Cannot access {info_file}") + + # Llamamos a la función "check_files", para añadir al archivo json las imágenes ".img" o ".dsk" aun no añadidas + # (que son las que tienen asociado un archivo ".info", aun no renombrado o eliminado): + print("Checking file images...") + check_files() + + # Llamamos a la función "check_dirs", para añadir al archivo json las imágenes aun no añadidas que tienen asociado un archivo "ogimg.info" + # (no sé qué imágenes son estas, pero de alguna forma están basadas en directorios): + print("Checking dir images...") + check_dirs() + + # Llamamos a la función "remove_from_json", para eliminar del archivo json las imágenes que fueron eliminadas del repositorio: + print("Removing deleted images...") + remove_from_json() + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- -- 2.40.1 From 9e5edcdbdf5a9a063855e82051d18aa90cbc40e3 Mon Sep 17 00:00:00 2001 From: ggil Date: Mon, 19 Aug 2024 15:49:35 +0200 Subject: [PATCH 10/70] refs #631 - Add deleteImage.py --- README.md | 62 +++++++--- py_scripts/deleteImage.py | 240 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+), 19 deletions(-) create mode 100644 py_scripts/deleteImage.py diff --git a/README.md b/README.md index fa164fe..d91579e 100644 --- a/README.md +++ b/README.md @@ -23,17 +23,18 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará 2. [Obtener Información de una Imagen concreta](#obtener-información-de-una-imagen-concreta) - `GET /ogrepository/v1/images/{name}` 3. [Actualizar Información del Repositorio](#actualizar-información-del-repositorio) - `PUT /ogrepository/v1/images` 4. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/{name}` -5. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/images/import-image` -6. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/images/{protocol}` -7. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/images/{protocol}` -8. [Crear archivo ".torrent"](#crear-archivo-torrent) - `POST /ogrepository/v1/images/create-torrent` -9. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/images/{protocol}` -10. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/images/check-image` -11. [Exportar una Imagen](#exportar-una-imagen) - `POST /ogrepository/v1/images/export-image` -12. [Definir Imagen Global](#definir-imagen-global) - `PUT /ogrepository/v1/images/set-global` -13. [Definir Imagen Local](#definir-imagen-local) - `PUT /ogrepository/v1/images/set-local` -14. [Ver Estado de Transmisiones Multicast-P2P](#ver-estado-de-transmisiones-multicast-p2p) - -15. [Cancelar Transmisión Multicast-P2P](#cancelar-transmisión-multicast-p2p) - +5. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/images/{name}` +6. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/images/import-image` +7. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/images/{protocol}` +8. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/images/{protocol}` +9. [Crear archivo ".torrent"](#crear-archivo-torrent) - `POST /ogrepository/v1/images/create-torrent` +10. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/images/{protocol}` +11. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/images/check-image` +12. [Exportar una Imagen](#exportar-una-imagen) - `POST /ogrepository/v1/images/export-image` +13. [Definir Imagen Global](#definir-imagen-global) - `PUT /ogrepository/v1/images/set-global` +14. [Definir Imagen Local](#definir-imagen-local) - `PUT /ogrepository/v1/images/set-local` +15. [Ver Estado de Transmisiones Multicast-P2P](#ver-estado-de-transmisiones-multicast-p2p) - +16. [Cancelar Transmisión Multicast-P2P](#cancelar-transmisión-multicast-p2p) - --- ### Obtener Información de todas las Imágenes @@ -134,8 +135,8 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag ### Actualizar Información del Repositorio Se actualizará la información de las imágenes almacenadas en el repositorio, reflejándola en el archivo "**/opt/opengnsys/etc/repoinfo.json**". -Se puede hacer con el script "**checkrepo**", que actualmente se ejecuta a cada minuto por crontab (indirectamente, porque es llamado por el script "**deletepreimage**", que es el que realmente se ejecuta por crontab). -Creemos que este endpoint debe ser llamado por ogCore u ogLive cada vez que se haya creado una imagen, y ejecutado desde el propio ogRepository cada vez que se elimine una imagen. +Se puede hacer con el script "**updateRepoInfo.py**", que hemos programado recientemente (y que es similar al script bash original "**checkrepo**"). +Este endpoint es llamado por el script "**deleteImage.py**" (para actualizar la información cada vez que se elimine una imagen), y creemos que también debe ser llamado por ogCore u ogLive cada vez que se haya creado una imagen. **URL:** `/ogrepository/v1/images` **Método HTTP:** PUT @@ -152,25 +153,48 @@ curl -X PUT -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag --- ### Eliminar una Imagen -Se eliminará la imagen especificada como parámetro. -Se puede hacer con el script "**deleteimage**", que actualmente no se utiliza (lo que se hace ahora es eliminar todas las imágenes marcadas con ".delete", mediante el script "deletepreimage", que se ejecuta por crontab a cada minuto). -Además, el script "deleteimage" debería llamar al script "**checkrepo**", para actualizar la información del repositorio una vez eliminada la imagen. - -**NOTA**: En el pliego se solicita una función "papelera", para lo que habría que modificar los scripts existentes (y posiblemente crear otros endpoints, como "recuperar imagen de la papelera", por ejemplo). +Se eliminará la imagen especificada como parámetro, pudiendo eliminarla permanentemente o enviarla a la papelera. +Se puede hacer con el script "**deleteimage.py**", que hemos programado recientemente (y que incluye la funcionalidad "papelera"), y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. **URL:** `/ogrepository/v1/images/{name}` **Método HTTP:** DELETE +**Cuerpo de la Solicitud (JSON):** +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). +- **method**: Método de eliminación (puede ser "trash" o "permanent"). + **Ejemplo de Solicitud:** ```bash -curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/Windows10 +curl -X DELETE -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ou_subdir":"none", "method":"trash"}' http://example.com/ogrepository/v1/images/Windows10.img ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al eliminar la imagen. - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. - **Código 200 OK:** La imagen se eliminó exitosamente. +--- +### Recuperar una Imagen + +Se recuperará la imagen especificada como parámetro, desde la papelera. +Se puede hacer con el script "**recoverImage.py**", que hemos programado recientemente, y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. + +**URL:** `/ogrepository/v1/images/{name}` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ou_subdir":"none"}' http://example.com/ogrepository/v1/images/Windows10.img +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al recuperar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se recuperó exitosamente. + --- ### Importar una Imagen diff --git a/py_scripts/deleteImage.py b/py_scripts/deleteImage.py new file mode 100644 index 0000000..a17f71f --- /dev/null +++ b/py_scripts/deleteImage.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script elimina la imagen que recibe como parámetro (y todos sus archivos asociados), moviendo los archivos a la papelera + (respetando el subdirectorio correspondiente a la OU, si fuera el caso), o eliminándolos permanentemente si se especifica el parámetro "-p". +Es similar al script bash original (cuyo nombre es "deleteimage", a secas), pero este no incluía la funcionalidad papelera. +Llama al script "updateRepoInfo.py", para actualizar la información del repositorio. + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a eliminar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/images/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/images/ou_subdir/image1.img + +sys.argv[2] - Parámetro opcional para especificar que la eliminación sea permanente (sin papelera). + - Ejemplo: -p + + Sintaxis +---------- +./deleteImage.py [ou_subdir/]image_name|/image_path/image_name [-p] + + Ejemplos + --------- +./deleteImage.py image1.img -p +./deleteImage.py /opt/opengnsys/images/image1.img -p +./deleteImage.py ou_subdir/image1.img -p +./deleteImage.py /ou_subdir/image1.img +./deleteImage.py /opt/opengnsys/images/ou_subdir/image1.img +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import shutil +import pwd +import grp +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/images/' +trash_path = '/opt/opengnsys/images_trash/' +update_repo_script = '/opt/opengnsys/py_scripts/updateRepoInfo.py' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name [-p] + Ejemplo1: {script_name} image1.img -p + Ejemplo2: {script_name} /opt/opengnsys/images/image1.img -p + Ejemplo3: {script_name} ou_subdir/image1.img -p + Ejemplo4: {script_name} /ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/images/ou_subdir/image1.img + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script sin parámetros, se muestra un error y la ayuda, y se sale del script: + if len(sys.argv) == 1: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar al menos 1 parámetro") + show_help() + sys.exit(0) + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + elif len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(1) + # Si se ejecuta el script con el parámetro "help" y más parámetros, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) > 2 and sys.argv[1] == "help": + print(f"{script_name} Error: Formato incorrecto: Para invocar la ayuda, se debe especificar 'help' como único parámetro") + show_help() + sys.exit(2) + # Si se ejecuta el script con 2 parámetros y el segundo es diferente de "-p", se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) == 3 and sys.argv[2] != "-p": + print(f"{script_name} Error: Formato incorrecto: El segundo parámetro solo puede ser '-p'") + show_help() + sys.exit(3) + # Si se ejecuta el script con más de 2 parámetros, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) > 3: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 o 2 parámetros") + show_help() + sys.exit(4) + + +def build_file_path(): + """ Construye la ruta completa al archivo a eliminar + (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "repo_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(repo_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(repo_path): + file_path = os.path.join(repo_path, param_path) + else: + file_path = param_path + return file_path + + +def create_trash_folder(): + """ Crea el directorio correspondiente a la papelera, y le asigna propietarios y permisos. + Evidentemente, esta función solo es llamada cuando no existe el directorio. + """ + # Obtenemos el UID del usuario "root" y el GID del grupo "opengnsys": + uid = pwd.getpwnam('root').pw_uid + gid = grp.getgrnam('opengnsys').gr_gid + # Creamos el directorio correspondiente a la papelera: + os.mkdir(trash_path) + # Asignamos el usuario y el grupo propietarios del directorio: + os.chown(trash_path, uid, gid) + # Asignamos permisos "775" al directorio : + os.chmod(trash_path, 0o775) + + +def delete_normal_image(file_path, method, extensions): + """ Elimina la imagen "normal" que recibe en el parámetro "file_path", y todos sus archivos asociados, + moviéndolos a la papelera o eliminándolos permanentemente (en función del parámetro "method"). + """ + # Iteramos las extensiones de los archivos, y construimos la ruta completa de cada uno de ellos: + for ext in extensions: + file_to_remove = f"{file_path}{ext}" + # Si el archivo actual existe, lo eliminamos o lo movemos a la papelera + # (dependiendo del valor del parámetro "method"): + if os.path.exists(file_to_remove): + if method == 'trash': + shutil.move(file_to_remove, trash_path) + elif method == 'permanent': + os.remove(file_to_remove) + + +def delete_ou_image(file_path, method, extensions): + """ Elimina la imagen basada en OU que recibe en el parámetro "file_path", y todos sus archivos asociados, + moviéndolos a la papelera o eliminándolos permanentemente (en función del parámetro "method"). + """ + # Iteramos las extensiones de los archivos, y construimos la ruta completa de cada uno de ellos: + for ext in extensions: + file_to_remove = f"{file_path}{ext}" + # Si el archivo actual existe, lo eliminamos o lo movemos a la papelera (dependiendo del valor del parámetro "method"), + # y en el último caso lo situamos en un subdirectorio correspondiente a la OU: + if os.path.exists(file_to_remove): + if method == 'trash': + ou_subdir = file_to_remove.split('/')[4] + # Comprobamos si en la papelera existe un subdirectorio correspondiente a la OU (y si no existe lo creamos): + if not os.path.exists(f"{trash_path}{ou_subdir}"): + os.mkdir(f"{trash_path}{ou_subdir}") + shutil.move(file_to_remove, f"{trash_path}{ou_subdir}") + elif method == 'permanent': + os.remove(file_to_remove) + + +def update_repo_info(): + """ Actualiza la información del repositorio, ejecutando el script "updateRepoInfo.py". + Como se ve, es necesario que el script se ejecute como sudo, o dará error. + """ + try: + result = subprocess.run(['sudo', 'python3', update_repo_script], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except subprocess.CalledProcessError as error: + print(f"Error Output: {error.stderr.decode()}") + sys.exit(3) + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + sys.exit(4) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo a eliminar: + file_path = build_file_path() + + # Comprobamos si existe el directorio correspondiente a la papelera, y en caso contrario lo creamos: + if not os.path.exists(trash_path): + print("Creating trash folder...") + create_trash_folder() + + # Especificamos el método de eliminación (permanente o utilizando la papelera): + if len(sys.argv) == 3 and sys.argv[2] == "-p": + method = 'permanent' + else: + method = 'trash' + + # Creamos una lista con las extensiones de los archivos asociados a la imagen + # (incluyendo ninguna extensión, que corresponde a la propia imagen): + extensions = ['', '.sum', '.full.sum', '.torrent', '.info', '.info.checked'] + + # Evaluamos la cantidad de barras que hay en la ruta de la imagen, para diferenciar entre imágenes "normales" y basadas en OU + # (y llamamos a la función correspondiente para eliminarla): + if file_path.count('/') == 4: + print("Deleting normal image...") + delete_normal_image(file_path, method, extensions) + elif file_path.count('/') == 5: + print("Deleting OU based image...") + delete_ou_image(file_path, method, extensions) + + # Actualizamos la información del repositorio, ejecutando el script "updateRepoInfo.py": + print("Updating Repository Info...") + update_repo_info() + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- -- 2.40.1 From ccd0ebd8efa15fbbe01aec520898ad2ed759a017 Mon Sep 17 00:00:00 2001 From: ggil Date: Tue, 20 Aug 2024 13:05:51 +0200 Subject: [PATCH 11/70] refs #631 - Add recoverImage.py --- README.md | 49 +++++---- py_scripts/recoverImage.py | 199 +++++++++++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+), 25 deletions(-) create mode 100644 py_scripts/recoverImage.py diff --git a/README.md b/README.md index d91579e..b1a56dd 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (que corresponde a todas las imágenes monolíticas almacenadas en el repositorio). Se debe crear un script que devuelva dicha información, porque actualmente no hay ninguno. -**URL:** `/ogrepository/v1/images` +**URL:** `/ogrepository/v1/images` **Método HTTP:** GET **Ejemplo de Solicitud:** @@ -95,7 +95,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (correspondiente a la imagen especificada). Se debe crear un script que devuelva dicha información, porque actualmente no hay ninguno. -**URL:** `/ogrepository/v1/images/{name}` +**URL:** `/ogrepository/v1/images/{name}` **Método HTTP:** GET **Parámetros de la URL:** @@ -138,7 +138,7 @@ Se actualizará la información de las imágenes almacenadas en el repositorio, Se puede hacer con el script "**updateRepoInfo.py**", que hemos programado recientemente (y que es similar al script bash original "**checkrepo**"). Este endpoint es llamado por el script "**deleteImage.py**" (para actualizar la información cada vez que se elimine una imagen), y creemos que también debe ser llamado por ogCore u ogLive cada vez que se haya creado una imagen. -**URL:** `/ogrepository/v1/images` +**URL:** `/ogrepository/v1/images` **Método HTTP:** PUT **Ejemplo de Solicitud:** @@ -156,7 +156,7 @@ curl -X PUT -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag Se eliminará la imagen especificada como parámetro, pudiendo eliminarla permanentemente o enviarla a la papelera. Se puede hacer con el script "**deleteimage.py**", que hemos programado recientemente (y que incluye la funcionalidad "papelera"), y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. -**URL:** `/ogrepository/v1/images/{name}` +**URL:** `/ogrepository/v1/images/{name}` **Método HTTP:** DELETE **Cuerpo de la Solicitud (JSON):** @@ -179,7 +179,7 @@ curl -X DELETE -H "Authorization: $API_KEY" -H "Content-Type: application/json" Se recuperará la imagen especificada como parámetro, desde la papelera. Se puede hacer con el script "**recoverImage.py**", que hemos programado recientemente, y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. -**URL:** `/ogrepository/v1/images/{name}` +**URL:** `/ogrepository/v1/images/{name}` **Método HTTP:** POST **Cuerpo de la Solicitud (JSON):** @@ -199,9 +199,9 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d ### Importar una Imagen Se importará una imagen de un repositorio remoto al repositorio local. -Se puede hacer con el script "**importimage**", que actualmente no se utiliza. +Se puede intentar hacer con el script "**importimage**", que actualmente no se utiliza, pero seguramente habrá que modificarlo. -**URL:** `/ogrepository/v1/images/import-image` +**URL:** `/ogrepository/v1/images/import-image` **Método HTTP:** POST **Cuerpo de la Solicitud (JSON):** @@ -223,9 +223,9 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d ### Enviar una Imagen mediante UDPcast Se enviará una imagen por Multicast, mediante la aplicación UDPcast. -Se puede hacer con el script "**sendFileMcast**", que a su vez llama al binario "**udp-sender**", que es quien realmente realiza el envío. +Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binario "**udp-sender**", que es quien realmente realiza el envío. -**URL:** `/ogrepository/v1/images/{protocol}` +**URL:** `/ogrepository/v1/images/{protocol}` **Método HTTP:** POST **Parámetros de la URL:** @@ -257,7 +257,7 @@ Se enviará una imagen por Unicast o Multicast, mediante el protocolo "UFTP". Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente los clientes ogLive destino se pongan en escucha con un daemon "UFTPD" (ejecutando el script "**listenUFTPD.py**"). **NOTA**: Los envíos mediante "UFTP" funcionan al revés que los envíos mediante "UDPcast" (con este último, primero se debe ejecutar un comando en el servidor, y luego en los clientes). -**URL:** `/ogrepository/v1/images/{protocol}` +**URL:** `/ogrepository/v1/images/{protocol}` **Método HTTP:** POST **Parámetros de la URL:** @@ -286,7 +286,7 @@ Se creará un archivo ".torrent" para la imagen especificada como parámetro. Se debe crear un script que realice dicha tarea, porque actualmente se hace mediante el script "**torrent-creator**", que se ejecuta por crontab a cada minuto (y crea un archivo ".torrent" por cada imagen que no tenga uno asociado). **NOTA**: Puede que sea preferible que esta acción la realice el propio ogLive al crear una imagen, ya que también tiene las herramientas para hacerlo. -**URL:** `/ogrepository/v1/images/create-torrent` +**URL:** `/ogrepository/v1/images/create-torrent` **Método HTTP:** POST **Cuerpo de la Solicitud (JSON):** @@ -306,9 +306,9 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d ### Enviar una Imagen mediante P2P Se debe hacer tracking de los torrents almacenados en ogRepository, e iniciar la transferencia de la imagen especificada (además, los clientes deben disponer del torrent asociado, y añadirlo a su cliente torrent). -No tengo claro cómo se haría con los scripts existentes (que utilizan "bttrack" y "ctorrent"), pero si usáramos "opentracker" y "transmission", se debería crear nuevos scripts. +No tengo claro cómo se haría con los scripts existentes (que utilizan "bttrack" y "ctorrent"), pero si usáramos "opentracker" y "transmission" (como se había propuesto), se debería crear nuevos scripts. -**URL:** `/ogrepository/v1/images/{protocol}` +**URL:** `/ogrepository/v1/images/{protocol}` **Método HTTP:** POST **Parámetros de la URL:** @@ -331,9 +331,9 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d ### Chequear Integridad de Imagen Se comprobará la integridad de todos los ficheros asociados a la imagen especificada como parámetro. -Para esto, entiendo que se debe crear un script que compare el contenido de los ficheros "**.sum**" y "**.full.sum**" con una nueva extracción del checksum de la imagen, pero no veo como comprobar la integridad de todos los archivos asociados. +Para esto, entiendo que se debe crear un script que compare el contenido de los ficheros "**.sum**" y "**.full.sum**" con una nueva obtención del checksum de la imagen, pero no veo como comprobar la integridad de todos los archivos asociados. -**URL:** `/ogrepository/v1/images/check-image` +**URL:** `/ogrepository/v1/images/check-image` **Método HTTP:** GET **Cuerpo de la Solicitud (JSON):** @@ -354,11 +354,10 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d ### Exportar una Imagen Se exportará una imagen del repositorio local a un repositorio remoto. -Se debe crear un script que realice dicha tarea (o se puede utilizar el script "**importimage**", que realiza la acción contraria, pero que actualmente no se utiliza). - +Se debe crear un script que realice dicha tarea (o se puede intentar utilizar el script "**importimage**", que realiza la acción contraria, pero que actualmente no se utiliza). **NOTA**: Aunque no se indica en el pliego, entendemos que también será necesario especificar credenciales de acceso al repositorio remoto como parámetros de entrada. -**URL:** `/ogrepository/v1/images/export-image` +**URL:** `/ogrepository/v1/images/export-image` **Método HTTP:** POST **Cuerpo de la Solicitud (JSON):** @@ -380,11 +379,11 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d ### Definir Imagen Global Se marcará como "global" la imagen especificada como parámetro. -En principio, esto requerirá agregar una nueva clave al archivo "**/opt/opengnsys/etc/repoinfo.json**" (por ejemplo, "scope"), modificando el script "**checkrepo**", para que realice dicha modificación. -También debe crearse un script que realice la definición de la imagen especificada (modificando el valor de la nueva clave del archivo "**/opt/opengnsys/etc/repoinfo.json**", de "local" a "global", por ejemplo). +En principio, esto requerirá agregar una nueva clave al archivo "**/opt/opengnsys/etc/repoinfo.json**" (por ejemplo, "scope"), modificando el script "**updateRepoInfo.py**", para que realice dicha modificación. +También debe crearse un script que realice la definición de la imagen especificada (modificando el valor de la nueva clave del archivo "**/opt/opengnsys/etc/repoinfo.json**", de "local" a "global", por ejemplo). Además, deberá llamarse a un script que exporte dicha imagen a los demás repositorios gestionados por el servidor de administración (que aun no está creado). -**URL:** `/ogrepository/v1/images/set-global` +**URL:** `/ogrepository/v1/images/set-global` **Método HTTP:** PUT **Cuerpo de la Solicitud (JSON):** @@ -403,12 +402,12 @@ curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d --- ### Definir Imagen Local -Se marcará como "local" la imagen especificada como parámetro, que previamente habría sido marcada como "global" (ya que de forma predeterminada, todas las imágenes estarán marcadas como "local"). -Como comentábamos en el endpoint precedentte, esto requerirá agregar una nueva clave al archivo "**/opt/opengnsys/etc/repoinfo.json**" (por ejemplo, "scope"), modificando el script "**checkrepo**", para que realice dicha modificación. +Se marcará como "local" la imagen especificada como parámetro, que previamente habría sido marcada como "global" (ya que entiendo que de forma predeterminada, todas las imágenes estarán marcadas como "local"). +Como comentábamos en el endpoint precedentte, esto requerirá agregar una nueva clave al archivo "**/opt/opengnsys/etc/repoinfo.json**" (por ejemplo, "scope"), modificando el script "**updateRepoInfo.py**", para que realice dicha modificación. También debe crearse un script que realice la definición de la imagen especificada (modificando el valor de la nueva clave del archivo "**/opt/opengnsys/etc/repoinfo.json**", de "global" a "local", por ejemplo). Este endpoint deberá ser llamado en todos los repositorios gestionados por el mismo servidor de administración (para que todos hagan la modificación). -**URL:** `/ogrepository/v1/images/set-local` +**URL:** `/ogrepository/v1/images/set-local` **Método HTTP:** PUT **Cuerpo de la Solicitud (JSON):** @@ -429,7 +428,7 @@ curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se devolverá información del estado de las transmisiones existentes, con un identificador de cada sesión multicast o P2P, y la imagen asociada. Se debe estudiar como realizar esta tarea para cada uno de los protocolos de transmisión, ya que cada uno tiene sus particularidades, y habitualmente no tienen comandos asociados para comprobar el estado de las transmisiones. -Y tampoco está claro que protocolo se utilizará para transimisiones Multicast (¿"UDPcast", "UFTP", o ambos?), ni qué programas se utilizarán para P2P (¿"ctorrent/bttrack" u "opentracker/Transmission"?). +Y tampoco está claro qué protocolo se utilizará para transimisiones Multicast (¿"UDPcast", "UFTP", o ambos?), ni qué programas se utilizarán para P2P (¿"ctorrent/bttrack" u "opentracker/Transmission"?). **NOTA**: Posiblemente deba crearse un endpoint específico para cada uno de los protocolos que se utilicen. --- diff --git a/py_scripts/recoverImage.py b/py_scripts/recoverImage.py new file mode 100644 index 0000000..15281de --- /dev/null +++ b/py_scripts/recoverImage.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script recupera la imagen que recibe como parámetro (y todos sus archivos asociados), moviendo los archivos a "/opt/opengnsys/images", desde la papelera + (respetando el subdirectorio correspondiente a la OU, si fuera el caso). +Llama al script "updateRepoInfo.py", para actualizar la información del repositorio. + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a recuperar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/images_trash/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/images_trash/ou_subdir/image1.img + + Sintaxis +---------- +./recoverImage.py [ou_subdir/]image_name|/image_path/image_name + + Ejemplos + --------- +./recoverImage.py image1.img +./recoverImage.py /opt/opengnsys/images_trash/image1.img +./recoverImage.py ou_subdir/image1.img +./recoverImage.py /ou_subdir/image1.img +./recoverImage.py /opt/opengnsys/images_trash/ou_subdir/image1.img +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import shutil +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/images/' +trash_path = '/opt/opengnsys/images_trash/' +update_repo_script = '/opt/opengnsys/py_scripts/updateRepoInfo.py' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name + Ejemplo1: {script_name} image1.img + Ejemplo2: {script_name} /opt/opengnsys/images_trash/image1.img + Ejemplo3: {script_name} ou_subdir/image1.img + Ejemplo4: {script_name} /ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/images_trash/ou_subdir/image1.img + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script sin parámetros, se muestra un error y la ayuda, y se sale del script: + if len(sys.argv) == 1: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar al menos 1 parámetro") + show_help() + sys.exit(0) + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + elif len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(1) + # Si se ejecuta el script con más de 1 parámetro, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) > 2: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 parámetro") + show_help() + sys.exit(2) + + +def build_file_path(): + """ Construye la ruta completa al archivo a recuperar + (agregando "/opt/opengnsys/images_trash/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "trash_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(trash_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(trash_path): + file_path = os.path.join(trash_path, param_path) + else: + file_path = param_path + return file_path + + +def recover_normal_image(file_path, extensions): + """ Recupera la imagen "normal" que recibe en el parámetro "file_path", y todos sus archivos asociados, + moviéndolos a "/opt/opengnsys/images" (desde la papelera). + """ + # Iteramos las extensiones de los archivos, y construimos la ruta completa de cada uno de ellos: + for ext in extensions: + file_to_recover = f"{file_path}{ext}" + # Si el archivo actual existe, lo movemos a "/opt/opengnsys/images" (recuperándolo desde la papelera): + if os.path.exists(file_to_recover): + # Si la extensión del archivo actual es ".info.checked" la renombramos a ".info" (para que lo pille "updateRepoInfo"): + if ext == '.info.checked': + os.rename(file_to_recover, file_to_recover.strip('.checked')) + file_to_recover = file_to_recover.strip('.checked') + shutil.move(file_to_recover, repo_path) + + +def recover_ou_image(file_path, extensions): + """ Recupera la imagen basada en OU que recibe en el parámetro "file_path", y todos sus archivos asociados, + moviéndolos a "/opt/opengnsys/images" (desde la papelera), respetando el subdirectorio correspondiente a la OU. + """ + # Iteramos las extensiones de los archivos, y construimos la ruta completa de cada uno de ellos: + for ext in extensions: + file_to_recover = f"{file_path}{ext}" + # Si el archivo actual existe, lo movemos a "/opt/opengnsys/images/ou_subdir": + if os.path.exists(file_to_recover): + # Si la extensión del archivo actual es ".info.checked" la renombramos a ".info" (para que lo pille "updateRepoInfo"): + if ext == '.info.checked': + os.rename(file_to_recover, file_to_recover.strip('.checked')) + file_to_recover = file_to_recover.strip('.checked') + ou_subdir = file_to_recover.split('/')[4] + # Comprobamos si en "repo_path" existe un subdirectorio correspondiente a la OU (y si no existe lo creamos): + if not os.path.exists(f"{repo_path}{ou_subdir}"): + os.mkdir(f"{repo_path}{ou_subdir}") + shutil.move(file_to_recover, f"{repo_path}{ou_subdir}") + + +def update_repo_info(): + """ Actualiza la información del repositorio, ejecutando el script "updateRepoInfo.py". + Como se ve, es necesario que el script se ejecute como sudo, o dará error. + """ + try: + result = subprocess.run(['sudo', 'python3', update_repo_script], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except subprocess.CalledProcessError as error: + print(f"Error Output: {error.stderr.decode()}") + sys.exit(3) + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + sys.exit(4) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo a eliminar: + file_path = build_file_path() + + # Creamos una lista con las extensiones de los archivos asociados a la imagen + # (incluyendo ninguna extensión, que corresponde a la propia imagen): + extensions = ['', '.sum', '.full.sum', '.torrent', '.info', '.info.checked'] + + # Evaluamos la cantidad de barras que hay en la ruta de la imagen, para diferenciar entre imágenes "normales" y basadas en OU + # (y llamamos a la función correspondiente para recuperarla): + if file_path.count('/') == 4: + print("Recovering normal image...") + recover_normal_image(file_path, extensions) + elif file_path.count('/') == 5: + print("Recovering OU based image...") + recover_ou_image(file_path, extensions) + + # Actualizamos la información del repositorio, ejecutando el script "updateRepoInfo.py": + print("Updating Repository Info...") + update_repo_info() + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- + + -- 2.40.1 From 99bd49d73074e769e42bde8a5b499ed4af9643a7 Mon Sep 17 00:00:00 2001 From: ggil Date: Wed, 21 Aug 2024 12:29:42 +0200 Subject: [PATCH 12/70] refs #631 - Add updateTrashInfo.py --- README.md | 91 +++++------ py_scripts/updateRepoInfo.py | 23 ++- py_scripts/updateTrashInfo.py | 274 ++++++++++++++++++++++++++++++++++ 3 files changed, 342 insertions(+), 46 deletions(-) create mode 100644 py_scripts/updateTrashInfo.py diff --git a/README.md b/README.md index b1a56dd..b8d802c 100644 --- a/README.md +++ b/README.md @@ -20,15 +20,15 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará ### Tabla de Contenido: 1. [Obtener Información de todas las Imágenes](#obtener-información-de-todas-las-imágenes) - `GET /ogrepository/v1/images` -2. [Obtener Información de una Imagen concreta](#obtener-información-de-una-imagen-concreta) - `GET /ogrepository/v1/images/{name}` +2. [Obtener Información de una Imagen concreta](#obtener-información-de-una-imagen-concreta) - `GET /ogrepository/v1/images/get-info` 3. [Actualizar Información del Repositorio](#actualizar-información-del-repositorio) - `PUT /ogrepository/v1/images` -4. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/{name}` -5. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/images/{name}` +4. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/delete-image` +5. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/images/recover-image` 6. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/images/import-image` -7. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/images/{protocol}` -8. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/images/{protocol}` +7. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/images/send-udpcast` +8. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/images/send-uftp` 9. [Crear archivo ".torrent"](#crear-archivo-torrent) - `POST /ogrepository/v1/images/create-torrent` -10. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/images/{protocol}` +10. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/images/send-p2p` 11. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/images/check-image` 12. [Exportar una Imagen](#exportar-una-imagen) - `POST /ogrepository/v1/images/export-image` 13. [Definir Imagen Global](#definir-imagen-global) - `PUT /ogrepository/v1/images/set-global` @@ -95,16 +95,17 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (correspondiente a la imagen especificada). Se debe crear un script que devuelva dicha información, porque actualmente no hay ninguno. -**URL:** `/ogrepository/v1/images/{name}` +**URL:** `/ogrepository/v1/images/get-info` **Método HTTP:** GET -**Parámetros de la URL:** -- `{name}`: Nombre de la imagen. +**Cuerpo de la Solicitud (JSON):** +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). **Ejemplo de Solicitud:** ```bash -curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/Windows10 +curl -X GET -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img","ou_subdir":"none"}' http://example.com/ogrepository/v1/images/get-info ``` **Respuestas:** @@ -156,17 +157,18 @@ curl -X PUT -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag Se eliminará la imagen especificada como parámetro, pudiendo eliminarla permanentemente o enviarla a la papelera. Se puede hacer con el script "**deleteimage.py**", que hemos programado recientemente (y que incluye la funcionalidad "papelera"), y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. -**URL:** `/ogrepository/v1/images/{name}` +**URL:** `/ogrepository/v1/images/delete-image` **Método HTTP:** DELETE **Cuerpo de la Solicitud (JSON):** +- **image**: Nombre de la imagen (con extensión). - **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). - **method**: Método de eliminación (puede ser "trash" o "permanent"). **Ejemplo de Solicitud:** ```bash -curl -X DELETE -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ou_subdir":"none", "method":"trash"}' http://example.com/ogrepository/v1/images/Windows10.img +curl -X DELETE -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none", "method":"trash"}' http://example.com/ogrepository/v1/images/delete-image ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al eliminar la imagen. @@ -179,16 +181,17 @@ curl -X DELETE -H "Authorization: $API_KEY" -H "Content-Type: application/json" Se recuperará la imagen especificada como parámetro, desde la papelera. Se puede hacer con el script "**recoverImage.py**", que hemos programado recientemente, y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. -**URL:** `/ogrepository/v1/images/{name}` +**URL:** `/ogrepository/v1/images/recover-image` **Método HTTP:** POST **Cuerpo de la Solicitud (JSON):** +- **image**: Nombre de la imagen (con extensión). - **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ou_subdir":"none"}' http://example.com/ogrepository/v1/images/Windows10.img +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/recover-image ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al recuperar la imagen. @@ -207,12 +210,13 @@ Se puede intentar hacer con el script "**importimage**", que actualmente no se u **Cuerpo de la Solicitud (JSON):** - **user**: Usuario con el que acceder al repositorio remoto (por defecto, usuario local). - **repo**: IP o hostname del repositorio remoto. -- **image**: Nombre de la imagen a importar. +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"user":"user_name", "repo":"192.168.56.100", "image":"Windows10"}' http://example.com/ogrepository/v1/images/import-image +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"user":"user_name", "repo":"192.168.56.100", "image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/import-image ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al importar la imagen. @@ -225,14 +229,12 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se enviará una imagen por Multicast, mediante la aplicación UDPcast. Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binario "**udp-sender**", que es quien realmente realiza el envío. -**URL:** `/ogrepository/v1/images/{protocol}` +**URL:** `/ogrepository/v1/images/send-udpcast` **Método HTTP:** POST -**Parámetros de la URL:** -- `{protocol}`: Protocolo que se utilizará para enviar la imagen (en este caso, "udpcast"). - **Cuerpo de la Solicitud (JSON):** -- **image**: Nombre completo de la imagen a enviar, con extensión (con o sin ruta). +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). - **port**: Puerto Multicast. - **method**: Modalidad half-duplex o full-duplex. - **ip**: IP Multicast. @@ -243,7 +245,7 @@ Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binar **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "port":"9000", "method":"full", "ip":"239.194.17.2", "bitrate":"70M", "nclients":"20", "maxtime":"120"}' http://example.com/ogrepository/v1/images/udpcast +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none", "port":"9000", "method":"full", "ip":"239.194.17.2", "bitrate":"70M", "nclients":"20", "maxtime":"120"}' http://example.com/ogrepository/v1/images/send-udpcast ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. @@ -257,14 +259,12 @@ Se enviará una imagen por Unicast o Multicast, mediante el protocolo "UFTP". Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente los clientes ogLive destino se pongan en escucha con un daemon "UFTPD" (ejecutando el script "**listenUFTPD.py**"). **NOTA**: Los envíos mediante "UFTP" funcionan al revés que los envíos mediante "UDPcast" (con este último, primero se debe ejecutar un comando en el servidor, y luego en los clientes). -**URL:** `/ogrepository/v1/images/{protocol}` +**URL:** `/ogrepository/v1/images/send-uftp` **Método HTTP:** POST -**Parámetros de la URL:** -- `{protocol}`: Protocolo que se utilizará para enviar la imagen (en este caso, "uftp"). - **Cuerpo de la Solicitud (JSON):** -- **image**: Nombre completo de la imagen a enviar, con extensión (con o sin ruta). +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). - **port**: Puerto Multicast. - **ip**: IP Unicast/Multicast. - **bitrate**: Velocidad de transmisión (con "K" para Kbps, "M" para Mbps o "G" para Gbps). @@ -272,7 +272,7 @@ Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "port":"9000", "ip":"239.194.17.2", "bitrate":"1G"}' http://example.com/ogrepository/v1/images/uftp +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none", "port":"9000", "ip":"239.194.17.2", "bitrate":"1G"}' http://example.com/ogrepository/v1/images/send-uftp ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. @@ -290,12 +290,13 @@ Se debe crear un script que realice dicha tarea, porque actualmente se hace medi **Método HTTP:** POST **Cuerpo de la Solicitud (JSON):** -- **image**: Nombre completo de la imagen a la que asociar un archivo torrent. +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img"}' http://example.com/ogrepository/v1/images/create-torrent +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/create-torrent ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al crear el archivo torrent. @@ -308,19 +309,17 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se debe hacer tracking de los torrents almacenados en ogRepository, e iniciar la transferencia de la imagen especificada (además, los clientes deben disponer del torrent asociado, y añadirlo a su cliente torrent). No tengo claro cómo se haría con los scripts existentes (que utilizan "bttrack" y "ctorrent"), pero si usáramos "opentracker" y "transmission" (como se había propuesto), se debería crear nuevos scripts. -**URL:** `/ogrepository/v1/images/{protocol}` +**URL:** `/ogrepository/v1/images/send-p2p` **Método HTTP:** POST -**Parámetros de la URL:** -- `{protocol}`: Protocolo que se utilizará para enviar la imagen (en este caso, "p2p"). - **Cuerpo de la Solicitud (JSON):** -- **image**: Nombre completo de la imagen a enviar, con extensión (con o sin ruta). +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img"}' http://example.com/ogrepository/v1/images/p2p +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/send-p2p ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. @@ -337,12 +336,13 @@ Para esto, entiendo que se debe crear un script que compare el contenido de los **Método HTTP:** GET **Cuerpo de la Solicitud (JSON):** -- **image**: Nombre completo de la imagen a chequear, con extensión (con o sin ruta). +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img"}' http://example.com/ogrepository/v1/images/check-image +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/check-image ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al chequear la imagen. @@ -363,12 +363,13 @@ Se debe crear un script que realice dicha tarea (o se puede intentar utilizar el **Cuerpo de la Solicitud (JSON):** - **repo**: IP o hostname del repositorio remoto. - **ou**: Unidad Organizativa del repositorio remoto. -- **image**: Nombre de la imagen a exportar. +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"repo":"192.168.56.200", "ou":"OU_Ejemplo", "image":"Windows10"}' http://example.com/ogrepository/v1/images/export-image +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"repo":"192.168.56.200", "ou":"OU_Ejemplo", "image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/export-image ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al exportar la imagen. @@ -387,12 +388,13 @@ Además, deberá llamarse a un script que exporte dicha imagen a los demás repo **Método HTTP:** PUT **Cuerpo de la Solicitud (JSON):** -- **image**: Nombre completo de la imagen a definir como global, con extensión (con o sin ruta). +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). **Ejemplo de Solicitud:** ```bash -curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img"}' http://example.com/ogrepository/v1/images/set-global +curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/set-global ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al definir la imagen. @@ -411,12 +413,13 @@ Este endpoint deberá ser llamado en todos los repositorios gestionados por el m **Método HTTP:** PUT **Cuerpo de la Solicitud (JSON):** -- **image**: Nombre completo de la imagen a definir como local, con extensión (con o sin ruta). +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). **Ejemplo de Solicitud:** ```bash -curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img"}' http://example.com/ogrepository/v1/images/set-local +curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/set-local ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al definir la imagen. diff --git a/py_scripts/updateRepoInfo.py b/py_scripts/updateRepoInfo.py index 0b5221a..c6a0514 100644 --- a/py_scripts/updateRepoInfo.py +++ b/py_scripts/updateRepoInfo.py @@ -6,8 +6,7 @@ Este script actualiza la información de las imágenes del repositorio, reflejá añadiendo información de las imágenes nuevas, y borrando la información de las imágenes que fueron eliminadas. La información es obtenida desde archivos ".info", que originalmente eran eliminados, pero que en esta versión son renombrados a ".info.checked", obteniendo la misma funcionalidad. - -En principio, debería hacer lo mismo que el script bash original (cuyo nombre es "checkrepo"), salvo por el detalle que acabamos de comentar. +Al acabar, llama al script "updateTrashInfo.py", para actualizar también la información de la papelera del repositorio. No recibe ningún parámetro, y debería ser llamado por el propio ogRepoitory cada vez que se elimine una imagen (desde el script "deleteImage.py"), y por ogCore u ogLive cada vez que se cree una imagen. @@ -30,6 +29,8 @@ import shutil repo_path = '/opt/opengnsys/images' info_file = '/opt/opengnsys/etc/repoinfo.json' +#update_trash_script = '/opt/opengnsys/py_scripts/updateTrashInfo.py' +update_trash_script = '/home/administrador/updateTrashInfo.py' # -------------------------------------------------------------------------------------------- @@ -236,6 +237,21 @@ def remove_from_json(): +def update_trash_info(): + """ Actualiza la información de la papelera del repositorio, ejecutando el script "updateTrashInfo.py". + Como se ve, es necesario que el script se ejecute como sudo, o dará error. + """ + try: + result = subprocess.run(['sudo', 'python3', update_trash_script], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except subprocess.CalledProcessError as error: + print(f"Error Output: {error.stderr.decode()}") + sys.exit(3) + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + sys.exit(4) + + + # -------------------------------------------------------------------------------------------- # MAIN # -------------------------------------------------------------------------------------------- @@ -272,6 +288,9 @@ def main(): print("Removing deleted images...") remove_from_json() + # Actualizamos la información de la papelera, ejecutando el script "updateTrashInfo.py": + print("Updating Trash Info...") + update_trash_info() # -------------------------------------------------------------------------------------------- diff --git a/py_scripts/updateTrashInfo.py b/py_scripts/updateTrashInfo.py new file mode 100644 index 0000000..233f6dd --- /dev/null +++ b/py_scripts/updateTrashInfo.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script actualiza la información de las imágenes de la papelera del repositorio, reflejándola en el archivo "/opt/opengnsys/etc/trashinfo.json", + añadiendo información de las nuevas imágenes eliminadas, y borrando la información de las imágenes que fueron restauradas (o que ya no están en la papelera). +La información es obtenida desde archivos ".info.checked", cuyo nombre original era ".info" + (pero que fueron renombrados al ser insertada su información en el archivo ""/opt/opengnsys/etc/repoinfo.json", antes de ser eliminadas). + +No recibe ningún parámetro, y no necesita ser llamado explícitamente (porque lo llama el script "updateRepoInfo.py")". +""" + + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import json +import subprocess +import shutil + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +trash_path = '/opt/opengnsys/images_trash' +info_file = '/opt/opengnsys/etc/trashinfo.json' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def create_empty_json(): + """ Esta función crea el archivo "trashinfo.json", con la estructura básica que debe contener. + Evidentemente, solo es llamada cuando no existe el archivo. + """ + # Creamos un diccionario con la estructura básica que debe tener el archivo json: + json_data = {"directory": trash_path, "images": [], "ous": []} + + # Abrimos el archivo json en modo escritura (creándolo si no existe, como es el caso), + # y le añadimos la estructura almacenada en el diccionario "json_data": + with open(info_file, 'w') as file: + json.dump(json_data, file, indent=2) + + + +def check_files(): + """ Esta función recorre el directorio de imágenes, para buscar archivos con extensiones ".img" o ".dsk". + Llama a la función "add_to_json" para que esta añada información de las imágenes no bloqueadas (sin archivo ".lock"), + que además tengan un archivo ".info.checked" asociado (siempre que este no se haya modificado antes que la propia imagen). + """ + # Iteramos recursivamente todos los archivos y directorios de "/opt/opengnsys/images_trash", + # y luego iteramos todos los archivos encontrados (en una iteración anidada): + for root, dirs, files in os.walk(trash_path): + for file in files: + # Si la imagen actual tiene extensión ".img" o ".dsk", construimos la ruta completa ("img_path"): + if file.endswith((".img", ".dsk")): + img_path = os.path.join(root, file) + # Si existe un archivo ".lock" asociado a la imagen actual, pasamos a la siguiente: + if os.path.exists(f"{img_path}.lock"): + continue + # Comprobamos si existe un archivo ".info.checked" asociado a la imagen actual: + info_file = f"{img_path}.info.checked" + if os.path.exists(info_file): + # Si la fecha de modificación del archivo ".info.checked" es anterior a la de la imagen, lo eliminamos (porque estará desactualizado): + if os.path.getmtime(info_file) < os.path.getmtime(img_path): + os.remove(info_file) + print(f"Warning: Deleted outdated file {info_file}") + # En caso contrario, almacenamos el contenido del archivo ".info.checked" (del tipo "PARTCLONE:LZOP:EXTFS:8500000:Ubuntu_20") en la variable "data": + else: + with open(info_file, 'r') as file: + data = file.read() + # Llamamos a la función "add_to_json", para que inserte la información de la imagen en el archivo json + # (pasándole el nombre de la imagen, la extensión, y los datos extraídos del archivo ".info.checked"): + img_name = os.path.relpath(img_path, trash_path) + add_to_json(os.path.splitext(img_name)[0], os.path.splitext(img_name)[1][1:], data) + + + +def check_dirs(): + """ Esta función recorre el directorio de imágenes, para buscar archivos con nombre "ogimg.info". + Llama a la función "add_to_json" para que esta añada información de las imágenes no bloqueadas (sin archivo ".lock"), + que además tengan un archivo "ogimg.info" asociado (pero no sé que tipo de imágenes son esas). + """ + # Iteramos recursivamente todos los archivos y directorios de "/opt/opengnsys/images_trash", + # y luego iteramos todos los archivos encontrados (en una iteración anidada): + for root, dirs, files in os.walk(trash_path): + for file in files: + # Si el nombre del archivo actual es "ogimg.info", construimos su ruta completa ("info_path"), y la ruta del directorio ("img_path"): + if file == "ogimg.info": + info_path = os.path.join(root, file) + img_path = os.path.dirname(info_path) + # Si existe un archivo ".lock" asociado a la imagen actual, o si la ruta coincide con "trash_path", pasamos a la siguiente: + if img_path == trash_path or os.path.exists(f"{img_path}.lock"): + continue + # Almacenamos el contenido del archivo "ogimg.info", en la variable "lines": + with open(info_path, 'r') as file: + lines = file.readlines() + # Iteramos las lineas obtenidas, para extraer el tipo de sistema de archivos ("fstype") y el tamaño de los datos ("sizedata"), + # y con ello construimos una cadena "rsync::{fstype}:{sizedata}:" (que almacenamos en "data"): + for line in lines: + if line.startswith("# fstype"): + fstype = line.split("=")[1].strip() + elif line.startswith("# sizedata"): + sizedata = line.split("=")[1].strip() + data = f"rsync::{fstype}:{sizedata}:" + # Llamamos a la función "add_to_json", para que inserte la información de la imagen en el archivo json + # (pasándole el nombre de la imagen, el tipo "dir"", y los datos extraídos del archivo "ogimg.info"): + img_name = os.path.relpath(img_path, trash_path) + add_to_json(img_name, "dir", data) + + + +def add_to_json(image_name, image_type, data): + """ Esta función añade al archivo "trashinfo.json" la información de las imágenes que aun no ha sido introducida en él (imágenes eliminadas recientemente, básicamente). + El procedimiento es diferente para las imágenes "normales" y para las imágenes basadas en OU. + """ + # Almacenamos el contenido de la variable "data" (del tipo "PARTCLONE:LZOP:EXTFS:8500000:Ubuntu_20") en variables separadas: + clonator, compressor, fstype, datasize, client = data.split(":") + + # Si la imagen está dentro de una OU (directorio), extraemos el nombre de la OU y de la imagen: + ou_name = None + if '/' in image_name: + ou_name = os.path.dirname(image_name) + image_name = os.path.basename(image_name) + + # Creamos un diccionario con los datos de la imagen, que luego insertaremos en el json: + json_data = { + "name": image_name, + "type": image_type.lower(), + "clientname": client.strip('\n'), # Eliminamos el salto de linea + "clonator": clonator.lower(), + "compressor": compressor.lower(), + "filesystem": fstype.upper(), + "datasize": int(datasize) * 1024 # Convertimos el valor a bytes (desde KB) + } + # Almacenamos el contenido del archivo "trashinfo.json" en la variable "info_data": + with open(info_file, 'r') as file: + info_data = json.load(file) + + # Comprobamos si las claves "info_data" (o sea, del archivo json) son las correctas: + if set(info_data.keys()) == {"directory", "images", "ous"}: + + # Actualizamos la información de las imágenes "normales" (no basadas en OU): + if ou_name is None: + img_index = next((i for i, img in enumerate(info_data["images"]) if img["name"] == image_name), None) + if img_index is not None: + # Update if image data changes + if info_data["images"][img_index] != json_data: + info_data["images"][img_index] = json_data + else: + # Append a new entry + info_data["images"].append(json_data) + + # Actualizamos la información de las imágenes basadas en OU: + else: + ou_index = next((i for i, ou in enumerate(info_data["ous"]) if ou["subdir"] == ou_name), None) + if ou_index is None: + # Append a new OU entry + info_data["ous"].append({"subdir": ou_name, "images": [json_data]}) + else: + img_index = next((i for i, img in enumerate(info_data["ous"][ou_index]["images"]) if img["name"] == image_name), None) + if img_index is not None: + # Update if image data changes + if info_data["ous"][ou_index]["images"][img_index] != json_data: + info_data["ous"][ou_index]["images"][img_index] = json_data + else: + # Append a new entry + info_data["ous"][ou_index]["images"].append(json_data) + + # Si las claves de "info_data" no son las correctas, creamos toda la estructura: + else: + if ou_name is None: + info_data = {"directory": trash_path, "images": [json_data], "ous": []} + else: + info_data = {"directory": trash_path, "images": [], "ous": [{"subdir": ou_name, "images": [json_data]}]} + + # Sobreescribimos el archivo "trashinfo.json", con el contenido de "info_data" (que estará actualizado): + with open(info_file, 'w') as file: + json.dump(info_data, file, indent=2) + + + +def remove_from_json(): + """ Esta función carga el contenido del archivo "trashinfo.json", y comprueba la existencia de las imágenes especificadas allí. + Elimina las claves correspondientes a imágenes inexistentes en la papelera, y posteriormente sobreescribe el archivo "trashinfo.json". + """ + # Almacenamos el contenido del archivo "trashinfo.json", en la variable "json_data": + with open(info_file, 'r') as file: + json_data = json.load(file) + + # Iteramos las imágenes de la clave "images" de "json_data", construimos sus rutas y comprobamos su existencia + # (si no existen, las eliminamos de "json_data"): + for i, image in enumerate(json_data.get("images", [])): + img_name = image["name"] + img_type = image["type"] + if img_type != "dir": + img_name = f"{img_name}.{img_type}" + img_path = os.path.join(trash_path, img_name) + if not os.path.exists(img_path) and not os.path.exists(f"{img_path}.lock"): + json_data["images"].pop(i) + + # Iteramos las imágenes de la clave "ous" de "json_data", construimos sus rutas y comprobamos su existencia + # (si no existen, las eliminamos de "json_data"): + for j, ou in enumerate(json_data.get("ous", [])): + ou_name = ou["subdir"] + ou_path = os.path.join(trash_path, ou_name) + if not os.path.exists(ou_path): + json_data["ous"].pop(j) + else: + for i, image in enumerate(ou.get("images", [])): + img_name = image["name"] + img_type = image["type"] + if img_type != "dir": + img_name = f"{img_name}.{img_type}" + img_path = os.path.join(ou_path, img_name) + if not os.path.exists(img_path) and not os.path.exists(f"{img_path}.lock"): + ou["images"].pop(i) + + # Sobreescribimos el archivo "trashinfo.json", con el contenido de "json_data" + # (una vez eliminadas las imágenes inexistentes): + with open(info_file, 'w') as file: + json.dump(json_data, file, indent=2) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Comprobamos si tenemos permisos de escritura sobre el directorio que contiene el archivo "trashinfo.json" + # ("/opt/opengnsys/etc"), y en caso contrario lanzamos una excepción: + if not os.access(os.path.dirname(info_file), os.W_OK): + raise PermissionError(f"Cannot access {info_file}") + + # Comprobamos si existe el archivo "trashinfo.json", y en caso contrario lo creamos: + if not os.path.exists(info_file): + print("Creating empty json file...") + create_empty_json() + + # Comprobamos si tenemos permisos de escritura sobre el archivo, y en caso contrario lanzamos un error: + if not os.access(info_file, os.W_OK): + raise PermissionError(f"Cannot access {info_file}") + + # Llamamos a la función "check_files", para añadir al archivo json las imágenes ".img" o ".dsk" aun no añadidas + # (que son las que tienen asociado un archivo ".info.checked"): + print("Checking file images...") + check_files() + + # Llamamos a la función "check_dirs", para añadir al archivo json las imágenes aun no añadidas que tienen asociado un archivo "ogimg.info" + # (no sé qué imágenes son estas, pero de alguna forma están basadas en directorios): + print("Checking dir images...") + check_dirs() + + # Llamamos a la función "remove_from_json", para eliminar del archivo json las imágenes que ya no están en la papelera: + print("Removing inexistent images...") + remove_from_json() + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- -- 2.40.1 From cbba3388fc0b004f6f19149fdb008d4ba97c1646 Mon Sep 17 00:00:00 2001 From: ggil Date: Wed, 21 Aug 2024 13:19:35 +0200 Subject: [PATCH 13/70] refs #631 - Modify updateTrashInfo.py --- py_scripts/updateTrashInfo.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/py_scripts/updateTrashInfo.py b/py_scripts/updateTrashInfo.py index 233f6dd..a82677a 100644 --- a/py_scripts/updateTrashInfo.py +++ b/py_scripts/updateTrashInfo.py @@ -16,6 +16,8 @@ No recibe ningún parámetro, y no necesita ser llamado explícitamente (porque # -------------------------------------------------------------------------------------------- import os +import pwd +import grp import json import subprocess import shutil @@ -34,6 +36,23 @@ info_file = '/opt/opengnsys/etc/trashinfo.json' # -------------------------------------------------------------------------------------------- + +def create_trash_folder(): + """ Crea el directorio correspondiente a la papelera, y le asigna propietarios y permisos. + Evidentemente, esta función solo es llamada cuando no existe el directorio. + """ + # Obtenemos el UID del usuario "root" y el GID del grupo "opengnsys": + uid = pwd.getpwnam('root').pw_uid + gid = grp.getgrnam('opengnsys').gr_gid + # Creamos el directorio correspondiente a la papelera: + os.mkdir(trash_path) + # Asignamos el usuario y el grupo propietarios del directorio: + os.chown(trash_path, uid, gid) + # Asignamos permisos "775" al directorio : + os.chmod(trash_path, 0o775) + + + def create_empty_json(): """ Esta función crea el archivo "trashinfo.json", con la estructura básica que debe contener. Evidentemente, solo es llamada cuando no existe el archivo. @@ -236,6 +255,11 @@ def remove_from_json(): def main(): """ """ + # Comprobamos si existe el directorio correspondiente a la papelera, y en caso contrario lo creamos: + if not os.path.exists(trash_path): + print("Creating trash folder...") + create_trash_folder() + # Comprobamos si tenemos permisos de escritura sobre el directorio que contiene el archivo "trashinfo.json" # ("/opt/opengnsys/etc"), y en caso contrario lanzamos una excepción: if not os.access(os.path.dirname(info_file), os.W_OK): -- 2.40.1 From 521220f62c487045d9ea3b950048ef4ca0db0023 Mon Sep 17 00:00:00 2001 From: ggil Date: Wed, 21 Aug 2024 13:28:06 +0200 Subject: [PATCH 14/70] refs #631 - Modify updateRepoInfo.py --- py_scripts/updateRepoInfo.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/py_scripts/updateRepoInfo.py b/py_scripts/updateRepoInfo.py index c6a0514..9f5b939 100644 --- a/py_scripts/updateRepoInfo.py +++ b/py_scripts/updateRepoInfo.py @@ -29,8 +29,7 @@ import shutil repo_path = '/opt/opengnsys/images' info_file = '/opt/opengnsys/etc/repoinfo.json' -#update_trash_script = '/opt/opengnsys/py_scripts/updateTrashInfo.py' -update_trash_script = '/home/administrador/updateTrashInfo.py' +update_trash_script = '/opt/opengnsys/py_scripts/updateTrashInfo.py' # -------------------------------------------------------------------------------------------- -- 2.40.1 From 5f88ba99d83e112a4d670d521f310ca3c935b8a4 Mon Sep 17 00:00:00 2001 From: ggil Date: Fri, 23 Aug 2024 14:16:41 +0200 Subject: [PATCH 15/70] refs #631 - Add importImage.py --- README.md | 95 ++-------------- py_scripts/importImage.py | 223 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 231 insertions(+), 87 deletions(-) create mode 100644 py_scripts/importImage.py diff --git a/README.md b/README.md index b8d802c..71116bb 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,8 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará 9. [Crear archivo ".torrent"](#crear-archivo-torrent) - `POST /ogrepository/v1/images/create-torrent` 10. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/images/send-p2p` 11. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/images/check-image` -12. [Exportar una Imagen](#exportar-una-imagen) - `POST /ogrepository/v1/images/export-image` -13. [Definir Imagen Global](#definir-imagen-global) - `PUT /ogrepository/v1/images/set-global` -14. [Definir Imagen Local](#definir-imagen-local) - `PUT /ogrepository/v1/images/set-local` -15. [Ver Estado de Transmisiones Multicast-P2P](#ver-estado-de-transmisiones-multicast-p2p) - -16. [Cancelar Transmisión Multicast-P2P](#cancelar-transmisión-multicast-p2p) - +12. [Ver Estado de Transmisiones Multicast-P2P](#ver-estado-de-transmisiones-multicast-p2p) - +13. [Cancelar Transmisión Multicast-P2P](#cancelar-transmisión-multicast-p2p) - --- ### Obtener Información de todas las Imágenes @@ -202,21 +199,21 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d ### Importar una Imagen Se importará una imagen de un repositorio remoto al repositorio local. -Se puede intentar hacer con el script "**importimage**", que actualmente no se utiliza, pero seguramente habrá que modificarlo. +Se puede hacer con el script "**importImage.py**", que hemos programado recientemente. **URL:** `/ogrepository/v1/images/import-image` **Método HTTP:** POST **Cuerpo de la Solicitud (JSON):** -- **user**: Usuario con el que acceder al repositorio remoto (por defecto, usuario local). -- **repo**: IP o hostname del repositorio remoto. - **image**: Nombre de la imagen (con extensión). -- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). - +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). +- **repo**: IP o hostname del repositorio remoto. +- **user**: Usuario con el que acceder al repositorio remoto. + **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"user":"user_name", "repo":"192.168.56.100", "image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/import-image +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none", "repo":"192.168.56.100", "user":"user_name"}' http://example.com/ogrepository/v1/images/import-image ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al importar la imagen. @@ -350,82 +347,6 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **Código 200 OK:** La imagen se ha chequeado exitosamente. - **Código 200 KO:** La imagen se ha chequeado correctamente, pero no ha pasado el test. ---- -### Exportar una Imagen - -Se exportará una imagen del repositorio local a un repositorio remoto. -Se debe crear un script que realice dicha tarea (o se puede intentar utilizar el script "**importimage**", que realiza la acción contraria, pero que actualmente no se utiliza). -**NOTA**: Aunque no se indica en el pliego, entendemos que también será necesario especificar credenciales de acceso al repositorio remoto como parámetros de entrada. - -**URL:** `/ogrepository/v1/images/export-image` -**Método HTTP:** POST - -**Cuerpo de la Solicitud (JSON):** -- **repo**: IP o hostname del repositorio remoto. -- **ou**: Unidad Organizativa del repositorio remoto. -- **image**: Nombre de la imagen (con extensión). -- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). - -**Ejemplo de Solicitud:** - -```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"repo":"192.168.56.200", "ou":"OU_Ejemplo", "image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/export-image -``` -**Respuestas:** -- **Código 500 Internal Server Error:** Ocurrió un error al exportar la imagen. -- **Código 400 Bad Request:** No se ha encontrado la imagen y/o el equipo remoto especificados. -- **Código 200 OK:** La imagen se ha exportado exitosamente. - ---- -### Definir Imagen Global - -Se marcará como "global" la imagen especificada como parámetro. -En principio, esto requerirá agregar una nueva clave al archivo "**/opt/opengnsys/etc/repoinfo.json**" (por ejemplo, "scope"), modificando el script "**updateRepoInfo.py**", para que realice dicha modificación. -También debe crearse un script que realice la definición de la imagen especificada (modificando el valor de la nueva clave del archivo "**/opt/opengnsys/etc/repoinfo.json**", de "local" a "global", por ejemplo). -Además, deberá llamarse a un script que exporte dicha imagen a los demás repositorios gestionados por el servidor de administración (que aun no está creado). - -**URL:** `/ogrepository/v1/images/set-global` -**Método HTTP:** PUT - -**Cuerpo de la Solicitud (JSON):** -- **image**: Nombre de la imagen (con extensión). -- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). - -**Ejemplo de Solicitud:** - -```bash -curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/set-global -``` -**Respuestas:** -- **Código 500 Internal Server Error:** Ocurrió un error al definir la imagen. -- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. -- **Código 200 OK:** La definición se realizó exitosamente. - ---- -### Definir Imagen Local - -Se marcará como "local" la imagen especificada como parámetro, que previamente habría sido marcada como "global" (ya que entiendo que de forma predeterminada, todas las imágenes estarán marcadas como "local"). -Como comentábamos en el endpoint precedentte, esto requerirá agregar una nueva clave al archivo "**/opt/opengnsys/etc/repoinfo.json**" (por ejemplo, "scope"), modificando el script "**updateRepoInfo.py**", para que realice dicha modificación. -También debe crearse un script que realice la definición de la imagen especificada (modificando el valor de la nueva clave del archivo "**/opt/opengnsys/etc/repoinfo.json**", de "global" a "local", por ejemplo). -Este endpoint deberá ser llamado en todos los repositorios gestionados por el mismo servidor de administración (para que todos hagan la modificación). - -**URL:** `/ogrepository/v1/images/set-local` -**Método HTTP:** PUT - -**Cuerpo de la Solicitud (JSON):** -- **image**: Nombre de la imagen (con extensión). -- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). - -**Ejemplo de Solicitud:** - -```bash -curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/set-local -``` -**Respuestas:** -- **Código 500 Internal Server Error:** Ocurrió un error al definir la imagen. -- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. -- **Código 200 OK:** La definición se realizó exitosamente. - --- ### Ver Estado de Transmisiones Multicast-P2P diff --git a/py_scripts/importImage.py b/py_scripts/importImage.py new file mode 100644 index 0000000..934b308 --- /dev/null +++ b/py_scripts/importImage.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script importa la imagen especificada como primer parámetro (y sus archivos asociados), desde el repositorio remoto especificado como segundo parámetro, + con las credenciales del usuario especificado como tercer parámetro (en principio, mediante claves). +Es muy similar al script bash original (cuyo nombre es "importimage", a secas), pero con ciertas diferencias. +Al acabar, llama al script "updateRepoInfo.py", para actualizar la información del repositorio. + +Librerías Python requeridas: "paramiko" (se puede instalar con "sudo apt install python3-paramiko") + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a importar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/images/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/images/ou_subdir/image1.img + +sys.argv[2] - IP o hostname del repositorio remoto. + - Ejemplo1: 192.168.56.100 + - Ejemplo2: remote_repo + +sys.argv[3] - Usuario con el que conectar al repositorio remoto. + - Ejemplo1: remote_user + - Ejemplo2: root + + Sintaxis +---------- +./importImage.py [ou_subdir/]image_name|/image_path/image_name remote_host remote_user + + Ejemplos + --------- +./importImage.py image1.img 192.168.56.100 user +./importImage.py /opt/opengnsys/images/image1.img 192.168.56.100 user +./importImage.py ou_subdir/image1.img remote_hostname user +./importImage.py /ou_subdir/image1.img remote_hostname root +./importImage.py /opt/opengnsys/images/ou_subdir/image1.img remote_hostname root +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import warnings +warnings.filterwarnings("ignore") +import os +import sys +import subprocess +import paramiko +import warnings + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/images/' +update_repo_script = '/opt/opengnsys/py_scripts/updateRepoInfo.py' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name remote_host remote_user + Ejemplo1: {script_name} image1.img 192.168.56.100 user + Ejemplo2: {script_name} /opt/opengnsys/images/image1.img 192.168.56.100 user + Ejemplo3: {script_name} ou_subdir/image1.img remote_hostname user + Ejemplo4: {script_name} /ou_subdir/image1.img remote_hostname root + Ejemplo5: {script_name} /opt/opengnsys/images/ou_subdir/image1.img remote_hostname root + """ + print(help_text) + + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 3 parámetros, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 4: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 3 parámetros") + show_help() + sys.exit(1) + + + +def build_file_path(): + """ Construye la ruta completa al archivo a importar + (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "repo_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(repo_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(repo_path): + file_path = os.path.join(repo_path, param_path) + else: + file_path = param_path + return file_path + + + +def import_image(file_path, remote_host, remote_user): + """ Conecta al repositorio remoto por SSH e inicia un cliente SFTP. + Comprueba que la imagen no esté bloqueada, en cuyo caso la descarga (junto con sus archivos asociados). + """ + # Creamos una lista con las extensiones de los archivos asociados a la imagen + # (incluyendo ninguna extensión, que corresponde a la propia imagen): + extensions = ['', '.sum', '.full.sum', '.torrent', '.info.checked'] + + # Iniciamos un cliente SSH: + ssh_client = paramiko.SSHClient() + # Establecemos la política por defecto para localizar la llave del host localmente: + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + # Intentamos conectar con el equipo remoto por SSH, e iniciar un cliente SFTP, + try: # y en caso de fallar devolvemos un error y salimos del script: + ssh_client.connect(remote_host, 22, remote_user) # Así se hace con claves + #ssh_client.connect(remote_host, 22, remote_user, 'opengnsys') # Así se haría con password + sftp_client = ssh_client.open_sftp() + except Exception as error_description: + print(f"Connection has returned an exception: {error_description}") + sys.exit(2) + + # Comprobamos si la imagen existe en el equipo remoto, y en caso contrario devolvemos un error y salimos del script: + try: + sftp_client.stat(file_path) + except IOError: + print("Remote image doesn't exist") + sys.exit(3) + + # Comprobamos si la imagen remota está bloqueada, en cuyo caso devolvemos un error y salimos del script, + try: # y en caso contrario la importamos (junto con todos sus archivos asociados): + sftp_client.stat(f"{file_path}.lock") + print("Remote image is locked.") + sys.exit(4) + except IOError: + print("Importing remote image...") + open(f"{file_path}.lock", "w").close() # Creamos un archivo de bloqueo + for ext in extensions: + sftp_client.get(f"{file_path}{ext}", f"{file_path}{ext}") + + # Cerramos el cliente SSH y el cliente SFTP: + ssh_client.close() + sftp_client.close() + + + +def update_repo_info(): + """ Actualiza la información del repositorio, ejecutando el script "updateRepoInfo.py". + Como se ve, es necesario que el script se ejecute como sudo, o dará error. + """ + try: + result = subprocess.run(['sudo', 'python3', update_repo_script], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except subprocess.CalledProcessError as error: + print(f"Error Output: {error.stderr.decode()}") + sys.exit(2) + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + sys.exit(3) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo a importar: + file_path = build_file_path() + + # Almacenamos la IP/hostname del repositorio remoto, y el usuario remoto (desde los parámetros): + remote_host = sys.argv[2] + remote_user = sys.argv[3] + + # Evaluamos si la ruta de la imagen tiene 5 barras, en cuyo caso corresponderá a una imagen basada en OU, + # y almacenamos el nombre del directorio correspondiente a la OU: + if file_path.count('/') == 5: + ou_subdir = file_path.split('/')[4] + # Si no existe un directorio correspondiente a la OU en el repo local, lo creamos: + if not os.path.exists(f"{repo_path}{ou_subdir}"): + os.mkdir(f"{repo_path}{ou_subdir}", 0o755) + + # Importamos la imagen del repositorio remoto: + import_image(file_path, remote_host, remote_user) + + # Eliminamos el archivo de bloqueo: + os.remove(f"{file_path}.lock") + + # Actualizamos la información del repositorio, ejecutando el script "updateRepoInfo.py": + print("Updating Repository Info...") + update_repo_info() + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- -- 2.40.1 From 6228f88c22e8975e35ca32b2386fe2148c5f92f8 Mon Sep 17 00:00:00 2001 From: ggil Date: Tue, 27 Aug 2024 12:45:04 +0200 Subject: [PATCH 16/70] refs #631 - Add getRepoInfo.py --- README.md | 129 ++++++---- admin/Sources/Services/ogAdmRepoAux | 285 +++++++++++++++++++++++ admin/Sources/Services/opengnsys.default | 15 ++ admin/Sources/Services/opengnsys.init | 224 ++++++++++++++++++ py_scripts/deleteImage.py | 5 + py_scripts/getRepoInfo.py | 191 +++++++++++++++ 6 files changed, 809 insertions(+), 40 deletions(-) create mode 100644 admin/Sources/Services/ogAdmRepoAux create mode 100644 admin/Sources/Services/opengnsys.default create mode 100644 admin/Sources/Services/opengnsys.init create mode 100644 py_scripts/getRepoInfo.py diff --git a/README.md b/README.md index 71116bb..53d40ab 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,10 @@ ogRepository - OpenGnsys Repository Manager Este repositorio GIT contiene la estructura de datos del repositorio de imágenes de OpenGnsys. -- **bin** ----------- Binarios y scripts de gestión del repositorio. -- **etc** ----------- Ficheros y plantillas de configuración del repositorio. -- **py_scripts** --- Scripts en Python 3, algunos de los cuales son traducciones de los scripts bash situados en "bin". +- **admin** --------- Archivos de configuración del repositorio. +- **bin** ----------- Binarios y scripts de gestión del repositorio. +- **etc** ----------- Ficheros y plantillas de configuración del repositorio. +- **py_scripts** --- Scripts en Python 3, algunos de los cuales son traducciones de los scripts bash situados en "bin". --- @@ -19,8 +20,8 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará --- ### Tabla de Contenido: -1. [Obtener Información de todas las Imágenes](#obtener-información-de-todas-las-imágenes) - `GET /ogrepository/v1/images` -2. [Obtener Información de una Imagen concreta](#obtener-información-de-una-imagen-concreta) - `GET /ogrepository/v1/images/get-info` +1. [Obtener Información de todas las Imágenes](#obtener-información-de-todas-las-imágenes) - `GET /ogrepository/v1/images/get-info` +2. [Obtener Información de una Imagen concreta](#obtener-información-de-una-imagen-concreta) - `GET /ogrepository/v1/images/get-info` 3. [Actualizar Información del Repositorio](#actualizar-información-del-repositorio) - `PUT /ogrepository/v1/images` 4. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/delete-image` 5. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/images/recover-image` @@ -36,16 +37,20 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará --- ### Obtener Información de todas las Imágenes -Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (que corresponde a todas las imágenes monolíticas almacenadas en el repositorio). -Se debe crear un script que devuelva dicha información, porque actualmente no hay ninguno. +Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (que corresponde a todas las imágenes almacenadas en el repositorio), y en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (que corresponde a las imágenes que fueron eliminadas, que estarán en la papelera). +Se puede utilizar el script "**getRepoInfo.py**, que hemos programado recientemente. -**URL:** `/ogrepository/v1/images` +**URL:** `/ogrepository/v1/images/get-info` **Método HTTP:** GET +**Cuerpo de la Solicitud (JSON):** +- **image**: Nombre de la imagen (con extensión), pero en este caso "all". +- **ou_subdir**: Subdirectorio correspondiente a la OU, pero en este caso "none". + **Ejemplo de Solicitud:** ```bash -curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images +curl -X GET -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"all","ou_subdir":"none"}' http://example.com/ogrepository/v1/images/get-info ``` **Respuestas:** @@ -53,30 +58,68 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag - **Código 200 OK:** La información de las imágenes se obtuvo exitosamente. - **Contenido:** Información de imágenes en formato JSON. ```json + { - "directory": "/opt/opengnsys/images", - "images": [ - { - "name": "UbuntuSATA", - "type": "img", - "clientname": "Ubuntu_SATA", - "clonator": "partclone", - "compressor": "lzop", - "filesystem": "EXTFS", - "datasize": 8704000000 - }, - { - "name": "Windows10", - "type": "img", - "clientname": "Windows_10", - "clonator": "partclone", - "compressor": "lzop", - "filesystem": "NTFS", - "datasize": 23654400000 + "REPOSITORY": { + "directory": "/opt/opengnsys/images", + "images": [ + { + "name": "Ubuntu24", + "type": "img", + "clientname": "Ubuntu_24", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "EXTFS", + "datasize": 9859634200 + }, + { + "name": "Windows10", + "type": "img", + "clientname": "Windows_10", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "NTFS", + "datasize": 23654400000 + } + ], + "ous": [ + { + "subdir": "OU_subdir", + "images": [ + { + "name": "Ubuntu20", + "type": "img", + "clientname": "Ubuntu_20", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "EXTFS", + "datasize": 8704000000 + } + ] + } + ] + } + }, + "TRASH": { + "directory": "/opt/opengnsys/images_trash", + "images": [], + "ous": [ + { + "subdir": "CentroVirtual", + "images": [ + { + "name": "Ubuntu20OLD", + "type": "img", + "clientname": "Ubuntu_20", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "EXTFS", + "datasize": 8704000000 + } + ] + } + ] } - ], - "ous": [] - } ``` - **name**: Nombre de la imagen, sin extensión. - **type**: Extensión de la imagen. @@ -89,8 +132,8 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag --- ### Obtener Información de una Imagen concreta -Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (correspondiente a la imagen especificada). -Se debe crear un script que devuelva dicha información, porque actualmente no hay ninguno. +Se devolverá la informacion de la imagen especificada, que puede estar en el archivo "**/opt/opengnsys/etc/repoinfo.json**" o en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (en este último caso, si la imagen está en la papelera). +Se puede utilizar el script "**getRepoInfo.py**, que hemos programado recientemente. **URL:** `/ogrepository/v1/images/get-info` **Método HTTP:** GET @@ -111,14 +154,20 @@ curl -X GET -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **Código 200 OK:** La información de la imagen se obtuvo exitosamente. - **Contenido:** Información de la imagen en formato JSON. ```json + { - "name": "Windows10", - "type": "img", - "clientname": "Windows_10", - "clonator": "partclone", - "compressor": "lzop", - "filesystem": "NTFS", - "datasize": 23654400000 + "directory": "/opt/opengnsys/images", + "images": [ + { + "name": "Windows10", + "type": "img", + "clientname": "Windows_10", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "NTFS", + "datasize": 23654400000 + } + ] } ``` - **name**: Nombre de la imagen, sin extensión. diff --git a/admin/Sources/Services/ogAdmRepoAux b/admin/Sources/Services/ogAdmRepoAux new file mode 100644 index 0000000..ede2bea --- /dev/null +++ b/admin/Sources/Services/ogAdmRepoAux @@ -0,0 +1,285 @@ +#!/bin/bash +PARM=`cat` +#PARM=$@ + + +#TODO: ticket 379 +#buscar parametro de identificador de operacion. +#usar parametro de identificacion para anexarlo al nombre de log +#Comprobar si la variable está seteas. +#Si no lo está setearla. +#Si esta seteada (en progreso) salir. + + +TIME=$SECONDS + +BASEDIR=/opt/opengnsys +PATH=$PATH:$BASEDIR/bin +REPONAME=ogAdmRepo +REPODIR="$BASEDIR/images/" + +# Para las sincronizadas +# BACKUP: Define si se realiza copia de seguridad al crear una imagen (true|false). +# IMGFS: Sistema de ficheros al crear las sincronizadas tipo archivo (EXT4|BTRFS). +[ -z $OGENGINECONFIGURATE ] && source $BASEDIR/client/etc/engine.cfg +# FS segun la configuracion y la version del kernel. ext4 para < 3.7, para >= BTRFS +KERNEL=$(file -bkr /opt/opengnsys/tftpboot/ogclient/ogvmlinuz |awk '/Linux/ {for(i=1;i<=NF;i++) if($i~/version/) {v=$(i+1);printf("%d",v);sub(/[0-9]*\./,"",v);printf(".%02d",v)}}') +[ $KERNEL \< 3.07 ] && IMGFS="EXT4" || IMGFS=${IMGFS:-"BTRFS"} + +# Añade registro de incidencias. +function echolog () { + logger --tag $0 --priority local0.info "$*" + echo "$*" +} + +function mountImage () { + #@param 1 image_file + #@param 2 mount_dir + #@param 3 openciones mount + [ "$3" != "" ] && OPTMOUNT=" -o $3 " + # Si está montado nada que hacer + df |grep "$2$" 2>&1 >/dev/null && return 0 + # FS de la imagen segun el contenido del archivo .img + if file "$1" |grep -i -e " ext4 filesystem " 2>&1 > /dev/null ; then + echolog "mount $OPTMOUNT -t ext4 $1 $2" + mount $OPTMOUNT -t ext4 $1 $2 + else + echolog "mount $OPTMOUNT -o compress=lzo $1 $2" + mount $OPTMOUNT -o compress=lzo "$1" "$2" + fi + # Si esta montado da error 32, lo damos como bueno + RETVAL=$? + [ $RETVAL -eq 32 ] && RETVAL=0 + return $RETVAL +} + + +PARM1=$(echo $PARM | cut -f1 -d" ") +PARM2=$(echo $PARM | cut -f2 -d" ") +PARM3=$(echo $PARM | cut -f3 -d" ") +PARM4=$(echo $PARM | cut -f4 -d" ") + +# Determinamos el tipo de sistema de fichero de las imagenes segun el kernel que tenga + + +case "$PARM1" in + START_MULTICAST) + #1 START_MULTICAST + #2 fichero a enviar + #3 opciones de multicast + FILE="$PARM2" + MCASTOPT="$PARM3" + echolog "Ejecutar $(which sendFileMcast) $FILE $MCASTOPT" + sendFileMcast $FILE $MCASTOPT |logger --tag $0 --priority local0.info + case $? in + 1) echolog "Parametros insuficientes" + exit 1 ;; + 2) echolog "Fichero no accesible" + exit 2 ;; + 3) echolog "Sesion multicast no valida" + exit 3 ;; + esac + ;; + CREATE_IMAGE) + # Creamos/Redimensionamos el fichero de imagen y lo montamos para que se pueda escribir sobre el + #1 CREATE_IMAGE + #2 nombre imagen + #3 tipo de imagen [ img | diff ] + #4 tamaño imagen + LOOPDEVICE=$(losetup -f) + DIRMOUNT="$REPODIR/mount/$PARM2" + if [ "$PARM3" == "img" ] ; then + IMGEXT="img" + else + IMGEXT="img.diff" + DIRMOUNT="$DIRMOUNT.diff" + fi + IMGFILE="$REPODIR/$PARM2.$IMGEXT" + IMGDIR="$(dirname $IMGFILE)" + [ -d $IMGDIR ] || mkdir -p $IMGDIR + mkdir -p "$DIRMOUNT" + + LOCKFILE="$IMGFILE.lock" + + SIZEREQUIRED="$PARM4" + + # Si existe la imagen hacemos copia de seguridad y la redimesionamos + if [ -f "$IMGFILE" ]; then + echolog "La imagen $IMGFILE ya existe." + # TODO modificar ogGetImageSize + IMGSIZE=$(ls -l --block-size=1024 $IMGFILE| cut -f5 -d" ") + + if [ "$BACKUP" == "true" -o "$BACKUP" == "TRUE" -o $IMGSIZE -lt $SIZEREQUIRED ]; then + # Si la imagen esta montada la desmonto + if [ -r "$DIRMOUNT/ogimg.info" ]; then + echolog "umount $DIRMOUNT" + umount "$DIRMOUNT" + [ $? -ne 0 ] && echolog "Error: No podemos desmontar la imagen para hacer copia de seguridad o redimensionar" && exit 1 + fi + fi + + # Copia de seguridad de la imagen + if [ "$BACKUP" == "true" -o "$BACKUP" == "TRUE" ]; then + echolog "Copia de seguridad de la imagen anterior" + echolog "cp $IMGFILE $IMGFILE.ant" + cp "$IMGFILE" "$IMGFILE.ant" + echolog mv -f "$IMGFILE.torrent" "$IMGFILE.torrent.ant" 2>/dev/null + mv -f "$IMGFILE.torrent" "$IMGFILE.torrent.ant" 2>/dev/null + fi + + # Redimensionamos la imagen al tamaño necesario + if [ $IMGSIZE -lt $SIZEREQUIRED ];then + echolog "Redimensionamos la imagen $IMGFILE al tamaño necesario: $SIZEREQUIRED" + echolog "truncate --size=\">$SIZEREQUIRED\"k $IMGFILE" + truncate --size=">$SIZEREQUIRED"k $IMGFILE 2>&1 |logger --tag $0 --priority local0.info + # FS de la imagen segun el contenido del archivo .img + if file "$IMGFILE" |grep -i -e " ext4 filesystem " 2>&1 > /dev/null ; then + losetup $LOOPDEVICE "$IMGFILE" + echolog "resize2fs -f $LOOPDEVICE" + resize2fs -f $LOOPDEVICE |logger --tag $0 --priority local0.info + else + mount -o compress=lzo "$IMGFILE" "$DIRMOUNT" + echolog "btrfs filesystem resize max $DIRMOUNT" + btrfs filesystem resize max "$DIRMOUNT" 2>&1 |logger --tag $0 --priority local0.info + fi + fi + + + # Si no existe la imagen creamos el fichero. + else + echolog "Creamos la imagen $IMGFILE al tamaño necesario: $SIZEREQUIRED" + touch "$IMGFILE" + echolog "truncate --size=\">$SIZEREQUIRED\"k $IMGFILE" + truncate --size=">$SIZEREQUIRED"k $IMGFILE 2>&1 |logger --tag $0 --priority local0.info + #Formateamos imagen + echo losetup $LOOPDEVICE "$IMGFILE" + losetup $LOOPDEVICE "$IMGFILE" + if [ $IMGFS == "EXT4" ] ; then + echolog " mkfs.ext4 -i 4096 -b 4096 -L ${PARM2##*\/} $LOOPDEVICE" + mkfs.ext4 -i 4096 -b 4096 -L ${PARM2##*\/} $LOOPDEVICE + else + echolog mkfs.btrfs -L ${PARM2##*\/} $LOOPDEVICE + mkfs.btrfs -L ${PARM2##*\/} $LOOPDEVICE #&> $OGLOGCOMMAND + fi + fi + # Montamos la imagen. + mountImage "$IMGFILE" "$DIRMOUNT" + if [ $? -ne 0 ]; then + rmdir "$DIRMOUNT" + echolog "Error al crear/redimensionar la imagen" + exit 1 + fi + + #touch "$DIRMOUNT/ogimg.info" + echo "mounted"> "$LOCKFILE" + TIME2=$[SECONDS-TIME] + echolog "Fin creación/redimension de la imagen: $[TIME2/60]m $[TIME2%60]s" + # Si existe dispositivo loop lo borramos. + [ $LOOPDEVICE ] && losetup -a| grep $LOOPDEVICE &> /dev/null && losetup -d $LOOPDEVICE + # TODO: comprobar que no se quede el losetup bloqueado. + + ;; + MOUNT_IMAGE) + # Montamos el fichero imagen para que se pueda + #1 MOUNT_IMAGE + #2 nombre imagen + #3 tipo de imagen [ img | diff ] + DIRMOUNT="$REPODIR""mount/$PARM2" + if [ "$PARM3" == "img" ] ; then + IMGEXT="img" + else + IMGEXT="img.diff" + DIRMOUNT="$DIRMOUNT.diff" + fi + IMGFILE="$REPODIR/$PARM2.$IMGEXT" + echolog "Montamos la imagen $IMGFILE " + mkdir -p "$DIRMOUNT" + mountImage "$IMGFILE" "$DIRMOUNT" ro || (echolog "Error al montar la imagen"; exit 1) + ;; + UMOUNT_IMAGE) + # Desmontamos el fichero imagen. + # Si el directorio esta ocupado no se desmontará + #1 UMOUNT_IMAGE + #2 nombre imagen + #3 tipo de imagen [ img | diff ] + IMGTYPE="$PARM3" + DIRMOUNT="$REPODIR/mount/$PARM2" + if [ "$IMGTYPE" == "img" ]; then + IMGEXT="img" + else + DIRMOUNT="$DIRMOUNT.$IMGTYPE" + IMGEXT="img.diff" + fi + LOCKFILE="$REPODIR/$PARM2.$IMGEXT.lock" + echolog "Desmontamos la imagen $PARM2 $PARM3 " + umount $DIRMOUNT + rmdir $DIRMOUNT + [ -f $LOCKFILE ] && sed -i s/mounted//g $LOCKFILE + + ;; + REDUCE_IMAGE) + # Reduce el archivo de la imagen a tamaño datos + 500M + #1 REDUCE_IMAGE + #2 Nombre Imagen + #3 Tipo de imagen [ img |diff ] + DIRMOUNT="${REPODIR}mount/${PARM2}" + if [ "$PARM3" == "img" ] ; then + IMGEXT="img" + else + IMGEXT="img.diff" + DIRMOUNT="$DIRMOUNT.diff" + fi + IMGFILE="$REPODIR$PARM2.$IMGEXT" + LOCKFILE="$IMGFILE.lock" + [ ! -f $IMGFILE ] && echolog "Imagen $IMGFILE no existe" && exit 1 + + # Para imagenes EXT4 reduzco, para BTRFS solo desmonto. + if file $IMGFILE |grep -i -e " ext4 filesystem " 2>&1 > /dev/null ; then + + [ -d $DIRMOUNT ] || mkdir $DIRMOUNT + mountImage "$IMGFILE" "$DIRMOUNT" || (echolog "Error al montar la imagen $IMGFILE"; exit 1) + + + # Si el espacio libre menor que 200Mb desmontamos la imagen y nos salimos + AVAILABLE=$(df -k|grep $DIRMOUNT|awk '{print $4}') + if [ $AVAILABLE -lt 200000 ]; then + echolog "reducir imagen REPO $PARM2 $IMGEXT. tamaño minimo, nada que hacer" + umount $DIRMOUNT || (echolog "Error al desmontar la imagen $IMGFILE"; exit 1) + else + + # Calculamos la diferencia entre el tamaño interno y externo + EXTSIZE=$(ls -l --block-size=1024 $IMGFILE | cut -f5 -d" ") + INTSIZE=$(df -k|grep "$DIRMOUNT"|awk '{print $2}') + let EDGESIZE=$EXTSIZE-$INTSIZE + + echolog "reducir imagen REPO $PARM2 $IMGEXT, tamaño final: $ENDSIZE" + umount $DIRMOUNT + LOOPDEVICE=$(losetup -f) + losetup $LOOPDEVICE "$IMGFILE" + + # Redimensiono sistema de ficheros + echolog "resize2fs -fpM $LOOPDEVICE " + resize2fs -fpM $LOOPDEVICE # 2>&1 |logger --tag $0 --priority local0.info + mountImage "$IMGFILE" "$DIRMOUNT" + # Calculamos el tamaño final del archivo + INTSIZE=$(df -k|grep "$DIRMOUNT"|awk '{print $2}') + let EXTSIZE=$INTSIZE+$EDGESIZE + umount $DIRMOUNT || (echolog "Error al desmontar la imagen $IMGFILE"; exit 1) + # Si existe dispositivo loop lo borramos. + [ $LOOPDEVICE ] && losetup -a| grep $LOOPDEVICE &> /dev/null && losetup -d $LOOPDEVICE + # Corto el archivo al tamaño del sistema de ficheros + echo "truncate --size=\"$EXTSIZE\"k $IMGFILE" + echolog "truncate --size=\"$EXTSIZE\"k $IMGFILE" + truncate --size="$EXTSIZE"k $IMGFILE + fi + else + umount $DIRMOUNT || (echolog "Error al desmontar la imagen $IMGFILE"; exit 1) + fi + rmdir $DIRMOUNT + echo "reduced" >$LOCKFILE + + ;; + default) + echolog "Solicitud con parametros \"$PARM\" no realizada, no registrada o con errores" + ;; +esac diff --git a/admin/Sources/Services/opengnsys.default b/admin/Sources/Services/opengnsys.default new file mode 100644 index 0000000..d7376cf --- /dev/null +++ b/admin/Sources/Services/opengnsys.default @@ -0,0 +1,15 @@ +# RUN_OGADMSERVER defined as OpenGnsys Admin Server +# RUN_OGADMREPO defined as OpenGnsys Repository Manager +# RUN_OGADMAGENT run task scheduler service, only if Admin Server is enabled +# RUN_BTTRACKER run Bittorrent Tracker, only if Repository is enabled +# RUN_BTSEEDER start seeding of selected torrent files, only if Repository is enabled +# BTSEEDER_PRIORITY nice priority to seed torrent files; recommended values: +# 8 for Admin Server or Repo without Torrent +# 0 for Admin Server and Repo with Torrent +# -8 for Repo with Torrent +RUN_OGADMSERVER="yes" +RUN_OGADMREPO="yes" +RUN_OGADMAGENT="yes" +RUN_BTTRACKER="yes" +RUN_BTSEEDER="yes" +BTSEEDER_PRIORITY=0 diff --git a/admin/Sources/Services/opengnsys.init b/admin/Sources/Services/opengnsys.init new file mode 100644 index 0000000..ae27593 --- /dev/null +++ b/admin/Sources/Services/opengnsys.init @@ -0,0 +1,224 @@ +#!/bin/bash + +### BEGIN INIT INFO +# Provides: opengnsys +# Required-Start: +# Required-Stop: +# Default-Start: 2 3 4 5 +# Default-Stop: 1 +# Short-Description: Servicios del sistema OpenGnsys +# Description: Servicios del sistema OpenGnsys +### END INIT INFO + +# +# Definiciones globales +# +BASEDIR=/opt/opengnsys +OPENGNSYSUSER="opengnsys" +IMAGEDIR=$BASEDIR/images +CLIENTLOGDIR=$BASEDIR/log/clients + +# +# Servidor de OpenGnsys +# +SERVERNAME=ogAdmServer +SERVERDAEMON=$BASEDIR/sbin/$SERVERNAME +SERVERCFG=$BASEDIR/etc/$SERVERNAME.cfg +SERVERLOG=$BASEDIR/log/$SERVERNAME.log +SERVERDAEMON_OPTIONS="-f $SERVERCFG -l $SERVERLOG" + +# +# Servidor de Repositorio +# +############## ADV +REPOAUXNAME=ogAdmRepoAux +REPOAUXDAEMON=$BASEDIR/sbin/$REPOAUXNAME +REPOAUXPORT=$(awk -F= '/PUERTO/ {print $2+1}' $SERVERCFG 2>/dev/null) +############## ADV +############# IRINA # para setBootMode desde el cliente +SERVERAUXNAME=ogAdmServerAux +SERVERAUXDAEMON=$BASEDIR/sbin/$SERVERAUXNAME +SERVERAUXPORT=2011 +############# IRINA + +# +# Servidor de tareas programadas +# +AGENTNAME=ogAdmAgent +AGENTDAEMON=$BASEDIR/sbin/$AGENTNAME +AGENTCFG=$BASEDIR/etc/$AGENTNAME.cfg +AGENTLOG=$BASEDIR/log/$AGENTNAME.log +AGENTDAEMON_OPTIONS="-f $AGENTCFG -l $AGENTLOG" + +# +# Opciones Bittorrent +# + +BTTRACK=/usr/bin/bttrack.bittorrent +BTSEEDER=/usr/bin/btlaunchmany.bittornado +BTTRACKPORT=6969 +BTTRACKDFILE=/tmp/dstate +BTTRACKLOG=$BASEDIR/log/bttrack.log +BTINTERVAL=30 +BTTORRENTSDIR=$BASEDIR/images +BTTRACK_OPTIONS=" --port $BTTRACKPORT --dfile $BTTRACKDFILE --reannounce_interval $BTINTERVAL --logfile $BTTRACKLOG --allowed_dir $BTTORRENTSDIR --allow_get 1" +BTTRACKPID="/var/run/bttrack.pid" +BTSEEDERPID="/var/run/btseeder.pid" + + +export PATH="${PATH:+$PATH:}/usr/sbin:/sbin:/usr/bin" + +# Read config file if it is present. +if [ -r /etc/default/opengnsys ]; then + source /etc/default/opengnsys +fi + +# Configuración de arranque según la distribución Linux usada. +config() { + if [ -f /etc/os-release ]; then + source /etc/os-release + OSDISTRIB="$ID" + else + OSDISTRIB=$(lsb_release -is 2>/dev/null) + fi + OSDISTRIB="${OSDISTRIB,,}" + case "$OSDISTRIB" in + ubuntu|debian|linuxmint) + INITFUNCTIONS=/lib/lsb/init-functions + DAEMONSTART="start-stop-daemon --start --quiet --background --exec" + EXTRAOPTS="--" + DAEMONSTOP="start-stop-daemon --stop --quiet --oknodo --name" + ACTIONMSG="log_daemon_msg" + SUCCESSMSG="log_end_msg 0" + FAILMSG="log_end_msg 1" + TRACKERSTART="start-stop-daemon --make-pidfile --pidfile $BTTRACKPID --start --quiet --background --exec" + BTTRACK_OPTIONS="$BTTRACK_OPTIONS --parse_allowed_interval 1" + TRACKERSTOP="start-stop-daemon --stop --quiet --oknodo --retry 30 --pidfile $BTTRACKPID" + SEEDERSTART="start-stop-daemon --make-pidfile --pidfile $BTSEEDERPID --start --quiet --background --exec" + SEEDERSTOP="start-stop-daemon --stop --quiet --oknodo --pidfile $BTSEEDERPID" + ;; + centos|fedora) + INITFUNCTIONS=/etc/init.d/functions + DAEMONSTART="daemon" + ENDOPTS="&" + DAEMONSTOP="killproc" + ACTIONMSG="echo -n" + SUCCESSMSG="eval ( success; echo )" + FAILMSG="eval ( failure; echo )" + BTTRACK=/usr/bin/bttrack.py + BTSEEDER=/usr/bin/btlaunchmany.py + TRACKERSTART="daemon --pidfile $BTTRACKPID" + TRACKERSTOP="killproc -p $BTTRACKPID $BTTRACK" + SEEDERSTART="daemon --pidfile $BTSEEDERPID" + SEEDERSTOP="killproc -p $BTSEEDERPID $BTSEEDER" + ;; + *) echo "Distribución Linux desconcocida o no soportada." + exit ;; + esac + if [ -r $INITFUNCTIONS ]; then + source $INITFUNCTIONS + fi +} + +arranca_demonios() { + # Comprobar que está instalado OpenGnsys. + if [ ! -d $BASEDIR ]; then + $ACTIONMSG "ERROR: No existe el directorio $BASEDIR" + $FAILMSG + exit $? + fi + # Deshabilitar modo reforzado de SELinux. + [ -f /selinux/enforce ] && echo 0 > /selinux/enforce + # Verificar permisos básicos. + if [ "$(stat --printf="%A%G" $IMAGEDIR 2>/dev/null)" != "drwxrwxr-x$OPENGNSYSUSER" ]; then + mkdir $IMAGEDIR 2>/dev/null + chmod 775 $IMAGEDIR + chgrp $OPENGNSYSUSER $IMAGEDIR + fi + if [ "$(stat --printf="%A%G" $CLIENTLOGDIR 2>/dev/null)" != "drwxrwxr-x$OPENGNSYSUSER" ]; then + mkdir -p $CLIENTLOGDIR 2>/dev/null + chmod 775 $CLIENTLOGDIR + chgrp $OPENGNSYSUSER $CLIENTLOGDIR + fi + # Arrancar los servicios indicados. + if [ $RUN_OGADMSERVER = "yes" ]; then + $ACTIONMSG "Iniciando demonio: $SERVERNAME" + $DAEMONSTART $SERVERDAEMON $EXTRAOPTS $SERVERDAEMON_OPTIONS $ENDOPTS + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG + # Para SetBootmode desde el cliente + $ACTIONMSG "Iniciando demonio: $SERVERAUXNAME" # + faucet $SERVERAUXPORT --daemon --in bash -c "$SERVERAUXDAEMON" # NUEVO + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG + fi + if [ $RUN_OGADMREPO = "yes" ]; then + $ACTIONMSG "Iniciando demonio: $REPOAUXNAME" + faucet $REPOAUXPORT --daemon --in bash -c "$REPOAUXDAEMON" + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG + fi + if [ $RUN_OGADMSERVER = "yes" ] && [ $RUN_OGADMAGENT = "yes" ]; then + sleep 5 # Damos tiempo a que ogAdmServer este funcionando + fi + if [ $RUN_OGADMAGENT = "yes" ]; then + $ACTIONMSG "Iniciando demonio: $AGENTNAME" + $DAEMONSTART $AGENTDAEMON $EXTRAOPTS $AGENTDAEMON_OPTIONS $ENDOPTS + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG + fi + if [ $RUN_BTTRACKER = "yes" ]; then + $ACTIONMSG "Iniciando demonio: $BTTRACK" + $TRACKERSTART $BTTRACK $EXTRAOPTS $BTTRACK_OPTIONS $ENDOPTS + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG + fi + if [ $RUN_BTSEEDER = "yes" ]; then + $ACTIONMSG "Iniciando demonio: $BTSEEDER" + $SEEDERSTART $BTSEEDER $EXTRAOPTS $BTTORRENTSDIR &>/dev/null $ENDOPTS + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG + fi + +} + +para_demonios() { + if [ -e $BTSEEDERPID ]; then + $ACTIONMSG "Parando demonio: $BTSEEDER" + $SEEDERSTOP + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG + rm -f $BTSEEDERPID + fi + if [ -e $BTTRACKPID ]; then + $ACTIONMSG "Parando demonio: $BTTRACK" + $TRACKERSTOP + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG + rm -f $BTTRACKPID + fi + $ACTIONMSG "Parando demonio: $AGENTNAME" + $DAEMONSTOP $AGENTNAME + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG + $ACTIONMSG "Parando demonio: $REPOAUXNAME" + pkill faucet + [ $? -le 1 ] && $SUCCESSMSG || $FAILMSG + $ACTIONMSG "Parando demonio: $SERVERNAME" + $DAEMONSTOP $SERVERNAME + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG +} + +config + +case "$1" in + start) + arranca_demonios + ;; + stop) + para_demonios + ;; + restart) + para_demonios + arranca_demonios + ;; + + *) + echo "Uso: $0 {start|stop|restart}" + exit 1 + ;; +esac + +exit 0 + diff --git a/py_scripts/deleteImage.py b/py_scripts/deleteImage.py index a17f71f..aac9b1a 100644 --- a/py_scripts/deleteImage.py +++ b/py_scripts/deleteImage.py @@ -202,6 +202,11 @@ def main(): # Obtenemos la ruta completa al archivo a eliminar: file_path = build_file_path() + # Si no existe el archivo de imagen, imprimimos un mensaje de error y salimos del script: + if not os.path.exists(file_path): + print("Image file doesn't exist") + sys.exit(5) + # Comprobamos si existe el directorio correspondiente a la papelera, y en caso contrario lo creamos: if not os.path.exists(trash_path): print("Creating trash folder...") diff --git a/py_scripts/getRepoInfo.py b/py_scripts/getRepoInfo.py new file mode 100644 index 0000000..4897725 --- /dev/null +++ b/py_scripts/getRepoInfo.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script devuelve información (en formato json) de todas las imágenes contenidas en el repositorio (incluída la papelera), + o de la imagen que se especifique como primer parámetro (debiendo especificar también el subdirectorio de OU como segundo parámetro, si procede). + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a consultar (con extensión y sin ruta), u "all" (para obtener información de todas las imágenes). + - Ejemplo1: all + - Ejemplo2: image1.img + +sys.argv[2] - Subdirectorio correspondiente a la OU, o "none" si no procede.. + - Ejemplo1: none + - Ejemplo2: OU_subdirectory + + Sintaxis +---------- +./getRepoInfo.py image_name|all ou_subdir|none + + Ejemplos + --------- +./getRepoInfo.py all none +./getRepoInfo.py image1.img none +./getRepoInfo.py image1.img OU_subdirectory +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import json + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_file = '/opt/opengnsys/etc/repoinfo.json' +trash_file = '/opt/opengnsys/etc/trashinfo.json' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} image_name|all ou_subdir|none + Ejemplo1: {script_name} all none + Ejemplo2: {script_name} image1.img none + Ejemplo3: {script_name} image1.img OU_subdirectory + """ + print(help_text) + + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 2 parámetros, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 3: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 2 parámetros") + show_help() + sys.exit(1) + + + +def get_all_info(repo_data, trash_data): + """ Imprime un json con la información de todo el repositorio, con todas las imágenes que contiene, + incluyendo las imágenes que fueron eliminadas (que estarán en la papelera). + """ + # Creamos un diccionario, combinando la información del repositorio y de la papelera: + dictionary = {"REPOSITORY": repo_data, + "TRASH": trash_data} + # Convertimos el diccionario a json, y lo imprimimos: + final_json = json.dumps(dictionary, indent=2) + print(final_json) + + + +def get_image_info(repo_data, trash_data, image_name, image_ext): + """ Busca la imagen especificada en el repositorio y en la papelera, devolviendo la información asociada si la encuentra, + (o devolviendo un mensaje de error y saliendo del script si no la encuentra). + """ + dictionary = "" + # Buscamos la imagen en el repositorio, y si la encontramos creamos un diccionario con los datos: + for image in repo_data['images']: + if image['name'] == image_name and image['type'] == image_ext: + dictionary = {"directory": repo_data['directory'], + "images": [image]} + # Buscamos la imagen en la papelera, y si la encontramos creamos un diccionario con los datos: + for image in trash_data['images']: + if image['name'] == image_name: + dictionary = {"directory": trash_data['directory'], + "images": [image]} + # Si hemos obtenido datos de la imagen, los pasamos a json y los imprmimos, + # y si no, imprimimos un mensaje de error y salimos del script: + if dictionary != "": + final_json = json.dumps(dictionary, indent=2) + print(final_json) + else: + print("No se ha encontrado la imagen especificada en el repositorio") + sys.exit(2) + + + +def get_ou_image_info(repo_data, trash_data, image_name, image_ext, ou_subdir): + """ Busca la imagen basada en OU en el repositorio y en la papelera, devolviendo la información asociada si la encuentra, + (o devolviendo un mensaje de error y saliendo del script si no la encuentra). + """ + dictionary = "" + # Buscamos la OU y la imagen en el repositorio, y si los encontramos creamos un diccionario con los datos: + for ou in repo_data['ous']: + if ou['subdir'] == ou_subdir: + for image in ou['images']: + if image['name'] == image_name and image['type'] == image_ext: + dictionary = {"directory": repo_data['directory'], + "ous": [{"subdir": ou_subdir, "images": [image]}]} + # Buscamos la OU y la imagen en la papelera, y si los encontramos creamos un diccionario con los datos: + for ou in trash_data['ous']: + if ou['subdir'] == ou_subdir: + for image in ou['images']: + if image['name'] == image_name: + dictionary = {"directory": trash_data['directory'], + "ous": [{"subdir": ou_subdir, "images": [image]}]} + # Si hemos obtenido datos de la imagen, los pasamos a json y los imprmimos, + # y si no, imprimimos un mensaje de error y salimos del script: + if dictionary != "": + final_json = json.dumps(dictionary, indent=2) + print(final_json) + else: + print("No se ha encontrado la imagen especificada en el repositorio") + sys.exit(3) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Almacenamos la información de las imágenes del repositorio, en la variable "repo_data": + with open(repo_file, 'r') as file: + repo_data = json.load(file) + + # Almacenamos la información de las imágenes de la papelera, en la variable "trash_data": + with open(trash_file, 'r') as file: + trash_data = json.load(file) + + # Dependiendo del valor de los parámetros, llamamos a la función correspondiente, para imprimir la información + # (extrayendo el nombre, la extensión de la imagen, y/o la OU cuando se necesite): + if sys.argv[1] == 'all': + get_all_info(repo_data, trash_data) + elif sys.argv[2] == 'none': + image_name = sys.argv[1].split('.')[0] + image_ext = sys.argv[1].split('.')[1] + get_image_info(repo_data, trash_data, image_name, image_ext) + else: + image_name = sys.argv[1].split('.')[0] + image_ext = sys.argv[1].split('.')[1] + ou_subdir = sys.argv[2] + get_ou_image_info(repo_data, trash_data, image_name, image_ext, ou_subdir) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- -- 2.40.1 From 335276ef9c52134bcaeed77a3067abe114a8ef66 Mon Sep 17 00:00:00 2001 From: ggil Date: Tue, 27 Aug 2024 12:48:30 +0200 Subject: [PATCH 17/70] refs #631 - Modify API proposal --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 53d40ab..2944d3b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ogRepository - OpenGnsys Repository Manager Este repositorio GIT contiene la estructura de datos del repositorio de imágenes de OpenGnsys. -- **admin** --------- Archivos de configuración del repositorio. +- **admin** ------- Archivos de configuración del repositorio. - **bin** ----------- Binarios y scripts de gestión del repositorio. - **etc** ----------- Ficheros y plantillas de configuración del repositorio. - **py_scripts** --- Scripts en Python 3, algunos de los cuales son traducciones de los scripts bash situados en "bin". -- 2.40.1 From 8b52312a0bc5bf4d43a19198f4c4cde5b2344f1e Mon Sep 17 00:00:00 2001 From: ggil Date: Tue, 27 Aug 2024 12:49:46 +0200 Subject: [PATCH 18/70] refs #631 - Modify README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2944d3b..18944e8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ogRepository - OpenGnsys Repository Manager Este repositorio GIT contiene la estructura de datos del repositorio de imágenes de OpenGnsys. -- **admin** ------- Archivos de configuración del repositorio. +- **admin** -------- Archivos de configuración del repositorio. - **bin** ----------- Binarios y scripts de gestión del repositorio. - **etc** ----------- Ficheros y plantillas de configuración del repositorio. - **py_scripts** --- Scripts en Python 3, algunos de los cuales son traducciones de los scripts bash situados en "bin". -- 2.40.1 From 528e189fb2bf74c1ab7348182b8c5f680a4e5586 Mon Sep 17 00:00:00 2001 From: ggil Date: Wed, 28 Aug 2024 15:13:31 +0200 Subject: [PATCH 19/70] refs #631 - Add createTorrentSum.py --- README.md | 55 ++++---- py_scripts/createTorrentSum.py | 222 +++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+), 29 deletions(-) create mode 100644 py_scripts/createTorrentSum.py diff --git a/README.md b/README.md index 18944e8..725b867 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,9 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará 4. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/delete-image` 5. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/images/recover-image` 6. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/images/import-image` -7. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/images/send-udpcast` -8. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/images/send-uftp` -9. [Crear archivo ".torrent"](#crear-archivo-torrent) - `POST /ogrepository/v1/images/create-torrent` +7. [Crear archivos "sum" y "torrent"](#crear-archivos-sum-y-torrent) - `POST /ogrepository/v1/images/create-torrentsum` +8. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/images/send-udpcast` +9. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/images/send-uftp` 10. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/images/send-p2p` 11. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/images/check-image` 12. [Ver Estado de Transmisiones Multicast-P2P](#ver-estado-de-transmisiones-multicast-p2p) - @@ -58,7 +58,6 @@ curl -X GET -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **Código 200 OK:** La información de las imágenes se obtuvo exitosamente. - **Contenido:** Información de imágenes en formato JSON. ```json - { "REPOSITORY": { "directory": "/opt/opengnsys/images", @@ -154,7 +153,6 @@ curl -X GET -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **Código 200 OK:** La información de la imagen se obtuvo exitosamente. - **Contenido:** Información de la imagen en formato JSON. ```json - { "directory": "/opt/opengnsys/images", "images": [ @@ -269,6 +267,29 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **Código 400 Bad Request:** No se ha encontrado la imagen y/o el equipo remoto especificados. - **Código 200 OK:** La imagen se ha importado exitosamente. +--- +### Crear archivos "sum" y "torrent" + +Se crearán los archivos ".sum", ".full.sum" y ".torrent", para la imagen especificada como parámetro. +Se puede hacer con el script "**createTorrentSum.py**", que hemos programado recientemente. + +**URL:** `/ogrepository/v1/images/create-torrentsum` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/create-torrentsum +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al crear los archivos. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** Los archivos se han creado exitosamente. + --- ### Enviar una Imagen mediante UDPcast @@ -325,30 +346,6 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. - **Código 200 OK:** La imagen se ha enviado exitosamente. ---- -### Crear archivo .torrent - -Se creará un archivo ".torrent" para la imagen especificada como parámetro. -Se debe crear un script que realice dicha tarea, porque actualmente se hace mediante el script "**torrent-creator**", que se ejecuta por crontab a cada minuto (y crea un archivo ".torrent" por cada imagen que no tenga uno asociado). -**NOTA**: Puede que sea preferible que esta acción la realice el propio ogLive al crear una imagen, ya que también tiene las herramientas para hacerlo. - -**URL:** `/ogrepository/v1/images/create-torrent` -**Método HTTP:** POST - -**Cuerpo de la Solicitud (JSON):** -- **image**: Nombre de la imagen (con extensión). -- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). - -**Ejemplo de Solicitud:** - -```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/create-torrent -``` -**Respuestas:** -- **Código 500 Internal Server Error:** Ocurrió un error al crear el archivo torrent. -- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. -- **Código 200 OK:** El archivo torrent se ha creado exitosamente. - --- ### Enviar una Imagen mediante P2P diff --git a/py_scripts/createTorrentSum.py b/py_scripts/createTorrentSum.py new file mode 100644 index 0000000..af8c1b4 --- /dev/null +++ b/py_scripts/createTorrentSum.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script crea los archivos ".sum", ".full.sum" y ".torrent" para la imagen que recibe como parámetro. +En principio, debería hacer lo mismo que el script bash original (cuyo nombre es "torrent-creator"). + +Debería ser llamado por ogCore u ogLive cada vez que se cree una imagen. + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a eliminar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/images/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/images/ou_subdir/image1.img + + Sintaxis +---------- +./createTorrentSum.py [ou_subdir/]image_name|/image_path/image_name + + Ejemplos + --------- +./createTorrentSum.py image1.img +./createTorrentSum.py /opt/opengnsys/images/image1.img +./createTorrentSum.py ou_subdir/image1.img +./createTorrentSum.py /ou_subdir/image1.img +./createTorrentSum.py /opt/opengnsys/images/ou_subdir/image1.img +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess +import hashlib + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/images/' +config_file = '/opt/opengnsys/etc/ogAdmRepo.cfg' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name + Ejemplo1: {script_name} image1.img + Ejemplo2: {script_name} /opt/opengnsys/images/image1.img + Ejemplo3: {script_name} ou_subdir/image1.img + Ejemplo4: {script_name} /ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/images/ou_subdir/image1.img + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 1 parámetro, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 2: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 parámetro") + show_help() + sys.exit(1) + + +def build_file_path(): + """ Construye la ruta completa al archivo a eliminar + (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "repo_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(repo_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(repo_path): + file_path = os.path.join(repo_path, param_path) + else: + file_path = param_path + return file_path + + +def get_md5_sum(file_path, megabytes=1): + """ Calcula y retorna el hash MD5 del último MB del archivo de imagen que recibe como parámetro. + Se utiliza para crear el archivo ".sum" (para transferencias Unicast y Multicast). + """ + hash_md5 = hashlib.md5() + with open(file_path, "rb") as f: + f.seek(-megabytes * 1024 * 1024, os.SEEK_END) + data = f.read(megabytes * 1024 * 1024) + hash_md5.update(data) + return hash_md5.hexdigest() + + +def get_md5_fullsum(file_path): + """ Calcula y retorna el hash MD5 del archivo de imagen que recibe como parámetro. + Se utiliza para crear el archivo ".full.sum" (para transferencias P2P). + """ + hash_md5 = hashlib.md5() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() + + +def get_IPlocal(): + """ Retorna la IP asociada a la variable "IPlocal", del archivo '/opt/opengnsys/etc/ogAdmRepo.cfg' + (que corresponde a la IP del repositorio). + """ + with open(config_file, 'r') as file: + for line in file: + if line.startswith('IPlocal'): + IPlocal = line.split('=')[1].strip() + return IPlocal + + +def create_torrent(file_path, torrent_file, datafullsum): + """ Crea un archivo ".torrent" para la imagen que recibe como primer parámetro. + Obtiene la IP del repositorio llamando a la función "get_IPlocal", + que a su vez la obtiene del archivo '/opt/opengnsys/etc/ogAdmRepo.cfg'. + """ + # Almacenamos la IP del repositorio, y construimos la URL del tracker: + repo_ip = get_IPlocal() + tracker_url = f"http://{repo_ip}:6969/announce" + + # Creamos una lista con el comando para crear el torrrent, y lo imprimimos con espacios: + splitted_cmd = f"nice -n 0 ctorrent -t {file_path} -u {tracker_url} -s {torrent_file} -c {datafullsum} -l 4194304".split() + print(f"Sending command: {' '.join(splitted_cmd)}") + + # Ejecutamos el comando en el sistema, e imprimimos el resultado: + try: + result = subprocess.run(splitted_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"ReturnCode: {result.returncode}") + except subprocess.CalledProcessError as error: + print(f"ReturnCode: {error.returncode}") + print(f"Error Output: {error.stderr.decode()}") + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa de la imagen: + file_path = build_file_path() + + # Si no existe el archivo de imagen, imprimimos un mensaje de error y salimos del script: + if not os.path.exists(file_path): + print("Image file doesn't exist") + sys.exit(2) + + # Si la imagen está bloqueada, imprimimos un mensaje de error y salimos del script: + if os.path.exists(f"{file_path}.lock"): + print("Image is locked") + sys.exit(3) + + # Creamos un archivo de bloqueo: + open(f"{file_path}.lock", "w").close() + + # Construimos las rutas completas de los archivos ".sum", ".full.sum" y ".torrent": + sum_file = f"{file_path}.sum" + fullsum_file = f"{file_path}.full.sum" + torrent_file = f"{file_path}.torrent" + + # Creamos el archivo ".sum" (para transferencias Unicast y Multicast): + print("Creating '.sum' file...") + with open(sum_file, 'w') as file: + datasum = get_md5_sum(file_path) + file.write(datasum) + + # Creamos el archivo ".full.sum" (para transferencias P2P): + print("Creating '.ful.sum' file...") + with open(fullsum_file, 'w') as file: + datafullsum = get_md5_fullsum(file_path) + file.write(datafullsum) + + # Creamos el archivo ".torrent" (siempre que no exista): + if not os.path.exists(torrent_file): + create_torrent(file_path, torrent_file, datafullsum) + else: + print("Torrent file exists") + + # Eliminamos el archivo de bloqueo: + os.remove(f"{file_path}.lock") + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- -- 2.40.1 From 97f1f9c62e605e624f25fe9f626c359c49614c8f Mon Sep 17 00:00:00 2001 From: ggil Date: Fri, 30 Aug 2024 13:43:49 +0200 Subject: [PATCH 20/70] refs #631 - Add runTorrentTracker.py and runTorrentSeeder.py --- README.md | 78 +++++++++++-------- bin/torrent-tracker | 7 +- bin/torrent-tracker_OLD | 20 +++++ py_scripts/runTorrentSeeder.py | 130 ++++++++++++++++++++++++++++++++ py_scripts/runTorrentTracker.py | 89 ++++++++++++++++++++++ 5 files changed, 291 insertions(+), 33 deletions(-) create mode 100644 bin/torrent-tracker_OLD create mode 100644 py_scripts/runTorrentSeeder.py create mode 100644 py_scripts/runTorrentTracker.py diff --git a/README.md b/README.md index 725b867..dbd1c9f 100644 --- a/README.md +++ b/README.md @@ -27,15 +27,16 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará 5. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/images/recover-image` 6. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/images/import-image` 7. [Crear archivos "sum" y "torrent"](#crear-archivos-sum-y-torrent) - `POST /ogrepository/v1/images/create-torrentsum` -8. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/images/send-udpcast` -9. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/images/send-uftp` -10. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/images/send-p2p` -11. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/images/check-image` -12. [Ver Estado de Transmisiones Multicast-P2P](#ver-estado-de-transmisiones-multicast-p2p) - -13. [Cancelar Transmisión Multicast-P2P](#cancelar-transmisión-multicast-p2p) - +8. [Iniciar el Tracker P2P](#iniciar-el-tracker-p2p) - `POST /ogrepository/v1/images/run-tracker` +9. [Iniciar el Seeder P2P](#iniciar-el-seeder-p2p) - `POST /ogrepository/v1/images/run-seeder` +10. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/images/send-udpcast` +11. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/images/send-uftp` +12. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/images/check-image` +13. [Ver Estado de Transmisiones Multicast-P2P](#ver-estado-de-transmisiones-multicast-p2p) - +14. [Cancelar Transmisión Multicast-P2P](#cancelar-transmisión-multicast-p2p) - --- -### Obtener Información de todas las Imágenes +### Obtener Información de todas las Imágenes Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (que corresponde a todas las imágenes almacenadas en el repositorio), y en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (que corresponde a las imágenes que fueron eliminadas, que estarán en la papelera). Se puede utilizar el script "**getRepoInfo.py**, que hemos programado recientemente. @@ -290,6 +291,46 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. - **Código 200 OK:** Los archivos se han creado exitosamente. +--- +### Iniciar el Tracker P2P + +Se iniciará el tracker "bttrack" (o se reiniciará, en el caso de que ya estuviera iniciado), para hacer tracking de los torrents almacenados en el directorio de imágenes de ogRepository. +Se puede hacer con el script "**runTorrentTracker.py**", que hemos programado recientemente. +**NOTA**: Actualmente esto se hace automáticamente, al iniciar ogRepopository (desde el script "**/etc/init.d/opengnsys**"), pero creemos que solo debe hacerse cuando se solicite una descarga P2P. + +**URL:** `/ogrepository/v1/images/run-tracker` +**Método HTTP:** POST + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/run-tracker +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al iniciar el tracker. +- **Código 200 OK:** El tracker se ha iniciado exitosamente. + +--- +### Iniciar el Seeder P2P + +Se iniciará el seeder "bittornado" (o se reiniciará, en el caso de que ya estuviera iniciado), para hacer seed de los torrents almacenados en la raíz del directorio de imágenes de ogRepository (o en el subidrectorio de OU que se especifique). +Se puede hacer con el script "**runTorrentSeeder.py**", que hemos programado recientemente. +**NOTA**: Actualmente esto se hace automáticamente, al iniciar ogRepopository (desde el script "**/etc/init.d/opengnsys**"), pero creemos que solo debe hacerse cuando se solicite una descarga P2P. + +**URL:** `/ogrepository/v1/images/run-seeder` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ou_subdir":"none"}' http://example.com/ogrepository/v1/images/run-seeder +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al iniciar el seeder. +- **Código 200 OK:** El seeder se ha iniciado exitosamente. --- ### Enviar una Imagen mediante UDPcast @@ -346,29 +387,6 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. - **Código 200 OK:** La imagen se ha enviado exitosamente. ---- -### Enviar una Imagen mediante P2P - -Se debe hacer tracking de los torrents almacenados en ogRepository, e iniciar la transferencia de la imagen especificada (además, los clientes deben disponer del torrent asociado, y añadirlo a su cliente torrent). -No tengo claro cómo se haría con los scripts existentes (que utilizan "bttrack" y "ctorrent"), pero si usáramos "opentracker" y "transmission" (como se había propuesto), se debería crear nuevos scripts. - -**URL:** `/ogrepository/v1/images/send-p2p` -**Método HTTP:** POST - -**Cuerpo de la Solicitud (JSON):** -- **image**: Nombre de la imagen (con extensión). -- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). - -**Ejemplo de Solicitud:** - -```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/send-p2p -``` -**Respuestas:** -- **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. -- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. -- **Código 200 OK:** La imagen se ha enviado exitosamente. - --- ### Chequear Integridad de Imagen diff --git a/bin/torrent-tracker b/bin/torrent-tracker index 55e3bef..36f8b19 100644 --- a/bin/torrent-tracker +++ b/bin/torrent-tracker @@ -3,18 +3,19 @@ BTTRACK=/usr/bin/bttrack.bittorrent BTSEEDER=/usr/bin/btlaunchmany.bittornado BTTRACKPORT=6969 BTTRACKDFILE=/tmp/dstate +BTTRACKLOG=/opt/opengnsys/log/bttrack.log BTINTERVAL=10 BTTORRENTSDIR=/opt/opengnsys/images # Desactivar descarga de torrents desde clientes no autorizados. BTALLOW_GET=0 # parametros basados en EAC 05-04-2009 antonio doblas viso. -BTTRACK_OPTIONS=" --save_dfile_interval $BTINTERVAL --timeout_downloaders_interval $BTINTERVAL --port $BTTRACKPORT --dfile $BTTRACKDFILE --reannounce_interval $BTINTERVAL --allowed_dir $BTTORRENTSDIR --allow_get $BTALLOW_GET " +BTTRACK_OPTIONS=" --save_dfile_interval $BTINTERVAL --timeout_downloaders_interval $BTINTERVAL --port $BTTRACKPORT --dfile $BTTRACKDFILE --reannounce_interval $BTINTERVAL --logfile $BTTRACKLOG --allowed_dir $BTTORRENTSDIR --allow_get $BTALLOW_GET " BTTRACKPID="/var/run/bttrack.pid" BTSEEDERPID="/var/run/btseeder.pid" ################### ####################################### -systemctl --user stop bttrack.service +pkill bttrack rm -f $BTTRACKDFILE sleep 2 -systemd-run --user --unit bttrack.service --wait bttrack $BTTRACK_OPTIONS +bttrack $BTTRACK_OPTIONS &>> $BTTRACKLOG & diff --git a/bin/torrent-tracker_OLD b/bin/torrent-tracker_OLD new file mode 100644 index 0000000..55e3bef --- /dev/null +++ b/bin/torrent-tracker_OLD @@ -0,0 +1,20 @@ +#!/bin/bash +BTTRACK=/usr/bin/bttrack.bittorrent +BTSEEDER=/usr/bin/btlaunchmany.bittornado +BTTRACKPORT=6969 +BTTRACKDFILE=/tmp/dstate +BTINTERVAL=10 +BTTORRENTSDIR=/opt/opengnsys/images +# Desactivar descarga de torrents desde clientes no autorizados. +BTALLOW_GET=0 +# parametros basados en EAC 05-04-2009 antonio doblas viso. +BTTRACK_OPTIONS=" --save_dfile_interval $BTINTERVAL --timeout_downloaders_interval $BTINTERVAL --port $BTTRACKPORT --dfile $BTTRACKDFILE --reannounce_interval $BTINTERVAL --allowed_dir $BTTORRENTSDIR --allow_get $BTALLOW_GET " +BTTRACKPID="/var/run/bttrack.pid" +BTSEEDERPID="/var/run/btseeder.pid" + +################### ####################################### + +systemctl --user stop bttrack.service +rm -f $BTTRACKDFILE +sleep 2 +systemd-run --user --unit bttrack.service --wait bttrack $BTTRACK_OPTIONS diff --git a/py_scripts/runTorrentSeeder.py b/py_scripts/runTorrentSeeder.py new file mode 100644 index 0000000..ad9b18c --- /dev/null +++ b/py_scripts/runTorrentSeeder.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script inicia el seeder "bittornado" (o lo reinicia, si ya estuviera iniciado), finalizando previamente cualquier proceso activo. +En principio, debería hacer lo mismo que la sección correspondiente del script "/etc/init.d/opengnsys", que se ejecuta al inicio (pero que debería dejar de hacerlo). +Creemos que debe ser llamado únicamente cuando se quiera hacer una descarga mediante P2P (junto al script "runTorrentTracker.py"). +NOTA: El paquete no hace una búsqueda recursiva, por lo que se debe especificar el subdirectorio correspondiente a la OU, si es el caso. + + Parámetros +------------ +sys.argv[1] - Subdirectorio correspondiente a la OU (o "none" si no es el caso). + - Ejemplo1: none + - Ejemplo2: ou_subdir + + Sintaxis +---------- +./runTorrentSeeder.py none|ou_subdir + + Ejemplos + --------- +./runTorrentSeeder.py none +./runTorrentSeeder.py ou_subdir +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/images' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} none|ou_subdir + Ejemplo1: {script_name} none + Ejemplo2: {script_name} ou_subdir + """ + print(help_text) + + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 1 parámetro, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 2: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 parámetro") + show_help() + sys.exit(1) + + + +def run_bittornado(torrent_path): + """ Ejecuta el comando "btlaunchmany.bittornado", con sus parámetros correspondientes. + Además, captura el resultado y los posibles errores, y los imprime. + """ + # Creamos una lista con el comando "btlaunchmany.bittornado" y sus parámetros, y lo imprimimos con espacios: + splitted_cmd = f"btlaunchmany.bittornado {torrent_path}".split() + print(f"Sending command: {' '.join(splitted_cmd)}") + + # Ejecutamos el comando "btlaunchmany.bittornado" en el sistema, e imprimimos el resultado: + try: + result = subprocess.run(splitted_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"Bittornado ReturnCode: {result.returncode}") + except subprocess.CalledProcessError as error: + print(f"Bittornado ReturnCode: {error.returncode}") + print(f"Bittornado Error Output: {error.stderr.decode()}") + except Exception as error: + print(f"Unexpected bittornado error: {error}") + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Finalizamos el proceso "btlaunchmany.bittornado" (en caso de que estuviera corriendo): + try: + subprocess.run(f"pkill btlaunchmany.bittornado".split(), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except Exception as error_description: + print(f"No bittornado process running? Returned error: {error_description}") + + # Construimos la ruta en la que buscar los torrents, en base al parámetro especificado: + if sys.argv[1] == 'none': + torrent_path = repo_path + else: + torrent_path = f"{repo_path}/{sys.argv[1]}" + + # Ejecutamos el comando "btlaunchmany.bittornado" (para hacer seed de los torrents): + run_bittornado(torrent_path) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/py_scripts/runTorrentTracker.py b/py_scripts/runTorrentTracker.py new file mode 100644 index 0000000..4e9cd38 --- /dev/null +++ b/py_scripts/runTorrentTracker.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script inicia el tracker "bttrack" (o lo reinicia, si ya estuviera iniciado), finalizando previamente cualquier proceso activo, y borrando el archivo "/tmp/dstate". +En principio, debería hacer lo mismo que el script bash original (cuyo nombre es "torrent-tracker"). + +No recibe ningún parámetro, y creemos que debe ser llamado únicamente cuando se quiera hacer una descarga mediante P2P (junto al script "runTorrentSeeder.py"). +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import subprocess +import time + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +repo_path = '/opt/opengnsys/images' + +bttrack_port = 6969 +bttrack_dfile = '/tmp/dstate' +bttrack_log = '/opt/opengnsys/log/bttrack.log' +bttrack_interval = 10 +bttrack_allow_get = 1 # Este es el valor que estaba en "etc/init.d/opengnsys" (en "torrent-tracker", que no se ejecutaba, el valor era "0") + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def run_bttrack(): + """ Ejecuta el comando "bttrack", con sus parámetros correspondientes. + Además, captura el resultado y los posibles errores, y los imprime. + """ + # Creamos una lista con el comando "bttrack" y sus parámetros, y lo imprimimos con espacios: + splitted_cmd = f"bttrack --port {bttrack_port} --dfile {bttrack_dfile} --save_dfile_interval {bttrack_interval} --reannounce_interval {bttrack_interval} --logfile {bttrack_log} --allowed_dir {repo_path} --allow_get {bttrack_allow_get}".split() + print(f"Sending command: {' '.join(splitted_cmd)}") + + # Ejecutamos el comando "bttrack" en el sistema, e imprimimos el resultado: + try: + result = subprocess.run(splitted_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"Bttrack ReturnCode: {result.returncode}") + except subprocess.CalledProcessError as error: + print(f"Bttrack ReturnCode: {error.returncode}") + print(f"Bttrack Error Output: {error.stderr.decode()}") + except Exception as error: + print(f"Unexpected bttrack error: {error}") + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Finalizamos el proceso "bttrack" (en caso de que estuviera corriendo): + try: + subprocess.run(f"pkill bttrack".split(), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except Exception as error_description: + print(f"No bttrack process running? Returned error: {error_description}") + + # Si existe el archivo "/tmp/dstate", lo eliminamos: + if os.path.exists(bttrack_dfile): + os.remove(bttrack_dfile) + + # Esperamos 2 segundos: + time.sleep(2) + + # Ejecutamos el comando "bttrack" (para hacer tracking de los torrents): + run_bttrack() + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- -- 2.40.1 From 709c2d7f276089d2ca6664e1fcdf6e8cdd179c2c Mon Sep 17 00:00:00 2001 From: ggil Date: Mon, 2 Sep 2024 17:14:33 +0200 Subject: [PATCH 21/70] refs #631 - Add size, sum and fullsum Info (6 scripts modified) --- README.md | 31 ++++++++++++--- py_scripts/createTorrentSum.py | 67 +++++++++++++++++++++++++-------- py_scripts/deleteImage.py | 2 +- py_scripts/importImage.py | 2 +- py_scripts/recoverImage.py | 4 +- py_scripts/runTorrentTracker.py | 4 +- py_scripts/updateRepoInfo.py | 33 ++++++++++++---- py_scripts/updateTrashInfo.py | 29 +++++++++++--- 8 files changed, 131 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index dbd1c9f..93c1c01 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,10 @@ curl -X GET -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d "clonator": "partclone", "compressor": "lzop", "filesystem": "EXTFS", - "datasize": 9859634200 + "datasize": 9859634200000, + "size": 4505673214, + "sum": "065a933c780ab1aaa044435ad5d4bf87", + "fullsum": "33575b9070e4a8043371b8c6ae52b80e" }, { "name": "Windows10", @@ -79,7 +82,10 @@ curl -X GET -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d "clonator": "partclone", "compressor": "lzop", "filesystem": "NTFS", - "datasize": 23654400000 + "datasize": 24222105600000, + "size": 13198910185, + "sum": "8874d5ab84314f44841c36c69bb5aa82", + "fullsum": "9e7cd32c606ebe5bd39ba212ce7aeb02" } ], "ous": [ @@ -93,7 +99,10 @@ curl -X GET -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d "clonator": "partclone", "compressor": "lzop", "filesystem": "EXTFS", - "datasize": 8704000000 + "datasize": 8912896000000, + "size": 3803794535, + "sum": "081a933c780ab1aaa044435ad5d4bf56", + "fullsum": "22735b9070e4a8043371b8c6ae52b90d" } ] } @@ -114,7 +123,10 @@ curl -X GET -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d "clonator": "partclone", "compressor": "lzop", "filesystem": "EXTFS", - "datasize": 8704000000 + "datasize": 8912896000000, + "size": 3803794535, + "sum": "081a933c780ab1aaa044435ad5d4bf56", + "fullsum": "22735b9070e4a8043371b8c6ae52b90d" } ] } @@ -128,6 +140,9 @@ curl -X GET -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **compressor**: Programa utilizado para la compresión. - **filesystem**: Sistema de archivos utilizado en la partición clonada. - **datasize**: Tamaño de la imagen una vez restaurada, en bytes (tamaño de los datos). + - **size**: Tamaño del archivo de imagen, en bytes. + - **sum**: Hash MD5 del último MB del archivo de imagen. + - **fullsum**: Hash MD5 de todo el archivo de imagen. --- ### Obtener Información de una Imagen concreta @@ -164,7 +179,10 @@ curl -X GET -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d "clonator": "partclone", "compressor": "lzop", "filesystem": "NTFS", - "datasize": 23654400000 + "datasize": 9859634200000, + "size": 4505673214, + "sum": "065a933c780ab1aaa044435ad5d4bf87", + "fullsum": "33575b9070e4a8043371b8c6ae52b80e" } ] } @@ -176,6 +194,9 @@ curl -X GET -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **compressor**: Programa utilizado para la compresión. - **filesystem**: Sistema de archivos utilizado en la partición clonada. - **datasize**: Tamaño de la imagen una vez restaurada, en bytes (tamaño de los datos). + - **size**: Tamaño del archivo de imagen, en bytes. + - **sum**: Hash MD5 del último MB del archivo de imagen. + - **fullsum**: Hash MD5 de todo el archivo de imagen. --- ### Actualizar Información del Repositorio diff --git a/py_scripts/createTorrentSum.py b/py_scripts/createTorrentSum.py index af8c1b4..b61f753 100644 --- a/py_scripts/createTorrentSum.py +++ b/py_scripts/createTorrentSum.py @@ -2,14 +2,15 @@ # -*- coding: utf-8 -*- """ -Este script crea los archivos ".sum", ".full.sum" y ".torrent" para la imagen que recibe como parámetro. -En principio, debería hacer lo mismo que el script bash original (cuyo nombre es "torrent-creator"). +Este script crea los archivos ".size", ".sum", ".full.sum" y ".torrent" para la imagen que recibe como parámetro. +Hace casi lo mismo que el script bash original (cuyo nombre es "torrent-creator"), pero añade el archivo ".size". +Al acabar, llama al script "updateRepoInfo.py", para actualizar la información del repositorio. Debería ser llamado por ogCore u ogLive cada vez que se cree una imagen. Parámetros ------------ -sys.argv[1] - Nombre completo de la imagen a eliminar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. +sys.argv[1] - Nombre completo de la imagen (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. - Ejemplo1: image1.img - Ejemplo2: /opt/opengnsys/images/image1.img - Ejemplo3: ou_subdir/image1.img @@ -46,6 +47,8 @@ import hashlib script_name = os.path.basename(__file__) repo_path = '/opt/opengnsys/images/' config_file = '/opt/opengnsys/etc/ogAdmRepo.cfg' +update_repo_script = '/opt/opengnsys/py_scripts/updateRepoInfo.py' + # -------------------------------------------------------------------------------------------- @@ -84,7 +87,7 @@ def check_params(): def build_file_path(): - """ Construye la ruta completa al archivo a eliminar + """ Construye la ruta completa al archivo de imagen (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). """ param_path = sys.argv[1] @@ -158,6 +161,20 @@ def create_torrent(file_path, torrent_file, datafullsum): print(f"Se ha producido un error inesperado: {error}") +def update_repo_info(): + """ Actualiza la información del repositorio, ejecutando el script "updateRepoInfo.py". + Como se ve, es necesario que el script se ejecute como sudo, o dará error. + """ + try: + result = subprocess.run(['sudo', 'python3', update_repo_script], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except subprocess.CalledProcessError as error: + print(f"Error Output: {error.stderr.decode()}") + sys.exit(3) + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + sys.exit(4) + + # -------------------------------------------------------------------------------------------- # MAIN @@ -186,22 +203,38 @@ def main(): # Creamos un archivo de bloqueo: open(f"{file_path}.lock", "w").close() - # Construimos las rutas completas de los archivos ".sum", ".full.sum" y ".torrent": + # Construimos las rutas completas de los archivos ".size", ".sum", ".full.sum" y ".torrent": + size_file = f"{file_path}.size" sum_file = f"{file_path}.sum" fullsum_file = f"{file_path}.full.sum" torrent_file = f"{file_path}.torrent" - # Creamos el archivo ".sum" (para transferencias Unicast y Multicast): - print("Creating '.sum' file...") - with open(sum_file, 'w') as file: - datasum = get_md5_sum(file_path) - file.write(datasum) + # Creamos el archivo ".size" (pque almacenará el tamaño del archivo), siempre que no exista: + if not os.path.exists(size_file): + print("Creating '.size' file...") + with open(size_file, 'w') as file: + datasize = os.path.getsize(file_path) + file.write(str(datasize)) + else: + print("Size file exists") - # Creamos el archivo ".full.sum" (para transferencias P2P): - print("Creating '.ful.sum' file...") - with open(fullsum_file, 'w') as file: - datafullsum = get_md5_fullsum(file_path) - file.write(datafullsum) + # Creamos el archivo ".sum" (para transferencias Unicast y Multicast), siempre que no exista: + if not os.path.exists(sum_file): + print("Creating '.sum' file...") + with open(sum_file, 'w') as file: + datasum = get_md5_sum(file_path) + file.write(datasum) + else: + print("Sum file exists") + + # Creamos el archivo ".full.sum" (para transferencias P2P), siempre que no exista: + if not os.path.exists(fullsum_file): + print("Creating '.ful.sum' file...") + with open(fullsum_file, 'w') as file: + datafullsum = get_md5_fullsum(file_path) + file.write(datafullsum) + else: + print("Fullsum file exists") # Creamos el archivo ".torrent" (siempre que no exista): if not os.path.exists(torrent_file): @@ -212,6 +245,10 @@ def main(): # Eliminamos el archivo de bloqueo: os.remove(f"{file_path}.lock") + # Actualizamos la información del repositorio, ejecutando el script "updateRepoInfo.py": + print("Updating Repository Info...") + update_repo_info() + # -------------------------------------------------------------------------------------------- diff --git a/py_scripts/deleteImage.py b/py_scripts/deleteImage.py index aac9b1a..b630319 100644 --- a/py_scripts/deleteImage.py +++ b/py_scripts/deleteImage.py @@ -220,7 +220,7 @@ def main(): # Creamos una lista con las extensiones de los archivos asociados a la imagen # (incluyendo ninguna extensión, que corresponde a la propia imagen): - extensions = ['', '.sum', '.full.sum', '.torrent', '.info', '.info.checked'] + extensions = ['', '.size', '.sum', '.full.sum', '.torrent', '.info', '.info.checked'] # Evaluamos la cantidad de barras que hay en la ruta de la imagen, para diferenciar entre imágenes "normales" y basadas en OU # (y llamamos a la función correspondiente para eliminarla): diff --git a/py_scripts/importImage.py b/py_scripts/importImage.py index 934b308..1df1b14 100644 --- a/py_scripts/importImage.py +++ b/py_scripts/importImage.py @@ -122,7 +122,7 @@ def import_image(file_path, remote_host, remote_user): """ # Creamos una lista con las extensiones de los archivos asociados a la imagen # (incluyendo ninguna extensión, que corresponde a la propia imagen): - extensions = ['', '.sum', '.full.sum', '.torrent', '.info.checked'] + extensions = ['', '.size', '.sum', '.full.sum', '.torrent', '.info.checked'] # Iniciamos un cliente SSH: ssh_client = paramiko.SSHClient() diff --git a/py_scripts/recoverImage.py b/py_scripts/recoverImage.py index 15281de..bc2f059 100644 --- a/py_scripts/recoverImage.py +++ b/py_scripts/recoverImage.py @@ -172,7 +172,7 @@ def main(): # Creamos una lista con las extensiones de los archivos asociados a la imagen # (incluyendo ninguna extensión, que corresponde a la propia imagen): - extensions = ['', '.sum', '.full.sum', '.torrent', '.info', '.info.checked'] + extensions = ['', '.size', '.sum', '.full.sum', '.torrent', '.info', '.info.checked'] # Evaluamos la cantidad de barras que hay en la ruta de la imagen, para diferenciar entre imágenes "normales" y basadas en OU # (y llamamos a la función correspondiente para recuperarla): @@ -195,5 +195,3 @@ if __name__ == "__main__": main() # -------------------------------------------------------------------------------------------- - - diff --git a/py_scripts/runTorrentTracker.py b/py_scripts/runTorrentTracker.py index 4e9cd38..b898fd2 100644 --- a/py_scripts/runTorrentTracker.py +++ b/py_scripts/runTorrentTracker.py @@ -3,7 +3,7 @@ """ Este script inicia el tracker "bttrack" (o lo reinicia, si ya estuviera iniciado), finalizando previamente cualquier proceso activo, y borrando el archivo "/tmp/dstate". -En principio, debería hacer lo mismo que el script bash original (cuyo nombre es "torrent-tracker"). +En principio, debería hacer lo mismo que el script bash original (cuyo nombre es "torrent-tracker"), que se ejecutaba por cron cada hora. No recibe ningún parámetro, y creemos que debe ser llamado únicamente cuando se quiera hacer una descarga mediante P2P (junto al script "runTorrentSeeder.py"). """ @@ -27,7 +27,7 @@ bttrack_port = 6969 bttrack_dfile = '/tmp/dstate' bttrack_log = '/opt/opengnsys/log/bttrack.log' bttrack_interval = 10 -bttrack_allow_get = 1 # Este es el valor que estaba en "etc/init.d/opengnsys" (en "torrent-tracker", que no se ejecutaba, el valor era "0") +bttrack_allow_get = 0 # Este valor impide la descarga desde clientes no autorizados # -------------------------------------------------------------------------------------------- diff --git a/py_scripts/updateRepoInfo.py b/py_scripts/updateRepoInfo.py index 9f5b939..2506de1 100644 --- a/py_scripts/updateRepoInfo.py +++ b/py_scripts/updateRepoInfo.py @@ -8,8 +8,7 @@ La información es obtenida desde archivos ".info", que originalmen pero que en esta versión son renombrados a ".info.checked", obteniendo la misma funcionalidad. Al acabar, llama al script "updateTrashInfo.py", para actualizar también la información de la papelera del repositorio. -No recibe ningún parámetro, y debería ser llamado por el propio ogRepoitory cada vez que se elimine una imagen (desde el script "deleteImage.py"), - y por ogCore u ogLive cada vez que se cree una imagen. +No recibe ningún parámetro, y es llamado por el propio ogRepoitory cada vez que se elimina una imagen (desde el script "deleteImage.py"). """ @@ -18,6 +17,7 @@ No recibe ningún parámetro, y debería ser llamado por el propio ogRepoitory c # -------------------------------------------------------------------------------------------- import os +import sys import json import subprocess import shutil @@ -77,11 +77,18 @@ def check_files(): # En caso contrario, almacenamos el contenido del archivo ".info" (del tipo "PARTCLONE:LZOP:EXTFS:8500000:Ubuntu_20") en la variable "data": else: with open(info_file, 'r') as file: - data = file.read() + info_data = file.read() + # Almacenamos el contenido de los archivos ".size", ".sum" y ".full.sum": + with open(f"{img_path}.size", 'r') as file: + size = file.read().strip('\n') + with open(f"{img_path}.sum", 'r') as file: + _sum = file.read().strip('\n') + with open(f"{img_path}.full.sum", 'r') as file: + fullsum = file.read().strip('\n') # Llamamos a la función "add_to_json", para que inserte la información de la imagen en el archivo json # (pasándole el nombre de la imagen, la extensión, y los datos extraídos del archivo ".info"): img_name = os.path.relpath(img_path, repo_path) - add_to_json(os.path.splitext(img_name)[0], os.path.splitext(img_name)[1][1:], data) + add_to_json(os.path.splitext(img_name)[0], os.path.splitext(img_name)[1][1:], info_data, size, _sum, fullsum) # Renombramos el archivo ".info" a ".info.checked", para que ya no se añada la información que contiene # (originalmente se eliminaba el archivo, pero creo que es mejor mantenerlo): @@ -115,15 +122,22 @@ def check_dirs(): fstype = line.split("=")[1].strip() elif line.startswith("# sizedata"): sizedata = line.split("=")[1].strip() - data = f"rsync::{fstype}:{sizedata}:" + info_data = f"rsync::{fstype}:{sizedata}:" + # Almacenamos el contenido de los archivos ".size", ".sum" y ".full.sum": + with open(f"{img_path}.size", 'r') as file: + size = file.read().strip('\n') + with open(f"{img_path}.sum", 'r') as file: + _sum = file.read().strip('\n') + with open(f"{img_path}.full.sum", 'r') as file: + fullsum = file.read().strip('\n') # Llamamos a la función "add_to_json", para que inserte la información de la imagen en el archivo json # (pasándole el nombre de la imagen, el tipo "dir"", y los datos extraídos del archivo "ogimg.info"): img_name = os.path.relpath(img_path, repo_path) - add_to_json(img_name, "dir", data) + add_to_json(img_name, "dir", info_data, size, _sum, fullsum) -def add_to_json(image_name, image_type, data): +def add_to_json(image_name, image_type, data, size, _sum, fullsum): """ Esta función añade al archivo "repoinfo.json" la información de las imágenes que aun no ha sido introducida en él (imágenes nuevas, básicamente). El procedimiento es diferente para las imágenes "normales" y para las imágenes basadas en OU. """ @@ -144,7 +158,10 @@ def add_to_json(image_name, image_type, data): "clonator": clonator.lower(), "compressor": compressor.lower(), "filesystem": fstype.upper(), - "datasize": int(datasize) * 1024 # Convertimos el valor a bytes (desde KB) + "datasize": int(datasize) * 1024, # Convertimos el valor a bytes (desde KB) + "size": int(size), + "sum": _sum, + "fullsum": fullsum } # Almacenamos el contenido del archivo "repoinfo.json" en la variable "info_data": with open(info_file, 'r') as file: diff --git a/py_scripts/updateTrashInfo.py b/py_scripts/updateTrashInfo.py index a82677a..eed6968 100644 --- a/py_scripts/updateTrashInfo.py +++ b/py_scripts/updateTrashInfo.py @@ -92,11 +92,18 @@ def check_files(): # En caso contrario, almacenamos el contenido del archivo ".info.checked" (del tipo "PARTCLONE:LZOP:EXTFS:8500000:Ubuntu_20") en la variable "data": else: with open(info_file, 'r') as file: - data = file.read() + info_data = file.read() + # Almacenamos el contenido de los archivos ".size", ".sum" y ".full.sum": + with open(f"{img_path}.size", 'r') as file: + size = file.read().strip('\n') + with open(f"{img_path}.sum", 'r') as file: + _sum = file.read().strip('\n') + with open(f"{img_path}.full.sum", 'r') as file: + fullsum = file.read().strip('\n') # Llamamos a la función "add_to_json", para que inserte la información de la imagen en el archivo json # (pasándole el nombre de la imagen, la extensión, y los datos extraídos del archivo ".info.checked"): img_name = os.path.relpath(img_path, trash_path) - add_to_json(os.path.splitext(img_name)[0], os.path.splitext(img_name)[1][1:], data) + add_to_json(os.path.splitext(img_name)[0], os.path.splitext(img_name)[1][1:], info_data, size, _sum, fullsum) @@ -126,15 +133,22 @@ def check_dirs(): fstype = line.split("=")[1].strip() elif line.startswith("# sizedata"): sizedata = line.split("=")[1].strip() - data = f"rsync::{fstype}:{sizedata}:" + info_data = f"rsync::{fstype}:{sizedata}:" + # Almacenamos el contenido de los archivos ".size", ".sum" y ".full.sum": + with open(f"{img_path}.size", 'r') as file: + size = file.read().strip('\n') + with open(f"{img_path}.sum", 'r') as file: + _sum = file.read().strip('\n') + with open(f"{img_path}.full.sum", 'r') as file: + fullsum = file.read().strip('\n') # Llamamos a la función "add_to_json", para que inserte la información de la imagen en el archivo json # (pasándole el nombre de la imagen, el tipo "dir"", y los datos extraídos del archivo "ogimg.info"): img_name = os.path.relpath(img_path, trash_path) - add_to_json(img_name, "dir", data) + add_to_json(img_name, "dir", info_data, size, _sum, fullsum) -def add_to_json(image_name, image_type, data): +def add_to_json(image_name, image_type, data, size, _sum, fullsum): """ Esta función añade al archivo "trashinfo.json" la información de las imágenes que aun no ha sido introducida en él (imágenes eliminadas recientemente, básicamente). El procedimiento es diferente para las imágenes "normales" y para las imágenes basadas en OU. """ @@ -155,7 +169,10 @@ def add_to_json(image_name, image_type, data): "clonator": clonator.lower(), "compressor": compressor.lower(), "filesystem": fstype.upper(), - "datasize": int(datasize) * 1024 # Convertimos el valor a bytes (desde KB) + "datasize": int(datasize) * 1024, # Convertimos el valor a bytes (desde KB) + "size": int(size), + "sum": _sum, + "fullsum": fullsum } # Almacenamos el contenido del archivo "trashinfo.json" en la variable "info_data": with open(info_file, 'r') as file: -- 2.40.1 From 7859bb65fadcceee496fd5e82c30e506e47e8334 Mon Sep 17 00:00:00 2001 From: ggil Date: Tue, 3 Sep 2024 16:34:55 +0200 Subject: [PATCH 22/70] refs #631 - Add checkImage.py --- README.md | 75 ++++++++--------- py_scripts/checkImage.py | 168 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+), 37 deletions(-) create mode 100644 py_scripts/checkImage.py diff --git a/README.md b/README.md index 93c1c01..00a5cd0 100644 --- a/README.md +++ b/README.md @@ -22,16 +22,16 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará 1. [Obtener Información de todas las Imágenes](#obtener-información-de-todas-las-imágenes) - `GET /ogrepository/v1/images/get-info` 2. [Obtener Información de una Imagen concreta](#obtener-información-de-una-imagen-concreta) - `GET /ogrepository/v1/images/get-info` -3. [Actualizar Información del Repositorio](#actualizar-información-del-repositorio) - `PUT /ogrepository/v1/images` -4. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/delete-image` -5. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/images/recover-image` -6. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/images/import-image` -7. [Crear archivos "sum" y "torrent"](#crear-archivos-sum-y-torrent) - `POST /ogrepository/v1/images/create-torrentsum` -8. [Iniciar el Tracker P2P](#iniciar-el-tracker-p2p) - `POST /ogrepository/v1/images/run-tracker` -9. [Iniciar el Seeder P2P](#iniciar-el-seeder-p2p) - `POST /ogrepository/v1/images/run-seeder` -10. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/images/send-udpcast` -11. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/images/send-uftp` -12. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/images/check-image` +3. [Actualizar Información del Repositorio](#actualizar-información-del-repositorio) - `PUT /ogrepository/v1/images` +4. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/images/check-image` +5. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/delete-image` +6. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/images/recover-image` +7. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/images/import-image` +8. [Crear archivos ".sum", ".full.sum", ".size" y ".torrent"](#crear-archivos-sum-fullsum-size-y-torrent) - `POST /ogrepository/v1/images/create-torrentsum` +9. [Iniciar el Tracker P2P](#iniciar-el-tracker-p2p) - `POST /ogrepository/v1/images/run-tracker` +10. [Iniciar el Seeder P2P](#iniciar-el-seeder-p2p) - `POST /ogrepository/v1/images/run-seeder` +11. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/images/send-udpcast` +12. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/images/send-uftp` 13. [Ver Estado de Transmisiones Multicast-P2P](#ver-estado-de-transmisiones-multicast-p2p) - 14. [Cancelar Transmisión Multicast-P2P](#cancelar-transmisión-multicast-p2p) - @@ -217,6 +217,31 @@ curl -X PUT -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag - **Código 500 Internal Server Error:** Ocurrió un error al actualizar la información de las imágenes. - **Código 200 OK:** La actualización se realizó exitosamente. +--- +### Chequear Integridad de Imagen + +Se comprobará la integridad del fichero de imagen especificado como parámetro. +Se puede hacer con el script "**checkImage.py**", que compara el tamaño actual del archivo con el almacenado en el archivo "**.size**", y el hash MD5 del último MB del archivo con el almacenado en el archivo "**.sum**". +**NOTA**: En lugar del archivo "**.sum**", se ppodría usar el archivo "**.full.sum**" (que contiene el hash MD5 de todo el archivo), pero en ese caso la comprobación tardaría un poco, dependiendo del tamaño de la imagen. + +**URL:** `/ogrepository/v1/images/check-image` +**Método HTTP:** GET + +**Cuerpo de la Solicitud (JSON):** +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/check-image +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al chequear la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se ha chequeado exitosamente. +- **Código 200 KO:** La imagen se ha chequeado correctamente, pero no ha pasado el test. + --- ### Eliminar una Imagen @@ -290,9 +315,9 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **Código 200 OK:** La imagen se ha importado exitosamente. --- -### Crear archivos "sum" y "torrent" +### Crear archivos ".sum", ".full.sum", ".size" y ".torrent" -Se crearán los archivos ".sum", ".full.sum" y ".torrent", para la imagen especificada como parámetro. +Se crearán los archivos ".sum", ".full.sum", ".size" y ".torrent", para la imagen especificada como parámetro. Se puede hacer con el script "**createTorrentSum.py**", que hemos programado recientemente. **URL:** `/ogrepository/v1/images/create-torrentsum` @@ -302,7 +327,7 @@ Se puede hacer con el script "**createTorrentSum.py**", que hemos programado rec - **image**: Nombre de la imagen (con extensión). - **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). -**Ejemplo de Solicitud:** +**Ejemplo de Solicitud:** ```bash curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/create-torrentsum @@ -408,30 +433,6 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. - **Código 200 OK:** La imagen se ha enviado exitosamente. ---- -### Chequear Integridad de Imagen - -Se comprobará la integridad de todos los ficheros asociados a la imagen especificada como parámetro. -Para esto, entiendo que se debe crear un script que compare el contenido de los ficheros "**.sum**" y "**.full.sum**" con una nueva obtención del checksum de la imagen, pero no veo como comprobar la integridad de todos los archivos asociados. - -**URL:** `/ogrepository/v1/images/check-image` -**Método HTTP:** GET - -**Cuerpo de la Solicitud (JSON):** -- **image**: Nombre de la imagen (con extensión). -- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). - -**Ejemplo de Solicitud:** - -```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/check-image -``` -**Respuestas:** -- **Código 500 Internal Server Error:** Ocurrió un error al chequear la imagen. -- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. -- **Código 200 OK:** La imagen se ha chequeado exitosamente. -- **Código 200 KO:** La imagen se ha chequeado correctamente, pero no ha pasado el test. - --- ### Ver Estado de Transmisiones Multicast-P2P diff --git a/py_scripts/checkImage.py b/py_scripts/checkImage.py new file mode 100644 index 0000000..d93443b --- /dev/null +++ b/py_scripts/checkImage.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script comprueba la integridad de la imagen que recibe como parámetro, volviendo a calcular el tamaño del archivo y el hash MD5 del último MB, + y comparándolos con los valores almacenados en el archivo ".size" y en el archivo ".sum", respectivamente. + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a chequear (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/images/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/images/ou_subdir/image1.img + + Sintaxis +---------- +./checkImage.py [ou_subdir/]image_name|/image_path/image_name + + Ejemplos + --------- +./checkImage.py image1.img +./checkImage.py /opt/opengnsys/images/image1.img +./checkImage.py ou_subdir/image1.img +./checkImage.py /ou_subdir/image1.img +./checkImage.py /opt/opengnsys/images/ou_subdir/image1.img +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import hashlib + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/images/' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name + Ejemplo1: {script_name} image1.img + Ejemplo2: {script_name} /opt/opengnsys/images/image1.img + Ejemplo3: {script_name} ou_subdir/image1.img + Ejemplo4: {script_name} /ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/images/ou_subdir/image1.img + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 1 parámetro, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 2: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 parámetro") + show_help() + sys.exit(1) + + +def build_file_path(): + """ Construye la ruta completa del archivo a chequear + (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "repo_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(repo_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(repo_path): + file_path = os.path.join(repo_path, param_path) + else: + file_path = param_path + return file_path + + +def get_md5_sum(file_path, megabytes=1): + """ Calcula y retorna el hash MD5 del último MB del archivo de imagen que recibe como parámetro. + Se utiliza para comprobar el valor del archivo ".sum". + """ + hash_md5 = hashlib.md5() + with open(file_path, "rb") as f: + f.seek(-megabytes * 1024 * 1024, os.SEEK_END) + data = f.read(megabytes * 1024 * 1024) + hash_md5.update(data) + return hash_md5.hexdigest() + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo a chequear: + file_path = build_file_path() + + # Si no existe el archivo de imagen, imprimimos un mensaje de error y salimos del script: + if not os.path.exists(file_path): + print("Image file doesn't exist") + sys.exit(2) + + # Comprobamos si existe el archivo ".sum", y si su contenido coincide con el valor real: + if os.path.exists(f"{file_path}.sum"): + sumOK = False + actual_datasum = get_md5_sum(file_path) + with open(f"{file_path}.sum", 'r') as file: + file_datasum = file.read().strip('\n') + if actual_datasum == file_datasum: + sumOK = True + else: + print("Sum file doesn't exist") + sys.exit(3) + + # Comprobamos si existe el archivo ".size", y si su contenido coincide con el valor real: + if os.path.exists(f"{file_path}.size"): + sizeOK = False + actual_size = os.path.getsize(file_path) + with open(f"{file_path}.size", 'r') as file: + file_datasize = int(file.read().strip('\n')) + if actual_size == file_datasize: + sizeOK = True + else: + print("Size file doesn't exist") + sys.exit(4) + + # Evaluamos los resultados, e imprimimos un mensaje "OK" o "KO": + if sumOK == True and sizeOK == True: + print("Image file passed the Integrity Check correctly") + else: + print("Error: Image file didn't pass the Integrity Check") + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- -- 2.40.1 From e4d5d72a9c7dd798e66861b08c3b460489877f53 Mon Sep 17 00:00:00 2001 From: ggil Date: Wed, 4 Sep 2024 14:24:24 +0200 Subject: [PATCH 23/70] refs #631 - Add sendWakeOnLan.py --- README.md | 37 +++++++++--- py_scripts/sendWakeOnLan.py | 113 ++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 py_scripts/sendWakeOnLan.py diff --git a/README.md b/README.md index 00a5cd0..43c6f4e 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,13 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará 6. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/images/recover-image` 7. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/images/import-image` 8. [Crear archivos ".sum", ".full.sum", ".size" y ".torrent"](#crear-archivos-sum-fullsum-size-y-torrent) - `POST /ogrepository/v1/images/create-torrentsum` -9. [Iniciar el Tracker P2P](#iniciar-el-tracker-p2p) - `POST /ogrepository/v1/images/run-tracker` -10. [Iniciar el Seeder P2P](#iniciar-el-seeder-p2p) - `POST /ogrepository/v1/images/run-seeder` -11. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/images/send-udpcast` -12. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/images/send-uftp` -13. [Ver Estado de Transmisiones Multicast-P2P](#ver-estado-de-transmisiones-multicast-p2p) - -14. [Cancelar Transmisión Multicast-P2P](#cancelar-transmisión-multicast-p2p) - +9. [Enviar paquete Wake On Lan](#enviar-paquete-wake-on-lan) - `POST /ogrepository/v1/images/send-wol` +10. [Iniciar el Tracker P2P](#iniciar-el-tracker-p2p) - `POST /ogrepository/v1/images/run-tracker` +11. [Iniciar el Seeder P2P](#iniciar-el-seeder-p2p) - `POST /ogrepository/v1/images/run-seeder` +12. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/images/send-udpcast` +13. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/images/send-uftp` +14. [Ver Estado de Transmisiones Multicast-P2P](#ver-estado-de-transmisiones-multicast-p2p) - +15. [Cancelar Transmisión Multicast-P2P](#cancelar-transmisión-multicast-p2p) - --- ### Obtener Información de todas las Imágenes @@ -222,7 +223,7 @@ curl -X PUT -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag Se comprobará la integridad del fichero de imagen especificado como parámetro. Se puede hacer con el script "**checkImage.py**", que compara el tamaño actual del archivo con el almacenado en el archivo "**.size**", y el hash MD5 del último MB del archivo con el almacenado en el archivo "**.sum**". -**NOTA**: En lugar del archivo "**.sum**", se ppodría usar el archivo "**.full.sum**" (que contiene el hash MD5 de todo el archivo), pero en ese caso la comprobación tardaría un poco, dependiendo del tamaño de la imagen. +**NOTA**: En lugar del archivo "**.sum**", se podría usar el archivo "**.full.sum**" (que contiene el hash MD5 de todo el archivo), pero en ese caso la comprobación tardaría un poco, dependiendo del tamaño de la imagen. **URL:** `/ogrepository/v1/images/check-image` **Método HTTP:** GET @@ -337,6 +338,28 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. - **Código 200 OK:** Los archivos se han creado exitosamente. +--- +### Enviar paquete Wake On Lan + +Se enviará un paquete Wake On Lan a la dirección MAC especificada, a través de la IP de broadcast especificada. +Se puede hacer con el script "**sendWakeOnLan.py**", que hemos programado recientemente. + +**URL:** `/ogrepository/v1/images/send-wol` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **broadcast_ip**: IP de broadcast a la que enviar el paquete (puede ser "255.255.255.255", o la IP de broadcast de una subred). +- **mac**: Dirección MAC del equipo que se desea encender via Wake On Lan. + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"broadcast_ip":"255.255.255.255", "mac":"00:19:99:5c:bb:bb"}' http://example.com/ogrepository/v1/images/send-wol +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al enviar el paquete Wake On Lan. +- **Código 200 OK:** El paquete Wake On Lan se ha enviado exitosamente. + --- ### Iniciar el Tracker P2P diff --git a/py_scripts/sendWakeOnLan.py b/py_scripts/sendWakeOnLan.py new file mode 100644 index 0000000..7fc0923 --- /dev/null +++ b/py_scripts/sendWakeOnLan.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script envía un paquete mágico WOL a la dirección MAC especificada como segundo parámetro, a través de la IP de broadcast especificada como primer parámetro. +La IP de broadcast puede corresponder a toda la red ("255.255.255.255"), que es el valor por defecto, o a una subred concreta (por ejemplo, "10.2.7.255"). + + Parámetros +------------ +sys.argv[1] - Dirección IP de broadcast de toda la red o de una subred concreta. + - Ejemplo1: 255.255.255.255 + - Ejemplo2: 10.2.7.255 + +sys.argv[2] - Dirección MAC del equipo que se quiere enceder via WOL. + - Ejemplo: 00:19:99:5c:bb:bb + + Sintaxis +---------- +./sendWakeOnLan.py broadcast_IP MAC + + Ejemplos + --------- +./sendWakeOnLan.py 255.255.255.255 00:19:99:5c:bb:bb +./sendWakeOnLan.py 10.2.7.255 00:19:99:5c:bb:bb +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} broadcast_IP MAC + Ejemplo1: {script_name} 255.255.255.255 00:19:99:5c:bb:bb + Ejemplo2: {script_name} 10.2.7.255 00:19:99:5c:bb:bb + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 2 parámetros, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 3: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 2 parámetros") + show_help() + sys.exit(1) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Almacenamos los parámetros en variables: + broadcast_IP = sys.argv[1] + MAC = sys.argv[2] + + # Creamos una lista con el comando a enviar, y lo imprimimos con espacios: + splitted_cmd = f"wakeonlan -i {broadcast_IP} {MAC}".split() + + print(f"Sending command: {' '.join(splitted_cmd)}") + + # Ejecutamos el comando en el sistema, e imprimimos el resultado: + try: + result = subprocess.run(splitted_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"ReturnCode: {result.returncode}") + except subprocess.CalledProcessError as error: + print(f"ReturnCode: {error.returncode}") + print(f"Error Output: {error.stderr.decode()}") + except Exception as error_description: + print(f"Unexpeted error: {error_description}") + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- -- 2.40.1 From 9eea598cb0733667684aa2aa35e68cf3c735f197 Mon Sep 17 00:00:00 2001 From: ggil Date: Fri, 6 Sep 2024 13:21:13 +0200 Subject: [PATCH 24/70] refs #631 - Modify 'sendFileMcast.py' and 'sendFileUFTP.py' --- py_scripts/sendFileMcast.py | 25 +++++++++++++++++++------ py_scripts/sendFileUFTP.py | 24 +++++++++++++++++++----- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/py_scripts/sendFileMcast.py b/py_scripts/sendFileMcast.py index ba7e06c..ad3683b 100644 --- a/py_scripts/sendFileMcast.py +++ b/py_scripts/sendFileMcast.py @@ -7,21 +7,27 @@ En principio, debería hacer lo mismo que el script bash original (cuyo nombre e Parámetros ------------ -sys.argv[1] - Nombre completo de la imagen a enviar (con o sin ruta) +sys.argv[1] - Nombre completo de la imagen a enviar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. - Ejemplo1: image1.img - Ejemplo2: /opt/opengnsys/images/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/images/ou_subdir/image1.img sys.argv[2] - Parámetros Multicast (en formato "Port:Duplex:IP:Mpbs:Nclients:Timeout") - Ejemplo: 9000:full:239.194.17.2:70M:20:120 Sintaxis ---------- -./sendFileMcast.py image_name|/image_path/image_name Port:Duplex:IP:Mpbs:Nclients:Timeout +./sendFileMcast.py [ou_subdir/]image_name|/image_path/image_name Port:Duplex:IP:Mpbs:Nclients:Timeout Ejemplos --------- ./sendFileMcast.py image1.img 9000:full:239.194.17.2:70M:20:120 ./sendFileMcast.py /opt/opengnsys/images/image1.img 9000:full:239.194.17.2:70M:20:120 +./sendFileMcast.py ou_subdir/image1.img 9000:full:239.194.17.2:70M:20:120 +./sendFileMcast.py /ou_subdir/image1.img 9000:full:239.194.17.2:70M:20:120 +./sendFileMcast.py /opt/opengnsys/images/ou_subdir/image1.img 9000:full:239.194.17.2:70M:20:120 """ # -------------------------------------------------------------------------------------------- @@ -52,9 +58,12 @@ def show_help(): """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". """ help_text = f""" - Sintaxis: {script_name} image_name|/image_path/image_name Port:Duplex:IP:Mpbs:Nclients:Timeout + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name Port:Duplex:IP:Mpbs:Nclients:Timeout Ejemplo1: {script_name} image1.img 9000:full-duplex:239.194.17.2:70M:20:120 Ejemplo2: {script_name} /opt/opengnsys/images/image1.img 9000:full-duplex:239.194.17.2:70M:20:120 + Ejemplo3: {script_name} ou_subdir/image1.img 9000:full-duplex:239.194.17.2:70M:20:120 + Ejemplo4: {script_name} /ou_subdir/image1.img 9000:full-duplex:239.194.17.2:70M:20:120 + Ejemplo5: {script_name} /opt/opengnsys/images/ou_subdir/image1.img 9000:full-duplex:239.194.17.2:70M:20:120 """ print(help_text) @@ -84,6 +93,11 @@ def build_file_path(): (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). """ param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "repo_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(repo_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: if not param_path.startswith(repo_path): file_path = os.path.join(repo_path, param_path) else: @@ -107,6 +121,7 @@ def get_repo_iface(): sys.exit(4) + # -------------------------------------------------------------------------------------------- # MAIN # -------------------------------------------------------------------------------------------- @@ -141,11 +156,8 @@ def main(): repo_iface = get_repo_iface() # Creamos una lista con el comando a enviar (esto es requerido por la función "subprocess.run"). - # NOTA: Se desabilita el uso de mbuffer, ya que esta versión del upd-sender no la admite (ya estaba así en el script original). - mbuffer = "" # which mbuffer &> /dev/null && MBUFFER="--pipe 'mbuffer -m 20M'" splitted_cmd = [ os.path.join(bin_path, 'udp-sender'), - mbuffer, '--nokbd', '--retries-until-drop', '65', '--portbase', port, @@ -157,6 +169,7 @@ def main(): '--ttl', '16', '--min-clients', nclients, '--max-wait', maxtime, + '--autostart', '20', # Esto hace que empiece el envío automáticamente a los 20 segundos (desde ogLive le envían "120", pero con ese valor da timeout) '--file', file_path ] diff --git a/py_scripts/sendFileUFTP.py b/py_scripts/sendFileUFTP.py index 7fe1906..b501ceb 100644 --- a/py_scripts/sendFileUFTP.py +++ b/py_scripts/sendFileUFTP.py @@ -10,9 +10,12 @@ Paquetes APT requeridos: "uftp" Parámetros ------------ -sys.argv[1] - Nombre completo de la imagen a enviar (con o sin ruta) +sys.argv[1] - Nombre completo de la imagen a enviar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. - Ejemplo1: image1.img - Ejemplo2: /opt/opengnsys/images/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/images/ou_subdir/image1.img sys.argv[2] - Parámetros Multicast/Unicast (en formato "Port:IP:Bitrate") - Ejemplo1: 9000:239.194.17.2:100M @@ -20,12 +23,15 @@ sys.argv[2] - Parámetros Multicast/Unicast (en formato "Port:IP:Bitrate") Sintaxis ---------- -./sendFileUFTP.py image_name|/image_path/image_name Port:IP:Bitrate +./sendFileUFTP.py [ou_subdir/]image_name|/image_path/image_name Port:IP:Bitrate Ejemplos --------- ./sendFileUFTP.py image1.img 9000:239.194.17.2:100M -./sendFileUFTP.py /opt/opengnsys/images/image1.img 9000:192.168.56.101:1G +./sendFileUFTP.py /opt/opengnsys/images/image1.img 9000:239.194.17.2:100M +./sendFileUFTP.py ou_subdir/image1.img 9000:192.168.56.101:1G +./sendFileUFTP.py /ou_subdir/image1.img 9000:192.168.56.101:1G +./sendFileUFTP.py /opt/opengnsys/images/ou_subdir/image1.img 9000:192.168.56.101:1G """ # -------------------------------------------------------------------------------------------- @@ -55,9 +61,12 @@ def show_help(): """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". """ help_text = f""" - Sintaxis: {script_name} image_name|/image_path/image_name Port:IP:Bitrate + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name Port:IP:Bitrate Ejemplo1: {script_name} image1.img 9000:239.194.17.2:100M - Ejemplo2: {script_name} /opt/opengnsys/images/image1.img 9000:192.168.56.101:1G + Ejemplo2: {script_name} /opt/opengnsys/images/image1.img 9000:239.194.17.2:100M + Ejemplo3: {script_name} ou_subdir/image1.img 9000:192.168.56.101:1G + Ejemplo4: {script_name} /ou_subdir/image1.img 9000:192.168.56.101:1G + Ejemplo5: {script_name} /opt/opengnsys/images/ou_subdir/image1.img 9000:192.168.56.101:1G """ print(help_text) @@ -87,6 +96,11 @@ def build_file_path(): (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). """ param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "repo_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(repo_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: if not param_path.startswith(repo_path): file_path = os.path.join(repo_path, param_path) else: -- 2.40.1 From b50459efd8e46dec3acbd9fe4fa03719d1e61c36 Mon Sep 17 00:00:00 2001 From: ggil Date: Fri, 6 Sep 2024 14:07:32 +0200 Subject: [PATCH 25/70] refs #631 - Modify README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 43c6f4e..9675e10 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará 5. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/delete-image` 6. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/images/recover-image` 7. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/images/import-image` -8. [Crear archivos ".sum", ".full.sum", ".size" y ".torrent"](#crear-archivos-sum-fullsum-size-y-torrent) - `POST /ogrepository/v1/images/create-torrentsum` +8. [Crear archivos sum, full.sum, size y torrent](#crear-archivos-sum-fullsum-size-y-torrent) - `POST /ogrepository/v1/images/create-torrentsum` 9. [Enviar paquete Wake On Lan](#enviar-paquete-wake-on-lan) - `POST /ogrepository/v1/images/send-wol` 10. [Iniciar el Tracker P2P](#iniciar-el-tracker-p2p) - `POST /ogrepository/v1/images/run-tracker` 11. [Iniciar el Seeder P2P](#iniciar-el-seeder-p2p) - `POST /ogrepository/v1/images/run-seeder` @@ -316,7 +316,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **Código 200 OK:** La imagen se ha importado exitosamente. --- -### Crear archivos ".sum", ".full.sum", ".size" y ".torrent" +### Crear archivos sum, full.sum, size y torrent Se crearán los archivos ".sum", ".full.sum", ".size" y ".torrent", para la imagen especificada como parámetro. Se puede hacer con el script "**createTorrentSum.py**", que hemos programado recientemente. -- 2.40.1 From aff7ec138cdbf82bc4a4132d1b01a6ef365669fa Mon Sep 17 00:00:00 2001 From: ggil Date: Fri, 6 Sep 2024 14:12:51 +0200 Subject: [PATCH 26/70] refs #631 - Modify 'README.md' --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9675e10..cc10a19 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará 5. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/delete-image` 6. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/images/recover-image` 7. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/images/import-image` -8. [Crear archivos sum, full.sum, size y torrent](#crear-archivos-sum-fullsum-size-y-torrent) - `POST /ogrepository/v1/images/create-torrentsum` +8. [Crear archivos auxiliares](#crear-archivos-auxiliares) - `POST /ogrepository/v1/images/create-torrentsum` 9. [Enviar paquete Wake On Lan](#enviar-paquete-wake-on-lan) - `POST /ogrepository/v1/images/send-wol` 10. [Iniciar el Tracker P2P](#iniciar-el-tracker-p2p) - `POST /ogrepository/v1/images/run-tracker` 11. [Iniciar el Seeder P2P](#iniciar-el-seeder-p2p) - `POST /ogrepository/v1/images/run-seeder` @@ -316,7 +316,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **Código 200 OK:** La imagen se ha importado exitosamente. --- -### Crear archivos sum, full.sum, size y torrent +### Crear archivos auxiliares Se crearán los archivos ".sum", ".full.sum", ".size" y ".torrent", para la imagen especificada como parámetro. Se puede hacer con el script "**createTorrentSum.py**", que hemos programado recientemente. -- 2.40.1 From f599e9d6fa0f1a70656ce42ac81a5563af33d3c4 Mon Sep 17 00:00:00 2001 From: ggil Date: Tue, 10 Sep 2024 13:06:00 +0200 Subject: [PATCH 27/70] refs #631 - Modify 'sendFileMcast.py' --- py_scripts/sendFileMcast.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/py_scripts/sendFileMcast.py b/py_scripts/sendFileMcast.py index ad3683b..1eeccda 100644 --- a/py_scripts/sendFileMcast.py +++ b/py_scripts/sendFileMcast.py @@ -47,6 +47,7 @@ script_name = os.path.basename(__file__) repo_path = '/opt/opengnsys/images/' bin_path = '/opt/opengnsys/bin/' repo_iface_script = '/opt/opengnsys/py_scripts/getRepoIface.py' +log_file = '/opt/opengnsys/log/udpcast.log' # -------------------------------------------------------------------------------------------- @@ -168,9 +169,10 @@ def main(): '--max-bitrate', bitrate, '--ttl', '16', '--min-clients', nclients, - '--max-wait', maxtime, - '--autostart', '20', # Esto hace que empiece el envío automáticamente a los 20 segundos (desde ogLive le envían "120", pero con ese valor da timeout) - '--file', file_path + '--max-wait', maxtime, # Esto hace que espere 120 segundos desde la conexión del primer cliente para empezar la transmisión. + #'--autostart', '20', # Esto hace que empiece la transmisión automáticamente después de enviar 20 paquetes "hello". + '--file', file_path, + '--log', log_file ] # Imprimimos el comando con espacios (como realmente se enviará): -- 2.40.1 From 98534c09a3d25d62461b15b4aa5dedd491ab3b94 Mon Sep 17 00:00:00 2001 From: ggil Date: Thu, 12 Sep 2024 16:47:26 +0200 Subject: [PATCH 28/70] refs #631 - Add 'stopUDPcast.py' --- README.md | 47 ++++++++--- py_scripts/stopUDPcast.py | 172 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 207 insertions(+), 12 deletions(-) create mode 100644 py_scripts/stopUDPcast.py diff --git a/README.md b/README.md index cc10a19..1ecc3fe 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ La API de ogRepository proporciona una interfaz para facilitar la administració El presente documento detalla los endpoints de la API, con sus respectivos parámetros de entrada, así como las acciones que llevan a cabo. --- -### Tabla de Contenido: +### Tabla de Contenido: 1. [Obtener Información de todas las Imágenes](#obtener-información-de-todas-las-imágenes) - `GET /ogrepository/v1/images/get-info` 2. [Obtener Información de una Imagen concreta](#obtener-información-de-una-imagen-concreta) - `GET /ogrepository/v1/images/get-info` @@ -33,8 +33,9 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará 11. [Iniciar el Seeder P2P](#iniciar-el-seeder-p2p) - `POST /ogrepository/v1/images/run-seeder` 12. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/images/send-udpcast` 13. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/images/send-uftp` -14. [Ver Estado de Transmisiones Multicast-P2P](#ver-estado-de-transmisiones-multicast-p2p) - -15. [Cancelar Transmisión Multicast-P2P](#cancelar-transmisión-multicast-p2p) - +14. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `POST /ogrepository/v1/images/stop-udpcast` +15. [Cancelar Transmisión P2P](#cancelar-transmisión-p2p) - +16. [Ver Estado de Transmisiones Multicast-P2P](#ver-estado-de-transmisiones-multicast-p2p) - --- ### Obtener Información de todas las Imágenes @@ -456,19 +457,41 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. - **Código 200 OK:** La imagen se ha enviado exitosamente. +--- +### Cancelar Transmisión UDPcast + +Se cancelará la transmisión por UDPcast existente de la imagen especificada como parámetro, deteniendo el proceso "udp-sender" asociado a dicha imagen. +Se puede hacer con el script "**stopUDPcast.py**", que hemos programado recientemente. + +**URL:** `/ogrepository/v1/images/stop-udpcast` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/stop-udpcast +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al cancelar la transmisión. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La transmisión se ha cancelado exitosamente. + +--- +### Cancelar Transmisión P2P + +Se cancelarán todas las transmisiones P2P activas en el ogRepository al que se envíe la orden, deteniendo los procesos "bttrack" y "btlaunchmany.bittornado". +Se debe programar un script para realizar esta tarea, ya que actualmente no hay ninguno. +**NOTA**: No he encontrado la forma de detener la transmisión de una imagen concreta, ya que "bttrack" hace tracking de todos los torrrents, y "btlaunchmany.bittornado" hace seed de todos los torrents existentes en la raíz del directorio especificado. + --- ### Ver Estado de Transmisiones Multicast-P2P Se devolverá información del estado de las transmisiones existentes, con un identificador de cada sesión multicast o P2P, y la imagen asociada. Se debe estudiar como realizar esta tarea para cada uno de los protocolos de transmisión, ya que cada uno tiene sus particularidades, y habitualmente no tienen comandos asociados para comprobar el estado de las transmisiones. -Y tampoco está claro qué protocolo se utilizará para transimisiones Multicast (¿"UDPcast", "UFTP", o ambos?), ni qué programas se utilizarán para P2P (¿"ctorrent/bttrack" u "opentracker/Transmission"?). -**NOTA**: Posiblemente deba crearse un endpoint específico para cada uno de los protocolos que se utilicen. - ---- -### Cancelar Transmisión Multicast-P2P - -Se cancelará la transmisión Multicast o P2P cuyo identificador se especifique como parámetro. -Aunque cancelar una transmisión Multicast o P2P es una tarea sencilla (independientemente del protocolo o programa que se utilice), en principio deberá crearse un script para cada uno de ellos. -Y la definición del endpoint depende de como se defina el endpoint anterior ("**Ver Estado de Transmisiones Multicast-P2P**"), ya que será el que determine cómo se especifica el identificador de la transmisión. +**NOTA**: Seguramente deba crearse un endpoint específico para cada uno de los protocolos que se utilicen. --- \ No newline at end of file diff --git a/py_scripts/stopUDPcast.py b/py_scripts/stopUDPcast.py new file mode 100644 index 0000000..f972925 --- /dev/null +++ b/py_scripts/stopUDPcast.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +Este script finaliza el proceso "udp-sender" asociado a la imagen que recibe como parámetro, + lo que en la práctica hará que se cancele la transmisión existente de dicha imagen mediante UDPcast. + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a cancelar su transmisión (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/images/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/images/ou_subdir/image1.img + + Sintaxis +---------- +./stopUDPcast.py [ou_subdir/]image_name|/image_path/image_name + + Ejemplos + --------- +./stopUDPcast.py image1.img +./stopUDPcast.py /opt/opengnsys/images/image1.img +./stopUDPcast.py ou_subdir/image1.img +./stopUDPcast.py /ou_subdir/image1.img +./stopUDPcast.py /opt/opengnsys/images/ou_subdir/image1.img +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/images/' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name + Ejemplo1: {script_name} image1.img + Ejemplo2: {script_name} /opt/opengnsys/images/image1.img + Ejemplo3: {script_name} ou_subdir/image1.img + Ejemplo4: {script_name} /ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/images/ou_subdir/image1.img + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 1 parámetro, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 2: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 parámetro") + show_help() + sys.exit(1) + + +def build_file_path(): + """ Construye la ruta completa del archivo a chequear + (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "repo_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(repo_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(repo_path): + file_path = os.path.join(repo_path, param_path) + else: + file_path = param_path + return file_path + + +def get_process_pid(file_path): + """ Busca un proceso que contenga "udp-sender" y la ruta de la imagen que recibe como parámetro, + y si lo encuentra almacena y retorna su pid asociado. + Si no encuentra ningún proceso que cumpla las condiciones (o si se produce una excepción) sale del script. + """ + try: + # Obtenemos todos los procesos, y almacenamos la salida y los errores: + result = subprocess.Popen(['ps', '-aux'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + out, error = result.communicate() + + # Almacenamos en una lista los procesos que contengan "udp-sender" y la imagen especificada como parámetro: + filtered_lines = [line for line in out.split('\n') if 'udp-sender' in line and file_path in line] + # Si hemos encontrado un proceso retornamos su pid, y si no imprimimos un mensaje de error y salimos del script: + if filtered_lines != []: + pid = filtered_lines[0].split()[1] + return pid + else: + print("udp-sender process not found") + sys.exit(3) + # Si se ha producido una excepción, imprimimos el error y salimos del script: + except Exception as error_description: + print(f"Unexpected error: {error_description}") + sys.exit(4) + + +def kill_udp_sender(pid): + """ Finaliza el proceso asociado al pid que recibe como parámetro, e imprime el return code. + Si se produce una excepción, imprime el error y sale del script. + """ + try: + # Finalizamos el proceso asociado al pid especificado como parámetro, e imprimimos el return code: + result = subprocess.run(f"kill {pid}".split(), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"ReturnCode: {result.returncode}") + # Si se ha producido una excepción, imprimimos el error y salimos del script: + except Exception as error_description: + print(f"Unexpected error: {error_description}") + sys.exit(5) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo de imagen: + file_path = build_file_path() + + # Si no existe el archivo de imagen, imprimimos un mensaje de error y salimos del script: + if not os.path.exists(file_path): + print("Image file doesn't exist") + sys.exit(2) + + # Obtenemos el pid del proceso "udp-sender" asociado a la imagen especificada: + pid = get_process_pid(file_path) + + # Finalizamos el proceso "udp-sender" encontrado: + kill_udp_sender(pid) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- -- 2.40.1 From 94bfef7af39b8f8aad9f515c92ba8c512cd17c49 Mon Sep 17 00:00:00 2001 From: ggil Date: Fri, 13 Sep 2024 13:31:04 +0200 Subject: [PATCH 29/70] refs #631 - Add 'stopP2P.py' --- README.md | 31 ++++++++++----- py_scripts/runTorrentSeeder.py | 4 +- py_scripts/stopP2P.py | 70 ++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 12 deletions(-) create mode 100644 py_scripts/stopP2P.py diff --git a/README.md b/README.md index 1ecc3fe..d8af825 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará 12. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/images/send-udpcast` 13. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/images/send-uftp` 14. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `POST /ogrepository/v1/images/stop-udpcast` -15. [Cancelar Transmisión P2P](#cancelar-transmisión-p2p) - +15. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `POST /ogrepository/v1/images/stop-p2p` 16. [Ver Estado de Transmisiones Multicast-P2P](#ver-estado-de-transmisiones-multicast-p2p) - --- @@ -420,7 +420,7 @@ Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binar - **nclients**: Número mínimo de clientes. - **maxtime**: Tiempo máximo de espera. -**Ejemplo de Solicitud:** +**Ejemplo de Solicitud:** ```bash curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none", "port":"9000", "method":"full", "ip":"239.194.17.2", "bitrate":"70M", "nclients":"20", "maxtime":"120"}' http://example.com/ogrepository/v1/images/send-udpcast @@ -460,7 +460,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d --- ### Cancelar Transmisión UDPcast -Se cancelará la transmisión por UDPcast existente de la imagen especificada como parámetro, deteniendo el proceso "udp-sender" asociado a dicha imagen. +Se cancelará la transmisión por UDPcast activa de la imagen especificada como parámetro, deteniendo el proceso "udp-sender" asociado a dicha imagen. Se puede hacer con el script "**stopUDPcast.py**", que hemos programado recientemente. **URL:** `/ogrepository/v1/images/stop-udpcast` @@ -481,17 +481,28 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **Código 200 OK:** La transmisión se ha cancelado exitosamente. --- -### Cancelar Transmisión P2P +### Cancelar Transmisiones P2P -Se cancelarán todas las transmisiones P2P activas en el ogRepository al que se envíe la orden, deteniendo los procesos "bttrack" y "btlaunchmany.bittornado". -Se debe programar un script para realizar esta tarea, ya que actualmente no hay ninguno. -**NOTA**: No he encontrado la forma de detener la transmisión de una imagen concreta, ya que "bttrack" hace tracking de todos los torrrents, y "btlaunchmany.bittornado" hace seed de todos los torrents existentes en la raíz del directorio especificado. +Se cancelarán las transmisiones P2P activas en el ogRepository al que se envíe la orden, deteniendo los procesos "bttrack" y "btlaunchmany.bittornado". +Se puede hacer con el script "**stopP2P.py**", que hemos programado recientemente. +**NOTA**: No he encontrado la forma de detener la transmisión de una imagen concreta, ya que "bttrack" hace tracking de todos los torrrents, y "btlaunchmany.bittornado" hace seed de todos los torrents existentes en la raíz del directorio especificado (aparte, no bastaría con finalizar el seeder, porque los clientes también hacen seed). + +**URL:** `/ogrepository/v1/images/stop-p2p` +**Método HTTP:** POST + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/stop-p2p +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al cancelar las transmisiones. +- **Código 200 OK:** Las transmisiones se han cancelado exitosamente. --- -### Ver Estado de Transmisiones Multicast-P2P +### Ver Estado de Transmisiones Multicast-P2P -Se devolverá información del estado de las transmisiones existentes, con un identificador de cada sesión multicast o P2P, y la imagen asociada. -Se debe estudiar como realizar esta tarea para cada uno de los protocolos de transmisión, ya que cada uno tiene sus particularidades, y habitualmente no tienen comandos asociados para comprobar el estado de las transmisiones. +Se debe estudiar como realizar esta tarea para cada uno de los protocolos de transmisión, ya que cada uno tiene sus particularidades, y habitualmente no tienen comandos asociados para comprobar el estado de las transmisiones. Es posible que sea neceario parsear los logs de ogRepo y/o de los clientes. **NOTA**: Seguramente deba crearse un endpoint específico para cada uno de los protocolos que se utilicen. --- \ No newline at end of file diff --git a/py_scripts/runTorrentSeeder.py b/py_scripts/runTorrentSeeder.py index ad9b18c..2ebd64c 100644 --- a/py_scripts/runTorrentSeeder.py +++ b/py_scripts/runTorrentSeeder.py @@ -107,9 +107,9 @@ def main(): # Finalizamos el proceso "btlaunchmany.bittornado" (en caso de que estuviera corriendo): try: - subprocess.run(f"pkill btlaunchmany.bittornado".split(), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + subprocess.run(f"pkill btlaunchmany".split(), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except Exception as error_description: - print(f"No bittornado process running? Returned error: {error_description}") + print(f"No btlaunchmany.bittornado process running? Returned error: {error_description}") # Construimos la ruta en la que buscar los torrents, en base al parámetro especificado: if sys.argv[1] == 'none': diff --git a/py_scripts/stopP2P.py b/py_scripts/stopP2P.py new file mode 100644 index 0000000..9905742 --- /dev/null +++ b/py_scripts/stopP2P.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script finaliza el proceso "btlaunchmany.bittornado" (correspondiente al seeder P2P) y el proceso "bttrack" (correspondiente al tracker P2P), + lo que en la práctica hará que se cancelen las transmisiones P2P activas en el momento de ejecutarlo. + +No he encontrado la forma de cancelar una transferencia P2P concreta, ya que "bttrack" hace tracking de todos los torrents de ogRepo, + y es el proceso que es necesario finalizar para cancelar las transferencias P2P (con el seeder no bastaría, porque los clientes también hacen seed). + +No recibe ningún parámetro. +""" +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import subprocess + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def kill_seeder(): + """ Finaliza cualquier proceso activo de "btlaunchmany.bittornado", + que corresponde al seeder P2P. + """ + try: + subprocess.run(f"pkill btlaunchmany".split(), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print("Process btlaunchmany.bittornado finalized") + except Exception as error_description: + print(f"No btlaunchmany.bittornado process running? Returned error: {error_description}") + + + +def kill_tracker(): + """ Finaliza cualquier proceso activo de "bttrack", + que corresponde al tracker P2P. + """ + try: + subprocess.run(f"pkill bttrack".split(), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print("Process bttrack finalized") + except Exception as error_description: + print(f"No bttrack process running? Returned error: {error_description}") + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Finalizamos cualquier proceso activo de "btlaunchmany.bittornado" (seeder): + kill_seeder() + + # Finalizamos cualquier proceso activo de "bttrack" (tracker): + kill_tracker() + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- -- 2.40.1 From 2ea31a622cd5366bb8459c55e5053fbebfa4d560 Mon Sep 17 00:00:00 2001 From: ggil Date: Tue, 1 Oct 2024 13:12:02 +0200 Subject: [PATCH 30/70] refs #794 - Modify API Proposal and Change Script Path (including script modify) --- README.md | 195 +++++++++++------------ bin/{ => OLD}/checkrepo | 0 bin/{ => OLD}/createfileimage | 0 bin/{ => OLD}/deleteimage | 0 bin/{ => OLD}/deletepreimage | 0 bin/{ => OLD}/getRepoIface | 0 bin/{ => OLD}/importimage | 0 bin/{ => OLD}/mountimage | 0 bin/{ => OLD}/partclone2sync | 0 bin/{ => OLD}/reduceimage | 0 bin/{ => OLD}/sendFileMcast | 0 bin/{ => OLD}/torrent-creator | 0 bin/{ => OLD}/torrent-tracker | 0 bin/{ => OLD}/torrent-tracker_OLD | 0 bin/{ => OLD}/unmountimage | 0 {py_scripts => bin}/checkImage.py | 0 {py_scripts => bin}/createTorrentSum.py | 2 +- {py_scripts => bin}/deleteImage.py | 2 +- {py_scripts => bin}/getRepoIface.py | 0 {py_scripts => bin}/getRepoInfo.py | 0 {py_scripts => bin}/importImage.py | 2 +- {py_scripts => bin}/recoverImage.py | 2 +- {py_scripts => bin}/runTorrentSeeder.py | 0 {py_scripts => bin}/runTorrentTracker.py | 0 {py_scripts => bin}/sendFileMcast.py | 2 +- {py_scripts => bin}/sendFileUFTP.py | 2 +- {py_scripts => bin}/sendWakeOnLan.py | 0 {py_scripts => bin}/stopP2P.py | 0 {py_scripts => bin}/stopUDPcast.py | 0 {py_scripts => bin}/updateRepoInfo.py | 2 +- {py_scripts => bin}/updateTrashInfo.py | 0 31 files changed, 98 insertions(+), 111 deletions(-) rename bin/{ => OLD}/checkrepo (100%) rename bin/{ => OLD}/createfileimage (100%) rename bin/{ => OLD}/deleteimage (100%) rename bin/{ => OLD}/deletepreimage (100%) rename bin/{ => OLD}/getRepoIface (100%) rename bin/{ => OLD}/importimage (100%) rename bin/{ => OLD}/mountimage (100%) rename bin/{ => OLD}/partclone2sync (100%) rename bin/{ => OLD}/reduceimage (100%) rename bin/{ => OLD}/sendFileMcast (100%) rename bin/{ => OLD}/torrent-creator (100%) rename bin/{ => OLD}/torrent-tracker (100%) rename bin/{ => OLD}/torrent-tracker_OLD (100%) rename bin/{ => OLD}/unmountimage (100%) rename {py_scripts => bin}/checkImage.py (100%) rename {py_scripts => bin}/createTorrentSum.py (99%) rename {py_scripts => bin}/deleteImage.py (99%) rename {py_scripts => bin}/getRepoIface.py (100%) rename {py_scripts => bin}/getRepoInfo.py (100%) rename {py_scripts => bin}/importImage.py (99%) rename {py_scripts => bin}/recoverImage.py (99%) rename {py_scripts => bin}/runTorrentSeeder.py (100%) rename {py_scripts => bin}/runTorrentTracker.py (100%) rename {py_scripts => bin}/sendFileMcast.py (99%) rename {py_scripts => bin}/sendFileUFTP.py (99%) rename {py_scripts => bin}/sendWakeOnLan.py (100%) rename {py_scripts => bin}/stopP2P.py (100%) rename {py_scripts => bin}/stopUDPcast.py (100%) rename {py_scripts => bin}/updateRepoInfo.py (99%) rename {py_scripts => bin}/updateTrashInfo.py (100%) diff --git a/README.md b/README.md index d8af825..7d0e02e 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ ogRepository - OpenGnsys Repository Manager Este repositorio GIT contiene la estructura de datos del repositorio de imágenes de OpenGnsys. -- **admin** -------- Archivos de configuración del repositorio. -- **bin** ----------- Binarios y scripts de gestión del repositorio. -- **etc** ----------- Ficheros y plantillas de configuración del repositorio. -- **py_scripts** --- Scripts en Python 3, algunos de los cuales son traducciones de los scripts bash situados en "bin". +- **admin** --- Archivos de configuración del repositorio. +- **bin** ------ Scripts en Python 3 y binarios de gestión del repositorio. +- **etc** ------ Ficheros y plantillas de configuración del repositorio. + --- @@ -20,21 +20,21 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará --- ### Tabla de Contenido: -1. [Obtener Información de todas las Imágenes](#obtener-información-de-todas-las-imágenes) - `GET /ogrepository/v1/images/get-info` -2. [Obtener Información de una Imagen concreta](#obtener-información-de-una-imagen-concreta) - `GET /ogrepository/v1/images/get-info` +1. [Obtener Información de todas las Imágenes](#obtener-información-de-todas-las-imágenes) - `GET /ogrepository/v1/images` +2. [Obtener Información de una Imagen concreta](#obtener-información-de-una-imagen-concreta) - `GET /ogrepository/v1/images/{ID_img}` 3. [Actualizar Información del Repositorio](#actualizar-información-del-repositorio) - `PUT /ogrepository/v1/images` 4. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/images/check-image` -5. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/delete-image` -6. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/images/recover-image` -7. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/images/import-image` -8. [Crear archivos auxiliares](#crear-archivos-auxiliares) - `POST /ogrepository/v1/images/create-torrentsum` -9. [Enviar paquete Wake On Lan](#enviar-paquete-wake-on-lan) - `POST /ogrepository/v1/images/send-wol` -10. [Iniciar el Tracker P2P](#iniciar-el-tracker-p2p) - `POST /ogrepository/v1/images/run-tracker` -11. [Iniciar el Seeder P2P](#iniciar-el-seeder-p2p) - `POST /ogrepository/v1/images/run-seeder` -12. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/images/send-udpcast` -13. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/images/send-uftp` -14. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `POST /ogrepository/v1/images/stop-udpcast` -15. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `POST /ogrepository/v1/images/stop-p2p` +5. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/{ID_img}` +6. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/trash/images/{ID_img}` +7. [Eliminar una Imagen de la Papelera](#eliminar-una-imagen-de-la-papelera) - `DELETE /ogrepository/v1/trash/images/{ID_img}` +8. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/repo/{ID_repo}/images/{ID_img}` +9. [Crear archivos auxiliares](#crear-archivos-auxiliares) - `POST /ogrepository/v1/images/create-torrentsum` +10. [Enviar paquete Wake On Lan](#enviar-paquete-wake-on-lan) - `POST /ogrepository/v1/images/send-wol` +11. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/udpcast/images/{ID_img}` +12. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/uftp/images/{ID_img}` +13. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/p2p/images/{ID_img}` +14. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}` +15. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p` 16. [Ver Estado de Transmisiones Multicast-P2P](#ver-estado-de-transmisiones-multicast-p2p) - --- @@ -42,18 +42,15 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (que corresponde a todas las imágenes almacenadas en el repositorio), y en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (que corresponde a las imágenes que fueron eliminadas, que estarán en la papelera). Se puede utilizar el script "**getRepoInfo.py**, que hemos programado recientemente. +**NOTA**: La versión actual de este script requiere que se le pase "all" como primer parámetro (que correspondería al nombre de la imagen) y "none" como segundo parámetro (que corresponderia al nombre del subdirectorio correspondiente a la OU). Esta transformación de parámetros se puede realizar en el controlador PHP. -**URL:** `/ogrepository/v1/images/get-info` +**URL:** `/ogrepository/v1/images` **Método HTTP:** GET -**Cuerpo de la Solicitud (JSON):** -- **image**: Nombre de la imagen (con extensión), pero en este caso "all". -- **ou_subdir**: Subdirectorio correspondiente a la OU, pero en este caso "none". - **Ejemplo de Solicitud:** ```bash -curl -X GET -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"all","ou_subdir":"none"}' http://example.com/ogrepository/v1/images/get-info +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images ``` **Respuestas:** @@ -150,19 +147,16 @@ curl -X GET -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d ### Obtener Información de una Imagen concreta Se devolverá la informacion de la imagen especificada, que puede estar en el archivo "**/opt/opengnsys/etc/repoinfo.json**" o en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (en este último caso, si la imagen está en la papelera). -Se puede utilizar el script "**getRepoInfo.py**, que hemos programado recientemente. +Se puede utilizar el script "**getRepoInfo.py**, que hemos programado recientemente. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión) como primer parámetro, y el subdirectorio correspondiente a la OU (o "none" si no es el caso) como segundo parámetro. En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP. -**URL:** `/ogrepository/v1/images/get-info` +**URL:** `/ogrepository/v1/images/{ID_img}` **Método HTTP:** GET -**Cuerpo de la Solicitud (JSON):** -- **image**: Nombre de la imagen (con extensión). -- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). - **Ejemplo de Solicitud:** ```bash -curl -X GET -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img","ou_subdir":"none"}' http://example.com/ogrepository/v1/images/get-info +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/{ID_img} ``` **Respuestas:** @@ -205,7 +199,7 @@ curl -X GET -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se actualizará la información de las imágenes almacenadas en el repositorio, reflejándola en el archivo "**/opt/opengnsys/etc/repoinfo.json**". Se puede hacer con el script "**updateRepoInfo.py**", que hemos programado recientemente (y que es similar al script bash original "**checkrepo**"). -Este endpoint es llamado por el script "**deleteImage.py**" (para actualizar la información cada vez que se elimine una imagen), y creemos que también debe ser llamado por ogCore u ogLive cada vez que se haya creado una imagen. +Este endpoint es llamado por el script "**deleteImage.py**" (para actualizar la información cada vez que se elimine una imagen), y creemos que también debe ser llamado por ogCore u ogLive cada vez que se haya creado una imagen. **URL:** `/ogrepository/v1/images` **Método HTTP:** PUT @@ -249,19 +243,18 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se eliminará la imagen especificada como parámetro, pudiendo eliminarla permanentemente o enviarla a la papelera. Se puede hacer con el script "**deleteimage.py**", que hemos programado recientemente (y que incluye la funcionalidad "papelera"), y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y el parámetro opcional "-p" (para que la eliminación sea permanente). En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP, pero en principio habrá que especificar el método de eliminación en un json (en la URL). -**URL:** `/ogrepository/v1/images/delete-image` +**URL:** `/ogrepository/v1/images/{ID_img}` **Método HTTP:** DELETE **Cuerpo de la Solicitud (JSON):** -- **image**: Nombre de la imagen (con extensión). -- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). - **method**: Método de eliminación (puede ser "trash" o "permanent"). **Ejemplo de Solicitud:** ```bash -curl -X DELETE -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none", "method":"trash"}' http://example.com/ogrepository/v1/images/delete-image +curl -X DELETE -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"method":"trash"}' http://example.com/ogrepository/v1/images/{ID_img} ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al eliminar la imagen. @@ -273,43 +266,58 @@ curl -X DELETE -H "Authorization: $API_KEY" -H "Content-Type: application/json" Se recuperará la imagen especificada como parámetro, desde la papelera. Se puede hacer con el script "**recoverImage.py**", que hemos programado recientemente, y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP. -**URL:** `/ogrepository/v1/images/recover-image` +**URL:** `/ogrepository/v1/trash/images/{ID_img}` **Método HTTP:** POST -**Cuerpo de la Solicitud (JSON):** -- **image**: Nombre de la imagen (con extensión). -- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). - **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/recover-image +curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/trash/images/{ID_img} ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al recuperar la imagen. - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. - **Código 200 OK:** La imagen se recuperó exitosamente. +--- +### Eliminar una Imagen de la Papelera + +Se eliminará permanentemente la imagen especificada como parámetro, desde la papelera. +Se puede hacer con el script "**deleteimage.py**", que hemos programado recientemente, y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. +**NOTA**: La versión actual de este script requiere que se le pase la ruta y nombre de la imagen como primer parámetro, y el parámetro opcional "-p" (para que la eliminación sea permanente). En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP. + +**URL:** `/ogrepository/v1/trash/images/{ID_img}` +**Método HTTP:** DELETE + +**Ejemplo de Solicitud:** + +```bash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/trash/images/{ID_img} +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al eliminar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se eliminó exitosamente. + --- ### Importar una Imagen Se importará una imagen de un repositorio remoto al repositorio local. Se puede hacer con el script "**importImage.py**", que hemos programado recientemente. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP, pero en principio habrá que especificar el usuario remoto en un json (en la URL). -**URL:** `/ogrepository/v1/images/import-image` +**URL:** `/ogrepository/v1/repo/{ID_repo}/images/{ID_img}` **Método HTTP:** POST **Cuerpo de la Solicitud (JSON):** -- **image**: Nombre de la imagen (con extensión). -- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). -- **repo**: IP o hostname del repositorio remoto. - **user**: Usuario con el que acceder al repositorio remoto. **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none", "repo":"192.168.56.100", "user":"user_name"}' http://example.com/ogrepository/v1/images/import-image +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"user":"user_name"}' http://example.com/ogrepository/v1/repo/{ID_repo}/images/{ID_img} ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al importar la imagen. @@ -361,53 +369,14 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **Código 500 Internal Server Error:** Ocurrió un error al enviar el paquete Wake On Lan. - **Código 200 OK:** El paquete Wake On Lan se ha enviado exitosamente. ---- -### Iniciar el Tracker P2P - -Se iniciará el tracker "bttrack" (o se reiniciará, en el caso de que ya estuviera iniciado), para hacer tracking de los torrents almacenados en el directorio de imágenes de ogRepository. -Se puede hacer con el script "**runTorrentTracker.py**", que hemos programado recientemente. -**NOTA**: Actualmente esto se hace automáticamente, al iniciar ogRepopository (desde el script "**/etc/init.d/opengnsys**"), pero creemos que solo debe hacerse cuando se solicite una descarga P2P. - -**URL:** `/ogrepository/v1/images/run-tracker` -**Método HTTP:** POST - -**Ejemplo de Solicitud:** - -```bash -curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/run-tracker -``` -**Respuestas:** -- **Código 500 Internal Server Error:** Ocurrió un error al iniciar el tracker. -- **Código 200 OK:** El tracker se ha iniciado exitosamente. - ---- -### Iniciar el Seeder P2P - -Se iniciará el seeder "bittornado" (o se reiniciará, en el caso de que ya estuviera iniciado), para hacer seed de los torrents almacenados en la raíz del directorio de imágenes de ogRepository (o en el subidrectorio de OU que se especifique). -Se puede hacer con el script "**runTorrentSeeder.py**", que hemos programado recientemente. -**NOTA**: Actualmente esto se hace automáticamente, al iniciar ogRepopository (desde el script "**/etc/init.d/opengnsys**"), pero creemos que solo debe hacerse cuando se solicite una descarga P2P. - -**URL:** `/ogrepository/v1/images/run-seeder` -**Método HTTP:** POST - -**Cuerpo de la Solicitud (JSON):** -- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). - -**Ejemplo de Solicitud:** - -```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ou_subdir":"none"}' http://example.com/ogrepository/v1/images/run-seeder -``` -**Respuestas:** -- **Código 500 Internal Server Error:** Ocurrió un error al iniciar el seeder. -- **Código 200 OK:** El seeder se ha iniciado exitosamente. --- ### Enviar una Imagen mediante UDPcast -Se enviará una imagen por Multicast, mediante la aplicación UDPcast. -Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binario "**udp-sender**", que es quien realmente realiza el envío. +Se enviará la imagen especificada por Multicast, mediante la aplicación UDPcast. +Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binario "**udp-sender**", que es quien realmente realiza el envío. +**NOTA**: Los datos multicast deben extraerse de la configuracién del Aula/OU (incluido el puerto). -**URL:** `/ogrepository/v1/images/send-udpcast` +**URL:** `/ogrepository/v1/udpcast/images/{ID_img}` **Método HTTP:** POST **Cuerpo de la Solicitud (JSON):** @@ -423,7 +392,7 @@ Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binar **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none", "port":"9000", "method":"full", "ip":"239.194.17.2", "bitrate":"70M", "nclients":"20", "maxtime":"120"}' http://example.com/ogrepository/v1/images/send-udpcast +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none", "port":"9000", "method":"full", "ip":"239.194.17.2", "bitrate":"70M", "nclients":"20", "maxtime":"120"}' http://example.com/ogrepository/v1/udpcast/images/{ID_img} ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. @@ -433,11 +402,11 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d --- ### Enviar una Imagen mediante UFTP -Se enviará una imagen por Unicast o Multicast, mediante el protocolo "UFTP". +Se enviará la imagen especificada por Unicast o Multicast, mediante el protocolo "UFTP". Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente los clientes ogLive destino se pongan en escucha con un daemon "UFTPD" (ejecutando el script "**listenUFTPD.py**"). **NOTA**: Los envíos mediante "UFTP" funcionan al revés que los envíos mediante "UDPcast" (con este último, primero se debe ejecutar un comando en el servidor, y luego en los clientes). -**URL:** `/ogrepository/v1/images/send-uftp` +**URL:** `/ogrepository/v1/uftp/images/{ID_img}` **Método HTTP:** POST **Cuerpo de la Solicitud (JSON):** @@ -450,7 +419,28 @@ Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none", "port":"9000", "ip":"239.194.17.2", "bitrate":"1G"}' http://example.com/ogrepository/v1/images/send-uftp +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none", "port":"9000", "ip":"239.194.17.2", "bitrate":"1G"}' http://example.com/ogrepository/v1/uftp/images/{ID_img} +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se ha enviado exitosamente. + +--- +### Enviar una Imagen mediante P2P + +Se enviará la imagen especificada mediante "P2P", iniciando el tracker y el seeder (que harán tracking y seed de los torrents contenidos en la raiz del subdirectorio especificado). +Se debe llamar al script "**runTorrentTracker.py**" y al script "**runTorrentSeeder.py**", especificando la ruta en la que está contenido el torrent (que debe estar en la raiz de dicha ruta). +**NOTA**: El directorio en el que está contenida la imagen y su torrent asociado puede obtenerse a partir del ID de la imagen (que corresponde al fullsum). + + +**URL:** `/ogrepository/v1/p2p/images/{ID_img}` +**Método HTTP:** POST + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/p2p/images/{ID_img} ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. @@ -462,18 +452,15 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se cancelará la transmisión por UDPcast activa de la imagen especificada como parámetro, deteniendo el proceso "udp-sender" asociado a dicha imagen. Se puede hacer con el script "**stopUDPcast.py**", que hemos programado recientemente. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP. -**URL:** `/ogrepository/v1/images/stop-udpcast` -**Método HTTP:** POST - -**Cuerpo de la Solicitud (JSON):** -- **image**: Nombre de la imagen (con extensión). -- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). +**URL:** `/ogrepository/v1/udpcast/images/{ID_img}` +**Método HTTP:** DELETE **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/stop-udpcast +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/udpcast/images/{ID_img} ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al cancelar la transmisión. @@ -485,15 +472,15 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se cancelarán las transmisiones P2P activas en el ogRepository al que se envíe la orden, deteniendo los procesos "bttrack" y "btlaunchmany.bittornado". Se puede hacer con el script "**stopP2P.py**", que hemos programado recientemente. -**NOTA**: No he encontrado la forma de detener la transmisión de una imagen concreta, ya que "bttrack" hace tracking de todos los torrrents, y "btlaunchmany.bittornado" hace seed de todos los torrents existentes en la raíz del directorio especificado (aparte, no bastaría con finalizar el seeder, porque los clientes también hacen seed). +**NOTA**: No he encontrado la forma de detener la transmisión de una imagen concreta, ya que "bttrack" y "btlaunchmany.bittornado" hacen tracking y seed (respectivamente) de todos los torrents existentes en la raíz del directorio especificado. -**URL:** `/ogrepository/v1/images/stop-p2p` -**Método HTTP:** POST +**URL:** `/ogrepository/v1/p2p` +**Método HTTP:** DELETE **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/stop-p2p +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/p2p ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al cancelar las transmisiones. diff --git a/bin/checkrepo b/bin/OLD/checkrepo similarity index 100% rename from bin/checkrepo rename to bin/OLD/checkrepo diff --git a/bin/createfileimage b/bin/OLD/createfileimage similarity index 100% rename from bin/createfileimage rename to bin/OLD/createfileimage diff --git a/bin/deleteimage b/bin/OLD/deleteimage similarity index 100% rename from bin/deleteimage rename to bin/OLD/deleteimage diff --git a/bin/deletepreimage b/bin/OLD/deletepreimage similarity index 100% rename from bin/deletepreimage rename to bin/OLD/deletepreimage diff --git a/bin/getRepoIface b/bin/OLD/getRepoIface similarity index 100% rename from bin/getRepoIface rename to bin/OLD/getRepoIface diff --git a/bin/importimage b/bin/OLD/importimage similarity index 100% rename from bin/importimage rename to bin/OLD/importimage diff --git a/bin/mountimage b/bin/OLD/mountimage similarity index 100% rename from bin/mountimage rename to bin/OLD/mountimage diff --git a/bin/partclone2sync b/bin/OLD/partclone2sync similarity index 100% rename from bin/partclone2sync rename to bin/OLD/partclone2sync diff --git a/bin/reduceimage b/bin/OLD/reduceimage similarity index 100% rename from bin/reduceimage rename to bin/OLD/reduceimage diff --git a/bin/sendFileMcast b/bin/OLD/sendFileMcast similarity index 100% rename from bin/sendFileMcast rename to bin/OLD/sendFileMcast diff --git a/bin/torrent-creator b/bin/OLD/torrent-creator similarity index 100% rename from bin/torrent-creator rename to bin/OLD/torrent-creator diff --git a/bin/torrent-tracker b/bin/OLD/torrent-tracker similarity index 100% rename from bin/torrent-tracker rename to bin/OLD/torrent-tracker diff --git a/bin/torrent-tracker_OLD b/bin/OLD/torrent-tracker_OLD similarity index 100% rename from bin/torrent-tracker_OLD rename to bin/OLD/torrent-tracker_OLD diff --git a/bin/unmountimage b/bin/OLD/unmountimage similarity index 100% rename from bin/unmountimage rename to bin/OLD/unmountimage diff --git a/py_scripts/checkImage.py b/bin/checkImage.py similarity index 100% rename from py_scripts/checkImage.py rename to bin/checkImage.py diff --git a/py_scripts/createTorrentSum.py b/bin/createTorrentSum.py similarity index 99% rename from py_scripts/createTorrentSum.py rename to bin/createTorrentSum.py index b61f753..d4973a9 100644 --- a/py_scripts/createTorrentSum.py +++ b/bin/createTorrentSum.py @@ -47,7 +47,7 @@ import hashlib script_name = os.path.basename(__file__) repo_path = '/opt/opengnsys/images/' config_file = '/opt/opengnsys/etc/ogAdmRepo.cfg' -update_repo_script = '/opt/opengnsys/py_scripts/updateRepoInfo.py' +update_repo_script = '/opt/opengnsys/bin/updateRepoInfo.py' diff --git a/py_scripts/deleteImage.py b/bin/deleteImage.py similarity index 99% rename from py_scripts/deleteImage.py rename to bin/deleteImage.py index b630319..5b512b5 100644 --- a/py_scripts/deleteImage.py +++ b/bin/deleteImage.py @@ -51,7 +51,7 @@ import subprocess script_name = os.path.basename(__file__) repo_path = '/opt/opengnsys/images/' trash_path = '/opt/opengnsys/images_trash/' -update_repo_script = '/opt/opengnsys/py_scripts/updateRepoInfo.py' +update_repo_script = '/opt/opengnsys/bin/updateRepoInfo.py' # -------------------------------------------------------------------------------------------- diff --git a/py_scripts/getRepoIface.py b/bin/getRepoIface.py similarity index 100% rename from py_scripts/getRepoIface.py rename to bin/getRepoIface.py diff --git a/py_scripts/getRepoInfo.py b/bin/getRepoInfo.py similarity index 100% rename from py_scripts/getRepoInfo.py rename to bin/getRepoInfo.py diff --git a/py_scripts/importImage.py b/bin/importImage.py similarity index 99% rename from py_scripts/importImage.py rename to bin/importImage.py index 1df1b14..d93ee54 100644 --- a/py_scripts/importImage.py +++ b/bin/importImage.py @@ -58,7 +58,7 @@ import warnings script_name = os.path.basename(__file__) repo_path = '/opt/opengnsys/images/' -update_repo_script = '/opt/opengnsys/py_scripts/updateRepoInfo.py' +update_repo_script = '/opt/opengnsys/bin/updateRepoInfo.py' # -------------------------------------------------------------------------------------------- diff --git a/py_scripts/recoverImage.py b/bin/recoverImage.py similarity index 99% rename from py_scripts/recoverImage.py rename to bin/recoverImage.py index bc2f059..970401b 100644 --- a/py_scripts/recoverImage.py +++ b/bin/recoverImage.py @@ -45,7 +45,7 @@ import subprocess script_name = os.path.basename(__file__) repo_path = '/opt/opengnsys/images/' trash_path = '/opt/opengnsys/images_trash/' -update_repo_script = '/opt/opengnsys/py_scripts/updateRepoInfo.py' +update_repo_script = '/opt/opengnsys/bin/updateRepoInfo.py' # -------------------------------------------------------------------------------------------- diff --git a/py_scripts/runTorrentSeeder.py b/bin/runTorrentSeeder.py similarity index 100% rename from py_scripts/runTorrentSeeder.py rename to bin/runTorrentSeeder.py diff --git a/py_scripts/runTorrentTracker.py b/bin/runTorrentTracker.py similarity index 100% rename from py_scripts/runTorrentTracker.py rename to bin/runTorrentTracker.py diff --git a/py_scripts/sendFileMcast.py b/bin/sendFileMcast.py similarity index 99% rename from py_scripts/sendFileMcast.py rename to bin/sendFileMcast.py index 1eeccda..ae7d1eb 100644 --- a/py_scripts/sendFileMcast.py +++ b/bin/sendFileMcast.py @@ -46,7 +46,7 @@ import subprocess script_name = os.path.basename(__file__) repo_path = '/opt/opengnsys/images/' bin_path = '/opt/opengnsys/bin/' -repo_iface_script = '/opt/opengnsys/py_scripts/getRepoIface.py' +repo_iface_script = '/opt/opengnsys/bin/getRepoIface.py' log_file = '/opt/opengnsys/log/udpcast.log' diff --git a/py_scripts/sendFileUFTP.py b/bin/sendFileUFTP.py similarity index 99% rename from py_scripts/sendFileUFTP.py rename to bin/sendFileUFTP.py index b501ceb..ef6e8bb 100644 --- a/py_scripts/sendFileUFTP.py +++ b/bin/sendFileUFTP.py @@ -49,7 +49,7 @@ import subprocess script_name = os.path.basename(__file__) repo_path = '/opt/opengnsys/images/' -log_file = '/opt/opengnsys/images/uftp.log' +log_file = '/opt/opengnsys/log/uftp.log' # -------------------------------------------------------------------------------------------- diff --git a/py_scripts/sendWakeOnLan.py b/bin/sendWakeOnLan.py similarity index 100% rename from py_scripts/sendWakeOnLan.py rename to bin/sendWakeOnLan.py diff --git a/py_scripts/stopP2P.py b/bin/stopP2P.py similarity index 100% rename from py_scripts/stopP2P.py rename to bin/stopP2P.py diff --git a/py_scripts/stopUDPcast.py b/bin/stopUDPcast.py similarity index 100% rename from py_scripts/stopUDPcast.py rename to bin/stopUDPcast.py diff --git a/py_scripts/updateRepoInfo.py b/bin/updateRepoInfo.py similarity index 99% rename from py_scripts/updateRepoInfo.py rename to bin/updateRepoInfo.py index 2506de1..8beb3c6 100644 --- a/py_scripts/updateRepoInfo.py +++ b/bin/updateRepoInfo.py @@ -29,7 +29,7 @@ import shutil repo_path = '/opt/opengnsys/images' info_file = '/opt/opengnsys/etc/repoinfo.json' -update_trash_script = '/opt/opengnsys/py_scripts/updateTrashInfo.py' +update_trash_script = '/opt/opengnsys/bin/updateTrashInfo.py' # -------------------------------------------------------------------------------------------- diff --git a/py_scripts/updateTrashInfo.py b/bin/updateTrashInfo.py similarity index 100% rename from py_scripts/updateTrashInfo.py rename to bin/updateTrashInfo.py -- 2.40.1 From ec3ce93f8358d7ba51599013462e00ff2a447444 Mon Sep 17 00:00:00 2001 From: ggil Date: Tue, 1 Oct 2024 13:15:26 +0200 Subject: [PATCH 31/70] refs #631 - Modify 'runTorrentTracker.py' --- bin/runTorrentTracker.py | 63 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/bin/runTorrentTracker.py b/bin/runTorrentTracker.py index b898fd2..9a5d6c4 100644 --- a/bin/runTorrentTracker.py +++ b/bin/runTorrentTracker.py @@ -4,8 +4,23 @@ """ Este script inicia el tracker "bttrack" (o lo reinicia, si ya estuviera iniciado), finalizando previamente cualquier proceso activo, y borrando el archivo "/tmp/dstate". En principio, debería hacer lo mismo que el script bash original (cuyo nombre es "torrent-tracker"), que se ejecutaba por cron cada hora. +Creemos que debe ser llamado únicamente cuando se quiera hacer una descarga mediante P2P (junto al script "runTorrentSeeder.py"). +NOTA: El paquete no hace una búsqueda recursiva, por lo que se debe especificar el subdirectorio correspondiente a la OU, si es el caso. -No recibe ningún parámetro, y creemos que debe ser llamado únicamente cuando se quiera hacer una descarga mediante P2P (junto al script "runTorrentSeeder.py"). + Parámetros +------------ +sys.argv[1] - Subdirectorio correspondiente a la OU (o "none" si no es el caso). + - Ejemplo1: none + - Ejemplo2: ou_subdir + + Sintaxis +---------- +./runTorrentTracker.py none|ou_subdir + + Ejemplos + --------- +./runTorrentTracker.py none +./runTorrentTracker.py ou_subdir """ # -------------------------------------------------------------------------------------------- @@ -13,6 +28,7 @@ No recibe ningún parámetro, y creemos que debe ser llamado únicamente cuando # -------------------------------------------------------------------------------------------- import os +import sys import subprocess import time @@ -21,6 +37,7 @@ import time # VARIABLES # -------------------------------------------------------------------------------------------- +script_name = os.path.basename(__file__) repo_path = '/opt/opengnsys/images' bttrack_port = 6969 @@ -35,12 +52,41 @@ bttrack_allow_get = 0 # Este valor impide la descarga desde clientes no autoriza # -------------------------------------------------------------------------------------------- -def run_bttrack(): +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} none|ou_subdir + Ejemplo1: {script_name} none + Ejemplo2: {script_name} ou_subdir + """ + print(help_text) + + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 1 parámetro, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 2: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 parámetro") + show_help() + sys.exit(1) + + + +def run_bttrack(torrent_path): """ Ejecuta el comando "bttrack", con sus parámetros correspondientes. Además, captura el resultado y los posibles errores, y los imprime. """ # Creamos una lista con el comando "bttrack" y sus parámetros, y lo imprimimos con espacios: - splitted_cmd = f"bttrack --port {bttrack_port} --dfile {bttrack_dfile} --save_dfile_interval {bttrack_interval} --reannounce_interval {bttrack_interval} --logfile {bttrack_log} --allowed_dir {repo_path} --allow_get {bttrack_allow_get}".split() + splitted_cmd = f"bttrack --port {bttrack_port} --dfile {bttrack_dfile} --save_dfile_interval {bttrack_interval} --reannounce_interval {bttrack_interval} --logfile {bttrack_log} --allowed_dir {torrent_path} --allow_get {bttrack_allow_get}".split() print(f"Sending command: {' '.join(splitted_cmd)}") # Ejecutamos el comando "bttrack" en el sistema, e imprimimos el resultado: @@ -63,6 +109,9 @@ def run_bttrack(): def main(): """ """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + # Finalizamos el proceso "bttrack" (en caso de que estuviera corriendo): try: subprocess.run(f"pkill bttrack".split(), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -76,8 +125,14 @@ def main(): # Esperamos 2 segundos: time.sleep(2) + # Construimos la ruta en la que buscar los torrents, en base al parámetro especificado: + if sys.argv[1] == 'none': + torrent_path = repo_path + else: + torrent_path = f"{repo_path}/{sys.argv[1]}" + # Ejecutamos el comando "bttrack" (para hacer tracking de los torrents): - run_bttrack() + run_bttrack(torrent_path) -- 2.40.1 From 0c47a50ecf9d482b299d2205c5c3ecd1b9ca22d2 Mon Sep 17 00:00:00 2001 From: ggil Date: Tue, 1 Oct 2024 17:26:24 +0200 Subject: [PATCH 32/70] refs #631 - Add 'deleteTrashImage.py' --- README.md | 18 ++--- bin/deleteTrashImage.py | 164 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 11 deletions(-) create mode 100644 bin/deleteTrashImage.py diff --git a/README.md b/README.md index 7d0e02e..fe5670a 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará 1. [Obtener Información de todas las Imágenes](#obtener-información-de-todas-las-imágenes) - `GET /ogrepository/v1/images` 2. [Obtener Información de una Imagen concreta](#obtener-información-de-una-imagen-concreta) - `GET /ogrepository/v1/images/{ID_img}` 3. [Actualizar Información del Repositorio](#actualizar-información-del-repositorio) - `PUT /ogrepository/v1/images` -4. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/images/check-image` +4. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/status/images/{ID_img}` 5. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/{ID_img}` 6. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/trash/images/{ID_img}` 7. [Eliminar una Imagen de la Papelera](#eliminar-una-imagen-de-la-papelera) - `DELETE /ogrepository/v1/trash/images/{ID_img}` @@ -218,19 +218,15 @@ curl -X PUT -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag Se comprobará la integridad del fichero de imagen especificado como parámetro. Se puede hacer con el script "**checkImage.py**", que compara el tamaño actual del archivo con el almacenado en el archivo "**.size**", y el hash MD5 del último MB del archivo con el almacenado en el archivo "**.sum**". -**NOTA**: En lugar del archivo "**.sum**", se podría usar el archivo "**.full.sum**" (que contiene el hash MD5 de todo el archivo), pero en ese caso la comprobación tardaría un poco, dependiendo del tamaño de la imagen. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP. -**URL:** `/ogrepository/v1/images/check-image` +**URL:** `/ogrepository/v1/status/images/{ID_img}` **Método HTTP:** GET -**Cuerpo de la Solicitud (JSON):** -- **image**: Nombre de la imagen (con extensión). -- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). - **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/check-image +curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/status/images/{ID_img} ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al chequear la imagen. @@ -285,8 +281,8 @@ curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/tra ### Eliminar una Imagen de la Papelera Se eliminará permanentemente la imagen especificada como parámetro, desde la papelera. -Se puede hacer con el script "**deleteimage.py**", que hemos programado recientemente, y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. -**NOTA**: La versión actual de este script requiere que se le pase la ruta y nombre de la imagen como primer parámetro, y el parámetro opcional "-p" (para que la eliminación sea permanente). En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP. +Se puede hacer con el script "**deleteTrashImage.py**", que hemos programado recientemente, y que a su vez llama al script "**updateTrashInfo.py**", para actualizar la información de la papelera. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP. **URL:** `/ogrepository/v1/trash/images/{ID_img}` **Método HTTP:** DELETE @@ -306,7 +302,7 @@ curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/t Se importará una imagen de un repositorio remoto al repositorio local. Se puede hacer con el script "**importImage.py**", que hemos programado recientemente. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP, pero en principio habrá que especificar el usuario remoto en un json (en la URL). +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. En principio, ogCore puede acceder a estos datos a partir del ID del repositorio y del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP, pero en principio habrá que especificar el usuario remoto en un json (en la URL). **URL:** `/ogrepository/v1/repo/{ID_repo}/images/{ID_img}` **Método HTTP:** POST diff --git a/bin/deleteTrashImage.py b/bin/deleteTrashImage.py new file mode 100644 index 0000000..df18372 --- /dev/null +++ b/bin/deleteTrashImage.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script elimina permanentemente la imagen que recibe como parámetro (y todos sus archivos asociados), desde la papelera. +Llama al script "updateTrashInfo.py", para actualizar la información de las imágenes de la papelera. + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a eliminar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/images_trash/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/images_trash/ou_subdir/image1.img + + Sintaxis +---------- +./deleteTrashImage.py [ou_subdir/]image_name|/image_path/image_name + + Ejemplos + --------- +./deleteTrashImage.py image1.img +./deleteTrashImage.py /opt/opengnsys/images_trash/image1.img +./deleteTrashImage.py ou_subdir/image1.img +./deleteTrashImage.py /ou_subdir/image1.img +./deleteTrashImage.py /opt/opengnsys/images_trash/ou_subdir/image1.img +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +trash_path = '/opt/opengnsys/images_trash/' +update_trash_script = '/opt/opengnsys/bin/updateTrashInfo.py' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name + Ejemplo1: {script_name} image1.img + Ejemplo2: {script_name} /opt/opengnsys/images_trash/image1.img + Ejemplo3: {script_name} ou_subdir/image1.img + Ejemplo4: {script_name} /ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/images_trash/ou_subdir/image1.img + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 1 parámetro, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 2: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 parámetro") + show_help() + sys.exit(1) + + +def build_file_path(): + """ Construye la ruta completa al archivo a eliminar + (agregando "/opt/opengnsys/images_trash/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "trash_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(trash_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(trash_path): + file_path = os.path.join(trash_path, param_path) + else: + file_path = param_path + return file_path + + +def delete_image(file_path, extensions): + """ Elimina permanentemente la imagen que recibe en el parámetro "file_path", y todos sus archivos asociados. + """ + # Iteramos las extensiones de los archivos, y construimos la ruta completa de cada uno de ellos: + for ext in extensions: + file_to_remove = f"{file_path}{ext}" + # Si el archivo actual existe, lo eliminamos permanentemente: + if os.path.exists(file_to_remove): + os.remove(file_to_remove) + + +def update_trash_info(): + """ Actualiza la información de la papelera, ejecutando el script "updateTrashInfo.py". + Como se ve, es necesario que el script se ejecute como sudo, o dará error. + """ + try: + result = subprocess.run(['sudo', 'python3', update_trash_script], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except subprocess.CalledProcessError as error: + print(f"Error Output: {error.stderr.decode()}") + sys.exit(3) + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + sys.exit(4) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo a eliminar: + file_path = build_file_path() + + # Si no existe el archivo de imagen, imprimimos un mensaje de error y salimos del script: + if not os.path.exists(file_path): + print("Image file doesn't exist") + sys.exit(2) + + # Creamos una lista con las extensiones de los archivos asociados a la imagen + # (incluyendo ninguna extensión, que corresponde a la propia imagen): + extensions = ['', '.size', '.sum', '.full.sum', '.torrent', '.info', '.info.checked'] + + # Eliminamos la imagen y sus archivos asociados: + delete_image(file_path, extensions) + + # Actualizamos la información de la papelera, ejecutando el script "updateTrashInfo.py": + print("Updating Trash Info...") + update_trash_info() + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- -- 2.40.1 From 550c3947357b643473983aff77d50008ed642928 Mon Sep 17 00:00:00 2001 From: ggil Date: Wed, 2 Oct 2024 11:38:22 +0200 Subject: [PATCH 33/70] refs #631 - Modify 'README.md' --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fe5670a..23a17b1 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag ### Obtener Información de una Imagen concreta Se devolverá la informacion de la imagen especificada, que puede estar en el archivo "**/opt/opengnsys/etc/repoinfo.json**" o en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (en este último caso, si la imagen está en la papelera). -Se puede utilizar el script "**getRepoInfo.py**, que hemos programado recientemente. +Se puede utilizar el script "**getRepoInfo.py**, que hemos programado recientemente. **NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión) como primer parámetro, y el subdirectorio correspondiente a la OU (o "none" si no es el caso) como segundo parámetro. En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP. **URL:** `/ogrepository/v1/images/{ID_img}` @@ -238,7 +238,7 @@ curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/sta ### Eliminar una Imagen Se eliminará la imagen especificada como parámetro, pudiendo eliminarla permanentemente o enviarla a la papelera. -Se puede hacer con el script "**deleteimage.py**", que hemos programado recientemente (y que incluye la funcionalidad "papelera"), y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. +Se puede hacer con el script "**deleteimage.py**", que hemos programado recientemente (y que incluye la funcionalidad "papelera"), y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. **NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y el parámetro opcional "-p" (para que la eliminación sea permanente). En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP, pero en principio habrá que especificar el método de eliminación en un json (en la URL). **URL:** `/ogrepository/v1/images/{ID_img}` @@ -281,7 +281,7 @@ curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/tra ### Eliminar una Imagen de la Papelera Se eliminará permanentemente la imagen especificada como parámetro, desde la papelera. -Se puede hacer con el script "**deleteTrashImage.py**", que hemos programado recientemente, y que a su vez llama al script "**updateTrashInfo.py**", para actualizar la información de la papelera. +Se puede hacer con el script "**deleteTrashImage.py**", que hemos programado recientemente, y que a su vez llama al script "**updateTrashInfo.py**", para actualizar la información de la papelera. **NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP. **URL:** `/ogrepository/v1/trash/images/{ID_img}` -- 2.40.1 From 3d44dc9116b8acb6388721c76677635d6264686e Mon Sep 17 00:00:00 2001 From: ggil Date: Thu, 3 Oct 2024 12:11:03 +0200 Subject: [PATCH 34/70] refs #631 - Add 'getRepoStatus.py' and Modify API Proposal --- README.md | 83 ++++++++++++++++++++++------ bin/getRepoStatus.py | 126 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 16 deletions(-) create mode 100644 bin/getRepoStatus.py diff --git a/README.md b/README.md index 23a17b1..a10106a 100644 --- a/README.md +++ b/README.md @@ -20,23 +20,74 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará --- ### Tabla de Contenido: -1. [Obtener Información de todas las Imágenes](#obtener-información-de-todas-las-imágenes) - `GET /ogrepository/v1/images` -2. [Obtener Información de una Imagen concreta](#obtener-información-de-una-imagen-concreta) - `GET /ogrepository/v1/images/{ID_img}` -3. [Actualizar Información del Repositorio](#actualizar-información-del-repositorio) - `PUT /ogrepository/v1/images` -4. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/status/images/{ID_img}` -5. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/{ID_img}` -6. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/trash/images/{ID_img}` -7. [Eliminar una Imagen de la Papelera](#eliminar-una-imagen-de-la-papelera) - `DELETE /ogrepository/v1/trash/images/{ID_img}` -8. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/repo/{ID_repo}/images/{ID_img}` -9. [Crear archivos auxiliares](#crear-archivos-auxiliares) - `POST /ogrepository/v1/images/create-torrentsum` -10. [Enviar paquete Wake On Lan](#enviar-paquete-wake-on-lan) - `POST /ogrepository/v1/images/send-wol` -11. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/udpcast/images/{ID_img}` -12. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/uftp/images/{ID_img}` -13. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/p2p/images/{ID_img}` -14. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}` -15. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p` -16. [Ver Estado de Transmisiones Multicast-P2P](#ver-estado-de-transmisiones-multicast-p2p) - +1. [Obtener Información de Estado de ogRepository](#obtener-información-de-estado-de-ogrepository) - `GET /ogrepository/v1/status` +2. [Obtener Información de todas las Imágenes](#obtener-información-de-todas-las-imágenes) - `GET /ogrepository/v1/images` +3. [Obtener Información de una Imagen concreta](#obtener-información-de-una-imagen-concreta) - `GET /ogrepository/v1/images/{ID_img}` +4. [Actualizar Información del Repositorio](#actualizar-información-del-repositorio) - `PUT /ogrepository/v1/images` +5. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/status/images/{ID_img}` +6. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/{ID_img}` +7. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/trash/images/{ID_img}` +8. [Eliminar una Imagen de la Papelera](#eliminar-una-imagen-de-la-papelera) - `DELETE /ogrepository/v1/trash/images/{ID_img}` +9. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/repo/{ID_repo}/images/{ID_img}` +10. [Crear archivos auxiliares](#crear-archivos-auxiliares) - `POST /ogrepository/v1/images/create-torrentsum` +11. [Enviar paquete Wake On Lan](#enviar-paquete-wake-on-lan) - `POST /ogrepository/v1/images/send-wol` +12. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/udpcast/images/{ID_img}` +13. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/uftp/images/{ID_img}` +14. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/p2p/images/{ID_img}` +15. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}` +16. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p` +17. [Ver Estado de Transmisiones Multicast-P2P](#ver-estado-de-transmisiones-multicast-p2p) - +--- +### Obtener Información de Estado de ogRepository + +Se devolverá informacion de CPU, memoria RAM, disco duro y el estado de ciertos servicios y procesos de ogRepository, en formato JSON. +Se puede utilizar el script "**getRepoStatus.py**, que hemos programado recientemente. +**NOTA**: En los apartados "services" y "processes" he especificado los servicios y procesos que me han parecido interesantes, pero se puede añadir o eliminar los que se desee. + +**URL:** `/ogrepository/v1/status` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/status +``` + +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al consultar y/o devolver la información de estado. +- **Código 200 OK:** La información de estado se obtuvo exitosamente. + - **Contenido:** Información de estado en formato JSON. + ```json + { + "cpu": { + "used_percentage": "35%" + }, + "ram": { + "total": "7.8GB", + "used": "0.3GB", + "available": "7.2GB", + "used_percentage": "7%" + }, + "disk": { + "total": "11.7GB", + "used": "7.7GB", + "available": "3.4GB", + "used_percentage": "69%" + }, + "services": { + "ssh": "active", + "smbd": "active", + "rsync": "active" + }, + "processes": { + "udp-sender": "stopped", + "uftp": "stopped", + "bttrack": "stopped", + "btlaunchmany": "stopped" + } + } + ``` --- ### Obtener Información de todas las Imágenes diff --git a/bin/getRepoStatus.py b/bin/getRepoStatus.py new file mode 100644 index 0000000..ad46ea2 --- /dev/null +++ b/bin/getRepoStatus.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script devuelve información de CPU, memoria RAM, disco duro y el estado de ciertos servicios y procesos de ogRepository, en formato json. +No recibe ningún parámetro. + +Librerías Python requeridas: "psutil" (se puede instalar con "sudo apt install python3-psutil". o "pip install psutil") +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import psutil +import os +import json +import subprocess + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def get_cpu_info(): + """ Obtiene y retorna información de la CPU. + """ + cpu_percent = psutil.cpu_percent(interval=1) + return cpu_percent + + +def get_ram_info(): + """ Obtiene y retorna información de la memoria RAM. + """ + ram = psutil.virtual_memory() + return ram.total, ram.used, ram.available, ram.percent + + +def get_disk_info(): + """ Obtiene y retorna información del disco duro. + """ + disk = psutil.disk_usage('/') + return disk.total, disk.used, disk.free, disk.percent + + +def get_service_status(service): + """ Obtiene y retorna el estado del servicio que recibe como parámetro. + En caso de error, retorna un mensaje estándar. + """ + try: + result = subprocess.run(['systemctl', 'is-active', service], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + service_status = result.stdout.strip() + return service_status + except Exception: + return "status not accesible" + + +def get_process_status(process): + """ Obtiene y retorna el estado del proceso que recibe como parámetro. + """ + #for proc in psutil.process_iter(['pid', 'name', 'status']): + for proc in psutil.process_iter(['name']): + if proc.info['name'] == process: + return 'running' + return 'stopped' + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + +def main(): + """ + """ + # Obtenemos información de la CPU: + cpu_percent = get_cpu_info() + + # Obtenemos información de la memoria RAM: + total_ram, used_ram, available_ram, percent_ram = get_ram_info() + + # Obtenemos información del disco duro: + total_disk, used_disk, free_disk, percent_disk = get_disk_info() + + # Obtenemos el estado de los servicios listados, que almacenamos en un diccionario: + service_list = ['ssh', 'smbd', 'rsync'] + services_status = {service: get_service_status(service) for service in service_list} + + # Obtenemos el estado de los procesos listados, que almacenamos en un diccionario: + process_list = ['udp-sender', 'uftp', 'bttrack', 'btlaunchmany'] + process_status = {process: get_process_status(process) for process in process_list} + + # Creamos un diccionario con toda la información obtenida: + data_dict = { + 'cpu': { + 'used_percentage': f"{int(cpu_percent)}%" + }, + 'ram': { + 'total': f"{round(total_ram / (1024 ** 3), 1)}GB", + 'used': f"{round(used_ram / (1024 ** 3), 1)}GB", + 'available': f"{round(available_ram / (1024 ** 3), 1)}GB", + 'used_percentage': f"{int(percent_ram)}%" + }, + 'disk': { + 'total': f"{round(total_disk / (1024 ** 3), 1)}GB", + 'used': f"{round(used_disk / (1024 ** 3), 1)}GB", + 'available': f"{round(free_disk / (1024 ** 3), 1)}GB", + 'used_percentage': f"{int(percent_disk)}%" + }, + 'services': services_status, + 'processes': process_status + } + + # Convertimos el diccionario a JSON, y lo imprimimos: + json_data = json.dumps(data_dict, indent=4) + print(json_data) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- -- 2.40.1 From 73bf6b609836b7f17c8446886e23459da273e319 Mon Sep 17 00:00:00 2001 From: ggil Date: Thu, 3 Oct 2024 13:32:13 +0200 Subject: [PATCH 35/70] refs #794 - Modify API Proposal --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a10106a..9355cfa 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará 7. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/trash/images/{ID_img}` 8. [Eliminar una Imagen de la Papelera](#eliminar-una-imagen-de-la-papelera) - `DELETE /ogrepository/v1/trash/images/{ID_img}` 9. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/repo/{ID_repo}/images/{ID_img}` -10. [Crear archivos auxiliares](#crear-archivos-auxiliares) - `POST /ogrepository/v1/images/create-torrentsum` -11. [Enviar paquete Wake On Lan](#enviar-paquete-wake-on-lan) - `POST /ogrepository/v1/images/send-wol` +10. [Crear archivos auxiliares](#crear-archivos-auxiliares) - `POST /ogrepository/v1/images/torrentsum` +11. [Enviar paquete Wake On Lan](#enviar-paquete-wake-on-lan) - `POST /ogrepository/v1/wol` 12. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/udpcast/images/{ID_img}` 13. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/uftp/images/{ID_img}` 14. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/p2p/images/{ID_img}` @@ -377,7 +377,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se crearán los archivos ".sum", ".full.sum", ".size" y ".torrent", para la imagen especificada como parámetro. Se puede hacer con el script "**createTorrentSum.py**", que hemos programado recientemente. -**URL:** `/ogrepository/v1/images/create-torrentsum` +**URL:** `/ogrepository/v1/images/torrentsum` **Método HTTP:** POST **Cuerpo de la Solicitud (JSON):** @@ -387,7 +387,7 @@ Se puede hacer con el script "**createTorrentSum.py**", que hemos programado rec **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/create-torrentsum +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/torrentsum ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al crear los archivos. @@ -400,7 +400,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se enviará un paquete Wake On Lan a la dirección MAC especificada, a través de la IP de broadcast especificada. Se puede hacer con el script "**sendWakeOnLan.py**", que hemos programado recientemente. -**URL:** `/ogrepository/v1/images/send-wol` +**URL:** `/ogrepository/v1/wol` **Método HTTP:** POST **Cuerpo de la Solicitud (JSON):** @@ -410,7 +410,7 @@ Se puede hacer con el script "**sendWakeOnLan.py**", que hemos programado recien **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"broadcast_ip":"255.255.255.255", "mac":"00:19:99:5c:bb:bb"}' http://example.com/ogrepository/v1/images/send-wol +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"broadcast_ip":"255.255.255.255", "mac":"00:19:99:5c:bb:bb"}' http://example.com/ogrepository/v1/wol ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al enviar el paquete Wake On Lan. -- 2.40.1 From 5bab3357331d4c604624c4e3c04390d54800f04c Mon Sep 17 00:00:00 2001 From: ggil Date: Fri, 4 Oct 2024 14:03:57 +0200 Subject: [PATCH 36/70] refs #631 - Add 'getUDPcastInfo.py' and modify API proposal --- README.md | 67 ++++++++++++++++++++----------- bin/getUDPcastInfo.py | 91 +++++++++++++++++++++++++++++++++++++++++++ bin/sendFileMcast.py | 4 +- 3 files changed, 138 insertions(+), 24 deletions(-) create mode 100644 bin/getUDPcastInfo.py diff --git a/README.md b/README.md index 9355cfa..398525a 100644 --- a/README.md +++ b/README.md @@ -34,16 +34,16 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará 12. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/udpcast/images/{ID_img}` 13. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/uftp/images/{ID_img}` 14. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/p2p/images/{ID_img}` -15. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}` -16. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p` -17. [Ver Estado de Transmisiones Multicast-P2P](#ver-estado-de-transmisiones-multicast-p2p) - +15. [Ver Estado de Transmisiones UDPcast](#ver-estado-de-transmisiones-udpcast) - `GET /ogrepository/v1/udpcast` +16. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}` +17. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p` --- ### Obtener Información de Estado de ogRepository Se devolverá informacion de CPU, memoria RAM, disco duro y el estado de ciertos servicios y procesos de ogRepository, en formato JSON. -Se puede utilizar el script "**getRepoStatus.py**, que hemos programado recientemente. -**NOTA**: En los apartados "services" y "processes" he especificado los servicios y procesos que me han parecido interesantes, pero se puede añadir o eliminar los que se desee. +Se puede utilizar el script "**getRepoStatus.py**, que debe ser llamado por el endpoint. +**NOTA**: En los apartados "services" y "processes" he especificado los servicios y procesos que me han parecido interesantes, pero se puede añadir o eliminar los que se desee. **URL:** `/ogrepository/v1/status` **Método HTTP:** GET @@ -92,7 +92,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/stat ### Obtener Información de todas las Imágenes Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (que corresponde a todas las imágenes almacenadas en el repositorio), y en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (que corresponde a las imágenes que fueron eliminadas, que estarán en la papelera). -Se puede utilizar el script "**getRepoInfo.py**, que hemos programado recientemente. +Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint. **NOTA**: La versión actual de este script requiere que se le pase "all" como primer parámetro (que correspondería al nombre de la imagen) y "none" como segundo parámetro (que corresponderia al nombre del subdirectorio correspondiente a la OU). Esta transformación de parámetros se puede realizar en el controlador PHP. **URL:** `/ogrepository/v1/images` @@ -198,7 +198,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag ### Obtener Información de una Imagen concreta Se devolverá la informacion de la imagen especificada, que puede estar en el archivo "**/opt/opengnsys/etc/repoinfo.json**" o en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (en este último caso, si la imagen está en la papelera). -Se puede utilizar el script "**getRepoInfo.py**, que hemos programado recientemente. +Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint. **NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión) como primer parámetro, y el subdirectorio correspondiente a la OU (o "none" si no es el caso) como segundo parámetro. En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP. **URL:** `/ogrepository/v1/images/{ID_img}` @@ -249,7 +249,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag ### Actualizar Información del Repositorio Se actualizará la información de las imágenes almacenadas en el repositorio, reflejándola en el archivo "**/opt/opengnsys/etc/repoinfo.json**". -Se puede hacer con el script "**updateRepoInfo.py**", que hemos programado recientemente (y que es similar al script bash original "**checkrepo**"). +Se puede hacer con el script "**updateRepoInfo.py**", que debe ser llamado por el endpoint (y que es similar al script bash original "**checkrepo**"). Este endpoint es llamado por el script "**deleteImage.py**" (para actualizar la información cada vez que se elimine una imagen), y creemos que también debe ser llamado por ogCore u ogLive cada vez que se haya creado una imagen. **URL:** `/ogrepository/v1/images` @@ -289,7 +289,7 @@ curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/sta ### Eliminar una Imagen Se eliminará la imagen especificada como parámetro, pudiendo eliminarla permanentemente o enviarla a la papelera. -Se puede hacer con el script "**deleteimage.py**", que hemos programado recientemente (y que incluye la funcionalidad "papelera"), y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. +Se puede hacer con el script "**deleteimage.py**", que debe ser llamado por el endpoint (y que incluye la funcionalidad "papelera"), y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. **NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y el parámetro opcional "-p" (para que la eliminación sea permanente). En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP, pero en principio habrá que especificar el método de eliminación en un json (en la URL). **URL:** `/ogrepository/v1/images/{ID_img}` @@ -312,7 +312,7 @@ curl -X DELETE -H "Authorization: $API_KEY" -H "Content-Type: application/json" ### Recuperar una Imagen Se recuperará la imagen especificada como parámetro, desde la papelera. -Se puede hacer con el script "**recoverImage.py**", que hemos programado recientemente, y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. +Se puede hacer con el script "**recoverImage.py**", que debe ser llamado por el endpoint, y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. **NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP. **URL:** `/ogrepository/v1/trash/images/{ID_img}` @@ -332,7 +332,7 @@ curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/tra ### Eliminar una Imagen de la Papelera Se eliminará permanentemente la imagen especificada como parámetro, desde la papelera. -Se puede hacer con el script "**deleteTrashImage.py**", que hemos programado recientemente, y que a su vez llama al script "**updateTrashInfo.py**", para actualizar la información de la papelera. +Se puede hacer con el script "**deleteTrashImage.py**", que debe ser llamado por el endpoint, y que a su vez llama al script "**updateTrashInfo.py**", para actualizar la información de la papelera. **NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP. **URL:** `/ogrepository/v1/trash/images/{ID_img}` @@ -352,7 +352,7 @@ curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/t ### Importar una Imagen Se importará una imagen de un repositorio remoto al repositorio local. -Se puede hacer con el script "**importImage.py**", que hemos programado recientemente. +Se puede hacer con el script "**importImage.py**", que debe ser llamado por el endpoint. **NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. En principio, ogCore puede acceder a estos datos a partir del ID del repositorio y del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP, pero en principio habrá que especificar el usuario remoto en un json (en la URL). **URL:** `/ogrepository/v1/repo/{ID_repo}/images/{ID_img}` @@ -375,7 +375,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d ### Crear archivos auxiliares Se crearán los archivos ".sum", ".full.sum", ".size" y ".torrent", para la imagen especificada como parámetro. -Se puede hacer con el script "**createTorrentSum.py**", que hemos programado recientemente. +Se puede hacer con el script "**createTorrentSum.py**", que debe ser llamado por el endpoint. **URL:** `/ogrepository/v1/images/torrentsum` **Método HTTP:** POST @@ -398,7 +398,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d ### Enviar paquete Wake On Lan Se enviará un paquete Wake On Lan a la dirección MAC especificada, a través de la IP de broadcast especificada. -Se puede hacer con el script "**sendWakeOnLan.py**", que hemos programado recientemente. +Se puede hacer con el script "**sendWakeOnLan.py**", que debe ser llamado por el endpoint. **URL:** `/ogrepository/v1/wol` **Método HTTP:** POST @@ -494,11 +494,40 @@ curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/p2p - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. - **Código 200 OK:** La imagen se ha enviado exitosamente. +--- +### Ver Estado de Transmisiones UDPcast + +Se devolverá el pid de los procesos de transferencias UDPcast activas, y sus imágenes asociadas, en formato JSON, o un mensaje informativo si no hay procesos activos, o si se produce un error. +Se puede hacer con el script "**getUDPcastInfo.py**", que debe ser llamado por el endpoint. + +**URL:** `/ogrepository/v1/udpcast` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/udpcast +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al comprobar las transmisiones UDPcast. +- **Código 400 Bad Request:** No se han encontrado transmisiones UDPcast activas. +- **Código 200 OK:** La información de las transmisiones UDPcast activas se obtuvo exitosamente. + - **Contenido:** Información de las transmisiones UDPcast activas en formato JSON. + ```json + { + "6720": { + "image": "Ubuntu20.img" + }, + "6721": { + "image": "Windows10.img" + } + } + ``` --- ### Cancelar Transmisión UDPcast Se cancelará la transmisión por UDPcast activa de la imagen especificada como parámetro, deteniendo el proceso "udp-sender" asociado a dicha imagen. -Se puede hacer con el script "**stopUDPcast.py**", que hemos programado recientemente. +Se puede hacer con el script "**stopUDPcast.py**", que debe ser llamado por el endpoint. **NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP. **URL:** `/ogrepository/v1/udpcast/images/{ID_img}` @@ -518,7 +547,7 @@ curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/u ### Cancelar Transmisiones P2P Se cancelarán las transmisiones P2P activas en el ogRepository al que se envíe la orden, deteniendo los procesos "bttrack" y "btlaunchmany.bittornado". -Se puede hacer con el script "**stopP2P.py**", que hemos programado recientemente. +Se puede hacer con el script "**stopP2P.py**", que debe ser llamado por el endpoint. **NOTA**: No he encontrado la forma de detener la transmisión de una imagen concreta, ya que "bttrack" y "btlaunchmany.bittornado" hacen tracking y seed (respectivamente) de todos los torrents existentes en la raíz del directorio especificado. **URL:** `/ogrepository/v1/p2p` @@ -533,10 +562,4 @@ curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/p - **Código 500 Internal Server Error:** Ocurrió un error al cancelar las transmisiones. - **Código 200 OK:** Las transmisiones se han cancelado exitosamente. ---- -### Ver Estado de Transmisiones Multicast-P2P - -Se debe estudiar como realizar esta tarea para cada uno de los protocolos de transmisión, ya que cada uno tiene sus particularidades, y habitualmente no tienen comandos asociados para comprobar el estado de las transmisiones. Es posible que sea neceario parsear los logs de ogRepo y/o de los clientes. -**NOTA**: Seguramente deba crearse un endpoint específico para cada uno de los protocolos que se utilicen. - --- \ No newline at end of file diff --git a/bin/getUDPcastInfo.py b/bin/getUDPcastInfo.py new file mode 100644 index 0000000..72dc4a9 --- /dev/null +++ b/bin/getUDPcastInfo.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +Este script busca procesos activos de "udp-sender", y si encuentra alguno devuelve el pid y la imagen asociada de cada uno de ellos, en una estructura JSON. +Si no encuentra ninguno, o si se produce un error, imprime un mensaje informativo. +No recibe ningún parámetro. + +En la práctica, permite comprobar las transminisones UDPcast activas, porque cuando finalizan también finaliza el proceso asociado. +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import subprocess +import json + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +repo_path = '/opt/opengnsys/images/' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def get_udpsender_processes(): + """ Busca procesos de "udp-sender", y si los encuentra retorna el pid y la imagen asociada de cada uno de ellos, en un diccionario. + Si no encuentra ningun proceso, o si se produce un error, retorna un mensaje. + """ + try: + # Obtenemos todos los procesos, y almacenamos la salida y los errores: + result = subprocess.Popen(['ps', '-aux'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + out, error = result.communicate() + + # Almacenamos en una lista los procesos que contengan "udp-sender": + process_list = [line for line in out.split('\n') if 'udp-sender' in line] + # Si hemos encontrado procesos de udp-sender creamos un diccionario para almacenarlos: + if process_list != []: + result_dict = {} + # Iteramos los procesos y extraemos el pid y el nombre de la imagen de cada uno, + # los almacenamos en el diccionario, y retornamos este: + for process in process_list: + pid = process.split()[1] + image = process.split(repo_path)[1] + result_dict[pid] = {'image':image} + return result_dict + # Si no hemos encontrado procesos de udp-sender retrornamos un mensaje: + else: + return "udp-sender process not found" + # Si se ha producido una excepción, retornamos un mensaje: + except Exception: + return "Unexpected error" + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Obtenemos información sobre los procesos de udp-sender: + results = get_udpsender_processes() + + # Si no hay procesos activos, o si se ha producido un error, imprimimos un mensaje explicativo: + if results == "udp-sender process not found": + print("No UDPcast active transmissions") + elif results == "Unexpected error": + print("Unexpected error checking UDPcast transmissions") + # Si hay procesos activos, convertimos el diccionario de resultados a JSON, e imprimimos este: + else: + json_data = json.dumps(results, indent=4) + print(json_data) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/sendFileMcast.py b/bin/sendFileMcast.py index ae7d1eb..81fa35b 100644 --- a/bin/sendFileMcast.py +++ b/bin/sendFileMcast.py @@ -171,8 +171,8 @@ def main(): '--min-clients', nclients, '--max-wait', maxtime, # Esto hace que espere 120 segundos desde la conexión del primer cliente para empezar la transmisión. #'--autostart', '20', # Esto hace que empiece la transmisión automáticamente después de enviar 20 paquetes "hello". - '--file', file_path, - '--log', log_file + '--log', log_file, + '--file', file_path ] # Imprimimos el comando con espacios (como realmente se enviará): -- 2.40.1 From de649f84dc6b86557139742a13379e92bc883ae6 Mon Sep 17 00:00:00 2001 From: ggil Date: Tue, 15 Oct 2024 14:44:48 +0200 Subject: [PATCH 37/70] refs #631 - Add 'exportImage.py' and modify API proposal --- README.md | 84 ++++++++++++------ bin/exportImage.py | 215 +++++++++++++++++++++++++++++++++++++++++++++ bin/importImage.py | 3 + 3 files changed, 276 insertions(+), 26 deletions(-) create mode 100644 bin/exportImage.py diff --git a/README.md b/README.md index 398525a..218f1f4 100644 --- a/README.md +++ b/README.md @@ -25,18 +25,19 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará 3. [Obtener Información de una Imagen concreta](#obtener-información-de-una-imagen-concreta) - `GET /ogrepository/v1/images/{ID_img}` 4. [Actualizar Información del Repositorio](#actualizar-información-del-repositorio) - `PUT /ogrepository/v1/images` 5. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/status/images/{ID_img}` -6. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/{ID_img}` -7. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/trash/images/{ID_img}` +6. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/{ID_img}?method={method}` +7. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/trash/images` 8. [Eliminar una Imagen de la Papelera](#eliminar-una-imagen-de-la-papelera) - `DELETE /ogrepository/v1/trash/images/{ID_img}` -9. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/repo/{ID_repo}/images/{ID_img}` -10. [Crear archivos auxiliares](#crear-archivos-auxiliares) - `POST /ogrepository/v1/images/torrentsum` -11. [Enviar paquete Wake On Lan](#enviar-paquete-wake-on-lan) - `POST /ogrepository/v1/wol` -12. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/udpcast/images/{ID_img}` -13. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/uftp/images/{ID_img}` -14. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/p2p/images/{ID_img}` -15. [Ver Estado de Transmisiones UDPcast](#ver-estado-de-transmisiones-udpcast) - `GET /ogrepository/v1/udpcast` -16. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}` -17. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p` +9. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/repo/images` +10. [Exportar una Imagen](#exportar-una-imagen) - `PUT /ogrepository/v1/repo/images` +11. [Crear archivos auxiliares](#crear-archivos-auxiliares) - `POST /ogrepository/v1/images/torrentsum` +12. [Enviar paquete Wake On Lan](#enviar-paquete-wake-on-lan) - `POST /ogrepository/v1/wol` +13. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/udpcast/images/{ID_img}` +14. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/uftp/images/{ID_img}` +15. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/p2p/images/{ID_img}` +16. [Ver Estado de Transmisiones UDPcast](#ver-estado-de-transmisiones-udpcast) - `GET /ogrepository/v1/udpcast` +17. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}` +18. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p` --- ### Obtener Información de Estado de ogRepository @@ -93,7 +94,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/stat Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (que corresponde a todas las imágenes almacenadas en el repositorio), y en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (que corresponde a las imágenes que fueron eliminadas, que estarán en la papelera). Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint. -**NOTA**: La versión actual de este script requiere que se le pase "all" como primer parámetro (que correspondería al nombre de la imagen) y "none" como segundo parámetro (que corresponderia al nombre del subdirectorio correspondiente a la OU). Esta transformación de parámetros se puede realizar en el controlador PHP. +**NOTA**: La versión actual de este script requiere que se le pase "all" como primer parámetro (que correspondería al nombre de la imagen) y "none" como segundo parámetro (que corresponderia al nombre del subdirectorio correspondiente a la OU). Esta transformación de parámetros se realiza en la API. **URL:** `/ogrepository/v1/images` **Método HTTP:** GET @@ -199,7 +200,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag Se devolverá la informacion de la imagen especificada, que puede estar en el archivo "**/opt/opengnsys/etc/repoinfo.json**" o en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (en este último caso, si la imagen está en la papelera). Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión) como primer parámetro, y el subdirectorio correspondiente a la OU (o "none" si no es el caso) como segundo parámetro. En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión) como primer parámetro, y el subdirectorio correspondiente a la OU (o "none" si no es el caso) como segundo parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. **URL:** `/ogrepository/v1/images/{ID_img}` **Método HTTP:** GET @@ -269,7 +270,7 @@ curl -X PUT -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag Se comprobará la integridad del fichero de imagen especificado como parámetro. Se puede hacer con el script "**checkImage.py**", que compara el tamaño actual del archivo con el almacenado en el archivo "**.size**", y el hash MD5 del último MB del archivo con el almacenado en el archivo "**.sum**". -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. **URL:** `/ogrepository/v1/status/images/{ID_img}` **Método HTTP:** GET @@ -290,18 +291,18 @@ curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/sta Se eliminará la imagen especificada como parámetro, pudiendo eliminarla permanentemente o enviarla a la papelera. Se puede hacer con el script "**deleteimage.py**", que debe ser llamado por el endpoint (y que incluye la funcionalidad "papelera"), y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y el parámetro opcional "-p" (para que la eliminación sea permanente). En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP, pero en principio habrá que especificar el método de eliminación en un json (en la URL). +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y el parámetro opcional "-p" (para que la eliminación sea permanente). Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros, pero también hay que especificar el método de eliminación en la URL, como parámetro adicional. -**URL:** `/ogrepository/v1/images/{ID_img}` +**URL:** `/ogrepository/v1/images/{ID_img}?method={method}` **Método HTTP:** DELETE -**Cuerpo de la Solicitud (JSON):** +**Parámetro adicional (en URL):** - **method**: Método de eliminación (puede ser "trash" o "permanent"). **Ejemplo de Solicitud:** ```bash -curl -X DELETE -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"method":"trash"}' http://example.com/ogrepository/v1/images/{ID_img} +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/{ID_img}?method=trash ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al eliminar la imagen. @@ -313,15 +314,18 @@ curl -X DELETE -H "Authorization: $API_KEY" -H "Content-Type: application/json" Se recuperará la imagen especificada como parámetro, desde la papelera. Se puede hacer con el script "**recoverImage.py**", que debe ser llamado por el endpoint, y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. -**URL:** `/ogrepository/v1/trash/images/{ID_img}` +**URL:** `/ogrepository/v1/trash/images` **Método HTTP:** POST +**Cuerpo de la Solicitud (JSON):** +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). + **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/trash/images/{ID_img} +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id"}' http://example.com/ogrepository/v1/trash/images ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al recuperar la imagen. @@ -333,7 +337,7 @@ curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/tra Se eliminará permanentemente la imagen especificada como parámetro, desde la papelera. Se puede hacer con el script "**deleteTrashImage.py**", que debe ser llamado por el endpoint, y que a su vez llama al script "**updateTrashInfo.py**", para actualizar la información de la papelera. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. **URL:** `/ogrepository/v1/trash/images/{ID_img}` **Método HTTP:** DELETE @@ -353,24 +357,52 @@ curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/t Se importará una imagen de un repositorio remoto al repositorio local. Se puede hacer con el script "**importImage.py**", que debe ser llamado por el endpoint. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. En principio, ogCore puede acceder a estos datos a partir del ID del repositorio y del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP, pero en principio habrá que especificar el usuario remoto en un json (en la URL). +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. Estos parámetros deben enviarse desde ogCore (en el JSON), porque el repositorio local no puede extraer la información de la imagen de un ID almacenado en un repositorio remoto. -**URL:** `/ogrepository/v1/repo/{ID_repo}/images/{ID_img}` +**URL:** `/ogrepository/v1/repo/images` **Método HTTP:** POST **Cuerpo de la Solicitud (JSON):** -- **user**: Usuario con el que acceder al repositorio remoto. +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). +- **repo_ip**: Dirección IP del repositorio remoto (desde el que se importará la imagen). +- **user**: Usuario con el que acceder al repositorio remoto. **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"user":"user_name"}' http://example.com/ogrepository/v1/repo/{ID_repo}/images/{ID_img} +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img","ou_subdir":"none","repo_ip":"192.168.56.100","user":"user_name"}' http://example.com/ogrepository/v1/repo/images ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al importar la imagen. - **Código 400 Bad Request:** No se ha encontrado la imagen y/o el equipo remoto especificados. - **Código 200 OK:** La imagen se ha importado exitosamente. +--- +### Exportar una Imagen + +Se exportará una imagen del repositorio local a un repositorio remoto. +Se puede hacer con el script "**exportImage.py**", que debe ser llamado por el endpoint. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero la IP del repositorio remoto y el usuario remoto deben enviarse desde ogCore (en el JSON). + +**URL:** `/ogrepository/v1/repo/images` +**Método HTTP:** PUT + +**Cuerpo de la Solicitud (JSON):** +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). +- **repo_ip**: Dirección IP del repositorio remoto (al que se exportrará la imagen). +- **user**: Usuario con el que acceder al repositorio remoto. + +**Ejemplo de Solicitud:** + +```bash +curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id","repo_ip":"192.168.56.100","user":"user_name"}' http://example.com/ogrepository/v1/repo/images +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al exportar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen y/o el equipo remoto especificados. +- **Código 200 OK:** La imagen se ha exportado exitosamente. + --- ### Crear archivos auxiliares diff --git a/bin/exportImage.py b/bin/exportImage.py new file mode 100644 index 0000000..15eba23 --- /dev/null +++ b/bin/exportImage.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script exporta la imagen especificada como primer parámetro (y sus archivos asociados), al repositorio remoto especificado como segundo parámetro, + con las credenciales del usuario especificado como tercer parámetro (en principio, mediante claves). +Realiza la acción contraria que el script "importImage.py", pero es preferible usar "exportImage.py" (porque permite buscar la imagen por ID). +Al acabar, ogCore debe llamar al script "updateRepoInfo.py" en el repositorio remoto, para actualizar la información de dicho repositorio. + +Librerías Python requeridas: "paramiko" (se puede instalar con "sudo apt install python3-paramiko") + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a exportar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/images/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/images/ou_subdir/image1.img + +sys.argv[2] - IP o hostname del repositorio remoto. + - Ejemplo1: 192.168.56.100 + - Ejemplo2: remote_repo + +sys.argv[3] - Usuario con el que conectar al repositorio remoto. + - Ejemplo1: remote_user + - Ejemplo2: root + + Sintaxis +---------- +./exportImage.py [ou_subdir/]image_name|/image_path/image_name remote_host remote_user + + Ejemplos + --------- +./exportImage.py image1.img 192.168.56.100 user +./exportImage.py /opt/opengnsys/images/image1.img 192.168.56.100 user +./exportImage.py ou_subdir/image1.img remote_hostname user +./exportImage.py /ou_subdir/image1.img remote_hostname root +./exportImage.py /opt/opengnsys/images/ou_subdir/image1.img remote_hostname root +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import warnings +warnings.filterwarnings("ignore") +import os +import sys +import subprocess +import paramiko +import warnings + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/images/' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name remote_host remote_user + Ejemplo1: {script_name} image1.img 192.168.56.100 user + Ejemplo2: {script_name} /opt/opengnsys/images/image1.img 192.168.56.100 user + Ejemplo3: {script_name} ou_subdir/image1.img remote_hostname user + Ejemplo4: {script_name} /ou_subdir/image1.img remote_hostname root + Ejemplo5: {script_name} /opt/opengnsys/images/ou_subdir/image1.img remote_hostname root + """ + print(help_text) + + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 3 parámetros, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 4: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 3 parámetros") + show_help() + sys.exit(1) + + + +def build_file_path(): + """ Construye la ruta completa al archivo a exportar + (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "repo_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(repo_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(repo_path): + file_path = os.path.join(repo_path, param_path) + else: + file_path = param_path + return file_path + + + +def export_image(file_path, remote_host, remote_user): + """ Conecta al repositorio remoto por SSH e inicia un cliente SFTP. + Luego exporta la imagen al repositorio remoto (junto con sus archivos asociados). + """ + # Creamos una lista con las extensiones de los archivos asociados a la imagen + # (incluyendo ninguna extensión, que corresponde a la propia imagen): + extensions = ['', '.size', '.sum', '.full.sum', '.torrent', '.info.checked'] + + # Iniciamos un cliente SSH: + ssh_client = paramiko.SSHClient() + # Establecemos la política por defecto para localizar la llave del host localmente: + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + # Intentamos conectar con el equipo remoto por SSH, e iniciar un cliente SFTP, + try: # y en caso de fallar devolvemos un error y salimos del script: + ssh_client.connect(remote_host, 22, remote_user) # Así se hace con claves + #ssh_client.connect(remote_host, 22, remote_user, 'opengnsys') # Así se haría con password + sftp_client = ssh_client.open_sftp() + except Exception as error_description: + print(f"Connection has returned an exception: {error_description}") + sys.exit(4) + + # Comprobamos si la imagen ya existe en el equipo remoto, en cuyo caso devolvemos un error y salimos del script: + try: + sftp_client.stat(file_path) + print("Image already exists on remote repository.") + sys.exit(5) + except IOError: + print("As expected, image doesn't exist on remote repository.") + + # Evaluamos si la ruta de la imagen tiene 5 barras, en cuyo caso corresponderá a una imagen basada en OU, + # y almacenamos el nombre del directorio correspondiente a la OU: + if file_path.count('/') == 5: + ou_subdir = file_path.split('/')[4] + # Comprobamos si el directorio de OU existe en el equipo remoto, y en caso contrario lo creamos: + try: + sftp_client.stat(f"{repo_path}{ou_subdir}") + except IOError: + sftp_client.mkdir(f"{repo_path}{ou_subdir}", mode=755) + + # Creamos un archivo de bloqueo en el servidor remoto: + sftp_client.open(f"{file_path}.lock", 'w') + + # Exportamos la imagen al servidor remoto, junto con sus archivos asociados: + for ext in extensions: + sftp_client.put(f"{file_path}{ext}", f"{file_path}{ext}") + + # Renombramos el archivo remoto ".info.checked" a ".info", para que lo pille el script "updateRepoInfo.py": + sftp_client.rename(f"{file_path}.info.checked", f"{file_path}.info") + + # Eliminamos el archivo de bloqueo del servidor remoto: + sftp_client.remove(f"{file_path}.lock") + + # Cerramos el cliente SSH y el cliente SFTP: + ssh_client.close() + sftp_client.close() + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo a exportar: + file_path = build_file_path() + + # Si no existe el archivo de imagen, imprimimos un mensaje de error y salimos del script: + if not os.path.exists(file_path): + print("Image file doesn't exist") + sys.exit(2) + + # Si la imagen está bloqueada, imprimimos un mensaje de error y salimos del script: + if os.path.exists(f"{file_path}.lock"): + print("Image is locked.") + sys.exit(3) + + # Almacenamos la IP/hostname del repositorio remoto, y el usuario remoto (desde los parámetros): + remote_host = sys.argv[2] + remote_user = sys.argv[3] + + # Exportamos la imagen al repositorio remoto: + export_image(file_path, remote_host, remote_user) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/importImage.py b/bin/importImage.py index d93ee54..31a1a2e 100644 --- a/bin/importImage.py +++ b/bin/importImage.py @@ -206,6 +206,9 @@ def main(): # Importamos la imagen del repositorio remoto: import_image(file_path, remote_host, remote_user) + # Renombramos el archivo ".info.checked" a ".info", para que lo pille el script "updateRepoInfo.py": + os.rename(f"{file_path}.info.checked", f"{file_path}.info") + # Eliminamos el archivo de bloqueo: os.remove(f"{file_path}.lock") -- 2.40.1 From bc30ed1e4564f79cca0d609cb6a03d41fde18ee2 Mon Sep 17 00:00:00 2001 From: ggil Date: Wed, 16 Oct 2024 17:36:10 +0200 Subject: [PATCH 38/70] refs #794 - Modify API proposal and 'sendFileMcast.py' --- README.md | 51 ++++++++++++++++++++++++--------------------- bin/sendFileUFTP.py | 2 +- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 218f1f4..5db2375 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará 10. [Exportar una Imagen](#exportar-una-imagen) - `PUT /ogrepository/v1/repo/images` 11. [Crear archivos auxiliares](#crear-archivos-auxiliares) - `POST /ogrepository/v1/images/torrentsum` 12. [Enviar paquete Wake On Lan](#enviar-paquete-wake-on-lan) - `POST /ogrepository/v1/wol` -13. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/udpcast/images/{ID_img}` -14. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/uftp/images/{ID_img}` -15. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/p2p/images/{ID_img}` +13. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/udpcast` +14. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/uftp` +15. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/p2p` 16. [Ver Estado de Transmisiones UDPcast](#ver-estado-de-transmisiones-udpcast) - `GET /ogrepository/v1/udpcast` 17. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}` 18. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p` @@ -406,8 +406,9 @@ curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d --- ### Crear archivos auxiliares -Se crearán los archivos ".sum", ".full.sum", ".size" y ".torrent", para la imagen especificada como parámetro. -Se puede hacer con el script "**createTorrentSum.py**", que debe ser llamado por el endpoint. +Se crearán los archivos ".sum", ".full.sum", ".size" y ".torrent", para la imagen especificada como parámetro. +Se puede hacer con el script "**createTorrentSum.py**", que debe ser llamado por el endpoint. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como unico parámetro. Este parámetro no puede obtenerse en la API, a partir del ID de imagen (como en otros casos), porque el ID corresponde al contenido del archivo "full.sum" asociado (que no estará creado hasta que no se ejecute este script). **URL:** `/ogrepository/v1/images/torrentsum` **Método HTTP:** POST @@ -427,10 +428,11 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d - **Código 200 OK:** Los archivos se han creado exitosamente. --- -### Enviar paquete Wake On Lan +### Enviar paquete Wake On Lan Se enviará un paquete Wake On Lan a la dirección MAC especificada, a través de la IP de broadcast especificada. Se puede hacer con el script "**sendWakeOnLan.py**", que debe ser llamado por el endpoint. +**NOTA**: La versión actual de este script requiere que se le pase la dirección IP de broadcast como primer parámetro, y la dirección MAC destino como segundo parámetro. Estos datos deben enviarse desde ogCore (en el JSON). **URL:** `/ogrepository/v1/wol` **Método HTTP:** POST @@ -453,14 +455,13 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se enviará la imagen especificada por Multicast, mediante la aplicación UDPcast. Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binario "**udp-sender**", que es quien realmente realiza el envío. -**NOTA**: Los datos multicast deben extraerse de la configuracién del Aula/OU (incluido el puerto). +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). -**URL:** `/ogrepository/v1/udpcast/images/{ID_img}` +**URL:** `/ogrepository/v1/udpcast` **Método HTTP:** POST **Cuerpo de la Solicitud (JSON):** -- **image**: Nombre de la imagen (con extensión). -- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). - **port**: Puerto Multicast. - **method**: Modalidad half-duplex o full-duplex. - **ip**: IP Multicast. @@ -471,7 +472,7 @@ Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binar **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none", "port":"9000", "method":"full", "ip":"239.194.17.2", "bitrate":"70M", "nclients":"20", "maxtime":"120"}' http://example.com/ogrepository/v1/udpcast/images/{ID_img} +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id", "port":"9000", "method":"full", "ip":"239.194.17.2", "bitrate":"70M", "nclients":"20", "maxtime":"120"}' http://example.com/ogrepository/v1/udpcast ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. @@ -482,15 +483,14 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d ### Enviar una Imagen mediante UFTP Se enviará la imagen especificada por Unicast o Multicast, mediante el protocolo "UFTP". -Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente los clientes ogLive destino se pongan en escucha con un daemon "UFTPD" (ejecutando el script "**listenUFTPD.py**"). -**NOTA**: Los envíos mediante "UFTP" funcionan al revés que los envíos mediante "UDPcast" (con este último, primero se debe ejecutar un comando en el servidor, y luego en los clientes). +Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente los clientes ogLive destino se pongan en escucha con un daemon "UFTPD" (ejecutando el script "**listenUFTPD.py**"). Esto funciona al revés que "UDPcast", ya que primero se debe ejecutar un comando en los clientes, y luego en el servidor. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). -**URL:** `/ogrepository/v1/uftp/images/{ID_img}` +**URL:** `/ogrepository/v1/uftp` **Método HTTP:** POST **Cuerpo de la Solicitud (JSON):** -- **image**: Nombre de la imagen (con extensión). -- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). - **port**: Puerto Multicast. - **ip**: IP Unicast/Multicast. - **bitrate**: Velocidad de transmisión (con "K" para Kbps, "M" para Mbps o "G" para Gbps). @@ -498,7 +498,7 @@ Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none", "port":"9000", "ip":"239.194.17.2", "bitrate":"1G"}' http://example.com/ogrepository/v1/uftp/images/{ID_img} +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id", "port":"9000", "ip":"239.194.17.2", "bitrate":"1G"}' http://example.com/ogrepository/v1/uftp ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. @@ -508,23 +508,26 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d --- ### Enviar una Imagen mediante P2P -Se enviará la imagen especificada mediante "P2P", iniciando el tracker y el seeder (que harán tracking y seed de los torrents contenidos en la raiz del subdirectorio especificado). -Se debe llamar al script "**runTorrentTracker.py**" y al script "**runTorrentSeeder.py**", especificando la ruta en la que está contenido el torrent (que debe estar en la raiz de dicha ruta). -**NOTA**: El directorio en el que está contenida la imagen y su torrent asociado puede obtenerse a partir del ID de la imagen (que corresponde al fullsum). +Se enviará la imagen especificada mediante "P2P", iniciando el tracker y el seeder (que harán tracking y seed de los torrents contenidos en la raiz del directorio especificado). +Se puede hacer con los scripts "**runTorrentTracker.py**" y "**runTorrentSeeder.py**", que deben ser llamados por el endpoint. +**NOTA**: La versión actual de estos scripts requiere que se le pase el directorio en el que está situada la imagen a enviar como único parámetro. Este dato se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"). -**URL:** `/ogrepository/v1/p2p/images/{ID_img}` +**URL:** `/ogrepository/v1/p2p` **Método HTTP:** POST +**Cuerpo de la Solicitud (JSON):** +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). + **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/p2p/images/{ID_img} +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id"}' http://example.com/ogrepository/v1/p2p ``` **Respuestas:** -- **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. +- **Código 500 Internal Server Error:** Ocurrió un error al intentar enviar la imagen. - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. -- **Código 200 OK:** La imagen se ha enviado exitosamente. +- **Código 200 OK:** La imagen se está enviando exitosamente. --- ### Ver Estado de Transmisiones UDPcast diff --git a/bin/sendFileUFTP.py b/bin/sendFileUFTP.py index ef6e8bb..c765b31 100644 --- a/bin/sendFileUFTP.py +++ b/bin/sendFileUFTP.py @@ -6,7 +6,7 @@ Este script envía mediante UFTP la imagen recibida como primer parámetro, al p a la velocidad de transferencia tambíén especificada en el segundo parámetro (la sintaxis de este parámetro es "Port:IP:Bitrate"). Previamente, los clientes deben haberse puesto a escuchar en la IP Multicast correspondiente (tanto para envíos Multicast como para envíos Unicast). -Paquetes APT requeridos: "uftp" +Paquetes APT requeridos: "uftp" (se puede instalar con "sudo apt install uftp"). Parámetros ------------ -- 2.40.1 From 93a1b6c937b79fb3bc64cb353bdfbac397be1168 Mon Sep 17 00:00:00 2001 From: ggil Date: Thu, 17 Oct 2024 17:38:30 +0200 Subject: [PATCH 39/70] refs #794 - Add API and modify 'getUDPcastInfo.py' --- README.md | 32 +- api/README.md | 591 ++++++++++++++++++++ api/repo_api.py | 1008 +++++++++++++++++++++++++++++++++++ bin/OLD/udp-sender_20120424 | Bin 0 -> 56836 bytes bin/getUDPcastInfo.py | 20 +- bin/udp-sender | Bin 56836 -> 70304 bytes 6 files changed, 1631 insertions(+), 20 deletions(-) create mode 100644 api/README.md create mode 100644 api/repo_api.py create mode 100644 bin/OLD/udp-sender_20120424 diff --git a/README.md b/README.md index 5db2375..7688f4a 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,19 @@ ogRepository - OpenGnsys Repository Manager Este repositorio GIT contiene la estructura de datos del repositorio de imágenes de OpenGnsys. -- **admin** --- Archivos de configuración del repositorio. -- **bin** ------ Scripts en Python 3 y binarios de gestión del repositorio. -- **etc** ------ Ficheros y plantillas de configuración del repositorio. +- **admin** --- Archivos de configuración de ogRepository. +- **api** ------ API de ogRepository. +- **bin** ------ Scripts en Python 3 y binarios de gestión de ogRepository. +- **etc** ------ Ficheros y plantillas de configuración de ogRepository. --- ## API de ogRepository -La API de ogRepository proporciona una interfaz para facilitar la administración de las imágenes almacenadas en los repositorios de imágenes, permitiendo eliminarlas, enviarlas a clientes ogLive (con diferentes protocolos de transmisión), importarlas desde otros repositorios, etc. +La API de ogRepository proporciona una interfaz para facilitar la administración de las imágenes almacenadas en los repositorios de imágenes, permitiendo eliminarlas, enviarlas a clientes ogLive (con diferentes protocolos de transmisión), importarlas desde otros repositorios, etc. -El presente documento detalla los endpoints de la API, con sus respectivos parámetros de entrada, así como las acciones que llevan a cabo. +El presente documento detalla los endpoints de la API, con sus respectivos parámetros de entrada, así como las acciones que llevan a cabo. --- ### Tabla de Contenido: @@ -532,7 +533,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d --- ### Ver Estado de Transmisiones UDPcast -Se devolverá el pid de los procesos de transferencias UDPcast activas, y sus imágenes asociadas, en formato JSON, o un mensaje informativo si no hay procesos activos, o si se produce un error. +Se devolverá el pid de los procesos de transferencias UDPcast activas, y sus imágenes asociadas (con nombre e ID), en formato JSON, o un mensaje informativo si no hay procesos activos, o si se produce un error. Se puede hacer con el script "**getUDPcastInfo.py**", que debe ser llamado por el endpoint. **URL:** `/ogrepository/v1/udpcast` @@ -551,19 +552,21 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/udpc ```json { "6720": { - "image": "Ubuntu20.img" + "image_id": "22735b9070e4a8043371b8c6ae52b90d", + "image_name": "Ubuntu20.img" }, "6721": { - "image": "Windows10.img" + "image_id": "9e7cd32c606ebe5bd39ba212ce7aeb02", + "image_name": "Windows10.img" } } ``` --- -### Cancelar Transmisión UDPcast +### Cancelar Transmisión UDPcast Se cancelará la transmisión por UDPcast activa de la imagen especificada como parámetro, deteniendo el proceso "udp-sender" asociado a dicha imagen. Se puede hacer con el script "**stopUDPcast.py**", que debe ser llamado por el endpoint. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. En principio, ogCore puede acceder a estos datos a partir del ID de la imagen (que hemos quedado que será el contenido del archivo "full.sum"), y la transformación de parámetros puede realizarse en el controlador PHP. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Este dato se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"). **URL:** `/ogrepository/v1/udpcast/images/{ID_img}` **Método HTTP:** DELETE @@ -574,9 +577,10 @@ Se puede hacer con el script "**stopUDPcast.py**", que debe ser llamado por el e curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/udpcast/images/{ID_img} ``` **Respuestas:** -- **Código 500 Internal Server Error:** Ocurrió un error al cancelar la transmisión. +- **Código 500 Internal Server Error:** Ocurrió un error al cancelar la transmisión UDPcast. - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. -- **Código 200 OK:** La transmisión se ha cancelado exitosamente. +- **Código 400 Bad Request:** No hay transmisiones UCPcast activas para la imagen especificada. +- **Código 200 OK:** La transmisión UDPcast se ha cancelado exitosamente. --- ### Cancelar Transmisiones P2P @@ -594,7 +598,7 @@ Se puede hacer con el script "**stopP2P.py**", que debe ser llamado por el endpo curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/p2p ``` **Respuestas:** -- **Código 500 Internal Server Error:** Ocurrió un error al cancelar las transmisiones. -- **Código 200 OK:** Las transmisiones se han cancelado exitosamente. +- **Código 500 Internal Server Error:** Ocurrió un error al cancelar las transmisiones P2P. +- **Código 200 OK:** Las transmisiones P2P se han cancelado exitosamente. --- \ No newline at end of file diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..2f6e0ef --- /dev/null +++ b/api/README.md @@ -0,0 +1,591 @@ + +## API de ogRepository + +La API de ogRepository proporciona una interfaz para facilitar la administración de las imágenes almacenadas en los repositorios de imágenes, permitiendo eliminarlas, enviarlas a clientes ogLive (con diferentes protocolos de transmisión), importarlas desde otros repositorios, etc. + +El presente documento detalla los endpoints de la API, con sus respectivos parámetros de entrada, así como las acciones que llevan a cabo. + +--- +### Tabla de Contenido: + +1. [Obtener Información de Estado de ogRepository](#obtener-información-de-estado-de-ogrepository) - `GET /ogrepository/v1/status` +2. [Obtener Información de todas las Imágenes](#obtener-información-de-todas-las-imágenes) - `GET /ogrepository/v1/images` +3. [Obtener Información de una Imagen concreta](#obtener-información-de-una-imagen-concreta) - `GET /ogrepository/v1/images/{ID_img}` +4. [Actualizar Información del Repositorio](#actualizar-información-del-repositorio) - `PUT /ogrepository/v1/images` +5. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/status/images/{ID_img}` +6. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/{ID_img}?method={method}` +7. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/trash/images` +8. [Eliminar una Imagen de la Papelera](#eliminar-una-imagen-de-la-papelera) - `DELETE /ogrepository/v1/trash/images/{ID_img}` +9. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/repo/images` +10. [Exportar una Imagen](#exportar-una-imagen) - `PUT /ogrepository/v1/repo/images` +11. [Crear archivos auxiliares](#crear-archivos-auxiliares) - `POST /ogrepository/v1/images/torrentsum` +12. [Enviar paquete Wake On Lan](#enviar-paquete-wake-on-lan) - `POST /ogrepository/v1/wol` +13. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/udpcast` +14. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/uftp` +15. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/p2p` +16. [Ver Estado de Transmisiones UDPcast](#ver-estado-de-transmisiones-udpcast) - `GET /ogrepository/v1/udpcast` +17. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}` +18. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p` + +--- +### Obtener Información de Estado de ogRepository + +Se devolverá informacion de CPU, memoria RAM, disco duro y el estado de ciertos servicios y procesos de ogRepository, en formato JSON. +Se puede utilizar el script "**getRepoStatus.py**, que debe ser llamado por el endpoint. +**NOTA**: En los apartados "services" y "processes" he especificado los servicios y procesos que me han parecido interesantes, pero se puede añadir o eliminar los que se desee. + +**URL:** `/ogrepository/v1/status` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/status +``` + +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al consultar y/o devolver la información de estado. +- **Código 200 OK:** La información de estado se obtuvo exitosamente. + - **Contenido:** Información de estado en formato JSON. + ```json + { + "cpu": { + "used_percentage": "35%" + }, + "ram": { + "total": "7.8GB", + "used": "0.3GB", + "available": "7.2GB", + "used_percentage": "7%" + }, + "disk": { + "total": "11.7GB", + "used": "7.7GB", + "available": "3.4GB", + "used_percentage": "69%" + }, + "services": { + "ssh": "active", + "smbd": "active", + "rsync": "active" + }, + "processes": { + "udp-sender": "stopped", + "uftp": "stopped", + "bttrack": "stopped", + "btlaunchmany": "stopped" + } + } + ``` +--- +### Obtener Información de todas las Imágenes + +Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (que corresponde a todas las imágenes almacenadas en el repositorio), y en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (que corresponde a las imágenes que fueron eliminadas, que estarán en la papelera). +Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint. +**NOTA**: La versión actual de este script requiere que se le pase "all" como primer parámetro (que correspondería al nombre de la imagen) y "none" como segundo parámetro (que corresponderia al nombre del subdirectorio correspondiente a la OU). Esta transformación de parámetros se realiza en la API. + +**URL:** `/ogrepository/v1/images` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images +``` + +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al consultar y/o devolver la información de las imágenes. +- **Código 200 OK:** La información de las imágenes se obtuvo exitosamente. + - **Contenido:** Información de imágenes en formato JSON. + ```json + { + "REPOSITORY": { + "directory": "/opt/opengnsys/images", + "images": [ + { + "name": "Ubuntu24", + "type": "img", + "clientname": "Ubuntu_24", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "EXTFS", + "datasize": 9859634200000, + "size": 4505673214, + "sum": "065a933c780ab1aaa044435ad5d4bf87", + "fullsum": "33575b9070e4a8043371b8c6ae52b80e" + }, + { + "name": "Windows10", + "type": "img", + "clientname": "Windows_10", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "NTFS", + "datasize": 24222105600000, + "size": 13198910185, + "sum": "8874d5ab84314f44841c36c69bb5aa82", + "fullsum": "9e7cd32c606ebe5bd39ba212ce7aeb02" + } + ], + "ous": [ + { + "subdir": "OU_subdir", + "images": [ + { + "name": "Ubuntu20", + "type": "img", + "clientname": "Ubuntu_20", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "EXTFS", + "datasize": 8912896000000, + "size": 3803794535, + "sum": "081a933c780ab1aaa044435ad5d4bf56", + "fullsum": "22735b9070e4a8043371b8c6ae52b90d" + } + ] + } + ] + } + }, + "TRASH": { + "directory": "/opt/opengnsys/images_trash", + "images": [], + "ous": [ + { + "subdir": "CentroVirtual", + "images": [ + { + "name": "Ubuntu20OLD", + "type": "img", + "clientname": "Ubuntu_20", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "EXTFS", + "datasize": 8912896000000, + "size": 3803794535, + "sum": "081a933c780ab1aaa044435ad5d4bf56", + "fullsum": "22735b9070e4a8043371b8c6ae52b90d" + } + ] + } + ] + } + ``` + - **name**: Nombre de la imagen, sin extensión. + - **type**: Extensión de la imagen. + - **clientname**: Nombre asignado al modelo del que se ha obtenido la imagen. + - **clonator**: Programa utilizado para la clonación. + - **compressor**: Programa utilizado para la compresión. + - **filesystem**: Sistema de archivos utilizado en la partición clonada. + - **datasize**: Tamaño de la imagen una vez restaurada, en bytes (tamaño de los datos). + - **size**: Tamaño del archivo de imagen, en bytes. + - **sum**: Hash MD5 del último MB del archivo de imagen. + - **fullsum**: Hash MD5 de todo el archivo de imagen. + +--- +### Obtener Información de una Imagen concreta + +Se devolverá la informacion de la imagen especificada, que puede estar en el archivo "**/opt/opengnsys/etc/repoinfo.json**" o en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (en este último caso, si la imagen está en la papelera). +Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión) como primer parámetro, y el subdirectorio correspondiente a la OU (o "none" si no es el caso) como segundo parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. + +**URL:** `/ogrepository/v1/images/{ID_img}` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/{ID_img} +``` + +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al consultar y/o devolver la información de las imágenes. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La información de la imagen se obtuvo exitosamente. + - **Contenido:** Información de la imagen en formato JSON. + ```json + { + "directory": "/opt/opengnsys/images", + "images": [ + { + "name": "Windows10", + "type": "img", + "clientname": "Windows_10", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "NTFS", + "datasize": 9859634200000, + "size": 4505673214, + "sum": "065a933c780ab1aaa044435ad5d4bf87", + "fullsum": "33575b9070e4a8043371b8c6ae52b80e" + } + ] + } + ``` + - **name**: Nombre de la imagen, sin extensión. + - **type**: Extensión de la imagen. + - **clientname**: Nombre asignado al modelo del que se ha obtenido la imagen. + - **clonator**: Programa utilizado para la clonación. + - **compressor**: Programa utilizado para la compresión. + - **filesystem**: Sistema de archivos utilizado en la partición clonada. + - **datasize**: Tamaño de la imagen una vez restaurada, en bytes (tamaño de los datos). + - **size**: Tamaño del archivo de imagen, en bytes. + - **sum**: Hash MD5 del último MB del archivo de imagen. + - **fullsum**: Hash MD5 de todo el archivo de imagen. + +--- +### Actualizar Información del Repositorio + +Se actualizará la información de las imágenes almacenadas en el repositorio, reflejándola en el archivo "**/opt/opengnsys/etc/repoinfo.json**". +Se puede hacer con el script "**updateRepoInfo.py**", que debe ser llamado por el endpoint (y que es similar al script bash original "**checkrepo**"). +Este endpoint es llamado por el script "**deleteImage.py**" (para actualizar la información cada vez que se elimine una imagen), y creemos que también debe ser llamado por ogCore u ogLive cada vez que se haya creado una imagen. + +**URL:** `/ogrepository/v1/images` +**Método HTTP:** PUT + +**Ejemplo de Solicitud:** + +```bash +curl -X PUT -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al actualizar la información de las imágenes. +- **Código 200 OK:** La actualización se realizó exitosamente. + +--- +### Chequear Integridad de Imagen + +Se comprobará la integridad del fichero de imagen especificado como parámetro. +Se puede hacer con el script "**checkImage.py**", que compara el tamaño actual del archivo con el almacenado en el archivo "**.size**", y el hash MD5 del último MB del archivo con el almacenado en el archivo "**.sum**". +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. + +**URL:** `/ogrepository/v1/status/images/{ID_img}` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/status/images/{ID_img} +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al chequear la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se ha chequeado exitosamente. +- **Código 200 KO:** La imagen se ha chequeado correctamente, pero no ha pasado el test. + +--- +### Eliminar una Imagen + +Se eliminará la imagen especificada como parámetro, pudiendo eliminarla permanentemente o enviarla a la papelera. +Se puede hacer con el script "**deleteimage.py**", que debe ser llamado por el endpoint (y que incluye la funcionalidad "papelera"), y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y el parámetro opcional "-p" (para que la eliminación sea permanente). Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros, pero también hay que especificar el método de eliminación en la URL, como parámetro adicional. + +**URL:** `/ogrepository/v1/images/{ID_img}?method={method}` +**Método HTTP:** DELETE + +**Parámetro adicional (en URL):** +- **method**: Método de eliminación (puede ser "trash" o "permanent"). + +**Ejemplo de Solicitud:** + +```bash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/{ID_img}?method=trash +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al eliminar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se eliminó exitosamente. + +--- +### Recuperar una Imagen + +Se recuperará la imagen especificada como parámetro, desde la papelera. +Se puede hacer con el script "**recoverImage.py**", que debe ser llamado por el endpoint, y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. + +**URL:** `/ogrepository/v1/trash/images` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id"}' http://example.com/ogrepository/v1/trash/images +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al recuperar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se recuperó exitosamente. + +--- +### Eliminar una Imagen de la Papelera + +Se eliminará permanentemente la imagen especificada como parámetro, desde la papelera. +Se puede hacer con el script "**deleteTrashImage.py**", que debe ser llamado por el endpoint, y que a su vez llama al script "**updateTrashInfo.py**", para actualizar la información de la papelera. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. + +**URL:** `/ogrepository/v1/trash/images/{ID_img}` +**Método HTTP:** DELETE + +**Ejemplo de Solicitud:** + +```bash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/trash/images/{ID_img} +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al eliminar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se eliminó exitosamente. + +--- +### Importar una Imagen + +Se importará una imagen de un repositorio remoto al repositorio local. +Se puede hacer con el script "**importImage.py**", que debe ser llamado por el endpoint. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. Estos parámetros deben enviarse desde ogCore (en el JSON), porque el repositorio local no puede extraer la información de la imagen de un ID almacenado en un repositorio remoto. + +**URL:** `/ogrepository/v1/repo/images` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). +- **repo_ip**: Dirección IP del repositorio remoto (desde el que se importará la imagen). +- **user**: Usuario con el que acceder al repositorio remoto. + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img","ou_subdir":"none","repo_ip":"192.168.56.100","user":"user_name"}' http://example.com/ogrepository/v1/repo/images +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al importar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen y/o el equipo remoto especificados. +- **Código 200 OK:** La imagen se ha importado exitosamente. + +--- +### Exportar una Imagen + +Se exportará una imagen del repositorio local a un repositorio remoto. +Se puede hacer con el script "**exportImage.py**", que debe ser llamado por el endpoint. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero la IP del repositorio remoto y el usuario remoto deben enviarse desde ogCore (en el JSON). + +**URL:** `/ogrepository/v1/repo/images` +**Método HTTP:** PUT + +**Cuerpo de la Solicitud (JSON):** +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). +- **repo_ip**: Dirección IP del repositorio remoto (al que se exportrará la imagen). +- **user**: Usuario con el que acceder al repositorio remoto. + +**Ejemplo de Solicitud:** + +```bash +curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id","repo_ip":"192.168.56.100","user":"user_name"}' http://example.com/ogrepository/v1/repo/images +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al exportar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen y/o el equipo remoto especificados. +- **Código 200 OK:** La imagen se ha exportado exitosamente. + +--- +### Crear archivos auxiliares + +Se crearán los archivos ".sum", ".full.sum", ".size" y ".torrent", para la imagen especificada como parámetro. +Se puede hacer con el script "**createTorrentSum.py**", que debe ser llamado por el endpoint. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como unico parámetro. Este parámetro no puede obtenerse en la API, a partir del ID de imagen (como en otros casos), porque el ID corresponde al contenido del archivo "full.sum" asociado (que no estará creado hasta que no se ejecute este script). + +**URL:** `/ogrepository/v1/images/torrentsum` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/torrentsum +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al crear los archivos. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** Los archivos se han creado exitosamente. + +--- +### Enviar paquete Wake On Lan + +Se enviará un paquete Wake On Lan a la dirección MAC especificada, a través de la IP de broadcast especificada. +Se puede hacer con el script "**sendWakeOnLan.py**", que debe ser llamado por el endpoint. +**NOTA**: La versión actual de este script requiere que se le pase la dirección IP de broadcast como primer parámetro, y la dirección MAC destino como segundo parámetro. Estos datos deben enviarse desde ogCore (en el JSON). + +**URL:** `/ogrepository/v1/wol` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **broadcast_ip**: IP de broadcast a la que enviar el paquete (puede ser "255.255.255.255", o la IP de broadcast de una subred). +- **mac**: Dirección MAC del equipo que se desea encender via Wake On Lan. + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"broadcast_ip":"255.255.255.255", "mac":"00:19:99:5c:bb:bb"}' http://example.com/ogrepository/v1/wol +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al enviar el paquete Wake On Lan. +- **Código 200 OK:** El paquete Wake On Lan se ha enviado exitosamente. + +--- +### Enviar una Imagen mediante UDPcast + +Se enviará la imagen especificada por Multicast, mediante la aplicación UDPcast. +Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binario "**udp-sender**", que es quien realmente realiza el envío. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). + +**URL:** `/ogrepository/v1/udpcast` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). +- **port**: Puerto Multicast. +- **method**: Modalidad half-duplex o full-duplex. +- **ip**: IP Multicast. +- **bitrate**: Velocidad de transmisión (en Mbps). +- **nclients**: Número mínimo de clientes. +- **maxtime**: Tiempo máximo de espera. + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id", "port":"9000", "method":"full", "ip":"239.194.17.2", "bitrate":"70M", "nclients":"20", "maxtime":"120"}' http://example.com/ogrepository/v1/udpcast +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se ha enviado exitosamente. + +--- +### Enviar una Imagen mediante UFTP + +Se enviará la imagen especificada por Unicast o Multicast, mediante el protocolo "UFTP". +Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente los clientes ogLive destino se pongan en escucha con un daemon "UFTPD" (ejecutando el script "**listenUFTPD.py**"). Esto funciona al revés que "UDPcast", ya que primero se debe ejecutar un comando en los clientes, y luego en el servidor. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). + +**URL:** `/ogrepository/v1/uftp` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). +- **port**: Puerto Multicast. +- **ip**: IP Unicast/Multicast. +- **bitrate**: Velocidad de transmisión (con "K" para Kbps, "M" para Mbps o "G" para Gbps). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id", "port":"9000", "ip":"239.194.17.2", "bitrate":"1G"}' http://example.com/ogrepository/v1/uftp +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se ha enviado exitosamente. + +--- +### Enviar una Imagen mediante P2P + +Se enviará la imagen especificada mediante "P2P", iniciando el tracker y el seeder (que harán tracking y seed de los torrents contenidos en la raiz del directorio especificado). +Se puede hacer con los scripts "**runTorrentTracker.py**" y "**runTorrentSeeder.py**", que deben ser llamados por el endpoint. +**NOTA**: La versión actual de estos scripts requiere que se le pase el directorio en el que está situada la imagen a enviar como único parámetro. Este dato se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"). + + +**URL:** `/ogrepository/v1/p2p` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id"}' http://example.com/ogrepository/v1/p2p +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al intentar enviar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se está enviando exitosamente. + +--- +### Ver Estado de Transmisiones UDPcast + +Se devolverá el pid de los procesos de transferencias UDPcast activas, y sus imágenes asociadas (con nombre e ID), en formato JSON, o un mensaje informativo si no hay procesos activos, o si se produce un error. +Se puede hacer con el script "**getUDPcastInfo.py**", que debe ser llamado por el endpoint. + +**URL:** `/ogrepository/v1/udpcast` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/udpcast +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al comprobar las transmisiones UDPcast. +- **Código 400 Bad Request:** No se han encontrado transmisiones UDPcast activas. +- **Código 200 OK:** La información de las transmisiones UDPcast activas se obtuvo exitosamente. + - **Contenido:** Información de las transmisiones UDPcast activas en formato JSON. + ```json + { + "6720": { + "image_id": "22735b9070e4a8043371b8c6ae52b90d", + "image_name": "Ubuntu20.img" + }, + "6721": { + "image_id": "9e7cd32c606ebe5bd39ba212ce7aeb02", + "image_name": "Windows10.img" + } + } + ``` +--- +### Cancelar Transmisión UDPcast + +Se cancelará la transmisión por UDPcast activa de la imagen especificada como parámetro, deteniendo el proceso "udp-sender" asociado a dicha imagen. +Se puede hacer con el script "**stopUDPcast.py**", que debe ser llamado por el endpoint. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Este dato se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"). + +**URL:** `/ogrepository/v1/udpcast/images/{ID_img}` +**Método HTTP:** DELETE + +**Ejemplo de Solicitud:** + +```bash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/udpcast/images/{ID_img} +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al cancelar la transmisión UDPcast. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 400 Bad Request:** No hay transmisiones UCPcast activas para la imagen especificada. +- **Código 200 OK:** La transmisión UDPcast se ha cancelado exitosamente. + +--- +### Cancelar Transmisiones P2P + +Se cancelarán las transmisiones P2P activas en el ogRepository al que se envíe la orden, deteniendo los procesos "bttrack" y "btlaunchmany.bittornado". +Se puede hacer con el script "**stopP2P.py**", que debe ser llamado por el endpoint. +**NOTA**: No he encontrado la forma de detener la transmisión de una imagen concreta, ya que "bttrack" y "btlaunchmany.bittornado" hacen tracking y seed (respectivamente) de todos los torrents existentes en la raíz del directorio especificado. + +**URL:** `/ogrepository/v1/p2p` +**Método HTTP:** DELETE + +**Ejemplo de Solicitud:** + +```bash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/p2p +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al cancelar las transmisiones P2P. +- **Código 200 OK:** Las transmisiones P2P se han cancelado exitosamente. + +--- \ No newline at end of file diff --git a/api/repo_api.py b/api/repo_api.py new file mode 100644 index 0000000..4757709 --- /dev/null +++ b/api/repo_api.py @@ -0,0 +1,1008 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + API de ogRepository, programada en Flask. + +Responde a peticiones HTTP (en principio, enviadas desde ogCore) mediante endpoints, que a su vez ejecutan los scripts Python almacenados en ogRepo. +En ciertos casos, transforma los parámetros recibidos desde el portal, para adaptarlos a los que es necesario enviar a los scripts + (por ejemplo, a partir del ID de una imagen obtiene su nombre, su extensión y el subdirectorio de OU). + +Librerías Python requeridas: Flask (se puede instalar con "sudo apt install python3-flask"). +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +from flask import Flask, jsonify, request +import subprocess +import json +from time import sleep + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +repo_path = '/opt/opengnsys/images/' +script_path = '/opt/opengnsys/bin' +repo_file = '/opt/opengnsys/etc/repoinfo.json' +trash_file = '/opt/opengnsys/etc/trashinfo.json' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +# Creamos una instancia de la aplicación Flask: +app = Flask(__name__) + + +# --------------------------------------------------------- + + +def get_image_params(image_id, search='all'): + """ A partir de un ID de imagen (que corresponde al "fullsum"), busca la imagen en el repositorio y/o en la papelera (dependiendo del parámetro "search"). + Si encuentra la imagen devuelve su nombre, su extensión y el subdirectorio de OU (si fuera el caso) en un diccionario, + y si no encuentra la imagen especificada retorna "None". + El parámtro "search" tiene el valor predeterminado "all" (que hará que busque tanto en el repo como en la papelera), + pero se le puede pasar el valor "repo" (para que busque solo en el repo) o "trash" (para que busque solo en la papelera). + """ + # Creamos un diccionario vacío, para almacenar los resultados: + result = {} + + # Abrimos y almacenamos el archivo "repoinfo.json" (solo si se ha de buscar en el repo): + if search == 'all' or search == 'repo': + with open(repo_file, 'r') as file: + repo_data = json.load(file) + # Iteramos la clave "images" y buscamos la imagen (y si la encontramos almacenamos el nombre y la extension): + for image in repo_data.get('images', []): + if image.get('fullsum') == image_id: + result['name'] = image.get('name') + result['extension'] = image.get('type') + return result + # Iteramos la clave "ous" y su sublclave "images" y buscamos la imagen (y si la encontramos almacenamos el nombre y la extension): + for ou in repo_data.get('ous', []): + for image in ou.get('images', []): + if image.get('fullsum') == image_id: + result['name'] = image.get('name') + result['extension'] = image.get('type') + result['subdir'] = ou.get('subdir') + return result + + # Abrimos y almacenamos el archivo "trashinfo.json" (solo si se ha de buscar en la papelera): + if search == 'all' or search == 'trash': + with open(trash_file, 'r') as file: + trash_data = json.load(file) + # Iteramos la clave "images" y buscamos la imagen (y si la encontramos almacenamos el nombre y la extension): + for image in trash_data.get('images', []): + if image.get('fullsum') == image_id: + result['name'] = image.get('name') + result['extension'] = image.get('type') + return result + # Iteramos la clave "ous" y su sublclave "images" y buscamos la imagen (y si la encontramos almacenamos el nombre y la extension): + for ou in trash_data.get('ous', []): + for image in ou.get('images', []): + if image.get('fullsum') == image_id: + result['name'] = image.get('name') + result['extension'] = image.get('type') + result['subdir'] = ou.get('subdir') + return result + + # Si no encontramos la imagen, retornamos "None": + return None + + +# --------------------------------------------------------- + + +def search_process(process, string_to_search): + """ Busca procesos que contengan el valor del parámetro "process" y el valor del parámetro "string_to_search" (la ruta de la imagen, normalmente). + Si encuentra alguno retorna "True", y si no encuentra ninguno retorna "False". + """ + try: + # Obtenemos todos los procesos que están corriendo, y almacenamos la salida y los errores: + result = subprocess.Popen(['ps', '-aux'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + out, error = result.communicate() + + # Almacenamos en una lista los procesos que contengan el proceso del parámetro y la cadena a buscar: + process_list = [line for line in out.split('\n') if process in line and string_to_search in line] + + # Si hemos encontrado algún proceso que cumpla las condiciones, retornamos "True", y si no retornamos "False": + if process_list != []: + return True + else: + return False + # Si se ha producido una excepción, imprimimos el error: + except Exception as error_description: + print(f"Unexpected error: {error_description}") + + + +# -------------------------------------------------------------------------------------------- +# ENDPOINTS +# -------------------------------------------------------------------------------------------- + + +# 1 - Endpoint "Obtener Información de Estado de ogRepository": +@app.route("/ogrepository/v1/status", methods=['GET']) +def get_repo_status(): + """ Este endpoint devuelve información de CPU, memoria RAM, disco duro y el estado de ciertos servicios y procesos de ogRepository, en formato json. + Para ello, ejecuta el script "getRepoStatus.py", que no recibe parámetros. + """ + try: + # Ejecutamos el script "getRepoStatus.py", y almacenamos el resultado: + result = subprocess.run(['sudo', 'python3', f"{script_path}/getRepoStatus.py"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos la respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": json.loads(result.stdout) + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 2 - Endpoint "Obtener Información de todas las Imágenes": +@app.route("/ogrepository/v1/images", methods=['GET']) +def get_repo_info(): + """ Este endpoint devuelve información de todas las imágenes contenidas en el repositorio (incluída la papelera), en formato json. + Para ello, ejecuta el script "getRepoInfo.py", con los parámetros "all" y "none". + """ + try: + # Ejecutamos el script "getRepoInfo.py" (con los parámetros "all" y "none"), y almacenamos el resultado: + result = subprocess.run(['sudo', 'python3', f"{script_path}/getRepoInfo.py", 'all', 'none'], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos la respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": json.loads(result.stdout) + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 3 - Endpoint "Obtener Información de una Imagen concreta": +@app.route("/ogrepository/v1/images/", methods=['GET']) +def get_repo_image_info(imageId): + """ Este endpoint devuelve información de la imagen especificada como parámetro, en formato json. + Para ello, ejecuta el script "getRepoInfo.py", con el nombre de la imagen como primer parámetro, + y el subdirectorio correspondiente a la OU (o "none") como segundo parámetro. + """ + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(imageId) + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd = ['sudo', 'python3', f"{script_path}/getRepoInfo.py", f"{param_dict['name']}.{param_dict['extension']}", param_dict['subdir']] + else: + cmd = ['sudo', 'python3', f"{script_path}/getRepoInfo.py", f"{param_dict['name']}.{param_dict['extension']}", 'none'] + else: + return jsonify({ + "success": False, + "error": "Image not found" + }), 400 + + try: + # Ejecutamos el script "getRepoInfo.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos la respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": json.loads(result.stdout) + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 4 - Endpoint "Actualizar Información del Repositorio": +@app.route("/ogrepository/v1/images", methods=['PUT']) +def update_repo_info(): + """ Este endpoint actualiza la información del repositorio y de la papelera, reflejándola en los archivos "repoinfo.json" y "trashinfo.josn". + Para ello, ejecuta el script "updateRepoInfo.py", que a su vez ejecuta el script "updateTrashInfo.py". + """ + try: + # Ejecutamos el script "updateRepoInfo.py" (sin parámetros), y almacenamos el resultado: + result = subprocess.run(['sudo', 'python3', f"{script_path}/updateRepoInfo.py"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "Repository info updated successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 5 - Endpoint "Chequear Integridad de Imagen": +@app.route("/ogrepository/v1/status/images/", methods=['GET']) +def check_image(imageId): + """ Este endpoint comprueba la integridad de la imagen especificada como parámetro, + comparando el tamaño y el hash MD5 del último MB con los valores almacenados en los archivos "size" y "sum". + Para ello, ejecuta el script "checkImage.py", con el nombre de la imagen como único parámetro, + incluyendo el subdirectorio correspondiente a la OU (si fuera el caso). + """ + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(imageId, "repo") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd = ['sudo', 'python3', f"{script_path}/checkImage.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}"] + else: + cmd = ['sudo', 'python3', f"{script_path}/checkImage.py", f"{param_dict['name']}.{param_dict['extension']}"] + else: + return jsonify({ + "success": False, + "error": "Image not found (inexistent or deleted)" + }), 400 + + try: + # Ejecutamos el script "checkImage.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos la respuesta: + if result.returncode == 0: + if "Error" in result.stdout: + return jsonify({ + "success": True, + "output": "Image file didn't pass the Integrity Check" + }), 200 + else: + return jsonify({ + "success": True, + "output": "Image file passed the Integrity Check correctly" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 6 - Endpoint "Eliminar una Imagen": +@app.route("/ogrepository/v1/images/", methods=['DELETE']) +def delete_image(imageId): + """ Este endpoint elimina la imagen especificada como parámetro (y todos sus archivos asociados), + moviéndolos a la papelera o eliminándolos permanentemente (dependiendo del parámetro "method"). + Para ello, ejecuta el script "deleteImage.py", con el nombre de la imagen como primer parámetro, + incluyendo el subdirectorio correspondiente a la OU (si fuera el caso), y el parámetro opcional "-p". + """ + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(imageId, "repo") + # Almacenams el método de eliminación ("trash" o "permanent") + method = request.values.get('method') + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + if method == "trash": + cmd = ['sudo', 'python3', f"{script_path}/deleteImage.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}"] + else: + cmd = ['sudo', 'python3', f"{script_path}/deleteImage.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}", '-p'] + else: + if method == "trash": + cmd = ['sudo', 'python3', f"{script_path}/deleteImage.py", f"{param_dict['name']}.{param_dict['extension']}"] + else: + cmd = ['sudo', 'python3', f"{script_path}/deleteImage.py", f"{param_dict['name']}.{param_dict['extension']}", '-p'] + else: + return jsonify({ + "success": False, + "error": "Image not found (inexistent or deleted)" + }), 400 + + try: + # Ejecutamos el script "deleteImage.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "Image deleted successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 7 - Endpoint "Recuperar una Imagen": +@app.route("/ogrepository/v1/trash/images", methods=['POST']) +def recover_image(): + """ Este endpoint recupera la imagen especificada como parámetro (y todos sus archivos asociados), + moviéndolos nuevamente al repositorio de imágenes, desde la papelera. + Para ello, ejecuta el script "recoverImage.py", con el nombre de la imagen como único parámetro, + incluyendo el subdirectorio correspondiente a la OU (si fuera el caso). + """ + # Almacenamos el parámetro "ID_img" (enviado por JSON): + json_data = json.loads(request.data) + image_id = json_data.get("ID_img") + + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(image_id, "trash") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd = ['sudo', 'python3', f"{script_path}/recoverImage.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}"] + else: + cmd = ['sudo', 'python3', f"{script_path}/recoverImage.py", f"{param_dict['name']}.{param_dict['extension']}"] + else: + return jsonify({ + "success": False, + "error": "Image not found (inexistent or recovered previously)" + }), 400 + + try: + # Ejecutamos el script "recoverImage.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "Image recovered successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 8 - Endpoint "Eliminar una Imagen de la Papelera": +@app.route("/ogrepository/v1/trash/images/", methods=['DELETE']) +def delete_trash_image(imageId): + """ Este endpoint elimina permanentemente la imagen especificada como parámetro (y todos sus archivos asociados), desde la papelera. + Para ello, ejecuta el script "deleteTrashImage.py", con el nombre de la imagen como único parámetro, + incluyendo el subdirectorio correspondiente a la OU (si fuera el caso). + """ + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(imageId, "trash") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd = ['sudo', 'python3', f"{script_path}/deleteTrashImage.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}"] + else: + cmd = ['sudo', 'python3', f"{script_path}/deleteTrashImage.py", f"{param_dict['name']}.{param_dict['extension']}"] + else: + return jsonify({ + "success": False, + "error": "Image not found at trash" + }), 400 + + try: + # Ejecutamos el script "deleteTrashImage.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "Image deleted successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 9 - Endpoint "Importar una Imagen": +@app.route("/ogrepository/v1/repo/images", methods=['POST']) +def import_image(): + """ Este endpoint importa la imagen especificada como primer parámetro (y todos sus archivos asociados), + desde un servidor remoto al servidor local. + Para ello, ejecuta el script "importImage.py", con el nombre de la imagen como primer parámetro (con subdirectorio de OU, si es el caso), + la IP o hostname del servidor remoto como segundo parámetro, y el usuario con el que conectar al servidor como tercer parámetro. + """ + # Almacenamos los parámetros enviados en el JSON: + json_data = json.loads(request.data) + image_name = json_data.get("image") + ou_subdir = json_data.get("ou_subdir") + remote_ip = json_data.get("repo_ip") + remote_user = json_data.get("user") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script: + if ou_subdir == "none": + cmd = ['sudo', 'python3', f"{script_path}/importImage.py", f"{repo_path}{image_name}", remote_ip, remote_user] + else: + cmd = ['sudo', 'python3', f"{script_path}/importImage.py", f"{repo_path}{ou_subdir}/{image_name}", remote_ip, remote_user] + + try: + # Ejecutamos el script "importImage.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "Image imported successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + if "exit status 2" in str(error_description): + return jsonify({ + "success": False, + "exception": "Can't connect to remote server" + }), 400 + elif "exit status 3" in str(error_description): + return jsonify({ + "success": False, + "exception": "Remote image not found" + }), 400 + elif "exit status 4" in str(error_description): + return jsonify({ + "success": False, + "exception": "Remote image is locked" + }), 400 + else: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 10 - Endpoint "Exportar una Imagen": +@app.route("/ogrepository/v1/repo/images", methods=['PUT']) +def export_image(): + """ Este endpoint exporta la imagen especificada como primer parámetro (y todos sus archivos asociados), + desde el servidor local a un servidor remoto. + Para ello, ejecuta el script "exportImage.py", con el nombre de la imagen como primer parámetro (con subdirectorio de OU, si es el caso), + la IP o hostname del servidor remoto como segundo parámetro, y el usuario con el que conectar al servidor como tercer parámetro. + """ + # Almacenamos los parámetros enviados en el JSON: + json_data = json.loads(request.data) + image_id = json_data.get("ID_img") + remote_ip = json_data.get("repo_ip") + remote_user = json_data.get("user") + + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(image_id, "repo") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd = ['sudo', 'python3', f"{script_path}/exportImage.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}", remote_ip, remote_user] + else: + cmd = ['sudo', 'python3', f"{script_path}/exportImage.py", f"{param_dict['name']}.{param_dict['extension']}", remote_ip, remote_user] + else: + return jsonify({ + "success": False, + "error": "Image not found" + }), 400 + + try: + # Ejecutamos el script "exportImage.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "Image exported successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + if "exit status 3" in str(error_description): + return jsonify({ + "success": False, + "exception": "Image is locked" + }), 400 + elif "exit status 4" in str(error_description): + return jsonify({ + "success": False, + "exception": "Can't connect to remote server" + }), 400 + elif "exit status 5" in str(error_description): + return jsonify({ + "success": False, + "exception": "Image already exists on remote server" + }), 400 + else: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 11 - Endpoint "Crear archivos auxiliares": +@app.route("/ogrepository/v1/images/torrentsum", methods=['POST']) +def create_torrent_sum(): + """ Este endpoint crea los archivos ".size", ".sum", ".full.sum" y ".torrent" para la imagen que recibe como parámetro. + Para ello, ejecuta el script "createTorrentSum.py", con el nombre de la imagen como único parámetro (con subdirectorio de OU, si es el caso). + """ + # Almacenamos los parámetros enviados en el JSON: + json_data = json.loads(request.data) + image_name = json_data.get("image") + ou_subdir = json_data.get("ou_subdir") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script: + if ou_subdir == "none": + cmd = ['sudo', 'python3', f"{script_path}/createTorrentSum.py", image_name] + else: + cmd = ['sudo', 'python3', f"{script_path}/createTorrentSum.py", f"{ou_subdir}/{image_name}"] + + try: + # Ejecutamos el script "createTorrentSum.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "Files created successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + if "exit status 2" in str(error_description): + return jsonify({ + "success": False, + "exception": "Image not found" + }), 400 + elif "exit status 3" in str(error_description): + return jsonify({ + "success": False, + "exception": "Image is locked" + }), 400 + else: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 12 - Endpoint "Enviar paquete Wake On Lan": +@app.route("/ogrepository/v1/wol", methods=['POST']) +def send_wakeonlan(): + """ Este endpoint envía un paquete mágico Wake On Lan a la dirección MAC especificada, a través de la IP de broadcast especificadac. + Para ello, ejecuta el script "sendWakeOnLan.py", con la IP de broadcast como primer parámetro, y la MAC como segundo parámetro. + """ + # Almacenamos los parámetros enviados en el JSON: + json_data = json.loads(request.data) + broadcast_ip = json_data.get("broadcast_ip") + mac = json_data.get("mac") + + try: + # Ejecutamos el script "sendWakeOnLan.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(['sudo', 'python3', f"{script_path}/sendWakeOnLan.py", broadcast_ip, mac], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "Wake On Lan packet sended successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 13 - Endpoint "Enviar una Imagen mediante UDPcast": +@app.route("/ogrepository/v1/udpcast", methods=['POST']) +def send_udpcast(): + """ Este endpoint envía mediante UDPcast la imagen que recibe como primer parámetro, con los datos de transferencia que recibe en los demás parámetros. + Para ello, ejecuta el script "sendFileMcast.py", con la imagen como primer parámetro, y los demás en una cadena (como segundo parámetro). + """ + # Almacenamos los parámetros enviados en el JSON: + json_data = json.loads(request.data) + image_id = json_data.get("ID_img") + port = json_data.get("port") + method = json_data.get("method") + ip = json_data.get("ip") + bitrate = json_data.get("bitrate") + nclients = json_data.get("nclients") + maxtime = json_data.get("maxtime") + + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(image_id, "repo") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd = ['sudo', 'python3', f"{script_path}/sendFileMcast.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}", f"{port}:{method}:{ip}:{bitrate}:{nclients}:{maxtime}"] + else: + cmd = ['sudo', 'python3', f"{script_path}/sendFileMcast.py", f"{param_dict['name']}.{param_dict['extension']}", f"{port}:{method}:{ip}:{bitrate}:{nclients}:{maxtime}"] + else: + return jsonify({ + "success": False, + "error": "Image not found" + }), 400 + + try: + # Ejecutamos el script "sendFileMcast.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: # NOTA: Devolverá "0" cuando finalize la transmisión, lo que finalizará también el proceso asociado, + return jsonify({ # pero si ningún cliente se conecta, el proceso quedará corriendo, y el script no devolverá nada. + "success": True, # Esto podría paliarse utilizando el parámetro "--autostart", pero creo que no será necesario. + "output": "Image sended successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 14 - Endpoint "Enviar una Imagen mediante UFTP": +@app.route("/ogrepository/v1/uftp", methods=['POST']) +def send_uftp(): + """ Este endpoint envía mediante UFTP la imagen que recibe como primer parámetro, con los datos de transferencia que recibe en los demás parámetros. + Para ello, ejecuta el script "sendFileUFTP.py", con la imagen como primer parámetro, y los demás en una cadena (como segundo parámetro). + NOTA: Es necesario que los clientes se hayan puesto en escucha previamente (ejecutando el script "listenUFTPD.py"). + """ + # Almacenamos los parámetros enviados en el JSON: + json_data = json.loads(request.data) + image_id = json_data.get("ID_img") + port = json_data.get("port") + ip = json_data.get("ip") + bitrate = json_data.get("bitrate") + + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(image_id, "repo") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd = ['sudo', 'python3', f"{script_path}/sendFileUFTP.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}", f"{port}:{ip}:{bitrate}"] + else: + cmd = ['sudo', 'python3', f"{script_path}/sendFileUFTP.py", f"{param_dict['name']}.{param_dict['extension']}", f"{port}:{ip}:{bitrate}"] + else: + return jsonify({ + "success": False, + "error": "Image not found" + }), 400 + + try: + # Ejecutamos el script "sendFileUFTP.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: # NOTA: Devolverá "0" cuando finalize la transmisión, lo que finalizará también el proceso asociado, + return jsonify({ # pero si ningún cliente se conecta, el proceso finalizará automáticamente (y tambien devolverá "0""). + "success": True, + "output": "Image sended successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 15 - Endpoint "Enviar una Imagen mediante P2P": +@app.route("/ogrepository/v1/p2p", methods=['POST']) +def send_p2p(): + """ Este endpoint inicia el tracker "bttrack" y el seeder "bittornado", en el directorio en el que esté situada la imagen que recibe como parámetro. + Para ello, ejecuta los scripts "runTorrentTracker.py" y "runTorrentSeeder.py", pasándoles el subdirectorio de OU de la imagen (o "none" si no es el caso). + """ + # Almacenamos los parámetros enviados en el JSON: + json_data = json.loads(request.data) + image_id = json_data.get("ID_img") + + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(image_id, "repo") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd_tracker = ['sudo', 'python3', f"{script_path}/runTorrentTracker.py", param_dict['subdir']] + cmd_seeder = ['sudo', 'python3', f"{script_path}/runTorrentSeeder.py", param_dict['subdir']] + base_path = f"{repo_path}{param_dict['subdir']}" + else: + cmd_tracker = ['sudo', 'python3', f"{script_path}/runTorrentTracker.py", 'none'] + cmd_seeder = ['sudo', 'python3', f"{script_path}/runTorrentSeeder.py", 'none'] + base_path = repo_path.rstrip('/') # Le quito la última barra para poder buscar correctamente en los procesos + else: + return jsonify({ + "success": False, + "error": "Image not found" + }), 400 + + # Ejecutamos los scripts "runTorrentSeeder.py" y "runTorrentSeeder.py" (con los parámetros almacenados). + # NOTA: No almacenamos la salida ni comprobamos los errores, porque los procesos quedarán corriendo hasta que se finalicen manualmente, + # por lo que no podemos comprobar el returncode (luego comprobaremos si los procesos se han iniciado correctamente). + subprocess.Popen(cmd_tracker) + subprocess.Popen(cmd_seeder) + + # Comprobamos si el tracker y el seeder están corriendo, y si apuntan al directorio que le hemos pasado + # (esperamos 10 segundos antes de hacerlo, porque los procesos no se inician inmediatamente): + sleep(10) + tracker_running = search_process('bttrack', base_path) + seeder_running = search_process('btlaunchmany', base_path) + + # Evaluamos las comprobaciones anteriores, para devolver la respuesta que corresponda: + if tracker_running and seeder_running: + return jsonify({ + "success": True, + "output": "Tracker and Seeder serving image correctly" + }), 200 + else: + return jsonify({ + "success": False, + "error": "Tracker or Seeder (or both) not running" + }), 500 + + +# --------------------------------------------------------- + + +# 16 - Endpoint "Ver Estado de Transmisiones UDPcast": +@app.route("/ogrepository/v1/udpcast", methods=['GET']) +def get_udpcast_info(): + """ Este endpoint devuelve información sobre los procesos de "udp-sender" activos, en formato json, + lo que en la práctica permite comprobar las transferencias UDPcast activas. + Para ello, ejecuta el script "getUDPcastInfo.py", que no recibe parámetros. + """ + try: + # Ejecutamos el script "getUDPcastInfo.py", y almacenamos el resultado: + result = subprocess.run(['sudo', 'python3', f"{script_path}/getUDPcastInfo.py"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos la respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": json.loads(result.stdout) + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + if "exit status 1" in str(error_description): + return jsonify({ + "success": False, + "exception": "No UDPCast active transmissions" + }), 400 + elif "exit status 2" in str(error_description): + return jsonify({ + "success": False, + "exception": "Unexpected error checking UDPcast transmissions" + }), 500 + else: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 17 - Endpoint "Cancelar Transmisión UDPcast": +@app.route("/ogrepository/v1/udpcast/images/", methods=['DELETE']) +def stop_udpcast(imageId): + """ Este endpoint cancela la transmisión UDPcast de la imagen que recibe como parámetro, finalizando el proceso "udp-sender" asociado. + Para ello, ejecuta el script "stopUDPcast.py", pasándole el nombre de la imagen (con subdirectorio de OU, si procede). + """ + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(imageId, "repo") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd = ['sudo', 'python3', f"{script_path}/stopUDPcast.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}"] + else: + cmd = ['sudo', 'python3', f"{script_path}/stopUDPcast.py", f"{param_dict['name']}.{param_dict['extension']}"] + else: + return jsonify({ + "success": False, + "error": "Image not found" + }), 400 + + try: + # Ejecutamos el script "stopUDPcast.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "Image transmission canceled successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + if "exit status 3" in str(error_description): + return jsonify({ + "success": False, + "exception": "No UDPCast active transmissions for specified image" + }), 400 + elif "exit status 4" in str(error_description): + return jsonify({ + "success": False, + "exception": "Unexpected error checking UDPcast transmissions" + }), 500 + elif "exit status 5" in str(error_description): + return jsonify({ + "success": False, + "exception": "Unexpected error finalizing UDPcast transmission" + }), 500 + else: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 18 - Endpoint "Cancelar Transmisiones P2P": +@app.route("/ogrepository/v1/p2p", methods=['DELETE']) +def stop_p2p(): + """ Este endpoint cancela las transmisiones P2P activas, finalizando los procesos "btlaunchmany.bittornado" (seeder) y "bttrack" (tracker). + Para ello, ejecuta el script "stopP2P.py", que no recibe parámetros. + """ + try: + # Ejecutamos el script "stopP2P.py", y almacenamos el resultado: + result = subprocess.run(['sudo', 'python3', f"{script_path}/stopP2P.py"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos la respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "P2P transmissions canceled successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + + +# -------------------------------------------------------------------------------------------- + + +# Ejecutamos la aplicación, en el puerto "8006": +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=8006) + + +# -------------------------------------------------------------------------------------------- diff --git a/bin/OLD/udp-sender_20120424 b/bin/OLD/udp-sender_20120424 new file mode 100644 index 0000000000000000000000000000000000000000..87789a05889ab87dcd595269551ba66b76579ce1 GIT binary patch literal 56836 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 X6aB??k|v6c@BO`D`~zk26Vv@S9}9}= literal 0 HcmV?d00001 diff --git a/bin/getUDPcastInfo.py b/bin/getUDPcastInfo.py index 72dc4a9..5fa272f 100644 --- a/bin/getUDPcastInfo.py +++ b/bin/getUDPcastInfo.py @@ -16,6 +16,7 @@ En la práctica, permite comprobar las transminisones UDPcast activas, porque cu import subprocess import json +import sys # -------------------------------------------------------------------------------------------- @@ -31,7 +32,7 @@ repo_path = '/opt/opengnsys/images/' def get_udpsender_processes(): - """ Busca procesos de "udp-sender", y si los encuentra retorna el pid y la imagen asociada de cada uno de ellos, en un diccionario. + """ Busca procesos de "udp-sender", y si los encuentra retorna el pid, el ID, y la imagen asociada de cada uno de ellos, en un diccionario. Si no encuentra ningun proceso, o si se produce un error, retorna un mensaje. """ try: @@ -44,12 +45,17 @@ def get_udpsender_processes(): # Si hemos encontrado procesos de udp-sender creamos un diccionario para almacenarlos: if process_list != []: result_dict = {} - # Iteramos los procesos y extraemos el pid y el nombre de la imagen de cada uno, - # los almacenamos en el diccionario, y retornamos este: + # Iteramos los procesos y extraemos el pid, el nombre de la imagen (con subdirectorio de OU, si es el caso), y la ruta de la imagen de cada uno: for process in process_list: pid = process.split()[1] - image = process.split(repo_path)[1] - result_dict[pid] = {'image':image} + image_name = process.split(repo_path)[1] + image_path = process.split('--file ')[1] + # Obtenemos el ID de la imagen actual: + with open(f"{image_path}.full.sum", 'r') as file: + image_id = file.read().strip('\n') + # Creamos una clave en el diccionario de resultados, correspondiente a la imagen actual: + result_dict[pid] = {'image_id':image_id, 'image_name':image_name} + # Retornamos el diccionario de resultados: return result_dict # Si no hemos encontrado procesos de udp-sender retrornamos un mensaje: else: @@ -71,11 +77,13 @@ def main(): # Obtenemos información sobre los procesos de udp-sender: results = get_udpsender_processes() - # Si no hay procesos activos, o si se ha producido un error, imprimimos un mensaje explicativo: + # Si no hay procesos activos, o si se ha producido un error, imprimimos un mensaje explicativo, y salimos del script: if results == "udp-sender process not found": print("No UDPcast active transmissions") + sys.exit(1) elif results == "Unexpected error": print("Unexpected error checking UDPcast transmissions") + sys.exit(2) # Si hay procesos activos, convertimos el diccionario de resultados a JSON, e imprimimos este: else: json_data = json.dumps(results, indent=4) diff --git a/bin/udp-sender b/bin/udp-sender index 87789a05889ab87dcd595269551ba66b76579ce1..4c5bbd51d82cc7b3c2b371bc8438540fd95e047f 100644 GIT binary patch literal 70304 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 X6aB??k|v6c@BO`D`~zk26Vv@S9}9}= -- 2.40.1 From 126aa3cf8f55f3b956b9409493d0da841ad581a0 Mon Sep 17 00:00:00 2001 From: ggil Date: Mon, 21 Oct 2024 17:27:46 +0200 Subject: [PATCH 40/70] refs #631 - Add 'getUFTPInfo.py', 'stopUFTP.py' and associated endpoints --- README.md | 62 ++++++++++++++-- api/README.md | 62 ++++++++++++++-- api/repo_api.py | 114 +++++++++++++++++++++++++++-- bin/getUFTPInfo.py | 99 +++++++++++++++++++++++++ bin/sendFileUFTP.py | 17 +++-- bin/stopUFTP.py | 172 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 509 insertions(+), 17 deletions(-) create mode 100644 bin/getUFTPInfo.py create mode 100644 bin/stopUFTP.py diff --git a/README.md b/README.md index 7688f4a..2bc905c 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,15 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará 14. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/uftp` 15. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/p2p` 16. [Ver Estado de Transmisiones UDPcast](#ver-estado-de-transmisiones-udpcast) - `GET /ogrepository/v1/udpcast` -17. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}` -18. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p` +17. [Ver Estado de Transmisiones UFTP](#ver-estado-de-transmisiones-uftp) - `GET /ogrepository/v1/uftp` +18. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}` +19. [Cancelar Transmisión UFTP](#cancelar-transmisión-uftp) - `DELETE /ogrepository/v1/uftp/images/{ID_img}` +20. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p` --- ### Obtener Información de Estado de ogRepository -Se devolverá informacion de CPU, memoria RAM, disco duro y el estado de ciertos servicios y procesos de ogRepository, en formato JSON. +Se devolverá informacion de CPU, memoria RAM, disco duro y el estado de ciertos servicios y procesos de ogRepository, en formato JSON. Se puede utilizar el script "**getRepoStatus.py**, que debe ser llamado por el endpoint. **NOTA**: En los apartados "services" y "processes" he especificado los servicios y procesos que me han parecido interesantes, pero se puede añadir o eliminar los que se desee. @@ -562,6 +564,37 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/udpc } ``` --- +### Ver Estado de Transmisiones UFTP + +Se devolverá el pid de los procesos de transferencias UFTP activas, y sus imágenes asociadas (con nombre e ID), en formato JSON, o un mensaje informativo si no hay procesos activos, o si se produce un error. +Se puede hacer con el script "**getUFTPInfo.py**", que debe ser llamado por el endpoint. + +**URL:** `/ogrepository/v1/uftp` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/uftp +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al comprobar las transmisiones UFTP. +- **Código 400 Bad Request:** No se han encontrado transmisiones UFTP activas. +- **Código 200 OK:** La información de las transmisiones UFTP activas se obtuvo exitosamente. + - **Contenido:** Información de las transmisiones UFTP activas en formato JSON. + ```json + { + "3427": { + "image_id": "22735b9070e4a8043371b8c6ae52b90d", + "image_name": "Ubuntu20.img" + }, + "4966": { + "image_id": "9e7cd32c606ebe5bd39ba212ce7aeb02", + "image_name": "Windows10.img" + } + } + ``` +--- ### Cancelar Transmisión UDPcast Se cancelará la transmisión por UDPcast activa de la imagen especificada como parámetro, deteniendo el proceso "udp-sender" asociado a dicha imagen. @@ -579,9 +612,30 @@ curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/u **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al cancelar la transmisión UDPcast. - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. -- **Código 400 Bad Request:** No hay transmisiones UCPcast activas para la imagen especificada. +- **Código 400 Bad Request:** No hay transmisiones UDPcast activas para la imagen especificada. - **Código 200 OK:** La transmisión UDPcast se ha cancelado exitosamente. +--- +### Cancelar Transmisión UFTP + +Se cancelará la transmisión por UFTP activa de la imagen especificada como parámetro, deteniendo el proceso "uftp" asociado a dicha imagen. +Se puede hacer con el script "**stopUFTP.py**", que debe ser llamado por el endpoint. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Este dato se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"). + +**URL:** `/ogrepository/v1/uftp/images/{ID_img}` +**Método HTTP:** DELETE + +**Ejemplo de Solicitud:** + +```bash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/uftp/images/{ID_img} +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al cancelar la transmisión UFTP. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 400 Bad Request:** No hay transmisiones UFTP activas para la imagen especificada. +- **Código 200 OK:** La transmisión UFTP se ha cancelado exitosamente. + --- ### Cancelar Transmisiones P2P diff --git a/api/README.md b/api/README.md index 2f6e0ef..5d6deaa 100644 --- a/api/README.md +++ b/api/README.md @@ -24,13 +24,15 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará 14. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/uftp` 15. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/p2p` 16. [Ver Estado de Transmisiones UDPcast](#ver-estado-de-transmisiones-udpcast) - `GET /ogrepository/v1/udpcast` -17. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}` -18. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p` +17. [Ver Estado de Transmisiones UFTP](#ver-estado-de-transmisiones-uftp) - `GET /ogrepository/v1/uftp` +18. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}` +19. [Cancelar Transmisión UFTP](#cancelar-transmisión-uftp) - `DELETE /ogrepository/v1/uftp/images/{ID_img}` +20. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p` --- ### Obtener Información de Estado de ogRepository -Se devolverá informacion de CPU, memoria RAM, disco duro y el estado de ciertos servicios y procesos de ogRepository, en formato JSON. +Se devolverá informacion de CPU, memoria RAM, disco duro y el estado de ciertos servicios y procesos de ogRepository, en formato JSON. Se puede utilizar el script "**getRepoStatus.py**, que debe ser llamado por el endpoint. **NOTA**: En los apartados "services" y "processes" he especificado los servicios y procesos que me han parecido interesantes, pero se puede añadir o eliminar los que se desee. @@ -549,6 +551,37 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/udpc } ``` --- +### Ver Estado de Transmisiones UFTP + +Se devolverá el pid de los procesos de transferencias UFTP activas, y sus imágenes asociadas (con nombre e ID), en formato JSON, o un mensaje informativo si no hay procesos activos, o si se produce un error. +Se puede hacer con el script "**getUFTPInfo.py**", que debe ser llamado por el endpoint. + +**URL:** `/ogrepository/v1/uftp` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/uftp +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al comprobar las transmisiones UFTP. +- **Código 400 Bad Request:** No se han encontrado transmisiones UFTP activas. +- **Código 200 OK:** La información de las transmisiones UFTP activas se obtuvo exitosamente. + - **Contenido:** Información de las transmisiones UFTP activas en formato JSON. + ```json + { + "3427": { + "image_id": "22735b9070e4a8043371b8c6ae52b90d", + "image_name": "Ubuntu20.img" + }, + "4966": { + "image_id": "9e7cd32c606ebe5bd39ba212ce7aeb02", + "image_name": "Windows10.img" + } + } + ``` +--- ### Cancelar Transmisión UDPcast Se cancelará la transmisión por UDPcast activa de la imagen especificada como parámetro, deteniendo el proceso "udp-sender" asociado a dicha imagen. @@ -566,9 +599,30 @@ curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/u **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al cancelar la transmisión UDPcast. - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. -- **Código 400 Bad Request:** No hay transmisiones UCPcast activas para la imagen especificada. +- **Código 400 Bad Request:** No hay transmisiones UDPcast activas para la imagen especificada. - **Código 200 OK:** La transmisión UDPcast se ha cancelado exitosamente. +--- +### Cancelar Transmisión UFTP + +Se cancelará la transmisión por UFTP activa de la imagen especificada como parámetro, deteniendo el proceso "uftp" asociado a dicha imagen. +Se puede hacer con el script "**stopUFTP.py**", que debe ser llamado por el endpoint. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Este dato se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"). + +**URL:** `/ogrepository/v1/uftp/images/{ID_img}` +**Método HTTP:** DELETE + +**Ejemplo de Solicitud:** + +```bash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/uftp/images/{ID_img} +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al cancelar la transmisión UFTP. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 400 Bad Request:** No hay transmisiones UFTP activas para la imagen especificada. +- **Código 200 OK:** La transmisión UFTP se ha cancelado exitosamente. + --- ### Cancelar Transmisiones P2P diff --git a/api/repo_api.py b/api/repo_api.py index 4757709..cb21de6 100644 --- a/api/repo_api.py +++ b/api/repo_api.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ - API de ogRepository, programada en Flask. + API de ogRepository, programada en Flask. Responde a peticiones HTTP (en principio, enviadas desde ogCore) mediante endpoints, que a su vez ejecutan los scripts Python almacenados en ogRepo. En ciertos casos, transforma los parámetros recibidos desde el portal, para adaptarlos a los que es necesario enviar a los scripts @@ -31,6 +31,7 @@ repo_file = '/opt/opengnsys/etc/repoinfo.json' trash_file = '/opt/opengnsys/etc/trashinfo.json' + # -------------------------------------------------------------------------------------------- # FUNCTIONS # -------------------------------------------------------------------------------------------- @@ -906,7 +907,50 @@ def get_udpcast_info(): # --------------------------------------------------------- -# 17 - Endpoint "Cancelar Transmisión UDPcast": +# 17 - Endpoint "Ver Estado de Transmisiones UFTP": +@app.route("/ogrepository/v1/uftp", methods=['GET']) +def get_uftp_info(): + """ Este endpoint devuelve información sobre los procesos de "uftp" activos, en formato json, + lo que en la práctica permite comprobar las transferencias UFTP activas. + Para ello, ejecuta el script "getUFTPInfo.py", que no recibe parámetros. + """ + try: + # Ejecutamos el script "getUFTPInfo.py", y almacenamos el resultado: + result = subprocess.run(['sudo', 'python3', f"{script_path}/getUFTPInfo.py"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos la respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": json.loads(result.stdout) + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + if "exit status 1" in str(error_description): + return jsonify({ + "success": False, + "exception": "No UFTP active transmissions" + }), 400 + elif "exit status 2" in str(error_description): + return jsonify({ + "success": False, + "exception": "Unexpected error checking UFTP transmissions" + }), 500 + else: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 18 - Endpoint "Cancelar Transmisión UDPcast": @app.route("/ogrepository/v1/udpcast/images/", methods=['DELETE']) def stop_udpcast(imageId): """ Este endpoint cancela la transmisión UDPcast de la imagen que recibe como parámetro, finalizando el proceso "udp-sender" asociado. @@ -963,12 +1007,74 @@ def stop_udpcast(imageId): "success": False, "exception": str(error_description) }), 500 - + # --------------------------------------------------------- -# 18 - Endpoint "Cancelar Transmisiones P2P": +# 19 - Endpoint "Cancelar Transmisión UFTP": +@app.route("/ogrepository/v1/uftp/images/", methods=['DELETE']) +def stop_uftp(imageId): + """ Este endpoint cancela la transmisión UFTP de la imagen que recibe como parámetro, finalizando el proceso "uftp" asociado. + Para ello, ejecuta el script "stopUFTP.py", pasándole el nombre de la imagen (con subdirectorio de OU, si procede). + """ + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(imageId, "repo") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd = ['sudo', 'python3', f"{script_path}/stopUFTP.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}"] + else: + cmd = ['sudo', 'python3', f"{script_path}/stopUFTP.py", f"{param_dict['name']}.{param_dict['extension']}"] + else: + return jsonify({ + "success": False, + "error": "Image not found" + }), 400 + + try: + # Ejecutamos el script "stopUFTP.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "Image transmission canceled successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + if "exit status 3" in str(error_description): + return jsonify({ + "success": False, + "exception": "No UFTP active transmissions for specified image" + }), 400 + elif "exit status 4" in str(error_description): + return jsonify({ + "success": False, + "exception": "Unexpected error checking UFTP transmissions" + }), 500 + elif "exit status 5" in str(error_description): + return jsonify({ + "success": False, + "exception": "Unexpected error finalizing UFTP transmission" + }), 500 + else: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 20 - Endpoint "Cancelar Transmisiones P2P": @app.route("/ogrepository/v1/p2p", methods=['DELETE']) def stop_p2p(): """ Este endpoint cancela las transmisiones P2P activas, finalizando los procesos "btlaunchmany.bittornado" (seeder) y "bttrack" (tracker). diff --git a/bin/getUFTPInfo.py b/bin/getUFTPInfo.py new file mode 100644 index 0000000..a34ee21 --- /dev/null +++ b/bin/getUFTPInfo.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +Este script busca procesos activos de "uftp", y si encuentra alguno devuelve el pid y la imagen asociada de cada uno de ellos, en una estructura JSON. +Si no encuentra ninguno, o si se produce un error, imprime un mensaje informativo. +No recibe ningún parámetro. + +En la práctica, permite comprobar las transminisones UFTP activas, porque cuando finalizan también finaliza el proceso asociado. +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import subprocess +import json +import sys + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +repo_path = '/opt/opengnsys/images/' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def get_uftp_processes(): + """ Busca procesos de "uftp", y si los encuentra retorna el pid, el ID, y la imagen asociada de cada uno de ellos, en un diccionario. + Si no encuentra ningun proceso, o si se produce un error, retorna un mensaje. + """ + try: + # Obtenemos todos los procesos, y almacenamos la salida y los errores: + result = subprocess.Popen(['ps', '-aux'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + out, error = result.communicate() + + # Almacenamos en una lista los procesos que contengan "uftp": + process_list = [line for line in out.split('\n') if 'uftp' in line] + # Si hemos encontrado procesos de udp-sender creamos un diccionario para almacenarlos: + if process_list != []: + result_dict = {} + # Iteramos los procesos y extraemos el pid, el nombre de la imagen (con subdirectorio de OU, si es el caso), y la ruta de la imagen de cada uno: + for process in process_list: + pid = process.split()[1] + image_name = process.split(repo_path)[1] + image_path = process.split()[-1] + # Obtenemos el ID de la imagen actual: + with open(f"{image_path}.full.sum", 'r') as file: + image_id = file.read().strip('\n') + # Creamos una clave en el diccionario de resultados, correspondiente a la imagen actual: + result_dict[pid] = {'image_id':image_id, 'image_name':image_name} + # Retornamos el diccionario de resultados: + return result_dict + # Si no hemos encontrado procesos de uftp retornamos un mensaje: + else: + return "uftp process not found" + # Si se ha producido una excepción, retornamos un mensaje: + except Exception: + return "Unexpected error" + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Obtenemos información sobre los procesos "uftp": + results = get_uftp_processes() + + # Si no hay procesos activos, o si se ha producido un error, imprimimos un mensaje explicativo, y salimos del script: + if results == "uftp process not found": + print("No UFTP active transmissions") + sys.exit(1) + elif results == "Unexpected error": + print("Unexpected error checking UFTP transmissions") + sys.exit(2) + # Si hay procesos activos, convertimos el diccionario de resultados a JSON, e imprimimos este: + else: + json_data = json.dumps(results, indent=4) + print(json_data) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/sendFileUFTP.py b/bin/sendFileUFTP.py index c765b31..dabd726 100644 --- a/bin/sendFileUFTP.py +++ b/bin/sendFileUFTP.py @@ -4,7 +4,8 @@ """ Este script envía mediante UFTP la imagen recibida como primer parámetro, al puerto e IP (Multicast o Unicast) especificados en el segundo parámetro, a la velocidad de transferencia tambíén especificada en el segundo parámetro (la sintaxis de este parámetro es "Port:IP:Bitrate"). -Previamente, los clientes deben haberse puesto a escuchar en la IP Multicast correspondiente (tanto para envíos Multicast como para envíos Unicast). +Previamente, los clientes deben haberse puesto a escuchar con un proceso "uftpd" en la IP Multicast correspondiente (tanto para envíos Multicast como para envíos Unicast). +NOTA: La imagen se envía a la ruta de la caché de los clientes (que actualmente es "/opt/opengnsys/cache"). Paquetes APT requeridos: "uftp" (se puede instalar con "sudo apt install uftp"). @@ -49,6 +50,7 @@ import subprocess script_name = os.path.basename(__file__) repo_path = '/opt/opengnsys/images/' +cache_path = '/opt/opengnsys/cache' log_file = '/opt/opengnsys/log/uftp.log' @@ -137,11 +139,16 @@ def main(): # Obtenemos la ruta completa al archivo a enviar: file_path = build_file_path() - # Si el fichero no es accesible, devolvermos un error, y salimos del script: - if not os.path.isfile(file_path): - print(f"{script_name} Error: Fichero \"{file_path}\" no accesible") + # Si no existe el archivo de imagen, imprimimos un mensaje de error y salimos del script: + if not os.path.exists(file_path): + print("Image file doesn't exist") sys.exit(2) + # Si no existe el archivo de log, lo creamos: + if not os.path.exists(log_file): + print("Creating log file...") + open(log_file, "w").close() + # Almacenamos los elementos del segundo parámetro en variables (su formato es "puerto:ip:bitrate"): param_list = sys.argv[2].split(':') port, ip, bitrate = param_list @@ -150,7 +157,7 @@ def main(): bitrate = calculate_bitrate(bitrate) # Creamos una lista con el comando a enviar (esto es requerido por la función "subprocess.run"), e imprimimos el comando con espacios: - splitted_cmd = f"uftp -M {ip} -p {port} -L {log_file} -Y aes256-cbc -h sha256 -e rsa -c -K 1024 -R {bitrate} {file_path}".split() + splitted_cmd = f"uftp -M {ip} -p {port} -L {log_file} -o -D {cache_path} -Y aes256-cbc -h sha256 -e rsa -c -K 1024 -R {bitrate} {file_path}".split() print(f"Sending command: {' '.join(splitted_cmd)}") diff --git a/bin/stopUFTP.py b/bin/stopUFTP.py new file mode 100644 index 0000000..377f652 --- /dev/null +++ b/bin/stopUFTP.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +Este script finaliza el proceso "uftp" asociado a la imagen que recibe como parámetro, + lo que en la práctica hará que se cancele la transmisión existente de dicha imagen mediante UFTP. + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a cancelar su transmisión (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/images/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/images/ou_subdir/image1.img + + Sintaxis +---------- +./stopUDPcast.py [ou_subdir/]image_name|/image_path/image_name + + Ejemplos + --------- +./stopUDPcast.py image1.img +./stopUDPcast.py /opt/opengnsys/images/image1.img +./stopUDPcast.py ou_subdir/image1.img +./stopUDPcast.py /ou_subdir/image1.img +./stopUDPcast.py /opt/opengnsys/images/ou_subdir/image1.img +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/images/' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name + Ejemplo1: {script_name} image1.img + Ejemplo2: {script_name} /opt/opengnsys/images/image1.img + Ejemplo3: {script_name} ou_subdir/image1.img + Ejemplo4: {script_name} /ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/images/ou_subdir/image1.img + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 1 parámetro, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 2: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 parámetro") + show_help() + sys.exit(1) + + +def build_file_path(): + """ Construye la ruta completa del archivo a chequear + (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "repo_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(repo_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(repo_path): + file_path = os.path.join(repo_path, param_path) + else: + file_path = param_path + return file_path + + +def get_process_pid(file_path): + """ Busca un proceso que contenga "uftp" y la ruta de la imagen que recibe como parámetro, + y si lo encuentra almacena y retorna su pid asociado. + Si no encuentra ningún proceso que cumpla las condiciones (o si se produce una excepción) sale del script. + """ + try: + # Obtenemos todos los procesos, y almacenamos la salida y los errores: + result = subprocess.Popen(['ps', '-aux'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + out, error = result.communicate() + + # Almacenamos en una lista los procesos que contengan "uftp" y la imagen especificada como parámetro: + filtered_lines = [line for line in out.split('\n') if 'uftp' in line and file_path in line] + # Si hemos encontrado un proceso retornamos su pid, y si no imprimimos un mensaje de error y salimos del script: + if filtered_lines != []: + pid = filtered_lines[0].split()[1] + return pid + else: + print("uftp process not found") + sys.exit(3) + # Si se ha producido una excepción, imprimimos el error y salimos del script: + except Exception as error_description: + print(f"Unexpected error: {error_description}") + sys.exit(4) + + +def kill_uftp(pid): + """ Finaliza el proceso asociado al pid que recibe como parámetro, e imprime el return code. + Si se produce una excepción, imprime el error y sale del script. + """ + try: + # Finalizamos el proceso asociado al pid especificado como parámetro, e imprimimos el return code: + result = subprocess.run(f"kill {pid}".split(), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"ReturnCode: {result.returncode}") + # Si se ha producido una excepción, imprimimos el error y salimos del script: + except Exception as error_description: + print(f"Unexpected error: {error_description}") + sys.exit(5) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo de imagen: + file_path = build_file_path() + + # Si no existe el archivo de imagen, imprimimos un mensaje de error y salimos del script: + if not os.path.exists(file_path): + print("Image file doesn't exist") + sys.exit(2) + + # Obtenemos el pid del proceso "uftp" asociado a la imagen especificada: + pid = get_process_pid(file_path) + + # Finalizamos el proceso "uftp" encontrado: + kill_uftp(pid) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- -- 2.40.1 From 38b8ecd3981217d19ff7535117ce5f19ce3b4bed Mon Sep 17 00:00:00 2001 From: ggil Date: Fri, 25 Oct 2024 14:29:12 +0200 Subject: [PATCH 41/70] refs #631 - Some script modifies --- bin/getRepoInfo.py | 64 +++++++++++++++++++++++++----------------- bin/updateRepoInfo.py | 18 ++++++++---- bin/updateTrashInfo.py | 18 ++++++++---- 3 files changed, 62 insertions(+), 38 deletions(-) diff --git a/bin/getRepoInfo.py b/bin/getRepoInfo.py index 4897725..2c235bd 100644 --- a/bin/getRepoInfo.py +++ b/bin/getRepoInfo.py @@ -98,15 +98,17 @@ def get_image_info(repo_data, trash_data, image_name, image_ext): """ dictionary = "" # Buscamos la imagen en el repositorio, y si la encontramos creamos un diccionario con los datos: - for image in repo_data['images']: - if image['name'] == image_name and image['type'] == image_ext: - dictionary = {"directory": repo_data['directory'], - "images": [image]} + if repo_data != "": + for image in repo_data['images']: + if image['name'] == image_name and image['type'] == image_ext: + dictionary = {"directory": repo_data['directory'], + "images": [image]} # Buscamos la imagen en la papelera, y si la encontramos creamos un diccionario con los datos: - for image in trash_data['images']: - if image['name'] == image_name: - dictionary = {"directory": trash_data['directory'], - "images": [image]} + if trash_data != "": + for image in trash_data['images']: + if image['name'] == image_name: + dictionary = {"directory": trash_data['directory'], + "images": [image]} # Si hemos obtenido datos de la imagen, los pasamos a json y los imprmimos, # y si no, imprimimos un mensaje de error y salimos del script: if dictionary != "": @@ -124,19 +126,21 @@ def get_ou_image_info(repo_data, trash_data, image_name, image_ext, ou_subdir): """ dictionary = "" # Buscamos la OU y la imagen en el repositorio, y si los encontramos creamos un diccionario con los datos: - for ou in repo_data['ous']: - if ou['subdir'] == ou_subdir: - for image in ou['images']: - if image['name'] == image_name and image['type'] == image_ext: - dictionary = {"directory": repo_data['directory'], - "ous": [{"subdir": ou_subdir, "images": [image]}]} + if repo_data != "": + for ou in repo_data['ous']: + if ou['subdir'] == ou_subdir: + for image in ou['images']: + if image['name'] == image_name and image['type'] == image_ext: + dictionary = {"directory": repo_data['directory'], + "ous": [{"subdir": ou_subdir, "images": [image]}]} # Buscamos la OU y la imagen en la papelera, y si los encontramos creamos un diccionario con los datos: - for ou in trash_data['ous']: - if ou['subdir'] == ou_subdir: - for image in ou['images']: - if image['name'] == image_name: - dictionary = {"directory": trash_data['directory'], - "ous": [{"subdir": ou_subdir, "images": [image]}]} + if trash_data != "": + for ou in trash_data['ous']: + if ou['subdir'] == ou_subdir: + for image in ou['images']: + if image['name'] == image_name: + dictionary = {"directory": trash_data['directory'], + "ous": [{"subdir": ou_subdir, "images": [image]}]} # Si hemos obtenido datos de la imagen, los pasamos a json y los imprmimos, # y si no, imprimimos un mensaje de error y salimos del script: if dictionary != "": @@ -159,13 +163,21 @@ def main(): # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: check_params() - # Almacenamos la información de las imágenes del repositorio, en la variable "repo_data": - with open(repo_file, 'r') as file: - repo_data = json.load(file) + # Almacenamos la información de las imágenes del repositorio, en la variable "repo_data" + # (solo si el archivo tiene contenido, o dará error): + if os.path.getsize(repo_file) > 0: + with open(repo_file, 'r') as file: + repo_data = json.load(file) + else: + repo_data = "" - # Almacenamos la información de las imágenes de la papelera, en la variable "trash_data": - with open(trash_file, 'r') as file: - trash_data = json.load(file) + # Almacenamos la información de las imágenes de la papelera, en la variable "trash_data" + # (solo si el archivo tiene contenido, o dará error): + if os.path.getsize(trash_file) > 0: + with open(trash_file, 'r') as file: + trash_data = json.load(file) + else: + trash_data = "" # Dependiendo del valor de los parámetros, llamamos a la función correspondiente, para imprimir la información # (extrayendo el nombre, la extensión de la imagen, y/o la OU cuando se necesite): diff --git a/bin/updateRepoInfo.py b/bin/updateRepoInfo.py index 8beb3c6..dab0d65 100644 --- a/bin/updateRepoInfo.py +++ b/bin/updateRepoInfo.py @@ -163,9 +163,13 @@ def add_to_json(image_name, image_type, data, size, _sum, fullsum): "sum": _sum, "fullsum": fullsum } - # Almacenamos el contenido del archivo "repoinfo.json" en la variable "info_data": - with open(info_file, 'r') as file: - info_data = json.load(file) + # Almacenamos el contenido del archivo "repoinfo.json" en la variable "info_data" + # (y si no tiene contenido creamos la estructura básica): + if os.path.getsize(info_file) > 0: + with open(info_file, 'r') as file: + info_data = json.load(file) + else: + info_data = {"directory": repo_path, "images": [], "ous": []} # Comprobamos si las claves "info_data" (o sea, del archivo json) son las correctas: if set(info_data.keys()) == {"directory", "images", "ous"}: @@ -301,10 +305,12 @@ def main(): check_dirs() # Llamamos a la función "remove_from_json", para eliminar del archivo json las imágenes que fueron eliminadas del repositorio: - print("Removing deleted images...") - remove_from_json() + if os.path.getsize(info_file) > 0: + print("Removing deleted images...") + remove_from_json() - # Actualizamos la información de la papelera, ejecutando el script "updateTrashInfo.py": + # Actualizamos la información de la papelera, ejecutando el script "updateTrashInfo.py" + # (solo si el archivo tiene contenido, o dará error): print("Updating Trash Info...") update_trash_info() diff --git a/bin/updateTrashInfo.py b/bin/updateTrashInfo.py index eed6968..626fa44 100644 --- a/bin/updateTrashInfo.py +++ b/bin/updateTrashInfo.py @@ -174,9 +174,13 @@ def add_to_json(image_name, image_type, data, size, _sum, fullsum): "sum": _sum, "fullsum": fullsum } - # Almacenamos el contenido del archivo "trashinfo.json" en la variable "info_data": - with open(info_file, 'r') as file: - info_data = json.load(file) + # Almacenamos el contenido del archivo "trashinfo.json" en la variable "info_data" + # (y si no tiene contenido creamos la estructura básica): + if os.path.getsize(info_file) > 0: + with open(info_file, 'r') as file: + info_data = json.load(file) + else: + info_data = {"directory": trash_path, "images": [], "ous": []} # Comprobamos si las claves "info_data" (o sea, del archivo json) son las correctas: if set(info_data.keys()) == {"directory", "images", "ous"}: @@ -301,9 +305,11 @@ def main(): print("Checking dir images...") check_dirs() - # Llamamos a la función "remove_from_json", para eliminar del archivo json las imágenes que ya no están en la papelera: - print("Removing inexistent images...") - remove_from_json() + # Llamamos a la función "remove_from_json", para eliminar del archivo json las imágenes que ya no están en la papelera + # (solo si el archivo tiene contenido, o dará error): + if os.path.getsize(info_file) > 0: + print("Removing inexistent images...") + remove_from_json() -- 2.40.1 From 62e67b74373ead3d1b439fad6043fdda7bf7a2fb Mon Sep 17 00:00:00 2001 From: ggil Date: Mon, 28 Oct 2024 13:32:52 +0100 Subject: [PATCH 42/70] refs #631 - Modify 'repo_api.py' --- api/repo_api.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/api/repo_api.py b/api/repo_api.py index cb21de6..70a3d16 100644 --- a/api/repo_api.py +++ b/api/repo_api.py @@ -16,6 +16,7 @@ Librerías Python requeridas: Flask (se puede instalar con "sudo apt install pyt # -------------------------------------------------------------------------------------------- from flask import Flask, jsonify, request +import os import subprocess import json from time import sleep @@ -54,8 +55,8 @@ def get_image_params(image_id, search='all'): # Creamos un diccionario vacío, para almacenar los resultados: result = {} - # Abrimos y almacenamos el archivo "repoinfo.json" (solo si se ha de buscar en el repo): - if search == 'all' or search == 'repo': + # Abrimos y almacenamos el archivo "repoinfo.json" (solo si se ha de buscar en el repo, y si el archivo tiene contenido): + if (search == 'all' or search == 'repo') and os.path.getsize(repo_file) > 0: with open(repo_file, 'r') as file: repo_data = json.load(file) # Iteramos la clave "images" y buscamos la imagen (y si la encontramos almacenamos el nombre y la extension): @@ -73,8 +74,8 @@ def get_image_params(image_id, search='all'): result['subdir'] = ou.get('subdir') return result - # Abrimos y almacenamos el archivo "trashinfo.json" (solo si se ha de buscar en la papelera): - if search == 'all' or search == 'trash': + # Abrimos y almacenamos el archivo "trashinfo.json" (solo si se ha de buscar en la papelera, y si el archivo tiene contenido): + if (search == 'all' or search == 'trash') and os.path.getsize(trash_file) > 0: with open(trash_file, 'r') as file: trash_data = json.load(file) # Iteramos la clave "images" y buscamos la imagen (y si la encontramos almacenamos el nombre y la extension): -- 2.40.1 From 43846b038230c2983401c70eaf32c5e27bf26448 Mon Sep 17 00:00:00 2001 From: lgromero Date: Tue, 29 Oct 2024 13:15:32 +0100 Subject: [PATCH 43/70] refs #1084 Adds swagger documentation to all endpoints of ogrepository --- api/repo_api.py | 9 +- api/swagger.yaml | 1510 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1515 insertions(+), 4 deletions(-) create mode 100644 api/swagger.yaml diff --git a/api/repo_api.py b/api/repo_api.py index 70a3d16..fe80c9c 100644 --- a/api/repo_api.py +++ b/api/repo_api.py @@ -20,8 +20,8 @@ import os import subprocess import json from time import sleep - - +from flasgger import Swagger +import yaml # -------------------------------------------------------------------------------------------- # VARIABLES # -------------------------------------------------------------------------------------------- @@ -32,7 +32,6 @@ repo_file = '/opt/opengnsys/etc/repoinfo.json' trash_file = '/opt/opengnsys/etc/trashinfo.json' - # -------------------------------------------------------------------------------------------- # FUNCTIONS # -------------------------------------------------------------------------------------------- @@ -41,7 +40,9 @@ trash_file = '/opt/opengnsys/etc/trashinfo.json' # Creamos una instancia de la aplicación Flask: app = Flask(__name__) - +with open("swagger.yaml", "r") as file: + swagger_template = yaml.safe_load(file) +swagger = Swagger(app, template=swagger_template) # --------------------------------------------------------- diff --git a/api/swagger.yaml b/api/swagger.yaml new file mode 100644 index 0000000..8ac0a39 --- /dev/null +++ b/api/swagger.yaml @@ -0,0 +1,1510 @@ +swagger: "2.0" +info: + title: "Ogrepository API" + version: "1.0" + description: | + API de ogRepository, programada en Flask. + + Responde a peticiones HTTP (enviadas desde ogCore) mediante endpoints, que a su vez ejecutan los scripts Python almacenados en ogRepo. + En ciertos casos, transforma los parámetros recibidos desde el portal, para adaptarlos a los que es necesario enviar a los scripts + (por ejemplo, a partir del ID de una imagen obtiene su nombre, su extensión y el subdirectorio de OU). + + Librerías Python requeridas: Flask (se puede instalar con "sudo apt install python3-flask"). + +paths: + /ogrepository/v1/status: + get: + summary: "Obtener Información de Estado de ogRepository" + description: > + Este endpoint ejecuta el script "getRepoStatus.py" y devuelve su salida en formato JSON, + incluyendo información sobre la CPU, memoria RAM, disco duro, servicios, y procesos específicos de ogRepository. + tags: + - "Estado de ogRepository" + responses: + 200: + description: "La información de estado se obtuvo exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: object + properties: + cpu: + type: object + properties: + used_percentage: + type: string + example: "35%" + ram: + type: object + properties: + total: + type: string + example: "7.8GB" + used: + type: string + example: "0.3GB" + available: + type: string + example: "7.2GB" + used_percentage: + type: string + example: "7%" + disk: + type: object + properties: + total: + type: string + example: "11.7GB" + used: + type: string + example: "7.7GB" + available: + type: string + example: "3.4GB" + used_percentage: + type: string + example: "69%" + services: + type: object + properties: + ssh: + type: string + example: "active" + smbd: + type: string + example: "active" + rsync: + type: string + example: "active" + processes: + type: object + properties: + udp-sender: + type: string + example: "stopped" + uftp: + type: string + example: "stopped" + bttrack: + type: string + example: "stopped" + btlaunchmany: + type: string + example: "stopped" + 500: + description: "Error al consultar y/o devolver la información de estado." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Generic error description for unexpected exceptions." + + /ogrepository/v1/images: + get: + summary: "Obtener Información de todas las Imágenes" + description: > + Este endpoint ejecuta el script "getRepoInfo.py" con los parámetros "all" y "none" para devolver información + de todas las imágenes almacenadas en el repositorio y en la papelera. + Devuelve detalles como el nombre de la imagen, tipo, nombre del cliente, clonador, compresor, sistema de archivos, + tamaño de los datos, tamaño de la imagen, y hashes MD5. + tags: + - "Imágenes de Repositorio" + responses: + 200: + description: "La información de las imágenes se obtuvo exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: object + properties: + REPOSITORY: + type: object + properties: + directory: + type: string + example: "/opt/opengnsys/images" + images: + type: array + items: + type: object + properties: + name: + type: string + example: "Ubuntu24" + type: + type: string + example: "img" + clientname: + type: string + example: "Ubuntu_24" + clonator: + type: string + example: "partclone" + compressor: + type: string + example: "lzop" + filesystem: + type: string + example: "EXTFS" + datasize: + type: integer + example: 9859634200000 + size: + type: integer + example: 4505673214 + sum: + type: string + example: "065a933c780ab1aaa044435ad5d4bf87" + fullsum: + type: string + example: "33575b9070e4a8043371b8c6ae52b80e" + ous: + type: array + items: + type: object + properties: + subdir: + type: string + example: "OU_subdir" + images: + type: array + items: + type: object + properties: + name: + type: string + example: "Ubuntu20" + type: + type: string + example: "img" + clientname: + type: string + example: "Ubuntu_20" + clonator: + type: string + example: "partclone" + compressor: + type: string + example: "lzop" + filesystem: + type: string + example: "EXTFS" + datasize: + type: integer + example: 8912896000000 + size: + type: integer + example: 3803794535 + sum: + type: string + example: "081a933c780ab1aaa044435ad5d4bf56" + fullsum: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + TRASH: + type: object + properties: + directory: + type: string + example: "/opt/opengnsys/images_trash" + images: + type: array + items: + type: object + ous: + type: array + items: + type: object + properties: + subdir: + type: string + example: "CentroVirtual" + images: + type: array + items: + type: object + properties: + name: + type: string + example: "Ubuntu20OLD" + type: + type: string + example: "img" + clientname: + type: string + example: "Ubuntu_20" + clonator: + type: string + example: "partclone" + compressor: + type: string + example: "lzop" + filesystem: + type: string + example: "EXTFS" + datasize: + type: integer + example: 8912896000000 + size: + type: integer + example: 3803794535 + sum: + type: string + example: "081a933c780ab1aaa044435ad5d4bf56" + fullsum: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + 500: + description: "Error al consultar y/o devolver la información de las imágenes." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Generic error description for unexpected exceptions." + + put: + summary: "Actualizar Información del Repositorio" + description: > + Este endpoint actualiza la información de las imágenes almacenadas en el repositorio, reflejándola en los archivos "repoinfo.json" y "trashinfo.json". + Utiliza el script "updateRepoInfo.py", que también ejecuta "updateTrashInfo.py". + Este proceso se realiza para mantener actualizados los datos del repositorio y la papelera, y debería ser invocado cada vez que se elimine o cree una imagen. + tags: + - "Imágenes de Repositorio" + responses: + 200: + description: "La actualización de la información del repositorio se realizó exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Repository info updated successfully" + 500: + description: "Error al actualizar la información de las imágenes." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Generic error description for unexpected exceptions." + + /ogrepository/v1/images/{imageId}: + get: + summary: "Obtener Información de una Imagen concreta" + description: > + Este endpoint devuelve información de la imagen especificada mediante su ID en formato JSON. + Puede utilizar el script "getRepoInfo.py" que recibe como parámetros el nombre de la imagen con extensión y el subdirectorio correspondiente a la OU (o "none" si no es el caso). + La imagen puede estar en el archivo "/opt/opengnsys/etc/repoinfo.json" (si está almacenada) o en "/opt/opengnsys/etc/trashinfo.json" (si está en la papelera). + tags: + - "Imágenes de Repositorio" + parameters: + - name: imageId + in: path + required: true + type: string + description: "El ID de la imagen (corresponde al hash MD5 'fullsum' de la imagen)" + responses: + 200: + description: "La información de la imagen se obtuvo exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: object + properties: + directory: + type: string + example: "/opt/opengnsys/images" + images: + type: array + items: + type: object + properties: + name: + type: string + example: "Windows10" + type: + type: string + example: "img" + clientname: + type: string + example: "Windows_10" + clonator: + type: string + example: "partclone" + compressor: + type: string + example: "lzop" + filesystem: + type: string + example: "NTFS" + datasize: + type: integer + example: 9859634200000 + size: + type: integer + example: 4505673214 + sum: + type: string + example: "065a933c780ab1aaa044435ad5d4bf87" + fullsum: + type: string + example: "33575b9070e4a8043371b8c6ae52b80e" + 400: + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found" + 500: + description: "Error al consultar y/o devolver la información de la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Generic error description for unexpected exceptions." + delete: + summary: "Eliminar una Imagen" + description: > + Este endpoint elimina la imagen especificada como parámetro (y todos sus archivos asociados), + moviéndolos a la papelera o eliminándolos permanentemente dependiendo del parámetro "method". + Utiliza el script "deleteImage.py" que recibe el nombre de la imagen (con extensión y subdirectorio correspondiente a la OU, si aplica) + como primer parámetro, y opcionalmente el parámetro "-p" para eliminación permanente. + tags: + - "Imágenes de Repositorio" + parameters: + - name: imageId + in: path + required: true + type: string + description: "El ID de la imagen (corresponde al hash MD5 'fullsum' de la imagen)" + - name: method + in: query + required: false + type: string + description: "Método de eliminación (puede ser 'trash' para enviar a la papelera o 'permanent' para eliminar definitivamente)" + responses: + 200: + description: "La imagen se eliminó exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image deleted successfully" + 400: + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found (inexistent or deleted)" + 500: + description: > + Error al eliminar la imagen. Puede ocurrir debido a: + - Un error de ejecución del script (con salida no 0). + - Una excepción inesperada durante el proceso. + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Script execution error description." + 500 (Exception): + description: "Error inesperado durante la eliminación de la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Generic error description for unexpected exceptions." + + /ogrepository/v1/status/images/{imageId}: + get: + summary: "Chequear Integridad de Imagen" + description: > + Este endpoint comprueba la integridad de la imagen especificada como parámetro, comparando el tamaño y el hash MD5 del último MB con los valores almacenados en los archivos ".size" y ".sum". + Utiliza el script "checkImage.py", que recibe el nombre de la imagen (con extensión) y el subdirectorio correspondiente a la OU, si aplica. + tags: + - "Integridad de Imágenes" + parameters: + - name: imageId + in: path + required: true + type: string + description: "El ID de la imagen (corresponde al hash MD5 'fullsum' de la imagen)" + responses: + 200: + description: > + La imagen se ha chequeado exitosamente. El chequeo puede devolver dos resultados: + - Si pasa la verificación de integridad. + - Si no pasa la verificación de integridad. + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image file passed the Integrity Check correctly" + 400: + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found (inexistent or deleted)" + 500: + description: > + Error al chequear la imagen. Puede ocurrir debido a una excepción inesperada. + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Generic error description for unexpected exceptions." + 200 (KO): + description: "La imagen se ha chequeado correctamente, pero no ha pasado el test de integridad." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image file didn't pass the Integrity Check" + + /ogrepository/v1/trash/images: + post: + summary: "Recuperar una Imagen" + description: > + Este endpoint recupera la imagen especificada, moviéndola desde la papelera al repositorio de imágenes. + Utiliza el script "recoverImage.py", que recibe el nombre de la imagen (con extensión y subdirectorio correspondiente a la OU, si aplica). + tags: + - "Imágenes de Papelera" + parameters: + - name: ID_img + in: body + required: true + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum' asociado)" + schema: + type: object + properties: + ID_img: + type: string + example: "image_id" + responses: + 200: + description: "La imagen se recuperó exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image recovered successfully" + 400: + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found (inexistent or recovered previously)" + 500: + description: > + Error al recuperar la imagen. Puede ocurrir debido a: + - Un error de ejecución del script (con salida no 0). + - Una excepción inesperada durante el proceso. + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Script execution error description." + 500 (Exception): + description: "Error inesperado durante la recuperación de la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Generic error description for unexpected exceptions." + + /ogrepository/v1/trash/images/{imageId}: + delete: + summary: "Eliminar una Imagen de la Papelera" + description: > + Este endpoint elimina permanentemente la imagen especificada desde la papelera. + Utiliza el script "deleteTrashImage.py", que recibe el nombre de la imagen (con extensión y subdirectorio correspondiente a la OU, si aplica). + tags: + - "Imágenes de Papelera" + parameters: + - name: imageId + in: path + required: true + type: string + description: "El ID de la imagen (corresponde al hash MD5 'fullsum' de la imagen en la papelera)" + responses: + 200: + description: "La imagen se eliminó exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image deleted successfully" + 400: + description: "No se ha encontrado la imagen especificada en la papelera." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found at trash" + 500: + description: > + Error al eliminar la imagen desde la papelera. Puede ocurrir debido a: + - Un error de ejecución del script (con salida no 0). + - Una excepción inesperada durante el proceso. + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Script execution error description." + 500 (Exception): + description: "Error inesperado durante la eliminación de la imagen desde la papelera." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Generic error description for unexpected exceptions." + + /ogrepository/v1/repo/images: + post: + summary: "Importar una Imagen" + description: > + Este endpoint importa la imagen especificada desde un servidor remoto al servidor local. + Utiliza el script "importImage.py", que recibe como parámetros el nombre de la imagen, la IP o hostname del servidor remoto, + el usuario para la conexión, y el subdirectorio correspondiente a la OU (si aplica). + tags: + - "Imágenes de Repositorio" + parameters: + - name: image + in: body + required: true + description: "Nombre de la imagen (con extensión)" + schema: + type: object + properties: + image: + type: string + example: "Windows10.img" + ou_subdir: + type: string + description: "Subdirectorio correspondiente a la OU (o 'none' si no es el caso)" + example: "none" + repo_ip: + type: string + description: "Dirección IP del repositorio remoto" + example: "192.168.56.100" + user: + type: string + description: "Usuario para acceder al repositorio remoto" + example: "user_name" + responses: + 200: + description: "La imagen se ha importado exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image imported successfully" + 400: + description: "Error de conexión o imagen no disponible en el servidor remoto." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Can't connect to remote server | Remote image not found | Remote image is locked" + 500: + description: > + Error interno al importar la imagen. Puede ocurrir debido a un problema en la ejecución del script o una excepción inesperada. + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Script execution error description." + 500 (Exception): + description: "Error inesperado durante la importación de la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Generic error description for unexpected exceptions." + put: + summary: "Exportar una Imagen" + description: > + Este endpoint exporta la imagen especificada desde el servidor local a un servidor remoto. + Utiliza el script "exportImage.py", que recibe como parámetros el nombre de la imagen, la IP o hostname del servidor remoto, + el usuario para la conexión, y el subdirectorio correspondiente a la OU (si aplica). + tags: + - "Imágenes de Repositorio" + parameters: + - name: ID_img + in: body + required: true + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum' asociado)" + schema: + type: object + properties: + ID_img: + type: string + example: "image_id" + repo_ip: + type: string + description: "Dirección IP del repositorio remoto" + example: "192.168.56.100" + user: + type: string + description: "Usuario para acceder al repositorio remoto" + example: "user_name" + responses: + 200: + description: "La imagen se ha exportado exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image exported successfully" + 400: + description: "Error de conexión o imagen no disponible en el servidor remoto." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Image is locked | Can't connect to remote server | Image already exists on remote server" + 500: + description: > + Error interno al exportar la imagen. Puede ocurrir debido a un problema en la ejecución del script o una excepción inesperada. + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Script execution error description." + 500 (Exception): + description: "Error inesperado durante la exportación de la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Generic error description for unexpected exceptions." + /ogrepository/v1/images/torrentsum: + post: + summary: "Crear archivos auxiliares" + description: > + Este endpoint crea los archivos ".size", ".sum", ".full.sum" y ".torrent" para la imagen especificada. + Utiliza el script "createTorrentSum.py", que recibe como parámetro el nombre de la imagen (con subdirectorio de OU, si aplica). + tags: + - "Archivos Auxiliares de Imágenes" + parameters: + - name: image + in: body + required: true + description: "Nombre de la imagen (con extensión)" + schema: + type: object + properties: + image: + type: string + example: "Windows10.img" + ou_subdir: + type: string + description: "Subdirectorio correspondiente a la OU (o 'none' si no es el caso)" + example: "none" + responses: + 200: + description: "Los archivos se han creado exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Files created successfully" + 400: + description: "Error de conexión o imagen no disponible en el servidor remoto." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Image not found | Image is locked" + 500: + description: > + Error interno al crear los archivos auxiliares. Puede ocurrir debido a un problema en la ejecución del script o una excepción inesperada. + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Script execution error description." + 500 (Exception): + description: "Error inesperado durante la creación de los archivos auxiliares." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Generic error description for unexpected exceptions." + + /ogrepository/v1/wol: + post: + summary: "Enviar paquete Wake On Lan" + description: > + Este endpoint envía un paquete mágico Wake On Lan (WOL) a la dirección MAC especificada, a través de la IP de broadcast especificada. + Utiliza el script "sendWakeOnLan.py", que recibe como parámetros la IP de broadcast y la dirección MAC del equipo a encender. + tags: + - "Wake On Lan" + parameters: + - name: broadcast_ip + in: body + required: true + description: "IP de broadcast a la que se enviará el paquete WOL" + schema: + type: object + properties: + broadcast_ip: + type: string + example: "255.255.255.255" + mac: + type: string + description: "Dirección MAC del equipo a encender" + example: "00:19:99:5c:bb:bb" + responses: + 200: + description: "El paquete Wake On Lan se ha enviado exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Wake On Lan packet sent successfully" + 500: + description: > + Error interno al enviar el paquete Wake On Lan. Puede ocurrir debido a un problema en la ejecución del script o una excepción inesperada. + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Script execution error description." + 500 (Exception): + description: "Error inesperado durante el envío del paquete Wake On Lan." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Generic error description for unexpected exceptions." + + /ogrepository/v1/udpcast: + get: + summary: "Ver Estado de Transmisiones UDPcast" + description: > + Este endpoint devuelve información sobre los procesos activos de "udp-sender" en formato JSON, + permitiendo comprobar las transferencias UDPcast activas. Utiliza el script "getUDPcastInfo.py" para obtener esta información. + tags: + - "Transferencia de Imágenes" + responses: + 200: + description: "La información de las transmisiones UDPcast activas se obtuvo exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: object + additionalProperties: + type: object + properties: + image_id: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + image_name: + type: string + example: "Ubuntu20.img" + 400: + description: "No se han encontrado transmisiones UDPcast activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "No UDPCast active transmissions" + 500: + description: > + Error al comprobar las transmisiones UDPcast activas, posiblemente debido a un error inesperado durante la ejecución del script. + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Script execution error description." + 500 (Exception): + description: "Error inesperado durante la comprobación de transmisiones UDPcast activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Unexpected error checking UDPcast transmissions" + post: + summary: "Enviar una Imagen mediante UDPcast" + description: > + Este endpoint envía una imagen especificada a través de UDPcast utilizando el script "sendFileMcast.py". + Recibe la imagen y los parámetros de configuración de transferencia, que son usados para construir la cadena de parámetros que se envía al script. + tags: + - "Transferencia de Imágenes" + parameters: + - name: ID_img + in: body + required: true + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum' asociado)" + schema: + type: object + properties: + ID_img: + type: string + example: "image_id" + port: + type: string + description: "Puerto Multicast a utilizar para la transmisión" + example: "9000" + method: + type: string + description: "Modalidad de transmisión (puede ser 'half' o 'full' para half-duplex o full-duplex)" + example: "full" + ip: + type: string + description: "IP Multicast a la que se enviará la imagen" + example: "239.194.17.2" + bitrate: + type: string + description: "Velocidad de transmisión en Mbps" + example: "70M" + nclients: + type: integer + description: "Número mínimo de clientes" + example: 20 + maxtime: + type: integer + description: "Tiempo máximo de espera en segundos" + example: 120 + responses: + 200: + description: "La imagen se ha enviado exitosamente mediante UDPcast." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image sent successfully" + 400: + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found" + 500: + description: > + Error interno al enviar la imagen mediante UDPcast. Puede ocurrir debido a un problema en la ejecución del script o una excepción inesperada. + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Script execution error description." + 500 (Exception): + description: "Error inesperado durante el envío de la imagen mediante UDPcast." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Generic error description for unexpected exceptions." + + /ogrepository/v1/uftp: + get: + summary: "Ver Estado de Transmisiones UFTP" + description: > + Este endpoint devuelve información sobre los procesos activos de "uftp" en formato JSON, + permitiendo comprobar las transferencias UFTP activas. Utiliza el script "getUFTPInfo.py" para obtener esta información. + tags: + - "Transferencia de Imágenes" + responses: + 200: + description: "La información de las transmisiones UFTP activas se obtuvo exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: object + additionalProperties: + type: object + properties: + image_id: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + image_name: + type: string + example: "Ubuntu20.img" + 400: + description: "No se han encontrado transmisiones UFTP activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "No UFTP active transmissions" + 500: + description: > + Error al comprobar las transmisiones UFTP activas, posiblemente debido a un error inesperado durante la ejecución del script. + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Script execution error description." + 500 (Exception): + description: "Error inesperado durante la comprobación de transmisiones UFTP activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Unexpected error checking UFTP transmissions" + post: + summary: "Enviar una Imagen mediante UFTP" + description: > + Este endpoint envía una imagen especificada a través de UFTP, utilizando el script "sendFileUFTP.py". + Requiere que los clientes ogLive estén previamente en escucha con un daemon "UFTPD" ejecutando el script "listenUFTPD.py". + Recibe la imagen y los parámetros de configuración de transferencia, que son usados para construir la cadena de parámetros que se envía al script. + tags: + - "Transferencia de Imágenes" + parameters: + - name: ID_img + in: body + required: true + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum' asociado)" + schema: + type: object + properties: + ID_img: + type: string + example: "image_id" + port: + type: string + description: "Puerto para la transmisión UFTP" + example: "9000" + ip: + type: string + description: "IP Unicast o Multicast para la transmisión" + example: "239.194.17.2" + bitrate: + type: string + description: "Velocidad de transmisión (con 'K' para Kbps, 'M' para Mbps, o 'G' para Gbps)" + example: "1G" + responses: + 200: + description: "La imagen se ha enviado exitosamente mediante UFTP." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image sent successfully" + 400: + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found" + 500: + description: > + Error interno al enviar la imagen mediante UFTP. Puede ocurrir debido a un problema en la ejecución del script o una excepción inesperada. + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Script execution error description." + 500 (Exception): + description: "Error inesperado durante el envío de la imagen mediante UFTP." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Generic error description for unexpected exceptions." + + + /ogrepository/v1/p2p: + post: + summary: "Enviar una Imagen mediante P2P" + description: > + Este endpoint inicia el tracker y el seeder de torrents para enviar una imagen especificada mediante P2P. + Utiliza los scripts "runTorrentTracker.py" y "runTorrentSeeder.py" para iniciar el tracker y el seeder en el directorio de la imagen. + tags: + - "Transferencia de Imágenes" + parameters: + - name: ID_img + in: body + required: true + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum' asociado)" + schema: + type: object + properties: + ID_img: + type: string + example: "image_id" + responses: + 200: + description: "La imagen se está enviando exitosamente a través de P2P." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Tracker and Seeder serving image correctly" + 400: + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found" + 500: + description: > + Error al intentar iniciar el tracker o el seeder para el envío P2P. + Puede ocurrir si alguno de los procesos no se inicia correctamente o si ocurre una excepción inesperada. + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Tracker or Seeder (or both) not running" + 500 (Exception): + description: "Error inesperado durante el proceso de envío P2P." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Generic error description for unexpected exceptions." + delete: + summary: "Cancelar Transmisiones P2P" + description: > + Este endpoint cancela todas las transmisiones P2P activas, finalizando los procesos "bttrack" (tracker) y "btlaunchmany.bittornado" (seeder). + Utiliza el script "stopP2P.py" para detener las transmisiones P2P activas en el servidor. + tags: + - "Transferencia de Imágenes" + responses: + 200: + description: "Las transmisiones P2P se han cancelado exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "P2P transmissions canceled successfully" + 500 (Error del script): + description: "Error en la ejecución del script durante la cancelación de las transmisiones P2P." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Detailed error message from script stderr output." + 500 (Excepción general): + description: "Excepción inesperada durante la cancelación de las transmisiones P2P." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "General error description for unexpected exceptions" + + /ogrepository/v1/udpcast/images/{ID_img}: + delete: + summary: "Cancelar Transmisión UDPcast" + description: > + Este endpoint cancela la transmisión UDPcast activa de una imagen especificada, deteniendo el proceso "udp-sender" asociado. + Utiliza el script "stopUDPcast.py" para finalizar la transmisión. + tags: + - "Transferencia de Imágenes" + parameters: + - name: ID_img + in: path + required: true + type: string + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum' asociado)" + example: "image_id" + responses: + 200: + description: "La transmisión UDPcast se ha cancelado exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image transmission canceled successfully" + 400: + description: "No se ha encontrado la imagen especificada o no hay transmisiones UDPcast activas para la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "No UDPCast active transmissions for specified image" + 500 (Error del script): + description: "Error en la ejecución del script durante la cancelación de la transmisión." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Detailed error message from script stderr output." + 500 (Error en verificación): + description: "Error inesperado al verificar las transmisiones UDPcast activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Unexpected error checking UDPcast transmissions" + 500 (Error en finalización): + description: "Error inesperado al finalizar la transmisión UDPcast." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Unexpected error finalizing UDPcast transmission" + 500 (Excepción general): + description: "Excepción inesperada durante la cancelación de la transmisión UDPcast." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "General error description for unexpected exceptions" + + /ogrepository/v1/uftp/images/{ID_img}: + delete: + summary: "Cancelar Transmisión UFTP" + description: > + Este endpoint cancela la transmisión UFTP activa de una imagen especificada, deteniendo el proceso "uftp" asociado. + Utiliza el script "stopUFTP.py" para finalizar la transmisión. + tags: + - "Transferencia de Imágenes" + parameters: + - name: ID_img + in: path + required: true + type: string + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum' asociado)" + example: "image_id" + responses: + 200: + description: "La transmisión UFTP se ha cancelado exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image transmission canceled successfully" + 400: + description: "No se ha encontrado la imagen especificada o no hay transmisiones UFTP activas para la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "No UFTP active transmissions for specified image" + 500 (Error del script): + description: "Error en la ejecución del script durante la cancelación de la transmisión." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Detailed error message from script stderr output." + 500 (Error en verificación): + description: "Error inesperado al verificar las transmisiones UFTP activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Unexpected error checking UFTP transmissions" + 500 (Error en finalización): + description: "Error inesperado al finalizar la transmisión UFTP." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Unexpected error finalizing UFTP transmission" + 500 (Excepción general): + description: "Excepción inesperada durante la cancelación de la transmisión UFTP." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "General error description for unexpected exceptions" + + -- 2.40.1 From e7c5de8628f50250a9345fbb48190dd19359d1e3 Mon Sep 17 00:00:00 2001 From: ggil Date: Thu, 31 Oct 2024 17:25:43 +0100 Subject: [PATCH 44/70] refs #631 - Modify API and Swagger --- api/repo_api.py | 276 +++++++++++++++++++++++++++++++++++++---------- api/swagger.yaml | 153 +++++++++++++------------- 2 files changed, 296 insertions(+), 133 deletions(-) diff --git a/api/repo_api.py b/api/repo_api.py index fe80c9c..fd7c710 100644 --- a/api/repo_api.py +++ b/api/repo_api.py @@ -8,7 +8,10 @@ Responde a peticiones HTTP (en principio, enviadas desde ogCore) mediante endpoi En ciertos casos, transforma los parámetros recibidos desde el portal, para adaptarlos a los que es necesario enviar a los scripts (por ejemplo, a partir del ID de una imagen obtiene su nombre, su extensión y el subdirectorio de OU). -Librerías Python requeridas: Flask (se puede instalar con "sudo apt install python3-flask"). +Librerías Python requeridas: - "flask" (se puede instalar con "sudo apt install python3-flask") + - "paramiko" (se puede instalar con "sudo apt install python3-paramiko") + - "requests" (se puede instalar con "sudo apt install python3-requests") - No tengo claro que para este paquete sea necesario + - "flasgger" (se puede instalar con "sudo apt install python3-flasgger") """ # -------------------------------------------------------------------------------------------- @@ -20,8 +23,15 @@ import os import subprocess import json from time import sleep +import paramiko +import logging +import threading +import requests +# Imports para Swagger: from flasgger import Swagger import yaml + + # -------------------------------------------------------------------------------------------- # VARIABLES # -------------------------------------------------------------------------------------------- @@ -31,6 +41,12 @@ script_path = '/opt/opengnsys/bin' repo_file = '/opt/opengnsys/etc/repoinfo.json' trash_file = '/opt/opengnsys/etc/trashinfo.json' +""" +repo_path = '/home/user/images/' +script_path = '/home/user' +repo_file = '/home/user/jsons/repoinfo.json' +trash_file = '/home/user/jsons/trashinfo.json' +""" # -------------------------------------------------------------------------------------------- # FUNCTIONS @@ -40,9 +56,14 @@ trash_file = '/opt/opengnsys/etc/trashinfo.json' # Creamos una instancia de la aplicación Flask: app = Flask(__name__) + +# Configuración Swagger: with open("swagger.yaml", "r") as file: swagger_template = yaml.safe_load(file) + swagger = Swagger(app, template=swagger_template) + + # --------------------------------------------------------- @@ -123,13 +144,104 @@ def search_process(process, string_to_search): print(f"Unexpected error: {error_description}") +# --------------------------------------------------------- + + +def check_lock_local(image_file_path): + """ Cada minuto comprueba si existe un archivo ".lock" asociado a la imagen que recibe como parámetro + (lo que significará que hay una tarea en curso), en el repositorio local. + Cuando no encuentre el archivo ".lock" lo comunicará a ogCore, llamando a un endpoint, + y dejará de realizar la comprobación (saliendo del bucle). + """ + # Esperamos 30 segundos, para dar tiempo a que se cree el archivo ".lock": + sleep(30) + + # Creamos un bucle infinito: + while True: + # Si ya no existe el archivo ".lock", imprimimos un mensaje en la API, y salimos del bucle: + if not os.path.exists(f"{image_file_path}.lock"): + app.logger.info("Task finalized (no .lock file)") # De momento solamente imprimimos un mensaje en la API (pero debe llamar a un endpoint) + break + # Si aun existe el archivo ".lock", imprimimos un mensaje en la API: + else: + app.logger.info("Task in process (.lock file exists)") + # Esperamos 1 minuto para volver a realizar la comprobación: + sleep(60) + + +# --------------------------------------------------------- + + +def check_lock_remote(image_file_path, remote_host, remote_user): + """ Cada minuto comprueba si existe un archivo ".lock" asociado a la imagen que recibe como parámetro + (lo que significará que hay una tarea en curso), en un repositorio remoto (al que conecta por SSH/SFTP). + Cuando no encuentre el archivo ".lock" lo comunicará a ogCore, llamando a un endpoint, + y dejará de realizar la comprobación (saliendo del bucle). + """ + # Iniciamos un cliente SSH: + ssh_client = paramiko.SSHClient() + # Establecemos la política por defecto para localizar la llave del host localmente: + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + # Conectamos con el equipo remoto por SSH: + ssh_client.connect(remote_host, 22, remote_user) # Así se hace con claves + #ssh_client.connect(remote_host, 22, remote_user, 'opengnsys') # Así se haría con password + + # Iniciamos un cliente SFTP: + sftp_client = ssh_client.open_sftp() + # Esperamos 30 segundos, para dar tiempo a que se cree el archivo ".lock": + sleep(30) + + # Creamos un bucle infinito: + while True: + try: + # Si aun existe el archivo ".lock", imprimimos un mensaje en la API: + sftp_client.stat(f"{image_file_path}.lock") + app.logger.info("Task in process (.lock file exists)") + except IOError: + # Si ya no existe el archivo ".lock", imprimimos un mensaje en la API, y salimos del bucle: + app.logger.info("Task finalized (no .lock file)") # De momento solamente imprimimos un mensaje en la API (pero debe llamar a un endpoint) + break + # Esperamos 1 minuto para volver a realizar la comprobación: + sleep(60) + + # Ya fuera del bucle, cerramos el cliente SSH y el cliente SFTP: + ssh_client.close() + sftp_client.close() + + +# --------------------------------------------------------- + + +def check_aux_files(image_file_path): + """ Cada 10 segundos comprueba si se han creado todos los archivos auxiliares de la imagen que recibe como parámetro, + en cuyo caso lo comunicará a ogCore, llamando a un endpoint, y dejará de realizar la comprobación. + También obtiene el valor del archivo ".full.sum" (que corresonde al ID), y se lo comunica a ogCore. + """ + # Creamos un bucle infinito: + while True: + # Si faltan archivos auxiliares por crear, imprimimos un mensaje en la API: + if not os.path.exists(f"{image_file_path}.size") or not os.path.exists(f"{image_file_path}.sum") or not os.path.exists(f"{image_file_path}.full.sum") or not os.path.exists(f"{image_file_path}.torrent") or not os.path.exists(f"{image_file_path}.info.checked"): + app.logger.info("Task in process (auxiliar files remaining)") + # Si ya se han creado todos los archivos auxiliares, imprimimos un mensaje en la API, y salimos del bucle: + else: + app.logger.info("Task finalized (all auxilar files created)") # De momento solamente imprimimos un mensaje en la API (pero debe llamar a un endpoint) + # Obtenemos el valor del archivo "full.sum", que corresponde al ID, y lo imprimimos: + with open(f"{image_file_path}.full.sum", 'r') as file: + image_id = file.read().strip('\n') + app.logger.info(f"Image_ID: {image_id}") + break + # Esperamos 10 segundos para volver a realizar la comprobación: + sleep(10) + + # -------------------------------------------------------------------------------------------- # ENDPOINTS # -------------------------------------------------------------------------------------------- -# 1 - Endpoint "Obtener Información de Estado de ogRepository": +# 1 - Endpoint "Obtener Información de Estado de ogRepository" (SINCRONO): @app.route("/ogrepository/v1/status", methods=['GET']) def get_repo_status(): """ Este endpoint devuelve información de CPU, memoria RAM, disco duro y el estado de ciertos servicios y procesos de ogRepository, en formato json. @@ -160,7 +272,7 @@ def get_repo_status(): # --------------------------------------------------------- -# 2 - Endpoint "Obtener Información de todas las Imágenes": +# 2 - Endpoint "Obtener Información de todas las Imágenes" (SINCRONO): @app.route("/ogrepository/v1/images", methods=['GET']) def get_repo_info(): """ Este endpoint devuelve información de todas las imágenes contenidas en el repositorio (incluída la papelera), en formato json. @@ -191,7 +303,7 @@ def get_repo_info(): # --------------------------------------------------------- -# 3 - Endpoint "Obtener Información de una Imagen concreta": +# 3 - Endpoint "Obtener Información de una Imagen concreta" (SINCRONO): @app.route("/ogrepository/v1/images/", methods=['GET']) def get_repo_image_info(imageId): """ Este endpoint devuelve información de la imagen especificada como parámetro, en formato json. @@ -238,7 +350,7 @@ def get_repo_image_info(imageId): # --------------------------------------------------------- -# 4 - Endpoint "Actualizar Información del Repositorio": +# 4 - Endpoint "Actualizar Información del Repositorio" (SINCRONO): @app.route("/ogrepository/v1/images", methods=['PUT']) def update_repo_info(): """ Este endpoint actualiza la información del repositorio y de la papelera, reflejándola en los archivos "repoinfo.json" y "trashinfo.josn". @@ -269,7 +381,7 @@ def update_repo_info(): # --------------------------------------------------------- -# 5 - Endpoint "Chequear Integridad de Imagen": +# 5 - Endpoint "Chequear Integridad de Imagen" (SINCRONO): @app.route("/ogrepository/v1/status/images/", methods=['GET']) def check_image(imageId): """ Este endpoint comprueba la integridad de la imagen especificada como parámetro, @@ -323,7 +435,7 @@ def check_image(imageId): # --------------------------------------------------------- -# 6 - Endpoint "Eliminar una Imagen": +# 6 - Endpoint "Eliminar una Imagen" (SINCRONO): @app.route("/ogrepository/v1/images/", methods=['DELETE']) def delete_image(imageId): """ Este endpoint elimina la imagen especificada como parámetro (y todos sus archivos asociados), @@ -379,7 +491,7 @@ def delete_image(imageId): # --------------------------------------------------------- -# 7 - Endpoint "Recuperar una Imagen": +# 7 - Endpoint "Recuperar una Imagen" (SINCRONO): @app.route("/ogrepository/v1/trash/images", methods=['POST']) def recover_image(): """ Este endpoint recupera la imagen especificada como parámetro (y todos sus archivos asociados), @@ -431,7 +543,7 @@ def recover_image(): # --------------------------------------------------------- -# 8 - Endpoint "Eliminar una Imagen de la Papelera": +# 8 - Endpoint "Eliminar una Imagen de la Papelera" (SINCRONO): @app.route("/ogrepository/v1/trash/images/", methods=['DELETE']) def delete_trash_image(imageId): """ Este endpoint elimina permanentemente la imagen especificada como parámetro (y todos sus archivos asociados), desde la papelera. @@ -478,7 +590,7 @@ def delete_trash_image(imageId): # --------------------------------------------------------- -# 9 - Endpoint "Importar una Imagen": +# 9 - Endpoint "Importar una Imagen" (ASINCRONO): @app.route("/ogrepository/v1/repo/images", methods=['POST']) def import_image(): """ Este endpoint importa la imagen especificada como primer parámetro (y todos sus archivos asociados), @@ -493,26 +605,39 @@ def import_image(): remote_ip = json_data.get("repo_ip") remote_user = json_data.get("user") - # Evaluamos los parámetros obtenidos, para construir la llamada al script: + # Evaluamos los parámetros obtenidos, para construir la ruta de la imagen: if ou_subdir == "none": - cmd = ['sudo', 'python3', f"{script_path}/importImage.py", f"{repo_path}{image_name}", remote_ip, remote_user] + image_file_path = f"{repo_path}{image_name}" else: - cmd = ['sudo', 'python3', f"{script_path}/importImage.py", f"{repo_path}{ou_subdir}/{image_name}", remote_ip, remote_user] + image_file_path = f"{repo_path}{ou_subdir}/{image_name}" + + # Construimos la llamada al script: + cmd = ['sudo', 'python3', f"{script_path}/importImage.py", image_file_path, remote_ip, remote_user] try: - # Ejecutamos el script "importImage.py" (con los parámetros almacenados), y almacenamos el resultado: - result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + # Ejecutamos el script "importImage.py" (con los parámetros almacenados), y almacenamos el resultado (pero sin esperar a que termine el proceso): + result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: - if result.returncode == 0: + if result.returncode is None: + # Si el resultado es correcto, llamamos a la función "check_lock_local" en un hilo paralelo + # (para que compruebe si la imagen se ha acabado de importar exitosamente): + threading.Thread(target=check_lock_local, args=(image_file_path,)).start() + + # Informamos que la imagen se está importando, y salimos del endpoint: return jsonify({ "success": True, - "output": "Image imported successfully" + "output": "Importing image..." }), 200 else: return jsonify({ "success": False, - "error": result.stderr + "error": "Image import failed" + }), 500 + except subprocess.CalledProcessError as error: + return jsonify({ + "success": False, + "process exception": str(error) }), 500 except Exception as error_description: if "exit status 2" in str(error_description): @@ -540,7 +665,7 @@ def import_image(): # --------------------------------------------------------- -# 10 - Endpoint "Exportar una Imagen": +# 10 - Endpoint "Exportar una Imagen" (ASINCRONO): @app.route("/ogrepository/v1/repo/images", methods=['PUT']) def export_image(): """ Este endpoint exporta la imagen especificada como primer parámetro (y todos sus archivos asociados), @@ -557,33 +682,46 @@ def export_image(): # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): param_dict = get_image_params(image_id, "repo") - # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + # Evaluamos los parámetros obtenidos, para construir la ruta de la imagen, o para devover un error si no se ha encontrado la imagen: if param_dict: if 'subdir' in param_dict: - cmd = ['sudo', 'python3', f"{script_path}/exportImage.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}", remote_ip, remote_user] + image_file_path = f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}" else: - cmd = ['sudo', 'python3', f"{script_path}/exportImage.py", f"{param_dict['name']}.{param_dict['extension']}", remote_ip, remote_user] + image_file_path = f"{param_dict['name']}.{param_dict['extension']}" else: return jsonify({ "success": False, "error": "Image not found" }), 400 + # Construimos la llamada al script: + cmd = ['sudo', 'python3', f"{script_path}/exportImage.py", image_file_path, remote_ip, remote_user] + try: - # Ejecutamos el script "exportImage.py" (con los parámetros almacenados), y almacenamos el resultado: - result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + # Ejecutamos el script "exportImage.py" (con los parámetros almacenados), y almacenamos el resultado (pero sin esperar a que termine el proceso): + result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: - if result.returncode == 0: + if result.returncode is None: + # Si el resultado es correcto, llamamos a la función "check_lock_remote" en un hilo paralelo + # (para que compruebe si la imagen se ha acabado de exportar exitosamente): + threading.Thread(target=check_lock_remote, args=(f"{repo_path}{image_file_path}", remote_ip, remote_user,)).start() + + # Informamos que la imagen se está exportando, y salimos del endpoint: return jsonify({ "success": True, - "output": "Image exported successfully" + "output": "Exporting image" }), 200 else: return jsonify({ "success": False, "error": result.stderr }), 500 + except subprocess.CalledProcessError as error: + return jsonify({ + "success": False, + "process exception": str(error) + }), 500 except Exception as error_description: if "exit status 3" in str(error_description): return jsonify({ @@ -610,7 +748,7 @@ def export_image(): # --------------------------------------------------------- -# 11 - Endpoint "Crear archivos auxiliares": +# 11 - Endpoint "Crear archivos auxiliares" (ASINCRONO): @app.route("/ogrepository/v1/images/torrentsum", methods=['POST']) def create_torrent_sum(): """ Este endpoint crea los archivos ".size", ".sum", ".full.sum" y ".torrent" para la imagen que recibe como parámetro. @@ -621,21 +759,29 @@ def create_torrent_sum(): image_name = json_data.get("image") ou_subdir = json_data.get("ou_subdir") - # Evaluamos los parámetros obtenidos, para construir la llamada al script: + # Evaluamos los parámetros obtenidos, para construir la ruta de la imagen (relativa a "repo_path"): if ou_subdir == "none": - cmd = ['sudo', 'python3', f"{script_path}/createTorrentSum.py", image_name] + image_file_path = image_name else: - cmd = ['sudo', 'python3', f"{script_path}/createTorrentSum.py", f"{ou_subdir}/{image_name}"] + image_file_path = f"{ou_subdir}/{image_name}" + + # Construimos la llamada al script: + cmd = ['sudo', 'python3', f"{script_path}/createTorrentSum.py", image_file_path] try: - # Ejecutamos el script "createTorrentSum.py" (con los parámetros almacenados), y almacenamos el resultado: - result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + # Ejecutamos el script "createTorrentSum.py" (con los parámetros almacenados), y almacenamos el resultado (pero sin esperar a que termine el proceso): + result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: - if result.returncode == 0: + if result.returncode is None: + # Si el resultado es correcto, llamamos a la función "check_aux_files" en un hilo paralelo + # (para que compruebe si se han creado todos los archivos auxiliares exitosamente): + threading.Thread(target=check_aux_files, args=(f"{repo_path}{image_file_path}",)).start() + + # Informamos que los archivos auxiliares se están creando, y salimos del endpoint: return jsonify({ "success": True, - "output": "Files created successfully" + "output": "Creating auxiliar files..." }), 200 else: return jsonify({ @@ -663,7 +809,7 @@ def create_torrent_sum(): # --------------------------------------------------------- -# 12 - Endpoint "Enviar paquete Wake On Lan": +# 12 - Endpoint "Enviar paquete Wake On Lan" (SINCRONO): @app.route("/ogrepository/v1/wol", methods=['POST']) def send_wakeonlan(): """ Este endpoint envía un paquete mágico Wake On Lan a la dirección MAC especificada, a través de la IP de broadcast especificadac. @@ -699,7 +845,7 @@ def send_wakeonlan(): # --------------------------------------------------------- -# 13 - Endpoint "Enviar una Imagen mediante UDPcast": +# 13 - Endpoint "Enviar una Imagen mediante UDPcast" (ASINCRONO): @app.route("/ogrepository/v1/udpcast", methods=['POST']) def send_udpcast(): """ Este endpoint envía mediante UDPcast la imagen que recibe como primer parámetro, con los datos de transferencia que recibe en los demás parámetros. @@ -731,19 +877,28 @@ def send_udpcast(): }), 400 try: - # Ejecutamos el script "sendFileMcast.py" (con los parámetros almacenados), y almacenamos el resultado: - result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + # Ejecutamos el script "sendFileMcast.py" (con los parámetros almacenados), y almacenamos el resultado (pero sin esperar a que termine el proceso): + result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Comprobamos si está corriendo el proceso correspondiente de "udp-sender" (esperando 5 segundos para darle tiempo a iniciarse): + sleep(5) + process_running = search_process('udp-sender', f"{param_dict['name']}.{param_dict['extension']}") # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: - if result.returncode == 0: # NOTA: Devolverá "0" cuando finalize la transmisión, lo que finalizará también el proceso asociado, - return jsonify({ # pero si ningún cliente se conecta, el proceso quedará corriendo, y el script no devolverá nada. - "success": True, # Esto podría paliarse utilizando el parámetro "--autostart", pero creo que no será necesario. - "output": "Image sended successfully" + if result.returncode is None and process_running == True: + return jsonify({ + "success": True, + "output": "Sending image..." }), 200 else: return jsonify({ "success": False, - "error": result.stderr + "error": "Image send failed" + }), 500 + except subprocess.CalledProcessError as error: + return jsonify({ + "success": False, + "process exeption": str(error) }), 500 except Exception as error_description: return jsonify({ @@ -755,7 +910,7 @@ def send_udpcast(): # --------------------------------------------------------- -# 14 - Endpoint "Enviar una Imagen mediante UFTP": +# 14 - Endpoint "Enviar una Imagen mediante UFTP" (ASINCRONO): @app.route("/ogrepository/v1/uftp", methods=['POST']) def send_uftp(): """ Este endpoint envía mediante UFTP la imagen que recibe como primer parámetro, con los datos de transferencia que recibe en los demás parámetros. @@ -785,19 +940,28 @@ def send_uftp(): }), 400 try: - # Ejecutamos el script "sendFileUFTP.py" (con los parámetros almacenados), y almacenamos el resultado: - result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + # Ejecutamos el script "sendFileUFTP.py" (con los parámetros almacenados), y almacenamos el resultado (pero sin esperar a que termine el proceso): + result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Comprobamos si está corriendo el proceso correspondiente de "uftp" (esperando 5 segundos para darle tiempo a iniciarse): + sleep(5) + process_running = search_process('uftp', f"{param_dict['name']}.{param_dict['extension']}") # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: - if result.returncode == 0: # NOTA: Devolverá "0" cuando finalize la transmisión, lo que finalizará también el proceso asociado, - return jsonify({ # pero si ningún cliente se conecta, el proceso finalizará automáticamente (y tambien devolverá "0""). + if result.returncode is None and process_running == True: + return jsonify({ "success": True, - "output": "Image sended successfully" + "output": "Sending image..." }), 200 else: return jsonify({ "success": False, - "error": result.stderr + "error": "Image send failed" + }), 500 + except subprocess.CalledProcessError as error: + return jsonify({ + "success": False, + "process exeption": str(error) }), 500 except Exception as error_description: return jsonify({ @@ -809,7 +973,7 @@ def send_uftp(): # --------------------------------------------------------- -# 15 - Endpoint "Enviar una Imagen mediante P2P": +# 15 - Endpoint "Enviar una Imagen mediante P2P" (ASINCRONO): @app.route("/ogrepository/v1/p2p", methods=['POST']) def send_p2p(): """ Este endpoint inicia el tracker "bttrack" y el seeder "bittornado", en el directorio en el que esté situada la imagen que recibe como parámetro. @@ -866,7 +1030,7 @@ def send_p2p(): # --------------------------------------------------------- -# 16 - Endpoint "Ver Estado de Transmisiones UDPcast": +# 16 - Endpoint "Ver Estado de Transmisiones UDPcast" (SINCRONO): @app.route("/ogrepository/v1/udpcast", methods=['GET']) def get_udpcast_info(): """ Este endpoint devuelve información sobre los procesos de "udp-sender" activos, en formato json, @@ -909,7 +1073,7 @@ def get_udpcast_info(): # --------------------------------------------------------- -# 17 - Endpoint "Ver Estado de Transmisiones UFTP": +# 17 - Endpoint "Ver Estado de Transmisiones UFTP" (SINCRONO): @app.route("/ogrepository/v1/uftp", methods=['GET']) def get_uftp_info(): """ Este endpoint devuelve información sobre los procesos de "uftp" activos, en formato json, @@ -952,7 +1116,7 @@ def get_uftp_info(): # --------------------------------------------------------- -# 18 - Endpoint "Cancelar Transmisión UDPcast": +# 18 - Endpoint "Cancelar Transmisión UDPcast" (SINCRONO): @app.route("/ogrepository/v1/udpcast/images/", methods=['DELETE']) def stop_udpcast(imageId): """ Este endpoint cancela la transmisión UDPcast de la imagen que recibe como parámetro, finalizando el proceso "udp-sender" asociado. @@ -1014,7 +1178,7 @@ def stop_udpcast(imageId): # --------------------------------------------------------- -# 19 - Endpoint "Cancelar Transmisión UFTP": +# 19 - Endpoint "Cancelar Transmisión UFTP" (SINCRONO): @app.route("/ogrepository/v1/uftp/images/", methods=['DELETE']) def stop_uftp(imageId): """ Este endpoint cancela la transmisión UFTP de la imagen que recibe como parámetro, finalizando el proceso "uftp" asociado. @@ -1076,7 +1240,7 @@ def stop_uftp(imageId): # --------------------------------------------------------- -# 20 - Endpoint "Cancelar Transmisiones P2P": +# 20 - Endpoint "Cancelar Transmisiones P2P" (SINCRONO): @app.route("/ogrepository/v1/p2p", methods=['DELETE']) def stop_p2p(): """ Este endpoint cancela las transmisiones P2P activas, finalizando los procesos "btlaunchmany.bittornado" (seeder) y "bttrack" (tracker). diff --git a/api/swagger.yaml b/api/swagger.yaml index 8ac0a39..ddb3c2e 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -1,6 +1,6 @@ swagger: "2.0" info: - title: "Ogrepository API" + title: "OgRepository API" version: "1.0" description: | API de ogRepository, programada en Flask. @@ -21,7 +21,7 @@ paths: tags: - "Estado de ogRepository" responses: - 200: + "200": description: "La información de estado se obtuvo exitosamente." schema: type: object @@ -95,7 +95,7 @@ paths: btlaunchmany: type: string example: "stopped" - 500: + "500": description: "Error al consultar y/o devolver la información de estado." schema: type: object @@ -118,7 +118,7 @@ paths: tags: - "Imágenes de Repositorio" responses: - 200: + "200": description: "La información de las imágenes se obtuvo exitosamente." schema: type: object @@ -266,7 +266,7 @@ paths: fullsum: type: string example: "22735b9070e4a8043371b8c6ae52b90d" - 500: + "500": description: "Error al consultar y/o devolver la información de las imágenes." schema: type: object @@ -287,7 +287,7 @@ paths: tags: - "Imágenes de Repositorio" responses: - 200: + "200": description: "La actualización de la información del repositorio se realizó exitosamente." schema: type: object @@ -298,7 +298,7 @@ paths: output: type: string example: "Repository info updated successfully" - 500: + "500": description: "Error al actualizar la información de las imágenes." schema: type: object @@ -326,7 +326,7 @@ paths: type: string description: "El ID de la imagen (corresponde al hash MD5 'fullsum' de la imagen)" responses: - 200: + "200": description: "La información de la imagen se obtuvo exitosamente." schema: type: object @@ -375,7 +375,7 @@ paths: fullsum: type: string example: "33575b9070e4a8043371b8c6ae52b80e" - 400: + "400": description: "No se ha encontrado la imagen especificada." schema: type: object @@ -386,7 +386,7 @@ paths: error: type: string example: "Image not found" - 500: + "500": description: "Error al consultar y/o devolver la información de la imagen." schema: type: object @@ -418,7 +418,7 @@ paths: type: string description: "Método de eliminación (puede ser 'trash' para enviar a la papelera o 'permanent' para eliminar definitivamente)" responses: - 200: + "200": description: "La imagen se eliminó exitosamente." schema: type: object @@ -429,7 +429,7 @@ paths: output: type: string example: "Image deleted successfully" - 400: + "400": description: "No se ha encontrado la imagen especificada." schema: type: object @@ -440,7 +440,7 @@ paths: error: type: string example: "Image not found (inexistent or deleted)" - 500: + "500": description: > Error al eliminar la imagen. Puede ocurrir debido a: - Un error de ejecución del script (con salida no 0). @@ -454,7 +454,7 @@ paths: error: type: string example: "Script execution error description." - 500 (Exception): + "500 (Exception)": description: "Error inesperado durante la eliminación de la imagen." schema: type: object @@ -481,7 +481,7 @@ paths: type: string description: "El ID de la imagen (corresponde al hash MD5 'fullsum' de la imagen)" responses: - 200: + "200": description: > La imagen se ha chequeado exitosamente. El chequeo puede devolver dos resultados: - Si pasa la verificación de integridad. @@ -495,7 +495,7 @@ paths: output: type: string example: "Image file passed the Integrity Check correctly" - 400: + "400": description: "No se ha encontrado la imagen especificada." schema: type: object @@ -506,7 +506,7 @@ paths: error: type: string example: "Image not found (inexistent or deleted)" - 500: + "500": description: > Error al chequear la imagen. Puede ocurrir debido a una excepción inesperada. schema: @@ -518,7 +518,7 @@ paths: exception: type: string example: "Generic error description for unexpected exceptions." - 200 (KO): + "200 (KO)": description: "La imagen se ha chequeado correctamente, pero no ha pasado el test de integridad." schema: type: object @@ -550,7 +550,7 @@ paths: type: string example: "image_id" responses: - 200: + "200": description: "La imagen se recuperó exitosamente." schema: type: object @@ -561,7 +561,7 @@ paths: output: type: string example: "Image recovered successfully" - 400: + "400": description: "No se ha encontrado la imagen especificada." schema: type: object @@ -572,7 +572,7 @@ paths: error: type: string example: "Image not found (inexistent or recovered previously)" - 500: + "500": description: > Error al recuperar la imagen. Puede ocurrir debido a: - Un error de ejecución del script (con salida no 0). @@ -586,7 +586,7 @@ paths: error: type: string example: "Script execution error description." - 500 (Exception): + "500 (Exception)": description: "Error inesperado durante la recuperación de la imagen." schema: type: object @@ -613,7 +613,7 @@ paths: type: string description: "El ID de la imagen (corresponde al hash MD5 'fullsum' de la imagen en la papelera)" responses: - 200: + "200": description: "La imagen se eliminó exitosamente." schema: type: object @@ -624,7 +624,7 @@ paths: output: type: string example: "Image deleted successfully" - 400: + "400": description: "No se ha encontrado la imagen especificada en la papelera." schema: type: object @@ -635,7 +635,7 @@ paths: error: type: string example: "Image not found at trash" - 500: + "500": description: > Error al eliminar la imagen desde la papelera. Puede ocurrir debido a: - Un error de ejecución del script (con salida no 0). @@ -649,7 +649,7 @@ paths: error: type: string example: "Script execution error description." - 500 (Exception): + "500 (Exception)": description: "Error inesperado durante la eliminación de la imagen desde la papelera." schema: type: object @@ -694,7 +694,7 @@ paths: description: "Usuario para acceder al repositorio remoto" example: "user_name" responses: - 200: + "200": description: "La imagen se ha importado exitosamente." schema: type: object @@ -705,7 +705,7 @@ paths: output: type: string example: "Image imported successfully" - 400: + "400": description: "Error de conexión o imagen no disponible en el servidor remoto." schema: type: object @@ -716,7 +716,7 @@ paths: exception: type: string example: "Can't connect to remote server | Remote image not found | Remote image is locked" - 500: + "500": description: > Error interno al importar la imagen. Puede ocurrir debido a un problema en la ejecución del script o una excepción inesperada. schema: @@ -728,7 +728,7 @@ paths: error: type: string example: "Script execution error description." - 500 (Exception): + "500 (Exception)": description: "Error inesperado durante la importación de la imagen." schema: type: object @@ -767,7 +767,7 @@ paths: description: "Usuario para acceder al repositorio remoto" example: "user_name" responses: - 200: + "200": description: "La imagen se ha exportado exitosamente." schema: type: object @@ -778,7 +778,7 @@ paths: output: type: string example: "Image exported successfully" - 400: + "400": description: "Error de conexión o imagen no disponible en el servidor remoto." schema: type: object @@ -789,7 +789,7 @@ paths: exception: type: string example: "Image is locked | Can't connect to remote server | Image already exists on remote server" - 500: + "500": description: > Error interno al exportar la imagen. Puede ocurrir debido a un problema en la ejecución del script o una excepción inesperada. schema: @@ -801,7 +801,7 @@ paths: error: type: string example: "Script execution error description." - 500 (Exception): + "500 (Exception)": description: "Error inesperado durante la exportación de la imagen." schema: type: object @@ -836,7 +836,7 @@ paths: description: "Subdirectorio correspondiente a la OU (o 'none' si no es el caso)" example: "none" responses: - 200: + "200": description: "Los archivos se han creado exitosamente." schema: type: object @@ -847,7 +847,7 @@ paths: output: type: string example: "Files created successfully" - 400: + "400": description: "Error de conexión o imagen no disponible en el servidor remoto." schema: type: object @@ -858,7 +858,7 @@ paths: exception: type: string example: "Image not found | Image is locked" - 500: + "500": description: > Error interno al crear los archivos auxiliares. Puede ocurrir debido a un problema en la ejecución del script o una excepción inesperada. schema: @@ -870,7 +870,7 @@ paths: error: type: string example: "Script execution error description." - 500 (Exception): + "500 (Exception)": description: "Error inesperado durante la creación de los archivos auxiliares." schema: type: object @@ -906,7 +906,7 @@ paths: description: "Dirección MAC del equipo a encender" example: "00:19:99:5c:bb:bb" responses: - 200: + "200": description: "El paquete Wake On Lan se ha enviado exitosamente." schema: type: object @@ -917,7 +917,7 @@ paths: output: type: string example: "Wake On Lan packet sent successfully" - 500: + "500": description: > Error interno al enviar el paquete Wake On Lan. Puede ocurrir debido a un problema en la ejecución del script o una excepción inesperada. schema: @@ -929,7 +929,7 @@ paths: error: type: string example: "Script execution error description." - 500 (Exception): + "500 (Exception)": description: "Error inesperado durante el envío del paquete Wake On Lan." schema: type: object @@ -950,7 +950,7 @@ paths: tags: - "Transferencia de Imágenes" responses: - 200: + "200": description: "La información de las transmisiones UDPcast activas se obtuvo exitosamente." schema: type: object @@ -969,7 +969,7 @@ paths: image_name: type: string example: "Ubuntu20.img" - 400: + "400": description: "No se han encontrado transmisiones UDPcast activas." schema: type: object @@ -980,7 +980,7 @@ paths: exception: type: string example: "No UDPCast active transmissions" - 500: + "500": description: > Error al comprobar las transmisiones UDPcast activas, posiblemente debido a un error inesperado durante la ejecución del script. schema: @@ -992,7 +992,7 @@ paths: error: type: string example: "Script execution error description." - 500 (Exception): + "500 (Exception)": description: "Error inesperado durante la comprobación de transmisiones UDPcast activas." schema: type: object @@ -1046,7 +1046,7 @@ paths: description: "Tiempo máximo de espera en segundos" example: 120 responses: - 200: + "200": description: "La imagen se ha enviado exitosamente mediante UDPcast." schema: type: object @@ -1057,7 +1057,7 @@ paths: output: type: string example: "Image sent successfully" - 400: + "400": description: "No se ha encontrado la imagen especificada." schema: type: object @@ -1068,7 +1068,7 @@ paths: error: type: string example: "Image not found" - 500: + "500": description: > Error interno al enviar la imagen mediante UDPcast. Puede ocurrir debido a un problema en la ejecución del script o una excepción inesperada. schema: @@ -1080,7 +1080,7 @@ paths: error: type: string example: "Script execution error description." - 500 (Exception): + "500 (Exception)": description: "Error inesperado durante el envío de la imagen mediante UDPcast." schema: type: object @@ -1101,7 +1101,7 @@ paths: tags: - "Transferencia de Imágenes" responses: - 200: + "200": description: "La información de las transmisiones UFTP activas se obtuvo exitosamente." schema: type: object @@ -1120,7 +1120,7 @@ paths: image_name: type: string example: "Ubuntu20.img" - 400: + "400": description: "No se han encontrado transmisiones UFTP activas." schema: type: object @@ -1131,7 +1131,7 @@ paths: exception: type: string example: "No UFTP active transmissions" - 500: + "500": description: > Error al comprobar las transmisiones UFTP activas, posiblemente debido a un error inesperado durante la ejecución del script. schema: @@ -1143,7 +1143,7 @@ paths: error: type: string example: "Script execution error description." - 500 (Exception): + "500 (Exception)": description: "Error inesperado durante la comprobación de transmisiones UFTP activas." schema: type: object @@ -1186,7 +1186,7 @@ paths: description: "Velocidad de transmisión (con 'K' para Kbps, 'M' para Mbps, o 'G' para Gbps)" example: "1G" responses: - 200: + "200": description: "La imagen se ha enviado exitosamente mediante UFTP." schema: type: object @@ -1197,7 +1197,7 @@ paths: output: type: string example: "Image sent successfully" - 400: + "400": description: "No se ha encontrado la imagen especificada." schema: type: object @@ -1208,7 +1208,7 @@ paths: error: type: string example: "Image not found" - 500: + "500": description: > Error interno al enviar la imagen mediante UFTP. Puede ocurrir debido a un problema en la ejecución del script o una excepción inesperada. schema: @@ -1220,7 +1220,7 @@ paths: error: type: string example: "Script execution error description." - 500 (Exception): + "500 (Exception)": description: "Error inesperado durante el envío de la imagen mediante UFTP." schema: type: object @@ -1253,7 +1253,7 @@ paths: type: string example: "image_id" responses: - 200: + "200": description: "La imagen se está enviando exitosamente a través de P2P." schema: type: object @@ -1264,7 +1264,7 @@ paths: output: type: string example: "Tracker and Seeder serving image correctly" - 400: + "400": description: "No se ha encontrado la imagen especificada." schema: type: object @@ -1275,7 +1275,7 @@ paths: error: type: string example: "Image not found" - 500: + "500": description: > Error al intentar iniciar el tracker o el seeder para el envío P2P. Puede ocurrir si alguno de los procesos no se inicia correctamente o si ocurre una excepción inesperada. @@ -1288,7 +1288,7 @@ paths: error: type: string example: "Tracker or Seeder (or both) not running" - 500 (Exception): + "500 (Exception)": description: "Error inesperado durante el proceso de envío P2P." schema: type: object @@ -1307,7 +1307,7 @@ paths: tags: - "Transferencia de Imágenes" responses: - 200: + "200": description: "Las transmisiones P2P se han cancelado exitosamente." schema: type: object @@ -1318,7 +1318,7 @@ paths: output: type: string example: "P2P transmissions canceled successfully" - 500 (Error del script): + "500 (Error del script)": description: "Error en la ejecución del script durante la cancelación de las transmisiones P2P." schema: type: object @@ -1329,7 +1329,7 @@ paths: error: type: string example: "Detailed error message from script stderr output." - 500 (Excepción general): + "500 (Excepción general)": description: "Excepción inesperada durante la cancelación de las transmisiones P2P." schema: type: object @@ -1357,7 +1357,7 @@ paths: description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum' asociado)" example: "image_id" responses: - 200: + "200": description: "La transmisión UDPcast se ha cancelado exitosamente." schema: type: object @@ -1368,7 +1368,7 @@ paths: output: type: string example: "Image transmission canceled successfully" - 400: + "400": description: "No se ha encontrado la imagen especificada o no hay transmisiones UDPcast activas para la imagen especificada." schema: type: object @@ -1379,7 +1379,7 @@ paths: exception: type: string example: "No UDPCast active transmissions for specified image" - 500 (Error del script): + "500 (Error del script)": description: "Error en la ejecución del script durante la cancelación de la transmisión." schema: type: object @@ -1390,7 +1390,7 @@ paths: error: type: string example: "Detailed error message from script stderr output." - 500 (Error en verificación): + "500 (Error en verificación)": description: "Error inesperado al verificar las transmisiones UDPcast activas." schema: type: object @@ -1401,7 +1401,7 @@ paths: exception: type: string example: "Unexpected error checking UDPcast transmissions" - 500 (Error en finalización): + "500 (Error en finalización)": description: "Error inesperado al finalizar la transmisión UDPcast." schema: type: object @@ -1412,7 +1412,7 @@ paths: exception: type: string example: "Unexpected error finalizing UDPcast transmission" - 500 (Excepción general): + "500 (Excepción general)": description: "Excepción inesperada durante la cancelación de la transmisión UDPcast." schema: type: object @@ -1440,7 +1440,7 @@ paths: description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum' asociado)" example: "image_id" responses: - 200: + "200": description: "La transmisión UFTP se ha cancelado exitosamente." schema: type: object @@ -1451,7 +1451,7 @@ paths: output: type: string example: "Image transmission canceled successfully" - 400: + "400": description: "No se ha encontrado la imagen especificada o no hay transmisiones UFTP activas para la imagen especificada." schema: type: object @@ -1462,7 +1462,7 @@ paths: exception: type: string example: "No UFTP active transmissions for specified image" - 500 (Error del script): + "500 (Error del script)": description: "Error en la ejecución del script durante la cancelación de la transmisión." schema: type: object @@ -1473,7 +1473,7 @@ paths: error: type: string example: "Detailed error message from script stderr output." - 500 (Error en verificación): + "500 (Error en verificación)": description: "Error inesperado al verificar las transmisiones UFTP activas." schema: type: object @@ -1484,7 +1484,7 @@ paths: exception: type: string example: "Unexpected error checking UFTP transmissions" - 500 (Error en finalización): + "500 (Error en finalización)": description: "Error inesperado al finalizar la transmisión UFTP." schema: type: object @@ -1495,7 +1495,7 @@ paths: exception: type: string example: "Unexpected error finalizing UFTP transmission" - 500 (Excepción general): + "500 (Excepción general)": description: "Excepción inesperada durante la cancelación de la transmisión UFTP." schema: type: object @@ -1507,4 +1507,3 @@ paths: type: string example: "General error description for unexpected exceptions" - -- 2.40.1 From 49a114dfcdfcc0a263404eedf61331b44c5d3d04 Mon Sep 17 00:00:00 2001 From: ggil Date: Mon, 4 Nov 2024 17:21:24 +0100 Subject: [PATCH 45/70] refs #631 - Modify API and Swagger --- README.md | 34 +- api/README.md | 34 +- api/repo_api.py | 53 ++- api/swagger.yaml | 933 +++++++++++++++++++++++++++++------------------ 4 files changed, 649 insertions(+), 405 deletions(-) diff --git a/README.md b/README.md index 2bc905c..24e490f 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag ``` **Respuestas:** -- **Código 500 Internal Server Error:** Ocurrió un error al consultar y/o devolver la información de las imágenes. +- **Código 500 Internal Server Error:** Ocurrió un error al consultar y/o devolver la información de la imagen. - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. - **Código 200 OK:** La información de la imagen se obtuvo exitosamente. - **Contenido:** Información de la imagen en formato JSON. @@ -360,7 +360,8 @@ curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/t Se importará una imagen de un repositorio remoto al repositorio local. Se puede hacer con el script "**importImage.py**", que debe ser llamado por el endpoint. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. Estos parámetros deben enviarse desde ogCore (en el JSON), porque el repositorio local no puede extraer la información de la imagen de un ID almacenado en un repositorio remoto. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. Estos parámetros deben enviarse desde ogCore (en el JSON), porque el repositorio local no puede extraer la información de la imagen de un ID almacenado en un repositorio remoto. +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está importando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). **URL:** `/ogrepository/v1/repo/images` **Método HTTP:** POST @@ -379,14 +380,15 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al importar la imagen. - **Código 400 Bad Request:** No se ha encontrado la imagen y/o el equipo remoto especificados. -- **Código 200 OK:** La imagen se ha importado exitosamente. +- **Código 200 OK:** La imagen se está importando. --- ### Exportar una Imagen Se exportará una imagen del repositorio local a un repositorio remoto. Se puede hacer con el script "**exportImage.py**", que debe ser llamado por el endpoint. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero la IP del repositorio remoto y el usuario remoto deben enviarse desde ogCore (en el JSON). +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero la IP del repositorio remoto y el usuario remoto deben enviarse desde ogCore (en el JSON). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está exportando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). **URL:** `/ogrepository/v1/repo/images` **Método HTTP:** PUT @@ -404,14 +406,15 @@ curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al exportar la imagen. - **Código 400 Bad Request:** No se ha encontrado la imagen y/o el equipo remoto especificados. -- **Código 200 OK:** La imagen se ha exportado exitosamente. +- **Código 200 OK:** La imagen se está exportando. --- ### Crear archivos auxiliares Se crearán los archivos ".sum", ".full.sum", ".size" y ".torrent", para la imagen especificada como parámetro. Se puede hacer con el script "**createTorrentSum.py**", que debe ser llamado por el endpoint. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como unico parámetro. Este parámetro no puede obtenerse en la API, a partir del ID de imagen (como en otros casos), porque el ID corresponde al contenido del archivo "full.sum" asociado (que no estará creado hasta que no se ejecute este script). +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como unico parámetro. Este parámetro no puede obtenerse en la API, a partir del ID de imagen (como en otros casos), porque el ID corresponde al contenido del archivo "full.sum" asociado (que no estará creado hasta que no se ejecute este script). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar cierto tiempo, por lo que solo informa de que los archivos auxiliares se están creando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). **URL:** `/ogrepository/v1/images/torrentsum` **Método HTTP:** POST @@ -426,9 +429,9 @@ Se puede hacer con el script "**createTorrentSum.py**", que debe ser llamado por curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/torrentsum ``` **Respuestas:** -- **Código 500 Internal Server Error:** Ocurrió un error al crear los archivos. +- **Código 500 Internal Server Error:** Ocurrió un error al crear los archivos auxiliares. - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. -- **Código 200 OK:** Los archivos se han creado exitosamente. +- **Código 200 OK:** Los archivos auxiliares se están creando. --- ### Enviar paquete Wake On Lan @@ -458,7 +461,8 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se enviará la imagen especificada por Multicast, mediante la aplicación UDPcast. Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binario "**udp-sender**", que es quien realmente realiza el envío. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). **URL:** `/ogrepository/v1/udpcast` **Método HTTP:** POST @@ -480,14 +484,15 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. -- **Código 200 OK:** La imagen se ha enviado exitosamente. +- **Código 200 OK:** La imagen se está enviando mediante UDPcast. --- ### Enviar una Imagen mediante UFTP Se enviará la imagen especificada por Unicast o Multicast, mediante el protocolo "UFTP". Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente los clientes ogLive destino se pongan en escucha con un daemon "UFTPD" (ejecutando el script "**listenUFTPD.py**"). Esto funciona al revés que "UDPcast", ya que primero se debe ejecutar un comando en los clientes, y luego en el servidor. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). **URL:** `/ogrepository/v1/uftp` **Método HTTP:** POST @@ -506,14 +511,15 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. -- **Código 200 OK:** La imagen se ha enviado exitosamente. +- **Código 200 OK:** La imagen se está enviando mediante UFTP. --- ### Enviar una Imagen mediante P2P Se enviará la imagen especificada mediante "P2P", iniciando el tracker y el seeder (que harán tracking y seed de los torrents contenidos en la raiz del directorio especificado). Se puede hacer con los scripts "**runTorrentTracker.py**" y "**runTorrentSeeder.py**", que deben ser llamados por el endpoint. -**NOTA**: La versión actual de estos scripts requiere que se le pase el directorio en el que está situada la imagen a enviar como único parámetro. Este dato se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"). +**NOTA**: Estos scripts requieren que se les pase el directorio en el que está situada la imagen a enviar como único parámetro. Este dato se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). **URL:** `/ogrepository/v1/p2p` @@ -530,7 +536,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al intentar enviar la imagen. - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. -- **Código 200 OK:** La imagen se está enviando exitosamente. +- **Código 200 OK:** La imagen se está enviando mediante P2P. --- ### Ver Estado de Transmisiones UDPcast diff --git a/api/README.md b/api/README.md index 5d6deaa..1a05998 100644 --- a/api/README.md +++ b/api/README.md @@ -202,7 +202,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag ``` **Respuestas:** -- **Código 500 Internal Server Error:** Ocurrió un error al consultar y/o devolver la información de las imágenes. +- **Código 500 Internal Server Error:** Ocurrió un error al consultar y/o devolver la información de la imagen. - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. - **Código 200 OK:** La información de la imagen se obtuvo exitosamente. - **Contenido:** Información de la imagen en formato JSON. @@ -347,7 +347,8 @@ curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/t Se importará una imagen de un repositorio remoto al repositorio local. Se puede hacer con el script "**importImage.py**", que debe ser llamado por el endpoint. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. Estos parámetros deben enviarse desde ogCore (en el JSON), porque el repositorio local no puede extraer la información de la imagen de un ID almacenado en un repositorio remoto. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. Estos parámetros deben enviarse desde ogCore (en el JSON), porque el repositorio local no puede extraer la información de la imagen de un ID almacenado en un repositorio remoto. +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está importando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). **URL:** `/ogrepository/v1/repo/images` **Método HTTP:** POST @@ -366,14 +367,15 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al importar la imagen. - **Código 400 Bad Request:** No se ha encontrado la imagen y/o el equipo remoto especificados. -- **Código 200 OK:** La imagen se ha importado exitosamente. +- **Código 200 OK:** La imagen se está importando. --- ### Exportar una Imagen Se exportará una imagen del repositorio local a un repositorio remoto. Se puede hacer con el script "**exportImage.py**", que debe ser llamado por el endpoint. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero la IP del repositorio remoto y el usuario remoto deben enviarse desde ogCore (en el JSON). +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero la IP del repositorio remoto y el usuario remoto deben enviarse desde ogCore (en el JSON). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está exportando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). **URL:** `/ogrepository/v1/repo/images` **Método HTTP:** PUT @@ -391,14 +393,15 @@ curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al exportar la imagen. - **Código 400 Bad Request:** No se ha encontrado la imagen y/o el equipo remoto especificados. -- **Código 200 OK:** La imagen se ha exportado exitosamente. +- **Código 200 OK:** La imagen se está exportando. --- ### Crear archivos auxiliares Se crearán los archivos ".sum", ".full.sum", ".size" y ".torrent", para la imagen especificada como parámetro. Se puede hacer con el script "**createTorrentSum.py**", que debe ser llamado por el endpoint. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como unico parámetro. Este parámetro no puede obtenerse en la API, a partir del ID de imagen (como en otros casos), porque el ID corresponde al contenido del archivo "full.sum" asociado (que no estará creado hasta que no se ejecute este script). +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como unico parámetro. Este parámetro no puede obtenerse en la API, a partir del ID de imagen (como en otros casos), porque el ID corresponde al contenido del archivo "full.sum" asociado (que no estará creado hasta que no se ejecute este script). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar cierto tiempo, por lo que solo informa de que los archivos auxiliares se están creando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). **URL:** `/ogrepository/v1/images/torrentsum` **Método HTTP:** POST @@ -413,9 +416,9 @@ Se puede hacer con el script "**createTorrentSum.py**", que debe ser llamado por curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/torrentsum ``` **Respuestas:** -- **Código 500 Internal Server Error:** Ocurrió un error al crear los archivos. +- **Código 500 Internal Server Error:** Ocurrió un error al crear los archivos auxiliares. - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. -- **Código 200 OK:** Los archivos se han creado exitosamente. +- **Código 200 OK:** Los archivos auxiliares se están creando. --- ### Enviar paquete Wake On Lan @@ -445,7 +448,8 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se enviará la imagen especificada por Multicast, mediante la aplicación UDPcast. Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binario "**udp-sender**", que es quien realmente realiza el envío. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). **URL:** `/ogrepository/v1/udpcast` **Método HTTP:** POST @@ -467,14 +471,15 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. -- **Código 200 OK:** La imagen se ha enviado exitosamente. +- **Código 200 OK:** La imagen se está enviando mediante UDPcast. --- ### Enviar una Imagen mediante UFTP Se enviará la imagen especificada por Unicast o Multicast, mediante el protocolo "UFTP". Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente los clientes ogLive destino se pongan en escucha con un daemon "UFTPD" (ejecutando el script "**listenUFTPD.py**"). Esto funciona al revés que "UDPcast", ya que primero se debe ejecutar un comando en los clientes, y luego en el servidor. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). **URL:** `/ogrepository/v1/uftp` **Método HTTP:** POST @@ -493,14 +498,15 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. -- **Código 200 OK:** La imagen se ha enviado exitosamente. +- **Código 200 OK:** La imagen se está enviando mediante UFTP. --- ### Enviar una Imagen mediante P2P Se enviará la imagen especificada mediante "P2P", iniciando el tracker y el seeder (que harán tracking y seed de los torrents contenidos en la raiz del directorio especificado). Se puede hacer con los scripts "**runTorrentTracker.py**" y "**runTorrentSeeder.py**", que deben ser llamados por el endpoint. -**NOTA**: La versión actual de estos scripts requiere que se le pase el directorio en el que está situada la imagen a enviar como único parámetro. Este dato se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"). +**NOTA**: Estos scripts requieren que se les pase el directorio en el que está situada la imagen a enviar como único parámetro. Este dato se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). **URL:** `/ogrepository/v1/p2p` @@ -517,7 +523,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al intentar enviar la imagen. - **Código 400 Bad Request:** No se ha encontrado la imagen especificada. -- **Código 200 OK:** La imagen se está enviando exitosamente. +- **Código 200 OK:** La imagen se está enviando mediante P2P. --- ### Ver Estado de Transmisiones UDPcast diff --git a/api/repo_api.py b/api/repo_api.py index fd7c710..9297628 100644 --- a/api/repo_api.py +++ b/api/repo_api.py @@ -8,10 +8,10 @@ Responde a peticiones HTTP (en principio, enviadas desde ogCore) mediante endpoi En ciertos casos, transforma los parámetros recibidos desde el portal, para adaptarlos a los que es necesario enviar a los scripts (por ejemplo, a partir del ID de una imagen obtiene su nombre, su extensión y el subdirectorio de OU). -Librerías Python requeridas: - "flask" (se puede instalar con "sudo apt install python3-flask") - - "paramiko" (se puede instalar con "sudo apt install python3-paramiko") - - "requests" (se puede instalar con "sudo apt install python3-requests") - No tengo claro que para este paquete sea necesario - - "flasgger" (se puede instalar con "sudo apt install python3-flasgger") +Librerías Python requeridas: - flask (se puede instalar con "sudo apt install python3-flask") + - paramiko (se puede instalar con "sudo apt install python3-paramiko") + - requests (se puede instalar con "sudo apt install python3-requests") - No tengo claro que para este paquete sea necesario + - flasgger (se puede instalar con "sudo apt install python3-flasgger") """ # -------------------------------------------------------------------------------------------- @@ -230,11 +230,42 @@ def check_aux_files(image_file_path): with open(f"{image_file_path}.full.sum", 'r') as file: image_id = file.read().strip('\n') app.logger.info(f"Image_ID: {image_id}") + + # Almacenamos en un diccionario los datos a enviar a ogCore: + data = { + 'job_id': 222, + 'image_id': image_id + } + # Llamamos al endpoint de ogCore, enviando los datos: + recall_ogcore(data) break # Esperamos 10 segundos para volver a realizar la comprobación: sleep(10) +# --------------------------------------------------------- + + +def recall_ogcore(data): + """ Hace una petición HTTP de tipo POST a un endpoint de ogCore, enviando datos que dependen del caso. + Se utiliza para informar a ogCore del resultado de una tarea asíncrona, + que estaba corriendo en un proceso independiente (no controlado por los endpoints). + """ + # Almacenamos la URL del endpoint de ogCore: + endpoint_url = 'http://192.168.56.101:8006//ogcore/v1/test' + + # Almacenamos los headers, y convertiomos "data" a JSON: + headers = {'content-type': 'application/json'} + data = json.dumps(data) + + # Hacemos una petición POST al endpoint, enviando lo almacenado en "data": + response = requests.post(endpoint_url, data=data, headers=headers) + + # Imprimimos el código de estado de la petición y la respuesta de ogCore: + app.logger.info(f"HTTP Status Code: {response.status_code}") + app.logger.info(f"HTTP Response: {response.text}") + + # -------------------------------------------------------------------------------------------- # ENDPOINTS @@ -710,12 +741,12 @@ def export_image(): # Informamos que la imagen se está exportando, y salimos del endpoint: return jsonify({ "success": True, - "output": "Exporting image" + "output": "Exporting image..." }), 200 else: return jsonify({ "success": False, - "error": result.stderr + "error": "Export image failed" }), 500 except subprocess.CalledProcessError as error: return jsonify({ @@ -1058,11 +1089,6 @@ def get_udpcast_info(): "success": False, "exception": "No UDPCast active transmissions" }), 400 - elif "exit status 2" in str(error_description): - return jsonify({ - "success": False, - "exception": "Unexpected error checking UDPcast transmissions" - }), 500 else: return jsonify({ "success": False, @@ -1101,11 +1127,6 @@ def get_uftp_info(): "success": False, "exception": "No UFTP active transmissions" }), 400 - elif "exit status 2" in str(error_description): - return jsonify({ - "success": False, - "exception": "Unexpected error checking UFTP transmissions" - }), 500 else: return jsonify({ "success": False, diff --git a/api/swagger.yaml b/api/swagger.yaml index ddb3c2e..9e03b87 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -7,9 +7,17 @@ info: Responde a peticiones HTTP (enviadas desde ogCore) mediante endpoints, que a su vez ejecutan los scripts Python almacenados en ogRepo. En ciertos casos, transforma los parámetros recibidos desde el portal, para adaptarlos a los que es necesario enviar a los scripts - (por ejemplo, a partir del ID de una imagen obtiene su nombre, su extensión y el subdirectorio de OU). + (por ejemplo, a partir del ID de una imagen obtiene su nombre, su extensión y el subdirectorio de OU). - Librerías Python requeridas: Flask (se puede instalar con "sudo apt install python3-flask"). + Librerías Python requeridas: + - flask (se puede instalar con "sudo apt install python3-flask") + - paramiko (se puede instalar con "sudo apt install python3-paramiko") + - requests (se puede instalar con "sudo apt install python3-requests") - No tengo claro que para este paquete sea necesario + - flasgger (se puede instalar con "sudo apt install python3-flasgger") + +# ----------------------------------------------------------------------------------------------------------- +# Apartado "Estado de ogRepository" +# ----------------------------------------------------------------------------------------------------------- paths: /ogrepository/v1/status: @@ -95,8 +103,19 @@ paths: btlaunchmany: type: string example: "stopped" - "500": + "500 (Error)": description: "Error al consultar y/o devolver la información de estado." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al consultar y/o devolver la información de estado." schema: type: object properties: @@ -105,9 +124,59 @@ paths: example: false exception: type: string - example: "Generic error description for unexpected exceptions." + example: "(Exception description)" +# ----------------------------------------------------------------------------------------------------------- +# Apartado "Información de Imágenes" +# ----------------------------------------------------------------------------------------------------------- + /ogrepository/v1/images: + put: + summary: "Actualizar Información del Repositorio" + description: > + Este endpoint actualiza la información de las imágenes almacenadas en el repositorio, reflejándola en los archivos "repoinfo.json" y "trashinfo.json". + Utiliza el script "updateRepoInfo.py", que también ejecuta "updateTrashInfo.py". + Este proceso se realiza para mantener actualizados los datos del repositorio y la papelera, y debería ser invocado cada vez que se elimine o cree una imagen. + tags: + - "Información de Imágenes" + responses: + "200": + description: "La actualización de la información de las imágenes se realizó exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Repository info updated successfully" + "500 (Error)": + description: "Error al actualizar la información de las imágenes." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al actualizar la información de las imágenes." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + #/ogrepository/v1/images: get: summary: "Obtener Información de todas las Imágenes" description: > @@ -116,7 +185,7 @@ paths: Devuelve detalles como el nombre de la imagen, tipo, nombre del cliente, clonador, compresor, sistema de archivos, tamaño de los datos, tamaño de la imagen, y hashes MD5. tags: - - "Imágenes de Repositorio" + - "Información de Imágenes" responses: "200": description: "La información de las imágenes se obtuvo exitosamente." @@ -266,7 +335,7 @@ paths: fullsum: type: string example: "22735b9070e4a8043371b8c6ae52b90d" - "500": + "500 (Error)": description: "Error al consultar y/o devolver la información de las imágenes." schema: type: object @@ -274,32 +343,11 @@ paths: success: type: boolean example: false - exception: + error: type: string - example: "Generic error description for unexpected exceptions." - - put: - summary: "Actualizar Información del Repositorio" - description: > - Este endpoint actualiza la información de las imágenes almacenadas en el repositorio, reflejándola en los archivos "repoinfo.json" y "trashinfo.json". - Utiliza el script "updateRepoInfo.py", que también ejecuta "updateTrashInfo.py". - Este proceso se realiza para mantener actualizados los datos del repositorio y la papelera, y debería ser invocado cada vez que se elimine o cree una imagen. - tags: - - "Imágenes de Repositorio" - responses: - "200": - description: "La actualización de la información del repositorio se realizó exitosamente." - schema: - type: object - properties: - success: - type: boolean - example: true - output: - type: string - example: "Repository info updated successfully" - "500": - description: "Error al actualizar la información de las imágenes." + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al consultar y/o devolver la información de las imágenes." schema: type: object properties: @@ -308,7 +356,9 @@ paths: example: false exception: type: string - example: "Generic error description for unexpected exceptions." + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- /ogrepository/v1/images/{imageId}: get: @@ -318,7 +368,7 @@ paths: Puede utilizar el script "getRepoInfo.py" que recibe como parámetros el nombre de la imagen con extensión y el subdirectorio correspondiente a la OU (o "none" si no es el caso). La imagen puede estar en el archivo "/opt/opengnsys/etc/repoinfo.json" (si está almacenada) o en "/opt/opengnsys/etc/trashinfo.json" (si está en la papelera). tags: - - "Imágenes de Repositorio" + - "Información de Imágenes" parameters: - name: imageId in: path @@ -386,8 +436,19 @@ paths: error: type: string example: "Image not found" - "500": + "500 (Error)": description: "Error al consultar y/o devolver la información de la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al consultar y/o devolver la información de la imagen." schema: type: object properties: @@ -396,7 +457,86 @@ paths: example: false exception: type: string - example: "Generic error description for unexpected exceptions." + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/status/images/{imageId}: + get: + summary: "Chequear Integridad de Imagen" + description: > + Este endpoint comprueba la integridad de la imagen especificada como parámetro, comparando el tamaño y el hash MD5 del último MB con los valores almacenados en los archivos ".size" y ".sum". + Utiliza el script "checkImage.py", que recibe el nombre de la imagen (con extensión) y el subdirectorio correspondiente a la OU, si aplica. + tags: + - "Información de Imágenes" + parameters: + - name: imageId + in: path + required: true + type: string + description: "El ID de la imagen (corresponde al hash MD5 'fullsum' de la imagen)" + responses: + "200 (Check OK)": + description: "La imagen se ha chequeado exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image file passed the Integrity Check correctly" + "200 (Check KO)": + description: "La imagen se ha chequeado exitosamente, pero no ha pasado el test de integridad." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image file didn't pass the Integrity Check" + "400": + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found (inexistent or deleted)" + "500 (Error)": + description: "Error al chequear la integridad de la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al chequear la integridad de la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- +# Apartado "Eliminar y Recuperar Imágenes" +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/images/{imageId}?method={method}: delete: summary: "Eliminar una Imagen" description: > @@ -405,7 +545,7 @@ paths: Utiliza el script "deleteImage.py" que recibe el nombre de la imagen (con extensión y subdirectorio correspondiente a la OU, si aplica) como primer parámetro, y opcionalmente el parámetro "-p" para eliminación permanente. tags: - - "Imágenes de Repositorio" + - "Eliminar y Recuperar Imágenes" parameters: - name: imageId in: path @@ -440,11 +580,8 @@ paths: error: type: string example: "Image not found (inexistent or deleted)" - "500": - description: > - Error al eliminar la imagen. Puede ocurrir debido a: - - Un error de ejecución del script (con salida no 0). - - Una excepción inesperada durante el proceso. + "500 (Error)": + description: "Error al eliminar la imagen." schema: type: object properties: @@ -453,9 +590,9 @@ paths: example: false error: type: string - example: "Script execution error description." + example: "(Error description)" "500 (Exception)": - description: "Error inesperado durante la eliminación de la imagen." + description: "Excepción inesperada al eliminar la imagen." schema: type: object properties: @@ -464,71 +601,9 @@ paths: example: false exception: type: string - example: "Generic error description for unexpected exceptions." + example: "(Exception description)" - /ogrepository/v1/status/images/{imageId}: - get: - summary: "Chequear Integridad de Imagen" - description: > - Este endpoint comprueba la integridad de la imagen especificada como parámetro, comparando el tamaño y el hash MD5 del último MB con los valores almacenados en los archivos ".size" y ".sum". - Utiliza el script "checkImage.py", que recibe el nombre de la imagen (con extensión) y el subdirectorio correspondiente a la OU, si aplica. - tags: - - "Integridad de Imágenes" - parameters: - - name: imageId - in: path - required: true - type: string - description: "El ID de la imagen (corresponde al hash MD5 'fullsum' de la imagen)" - responses: - "200": - description: > - La imagen se ha chequeado exitosamente. El chequeo puede devolver dos resultados: - - Si pasa la verificación de integridad. - - Si no pasa la verificación de integridad. - schema: - type: object - properties: - success: - type: boolean - example: true - output: - type: string - example: "Image file passed the Integrity Check correctly" - "400": - description: "No se ha encontrado la imagen especificada." - schema: - type: object - properties: - success: - type: boolean - example: false - error: - type: string - example: "Image not found (inexistent or deleted)" - "500": - description: > - Error al chequear la imagen. Puede ocurrir debido a una excepción inesperada. - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "Generic error description for unexpected exceptions." - "200 (KO)": - description: "La imagen se ha chequeado correctamente, pero no ha pasado el test de integridad." - schema: - type: object - properties: - success: - type: boolean - example: true - output: - type: string - example: "Image file didn't pass the Integrity Check" +# ----------------------------------------------------------------------------------------------------------- /ogrepository/v1/trash/images: post: @@ -537,7 +612,7 @@ paths: Este endpoint recupera la imagen especificada, moviéndola desde la papelera al repositorio de imágenes. Utiliza el script "recoverImage.py", que recibe el nombre de la imagen (con extensión y subdirectorio correspondiente a la OU, si aplica). tags: - - "Imágenes de Papelera" + - "Eliminar y Recuperar Imágenes" parameters: - name: ID_img in: body @@ -572,11 +647,8 @@ paths: error: type: string example: "Image not found (inexistent or recovered previously)" - "500": - description: > - Error al recuperar la imagen. Puede ocurrir debido a: - - Un error de ejecución del script (con salida no 0). - - Una excepción inesperada durante el proceso. + "500 (Error)": + description: "Error al recuperar la imagen." schema: type: object properties: @@ -585,9 +657,9 @@ paths: example: false error: type: string - example: "Script execution error description." + example: "(Error description)" "500 (Exception)": - description: "Error inesperado durante la recuperación de la imagen." + description: "Excepción inesperada al recuperar la imagen." schema: type: object properties: @@ -596,7 +668,9 @@ paths: example: false exception: type: string - example: "Generic error description for unexpected exceptions." + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- /ogrepository/v1/trash/images/{imageId}: delete: @@ -605,7 +679,7 @@ paths: Este endpoint elimina permanentemente la imagen especificada desde la papelera. Utiliza el script "deleteTrashImage.py", que recibe el nombre de la imagen (con extensión y subdirectorio correspondiente a la OU, si aplica). tags: - - "Imágenes de Papelera" + - "Eliminar y Recuperar Imágenes" parameters: - name: imageId in: path @@ -635,11 +709,8 @@ paths: error: type: string example: "Image not found at trash" - "500": - description: > - Error al eliminar la imagen desde la papelera. Puede ocurrir debido a: - - Un error de ejecución del script (con salida no 0). - - Una excepción inesperada durante el proceso. + "500 (Error)": + description: "Error al eliminar la imagen de la papelera." schema: type: object properties: @@ -648,9 +719,9 @@ paths: example: false error: type: string - example: "Script execution error description." + example: "(Error description)" "500 (Exception)": - description: "Error inesperado durante la eliminación de la imagen desde la papelera." + description: "Excepción inesperada al eliminar la imagen de la papelera." schema: type: object properties: @@ -659,7 +730,11 @@ paths: example: false exception: type: string - example: "Generic error description for unexpected exceptions." + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- +# Apartado "Importar y Exportar Imágenes" +# ----------------------------------------------------------------------------------------------------------- /ogrepository/v1/repo/images: post: @@ -668,8 +743,10 @@ paths: Este endpoint importa la imagen especificada desde un servidor remoto al servidor local. Utiliza el script "importImage.py", que recibe como parámetros el nombre de la imagen, la IP o hostname del servidor remoto, el usuario para la conexión, y el subdirectorio correspondiente a la OU (si aplica). + NOTA: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está importando, + y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). tags: - - "Imágenes de Repositorio" + - "Importar y Exportar Imágenes" parameters: - name: image in: body @@ -695,7 +772,7 @@ paths: example: "user_name" responses: "200": - description: "La imagen se ha importado exitosamente." + description: "La imagen se está importando." schema: type: object properties: @@ -704,9 +781,9 @@ paths: example: true output: type: string - example: "Image imported successfully" - "400": - description: "Error de conexión o imagen no disponible en el servidor remoto." + example: "Importing image..." + "400 (Connection fail)": + description: "Error de conexión con el servidor remoto." schema: type: object properties: @@ -715,10 +792,31 @@ paths: example: false exception: type: string - example: "Can't connect to remote server | Remote image not found | Remote image is locked" + example: "Can't connect to remote server" + "400 (Image not found)": + description: "No se ha encontrado la imagen remota." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Remote image not found" + "400 (Image locked)": + description: "La imagen remota está bloqueada." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Remote image is locked" "500": - description: > - Error interno al importar la imagen. Puede ocurrir debido a un problema en la ejecución del script o una excepción inesperada. + description: "Error al importar la imagen." schema: type: object properties: @@ -727,9 +825,20 @@ paths: example: false error: type: string - example: "Script execution error description." + example: "Image import failed" + "500 (Error)": + description: "Error al importar la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" "500 (Exception)": - description: "Error inesperado durante la importación de la imagen." + description: "Excepción inesperada al importar la imagen." schema: type: object properties: @@ -738,15 +847,21 @@ paths: example: false exception: type: string - example: "Generic error description for unexpected exceptions." + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + #/ogrepository/v1/repo/images: put: summary: "Exportar una Imagen" description: > Este endpoint exporta la imagen especificada desde el servidor local a un servidor remoto. Utiliza el script "exportImage.py", que recibe como parámetros el nombre de la imagen, la IP o hostname del servidor remoto, el usuario para la conexión, y el subdirectorio correspondiente a la OU (si aplica). + NOTA: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está exportando, + y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). tags: - - "Imágenes de Repositorio" + - "Importar y Exportar Imágenes" parameters: - name: ID_img in: body @@ -768,7 +883,7 @@ paths: example: "user_name" responses: "200": - description: "La imagen se ha exportado exitosamente." + description: "La imagen se está exportando." schema: type: object properties: @@ -777,9 +892,9 @@ paths: example: true output: type: string - example: "Image exported successfully" - "400": - description: "Error de conexión o imagen no disponible en el servidor remoto." + example: "Exporting image..." + "400 (Image not found)": + description: "No se ha encontrado la imagen." schema: type: object properties: @@ -788,10 +903,42 @@ paths: example: false exception: type: string - example: "Image is locked | Can't connect to remote server | Image already exists on remote server" + example: "Image not found" + "400 (Image locked)": + description: "La imagen está bloqueada." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Image is locked" + "400 (Connection fail)": + description: "Error de conexión con el servidor remoto." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Can't connect to remote server" + "400 (Image present)": + description: "La imagen ya existe en el servidor remoto." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Image already exists on remote server" "500": - description: > - Error interno al exportar la imagen. Puede ocurrir debido a un problema en la ejecución del script o una excepción inesperada. + description: "Error al exportar la imagen." schema: type: object properties: @@ -800,9 +947,20 @@ paths: example: false error: type: string - example: "Script execution error description." + example: "Export image failed" + "500 (Error)": + description: "Error al exportar la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" "500 (Exception)": - description: "Error inesperado durante la exportación de la imagen." + description: "Excepción inesperada al exportar la imagen." schema: type: object properties: @@ -811,15 +969,22 @@ paths: example: false exception: type: string - example: "Generic error description for unexpected exceptions." + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- +# Apartado "Varios" +# ----------------------------------------------------------------------------------------------------------- + /ogrepository/v1/images/torrentsum: post: summary: "Crear archivos auxiliares" description: > Este endpoint crea los archivos ".size", ".sum", ".full.sum" y ".torrent" para la imagen especificada. Utiliza el script "createTorrentSum.py", que recibe como parámetro el nombre de la imagen (con subdirectorio de OU, si aplica). + NOTA: Este endpoint es asíncrono, ya que puede tardar cierto tiempo, por lo que solo informa de que se están creando los archivos auxiliares, + y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). tags: - - "Archivos Auxiliares de Imágenes" + - "Varios" parameters: - name: image in: body @@ -837,7 +1002,7 @@ paths: example: "none" responses: "200": - description: "Los archivos se han creado exitosamente." + description: "Los archivos auxiliares se están creando." schema: type: object properties: @@ -846,9 +1011,9 @@ paths: example: true output: type: string - example: "Files created successfully" - "400": - description: "Error de conexión o imagen no disponible en el servidor remoto." + example: "Creating auxiliar files..." + "400 (Image not found)": + description: "No se ha encontrado la imagen." schema: type: object properties: @@ -857,10 +1022,20 @@ paths: example: false exception: type: string - example: "Image not found | Image is locked" - "500": - description: > - Error interno al crear los archivos auxiliares. Puede ocurrir debido a un problema en la ejecución del script o una excepción inesperada. + example: "Image not found" + "400 (Image locked)": + description: "La imagen está bloqueada." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Image is locked" + "500 (Error)": + description: "Error al crear los archivos auxiliares." schema: type: object properties: @@ -869,9 +1044,9 @@ paths: example: false error: type: string - example: "Script execution error description." + example: "(Error description)" "500 (Exception)": - description: "Error inesperado durante la creación de los archivos auxiliares." + description: "Excepción inesperada al crear los archivos auxiliares." schema: type: object properties: @@ -880,7 +1055,9 @@ paths: example: false exception: type: string - example: "Generic error description for unexpected exceptions." + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- /ogrepository/v1/wol: post: @@ -889,7 +1066,7 @@ paths: Este endpoint envía un paquete mágico Wake On Lan (WOL) a la dirección MAC especificada, a través de la IP de broadcast especificada. Utiliza el script "sendWakeOnLan.py", que recibe como parámetros la IP de broadcast y la dirección MAC del equipo a encender. tags: - - "Wake On Lan" + - "Varios" parameters: - name: broadcast_ip in: body @@ -917,9 +1094,8 @@ paths: output: type: string example: "Wake On Lan packet sent successfully" - "500": - description: > - Error interno al enviar el paquete Wake On Lan. Puede ocurrir debido a un problema en la ejecución del script o una excepción inesperada. + "500 (Error)": + description: "Error al enviar el paquete Wake On Lan." schema: type: object properties: @@ -928,9 +1104,9 @@ paths: example: false error: type: string - example: "Script execution error description." + example: "(Error description)" "500 (Exception)": - description: "Error inesperado durante el envío del paquete Wake On Lan." + description: "Excepción inesperada al enviar el paquete Wake On Lan." schema: type: object properties: @@ -939,75 +1115,20 @@ paths: example: false exception: type: string - example: "Generic error description for unexpected exceptions." + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- +# Apartado "Transferencia de Imágenes" +# ----------------------------------------------------------------------------------------------------------- /ogrepository/v1/udpcast: - get: - summary: "Ver Estado de Transmisiones UDPcast" - description: > - Este endpoint devuelve información sobre los procesos activos de "udp-sender" en formato JSON, - permitiendo comprobar las transferencias UDPcast activas. Utiliza el script "getUDPcastInfo.py" para obtener esta información. - tags: - - "Transferencia de Imágenes" - responses: - "200": - description: "La información de las transmisiones UDPcast activas se obtuvo exitosamente." - schema: - type: object - properties: - success: - type: boolean - example: true - output: - type: object - additionalProperties: - type: object - properties: - image_id: - type: string - example: "22735b9070e4a8043371b8c6ae52b90d" - image_name: - type: string - example: "Ubuntu20.img" - "400": - description: "No se han encontrado transmisiones UDPcast activas." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "No UDPCast active transmissions" - "500": - description: > - Error al comprobar las transmisiones UDPcast activas, posiblemente debido a un error inesperado durante la ejecución del script. - schema: - type: object - properties: - success: - type: boolean - example: false - error: - type: string - example: "Script execution error description." - "500 (Exception)": - description: "Error inesperado durante la comprobación de transmisiones UDPcast activas." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "Unexpected error checking UDPcast transmissions" post: summary: "Enviar una Imagen mediante UDPcast" description: > Este endpoint envía una imagen especificada a través de UDPcast utilizando el script "sendFileMcast.py". Recibe la imagen y los parámetros de configuración de transferencia, que son usados para construir la cadena de parámetros que se envía al script. + NOTA: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, + y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). tags: - "Transferencia de Imágenes" parameters: @@ -1047,7 +1168,7 @@ paths: example: 120 responses: "200": - description: "La imagen se ha enviado exitosamente mediante UDPcast." + description: "La imagen se está enviando mediante UDPcast." schema: type: object properties: @@ -1056,7 +1177,7 @@ paths: example: true output: type: string - example: "Image sent successfully" + example: "Sending image.." "400": description: "No se ha encontrado la imagen especificada." schema: @@ -1068,9 +1189,8 @@ paths: error: type: string example: "Image not found" - "500": - description: > - Error interno al enviar la imagen mediante UDPcast. Puede ocurrir debido a un problema en la ejecución del script o una excepción inesperada. + "500 (Error)": + description: "Error al enviar la imagen mediante UDPcast." schema: type: object properties: @@ -1079,9 +1199,9 @@ paths: example: false error: type: string - example: "Script execution error description." + example: "Image send failed" "500 (Exception)": - description: "Error inesperado durante el envío de la imagen mediante UDPcast." + description: "Excepción inesperada al enviar la imagen mediante UDPcast." schema: type: object properties: @@ -1090,9 +1210,157 @@ paths: example: false exception: type: string - example: "Generic error description for unexpected exceptions." + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + #/ogrepository/v1/udpcast: + get: + summary: "Ver Estado de Transmisiones UDPcast" + description: > + Este endpoint devuelve información sobre los procesos activos de "udp-sender" en formato JSON, + permitiendo comprobar las transferencias UDPcast activas. Utiliza el script "getUDPcastInfo.py" para obtener esta información. + tags: + - "Transferencia de Imágenes" + responses: + "200": + description: "La información de las transmisiones UDPcast activas se obtuvo exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: object + additionalProperties: + type: object + properties: + image_id: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + image_name: + type: string + example: "Ubuntu20.img" + "400": + description: "No se han encontrado transmisiones UDPcast activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "No UDPCast active transmissions" + "500 (Error)": + description: "Error al comprobar las transmisiones UDPcast activas." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al comprobar las transmisiones UDPcast activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- /ogrepository/v1/uftp: + post: + summary: "Enviar una Imagen mediante UFTP" + description: > + Este endpoint envía una imagen especificada a través de UFTP, utilizando el script "sendFileUFTP.py". + Requiere que los clientes ogLive estén previamente en escucha con un daemon "UFTPD" ejecutando el script "listenUFTPD.py". + Recibe la imagen y los parámetros de configuración de transferencia, que son usados para construir la cadena de parámetros que se envía al script. + NOTA: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, + y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + tags: + - "Transferencia de Imágenes" + parameters: + - name: ID_img + in: body + required: true + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum' asociado)" + schema: + type: object + properties: + ID_img: + type: string + example: "image_id" + port: + type: string + description: "Puerto para la transmisión UFTP" + example: "9000" + ip: + type: string + description: "IP Unicast o Multicast para la transmisión" + example: "239.194.17.2" + bitrate: + type: string + description: "Velocidad de transmisión (con 'K' para Kbps, 'M' para Mbps, o 'G' para Gbps)" + example: "1G" + responses: + "200": + description: "La imagen se está enviando mediante UFTP." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Sending image..." + "400": + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found" + "500 (Error)": + description: "Error al enviar la imagen mediante UFTP." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image send failed" + "500 (Exception)": + description: "Excepción inesperada al enviar la imagen mediante UFTP." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + #/ogrepository/v1/uftp: get: summary: "Ver Estado de Transmisiones UFTP" description: > @@ -1131,9 +1399,8 @@ paths: exception: type: string example: "No UFTP active transmissions" - "500": - description: > - Error al comprobar las transmisiones UFTP activas, posiblemente debido a un error inesperado durante la ejecución del script. + "500 (Error)": + description: "Error al comprobar las transmisiones UFTP activas." schema: type: object properties: @@ -1142,9 +1409,9 @@ paths: example: false error: type: string - example: "Script execution error description." + example: "(Error description)" "500 (Exception)": - description: "Error inesperado durante la comprobación de transmisiones UFTP activas." + description: "Excepción inesperada al comprobar las transmisiones UFTP activas." schema: type: object properties: @@ -1153,85 +1420,9 @@ paths: example: false exception: type: string - example: "Unexpected error checking UFTP transmissions" - post: - summary: "Enviar una Imagen mediante UFTP" - description: > - Este endpoint envía una imagen especificada a través de UFTP, utilizando el script "sendFileUFTP.py". - Requiere que los clientes ogLive estén previamente en escucha con un daemon "UFTPD" ejecutando el script "listenUFTPD.py". - Recibe la imagen y los parámetros de configuración de transferencia, que son usados para construir la cadena de parámetros que se envía al script. - tags: - - "Transferencia de Imágenes" - parameters: - - name: ID_img - in: body - required: true - description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum' asociado)" - schema: - type: object - properties: - ID_img: - type: string - example: "image_id" - port: - type: string - description: "Puerto para la transmisión UFTP" - example: "9000" - ip: - type: string - description: "IP Unicast o Multicast para la transmisión" - example: "239.194.17.2" - bitrate: - type: string - description: "Velocidad de transmisión (con 'K' para Kbps, 'M' para Mbps, o 'G' para Gbps)" - example: "1G" - responses: - "200": - description: "La imagen se ha enviado exitosamente mediante UFTP." - schema: - type: object - properties: - success: - type: boolean - example: true - output: - type: string - example: "Image sent successfully" - "400": - description: "No se ha encontrado la imagen especificada." - schema: - type: object - properties: - success: - type: boolean - example: false - error: - type: string - example: "Image not found" - "500": - description: > - Error interno al enviar la imagen mediante UFTP. Puede ocurrir debido a un problema en la ejecución del script o una excepción inesperada. - schema: - type: object - properties: - success: - type: boolean - example: false - error: - type: string - example: "Script execution error description." - "500 (Exception)": - description: "Error inesperado durante el envío de la imagen mediante UFTP." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "Generic error description for unexpected exceptions." + example: "(Exception description)" +# ----------------------------------------------------------------------------------------------------------- /ogrepository/v1/p2p: post: @@ -1239,6 +1430,8 @@ paths: description: > Este endpoint inicia el tracker y el seeder de torrents para enviar una imagen especificada mediante P2P. Utiliza los scripts "runTorrentTracker.py" y "runTorrentSeeder.py" para iniciar el tracker y el seeder en el directorio de la imagen. + NOTA: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, + y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). tags: - "Transferencia de Imágenes" parameters: @@ -1254,7 +1447,7 @@ paths: example: "image_id" responses: "200": - description: "La imagen se está enviando exitosamente a través de P2P." + description: "La imagen se está enviando mediante P2P." schema: type: object properties: @@ -1276,9 +1469,7 @@ paths: type: string example: "Image not found" "500": - description: > - Error al intentar iniciar el tracker o el seeder para el envío P2P. - Puede ocurrir si alguno de los procesos no se inicia correctamente o si ocurre una excepción inesperada. + description: "Error al enviar la imagen mediante P2P." schema: type: object properties: @@ -1288,17 +1479,10 @@ paths: error: type: string example: "Tracker or Seeder (or both) not running" - "500 (Exception)": - description: "Error inesperado durante el proceso de envío P2P." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "Generic error description for unexpected exceptions." + +# ----------------------------------------------------------------------------------------------------------- + + #/ogrepository/v1/p2p: delete: summary: "Cancelar Transmisiones P2P" description: > @@ -1318,8 +1502,8 @@ paths: output: type: string example: "P2P transmissions canceled successfully" - "500 (Error del script)": - description: "Error en la ejecución del script durante la cancelación de las transmisiones P2P." + "500 (Error)": + description: "Error al cancelar las transmisiones P2P." schema: type: object properties: @@ -1328,9 +1512,9 @@ paths: example: false error: type: string - example: "Detailed error message from script stderr output." - "500 (Excepción general)": - description: "Excepción inesperada durante la cancelación de las transmisiones P2P." + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al cancelar las transmisiones P2P." schema: type: object properties: @@ -1339,7 +1523,9 @@ paths: example: false exception: type: string - example: "General error description for unexpected exceptions" + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- /ogrepository/v1/udpcast/images/{ID_img}: delete: @@ -1368,8 +1554,19 @@ paths: output: type: string example: "Image transmission canceled successfully" - "400": - description: "No se ha encontrado la imagen especificada o no hay transmisiones UDPcast activas para la imagen especificada." + "400 (Image not found)": + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Image not found" + "400 (No transmissions for image)": + description: "No hay transmisiones UDPcast activas para la imagen especificada." schema: type: object properties: @@ -1379,8 +1576,8 @@ paths: exception: type: string example: "No UDPCast active transmissions for specified image" - "500 (Error del script)": - description: "Error en la ejecución del script durante la cancelación de la transmisión." + "500 (Error)": + description: "Error al cancelar la transmisión UDPcast." schema: type: object properties: @@ -1389,9 +1586,9 @@ paths: example: false error: type: string - example: "Detailed error message from script stderr output." - "500 (Error en verificación)": - description: "Error inesperado al verificar las transmisiones UDPcast activas." + example: "(Error description)" + "500 (Check Exception)": + description: "Error al verificar las transmisiones UDPcast activas." schema: type: object properties: @@ -1401,7 +1598,7 @@ paths: exception: type: string example: "Unexpected error checking UDPcast transmissions" - "500 (Error en finalización)": + "500 (Finalize Exception)": description: "Error inesperado al finalizar la transmisión UDPcast." schema: type: object @@ -1412,8 +1609,8 @@ paths: exception: type: string example: "Unexpected error finalizing UDPcast transmission" - "500 (Excepción general)": - description: "Excepción inesperada durante la cancelación de la transmisión UDPcast." + "500 (General Exception)": + description: "Excepción inesperada al cancelar la transmisión UDPcast." schema: type: object properties: @@ -1422,7 +1619,9 @@ paths: example: false exception: type: string - example: "General error description for unexpected exceptions" + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- /ogrepository/v1/uftp/images/{ID_img}: delete: @@ -1451,8 +1650,19 @@ paths: output: type: string example: "Image transmission canceled successfully" - "400": - description: "No se ha encontrado la imagen especificada o no hay transmisiones UFTP activas para la imagen especificada." + "400 (Image not found)": + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Image not found" + "400 (No transmissions for image)": + description: "No hay transmisiones UFTP activas para la imagen especificada." schema: type: object properties: @@ -1462,8 +1672,8 @@ paths: exception: type: string example: "No UFTP active transmissions for specified image" - "500 (Error del script)": - description: "Error en la ejecución del script durante la cancelación de la transmisión." + "500 (Error)": + description: "Error al cancelar la transmisión UFTP." schema: type: object properties: @@ -1472,9 +1682,9 @@ paths: example: false error: type: string - example: "Detailed error message from script stderr output." - "500 (Error en verificación)": - description: "Error inesperado al verificar las transmisiones UFTP activas." + example: "(Error description)" + "500 (Check Exception)": + description: "Error al verificar las transmisiones UFTP activas." schema: type: object properties: @@ -1484,7 +1694,7 @@ paths: exception: type: string example: "Unexpected error checking UFTP transmissions" - "500 (Error en finalización)": + "500 (Finalize Exception)": description: "Error inesperado al finalizar la transmisión UFTP." schema: type: object @@ -1495,8 +1705,8 @@ paths: exception: type: string example: "Unexpected error finalizing UFTP transmission" - "500 (Excepción general)": - description: "Excepción inesperada durante la cancelación de la transmisión UFTP." + "500 (General Exception)": + description: "Excepción inesperada al cancelar la transmisión UFTP." schema: type: object properties: @@ -1505,5 +1715,6 @@ paths: example: false exception: type: string - example: "General error description for unexpected exceptions" + example: "(Exception description)" +# ----------------------------------------------------------------------------------------------------------- -- 2.40.1 From 7c29e453e64f91602188a2bf18a2fde1d78dde91 Mon Sep 17 00:00:00 2001 From: ggil Date: Tue, 5 Nov 2024 17:44:25 +0100 Subject: [PATCH 46/70] refs #1084 - Modify API and Swagger documentation --- README.md | 36 +- api/README.md | 36 +- api/repo_api.py | 2 +- api/swagger.yaml | 1379 ++++++++++++++++++++++++---------------------- 4 files changed, 746 insertions(+), 707 deletions(-) diff --git a/README.md b/README.md index 24e490f..90237d3 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/stat Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (que corresponde a todas las imágenes almacenadas en el repositorio), y en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (que corresponde a las imágenes que fueron eliminadas, que estarán en la papelera). Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint. -**NOTA**: La versión actual de este script requiere que se le pase "all" como primer parámetro (que correspondería al nombre de la imagen) y "none" como segundo parámetro (que corresponderia al nombre del subdirectorio correspondiente a la OU). Esta transformación de parámetros se realiza en la API. +**NOTA**: El script requiere que se le pase "all" como primer parámetro (que correspondería al nombre de la imagen) y "none" como segundo parámetro (que corresponderia al nombre del subdirectorio correspondiente a la OU). Esta transformación de parámetros se realiza en la API. **URL:** `/ogrepository/v1/images` **Método HTTP:** GET @@ -203,7 +203,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag Se devolverá la informacion de la imagen especificada, que puede estar en el archivo "**/opt/opengnsys/etc/repoinfo.json**" o en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (en este último caso, si la imagen está en la papelera). Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión) como primer parámetro, y el subdirectorio correspondiente a la OU (o "none" si no es el caso) como segundo parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión) como primer parámetro, y el subdirectorio correspondiente a la OU (o "none" si no es el caso) como segundo parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. **URL:** `/ogrepository/v1/images/{ID_img}` **Método HTTP:** GET @@ -211,7 +211,7 @@ Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el end **Ejemplo de Solicitud:** ```bash -curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/{ID_img} +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/22735b9070e4a8043371b8c6ae52b90d ``` **Respuestas:** @@ -273,7 +273,7 @@ curl -X PUT -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag Se comprobará la integridad del fichero de imagen especificado como parámetro. Se puede hacer con el script "**checkImage.py**", que compara el tamaño actual del archivo con el almacenado en el archivo "**.size**", y el hash MD5 del último MB del archivo con el almacenado en el archivo "**.sum**". -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. **URL:** `/ogrepository/v1/status/images/{ID_img}` **Método HTTP:** GET @@ -281,7 +281,7 @@ Se puede hacer con el script "**checkImage.py**", que compara el tamaño actual **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/status/images/{ID_img} +curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/status/images/22735b9070e4a8043371b8c6ae52b90d ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al chequear la imagen. @@ -294,7 +294,7 @@ curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/sta Se eliminará la imagen especificada como parámetro, pudiendo eliminarla permanentemente o enviarla a la papelera. Se puede hacer con el script "**deleteimage.py**", que debe ser llamado por el endpoint (y que incluye la funcionalidad "papelera"), y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y el parámetro opcional "-p" (para que la eliminación sea permanente). Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros, pero también hay que especificar el método de eliminación en la URL, como parámetro adicional. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y el parámetro opcional "-p" (para que la eliminación sea permanente). Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros, pero también hay que especificar el método de eliminación en la URL, como parámetro adicional. **URL:** `/ogrepository/v1/images/{ID_img}?method={method}` **Método HTTP:** DELETE @@ -305,7 +305,7 @@ Se puede hacer con el script "**deleteimage.py**", que debe ser llamado por el e **Ejemplo de Solicitud:** ```bash -curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/{ID_img}?method=trash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/22735b9070e4a8043371b8c6ae52b90d?method=trash ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al eliminar la imagen. @@ -317,7 +317,7 @@ curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/i Se recuperará la imagen especificada como parámetro, desde la papelera. Se puede hacer con el script "**recoverImage.py**", que debe ser llamado por el endpoint, y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. **URL:** `/ogrepository/v1/trash/images` **Método HTTP:** POST @@ -328,7 +328,7 @@ Se puede hacer con el script "**recoverImage.py**", que debe ser llamado por el **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id"}' http://example.com/ogrepository/v1/trash/images +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d"}' http://example.com/ogrepository/v1/trash/images ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al recuperar la imagen. @@ -340,7 +340,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se eliminará permanentemente la imagen especificada como parámetro, desde la papelera. Se puede hacer con el script "**deleteTrashImage.py**", que debe ser llamado por el endpoint, y que a su vez llama al script "**updateTrashInfo.py**", para actualizar la información de la papelera. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. **URL:** `/ogrepository/v1/trash/images/{ID_img}` **Método HTTP:** DELETE @@ -348,7 +348,7 @@ Se puede hacer con el script "**deleteTrashImage.py**", que debe ser llamado por **Ejemplo de Solicitud:** ```bash -curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/trash/images/{ID_img} +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/trash/images/22735b9070e4a8043371b8c6ae52b90d ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al eliminar la imagen. @@ -375,7 +375,7 @@ Se puede hacer con el script "**importImage.py**", que debe ser llamado por el e **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img","ou_subdir":"none","repo_ip":"192.168.56.100","user":"user_name"}' http://example.com/ogrepository/v1/repo/images +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none", "repo_ip":"192.168.56.100", "user":"opengnsys"}' http://example.com/ogrepository/v1/repo/images ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al importar la imagen. @@ -401,7 +401,7 @@ Se puede hacer con el script "**exportImage.py**", que debe ser llamado por el e **Ejemplo de Solicitud:** ```bash -curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id","repo_ip":"192.168.56.100","user":"user_name"}' http://example.com/ogrepository/v1/repo/images +curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d", "repo_ip":"192.168.56.100", "user":"opengnsys"}' http://example.com/ogrepository/v1/repo/images ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al exportar la imagen. @@ -479,7 +479,7 @@ Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binar **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id", "port":"9000", "method":"full", "ip":"239.194.17.2", "bitrate":"70M", "nclients":"20", "maxtime":"120"}' http://example.com/ogrepository/v1/udpcast +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d", "port":"9000", "method":"full", "ip":"239.194.17.2", "bitrate":"70M", "nclients":"20", "maxtime":"120"}' http://example.com/ogrepository/v1/udpcast ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. @@ -506,7 +506,7 @@ Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id", "port":"9000", "ip":"239.194.17.2", "bitrate":"1G"}' http://example.com/ogrepository/v1/uftp +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d", "port":"9000", "ip":"239.194.17.2", "bitrate":"1G"}' http://example.com/ogrepository/v1/uftp ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. @@ -531,7 +531,7 @@ Se puede hacer con los scripts "**runTorrentTracker.py**" y "**runTorrentSeeder. **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id"}' http://example.com/ogrepository/v1/p2p +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d"}' http://example.com/ogrepository/v1/p2p ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al intentar enviar la imagen. @@ -613,7 +613,7 @@ Se puede hacer con el script "**stopUDPcast.py**", que debe ser llamado por el e **Ejemplo de Solicitud:** ```bash -curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/udpcast/images/{ID_img} +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/udpcast/images/22735b9070e4a8043371b8c6ae52b90d ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al cancelar la transmisión UDPcast. @@ -634,7 +634,7 @@ Se puede hacer con el script "**stopUFTP.py**", que debe ser llamado por el endp **Ejemplo de Solicitud:** ```bash -curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/uftp/images/{ID_img} +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/uftp/images/22735b9070e4a8043371b8c6ae52b90d ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al cancelar la transmisión UFTP. diff --git a/api/README.md b/api/README.md index 1a05998..0d250ae 100644 --- a/api/README.md +++ b/api/README.md @@ -84,7 +84,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/stat Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (que corresponde a todas las imágenes almacenadas en el repositorio), y en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (que corresponde a las imágenes que fueron eliminadas, que estarán en la papelera). Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint. -**NOTA**: La versión actual de este script requiere que se le pase "all" como primer parámetro (que correspondería al nombre de la imagen) y "none" como segundo parámetro (que corresponderia al nombre del subdirectorio correspondiente a la OU). Esta transformación de parámetros se realiza en la API. +**NOTA**: El script requiere que se le pase "all" como primer parámetro (que correspondería al nombre de la imagen) y "none" como segundo parámetro (que corresponderia al nombre del subdirectorio correspondiente a la OU). Esta transformación de parámetros se realiza en la API. **URL:** `/ogrepository/v1/images` **Método HTTP:** GET @@ -190,7 +190,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag Se devolverá la informacion de la imagen especificada, que puede estar en el archivo "**/opt/opengnsys/etc/repoinfo.json**" o en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (en este último caso, si la imagen está en la papelera). Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión) como primer parámetro, y el subdirectorio correspondiente a la OU (o "none" si no es el caso) como segundo parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión) como primer parámetro, y el subdirectorio correspondiente a la OU (o "none" si no es el caso) como segundo parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. **URL:** `/ogrepository/v1/images/{ID_img}` **Método HTTP:** GET @@ -198,7 +198,7 @@ Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el end **Ejemplo de Solicitud:** ```bash -curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/{ID_img} +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/22735b9070e4a8043371b8c6ae52b90d ``` **Respuestas:** @@ -260,7 +260,7 @@ curl -X PUT -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag Se comprobará la integridad del fichero de imagen especificado como parámetro. Se puede hacer con el script "**checkImage.py**", que compara el tamaño actual del archivo con el almacenado en el archivo "**.size**", y el hash MD5 del último MB del archivo con el almacenado en el archivo "**.sum**". -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. **URL:** `/ogrepository/v1/status/images/{ID_img}` **Método HTTP:** GET @@ -268,7 +268,7 @@ Se puede hacer con el script "**checkImage.py**", que compara el tamaño actual **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/status/images/{ID_img} +curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/status/images/22735b9070e4a8043371b8c6ae52b90d ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al chequear la imagen. @@ -281,7 +281,7 @@ curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/sta Se eliminará la imagen especificada como parámetro, pudiendo eliminarla permanentemente o enviarla a la papelera. Se puede hacer con el script "**deleteimage.py**", que debe ser llamado por el endpoint (y que incluye la funcionalidad "papelera"), y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y el parámetro opcional "-p" (para que la eliminación sea permanente). Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros, pero también hay que especificar el método de eliminación en la URL, como parámetro adicional. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y el parámetro opcional "-p" (para que la eliminación sea permanente). Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros, pero también hay que especificar el método de eliminación en la URL, como parámetro adicional. **URL:** `/ogrepository/v1/images/{ID_img}?method={method}` **Método HTTP:** DELETE @@ -292,7 +292,7 @@ Se puede hacer con el script "**deleteimage.py**", que debe ser llamado por el e **Ejemplo de Solicitud:** ```bash -curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/{ID_img}?method=trash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/22735b9070e4a8043371b8c6ae52b90d?method=trash ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al eliminar la imagen. @@ -304,7 +304,7 @@ curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/i Se recuperará la imagen especificada como parámetro, desde la papelera. Se puede hacer con el script "**recoverImage.py**", que debe ser llamado por el endpoint, y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. **URL:** `/ogrepository/v1/trash/images` **Método HTTP:** POST @@ -315,7 +315,7 @@ Se puede hacer con el script "**recoverImage.py**", que debe ser llamado por el **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id"}' http://example.com/ogrepository/v1/trash/images +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d"}' http://example.com/ogrepository/v1/trash/images ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al recuperar la imagen. @@ -327,7 +327,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se eliminará permanentemente la imagen especificada como parámetro, desde la papelera. Se puede hacer con el script "**deleteTrashImage.py**", que debe ser llamado por el endpoint, y que a su vez llama al script "**updateTrashInfo.py**", para actualizar la información de la papelera. -**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. **URL:** `/ogrepository/v1/trash/images/{ID_img}` **Método HTTP:** DELETE @@ -335,7 +335,7 @@ Se puede hacer con el script "**deleteTrashImage.py**", que debe ser llamado por **Ejemplo de Solicitud:** ```bash -curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/trash/images/{ID_img} +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/trash/images/22735b9070e4a8043371b8c6ae52b90d ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al eliminar la imagen. @@ -362,7 +362,7 @@ Se puede hacer con el script "**importImage.py**", que debe ser llamado por el e **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img","ou_subdir":"none","repo_ip":"192.168.56.100","user":"user_name"}' http://example.com/ogrepository/v1/repo/images +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none", "repo_ip":"192.168.56.100", "user":"opengnsys"}' http://example.com/ogrepository/v1/repo/images ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al importar la imagen. @@ -388,7 +388,7 @@ Se puede hacer con el script "**exportImage.py**", que debe ser llamado por el e **Ejemplo de Solicitud:** ```bash -curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id","repo_ip":"192.168.56.100","user":"user_name"}' http://example.com/ogrepository/v1/repo/images +curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d", "repo_ip":"192.168.56.100", "user":"opengnsys"}' http://example.com/ogrepository/v1/repo/images ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al exportar la imagen. @@ -466,7 +466,7 @@ Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binar **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id", "port":"9000", "method":"full", "ip":"239.194.17.2", "bitrate":"70M", "nclients":"20", "maxtime":"120"}' http://example.com/ogrepository/v1/udpcast +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d", "port":"9000", "method":"full", "ip":"239.194.17.2", "bitrate":"70M", "nclients":"20", "maxtime":"120"}' http://example.com/ogrepository/v1/udpcast ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. @@ -493,7 +493,7 @@ Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id", "port":"9000", "ip":"239.194.17.2", "bitrate":"1G"}' http://example.com/ogrepository/v1/uftp +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d", "port":"9000", "ip":"239.194.17.2", "bitrate":"1G"}' http://example.com/ogrepository/v1/uftp ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. @@ -518,7 +518,7 @@ Se puede hacer con los scripts "**runTorrentTracker.py**" y "**runTorrentSeeder. **Ejemplo de Solicitud:** ```bash -curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"image_id"}' http://example.com/ogrepository/v1/p2p +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d"}' http://example.com/ogrepository/v1/p2p ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al intentar enviar la imagen. @@ -600,7 +600,7 @@ Se puede hacer con el script "**stopUDPcast.py**", que debe ser llamado por el e **Ejemplo de Solicitud:** ```bash -curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/udpcast/images/{ID_img} +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/udpcast/images/22735b9070e4a8043371b8c6ae52b90d ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al cancelar la transmisión UDPcast. @@ -621,7 +621,7 @@ Se puede hacer con el script "**stopUFTP.py**", que debe ser llamado por el endp **Ejemplo de Solicitud:** ```bash -curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/uftp/images/{ID_img} +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/uftp/images/22735b9070e4a8043371b8c6ae52b90d ``` **Respuestas:** - **Código 500 Internal Server Error:** Ocurrió un error al cancelar la transmisión UFTP. diff --git a/api/repo_api.py b/api/repo_api.py index 9297628..fe4f347 100644 --- a/api/repo_api.py +++ b/api/repo_api.py @@ -4,7 +4,7 @@ """ API de ogRepository, programada en Flask. -Responde a peticiones HTTP (en principio, enviadas desde ogCore) mediante endpoints, que a su vez ejecutan los scripts Python almacenados en ogRepo. +Responde a peticiones HTTP (en principio, enviadas desde ogCore) mediante endpoints, que a su vez ejecutan los scripts Python almacenados en ogRepository. En ciertos casos, transforma los parámetros recibidos desde el portal, para adaptarlos a los que es necesario enviar a los scripts (por ejemplo, a partir del ID de una imagen obtiene su nombre, su extensión y el subdirectorio de OU). diff --git a/api/swagger.yaml b/api/swagger.yaml index 9e03b87..1d6dc62 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -3,17 +3,30 @@ info: title: "OgRepository API" version: "1.0" description: | - API de ogRepository, programada en Flask. + **API de ogRepository, programada en Flask**. - Responde a peticiones HTTP (enviadas desde ogCore) mediante endpoints, que a su vez ejecutan los scripts Python almacenados en ogRepo. - En ciertos casos, transforma los parámetros recibidos desde el portal, para adaptarlos a los que es necesario enviar a los scripts - (por ejemplo, a partir del ID de una imagen obtiene su nombre, su extensión y el subdirectorio de OU). + Responde a peticiones HTTP (enviadas desde ogCore) mediante endpoints, que a su vez ejecutan los scripts Python almacenados en ogRepository. + En ciertos casos, transforma los parámetros recibidos desde el portal, para adaptarlos a los que es necesario enviar a los scripts (por ejemplo, a partir del ID de una imagen obtiene su nombre, su extensión y el subdirectorio de OU). Librerías Python requeridas: - - flask (se puede instalar con "sudo apt install python3-flask") - - paramiko (se puede instalar con "sudo apt install python3-paramiko") - - requests (se puede instalar con "sudo apt install python3-requests") - No tengo claro que para este paquete sea necesario - - flasgger (se puede instalar con "sudo apt install python3-flasgger") + - **flask** (se puede instalar con "sudo apt install python3-flask") + - **paramiko** (se puede instalar con "sudo apt install python3-paramiko") + - **requests** (se puede instalar con "sudo apt install python3-requests") - No tengo claro que para este paquete sea necesario + - **flasgger** (se puede instalar con "sudo apt install python3-flasgger") + +# ----------------------------------------------------------------------------------------------------------- + +# Esto hace que el Swagger se ordene por los tags (apartados), de la forma especificada: +tags: + - name: "Estado de ogRepository" + - name: "Información de Imágenes" + - name: "Eliminar y Recuperar Imágenes" + - name: "Transferencia de Imágenes (UDPcast)" + - name: "Transferencia de Imágenes (UFTP)" + - name: "Transferencia de Imágenes (P2P)" + - name: "Importar y Exportar Imágenes" + - name: "Varios" + # ----------------------------------------------------------------------------------------------------------- # Apartado "Estado de ogRepository" @@ -24,7 +37,7 @@ paths: get: summary: "Obtener Información de Estado de ogRepository" description: > - Este endpoint ejecuta el script "getRepoStatus.py" y devuelve su salida en formato JSON, + Este endpoint ejecuta el script "**getRepoStatus.py**" y devuelve su salida en formato JSON, incluyendo información sobre la CPU, memoria RAM, disco duro, servicios, y procesos específicos de ogRepository. tags: - "Estado de ogRepository" @@ -133,10 +146,11 @@ paths: /ogrepository/v1/images: put: summary: "Actualizar Información del Repositorio" - description: > - Este endpoint actualiza la información de las imágenes almacenadas en el repositorio, reflejándola en los archivos "repoinfo.json" y "trashinfo.json". - Utiliza el script "updateRepoInfo.py", que también ejecuta "updateTrashInfo.py". - Este proceso se realiza para mantener actualizados los datos del repositorio y la papelera, y debería ser invocado cada vez que se elimine o cree una imagen. + description: | + Este endpoint actualiza la información de las imágenes almacenadas en el repositorio, reflejándola en los archivos "**repoinfo.json**" y "**trashinfo.json**". + Utiliza el script "**updateRepoInfo.py**", que a su vez llama al script "**updateTrashInfo.py**", para actualizar también la información de la papelera. + + No hace falta que se le llame al crear o exportar una imagen, ya que lo llama el endpoint "**Crear archivos auxiliares**" (que sí debe ser llamado en esos casos). tags: - "Información de Imágenes" responses: @@ -179,11 +193,9 @@ paths: #/ogrepository/v1/images: get: summary: "Obtener Información de todas las Imágenes" - description: > - Este endpoint ejecuta el script "getRepoInfo.py" con los parámetros "all" y "none" para devolver información - de todas las imágenes almacenadas en el repositorio y en la papelera. - Devuelve detalles como el nombre de la imagen, tipo, nombre del cliente, clonador, compresor, sistema de archivos, - tamaño de los datos, tamaño de la imagen, y hashes MD5. + description: | + Este endpoint ejecuta el script "**getRepoInfo.py**" con los parámetros "**all**" y "**none**" para devolver información de todas las imágenes almacenadas en el repositorio y en la papelera. + Devuelve detalles como el nombre de la imagen, tipo, nombre del cliente, clonador, compresor, sistema de archivos, tamaño de los datos, tamaño de la imagen, y hashes MD5. tags: - "Información de Imágenes" responses: @@ -363,10 +375,10 @@ paths: /ogrepository/v1/images/{imageId}: get: summary: "Obtener Información de una Imagen concreta" - description: > - Este endpoint devuelve información de la imagen especificada mediante su ID en formato JSON. - Puede utilizar el script "getRepoInfo.py" que recibe como parámetros el nombre de la imagen con extensión y el subdirectorio correspondiente a la OU (o "none" si no es el caso). - La imagen puede estar en el archivo "/opt/opengnsys/etc/repoinfo.json" (si está almacenada) o en "/opt/opengnsys/etc/trashinfo.json" (si está en la papelera). + description: | + Este endpoint devuelve información de la imagen especificada mediante su ID, en formato JSON. + Utiliza el script "**getRepoInfo.py**" que recibe como parámetros el nombre de la imagen con extensión y el subdirectorio correspondiente a la OU (o "none" si no es el caso). + La imagen puede estar en el archivo "**repoinfo.json**" (si está almacenada en el repositorio) o en "**trashinfo.json**" (si está en la papelera). tags: - "Información de Imágenes" parameters: @@ -374,7 +386,7 @@ paths: in: path required: true type: string - description: "El ID de la imagen (corresponde al hash MD5 'fullsum' de la imagen)" + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum')" responses: "200": description: "La información de la imagen se obtuvo exitosamente." @@ -464,9 +476,9 @@ paths: /ogrepository/v1/status/images/{imageId}: get: summary: "Chequear Integridad de Imagen" - description: > + description: | Este endpoint comprueba la integridad de la imagen especificada como parámetro, comparando el tamaño y el hash MD5 del último MB con los valores almacenados en los archivos ".size" y ".sum". - Utiliza el script "checkImage.py", que recibe el nombre de la imagen (con extensión) y el subdirectorio correspondiente a la OU, si aplica. + Utiliza el script "**checkImage.py**", que recibe el nombre de la imagen (con extensión) y el subdirectorio correspondiente a la OU, si aplica. tags: - "Información de Imágenes" parameters: @@ -474,7 +486,7 @@ paths: in: path required: true type: string - description: "El ID de la imagen (corresponde al hash MD5 'fullsum' de la imagen)" + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum')" responses: "200 (Check OK)": description: "La imagen se ha chequeado exitosamente." @@ -539,11 +551,9 @@ paths: /ogrepository/v1/images/{imageId}?method={method}: delete: summary: "Eliminar una Imagen" - description: > - Este endpoint elimina la imagen especificada como parámetro (y todos sus archivos asociados), - moviéndolos a la papelera o eliminándolos permanentemente dependiendo del parámetro "method". - Utiliza el script "deleteImage.py" que recibe el nombre de la imagen (con extensión y subdirectorio correspondiente a la OU, si aplica) - como primer parámetro, y opcionalmente el parámetro "-p" para eliminación permanente. + description: | + Este endpoint elimina la imagen especificada como parámetro (y todos sus archivos asociados), moviéndolos a la papelera o eliminándolos permanentemente (dependiendo del parámetro "method"). + Utiliza el script "**deleteImage.py**" que recibe el nombre de la imagen (con extensión y subdirectorio correspondiente a la OU, si aplica) como primer parámetro, y opcionalmente el parámetro "-p" (para eliminación permanente), que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. tags: - "Eliminar y Recuperar Imágenes" parameters: @@ -551,12 +561,12 @@ paths: in: path required: true type: string - description: "El ID de la imagen (corresponde al hash MD5 'fullsum' de la imagen)" + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum')" - name: method in: query - required: false + required: true type: string - description: "Método de eliminación (puede ser 'trash' para enviar a la papelera o 'permanent' para eliminar definitivamente)" + description: "Método de eliminación (puede ser 'trash', para enviar la imagen a la papelera, o 'permanent', para eliminarla definitivamente)" responses: "200": description: "La imagen se eliminó exitosamente." @@ -608,22 +618,23 @@ paths: /ogrepository/v1/trash/images: post: summary: "Recuperar una Imagen" - description: > + description: | Este endpoint recupera la imagen especificada, moviéndola desde la papelera al repositorio de imágenes. - Utiliza el script "recoverImage.py", que recibe el nombre de la imagen (con extensión y subdirectorio correspondiente a la OU, si aplica). + Utiliza el script "**recoverImage.py**", que recibe el nombre de la imagen (con extensión y subdirectorio correspondiente a la OU, si aplica), que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. tags: - "Eliminar y Recuperar Imágenes" parameters: - - name: ID_img + - name: JSON in: body required: true - description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum' asociado)" + description: | + * **ID_img** - Identificador de la imagen, correspondiente al contenido del archivo 'full.sum' schema: type: object properties: ID_img: type: string - example: "image_id" + example: "22735b9070e4a8043371b8c6ae52b90d" responses: "200": description: "La imagen se recuperó exitosamente." @@ -675,9 +686,9 @@ paths: /ogrepository/v1/trash/images/{imageId}: delete: summary: "Eliminar una Imagen de la Papelera" - description: > - Este endpoint elimina permanentemente la imagen especificada desde la papelera. - Utiliza el script "deleteTrashImage.py", que recibe el nombre de la imagen (con extensión y subdirectorio correspondiente a la OU, si aplica). + description: | + Este endpoint elimina permanentemente la imagen especificada, desde la papelera. + Utiliza el script "**deleteTrashImage.py**", que recibe el nombre de la imagen (con extensión y subdirectorio correspondiente a la OU, si aplica), que a su vez llama al script "**updateTrashInfo.py**", para actualizar la información de la papelera. tags: - "Eliminar y Recuperar Imágenes" parameters: @@ -685,7 +696,7 @@ paths: in: path required: true type: string - description: "El ID de la imagen (corresponde al hash MD5 'fullsum' de la imagen en la papelera)" + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum')" responses: "200": description: "La imagen se eliminó exitosamente." @@ -732,6 +743,621 @@ paths: type: string example: "(Exception description)" +# ----------------------------------------------------------------------------------------------------------- +# Apartado "Transferencia de Imágenes (UDPcast)" +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/udpcast: + post: + summary: "Enviar una Imagen mediante UDPcast" + description: | + Este endpoint envía la imagen especificada a través de UDPcast, utilizando el script "**sendFileMcast.py**". + Recibe la imagen y los parámetros de configuración de transferencia, que son usados para construir la cadena de parámetros que se envía al script. + + **NOTA**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + tags: + - "Transferencia de Imágenes (UDPcast)" + parameters: + - name: JSON + in: body + required: true + description: | + * **ID_img** - Identificador de la imagen, correspondiente al contenido del archivo 'full.sum' + * **port** - Puerto Multicast + * **method** - Modalidad half-duplex o full-duplex + * **ip** - IP Multicast + * **bitrate** - Velocidad de transmisión, en Mbps + * **nclients** - Número minimo de clientes + * **maxtime** - Tiempo máximo de espera + schema: + type: object + properties: + ID_img: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + port: + type: string + description: "Puerto Multicast a utilizar para la transmisión" + example: "9000" + method: + type: string + description: "Modalidad de transmisión (puede ser 'half' o 'full' para half-duplex o full-duplex)" + example: "full" + ip: + type: string + description: "IP Multicast a la que se enviará la imagen" + example: "239.194.17.2" + bitrate: + type: string + description: "Velocidad de transmisión en Mbps" + example: "70M" + nclients: + type: integer + description: "Número mínimo de clientes" + example: 20 + maxtime: + type: integer + description: "Tiempo máximo de espera en segundos" + example: 120 + responses: + "200": + description: "La imagen se está enviando mediante UDPcast." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Sending image.." + "400": + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found" + "500 (Error)": + description: "Error al enviar la imagen mediante UDPcast." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image send failed" + "500 (Exception)": + description: "Excepción inesperada al enviar la imagen mediante UDPcast." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + #/ogrepository/v1/udpcast: + get: + summary: "Ver Estado de Transmisiones UDPcast" + description: | + Este endpoint devuelve información sobre los procesos activos de "**udp-sender**" en formato JSON, permitiendo comprobar las transferencias UDPcast activas. + Utiliza el script "**getUDPcastInfo.py**" para obtener esta información. + tags: + - "Transferencia de Imágenes (UDPcast)" + responses: + "200": + description: "La información de las transmisiones UDPcast activas se obtuvo exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: object + additionalProperties: + type: object + properties: + image_id: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + image_name: + type: string + example: "Ubuntu20.img" + "400": + description: "No se han encontrado transmisiones UDPcast activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "No UDPCast active transmissions" + "500 (Error)": + description: "Error al comprobar las transmisiones UDPcast activas." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al comprobar las transmisiones UDPcast activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/udpcast/images/{imageId}: + delete: + summary: "Cancelar Transmisión UDPcast" + description: | + Este endpoint cancela la transmisión UDPcast activa de la imagen especificada, deteniendo el proceso "**udp-sender**" asociado. + Utiliza el script "**stopUDPcast.py**" para finalizar la transmisión. + tags: + - "Transferencia de Imágenes (UDPcast)" + parameters: + - name: imageId + in: path + required: true + type: string + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum')" + responses: + "200": + description: "La transmisión UDPcast se ha cancelado exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image transmission canceled successfully" + "400 (Image not found)": + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Image not found" + "400 (No transmissions for image)": + description: "No hay transmisiones UDPcast activas para la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "No UDPCast active transmissions for specified image" + "500 (Error)": + description: "Error al cancelar la transmisión UDPcast." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Check Exception)": + description: "Error al verificar las transmisiones UDPcast activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Unexpected error checking UDPcast transmissions" + "500 (Finalize Exception)": + description: "Error inesperado al finalizar la transmisión UDPcast." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Unexpected error finalizing UDPcast transmission" + "500 (General Exception)": + description: "Excepción inesperada al cancelar la transmisión UDPcast." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- +# Apartado "Transferencia de Imágenes (UFTP)" +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/uftp: + post: + summary: "Enviar una Imagen mediante UFTP" + description: | + Este endpoint envía una imagen especificada a través de UFTP, utilizando el script "**sendFileUFTP.py**". + Requiere que los clientes ogLive estén previamente en escucha con un daemon "UFTPD", ejecutando el script "**listenUFTPD.py**". + Recibe la imagen y los parámetros de configuración de transferencia, que son usados para construir la cadena de parámetros que se envía al script. + + **NOTA**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, + y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + tags: + - "Transferencia de Imágenes (UFTP)" + parameters: + - name: JSON + in: body + required: true + description: | + * **ID_img** - Identificador de la imagen, correspondiente al contenido del archivo 'full.sum' + * **port** - Puerto Multicast + * **ip** - IP Unicast/Multicast), + * **bitrate** - Velocidad de transmisión, con 'K' para Kbps, 'M' para Mbps o 'G' para Gbps + schema: + type: object + properties: + ID_img: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + port: + type: string + description: "Puerto para la transmisión UFTP" + example: "9000" + ip: + type: string + description: "IP Unicast o Multicast para la transmisión" + example: "239.194.17.2" + bitrate: + type: string + description: "Velocidad de transmisión (con 'K' para Kbps, 'M' para Mbps, o 'G' para Gbps)" + example: "1G" + responses: + "200": + description: "La imagen se está enviando mediante UFTP." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Sending image..." + "400": + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found" + "500 (Error)": + description: "Error al enviar la imagen mediante UFTP." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image send failed" + "500 (Exception)": + description: "Excepción inesperada al enviar la imagen mediante UFTP." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + #/ogrepository/v1/uftp: + get: + summary: "Ver Estado de Transmisiones UFTP" + description: | + Este endpoint devuelve información sobre los procesos activos de "**uftp**" en formato JSON, permitiendo comprobar las transferencias UFTP activas. + Utiliza el script "**getUFTPInfo.py**" para obtener esta información. + tags: + - "Transferencia de Imágenes (UFTP)" + responses: + "200": + description: "La información de las transmisiones UFTP activas se obtuvo exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: object + additionalProperties: + type: object + properties: + image_id: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + image_name: + type: string + example: "Ubuntu20.img" + "400": + description: "No se han encontrado transmisiones UFTP activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "No UFTP active transmissions" + "500 (Error)": + description: "Error al comprobar las transmisiones UFTP activas." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al comprobar las transmisiones UFTP activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/uftp/images/{imageId}: + delete: + summary: "Cancelar Transmisión UFTP" + description: | + Este endpoint cancela la transmisión UFTP activa de una imagen especificada, deteniendo el proceso "**uftp**" asociado. + Utiliza el script "**stopUFTP.py**" para finalizar la transmisión. + tags: + - "Transferencia de Imágenes (UFTP)" + parameters: + - name: imageId + in: path + required: true + type: string + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum')" + responses: + "200": + description: "La transmisión UFTP se ha cancelado exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image transmission canceled successfully" + "400 (Image not found)": + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Image not found" + "400 (No transmissions for image)": + description: "No hay transmisiones UFTP activas para la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "No UFTP active transmissions for specified image" + "500 (Error)": + description: "Error al cancelar la transmisión UFTP." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Check Exception)": + description: "Error al verificar las transmisiones UFTP activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Unexpected error checking UFTP transmissions" + "500 (Finalize Exception)": + description: "Error inesperado al finalizar la transmisión UFTP." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Unexpected error finalizing UFTP transmission" + "500 (General Exception)": + description: "Excepción inesperada al cancelar la transmisión UFTP." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- +# Apartado "Transferencia de Imágenes (P2P)" +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/p2p: + post: + summary: "Enviar una Imagen mediante P2P" + description: | + Este endpoint inicia el tracker y el seeder de torrents para enviar una imagen especificada mediante P2P. + Utiliza los scripts "**runTorrentTracker.py**" y "**runTorrentSeeder.py**" para iniciar el tracker y el seeder en el directorio de la imagen. + + **NOTA**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + tags: + - "Transferencia de Imágenes (P2P)" + parameters: + - name: JSON + in: body + required: true + description: | + * **ID_img** - Identificador de la imagen, correspondiente al contenido del archivo 'full.sum' + schema: + type: object + properties: + ID_img: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + responses: + "200": + description: "La imagen se está enviando mediante P2P." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Tracker and Seeder serving image correctly" + "400": + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found" + "500": + description: "Error al enviar la imagen mediante P2P." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Tracker or Seeder (or both) not running" + +# ----------------------------------------------------------------------------------------------------------- + + #/ogrepository/v1/p2p: + delete: + summary: "Cancelar Transmisiones P2P" + description: | + Este endpoint cancela todas las transmisiones P2P activas, finalizando los procesos "**bttrack**" (tracker) y "**btlaunchmany.bittornado**" (seeder). + Utiliza el script "**stopP2P.py**" para detener las transmisiones P2P activas en el servidor. + tags: + - "Transferencia de Imágenes (P2P)" + responses: + "200": + description: "Las transmisiones P2P se han cancelado exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "P2P transmissions canceled successfully" + "500 (Error)": + description: "Error al cancelar las transmisiones P2P." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al cancelar las transmisiones P2P." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + # ----------------------------------------------------------------------------------------------------------- # Apartado "Importar y Exportar Imágenes" # ----------------------------------------------------------------------------------------------------------- @@ -739,19 +1365,23 @@ paths: /ogrepository/v1/repo/images: post: summary: "Importar una Imagen" - description: > + description: | Este endpoint importa la imagen especificada desde un servidor remoto al servidor local. - Utiliza el script "importImage.py", que recibe como parámetros el nombre de la imagen, la IP o hostname del servidor remoto, - el usuario para la conexión, y el subdirectorio correspondiente a la OU (si aplica). - NOTA: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está importando, - y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + Utiliza el script "**importImage.py**", que recibe como parámetros el nombre de la imagen, la IP o hostname del servidor remoto, el usuario para la conexión, y el subdirectorio correspondiente a la OU (si aplica), + que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. + + **NOTA**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está importando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). tags: - "Importar y Exportar Imágenes" parameters: - - name: image + - name: JSON in: body required: true - description: "Nombre de la imagen (con extensión)" + description: | + * **image** - Nombre de la imagen, con extensión + * **ou_subdir** - Subdirectorio correspondiente a la OU, o 'none' si no procede + * **repo_ip** - Dirección IP del servidor remoto + * **user** - Usuario con el que conectar al servidor remoto schema: type: object properties: @@ -854,19 +1484,23 @@ paths: #/ogrepository/v1/repo/images: put: summary: "Exportar una Imagen" - description: > + description: | Este endpoint exporta la imagen especificada desde el servidor local a un servidor remoto. - Utiliza el script "exportImage.py", que recibe como parámetros el nombre de la imagen, la IP o hostname del servidor remoto, - el usuario para la conexión, y el subdirectorio correspondiente a la OU (si aplica). - NOTA: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está exportando, - y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + Utiliza el script "**exportImage.py**", que recibe como parámetros el nombre de la imagen, la IP o hostname del servidor remoto, el usuario para la conexión, y el subdirectorio correspondiente a la OU (si aplica). + + Una vez que acabe, debe llamarse al endpoint "**Actualizar Información del Repositorio**" en el ogRepository destino de la exportación, para actualizar la información del repositorio. + + **NOTA**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está exportando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). tags: - "Importar y Exportar Imágenes" parameters: - - name: ID_img + - name: JSON in: body required: true - description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum' asociado)" + description: | + * **ID_img** - Identificador de la imagen, correspondiente al contenido del archivo 'full.sum' + * **repo_ip** - Dirección IP del servidor remoto + * **user** - Usuario con el que conectar al servidor remoto schema: type: object properties: @@ -978,18 +1612,22 @@ paths: /ogrepository/v1/images/torrentsum: post: summary: "Crear archivos auxiliares" - description: > - Este endpoint crea los archivos ".size", ".sum", ".full.sum" y ".torrent" para la imagen especificada. - Utiliza el script "createTorrentSum.py", que recibe como parámetro el nombre de la imagen (con subdirectorio de OU, si aplica). - NOTA: Este endpoint es asíncrono, ya que puede tardar cierto tiempo, por lo que solo informa de que se están creando los archivos auxiliares, - y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + description: | + Este endpoint crea los archivos "**size**", "**sum**", "**full.sum**" y "**torrent**" para la imagen especificada. + Utiliza el script "**createTorrentSum.py**", que recibe como parámetro el nombre de la imagen (con subdirectorio de OU, si aplica), que a su vez llama al script "**updateRepoInfo.py**, para actualizar la información del repositorio". + + Debe ser llamado cada vez que se cree una imagen desde un ogLive, y cada vez que se llame al endpoint "**Exportar una Imagen**" (en este último caso, debe ejecutarse en el ogRepository destino de la exportación). + + **NOTA**: Este endpoint es asíncrono, ya que puede tardar cierto tiempo, por lo que solo informa de que se están creando los archivos auxiliares, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). tags: - "Varios" parameters: - - name: image + - name: JSON in: body required: true - description: "Nombre de la imagen (con extensión)" + description: | + * **image** - Nombre de la imagen, con extensión + * **ou_subdir** - Subdirectorio correspondiente a la OU, o 'none' si no procede schema: type: object properties: @@ -1062,16 +1700,18 @@ paths: /ogrepository/v1/wol: post: summary: "Enviar paquete Wake On Lan" - description: > - Este endpoint envía un paquete mágico Wake On Lan (WOL) a la dirección MAC especificada, a través de la IP de broadcast especificada. - Utiliza el script "sendWakeOnLan.py", que recibe como parámetros la IP de broadcast y la dirección MAC del equipo a encender. + description: | + Este endpoint envía un paquete mágico Wake On Lan (**WOL**) a la dirección MAC especificada, a través de la IP de broadcast especificada. + Utiliza el script "**sendWakeOnLan.py**", que recibe como parámetros la IP de broadcast y la dirección MAC del equipo a encender. tags: - "Varios" parameters: - - name: broadcast_ip + - name: JSON in: body required: true - description: "IP de broadcast a la que se enviará el paquete WOL" + description: | + * **broadcast_ip** - IP de broadcast a la que se enviará el paquete WOL, que puede ser '255.255.255.255' o la IP de broadcast de una subred + * **mac** - Dirección MAC del equipo que se desea encender via Wake On Lan schema: type: object properties: @@ -1080,7 +1720,6 @@ paths: example: "255.255.255.255" mac: type: string - description: "Dirección MAC del equipo a encender" example: "00:19:99:5c:bb:bb" responses: "200": @@ -1118,603 +1757,3 @@ paths: example: "(Exception description)" # ----------------------------------------------------------------------------------------------------------- -# Apartado "Transferencia de Imágenes" -# ----------------------------------------------------------------------------------------------------------- - - /ogrepository/v1/udpcast: - post: - summary: "Enviar una Imagen mediante UDPcast" - description: > - Este endpoint envía una imagen especificada a través de UDPcast utilizando el script "sendFileMcast.py". - Recibe la imagen y los parámetros de configuración de transferencia, que son usados para construir la cadena de parámetros que se envía al script. - NOTA: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, - y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). - tags: - - "Transferencia de Imágenes" - parameters: - - name: ID_img - in: body - required: true - description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum' asociado)" - schema: - type: object - properties: - ID_img: - type: string - example: "image_id" - port: - type: string - description: "Puerto Multicast a utilizar para la transmisión" - example: "9000" - method: - type: string - description: "Modalidad de transmisión (puede ser 'half' o 'full' para half-duplex o full-duplex)" - example: "full" - ip: - type: string - description: "IP Multicast a la que se enviará la imagen" - example: "239.194.17.2" - bitrate: - type: string - description: "Velocidad de transmisión en Mbps" - example: "70M" - nclients: - type: integer - description: "Número mínimo de clientes" - example: 20 - maxtime: - type: integer - description: "Tiempo máximo de espera en segundos" - example: 120 - responses: - "200": - description: "La imagen se está enviando mediante UDPcast." - schema: - type: object - properties: - success: - type: boolean - example: true - output: - type: string - example: "Sending image.." - "400": - description: "No se ha encontrado la imagen especificada." - schema: - type: object - properties: - success: - type: boolean - example: false - error: - type: string - example: "Image not found" - "500 (Error)": - description: "Error al enviar la imagen mediante UDPcast." - schema: - type: object - properties: - success: - type: boolean - example: false - error: - type: string - example: "Image send failed" - "500 (Exception)": - description: "Excepción inesperada al enviar la imagen mediante UDPcast." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "(Exception description)" - -# ----------------------------------------------------------------------------------------------------------- - - #/ogrepository/v1/udpcast: - get: - summary: "Ver Estado de Transmisiones UDPcast" - description: > - Este endpoint devuelve información sobre los procesos activos de "udp-sender" en formato JSON, - permitiendo comprobar las transferencias UDPcast activas. Utiliza el script "getUDPcastInfo.py" para obtener esta información. - tags: - - "Transferencia de Imágenes" - responses: - "200": - description: "La información de las transmisiones UDPcast activas se obtuvo exitosamente." - schema: - type: object - properties: - success: - type: boolean - example: true - output: - type: object - additionalProperties: - type: object - properties: - image_id: - type: string - example: "22735b9070e4a8043371b8c6ae52b90d" - image_name: - type: string - example: "Ubuntu20.img" - "400": - description: "No se han encontrado transmisiones UDPcast activas." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "No UDPCast active transmissions" - "500 (Error)": - description: "Error al comprobar las transmisiones UDPcast activas." - schema: - type: object - properties: - success: - type: boolean - example: false - error: - type: string - example: "(Error description)" - "500 (Exception)": - description: "Excepción inesperada al comprobar las transmisiones UDPcast activas." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "(Exception description)" - -# ----------------------------------------------------------------------------------------------------------- - - /ogrepository/v1/uftp: - post: - summary: "Enviar una Imagen mediante UFTP" - description: > - Este endpoint envía una imagen especificada a través de UFTP, utilizando el script "sendFileUFTP.py". - Requiere que los clientes ogLive estén previamente en escucha con un daemon "UFTPD" ejecutando el script "listenUFTPD.py". - Recibe la imagen y los parámetros de configuración de transferencia, que son usados para construir la cadena de parámetros que se envía al script. - NOTA: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, - y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). - tags: - - "Transferencia de Imágenes" - parameters: - - name: ID_img - in: body - required: true - description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum' asociado)" - schema: - type: object - properties: - ID_img: - type: string - example: "image_id" - port: - type: string - description: "Puerto para la transmisión UFTP" - example: "9000" - ip: - type: string - description: "IP Unicast o Multicast para la transmisión" - example: "239.194.17.2" - bitrate: - type: string - description: "Velocidad de transmisión (con 'K' para Kbps, 'M' para Mbps, o 'G' para Gbps)" - example: "1G" - responses: - "200": - description: "La imagen se está enviando mediante UFTP." - schema: - type: object - properties: - success: - type: boolean - example: true - output: - type: string - example: "Sending image..." - "400": - description: "No se ha encontrado la imagen especificada." - schema: - type: object - properties: - success: - type: boolean - example: false - error: - type: string - example: "Image not found" - "500 (Error)": - description: "Error al enviar la imagen mediante UFTP." - schema: - type: object - properties: - success: - type: boolean - example: false - error: - type: string - example: "Image send failed" - "500 (Exception)": - description: "Excepción inesperada al enviar la imagen mediante UFTP." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "(Exception description)" - -# ----------------------------------------------------------------------------------------------------------- - - #/ogrepository/v1/uftp: - get: - summary: "Ver Estado de Transmisiones UFTP" - description: > - Este endpoint devuelve información sobre los procesos activos de "uftp" en formato JSON, - permitiendo comprobar las transferencias UFTP activas. Utiliza el script "getUFTPInfo.py" para obtener esta información. - tags: - - "Transferencia de Imágenes" - responses: - "200": - description: "La información de las transmisiones UFTP activas se obtuvo exitosamente." - schema: - type: object - properties: - success: - type: boolean - example: true - output: - type: object - additionalProperties: - type: object - properties: - image_id: - type: string - example: "22735b9070e4a8043371b8c6ae52b90d" - image_name: - type: string - example: "Ubuntu20.img" - "400": - description: "No se han encontrado transmisiones UFTP activas." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "No UFTP active transmissions" - "500 (Error)": - description: "Error al comprobar las transmisiones UFTP activas." - schema: - type: object - properties: - success: - type: boolean - example: false - error: - type: string - example: "(Error description)" - "500 (Exception)": - description: "Excepción inesperada al comprobar las transmisiones UFTP activas." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "(Exception description)" - -# ----------------------------------------------------------------------------------------------------------- - - /ogrepository/v1/p2p: - post: - summary: "Enviar una Imagen mediante P2P" - description: > - Este endpoint inicia el tracker y el seeder de torrents para enviar una imagen especificada mediante P2P. - Utiliza los scripts "runTorrentTracker.py" y "runTorrentSeeder.py" para iniciar el tracker y el seeder en el directorio de la imagen. - NOTA: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, - y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). - tags: - - "Transferencia de Imágenes" - parameters: - - name: ID_img - in: body - required: true - description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum' asociado)" - schema: - type: object - properties: - ID_img: - type: string - example: "image_id" - responses: - "200": - description: "La imagen se está enviando mediante P2P." - schema: - type: object - properties: - success: - type: boolean - example: true - output: - type: string - example: "Tracker and Seeder serving image correctly" - "400": - description: "No se ha encontrado la imagen especificada." - schema: - type: object - properties: - success: - type: boolean - example: false - error: - type: string - example: "Image not found" - "500": - description: "Error al enviar la imagen mediante P2P." - schema: - type: object - properties: - success: - type: boolean - example: false - error: - type: string - example: "Tracker or Seeder (or both) not running" - -# ----------------------------------------------------------------------------------------------------------- - - #/ogrepository/v1/p2p: - delete: - summary: "Cancelar Transmisiones P2P" - description: > - Este endpoint cancela todas las transmisiones P2P activas, finalizando los procesos "bttrack" (tracker) y "btlaunchmany.bittornado" (seeder). - Utiliza el script "stopP2P.py" para detener las transmisiones P2P activas en el servidor. - tags: - - "Transferencia de Imágenes" - responses: - "200": - description: "Las transmisiones P2P se han cancelado exitosamente." - schema: - type: object - properties: - success: - type: boolean - example: true - output: - type: string - example: "P2P transmissions canceled successfully" - "500 (Error)": - description: "Error al cancelar las transmisiones P2P." - schema: - type: object - properties: - success: - type: boolean - example: false - error: - type: string - example: "(Error description)" - "500 (Exception)": - description: "Excepción inesperada al cancelar las transmisiones P2P." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "(Exception description)" - -# ----------------------------------------------------------------------------------------------------------- - - /ogrepository/v1/udpcast/images/{ID_img}: - delete: - summary: "Cancelar Transmisión UDPcast" - description: > - Este endpoint cancela la transmisión UDPcast activa de una imagen especificada, deteniendo el proceso "udp-sender" asociado. - Utiliza el script "stopUDPcast.py" para finalizar la transmisión. - tags: - - "Transferencia de Imágenes" - parameters: - - name: ID_img - in: path - required: true - type: string - description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum' asociado)" - example: "image_id" - responses: - "200": - description: "La transmisión UDPcast se ha cancelado exitosamente." - schema: - type: object - properties: - success: - type: boolean - example: true - output: - type: string - example: "Image transmission canceled successfully" - "400 (Image not found)": - description: "No se ha encontrado la imagen especificada." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "Image not found" - "400 (No transmissions for image)": - description: "No hay transmisiones UDPcast activas para la imagen especificada." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "No UDPCast active transmissions for specified image" - "500 (Error)": - description: "Error al cancelar la transmisión UDPcast." - schema: - type: object - properties: - success: - type: boolean - example: false - error: - type: string - example: "(Error description)" - "500 (Check Exception)": - description: "Error al verificar las transmisiones UDPcast activas." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "Unexpected error checking UDPcast transmissions" - "500 (Finalize Exception)": - description: "Error inesperado al finalizar la transmisión UDPcast." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "Unexpected error finalizing UDPcast transmission" - "500 (General Exception)": - description: "Excepción inesperada al cancelar la transmisión UDPcast." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "(Exception description)" - -# ----------------------------------------------------------------------------------------------------------- - - /ogrepository/v1/uftp/images/{ID_img}: - delete: - summary: "Cancelar Transmisión UFTP" - description: > - Este endpoint cancela la transmisión UFTP activa de una imagen especificada, deteniendo el proceso "uftp" asociado. - Utiliza el script "stopUFTP.py" para finalizar la transmisión. - tags: - - "Transferencia de Imágenes" - parameters: - - name: ID_img - in: path - required: true - type: string - description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum' asociado)" - example: "image_id" - responses: - "200": - description: "La transmisión UFTP se ha cancelado exitosamente." - schema: - type: object - properties: - success: - type: boolean - example: true - output: - type: string - example: "Image transmission canceled successfully" - "400 (Image not found)": - description: "No se ha encontrado la imagen especificada." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "Image not found" - "400 (No transmissions for image)": - description: "No hay transmisiones UFTP activas para la imagen especificada." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "No UFTP active transmissions for specified image" - "500 (Error)": - description: "Error al cancelar la transmisión UFTP." - schema: - type: object - properties: - success: - type: boolean - example: false - error: - type: string - example: "(Error description)" - "500 (Check Exception)": - description: "Error al verificar las transmisiones UFTP activas." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "Unexpected error checking UFTP transmissions" - "500 (Finalize Exception)": - description: "Error inesperado al finalizar la transmisión UFTP." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "Unexpected error finalizing UFTP transmission" - "500 (General Exception)": - description: "Excepción inesperada al cancelar la transmisión UFTP." - schema: - type: object - properties: - success: - type: boolean - example: false - exception: - type: string - example: "(Exception description)" - -# ----------------------------------------------------------------------------------------------------------- -- 2.40.1 From 06a29fc9ef635727fdfbe30d861f9ada9665a708 Mon Sep 17 00:00:00 2001 From: ggil Date: Wed, 6 Nov 2024 17:26:56 +0100 Subject: [PATCH 47/70] refs #1084 - Modify API and Swagger / Add 'listenUFTP.py' --- README.md | 8 +-- api/README.md | 8 +-- api/repo_api.py | 10 ++-- api/swagger.yaml | 31 ++++++++-- bin/clients/listenUFTPD.py | 116 +++++++++++++++++++++++++++++++++++++ 5 files changed, 154 insertions(+), 19 deletions(-) create mode 100644 bin/clients/listenUFTPD.py diff --git a/README.md b/README.md index 90237d3..28af2cc 100644 --- a/README.md +++ b/README.md @@ -462,7 +462,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se enviará la imagen especificada por Multicast, mediante la aplicación UDPcast. Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binario "**udp-sender**", que es quien realmente realiza el envío. **NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). -**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo (pero no avisa a ogCore de su finalización, porque no puede comprobar cuando acaba la tarea de restauración de la imagen). **URL:** `/ogrepository/v1/udpcast` **Método HTTP:** POST @@ -470,7 +470,7 @@ Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binar **Cuerpo de la Solicitud (JSON):** - **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). - **port**: Puerto Multicast. -- **method**: Modalidad half-duplex o full-duplex. +- **method**: Modalidad half-duplex o full-duplex ("half" o "full"). - **ip**: IP Multicast. - **bitrate**: Velocidad de transmisión (en Mbps). - **nclients**: Número mínimo de clientes. @@ -492,7 +492,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se enviará la imagen especificada por Unicast o Multicast, mediante el protocolo "UFTP". Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente los clientes ogLive destino se pongan en escucha con un daemon "UFTPD" (ejecutando el script "**listenUFTPD.py**"). Esto funciona al revés que "UDPcast", ya que primero se debe ejecutar un comando en los clientes, y luego en el servidor. **NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). -**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo (pero no avisa a ogCore de su finalización, porque no puede comprobar cuando acaba la tarea de restauración de la imagen). **URL:** `/ogrepository/v1/uftp` **Método HTTP:** POST @@ -519,7 +519,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se enviará la imagen especificada mediante "P2P", iniciando el tracker y el seeder (que harán tracking y seed de los torrents contenidos en la raiz del directorio especificado). Se puede hacer con los scripts "**runTorrentTracker.py**" y "**runTorrentSeeder.py**", que deben ser llamados por el endpoint. **NOTA**: Estos scripts requieren que se les pase el directorio en el que está situada la imagen a enviar como único parámetro. Este dato se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"). -**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo (pero no avisa a ogCore de su finalización, porque no puede comprobar cuando acaba la tarea de restauración de la imagen). **URL:** `/ogrepository/v1/p2p` diff --git a/api/README.md b/api/README.md index 0d250ae..bba6fa6 100644 --- a/api/README.md +++ b/api/README.md @@ -449,7 +449,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se enviará la imagen especificada por Multicast, mediante la aplicación UDPcast. Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binario "**udp-sender**", que es quien realmente realiza el envío. **NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). -**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo (pero no avisa a ogCore de su finalización, porque no puede comprobar cuando acaba la tarea de restauración de la imagen). **URL:** `/ogrepository/v1/udpcast` **Método HTTP:** POST @@ -457,7 +457,7 @@ Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binar **Cuerpo de la Solicitud (JSON):** - **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). - **port**: Puerto Multicast. -- **method**: Modalidad half-duplex o full-duplex. +- **method**: Modalidad half-duplex o full-duplex ("half" o "full"). - **ip**: IP Multicast. - **bitrate**: Velocidad de transmisión (en Mbps). - **nclients**: Número mínimo de clientes. @@ -479,7 +479,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se enviará la imagen especificada por Unicast o Multicast, mediante el protocolo "UFTP". Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente los clientes ogLive destino se pongan en escucha con un daemon "UFTPD" (ejecutando el script "**listenUFTPD.py**"). Esto funciona al revés que "UDPcast", ya que primero se debe ejecutar un comando en los clientes, y luego en el servidor. **NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). -**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo (pero no avisa a ogCore de su finalización, porque no puede comprobar cuando acaba la tarea de restauración de la imagen). **URL:** `/ogrepository/v1/uftp` **Método HTTP:** POST @@ -506,7 +506,7 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d Se enviará la imagen especificada mediante "P2P", iniciando el tracker y el seeder (que harán tracking y seed de los torrents contenidos en la raiz del directorio especificado). Se puede hacer con los scripts "**runTorrentTracker.py**" y "**runTorrentSeeder.py**", que deben ser llamados por el endpoint. **NOTA**: Estos scripts requieren que se les pase el directorio en el que está situada la imagen a enviar como único parámetro. Este dato se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"). -**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo (pero no avisa a ogCore de su finalización, porque no puede comprobar cuando acaba la tarea de restauración de la imagen). **URL:** `/ogrepository/v1/p2p` diff --git a/api/repo_api.py b/api/repo_api.py index fe4f347..2b63b09 100644 --- a/api/repo_api.py +++ b/api/repo_api.py @@ -8,8 +8,8 @@ Responde a peticiones HTTP (en principio, enviadas desde ogCore) mediante endpoi En ciertos casos, transforma los parámetros recibidos desde el portal, para adaptarlos a los que es necesario enviar a los scripts (por ejemplo, a partir del ID de una imagen obtiene su nombre, su extensión y el subdirectorio de OU). -Librerías Python requeridas: - flask (se puede instalar con "sudo apt install python3-flask") - - paramiko (se puede instalar con "sudo apt install python3-paramiko") +Librerías Python requeridas: - flask (se puede instalar con "sudo apt install python3-flask") + - paramiko (se puede instalar con "sudo apt install python3-paramiko") - requests (se puede instalar con "sudo apt install python3-requests") - No tengo claro que para este paquete sea necesario - flasgger (se puede instalar con "sudo apt install python3-flasgger") """ @@ -233,11 +233,11 @@ def check_aux_files(image_file_path): # Almacenamos en un diccionario los datos a enviar a ogCore: data = { - 'job_id': 222, + 'job_id': 222, # Este no es un dato real (deberá pasarmelo ogCore previamente) 'image_id': image_id } - # Llamamos al endpoint de ogCore, enviando los datos: - recall_ogcore(data) + # Llamamos al endpoint de ogCore, enviando los datos (de momento comento la llamada, porque la función llama a un endpoint inexistente): + #recall_ogcore(data) break # Esperamos 10 segundos para volver a realizar la comprobación: sleep(10) diff --git a/api/swagger.yaml b/api/swagger.yaml index 1d6dc62..3a7c77b 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -3,6 +3,8 @@ info: title: "OgRepository API" version: "1.0" description: | + --- + **API de ogRepository, programada en Flask**. Responde a peticiones HTTP (enviadas desde ogCore) mediante endpoints, que a su vez ejecutan los scripts Python almacenados en ogRepository. @@ -10,10 +12,28 @@ info: Librerías Python requeridas: - **flask** (se puede instalar con "sudo apt install python3-flask") - - **paramiko** (se puede instalar con "sudo apt install python3-paramiko") + - **paramiko** (se puede instalar con "sudo apt install python3-paramiko") + - **psutil** (se puede instalar con "sudo apt install python3-psutil") - **requests** (se puede instalar con "sudo apt install python3-requests") - No tengo claro que para este paquete sea necesario - **flasgger** (se puede instalar con "sudo apt install python3-flasgger") + Paquetes APT requeridos: + - **uftp** (se puede instalar con "sudo DEBIAN_FRONTEND=noninteractive apt install uftp -y", para que no pida la ruta predeterminada) + - **ctorrent** (se puede instalar con "sudo apt install ctorrent") + - **udpcast** (se puede instalar con "sudo apt install ./udpcast_20230924_amd64.deb", apuntando al paquete) + + Para que todos los endpoints de la API funcionen con la configuración actual deben existir los siguientes directorios: + - **/opt/opengnsys/images/** + - **/opt/opengnsys/images_trash/** (debe estar en la misma unidad que el anterior, o tardarán mucho las eliminaciones y restauraciones) + - **/opt/opengnsys/bin/** (aquí deben estar todos los scripts de Python, y el binario "udp-sender") + - **/opt/opengnsys/etc/** (aquí se guardan los archivos "repoinfo.json" y "trashinfo.json") + - **/opt/opengnsys/log/** (aquí se guardan los logs) + + Y también debe existir el siguiente archivo: + - **/opt/opengnsys/etc/ogAdmRepo.cfg** (de aquí pilla la IP de ogRepo) + + --- + # ----------------------------------------------------------------------------------------------------------- # Esto hace que el Swagger se ordene por los tags (apartados), de la forma especificada: @@ -754,7 +774,7 @@ paths: Este endpoint envía la imagen especificada a través de UDPcast, utilizando el script "**sendFileMcast.py**". Recibe la imagen y los parámetros de configuración de transferencia, que son usados para construir la cadena de parámetros que se envía al script. - **NOTA**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + **NOTA**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo (pero no avisa a ogCore de su finalización, porque no puede comprobar cuando acaba la tarea de restauración de la imagen). tags: - "Transferencia de Imágenes (UDPcast)" parameters: @@ -764,7 +784,7 @@ paths: description: | * **ID_img** - Identificador de la imagen, correspondiente al contenido del archivo 'full.sum' * **port** - Puerto Multicast - * **method** - Modalidad half-duplex o full-duplex + * **method** - Modalidad half-duplex o full-duplex ("half" o "full") * **ip** - IP Multicast * **bitrate** - Velocidad de transmisión, en Mbps * **nclients** - Número minimo de clientes @@ -1016,8 +1036,7 @@ paths: Requiere que los clientes ogLive estén previamente en escucha con un daemon "UFTPD", ejecutando el script "**listenUFTPD.py**". Recibe la imagen y los parámetros de configuración de transferencia, que son usados para construir la cadena de parámetros que se envía al script. - **NOTA**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, - y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + **NOTA**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo (pero no avisa a ogCore de su finalización, porque no puede comprobar cuando acaba la tarea de restauración de la imagen). tags: - "Transferencia de Imágenes (UFTP)" parameters: @@ -1263,7 +1282,7 @@ paths: Este endpoint inicia el tracker y el seeder de torrents para enviar una imagen especificada mediante P2P. Utiliza los scripts "**runTorrentTracker.py**" y "**runTorrentSeeder.py**" para iniciar el tracker y el seeder en el directorio de la imagen. - **NOTA**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + **NOTA**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo (pero no avisa a ogCore de su finalización, porque no puede comprobar cuando acaba la tarea de restauración de la imagen). tags: - "Transferencia de Imágenes (P2P)" parameters: diff --git a/bin/clients/listenUFTPD.py b/bin/clients/listenUFTPD.py new file mode 100644 index 0000000..f20d477 --- /dev/null +++ b/bin/clients/listenUFTPD.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script hace que el cliente se ponga a escuchar en el puerto e IP Multicast especificados en el único parámetro (cuya sintaxis es "Port:IP"), con un proceso "uftpd". +Posteriormente, el servidor puede hacer una transferencia UFTP a la IP Multicast, o directamente a la IP de un cliente ogLive. +NOTA: La imagen se enviará a la ruta de la caché de los clientes (que actualmente es "/opt/opengnsys/cache"). + +Paquetes APT requeridos: "uftp" (se puede instalar con "sudo apt install uftp"). + + Parámetros +------------ +sys.argv[1] - Parámetros Multicast (en formato "Port:IP") + - Ejemplo: 9000:239.194.17.2 + + Sintaxis +---------- +./listenUFTPD.py Port:IP + + Ejemplo +--------- +./listenUFTPD.py 9000:239.194.17.2 +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +cache_path = '/opt/opengnsys/cache' +log_file = '/tmp/uftpd.log' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} Port:IP + Ejemplo: {script_name} 9000:239.194.17.2 + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta la función con más o menos de 1 parámetro, se muestra un mensaje de error, y se sale del script: + elif len(sys.argv) != 2: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 parámetro (port:ip)") + sys.exit(1) + # Si en el parámetro no hay 2 elementos (separados por ":"), se muestra un mensaje de error, y se sale del script: + param_list = sys.argv[1].split(':') + if len(param_list) != 2: + print(f"{script_name} Error: Datos Multicast incorrectos: \"{sys.argv[1]}\" (se debe especificar \"puerto:ip\")") + sys.exit(2) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Almacenamos los elementos del parámetro en variables (su formato es "puerto:ip"): + param_list = sys.argv[1].split(':') + port, ip = param_list + + # Creamos una lista con el comando a enviar (esto es requerido por la función "subprocess.run"), e impimimos el comando con espacios: + splitted_cmd = f"uftpd -M {ip} -p {port} -L {log_file} -D {cache_path} -E -K rsa:1024".split() + + print(f"Sending command: {' '.join(splitted_cmd)}") + + # Ejecutamos el comando en el sistema, e imprimimos el resultado: + try: + result = subprocess.run(splitted_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"ReturnCode: {result.returncode}") + except subprocess.CalledProcessError as error: + print(f"ReturnCode: {error.returncode}") + print(f"Error Output: {error.stderr.decode()}") + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- -- 2.40.1 From fb0dff959819637e0565ca38b14320e913081721 Mon Sep 17 00:00:00 2001 From: ggil Date: Thu, 7 Nov 2024 12:12:40 +0100 Subject: [PATCH 48/70] refs #1084 - Modify documentation --- README.md | 29 ++++++++++++++++++++++++++++- api/swagger.yaml | 21 ++++++++++++--------- packets/udpcast_20230924_amd64.deb | Bin 0 -> 56244 bytes 3 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 packets/udpcast_20230924_amd64.deb diff --git a/README.md b/README.md index 28af2cc..57fcb59 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,35 @@ Este repositorio GIT contiene la estructura de datos del repositorio de imágene - **admin** --- Archivos de configuración de ogRepository. - **api** ------ API de ogRepository. - **bin** ------ Scripts en Python 3 y binarios de gestión de ogRepository. -- **etc** ------ Ficheros y plantillas de configuración de ogRepository. +- **etc** ------ Ficheros y plantillas de configuración de ogRepository. +- **packets** - Paquetes cuya intalación es requerida. +--- + +## Requerimientos: + +Paquetes APT requeridos: + - **uftp** (se puede instalar con "sudo DEBIAN_FRONTEND=noninteractive apt install uftp -y", para que no pida la ruta predeterminada) + - **udpcast** (se puede instalar con "sudo apt install ./udpcast_20230924_amd64.deb", apuntando al paquete, que debe descargarse previamente) + - **ctorrent** (se puede instalar con "sudo apt install ctorrent") + - **bittorrent** (se puede instalar con "sudo apt install bittorrent", pero previamente hay que añadir un repositorio de Debian) + - **bittornado** (se puede instalar con "sudo apt install bittornado", pero previamente hay que añadir un repositorio de Debian) + +Librerías Python requeridas: + - **flask** (se puede instalar con "sudo apt install python3-flask") + - **paramiko** (se puede instalar con "sudo apt install python3-paramiko") + - **psutil** (se puede instalar con "sudo apt install python3-psutil") + - **flasgger** (se puede instalar con "sudo apt install python3-flasgger") + +Para que todos los endpoints y scripts funcionen con la configuración actual deben existir los siguientes directorios: + - **/opt/opengnsys/images/** + - **/opt/opengnsys/images_trash/** (debe estar en la misma unidad que el anterior, o tardarán mucho las eliminaciones y restauraciones) + - **/opt/opengnsys/bin/** (aquí deben estar todos los scripts de Python, y el binario "udp-sender") + - **/opt/opengnsys/etc/** (aquí se guardan los archivos "repoinfo.json" y "trashinfo.json") + - **/opt/opengnsys/log/** (aquí se guardan los logs) + +Y también debe existir el siguiente archivo: + - **/opt/opengnsys/etc/ogAdmRepo.cfg** (de aquí pilla la IP de ogRepository) --- diff --git a/api/swagger.yaml b/api/swagger.yaml index 3a7c77b..cf4c185 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -7,21 +7,24 @@ info: **API de ogRepository, programada en Flask**. - Responde a peticiones HTTP (enviadas desde ogCore) mediante endpoints, que a su vez ejecutan los scripts Python almacenados en ogRepository. - En ciertos casos, transforma los parámetros recibidos desde el portal, para adaptarlos a los que es necesario enviar a los scripts (por ejemplo, a partir del ID de una imagen obtiene su nombre, su extensión y el subdirectorio de OU). + Responde a peticiones HTTP mediante endpoints, que a su vez ejecutan los scripts de Python 3 almacenados en ogRepository. En el entorno real, estas peticiones HTTP se enviarán desde ogCore. + En la mayoría de los casos, transforma los parámetros recibidos para adaptarlos a los que es necesario enviar a los scripts (por ejemplo, a partir del ID de una imagen obtiene su nombre, su extensión y el subdirectorio de OU). + --- + + Paquetes APT requeridos: + - **uftp** (se puede instalar con "sudo DEBIAN_FRONTEND=noninteractive apt install uftp -y", para que no pida la ruta predeterminada) + - **udpcast** (se puede instalar con "sudo apt install ./udpcast_20230924_amd64.deb", apuntando al paquete, que debe descargarse previamente) + - **ctorrent** (se puede instalar con "sudo apt install ctorrent") + - **bittorrent** (se puede instalar con "sudo apt install bittorrent", pero previamente hay que añadir un repositorio de Debian) + - **bittornado** (se puede instalar con "sudo apt install bittornado", pero previamente hay que añadir un repositorio de Debian) + Librerías Python requeridas: - **flask** (se puede instalar con "sudo apt install python3-flask") - **paramiko** (se puede instalar con "sudo apt install python3-paramiko") - **psutil** (se puede instalar con "sudo apt install python3-psutil") - - **requests** (se puede instalar con "sudo apt install python3-requests") - No tengo claro que para este paquete sea necesario - **flasgger** (se puede instalar con "sudo apt install python3-flasgger") - Paquetes APT requeridos: - - **uftp** (se puede instalar con "sudo DEBIAN_FRONTEND=noninteractive apt install uftp -y", para que no pida la ruta predeterminada) - - **ctorrent** (se puede instalar con "sudo apt install ctorrent") - - **udpcast** (se puede instalar con "sudo apt install ./udpcast_20230924_amd64.deb", apuntando al paquete) - Para que todos los endpoints de la API funcionen con la configuración actual deben existir los siguientes directorios: - **/opt/opengnsys/images/** - **/opt/opengnsys/images_trash/** (debe estar en la misma unidad que el anterior, o tardarán mucho las eliminaciones y restauraciones) @@ -30,7 +33,7 @@ info: - **/opt/opengnsys/log/** (aquí se guardan los logs) Y también debe existir el siguiente archivo: - - **/opt/opengnsys/etc/ogAdmRepo.cfg** (de aquí pilla la IP de ogRepo) + - **/opt/opengnsys/etc/ogAdmRepo.cfg** (de aquí pilla la IP de ogRepository) --- diff --git a/packets/udpcast_20230924_amd64.deb b/packets/udpcast_20230924_amd64.deb new file mode 100644 index 0000000000000000000000000000000000000000..a6e16b807030ac6809e75b3d91db73775b05fde5 GIT binary patch literal 56244 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 Date: Thu, 7 Nov 2024 16:37:57 +0100 Subject: [PATCH 49/70] refs #631 - Modify paths in scripts and API --- README.md | 32 ++++++++--------- api/README.md | 16 ++++----- api/repo_api.py | 77 +++++++++++++++++++++++++++++----------- api/swagger.yaml | 23 ++++++------ bin/checkImage.py | 14 ++++---- bin/createTorrentSum.py | 18 +++++----- bin/deleteImage.py | 18 +++++----- bin/deleteTrashImage.py | 16 ++++----- bin/exportImage.py | 14 ++++---- bin/getRepoIface.py | 4 +-- bin/getRepoInfo.py | 4 +-- bin/getUDPcastInfo.py | 2 +- bin/getUFTPInfo.py | 2 +- bin/importImage.py | 16 ++++----- bin/recoverImage.py | 20 +++++------ bin/runTorrentSeeder.py | 2 +- bin/runTorrentTracker.py | 4 +-- bin/sendFileMcast.py | 20 +++++------ bin/sendFileUFTP.py | 16 ++++----- bin/stopUDPcast.py | 14 ++++---- bin/stopUFTP.py | 14 ++++---- bin/updateRepoInfo.py | 8 ++--- bin/updateTrashInfo.py | 8 ++--- 23 files changed, 198 insertions(+), 164 deletions(-) diff --git a/README.md b/README.md index 57fcb59..84a9c45 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Este repositorio GIT contiene la estructura de datos del repositorio de imágene - **api** ------ API de ogRepository. - **bin** ------ Scripts en Python 3 y binarios de gestión de ogRepository. - **etc** ------ Ficheros y plantillas de configuración de ogRepository. -- **packets** - Paquetes cuya intalación es requerida. +- **packets** - Paquetes cuya instalación es requerida. --- @@ -16,7 +16,7 @@ Este repositorio GIT contiene la estructura de datos del repositorio de imágene Paquetes APT requeridos: - **uftp** (se puede instalar con "sudo DEBIAN_FRONTEND=noninteractive apt install uftp -y", para que no pida la ruta predeterminada) - - **udpcast** (se puede instalar con "sudo apt install ./udpcast_20230924_amd64.deb", apuntando al paquete, que debe descargarse previamente) + - **udpcast** (se puede instalar con "sudo apt install ./udpcast_20230924_amd64.deb", apuntando al paquete) - **ctorrent** (se puede instalar con "sudo apt install ctorrent") - **bittorrent** (se puede instalar con "sudo apt install bittorrent", pero previamente hay que añadir un repositorio de Debian) - **bittornado** (se puede instalar con "sudo apt install bittornado", pero previamente hay que añadir un repositorio de Debian) @@ -28,14 +28,14 @@ Librerías Python requeridas: - **flasgger** (se puede instalar con "sudo apt install python3-flasgger") Para que todos los endpoints y scripts funcionen con la configuración actual deben existir los siguientes directorios: - - **/opt/opengnsys/images/** - - **/opt/opengnsys/images_trash/** (debe estar en la misma unidad que el anterior, o tardarán mucho las eliminaciones y restauraciones) - - **/opt/opengnsys/bin/** (aquí deben estar todos los scripts de Python, y el binario "udp-sender") - - **/opt/opengnsys/etc/** (aquí se guardan los archivos "repoinfo.json" y "trashinfo.json") - - **/opt/opengnsys/log/** (aquí se guardan los logs) + - **/opt/opengnsys/ogrepository/images/** + - **/opt/opengnsys/ogrepository/images_trash/** (debe estar en la misma partición que el anterior, o tardarán mucho las eliminaciones y restauraciones) + - **/opt/opengnsys/ogrepository/bin/** (aquí deben estar todos los scripts de Python, y el binario "udp-sender") + - **/opt/opengnsys/ogrepository/etc/** (aquí se guardan los archivos "repoinfo.json" y "trashinfo.json") + - **/opt/opengnsys/ogrepository/log/** (aquí se guardan los logs) Y también debe existir el siguiente archivo: - - **/opt/opengnsys/etc/ogAdmRepo.cfg** (de aquí pilla la IP de ogRepository) + - **/opt/opengnsys/ogrepository/etc/ogAdmRepo.cfg** (de aquí pilla la IP de ogRepository) --- @@ -122,7 +122,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/stat --- ### Obtener Información de todas las Imágenes -Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (que corresponde a todas las imágenes almacenadas en el repositorio), y en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (que corresponde a las imágenes que fueron eliminadas, que estarán en la papelera). +Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/ogrepository/etc/repoinfo.json**" (que corresponde a todas las imágenes almacenadas en el repositorio), y en el archivo "**/opt/opengnsys/ogrepository/etc/trashinfo.json**" (que corresponde a las imágenes que fueron eliminadas, que estarán en la papelera). Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint. **NOTA**: El script requiere que se le pase "all" como primer parámetro (que correspondería al nombre de la imagen) y "none" como segundo parámetro (que corresponderia al nombre del subdirectorio correspondiente a la OU). Esta transformación de parámetros se realiza en la API. @@ -142,7 +142,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag ```json { "REPOSITORY": { - "directory": "/opt/opengnsys/images", + "directory": "/opt/opengnsys/ogrepository/images", "images": [ { "name": "Ubuntu24", @@ -191,7 +191,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag } }, "TRASH": { - "directory": "/opt/opengnsys/images_trash", + "directory": "/opt/opengnsys/ogrepository/images_trash", "images": [], "ous": [ { @@ -228,7 +228,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag --- ### Obtener Información de una Imagen concreta -Se devolverá la informacion de la imagen especificada, que puede estar en el archivo "**/opt/opengnsys/etc/repoinfo.json**" o en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (en este último caso, si la imagen está en la papelera). +Se devolverá la informacion de la imagen especificada, que puede estar en el archivo "**/opt/opengnsys/ogrepository/etc/repoinfo.json**" o en el archivo "**/opt/opengnsys/ogrepository/etc/trashinfo.json**" (en este último caso, si la imagen está en la papelera). Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint. **NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión) como primer parámetro, y el subdirectorio correspondiente a la OU (o "none" si no es el caso) como segundo parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. @@ -248,7 +248,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag - **Contenido:** Información de la imagen en formato JSON. ```json { - "directory": "/opt/opengnsys/images", + "directory": "/opt/opengnsys/ogrepository/images", "images": [ { "name": "Windows10", @@ -279,7 +279,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag --- ### Actualizar Información del Repositorio -Se actualizará la información de las imágenes almacenadas en el repositorio, reflejándola en el archivo "**/opt/opengnsys/etc/repoinfo.json**". +Se actualizará la información de las imágenes almacenadas en el repositorio, reflejándola en el archivo "**/opt/opengnsys/ogrepository/etc/repoinfo.json**". Se puede hacer con el script "**updateRepoInfo.py**", que debe ser llamado por el endpoint (y que es similar al script bash original "**checkrepo**"). Este endpoint es llamado por el script "**deleteImage.py**" (para actualizar la información cada vez que se elimine una imagen), y creemos que también debe ser llamado por ogCore u ogLive cada vez que se haya creado una imagen. @@ -423,7 +423,7 @@ Se puede hacer con el script "**exportImage.py**", que debe ser llamado por el e **Cuerpo de la Solicitud (JSON):** - **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). - **repo_ip**: Dirección IP del repositorio remoto (al que se exportrará la imagen). -- **user**: Usuario con el que acceder al repositorio remoto. +- **user**: Usuario con el que acceder al repositorio remoto. **Ejemplo de Solicitud:** @@ -448,7 +448,7 @@ Se puede hacer con el script "**createTorrentSum.py**", que debe ser llamado por **Cuerpo de la Solicitud (JSON):** - **image**: Nombre de la imagen (con extensión). -- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). **Ejemplo de Solicitud:** diff --git a/api/README.md b/api/README.md index bba6fa6..91a6df3 100644 --- a/api/README.md +++ b/api/README.md @@ -82,7 +82,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/stat --- ### Obtener Información de todas las Imágenes -Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/etc/repoinfo.json**" (que corresponde a todas las imágenes almacenadas en el repositorio), y en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (que corresponde a las imágenes que fueron eliminadas, que estarán en la papelera). +Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/ogrepository/etc/repoinfo.json**" (que corresponde a todas las imágenes almacenadas en el repositorio), y en el archivo "**/opt/opengnsys/ogrepository/etc/trashinfo.json**" (que corresponde a las imágenes que fueron eliminadas, que estarán en la papelera). Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint. **NOTA**: El script requiere que se le pase "all" como primer parámetro (que correspondería al nombre de la imagen) y "none" como segundo parámetro (que corresponderia al nombre del subdirectorio correspondiente a la OU). Esta transformación de parámetros se realiza en la API. @@ -102,7 +102,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag ```json { "REPOSITORY": { - "directory": "/opt/opengnsys/images", + "directory": "/opt/opengnsys/ogrepository/images", "images": [ { "name": "Ubuntu24", @@ -151,7 +151,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag } }, "TRASH": { - "directory": "/opt/opengnsys/images_trash", + "directory": "/opt/opengnsys/ogrepository/images_trash", "images": [], "ous": [ { @@ -188,7 +188,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag --- ### Obtener Información de una Imagen concreta -Se devolverá la informacion de la imagen especificada, que puede estar en el archivo "**/opt/opengnsys/etc/repoinfo.json**" o en el archivo "**/opt/opengnsys/etc/trashinfo.json**" (en este último caso, si la imagen está en la papelera). +Se devolverá la informacion de la imagen especificada, que puede estar en el archivo "**/opt/opengnsys/ogrepository/etc/repoinfo.json**" o en el archivo "**/opt/opengnsys/ogrepository/etc/trashinfo.json**" (en este último caso, si la imagen está en la papelera). Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint. **NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión) como primer parámetro, y el subdirectorio correspondiente a la OU (o "none" si no es el caso) como segundo parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. @@ -208,7 +208,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag - **Contenido:** Información de la imagen en formato JSON. ```json { - "directory": "/opt/opengnsys/images", + "directory": "/opt/opengnsys/ogrepository/images", "images": [ { "name": "Windows10", @@ -239,7 +239,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag --- ### Actualizar Información del Repositorio -Se actualizará la información de las imágenes almacenadas en el repositorio, reflejándola en el archivo "**/opt/opengnsys/etc/repoinfo.json**". +Se actualizará la información de las imágenes almacenadas en el repositorio, reflejándola en el archivo "**/opt/opengnsys/ogrepository/etc/repoinfo.json**". Se puede hacer con el script "**updateRepoInfo.py**", que debe ser llamado por el endpoint (y que es similar al script bash original "**checkrepo**"). Este endpoint es llamado por el script "**deleteImage.py**" (para actualizar la información cada vez que se elimine una imagen), y creemos que también debe ser llamado por ogCore u ogLive cada vez que se haya creado una imagen. @@ -383,7 +383,7 @@ Se puede hacer con el script "**exportImage.py**", que debe ser llamado por el e **Cuerpo de la Solicitud (JSON):** - **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). - **repo_ip**: Dirección IP del repositorio remoto (al que se exportrará la imagen). -- **user**: Usuario con el que acceder al repositorio remoto. +- **user**: Usuario con el que acceder al repositorio remoto. **Ejemplo de Solicitud:** @@ -408,7 +408,7 @@ Se puede hacer con el script "**createTorrentSum.py**", que debe ser llamado por **Cuerpo de la Solicitud (JSON):** - **image**: Nombre de la imagen (con extensión). -- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). **Ejemplo de Solicitud:** diff --git a/api/repo_api.py b/api/repo_api.py index 2b63b09..bac5742 100644 --- a/api/repo_api.py +++ b/api/repo_api.py @@ -27,6 +27,7 @@ import paramiko import logging import threading import requests +import random # Imports para Swagger: from flasgger import Swagger import yaml @@ -36,10 +37,12 @@ import yaml # VARIABLES # -------------------------------------------------------------------------------------------- -repo_path = '/opt/opengnsys/images/' -script_path = '/opt/opengnsys/bin' -repo_file = '/opt/opengnsys/etc/repoinfo.json' -trash_file = '/opt/opengnsys/etc/trashinfo.json' +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final +script_path = '/opt/opengnsys/ogrepository/bin' +repo_file = '/opt/opengnsys/ogrepository/etc/repoinfo.json' +trash_file = '/opt/opengnsys/ogrepository/etc/trashinfo.json' + +ogcore_ip = '192.168.56.101' # En la versión final, se tendrá que pillar de una variable de entorno """ repo_path = '/home/user/images/' @@ -147,7 +150,7 @@ def search_process(process, string_to_search): # --------------------------------------------------------- -def check_lock_local(image_file_path): +def check_lock_local(image_file_path, job_id): """ Cada minuto comprueba si existe un archivo ".lock" asociado a la imagen que recibe como parámetro (lo que significará que hay una tarea en curso), en el repositorio local. Cuando no encuentre el archivo ".lock" lo comunicará a ogCore, llamando a un endpoint, @@ -158,9 +161,17 @@ def check_lock_local(image_file_path): # Creamos un bucle infinito: while True: - # Si ya no existe el archivo ".lock", imprimimos un mensaje en la API, y salimos del bucle: + # Si ya no existe el archivo ".lock", imprimimos un mensaje en la API, respondemos a ogCore y salimos del bucle: if not os.path.exists(f"{image_file_path}.lock"): - app.logger.info("Task finalized (no .lock file)") # De momento solamente imprimimos un mensaje en la API (pero debe llamar a un endpoint) + app.logger.info("Task finalized (no .lock file)") + app.logger.info(f"Job_ID: {job_id}") + + # Almacenamos en un diccionario los datos a enviar a ogCore: + data = { + 'job_id': job_id + } + # Llamamos al endpoint de ogCore, enviando los datos (de momento comento la llamada, porque la función llama a un endpoint inexistente): + #recall_ogcore(data) break # Si aun existe el archivo ".lock", imprimimos un mensaje en la API: else: @@ -172,7 +183,7 @@ def check_lock_local(image_file_path): # --------------------------------------------------------- -def check_lock_remote(image_file_path, remote_host, remote_user): +def check_lock_remote(image_file_path, remote_host, remote_user, job_id): """ Cada minuto comprueba si existe un archivo ".lock" asociado a la imagen que recibe como parámetro (lo que significará que hay una tarea en curso), en un repositorio remoto (al que conecta por SSH/SFTP). Cuando no encuentre el archivo ".lock" lo comunicará a ogCore, llamando a un endpoint, @@ -199,8 +210,16 @@ def check_lock_remote(image_file_path, remote_host, remote_user): sftp_client.stat(f"{image_file_path}.lock") app.logger.info("Task in process (.lock file exists)") except IOError: - # Si ya no existe el archivo ".lock", imprimimos un mensaje en la API, y salimos del bucle: - app.logger.info("Task finalized (no .lock file)") # De momento solamente imprimimos un mensaje en la API (pero debe llamar a un endpoint) + # Si ya no existe el archivo ".lock", imprimimos un mensaje en la API, respondemos a ogCore y salimos del bucle: + app.logger.info("Task finalized (no .lock file)") + app.logger.info(f"Job_ID: {job_id}") + + # Almacenamos en un diccionario los datos a enviar a ogCore: + data = { + 'job_id': job_id + } + # Llamamos al endpoint de ogCore, enviando los datos (de momento comento la llamada, porque la función llama a un endpoint inexistente): + #recall_ogcore(data) break # Esperamos 1 minuto para volver a realizar la comprobación: sleep(60) @@ -213,7 +232,7 @@ def check_lock_remote(image_file_path, remote_host, remote_user): # --------------------------------------------------------- -def check_aux_files(image_file_path): +def check_aux_files(image_file_path, job_id): """ Cada 10 segundos comprueba si se han creado todos los archivos auxiliares de la imagen que recibe como parámetro, en cuyo caso lo comunicará a ogCore, llamando a un endpoint, y dejará de realizar la comprobación. También obtiene el valor del archivo ".full.sum" (que corresonde al ID), y se lo comunica a ogCore. @@ -223,17 +242,18 @@ def check_aux_files(image_file_path): # Si faltan archivos auxiliares por crear, imprimimos un mensaje en la API: if not os.path.exists(f"{image_file_path}.size") or not os.path.exists(f"{image_file_path}.sum") or not os.path.exists(f"{image_file_path}.full.sum") or not os.path.exists(f"{image_file_path}.torrent") or not os.path.exists(f"{image_file_path}.info.checked"): app.logger.info("Task in process (auxiliar files remaining)") - # Si ya se han creado todos los archivos auxiliares, imprimimos un mensaje en la API, y salimos del bucle: + # Si ya se han creado todos los archivos auxiliares, imprimimos un mensaje en la API, respondemos a ogCore y salimos del bucle: else: - app.logger.info("Task finalized (all auxilar files created)") # De momento solamente imprimimos un mensaje en la API (pero debe llamar a un endpoint) + app.logger.info("Task finalized (all auxilar files created)") # Obtenemos el valor del archivo "full.sum", que corresponde al ID, y lo imprimimos: with open(f"{image_file_path}.full.sum", 'r') as file: image_id = file.read().strip('\n') + app.logger.info(f"Job_ID: {job_id}") app.logger.info(f"Image_ID: {image_id}") # Almacenamos en un diccionario los datos a enviar a ogCore: data = { - 'job_id': 222, # Este no es un dato real (deberá pasarmelo ogCore previamente) + 'job_id': job_id, 'image_id': image_id } # Llamamos al endpoint de ogCore, enviando los datos (de momento comento la llamada, porque la función llama a un endpoint inexistente): @@ -252,7 +272,7 @@ def recall_ogcore(data): que estaba corriendo en un proceso independiente (no controlado por los endpoints). """ # Almacenamos la URL del endpoint de ogCore: - endpoint_url = 'http://192.168.56.101:8006//ogcore/v1/test' + endpoint_url = f"http://{ogcore_ip}:8006//ogcore/v1/test" # Almacenamos los headers, y convertiomos "data" a JSON: headers = {'content-type': 'application/json'} @@ -635,6 +655,7 @@ def import_image(): ou_subdir = json_data.get("ou_subdir") remote_ip = json_data.get("repo_ip") remote_user = json_data.get("user") + ogcore_ip = json_data.get("ogcore_ip") # Evaluamos los parámetros obtenidos, para construir la ruta de la imagen: if ou_subdir == "none": @@ -649,16 +670,20 @@ def import_image(): # Ejecutamos el script "importImage.py" (con los parámetros almacenados), y almacenamos el resultado (pero sin esperar a que termine el proceso): result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + # Generamos el ID para identificar el trabajo asíncrono: + job_id = '{}-{}'.format ('ImportImage', ''.join(random.choice('0123456789abcdef') for _ in range(8))) + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: if result.returncode is None: # Si el resultado es correcto, llamamos a la función "check_lock_local" en un hilo paralelo # (para que compruebe si la imagen se ha acabado de importar exitosamente): - threading.Thread(target=check_lock_local, args=(image_file_path,)).start() + threading.Thread(target=check_lock_local, args=(image_file_path, job_id,)).start() # Informamos que la imagen se está importando, y salimos del endpoint: return jsonify({ "success": True, - "output": "Importing image..." + "output": "Importing image...", + "job_id": job_id }), 200 else: return jsonify({ @@ -709,6 +734,7 @@ def export_image(): image_id = json_data.get("ID_img") remote_ip = json_data.get("repo_ip") remote_user = json_data.get("user") + ogcore_ip = json_data.get("ogcore_ip") # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): param_dict = get_image_params(image_id, "repo") @@ -732,16 +758,20 @@ def export_image(): # Ejecutamos el script "exportImage.py" (con los parámetros almacenados), y almacenamos el resultado (pero sin esperar a que termine el proceso): result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + # Generamos el ID para identificar el trabajo asíncrono: + job_id = '{}-{}'.format ('ExportImage', ''.join(random.choice('0123456789abcdef') for _ in range(8))) + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: if result.returncode is None: # Si el resultado es correcto, llamamos a la función "check_lock_remote" en un hilo paralelo # (para que compruebe si la imagen se ha acabado de exportar exitosamente): - threading.Thread(target=check_lock_remote, args=(f"{repo_path}{image_file_path}", remote_ip, remote_user,)).start() + threading.Thread(target=check_lock_remote, args=(f"{repo_path}{image_file_path}", remote_ip, remote_user, job_id,)).start() # Informamos que la imagen se está exportando, y salimos del endpoint: return jsonify({ "success": True, - "output": "Exporting image..." + "output": "Exporting image...", + "job_id": job_id }), 200 else: return jsonify({ @@ -789,6 +819,7 @@ def create_torrent_sum(): json_data = json.loads(request.data) image_name = json_data.get("image") ou_subdir = json_data.get("ou_subdir") + ogcore_ip = json_data.get("ogcore_ip") # Evaluamos los parámetros obtenidos, para construir la ruta de la imagen (relativa a "repo_path"): if ou_subdir == "none": @@ -803,16 +834,20 @@ def create_torrent_sum(): # Ejecutamos el script "createTorrentSum.py" (con los parámetros almacenados), y almacenamos el resultado (pero sin esperar a que termine el proceso): result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + # Generamos el ID para identificar el trabajo asíncrono: + job_id = '{}-{}'.format ('CreateAuxiliarFiles', ''.join(random.choice('0123456789abcdef') for _ in range(8))) + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: if result.returncode is None: # Si el resultado es correcto, llamamos a la función "check_aux_files" en un hilo paralelo # (para que compruebe si se han creado todos los archivos auxiliares exitosamente): - threading.Thread(target=check_aux_files, args=(f"{repo_path}{image_file_path}",)).start() + threading.Thread(target=check_aux_files, args=(f"{repo_path}{image_file_path}", job_id,)).start() # Informamos que los archivos auxiliares se están creando, y salimos del endpoint: return jsonify({ "success": True, - "output": "Creating auxiliar files..." + "output": "Creating auxiliar files...", + "job_id": job_id }), 200 else: return jsonify({ diff --git a/api/swagger.yaml b/api/swagger.yaml index cf4c185..e30025f 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -14,7 +14,7 @@ info: Paquetes APT requeridos: - **uftp** (se puede instalar con "sudo DEBIAN_FRONTEND=noninteractive apt install uftp -y", para que no pida la ruta predeterminada) - - **udpcast** (se puede instalar con "sudo apt install ./udpcast_20230924_amd64.deb", apuntando al paquete, que debe descargarse previamente) + - **udpcast** (se puede instalar con "sudo apt install ./udpcast_20230924_amd64.deb", apuntando al paquete) - **ctorrent** (se puede instalar con "sudo apt install ctorrent") - **bittorrent** (se puede instalar con "sudo apt install bittorrent", pero previamente hay que añadir un repositorio de Debian) - **bittornado** (se puede instalar con "sudo apt install bittornado", pero previamente hay que añadir un repositorio de Debian) @@ -26,14 +26,14 @@ info: - **flasgger** (se puede instalar con "sudo apt install python3-flasgger") Para que todos los endpoints de la API funcionen con la configuración actual deben existir los siguientes directorios: - - **/opt/opengnsys/images/** - - **/opt/opengnsys/images_trash/** (debe estar en la misma unidad que el anterior, o tardarán mucho las eliminaciones y restauraciones) - - **/opt/opengnsys/bin/** (aquí deben estar todos los scripts de Python, y el binario "udp-sender") - - **/opt/opengnsys/etc/** (aquí se guardan los archivos "repoinfo.json" y "trashinfo.json") - - **/opt/opengnsys/log/** (aquí se guardan los logs) + - **/opt/opengnsys/ogrepository/images/** + - **/opt/opengnsys/ogrepository/images_trash/** (debe estar en la misma partición que el anterior, o tardarán mucho las eliminaciones y restauraciones) + - **/opt/opengnsys/ogrepository/bin/** (aquí deben estar todos los scripts de Python, y el binario "udp-sender") + - **/opt/opengnsys/ogrepository/etc/** (aquí se guardan los archivos "repoinfo.json" y "trashinfo.json") + - **/opt/opengnsys/ogrepository/log/** (aquí se guardan los logs) Y también debe existir el siguiente archivo: - - **/opt/opengnsys/etc/ogAdmRepo.cfg** (de aquí pilla la IP de ogRepository) + - **/opt/opengnsys/ogrepository/etc/ogAdmRepo.cfg** (de aquí pilla la IP de ogRepository) --- @@ -238,7 +238,7 @@ paths: properties: directory: type: string - example: "/opt/opengnsys/images" + example: "/opt/opengnsys/ogrepository/images" images: type: array items: @@ -322,7 +322,7 @@ paths: properties: directory: type: string - example: "/opt/opengnsys/images_trash" + example: "/opt/opengnsys/ogrepository/images_trash" images: type: array items: @@ -424,7 +424,7 @@ paths: properties: directory: type: string - example: "/opt/opengnsys/images" + example: "/opt/opengnsys/ogrepository/images" images: type: array items: @@ -1528,7 +1528,7 @@ paths: properties: ID_img: type: string - example: "image_id" + example: "22735b9070e4a8043371b8c6ae52b90d" repo_ip: type: string description: "Dirección IP del repositorio remoto" @@ -1658,7 +1658,6 @@ paths: example: "Windows10.img" ou_subdir: type: string - description: "Subdirectorio correspondiente a la OU (o 'none' si no es el caso)" example: "none" responses: "200": diff --git a/bin/checkImage.py b/bin/checkImage.py index d93443b..e97867d 100644 --- a/bin/checkImage.py +++ b/bin/checkImage.py @@ -9,10 +9,10 @@ Este script comprueba la integridad de la imagen que recibe como parámetro, vol ------------ sys.argv[1] - Nombre completo de la imagen a chequear (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. - Ejemplo1: image1.img - - Ejemplo2: /opt/opengnsys/images/image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images/image1.img - Ejemplo3: ou_subdir/image1.img - Ejemplo4: /ou_subdir/image1.img - - Ejemplo5: /opt/opengnsys/images/ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images/ou_subdir/image1.img Sintaxis ---------- @@ -21,10 +21,10 @@ sys.argv[1] - Nombre completo de la imagen a chequear (con o sin ruta), pero inc Ejemplos --------- ./checkImage.py image1.img -./checkImage.py /opt/opengnsys/images/image1.img +./checkImage.py /opt/opengnsys/ogrepository/images/image1.img ./checkImage.py ou_subdir/image1.img ./checkImage.py /ou_subdir/image1.img -./checkImage.py /opt/opengnsys/images/ou_subdir/image1.img +./checkImage.py /opt/opengnsys/ogrepository/images/ou_subdir/image1.img """ # -------------------------------------------------------------------------------------------- @@ -41,7 +41,7 @@ import hashlib # -------------------------------------------------------------------------------------------- script_name = os.path.basename(__file__) -repo_path = '/opt/opengnsys/images/' +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final # -------------------------------------------------------------------------------------------- @@ -55,10 +55,10 @@ def show_help(): help_text = f""" Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name Ejemplo1: {script_name} image1.img - Ejemplo2: {script_name} /opt/opengnsys/images/image1.img + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images/image1.img Ejemplo3: {script_name} ou_subdir/image1.img Ejemplo4: {script_name} /ou_subdir/image1.img - Ejemplo5: {script_name} /opt/opengnsys/images/ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images/ou_subdir/image1.img """ print(help_text) diff --git a/bin/createTorrentSum.py b/bin/createTorrentSum.py index d4973a9..83d2a0e 100644 --- a/bin/createTorrentSum.py +++ b/bin/createTorrentSum.py @@ -12,10 +12,10 @@ Debería ser llamado por ogCore u ogLive cada vez que se cree una imagen. ------------ sys.argv[1] - Nombre completo de la imagen (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. - Ejemplo1: image1.img - - Ejemplo2: /opt/opengnsys/images/image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images/image1.img - Ejemplo3: ou_subdir/image1.img - Ejemplo4: /ou_subdir/image1.img - - Ejemplo5: /opt/opengnsys/images/ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images/ou_subdir/image1.img Sintaxis ---------- @@ -24,10 +24,10 @@ sys.argv[1] - Nombre completo de la imagen (con o sin ruta), pero incluyendo el Ejemplos --------- ./createTorrentSum.py image1.img -./createTorrentSum.py /opt/opengnsys/images/image1.img +./createTorrentSum.py /opt/opengnsys/ogrepository/images/image1.img ./createTorrentSum.py ou_subdir/image1.img ./createTorrentSum.py /ou_subdir/image1.img -./createTorrentSum.py /opt/opengnsys/images/ou_subdir/image1.img +./createTorrentSum.py /opt/opengnsys/ogrepository/images/ou_subdir/image1.img """ # -------------------------------------------------------------------------------------------- @@ -45,9 +45,9 @@ import hashlib # -------------------------------------------------------------------------------------------- script_name = os.path.basename(__file__) -repo_path = '/opt/opengnsys/images/' -config_file = '/opt/opengnsys/etc/ogAdmRepo.cfg' -update_repo_script = '/opt/opengnsys/bin/updateRepoInfo.py' +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final +config_file = '/opt/opengnsys/ogrepository/etc/ogAdmRepo.cfg' +update_repo_script = '/opt/opengnsys/ogrepository/bin/updateRepoInfo.py' @@ -62,10 +62,10 @@ def show_help(): help_text = f""" Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name Ejemplo1: {script_name} image1.img - Ejemplo2: {script_name} /opt/opengnsys/images/image1.img + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images/image1.img Ejemplo3: {script_name} ou_subdir/image1.img Ejemplo4: {script_name} /ou_subdir/image1.img - Ejemplo5: {script_name} /opt/opengnsys/images/ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images/ou_subdir/image1.img """ print(help_text) diff --git a/bin/deleteImage.py b/bin/deleteImage.py index 5b512b5..d51f0f1 100644 --- a/bin/deleteImage.py +++ b/bin/deleteImage.py @@ -11,10 +11,10 @@ Llama al script "updateRepoInfo.py", para actualizar la información del reposit ------------ sys.argv[1] - Nombre completo de la imagen a eliminar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. - Ejemplo1: image1.img - - Ejemplo2: /opt/opengnsys/images/image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images/image1.img - Ejemplo3: ou_subdir/image1.img - Ejemplo4: /ou_subdir/image1.img - - Ejemplo5: /opt/opengnsys/images/ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images/ou_subdir/image1.img sys.argv[2] - Parámetro opcional para especificar que la eliminación sea permanente (sin papelera). - Ejemplo: -p @@ -26,10 +26,10 @@ sys.argv[2] - Parámetro opcional para especificar que la eliminación sea perma Ejemplos --------- ./deleteImage.py image1.img -p -./deleteImage.py /opt/opengnsys/images/image1.img -p +./deleteImage.py /opt/opengnsys/ogrepository/images/image1.img -p ./deleteImage.py ou_subdir/image1.img -p ./deleteImage.py /ou_subdir/image1.img -./deleteImage.py /opt/opengnsys/images/ou_subdir/image1.img +./deleteImage.py /opt/opengnsys/ogrepository/images/ou_subdir/image1.img """ # -------------------------------------------------------------------------------------------- @@ -49,9 +49,9 @@ import subprocess # -------------------------------------------------------------------------------------------- script_name = os.path.basename(__file__) -repo_path = '/opt/opengnsys/images/' -trash_path = '/opt/opengnsys/images_trash/' -update_repo_script = '/opt/opengnsys/bin/updateRepoInfo.py' +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final +trash_path = '/opt/opengnsys/ogrepository/images_trash/' +update_repo_script = '/opt/opengnsys/ogrepository/bin/updateRepoInfo.py' # -------------------------------------------------------------------------------------------- @@ -66,10 +66,10 @@ def show_help(): help_text = f""" Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name [-p] Ejemplo1: {script_name} image1.img -p - Ejemplo2: {script_name} /opt/opengnsys/images/image1.img -p + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images/image1.img -p Ejemplo3: {script_name} ou_subdir/image1.img -p Ejemplo4: {script_name} /ou_subdir/image1.img - Ejemplo5: {script_name} /opt/opengnsys/images/ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images/ou_subdir/image1.img """ print(help_text) diff --git a/bin/deleteTrashImage.py b/bin/deleteTrashImage.py index df18372..8f9a4bb 100644 --- a/bin/deleteTrashImage.py +++ b/bin/deleteTrashImage.py @@ -9,10 +9,10 @@ Llama al script "updateTrashInfo.py", para actualizar la información de las im ------------ sys.argv[1] - Nombre completo de la imagen a eliminar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. - Ejemplo1: image1.img - - Ejemplo2: /opt/opengnsys/images_trash/image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images_trash/image1.img - Ejemplo3: ou_subdir/image1.img - Ejemplo4: /ou_subdir/image1.img - - Ejemplo5: /opt/opengnsys/images_trash/ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images_trash/ou_subdir/image1.img Sintaxis ---------- @@ -21,10 +21,10 @@ sys.argv[1] - Nombre completo de la imagen a eliminar (con o sin ruta), pero inc Ejemplos --------- ./deleteTrashImage.py image1.img -./deleteTrashImage.py /opt/opengnsys/images_trash/image1.img +./deleteTrashImage.py /opt/opengnsys/ogrepository/images_trash/image1.img ./deleteTrashImage.py ou_subdir/image1.img ./deleteTrashImage.py /ou_subdir/image1.img -./deleteTrashImage.py /opt/opengnsys/images_trash/ou_subdir/image1.img +./deleteTrashImage.py /opt/opengnsys/ogrepository/images_trash/ou_subdir/image1.img """ # -------------------------------------------------------------------------------------------- @@ -41,8 +41,8 @@ import subprocess # -------------------------------------------------------------------------------------------- script_name = os.path.basename(__file__) -trash_path = '/opt/opengnsys/images_trash/' -update_trash_script = '/opt/opengnsys/bin/updateTrashInfo.py' +trash_path = '/opt/opengnsys/ogrepository/images_trash/' # No borrar la barra final +update_trash_script = '/opt/opengnsys/ogrepository/bin/updateTrashInfo.py' # -------------------------------------------------------------------------------------------- @@ -57,10 +57,10 @@ def show_help(): help_text = f""" Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name Ejemplo1: {script_name} image1.img - Ejemplo2: {script_name} /opt/opengnsys/images_trash/image1.img + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images_trash/image1.img Ejemplo3: {script_name} ou_subdir/image1.img Ejemplo4: {script_name} /ou_subdir/image1.img - Ejemplo5: {script_name} /opt/opengnsys/images_trash/ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images_trash/ou_subdir/image1.img """ print(help_text) diff --git a/bin/exportImage.py b/bin/exportImage.py index 15eba23..c54bfdb 100644 --- a/bin/exportImage.py +++ b/bin/exportImage.py @@ -13,10 +13,10 @@ Librerías Python requeridas: "paramiko" (se puede instalar con "sudo apt instal ------------ sys.argv[1] - Nombre completo de la imagen a exportar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. - Ejemplo1: image1.img - - Ejemplo2: /opt/opengnsys/images/image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images/image1.img - Ejemplo3: ou_subdir/image1.img - Ejemplo4: /ou_subdir/image1.img - - Ejemplo5: /opt/opengnsys/images/ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images/ou_subdir/image1.img sys.argv[2] - IP o hostname del repositorio remoto. - Ejemplo1: 192.168.56.100 @@ -33,10 +33,10 @@ sys.argv[3] - Usuario con el que conectar al repositorio remoto. Ejemplos --------- ./exportImage.py image1.img 192.168.56.100 user -./exportImage.py /opt/opengnsys/images/image1.img 192.168.56.100 user +./exportImage.py /opt/opengnsys/ogrepository/images/image1.img 192.168.56.100 user ./exportImage.py ou_subdir/image1.img remote_hostname user ./exportImage.py /ou_subdir/image1.img remote_hostname root -./exportImage.py /opt/opengnsys/images/ou_subdir/image1.img remote_hostname root +./exportImage.py /opt/opengnsys/ogrepository/images/ou_subdir/image1.img remote_hostname root """ # -------------------------------------------------------------------------------------------- @@ -57,7 +57,7 @@ import warnings # -------------------------------------------------------------------------------------------- script_name = os.path.basename(__file__) -repo_path = '/opt/opengnsys/images/' +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final # -------------------------------------------------------------------------------------------- @@ -71,10 +71,10 @@ def show_help(): help_text = f""" Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name remote_host remote_user Ejemplo1: {script_name} image1.img 192.168.56.100 user - Ejemplo2: {script_name} /opt/opengnsys/images/image1.img 192.168.56.100 user + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images/image1.img 192.168.56.100 user Ejemplo3: {script_name} ou_subdir/image1.img remote_hostname user Ejemplo4: {script_name} /ou_subdir/image1.img remote_hostname root - Ejemplo5: {script_name} /opt/opengnsys/images/ou_subdir/image1.img remote_hostname root + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images/ou_subdir/image1.img remote_hostname root """ print(help_text) diff --git a/bin/getRepoIface.py b/bin/getRepoIface.py index f0d105b..84284e2 100644 --- a/bin/getRepoIface.py +++ b/bin/getRepoIface.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ -Este script obtiene y devuelve la interfaz de red asociada a la IP especificada en el archivo "/opt/opengnsys/etc/ogAdmRepo.cfg" (en la clave "IPlocal"). +Este script obtiene y devuelve la interfaz de red asociada a la IP especificada en el archivo "/opt/opengnsys/ogrepository/etc/ogAdmRepo.cfg" (en la clave "IPlocal"). En principio, debería hacer lo mismo que el script bash original (cuyo nombre es "getRepoIface", a secas). No recibe ningún parámetro, y siempre es llamado por otros scripts, que necesitan dicha interfaz (por ejemplo, "sendFileMcast"). @@ -21,7 +21,7 @@ import struct # VARIABLES # -------------------------------------------------------------------------------------------- -config_file = '/opt/opengnsys/etc/ogAdmRepo.cfg' +config_file = '/opt/opengnsys/ogrepository/etc/ogAdmRepo.cfg' # -------------------------------------------------------------------------------------------- diff --git a/bin/getRepoInfo.py b/bin/getRepoInfo.py index 2c235bd..c8a2b46 100644 --- a/bin/getRepoInfo.py +++ b/bin/getRepoInfo.py @@ -40,8 +40,8 @@ import json # -------------------------------------------------------------------------------------------- script_name = os.path.basename(__file__) -repo_file = '/opt/opengnsys/etc/repoinfo.json' -trash_file = '/opt/opengnsys/etc/trashinfo.json' +repo_file = '/opt/opengnsys/ogrepository/etc/repoinfo.json' +trash_file = '/opt/opengnsys/ogrepository/etc/trashinfo.json' # -------------------------------------------------------------------------------------------- diff --git a/bin/getUDPcastInfo.py b/bin/getUDPcastInfo.py index 5fa272f..f525a96 100644 --- a/bin/getUDPcastInfo.py +++ b/bin/getUDPcastInfo.py @@ -23,7 +23,7 @@ import sys # VARIABLES # -------------------------------------------------------------------------------------------- -repo_path = '/opt/opengnsys/images/' +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final # -------------------------------------------------------------------------------------------- diff --git a/bin/getUFTPInfo.py b/bin/getUFTPInfo.py index a34ee21..6d0579e 100644 --- a/bin/getUFTPInfo.py +++ b/bin/getUFTPInfo.py @@ -23,7 +23,7 @@ import sys # VARIABLES # -------------------------------------------------------------------------------------------- -repo_path = '/opt/opengnsys/images/' +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final # -------------------------------------------------------------------------------------------- diff --git a/bin/importImage.py b/bin/importImage.py index 31a1a2e..227c86c 100644 --- a/bin/importImage.py +++ b/bin/importImage.py @@ -13,10 +13,10 @@ Librerías Python requeridas: "paramiko" (se puede instalar con "sudo apt instal ------------ sys.argv[1] - Nombre completo de la imagen a importar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. - Ejemplo1: image1.img - - Ejemplo2: /opt/opengnsys/images/image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images/image1.img - Ejemplo3: ou_subdir/image1.img - Ejemplo4: /ou_subdir/image1.img - - Ejemplo5: /opt/opengnsys/images/ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images/ou_subdir/image1.img sys.argv[2] - IP o hostname del repositorio remoto. - Ejemplo1: 192.168.56.100 @@ -33,10 +33,10 @@ sys.argv[3] - Usuario con el que conectar al repositorio remoto. Ejemplos --------- ./importImage.py image1.img 192.168.56.100 user -./importImage.py /opt/opengnsys/images/image1.img 192.168.56.100 user +./importImage.py /opt/opengnsys/ogrepository/images/image1.img 192.168.56.100 user ./importImage.py ou_subdir/image1.img remote_hostname user ./importImage.py /ou_subdir/image1.img remote_hostname root -./importImage.py /opt/opengnsys/images/ou_subdir/image1.img remote_hostname root +./importImage.py /opt/opengnsys/ogrepository/images/ou_subdir/image1.img remote_hostname root """ # -------------------------------------------------------------------------------------------- @@ -57,8 +57,8 @@ import warnings # -------------------------------------------------------------------------------------------- script_name = os.path.basename(__file__) -repo_path = '/opt/opengnsys/images/' -update_repo_script = '/opt/opengnsys/bin/updateRepoInfo.py' +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final +update_repo_script = '/opt/opengnsys/ogrepository/bin/updateRepoInfo.py' # -------------------------------------------------------------------------------------------- @@ -72,10 +72,10 @@ def show_help(): help_text = f""" Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name remote_host remote_user Ejemplo1: {script_name} image1.img 192.168.56.100 user - Ejemplo2: {script_name} /opt/opengnsys/images/image1.img 192.168.56.100 user + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images/image1.img 192.168.56.100 user Ejemplo3: {script_name} ou_subdir/image1.img remote_hostname user Ejemplo4: {script_name} /ou_subdir/image1.img remote_hostname root - Ejemplo5: {script_name} /opt/opengnsys/images/ou_subdir/image1.img remote_hostname root + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images/ou_subdir/image1.img remote_hostname root """ print(help_text) diff --git a/bin/recoverImage.py b/bin/recoverImage.py index 970401b..e68b683 100644 --- a/bin/recoverImage.py +++ b/bin/recoverImage.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ -Este script recupera la imagen que recibe como parámetro (y todos sus archivos asociados), moviendo los archivos a "/opt/opengnsys/images", desde la papelera +Este script recupera la imagen que recibe como parámetro (y todos sus archivos asociados), moviendo los archivos a "/opt/opengnsys/ogrepository/images", desde la papelera (respetando el subdirectorio correspondiente a la OU, si fuera el caso). Llama al script "updateRepoInfo.py", para actualizar la información del repositorio. @@ -10,10 +10,10 @@ Llama al script "updateRepoInfo.py", para actualizar la información del reposit ------------ sys.argv[1] - Nombre completo de la imagen a recuperar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. - Ejemplo1: image1.img - - Ejemplo2: /opt/opengnsys/images_trash/image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images_trash/image1.img - Ejemplo3: ou_subdir/image1.img - Ejemplo4: /ou_subdir/image1.img - - Ejemplo5: /opt/opengnsys/images_trash/ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images_trash/ou_subdir/image1.img Sintaxis ---------- @@ -22,10 +22,10 @@ sys.argv[1] - Nombre completo de la imagen a recuperar (con o sin ruta), pero in Ejemplos --------- ./recoverImage.py image1.img -./recoverImage.py /opt/opengnsys/images_trash/image1.img +./recoverImage.py /opt/opengnsys/ogrepository/images_trash/image1.img ./recoverImage.py ou_subdir/image1.img ./recoverImage.py /ou_subdir/image1.img -./recoverImage.py /opt/opengnsys/images_trash/ou_subdir/image1.img +./recoverImage.py /opt/opengnsys/ogrepository/images_trash/ou_subdir/image1.img """ # -------------------------------------------------------------------------------------------- @@ -43,9 +43,9 @@ import subprocess # -------------------------------------------------------------------------------------------- script_name = os.path.basename(__file__) -repo_path = '/opt/opengnsys/images/' -trash_path = '/opt/opengnsys/images_trash/' -update_repo_script = '/opt/opengnsys/bin/updateRepoInfo.py' +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final +trash_path = '/opt/opengnsys/ogrepository/images_trash/' # No borrar la barra final +update_repo_script = '/opt/opengnsys/ogrepository/bin/updateRepoInfo.py' # -------------------------------------------------------------------------------------------- @@ -59,10 +59,10 @@ def show_help(): help_text = f""" Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name Ejemplo1: {script_name} image1.img - Ejemplo2: {script_name} /opt/opengnsys/images_trash/image1.img + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images_trash/image1.img Ejemplo3: {script_name} ou_subdir/image1.img Ejemplo4: {script_name} /ou_subdir/image1.img - Ejemplo5: {script_name} /opt/opengnsys/images_trash/ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images_trash/ou_subdir/image1.img """ print(help_text) diff --git a/bin/runTorrentSeeder.py b/bin/runTorrentSeeder.py index 2ebd64c..11b8735 100644 --- a/bin/runTorrentSeeder.py +++ b/bin/runTorrentSeeder.py @@ -37,7 +37,7 @@ import subprocess # -------------------------------------------------------------------------------------------- script_name = os.path.basename(__file__) -repo_path = '/opt/opengnsys/images' +repo_path = '/opt/opengnsys/ogrepository/images' # En este caso, no lleva barra final # -------------------------------------------------------------------------------------------- diff --git a/bin/runTorrentTracker.py b/bin/runTorrentTracker.py index 9a5d6c4..48f89cc 100644 --- a/bin/runTorrentTracker.py +++ b/bin/runTorrentTracker.py @@ -38,11 +38,11 @@ import time # -------------------------------------------------------------------------------------------- script_name = os.path.basename(__file__) -repo_path = '/opt/opengnsys/images' +repo_path = '/opt/opengnsys/ogrepository/images' # En este caso, no lleva barra final bttrack_port = 6969 bttrack_dfile = '/tmp/dstate' -bttrack_log = '/opt/opengnsys/log/bttrack.log' +bttrack_log = '/opt/opengnsys/ogrepository/log/bttrack.log' bttrack_interval = 10 bttrack_allow_get = 0 # Este valor impide la descarga desde clientes no autorizados diff --git a/bin/sendFileMcast.py b/bin/sendFileMcast.py index 81fa35b..452769a 100644 --- a/bin/sendFileMcast.py +++ b/bin/sendFileMcast.py @@ -9,10 +9,10 @@ En principio, debería hacer lo mismo que el script bash original (cuyo nombre e ------------ sys.argv[1] - Nombre completo de la imagen a enviar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. - Ejemplo1: image1.img - - Ejemplo2: /opt/opengnsys/images/image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images/image1.img - Ejemplo3: ou_subdir/image1.img - Ejemplo4: /ou_subdir/image1.img - - Ejemplo5: /opt/opengnsys/images/ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images/ou_subdir/image1.img sys.argv[2] - Parámetros Multicast (en formato "Port:Duplex:IP:Mpbs:Nclients:Timeout") - Ejemplo: 9000:full:239.194.17.2:70M:20:120 @@ -24,10 +24,10 @@ sys.argv[2] - Parámetros Multicast (en formato "Port:Duplex:IP:Mpbs:Nclients:Ti Ejemplos --------- ./sendFileMcast.py image1.img 9000:full:239.194.17.2:70M:20:120 -./sendFileMcast.py /opt/opengnsys/images/image1.img 9000:full:239.194.17.2:70M:20:120 +./sendFileMcast.py /opt/opengnsys/ogrepository/images/image1.img 9000:full:239.194.17.2:70M:20:120 ./sendFileMcast.py ou_subdir/image1.img 9000:full:239.194.17.2:70M:20:120 ./sendFileMcast.py /ou_subdir/image1.img 9000:full:239.194.17.2:70M:20:120 -./sendFileMcast.py /opt/opengnsys/images/ou_subdir/image1.img 9000:full:239.194.17.2:70M:20:120 +./sendFileMcast.py /opt/opengnsys/ogrepository/images/ou_subdir/image1.img 9000:full:239.194.17.2:70M:20:120 """ # -------------------------------------------------------------------------------------------- @@ -44,10 +44,10 @@ import subprocess # -------------------------------------------------------------------------------------------- script_name = os.path.basename(__file__) -repo_path = '/opt/opengnsys/images/' -bin_path = '/opt/opengnsys/bin/' -repo_iface_script = '/opt/opengnsys/bin/getRepoIface.py' -log_file = '/opt/opengnsys/log/udpcast.log' +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final +bin_path = '/opt/opengnsys/ogrepository/bin/' # No borrar la barra final +repo_iface_script = '/opt/opengnsys/ogrepository/bin/getRepoIface.py' +log_file = '/opt/opengnsys/ogrepository/log/udpcast.log' # -------------------------------------------------------------------------------------------- @@ -61,10 +61,10 @@ def show_help(): help_text = f""" Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name Port:Duplex:IP:Mpbs:Nclients:Timeout Ejemplo1: {script_name} image1.img 9000:full-duplex:239.194.17.2:70M:20:120 - Ejemplo2: {script_name} /opt/opengnsys/images/image1.img 9000:full-duplex:239.194.17.2:70M:20:120 + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images/image1.img 9000:full-duplex:239.194.17.2:70M:20:120 Ejemplo3: {script_name} ou_subdir/image1.img 9000:full-duplex:239.194.17.2:70M:20:120 Ejemplo4: {script_name} /ou_subdir/image1.img 9000:full-duplex:239.194.17.2:70M:20:120 - Ejemplo5: {script_name} /opt/opengnsys/images/ou_subdir/image1.img 9000:full-duplex:239.194.17.2:70M:20:120 + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images/ou_subdir/image1.img 9000:full-duplex:239.194.17.2:70M:20:120 """ print(help_text) diff --git a/bin/sendFileUFTP.py b/bin/sendFileUFTP.py index dabd726..85c9851 100644 --- a/bin/sendFileUFTP.py +++ b/bin/sendFileUFTP.py @@ -13,10 +13,10 @@ Paquetes APT requeridos: "uftp" (se puede instalar con "sudo apt install uftp"). ------------ sys.argv[1] - Nombre completo de la imagen a enviar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. - Ejemplo1: image1.img - - Ejemplo2: /opt/opengnsys/images/image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images/image1.img - Ejemplo3: ou_subdir/image1.img - Ejemplo4: /ou_subdir/image1.img - - Ejemplo5: /opt/opengnsys/images/ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images/ou_subdir/image1.img sys.argv[2] - Parámetros Multicast/Unicast (en formato "Port:IP:Bitrate") - Ejemplo1: 9000:239.194.17.2:100M @@ -29,10 +29,10 @@ sys.argv[2] - Parámetros Multicast/Unicast (en formato "Port:IP:Bitrate") Ejemplos --------- ./sendFileUFTP.py image1.img 9000:239.194.17.2:100M -./sendFileUFTP.py /opt/opengnsys/images/image1.img 9000:239.194.17.2:100M +./sendFileUFTP.py /opt/opengnsys/ogrepository/images/image1.img 9000:239.194.17.2:100M ./sendFileUFTP.py ou_subdir/image1.img 9000:192.168.56.101:1G ./sendFileUFTP.py /ou_subdir/image1.img 9000:192.168.56.101:1G -./sendFileUFTP.py /opt/opengnsys/images/ou_subdir/image1.img 9000:192.168.56.101:1G +./sendFileUFTP.py /opt/opengnsys/ogrepository/images/ou_subdir/image1.img 9000:192.168.56.101:1G """ # -------------------------------------------------------------------------------------------- @@ -49,9 +49,9 @@ import subprocess # -------------------------------------------------------------------------------------------- script_name = os.path.basename(__file__) -repo_path = '/opt/opengnsys/images/' +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final cache_path = '/opt/opengnsys/cache' -log_file = '/opt/opengnsys/log/uftp.log' +log_file = '/opt/opengnsys/ogrepository/log/uftp.log' # -------------------------------------------------------------------------------------------- @@ -65,10 +65,10 @@ def show_help(): help_text = f""" Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name Port:IP:Bitrate Ejemplo1: {script_name} image1.img 9000:239.194.17.2:100M - Ejemplo2: {script_name} /opt/opengnsys/images/image1.img 9000:239.194.17.2:100M + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images/image1.img 9000:239.194.17.2:100M Ejemplo3: {script_name} ou_subdir/image1.img 9000:192.168.56.101:1G Ejemplo4: {script_name} /ou_subdir/image1.img 9000:192.168.56.101:1G - Ejemplo5: {script_name} /opt/opengnsys/images/ou_subdir/image1.img 9000:192.168.56.101:1G + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images/ou_subdir/image1.img 9000:192.168.56.101:1G """ print(help_text) diff --git a/bin/stopUDPcast.py b/bin/stopUDPcast.py index f972925..83dbb91 100644 --- a/bin/stopUDPcast.py +++ b/bin/stopUDPcast.py @@ -10,10 +10,10 @@ Este script finaliza el proceso "udp-sender" asociado a la imagen que recibe com ------------ sys.argv[1] - Nombre completo de la imagen a cancelar su transmisión (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. - Ejemplo1: image1.img - - Ejemplo2: /opt/opengnsys/images/image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images/image1.img - Ejemplo3: ou_subdir/image1.img - Ejemplo4: /ou_subdir/image1.img - - Ejemplo5: /opt/opengnsys/images/ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images/ou_subdir/image1.img Sintaxis ---------- @@ -22,10 +22,10 @@ sys.argv[1] - Nombre completo de la imagen a cancelar su transmisión (con o sin Ejemplos --------- ./stopUDPcast.py image1.img -./stopUDPcast.py /opt/opengnsys/images/image1.img +./stopUDPcast.py /opt/opengnsys/ogrepository/images/image1.img ./stopUDPcast.py ou_subdir/image1.img ./stopUDPcast.py /ou_subdir/image1.img -./stopUDPcast.py /opt/opengnsys/images/ou_subdir/image1.img +./stopUDPcast.py /opt/opengnsys/ogrepository/images/ou_subdir/image1.img """ # -------------------------------------------------------------------------------------------- @@ -42,7 +42,7 @@ import subprocess # -------------------------------------------------------------------------------------------- script_name = os.path.basename(__file__) -repo_path = '/opt/opengnsys/images/' +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final # -------------------------------------------------------------------------------------------- @@ -56,10 +56,10 @@ def show_help(): help_text = f""" Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name Ejemplo1: {script_name} image1.img - Ejemplo2: {script_name} /opt/opengnsys/images/image1.img + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images/image1.img Ejemplo3: {script_name} ou_subdir/image1.img Ejemplo4: {script_name} /ou_subdir/image1.img - Ejemplo5: {script_name} /opt/opengnsys/images/ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images/ou_subdir/image1.img """ print(help_text) diff --git a/bin/stopUFTP.py b/bin/stopUFTP.py index 377f652..18fa7b9 100644 --- a/bin/stopUFTP.py +++ b/bin/stopUFTP.py @@ -10,10 +10,10 @@ Este script finaliza el proceso "uftp" asociado a la imagen que recibe como par ------------ sys.argv[1] - Nombre completo de la imagen a cancelar su transmisión (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. - Ejemplo1: image1.img - - Ejemplo2: /opt/opengnsys/images/image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images/image1.img - Ejemplo3: ou_subdir/image1.img - Ejemplo4: /ou_subdir/image1.img - - Ejemplo5: /opt/opengnsys/images/ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images/ou_subdir/image1.img Sintaxis ---------- @@ -22,10 +22,10 @@ sys.argv[1] - Nombre completo de la imagen a cancelar su transmisión (con o sin Ejemplos --------- ./stopUDPcast.py image1.img -./stopUDPcast.py /opt/opengnsys/images/image1.img +./stopUDPcast.py /opt/opengnsys/ogrepository/images/image1.img ./stopUDPcast.py ou_subdir/image1.img ./stopUDPcast.py /ou_subdir/image1.img -./stopUDPcast.py /opt/opengnsys/images/ou_subdir/image1.img +./stopUDPcast.py /opt/opengnsys/ogrepository/images/ou_subdir/image1.img """ # -------------------------------------------------------------------------------------------- @@ -42,7 +42,7 @@ import subprocess # -------------------------------------------------------------------------------------------- script_name = os.path.basename(__file__) -repo_path = '/opt/opengnsys/images/' +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final # -------------------------------------------------------------------------------------------- @@ -56,10 +56,10 @@ def show_help(): help_text = f""" Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name Ejemplo1: {script_name} image1.img - Ejemplo2: {script_name} /opt/opengnsys/images/image1.img + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images/image1.img Ejemplo3: {script_name} ou_subdir/image1.img Ejemplo4: {script_name} /ou_subdir/image1.img - Ejemplo5: {script_name} /opt/opengnsys/images/ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images/ou_subdir/image1.img """ print(help_text) diff --git a/bin/updateRepoInfo.py b/bin/updateRepoInfo.py index dab0d65..ca498d7 100644 --- a/bin/updateRepoInfo.py +++ b/bin/updateRepoInfo.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ -Este script actualiza la información de las imágenes del repositorio, reflejándola en el archivo "/opt/opengnsys/etc/repoinfo.json", +Este script actualiza la información de las imágenes del repositorio, reflejándola en el archivo "/opt/opengnsys/ogrepository/etc/repoinfo.json", añadiendo información de las imágenes nuevas, y borrando la información de las imágenes que fueron eliminadas. La información es obtenida desde archivos ".info", que originalmente eran eliminados, pero que en esta versión son renombrados a ".info.checked", obteniendo la misma funcionalidad. @@ -27,9 +27,9 @@ import shutil # VARIABLES # -------------------------------------------------------------------------------------------- -repo_path = '/opt/opengnsys/images' -info_file = '/opt/opengnsys/etc/repoinfo.json' -update_trash_script = '/opt/opengnsys/bin/updateTrashInfo.py' +repo_path = '/opt/opengnsys/ogrepository/images' # En este caso, no lleva barra final +info_file = '/opt/opengnsys/ogrepository/etc/repoinfo.json' +update_trash_script = '/opt/opengnsys/ogrepository/bin/updateTrashInfo.py' # -------------------------------------------------------------------------------------------- diff --git a/bin/updateTrashInfo.py b/bin/updateTrashInfo.py index 626fa44..bc0d3ea 100644 --- a/bin/updateTrashInfo.py +++ b/bin/updateTrashInfo.py @@ -2,10 +2,10 @@ # -*- coding: utf-8 -*- """ -Este script actualiza la información de las imágenes de la papelera del repositorio, reflejándola en el archivo "/opt/opengnsys/etc/trashinfo.json", +Este script actualiza la información de las imágenes de la papelera del repositorio, reflejándola en el archivo "/opt/opengnsys/ogrepository/etc/trashinfo.json", añadiendo información de las nuevas imágenes eliminadas, y borrando la información de las imágenes que fueron restauradas (o que ya no están en la papelera). La información es obtenida desde archivos ".info.checked", cuyo nombre original era ".info" - (pero que fueron renombrados al ser insertada su información en el archivo ""/opt/opengnsys/etc/repoinfo.json", antes de ser eliminadas). + (pero que fueron renombrados al ser insertada su información en el archivo ""/opt/opengnsys/ogrepository/etc/repoinfo.json", antes de ser eliminadas). No recibe ningún parámetro, y no necesita ser llamado explícitamente (porque lo llama el script "updateRepoInfo.py")". """ @@ -27,8 +27,8 @@ import shutil # VARIABLES # -------------------------------------------------------------------------------------------- -trash_path = '/opt/opengnsys/images_trash' -info_file = '/opt/opengnsys/etc/trashinfo.json' +trash_path = '/opt/opengnsys/ogrepository/images_trash' # En este caso, no lleva barra final +info_file = '/opt/opengnsys/ogrepository/etc/trashinfo.json' # -------------------------------------------------------------------------------------------- -- 2.40.1 From a367aed25ab64623fb0f2e15a0f1352c359738eb Mon Sep 17 00:00:00 2001 From: ggil Date: Thu, 7 Nov 2024 16:57:02 +0100 Subject: [PATCH 50/70] refs #631 - API correction --- api/repo_api.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/api/repo_api.py b/api/repo_api.py index bac5742..08ef053 100644 --- a/api/repo_api.py +++ b/api/repo_api.py @@ -655,7 +655,6 @@ def import_image(): ou_subdir = json_data.get("ou_subdir") remote_ip = json_data.get("repo_ip") remote_user = json_data.get("user") - ogcore_ip = json_data.get("ogcore_ip") # Evaluamos los parámetros obtenidos, para construir la ruta de la imagen: if ou_subdir == "none": @@ -734,7 +733,6 @@ def export_image(): image_id = json_data.get("ID_img") remote_ip = json_data.get("repo_ip") remote_user = json_data.get("user") - ogcore_ip = json_data.get("ogcore_ip") # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): param_dict = get_image_params(image_id, "repo") @@ -819,7 +817,6 @@ def create_torrent_sum(): json_data = json.loads(request.data) image_name = json_data.get("image") ou_subdir = json_data.get("ou_subdir") - ogcore_ip = json_data.get("ogcore_ip") # Evaluamos los parámetros obtenidos, para construir la ruta de la imagen (relativa a "repo_path"): if ou_subdir == "none": -- 2.40.1 From b3e31219dcff3a531066fc696ee2a50e39434432 Mon Sep 17 00:00:00 2001 From: ggil Date: Fri, 8 Nov 2024 11:43:37 +0100 Subject: [PATCH 51/70] refs #631 - Modify 'getRepoInfo.py' --- README.md | 4 ++-- api/README.md | 4 ++-- api/swagger.yaml | 4 ++-- bin/getRepoInfo.py | 21 +++++++++++++++++++++ 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 84a9c45..21ca53a 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/stat ### Obtener Información de todas las Imágenes Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/ogrepository/etc/repoinfo.json**" (que corresponde a todas las imágenes almacenadas en el repositorio), y en el archivo "**/opt/opengnsys/ogrepository/etc/trashinfo.json**" (que corresponde a las imágenes que fueron eliminadas, que estarán en la papelera). -Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint. +Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint, que a su vez llama al script "**updateRepoInfo.py**", para actualizar previamente la información del repositorio. **NOTA**: El script requiere que se le pase "all" como primer parámetro (que correspondería al nombre de la imagen) y "none" como segundo parámetro (que corresponderia al nombre del subdirectorio correspondiente a la OU). Esta transformación de parámetros se realiza en la API. **URL:** `/ogrepository/v1/images` @@ -229,7 +229,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag ### Obtener Información de una Imagen concreta Se devolverá la informacion de la imagen especificada, que puede estar en el archivo "**/opt/opengnsys/ogrepository/etc/repoinfo.json**" o en el archivo "**/opt/opengnsys/ogrepository/etc/trashinfo.json**" (en este último caso, si la imagen está en la papelera). -Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint. +Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint, que a su vez llama al script "**updateRepoInfo.py**", para actualizar previamente la información del repositorio. **NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión) como primer parámetro, y el subdirectorio correspondiente a la OU (o "none" si no es el caso) como segundo parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. **URL:** `/ogrepository/v1/images/{ID_img}` diff --git a/api/README.md b/api/README.md index 91a6df3..ee5d44e 100644 --- a/api/README.md +++ b/api/README.md @@ -83,7 +83,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/stat ### Obtener Información de todas las Imágenes Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/ogrepository/etc/repoinfo.json**" (que corresponde a todas las imágenes almacenadas en el repositorio), y en el archivo "**/opt/opengnsys/ogrepository/etc/trashinfo.json**" (que corresponde a las imágenes que fueron eliminadas, que estarán en la papelera). -Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint. +Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint, que a su vez llama al script "**updateRepoInfo.py**", para actualizar previamente la información del repositorio. **NOTA**: El script requiere que se le pase "all" como primer parámetro (que correspondería al nombre de la imagen) y "none" como segundo parámetro (que corresponderia al nombre del subdirectorio correspondiente a la OU). Esta transformación de parámetros se realiza en la API. **URL:** `/ogrepository/v1/images` @@ -189,7 +189,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/imag ### Obtener Información de una Imagen concreta Se devolverá la informacion de la imagen especificada, que puede estar en el archivo "**/opt/opengnsys/ogrepository/etc/repoinfo.json**" o en el archivo "**/opt/opengnsys/ogrepository/etc/trashinfo.json**" (en este último caso, si la imagen está en la papelera). -Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint. +Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint, que a su vez llama al script "**updateRepoInfo.py**", para actualizar previamente la información del repositorio. **NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión) como primer parámetro, y el subdirectorio correspondiente a la OU (o "none" si no es el caso) como segundo parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. **URL:** `/ogrepository/v1/images/{ID_img}` diff --git a/api/swagger.yaml b/api/swagger.yaml index e30025f..33232d4 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -217,7 +217,7 @@ paths: get: summary: "Obtener Información de todas las Imágenes" description: | - Este endpoint ejecuta el script "**getRepoInfo.py**" con los parámetros "**all**" y "**none**" para devolver información de todas las imágenes almacenadas en el repositorio y en la papelera. + Este endpoint ejecuta el script "**getRepoInfo.py**" con los parámetros "**all**" y "**none**" para devolver información de todas las imágenes almacenadas en el repositorio y en la papelera, que a su vez llama al script "**updateRepoInfo.py**", para actualizar previamente la información del repositorio. Devuelve detalles como el nombre de la imagen, tipo, nombre del cliente, clonador, compresor, sistema de archivos, tamaño de los datos, tamaño de la imagen, y hashes MD5. tags: - "Información de Imágenes" @@ -400,7 +400,7 @@ paths: summary: "Obtener Información de una Imagen concreta" description: | Este endpoint devuelve información de la imagen especificada mediante su ID, en formato JSON. - Utiliza el script "**getRepoInfo.py**" que recibe como parámetros el nombre de la imagen con extensión y el subdirectorio correspondiente a la OU (o "none" si no es el caso). + Utiliza el script "**getRepoInfo.py**" que recibe como parámetros el nombre de la imagen con extensión y el subdirectorio correspondiente a la OU (o "none" si no es el caso), que a su vez llama al script "**updateRepoInfo.py**", para actualizar previamente la información del repositorio. La imagen puede estar en el archivo "**repoinfo.json**" (si está almacenada en el repositorio) o en "**trashinfo.json**" (si está en la papelera). tags: - "Información de Imágenes" diff --git a/bin/getRepoInfo.py b/bin/getRepoInfo.py index c8a2b46..011a165 100644 --- a/bin/getRepoInfo.py +++ b/bin/getRepoInfo.py @@ -4,6 +4,7 @@ """ Este script devuelve información (en formato json) de todas las imágenes contenidas en el repositorio (incluída la papelera), o de la imagen que se especifique como primer parámetro (debiendo especificar también el subdirectorio de OU como segundo parámetro, si procede). +Previamente, llama al script "updateRepoInfo.py", para actualizar la información del repositorio (para evitar que dé error si no hay ninguna, por ejemplo). Parámetros ------------ @@ -32,6 +33,7 @@ sys.argv[2] - Subdirectorio correspondiente a la OU, o "none" si no procede.. import os import sys +import subprocess import json @@ -42,6 +44,7 @@ import json script_name = os.path.basename(__file__) repo_file = '/opt/opengnsys/ogrepository/etc/repoinfo.json' trash_file = '/opt/opengnsys/ogrepository/etc/trashinfo.json' +update_repo_script = '/opt/opengnsys/ogrepository/bin/updateRepoInfo.py' # -------------------------------------------------------------------------------------------- @@ -79,6 +82,21 @@ def check_params(): +def update_repo_info(): + """ Actualiza la información del repositorio, ejecutando el script "updateRepoInfo.py". + Como se ve, es necesario que el script se ejecute como sudo, o dará error. + """ + try: + result = subprocess.run(['sudo', 'python3', update_repo_script], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except subprocess.CalledProcessError as error: + print(f"Error Output: {error.stderr.decode()}") + sys.exit(3) + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + sys.exit(4) + + + def get_all_info(repo_data, trash_data): """ Imprime un json con la información de todo el repositorio, con todas las imágenes que contiene, incluyendo las imágenes que fueron eliminadas (que estarán en la papelera). @@ -163,6 +181,9 @@ def main(): # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: check_params() + # Actualizamos la información del repositorio, ejecutando el script "updateRepoInfo.py": + update_repo_info() + # Almacenamos la información de las imágenes del repositorio, en la variable "repo_data" # (solo si el archivo tiene contenido, o dará error): if os.path.getsize(repo_file) > 0: -- 2.40.1 From 5c233f6c112fac177a0853cec5b0487cd122c23e Mon Sep 17 00:00:00 2001 From: ggil Date: Fri, 8 Nov 2024 14:03:11 +0100 Subject: [PATCH 52/70] refs #631 - Modify 'repo_api.py' --- api/repo_api.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/repo_api.py b/api/repo_api.py index 08ef053..665133f 100644 --- a/api/repo_api.py +++ b/api/repo_api.py @@ -42,7 +42,7 @@ script_path = '/opt/opengnsys/ogrepository/bin' repo_file = '/opt/opengnsys/ogrepository/etc/repoinfo.json' trash_file = '/opt/opengnsys/ogrepository/etc/trashinfo.json' -ogcore_ip = '192.168.56.101' # En la versión final, se tendrá que pillar de una variable de entorno +ogcore_ip = '172.17.8.26' # En la versión final, se tendrá que pillar de una variable de entorno """ repo_path = '/home/user/images/' @@ -271,8 +271,8 @@ def recall_ogcore(data): Se utiliza para informar a ogCore del resultado de una tarea asíncrona, que estaba corriendo en un proceso independiente (no controlado por los endpoints). """ - # Almacenamos la URL del endpoint de ogCore: - endpoint_url = f"http://{ogcore_ip}:8006//ogcore/v1/test" + # Almacenamos la URL del endpoint de ogCore (prueba): + endpoint_url = f"http://{ogcore_ip}:8006/ogcore/v1/test" # Almacenamos los headers, y convertiomos "data" a JSON: headers = {'content-type': 'application/json'} @@ -670,7 +670,7 @@ def import_image(): result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') # Generamos el ID para identificar el trabajo asíncrono: - job_id = '{}-{}'.format ('ImportImage', ''.join(random.choice('0123456789abcdef') for _ in range(8))) + job_id = f"ImportImage_{''.join(random.choice('0123456789abcdef') for char in range(8))}" # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: if result.returncode is None: @@ -757,7 +757,7 @@ def export_image(): result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') # Generamos el ID para identificar el trabajo asíncrono: - job_id = '{}-{}'.format ('ExportImage', ''.join(random.choice('0123456789abcdef') for _ in range(8))) + job_id = f"ExportImage_{''.join(random.choice('0123456789abcdef') for char in range(8))}" # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: if result.returncode is None: @@ -832,7 +832,7 @@ def create_torrent_sum(): result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') # Generamos el ID para identificar el trabajo asíncrono: - job_id = '{}-{}'.format ('CreateAuxiliarFiles', ''.join(random.choice('0123456789abcdef') for _ in range(8))) + job_id = f"CreateAuxiliarFiles_{''.join(random.choice('0123456789abcdef') for char in range(8))}" # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: if result.returncode is None: -- 2.40.1 From 377da58b04e385f384a53c03efa5f3f11a32660a Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Mon, 11 Nov 2024 09:31:34 +0100 Subject: [PATCH 53/70] First installer version --- installer/files/ctorrent.sources | 6 +++ installer/files/ogrepo-api.service | 12 +++++ installer/installer.sh | 70 ++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 installer/files/ctorrent.sources create mode 100644 installer/files/ogrepo-api.service create mode 100644 installer/installer.sh diff --git a/installer/files/ctorrent.sources b/installer/files/ctorrent.sources new file mode 100644 index 0000000..dc5dea0 --- /dev/null +++ b/installer/files/ctorrent.sources @@ -0,0 +1,6 @@ +Types: deb +URIs: http://ftp.de.debian.org/debian +Suites: buster +Components: main +Signed-By: /usr/share/keyrings/debian-archive-buster-stable.gpg + diff --git a/installer/files/ogrepo-api.service b/installer/files/ogrepo-api.service new file mode 100644 index 0000000..f127ffa --- /dev/null +++ b/installer/files/ogrepo-api.service @@ -0,0 +1,12 @@ +[Unit] +Description=Gunicorn instance to serve repo_api +After=network.target + +[Service] +User=root +Group=root +WorkingDirectory=/opt/opengnsys/ogrepository/api +ExecStart=/usr/bin/gunicorn -w 4 -b 0.0.0.0:8006 repo_api:app + +[Install] +WantedBy=multi-user.target diff --git a/installer/installer.sh b/installer/installer.sh new file mode 100644 index 0000000..19a9b55 --- /dev/null +++ b/installer/installer.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +set -e + +GIT_BRANCH=$1 +GIT_REPO=https://ognproject.evlt.uma.es/gitea/opengnsys/ogrepository.git +INSTALL_DIR=/opt/opengnsys/ogrepository +DEBIAN_FRONTEND=noninteractive +export DEBIAN_FRONTEND + +clone_repository() { + local BRANCH=$1 + git clone -b "$BRANCH" $GIT_REPO /tmp/ogrepository + chown -R ogrepository:ogrepository /tmp/ogrepository +} + +check_root() { + if [ "$(id -u)" != "0" ]; then + echo "This script must be run as root" 1>&2 + exit 1 + fi +} + +add_user_ogrepository() { + useradd -r -s /bin/bash ogrepository +} + +create_directories() { + mkdir -p $INSTALL_DIR + mkdir -p $INSTALL_DIR/images $INSTALL_DIR/images_trash/ $INSTALL_DIR/bin/ $INSTALL_DIR/etc/ $INSTALL_DIR/log/ + chown -R ogrepository:ogrepository $INSTALL_DIR +} + +install_dependencies() { + apt update -y + apt install -y git python3 python3-pip python3-flask python3-paramiko python3-psutil python3-flasgger debian-archive-keyring samba gunicorn +} + +install_ext_repo() { + cp installer/files/ctorrent.sources /etc/apt/sources.list.d/ctorrent.sources + apt update -y +} + +install_external_packages() { + apt install -y bittorrent bittornado ctorrent +} + +install_ogrepo-api_service() { + cp -r installer/files/ogrepo-api.service /etc/systemd/system/ogrepo-api.service + systemctl enable --now ogrepo-api + systemctl start ogrepository +} + +install_files() { + install bin/* /opt/opengnsys/bin/ + install bin/clients/* /opt/opengnsys/bin/clients + install etc/* /opt/opengnsys/etc/ + install api/* /opt/opengnsys/api +} + +## Main program +check_root +install_dependencies +add_user_ogrepository +git_clone_repository "$GIT_BRANCH" +create_directories +install_ogrepo-api_service + + + -- 2.40.1 From 853ade27769b3f8d780e53343473b67998b2af03 Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Mon, 11 Nov 2024 09:40:41 +0100 Subject: [PATCH 54/70] Fix user in service --- installer/files/ogrepo-api.service | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/installer/files/ogrepo-api.service b/installer/files/ogrepo-api.service index f127ffa..136de86 100644 --- a/installer/files/ogrepo-api.service +++ b/installer/files/ogrepo-api.service @@ -3,8 +3,8 @@ Description=Gunicorn instance to serve repo_api After=network.target [Service] -User=root -Group=root +User=ogrepository +Group=ogrepository WorkingDirectory=/opt/opengnsys/ogrepository/api ExecStart=/usr/bin/gunicorn -w 4 -b 0.0.0.0:8006 repo_api:app -- 2.40.1 From 56e7418c716b7324227e65d20b7692e546453527 Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Mon, 11 Nov 2024 09:45:48 +0100 Subject: [PATCH 55/70] Fix service --- installer/installer.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer/installer.sh b/installer/installer.sh index 19a9b55..595ac69 100644 --- a/installer/installer.sh +++ b/installer/installer.sh @@ -62,7 +62,7 @@ install_files() { check_root install_dependencies add_user_ogrepository -git_clone_repository "$GIT_BRANCH" +clone_repository "$GIT_BRANCH" create_directories install_ogrepo-api_service -- 2.40.1 From 18d7bd4ddfae9c12a977e5587d873587801b5019 Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Mon, 11 Nov 2024 09:48:42 +0100 Subject: [PATCH 56/70] Enable ssl no verify for GIT --- installer/installer.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/installer/installer.sh b/installer/installer.sh index 595ac69..c609cda 100644 --- a/installer/installer.sh +++ b/installer/installer.sh @@ -4,9 +4,11 @@ set -e GIT_BRANCH=$1 GIT_REPO=https://ognproject.evlt.uma.es/gitea/opengnsys/ogrepository.git +GIT_SSL_NO_VERIFY=true INSTALL_DIR=/opt/opengnsys/ogrepository DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND +export GIT_SSL_NO_VERIFY clone_repository() { local BRANCH=$1 -- 2.40.1 From 2e78e3d0124c997674a29a902ae84913a3f91e07 Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Mon, 11 Nov 2024 09:51:50 +0100 Subject: [PATCH 57/70] Fixes download dir --- installer/installer.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/installer/installer.sh b/installer/installer.sh index c609cda..53c462e 100644 --- a/installer/installer.sh +++ b/installer/installer.sh @@ -6,14 +6,15 @@ GIT_BRANCH=$1 GIT_REPO=https://ognproject.evlt.uma.es/gitea/opengnsys/ogrepository.git GIT_SSL_NO_VERIFY=true INSTALL_DIR=/opt/opengnsys/ogrepository +DOWNLOAD_DIR=/tmp/ogrepository DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND export GIT_SSL_NO_VERIFY clone_repository() { local BRANCH=$1 - git clone -b "$BRANCH" $GIT_REPO /tmp/ogrepository - chown -R ogrepository:ogrepository /tmp/ogrepository + git clone -b "$BRANCH" $GIT_REPO $DOWNLOAD_DIR + chown -R ogrepository:ogrepository $DOWNLOAD_DIR } check_root() { @@ -48,12 +49,14 @@ install_external_packages() { } install_ogrepo-api_service() { + cd $DOWNLOAD_DIR cp -r installer/files/ogrepo-api.service /etc/systemd/system/ogrepo-api.service systemctl enable --now ogrepo-api systemctl start ogrepository } install_files() { + cd $DOWNLOAD_DIR install bin/* /opt/opengnsys/bin/ install bin/clients/* /opt/opengnsys/bin/clients install etc/* /opt/opengnsys/etc/ -- 2.40.1 From 7f6be093a792887decb752091ea672c811250222 Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Mon, 11 Nov 2024 09:53:13 +0100 Subject: [PATCH 58/70] Fix typo --- installer/installer.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/installer/installer.sh b/installer/installer.sh index 53c462e..9ec7546 100644 --- a/installer/installer.sh +++ b/installer/installer.sh @@ -13,6 +13,7 @@ export GIT_SSL_NO_VERIFY clone_repository() { local BRANCH=$1 + rm -rf $DOWNLOAD_DIR git clone -b "$BRANCH" $GIT_REPO $DOWNLOAD_DIR chown -R ogrepository:ogrepository $DOWNLOAD_DIR } -- 2.40.1 From e51e935c11f9cbfe37c21593bd736231ed43e5f1 Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Mon, 11 Nov 2024 10:31:13 +0100 Subject: [PATCH 59/70] Fixing files --- installer/installer.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/installer/installer.sh b/installer/installer.sh index 9ec7546..589f83e 100644 --- a/installer/installer.sh +++ b/installer/installer.sh @@ -31,7 +31,7 @@ add_user_ogrepository() { create_directories() { mkdir -p $INSTALL_DIR - mkdir -p $INSTALL_DIR/images $INSTALL_DIR/images_trash/ $INSTALL_DIR/bin/ $INSTALL_DIR/etc/ $INSTALL_DIR/log/ + mkdir -p $INSTALL_DIR/images $INSTALL_DIR/images_trash/ $INSTALL_DIR/bin/ $INSTALL_DIR/etc/ $INSTALL_DIR/log/ $INSTALL_DIR/api/ chown -R ogrepository:ogrepository $INSTALL_DIR } @@ -50,18 +50,17 @@ install_external_packages() { } install_ogrepo-api_service() { - cd $DOWNLOAD_DIR - cp -r installer/files/ogrepo-api.service /etc/systemd/system/ogrepo-api.service + cp -r $DOWNLOAD_DIR/installer/files/ogrepo-api.service /etc/systemd/system/ogrepo-api.service systemctl enable --now ogrepo-api systemctl start ogrepository } install_files() { - cd $DOWNLOAD_DIR - install bin/* /opt/opengnsys/bin/ - install bin/clients/* /opt/opengnsys/bin/clients - install etc/* /opt/opengnsys/etc/ - install api/* /opt/opengnsys/api + install $DOWNLOAD_DIR/bin/* $INSTALL_DIR/bin/ + install $DOWNLOAD_DIR/bin/clients/* $INSTALL_DIR/bin/clients + install $DOWNLOAD_DIR/etc/* $INSTALL_DIR/etc/ + install $DOWNLOAD_DIR/api/* $INSTALL_DIR/api + chown -R ogrepository:ogrepository $INSTALL_DIR } ## Main program @@ -70,6 +69,7 @@ install_dependencies add_user_ogrepository clone_repository "$GIT_BRANCH" create_directories +install_files install_ogrepo-api_service -- 2.40.1 From 212b4e9eac1392584e82112daa34aad2958d1618 Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Mon, 11 Nov 2024 10:41:28 +0100 Subject: [PATCH 60/70] Changing install dirs --- installer/installer.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/installer/installer.sh b/installer/installer.sh index 589f83e..2ae5666 100644 --- a/installer/installer.sh +++ b/installer/installer.sh @@ -31,7 +31,7 @@ add_user_ogrepository() { create_directories() { mkdir -p $INSTALL_DIR - mkdir -p $INSTALL_DIR/images $INSTALL_DIR/images_trash/ $INSTALL_DIR/bin/ $INSTALL_DIR/etc/ $INSTALL_DIR/log/ $INSTALL_DIR/api/ + mkdir -p $INSTALL_DIR/images $INSTALL_DIR/images_trash/ $INSTALL_DIR/bin/clients $INSTALL_DIR/etc/ $INSTALL_DIR/log/ $INSTALL_DIR/api/ chown -R ogrepository:ogrepository $INSTALL_DIR } @@ -57,9 +57,9 @@ install_ogrepo-api_service() { install_files() { install $DOWNLOAD_DIR/bin/* $INSTALL_DIR/bin/ - install $DOWNLOAD_DIR/bin/clients/* $INSTALL_DIR/bin/clients + install $DOWNLOAD_DIR/bin/clients/* $INSTALL_DIR/bin/clients/ install $DOWNLOAD_DIR/etc/* $INSTALL_DIR/etc/ - install $DOWNLOAD_DIR/api/* $INSTALL_DIR/api + install $DOWNLOAD_DIR/api/* $INSTALL_DIR/api/ chown -R ogrepository:ogrepository $INSTALL_DIR } -- 2.40.1 From d8a428f9a80b983731fcd6afde3eefdcd3145d6d Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Mon, 11 Nov 2024 10:51:46 +0100 Subject: [PATCH 61/70] Change install for pr --- installer/installer.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/installer/installer.sh b/installer/installer.sh index 2ae5666..434f834 100644 --- a/installer/installer.sh +++ b/installer/installer.sh @@ -31,7 +31,7 @@ add_user_ogrepository() { create_directories() { mkdir -p $INSTALL_DIR - mkdir -p $INSTALL_DIR/images $INSTALL_DIR/images_trash/ $INSTALL_DIR/bin/clients $INSTALL_DIR/etc/ $INSTALL_DIR/log/ $INSTALL_DIR/api/ + mkdir -p $INSTALL_DIR/images $INSTALL_DIR/images_trash/ $INSTALL_DIR/bin/ $INSTALL_DIR/etc/ $INSTALL_DIR/log/ $INSTALL_DIR/api/ chown -R ogrepository:ogrepository $INSTALL_DIR } @@ -56,10 +56,9 @@ install_ogrepo-api_service() { } install_files() { - install $DOWNLOAD_DIR/bin/* $INSTALL_DIR/bin/ - install $DOWNLOAD_DIR/bin/clients/* $INSTALL_DIR/bin/clients/ - install $DOWNLOAD_DIR/etc/* $INSTALL_DIR/etc/ - install $DOWNLOAD_DIR/api/* $INSTALL_DIR/api/ + cp -pr $DOWNLOAD_DIR/bin/* $INSTALL_DIR/bin/ + cp -pr $DOWNLOAD_DIR/etc/* $INSTALL_DIR/etc/ + cp -pr $DOWNLOAD_DIR/api/* $INSTALL_DIR/api/ chown -R ogrepository:ogrepository $INSTALL_DIR } -- 2.40.1 From fe9e38bc0e8781d1ea05dd681cd01958b6065ea3 Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Mon, 11 Nov 2024 10:57:12 +0100 Subject: [PATCH 62/70] Remove wrong line --- installer/installer.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/installer/installer.sh b/installer/installer.sh index 434f834..d356cf4 100644 --- a/installer/installer.sh +++ b/installer/installer.sh @@ -52,7 +52,6 @@ install_external_packages() { install_ogrepo-api_service() { cp -r $DOWNLOAD_DIR/installer/files/ogrepo-api.service /etc/systemd/system/ogrepo-api.service systemctl enable --now ogrepo-api - systemctl start ogrepository } install_files() { -- 2.40.1 From c93e6493096c0d2a76dede2b52f5b4b719a6c923 Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Mon, 11 Nov 2024 11:07:17 +0100 Subject: [PATCH 63/70] Configure sudo --- installer/installer.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/installer/installer.sh b/installer/installer.sh index d356cf4..282ddc9 100644 --- a/installer/installer.sh +++ b/installer/installer.sh @@ -27,6 +27,7 @@ check_root() { add_user_ogrepository() { useradd -r -s /bin/bash ogrepository + echo 'ogrepository ALL=(ALL) NOPASSWD: /usr/bin/python3' > ogrepository } create_directories() { -- 2.40.1 From 69509d6fa170967509ae8402f685734611075d1a Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Mon, 11 Nov 2024 11:17:36 +0100 Subject: [PATCH 64/70] Install external packages --- installer/installer.sh | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/installer/installer.sh b/installer/installer.sh index 282ddc9..f7716f9 100644 --- a/installer/installer.sh +++ b/installer/installer.sh @@ -25,9 +25,17 @@ check_root() { fi } +install_uftp() { + apt install uftp -y +} + +install_updcast () { + apt install $DOWNLOAD_DIR/packets/udpcast_20230924_amd64.deb +} + add_user_ogrepository() { - useradd -r -s /bin/bash ogrepository - echo 'ogrepository ALL=(ALL) NOPASSWD: /usr/bin/python3' > ogrepository + useradd -r -s /bin/bash ogrepository + echo 'ogrepository ALL=(ALL) NOPASSWD: ALL' > ogrepository } create_directories() { @@ -65,6 +73,9 @@ install_files() { ## Main program check_root install_dependencies +install_external_packages +install_uftp +install_updcast add_user_ogrepository clone_repository "$GIT_BRANCH" create_directories -- 2.40.1 From dffe6c1744c6b144b31fcbbe35a34629f916048b Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Mon, 11 Nov 2024 11:30:40 +0100 Subject: [PATCH 65/70] Fix route to install deb keyring sources --- installer/installer.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer/installer.sh b/installer/installer.sh index f7716f9..5948d9e 100644 --- a/installer/installer.sh +++ b/installer/installer.sh @@ -50,7 +50,7 @@ install_dependencies() { } install_ext_repo() { - cp installer/files/ctorrent.sources /etc/apt/sources.list.d/ctorrent.sources + cp $DOWNLOAD_DIR/installer/files/ctorrent.sources /etc/apt/sources.list.d/ctorrent.sources apt update -y } -- 2.40.1 From 094ce0ecc1cf19826c99745cb1095ff34242ea20 Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Mon, 11 Nov 2024 11:47:41 +0100 Subject: [PATCH 66/70] Fix user creation --- installer/installer.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/installer/installer.sh b/installer/installer.sh index 5948d9e..7090ce0 100644 --- a/installer/installer.sh +++ b/installer/installer.sh @@ -34,8 +34,15 @@ install_updcast () { } add_user_ogrepository() { - useradd -r -s /bin/bash ogrepository - echo 'ogrepository ALL=(ALL) NOPASSWD: ALL' > ogrepository + if ! id "ogrepository" &>/dev/null; then + echo "User ogrepository does not exist, creating it" + useradd -r -s /bin/bash ogrepository + fi + if [ ! -f /etc/sudoers.d/ogrepository ]; then + echo "User ogrepository does not have sudo permissions, adding it" + echo 'ogrepository ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/ogrepository + fi + } create_directories() { @@ -72,6 +79,7 @@ install_files() { ## Main program check_root +install_ext_repo install_dependencies install_external_packages install_uftp -- 2.40.1 From c278105f55273c7e660527e89a27d33e10481764 Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Mon, 11 Nov 2024 11:58:31 +0100 Subject: [PATCH 67/70] Fixing command order to proper installation --- installer/installer.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/installer/installer.sh b/installer/installer.sh index 7090ce0..55023b2 100644 --- a/installer/installer.sh +++ b/installer/installer.sh @@ -79,13 +79,13 @@ install_files() { ## Main program check_root -install_ext_repo install_dependencies +add_user_ogrepository +clone_repository "$GIT_BRANCH" +install_ext_repo install_external_packages install_uftp install_updcast -add_user_ogrepository -clone_repository "$GIT_BRANCH" create_directories install_files install_ogrepo-api_service -- 2.40.1 From 5e56b83f461d51754c7a745b0a9ecb3c7499bb8c Mon Sep 17 00:00:00 2001 From: ggil Date: Mon, 11 Nov 2024 12:22:14 +0100 Subject: [PATCH 68/70] refs #631 - Modify some scripts and API --- api/repo_api.py | 18 ++++++++++++++---- bin/deleteImage.py | 4 ++-- bin/exportImage.py | 6 +++--- bin/importImage.py | 6 +++--- bin/recoverImage.py | 4 ++-- 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/api/repo_api.py b/api/repo_api.py index 665133f..6544a53 100644 --- a/api/repo_api.py +++ b/api/repo_api.py @@ -502,15 +502,25 @@ def delete_image(imageId): # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: if param_dict: if 'subdir' in param_dict: - if method == "trash": + if method == "permanent": + cmd = ['sudo', 'python3', f"{script_path}/deleteImage.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}", '-p'] + elif method == "trash": cmd = ['sudo', 'python3', f"{script_path}/deleteImage.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}"] else: - cmd = ['sudo', 'python3', f"{script_path}/deleteImage.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}", '-p'] + return jsonify({ + "success": False, + "error": "Incorrect method (must be 'permanent' or 'trash')" + }), 400 else: - if method == "trash": + if method == "permanent": + cmd = ['sudo', 'python3', f"{script_path}/deleteImage.py", f"{param_dict['name']}.{param_dict['extension']}", '-p'] + elif method == "trash": cmd = ['sudo', 'python3', f"{script_path}/deleteImage.py", f"{param_dict['name']}.{param_dict['extension']}"] else: - cmd = ['sudo', 'python3', f"{script_path}/deleteImage.py", f"{param_dict['name']}.{param_dict['extension']}", '-p'] + return jsonify({ + "success": False, + "error": "Incorrect method (must be 'permanent' or 'trash')" + }), 400 else: return jsonify({ "success": False, diff --git a/bin/deleteImage.py b/bin/deleteImage.py index d51f0f1..15ca10c 100644 --- a/bin/deleteImage.py +++ b/bin/deleteImage.py @@ -224,10 +224,10 @@ def main(): # Evaluamos la cantidad de barras que hay en la ruta de la imagen, para diferenciar entre imágenes "normales" y basadas en OU # (y llamamos a la función correspondiente para eliminarla): - if file_path.count('/') == 4: + if file_path.count('/') == 5: print("Deleting normal image...") delete_normal_image(file_path, method, extensions) - elif file_path.count('/') == 5: + elif file_path.count('/') == 6: print("Deleting OU based image...") delete_ou_image(file_path, method, extensions) diff --git a/bin/exportImage.py b/bin/exportImage.py index c54bfdb..ddbbb36 100644 --- a/bin/exportImage.py +++ b/bin/exportImage.py @@ -145,10 +145,10 @@ def export_image(file_path, remote_host, remote_user): except IOError: print("As expected, image doesn't exist on remote repository.") - # Evaluamos si la ruta de la imagen tiene 5 barras, en cuyo caso corresponderá a una imagen basada en OU, + # Evaluamos si la ruta de la imagen tiene 6 barras, en cuyo caso corresponderá a una imagen basada en OU, # y almacenamos el nombre del directorio correspondiente a la OU: - if file_path.count('/') == 5: - ou_subdir = file_path.split('/')[4] + if file_path.count('/') == 6: + ou_subdir = file_path.split('/')[5] # Comprobamos si el directorio de OU existe en el equipo remoto, y en caso contrario lo creamos: try: sftp_client.stat(f"{repo_path}{ou_subdir}") diff --git a/bin/importImage.py b/bin/importImage.py index 227c86c..e6798b7 100644 --- a/bin/importImage.py +++ b/bin/importImage.py @@ -195,10 +195,10 @@ def main(): remote_host = sys.argv[2] remote_user = sys.argv[3] - # Evaluamos si la ruta de la imagen tiene 5 barras, en cuyo caso corresponderá a una imagen basada en OU, + # Evaluamos si la ruta de la imagen tiene 6 barras, en cuyo caso corresponderá a una imagen basada en OU, # y almacenamos el nombre del directorio correspondiente a la OU: - if file_path.count('/') == 5: - ou_subdir = file_path.split('/')[4] + if file_path.count('/') == 6: + ou_subdir = file_path.split('/')[5] # Si no existe un directorio correspondiente a la OU en el repo local, lo creamos: if not os.path.exists(f"{repo_path}{ou_subdir}"): os.mkdir(f"{repo_path}{ou_subdir}", 0o755) diff --git a/bin/recoverImage.py b/bin/recoverImage.py index e68b683..e83f306 100644 --- a/bin/recoverImage.py +++ b/bin/recoverImage.py @@ -176,10 +176,10 @@ def main(): # Evaluamos la cantidad de barras que hay en la ruta de la imagen, para diferenciar entre imágenes "normales" y basadas en OU # (y llamamos a la función correspondiente para recuperarla): - if file_path.count('/') == 4: + if file_path.count('/') == 5: print("Recovering normal image...") recover_normal_image(file_path, extensions) - elif file_path.count('/') == 5: + elif file_path.count('/') == 6: print("Recovering OU based image...") recover_ou_image(file_path, extensions) -- 2.40.1 From 33ff1c2b7f008ecdf04411d274256457b49f4015 Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Mon, 11 Nov 2024 14:13:32 +0100 Subject: [PATCH 69/70] Configure Samba Configure REPO_IP --- installer/files/ogrepo-smb.conf | 8 ++++++++ installer/installer.sh | 16 +++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 installer/files/ogrepo-smb.conf diff --git a/installer/files/ogrepo-smb.conf b/installer/files/ogrepo-smb.conf new file mode 100644 index 0000000..3b76b2b --- /dev/null +++ b/installer/files/ogrepo-smb.conf @@ -0,0 +1,8 @@ +[ogimages] +comment = OpenGnsys Repository +browseable = no +writeable = yes +locking = no +path = /opt/opengnsys/ogrepository/images +guest ok = no +valid users = %%OGREPOSITORY_USER%% diff --git a/installer/installer.sh b/installer/installer.sh index 55023b2..3d18c7e 100644 --- a/installer/installer.sh +++ b/installer/installer.sh @@ -5,6 +5,9 @@ set -e GIT_BRANCH=$1 GIT_REPO=https://ognproject.evlt.uma.es/gitea/opengnsys/ogrepository.git GIT_SSL_NO_VERIFY=true +REPO_IP=${REPO_IP:-"127.0.0.1"} +SMBUSER=${SMBUSER:-"ogrepository"} +SMBPASS=${SMBPASS:-"ogrepository"} INSTALL_DIR=/opt/opengnsys/ogrepository DOWNLOAD_DIR=/tmp/ogrepository DEBIAN_FRONTEND=noninteractive @@ -75,6 +78,17 @@ install_files() { cp -pr $DOWNLOAD_DIR/etc/* $INSTALL_DIR/etc/ cp -pr $DOWNLOAD_DIR/api/* $INSTALL_DIR/api/ chown -R ogrepository:ogrepository $INSTALL_DIR + echo IPlocal="$REPO_IP" > $INSTALL_DIR/etc/ogAdmRepo.cfg +} + +configure_samba() { + echo "include = /etc/samba/smb.conf.ogrepository" >> /etc/samba/smb.conf + cp $DOWNLOAD_DIR/installer/files/ogrepo-smb.conf /etc/samba/smb.conf.ogrepository + sed -i "s/%%OGREPOSITORY_USER%%/$SMBUSER/g" /etc/samba/smb.conf.ogrepository + systemctl restart smbd + # Create default user ogrepository + (echo $SMBPASS; echo $SMBPASS) | smbpasswd -s -a $SMBUSER + } ## Main program @@ -89,6 +103,6 @@ install_updcast create_directories install_files install_ogrepo-api_service - +configure_samba -- 2.40.1 From 146ca64ad5f1da282eb6f9033dce04716ede3f92 Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Mon, 11 Nov 2024 14:23:37 +0100 Subject: [PATCH 70/70] Set proper permissions to ogrepository user --- installer/installer.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/installer/installer.sh b/installer/installer.sh index 3d18c7e..0015672 100644 --- a/installer/installer.sh +++ b/installer/installer.sh @@ -79,6 +79,7 @@ install_files() { cp -pr $DOWNLOAD_DIR/api/* $INSTALL_DIR/api/ chown -R ogrepository:ogrepository $INSTALL_DIR echo IPlocal="$REPO_IP" > $INSTALL_DIR/etc/ogAdmRepo.cfg + sudo chown ogrepository:ogrepository $INSTALL_DIR/etc/ogAdmRepo.cfg } configure_samba() { -- 2.40.1