add_python_scripts #20

Merged
ggil merged 2 commits from add_python_scripts into main 2025-02-06 16:33:53 +01:00
7 changed files with 168 additions and 101 deletions

View File

@ -365,7 +365,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**: El script requiere que se le pase el nombre de la imagen (con extensión) 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).
**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está transfiriendo, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore).
**NOTA3**: Este endpoint comprueba si la importación se ha realizado correctamente, comparando el contenido de los archivos "sum" y "size" con los valores reales (que vuelve a calcular). Si la importación ha sido correcta llama al script "createTorrentSum.py", para crear el archivo ".torrent" (que debe crearse desde el repositorio en el que esté), y actualizar la info del repositorio. Si la importación no ha sido correcta (porque la imagen no ha pasado el check de integridad), borra los archivos importados.
**URL:** `/ogrepository/v1/repo/images`
**Método HTTP:** POST
@ -391,7 +392,8 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d
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**: El script requiere que se le pase el nombre de la imagen (con extensión) 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).
**NOTA2**: Este endpoint puede tardar mucho tiempo, pero no lo hemos hecho asíncrono (porque no es llamado desde el portal), por lo que siempre informará del resultado final de la ejecución (a diferencia del endpoint "Importar una Imagen"), pero dependiendo del tamaño de la imagen, puede tardar bastante.
**NOTA3**: Este endpoint no genera el archivo ".torrent", ni comprueba si la exportación se ha realizado correctamente, por lo que posteriormente se debe llamar al endpoint "Crear archivos auxiliares" (para crear el archivo ".torrent" y actualizar el repositorio) y al endpoint "Chequear integridad de Imagen", ambos desde el equipo destino. No es realmente necesario llamar a este último endpoint, pero si recomendado.
**URL:** `/ogrepository/v1/repo/images`
**Método HTTP:** PUT
@ -409,7 +411,7 @@ 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 está exportando.
- **Código 200 OK:** La imagen se ha exportando exitosamente.
---
### Crear archivos auxiliares

View File

@ -75,7 +75,7 @@ curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/stat
"udp-sender": "stopped",
"uftp": "stopped",
"bttrack": "stopped",
"btlaunchmany": "stopped"
"btlaunchmany.bittornado": "stopped"
}
}
```
@ -323,7 +323,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**: El script requiere que se le pase el nombre de la imagen (con extensión) 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).
**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está transfiriendo, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore).
**NOTA3**: Este endpoint comprueba si la importación se ha realizado correctamente, comparando el contenido de los archivos "sum" y "size" con los valores reales (que vuelve a calcular). Si la importación ha sido correcta llama al script "createTorrentSum.py", para crear el archivo ".torrent" (que debe crearse desde el repositorio en el que esté), y actualizar la info del repositorio. Si la importación no ha sido correcta (porque la imagen no ha pasado el check de integridad), borra los archivos importados.
**URL:** `/ogrepository/v1/repo/images`
**Método HTTP:** POST
@ -349,7 +350,8 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d
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**: El script requiere que se le pase el nombre de la imagen (con extensión) 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).
**NOTA2**: Este endpoint puede tardar mucho tiempo, pero no lo hemos hecho asíncrono (porque no es llamado desde el portal), por lo que siempre informará del resultado final de la ejecución (a diferencia del endpoint "Importar una Imagen"), pero dependiendo del tamaño de la imagen, puede tardar bastante.
**NOTA3**: Este endpoint no genera el archivo ".torrent", ni comprueba si la exportación se ha realizado correctamente, por lo que posteriormente se debe llamar al endpoint "Crear archivos auxiliares" (para crear el archivo ".torrent" y actualizar el repositorio) y al endpoint "Chequear integridad de Imagen", ambos desde el equipo destino. No es realmente necesario llamar a este último endpoint, pero si recomendado.
**URL:** `/ogrepository/v1/repo/images`
**Método HTTP:** PUT
@ -367,7 +369,7 @@ 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 está exportando.
- **Código 200 OK:** La imagen se ha exportando exitosamente.
---
### Crear archivos auxiliares

View File

@ -231,12 +231,23 @@ def check_remote_image(remote_ip, remote_user, image_file_path):
sftp_client = ssh_client.open_sftp()
# Si la imagen no existe, retornamos el mensaje correspondiente:
if not os.path.exists(image_file_path):
try:
sftp_client.stat(image_file_path)
except IOError:
return "Remote image not found"
#if not os.path.exists(image_file_path):
# return "Remote image not found"
# Si la imagen existe pero está bloqueada, retornamos el mensaje correspondiente:
if os.path.exists(f"{image_file_path}.lock"):
try:
sftp_client.stat(f"{image_file_path}.lock")
return "Remote image is locked"
except IOError:
print("Remote image is not locked, as expected")
#if os.path.exists(f"{image_file_path}.lock"):
# return "Remote image is locked"
# Cerramos el cliente SSH y el cliente SFTP:
ssh_client.close()
@ -247,10 +258,10 @@ def check_remote_image(remote_ip, remote_user, 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,
y dejará de realizar la comprobación (saliendo del bucle).
""" Cada minuto comprueba si existe un archivo ".lock" asociado a la imagen que recibe como parámetro, y si existe la imagen y sus archivos asociados.
Cuando ya no exista el archivo ".lock" (pero si los demás archivos), le comunicará a ogCore que la importación ha sido exitosa, y saldrá de bucle.
Cuando ya no exista el archivo ".lock" (pero tampoco los demás), le comunicará a ogCore que la importación ha fallado, y saldrá de bucle.
Mientras no se cumpla ninguna de las condiciones anteriores, y aun exista el archivo ".lock", seguirá haciendo la comprobación (repitiendo el bucle cada minuto).
"""
journal.send("Running function 'check_lock_local'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
@ -259,23 +270,36 @@ def check_lock_local(image_file_path, job_id):
# Creamos un bucle infinito:
while True:
# 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)")
app.logger.info(f"Job_ID: {job_id}")
# Si ya no existe el archivo ".lock" (pero sí existen los demás), respondemos a ogCore (con "success: True") y salimos del bucle:
if not os.path.exists(f"{image_file_path}.lock") and os.path.exists(image_file_path) and os.path.exists(f"{image_file_path}.full.sum") and os.path.exists(f"{image_file_path}.info.checked") and os.path.exists(f"{image_file_path}.size") and os.path.exists(f"{image_file_path}.sum") and os.path.exists(f"{image_file_path}.torrent"):
journal.send("Image unlocked", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'INFO', 'operation':'Run function check_lock_local', 'desc':'Image unlocked'}", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api")
# Almacenamos en un diccionario los datos a enviar a ogCore:
data = {
'job_id': job_id
'job_id': job_id,
'success': True
}
# Llamamos al endpoint de ogCore, enviando los datos del diccionario:
journal.send(f"Calling function 'recall_ogcore' (JOB_ID: {job_id})", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send(f"Calling function 'recall_ogcore' (JOB_ID: {job_id}, SUCCESS: True)", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
recall_ogcore(data)
break
# Si aun existe el archivo ".lock", imprimimos un mensaje en la API:
else:
# Si no existe el archivo ".lock" (pero tampoco los demás), respondemos a ogCore (con "success: False") y salimos del bucle:
elif not os.path.exists(f"{image_file_path}.lock") and not os.path.exists(image_file_path) and not os.path.exists(f"{image_file_path}.full.sum") and not os.path.exists(f"{image_file_path}.info") and not os.path.exists(f"{image_file_path}.size") and not os.path.exists(f"{image_file_path}.sum") and not os.path.exists(f"{image_file_path}.torrent"):
journal.send("Imported image didn't pass the Integrity Check", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'ERROR', 'operation':'Run function check_lock_local', 'desc':'Integrity check failed'}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api")
# Almacenamos en un diccionario los datos a enviar a ogCore:
data = {
'job_id': job_id,
'success': False
}
# Llamamos al endpoint de ogCore, enviando los datos del diccionario:
journal.send(f"Calling function 'recall_ogcore' (JOB_ID: {job_id}, SUCCESS: False)", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
recall_ogcore(data)
break
# Si aun existe el archivo ".lock" (y no se han cumplido las condiciones anteriores), imprimimos un mensaje en la API:
elif os.path.exists(f"{image_file_path}.lock"):
app.logger.info("Task in process (.lock file exists)")
# Esperamos 1 minuto para volver a realizar la comprobación:
sleep(60)
@ -289,6 +313,7 @@ def check_lock_remote(image_file_path, remote_host, remote_user, job_id):
(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).
NOTA: Esta función ya no se utiliza, porque el endpoint "Exportar una Imagen" ahora es síncrono.
"""
journal.send("Running function 'check_lock_remote'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
@ -894,7 +919,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 = f"ImportImage_{''.join(random.choice('0123456789abcdef') for char in range(8))}"
job_id = f"TransferImage_{''.join(random.choice('0123456789abcdef') for char in range(8))}"
journal.send(f"JOB ID generated ({job_id})", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Evaluamos el resultado de la ejecución, y devolvemos una respuesta:
@ -939,7 +964,7 @@ def import_image():
# ---------------------------------------------------------
# 10 - Endpoint "Exportar una Imagen" (ASINCRONO):
# 10 - Endpoint "Exportar una Imagen" (SINCRONO):
@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.
@ -996,41 +1021,23 @@ def export_image():
try:
journal.send("Running script 'exportImage.py'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# 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 = f"ExportImage_{''.join(random.choice('0123456789abcdef') for char in range(8))}"
journal.send(f"JOB ID generated ({job_id})", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# 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 is None:
journal.send("Script 'exportImage.py' result OK (ReturnCode: None)", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'INFO', 'http_code':'200', 'operation':'Run script exportImage.py', 'desc':'Result OK (ReturnCode: None)'}", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api")
# 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):
journal.send("Calling function 'check_lock_remote'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
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:
if result.returncode == 0:
journal.send("Script 'exportImage.py' result OK (ReturnCode: 0)", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'INFO', 'http_code':'200', 'operation':'Run script exportImage.py', 'desc':'Result OK (ReturnCode: 0)'}", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api")
return jsonify({
"success": True,
"output": "Exporting image...",
"job_id": job_id
"output": "Image exported successfully"
}), 200
else:
journal.send("Script 'exportImage.py' result KO (Export image failed)", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'ERROR', 'http_code':'500', 'operation':'Run script exportImage.py', 'desc':'Result KO (Export image failed)'}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api")
journal.send(f"Script 'exportImage.py' result KO (Error: {result.stderr})", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send(f"{{'component':'ogRepo', 'severity':'ERROR', 'http_code':'500', 'operation':'Run script exportImage.py', 'desc':'Result KO (Error: {result.stderr})'}}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api")
return jsonify({
"success": False,
"error": "Export image failed"
}), 500
except subprocess.CalledProcessError as error:
journal.send(f"Script 'exportImage.py' result KO (Process Exception: {str(error)})", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send(f"{{'component':'ogRepo', 'severity':'ERROR', 'http_code':'500', 'operation':'Run script exportImage.py', 'desc':'Result KO (Process Exception: {str(error_)})'}}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api")
return jsonify({
"success": False,
"process exception": str(error)
"error": result.stderr
}), 500
except Exception as error_description:
if "exit status 5" in str(error_description):

View File

@ -15,7 +15,7 @@ tags:
- 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: "Transferencia de Imágenes entre Repositorios"
- name: "Varios"
@ -1294,7 +1294,7 @@ paths:
example: "(Exception description)"
# -----------------------------------------------------------------------------------------------------------
# Apartado "Importar y Exportar Imágenes"
# Apartado "Transferencia de Imágenes entre Repositorios"
# -----------------------------------------------------------------------------------------------------------
/ogrepository/v1/repo/images:
@ -1307,7 +1307,7 @@ paths:
**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"
- "Transferencia de Imágenes entre Repositorios"
parameters:
- name: JSON
in: body
@ -1422,7 +1422,7 @@ paths:
**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"
- "Transferencia de Imágenes entre Repositorios"
parameters:
- name: JSON
in: body
@ -1447,7 +1447,7 @@ paths:
example: "user_name"
responses:
"200":
description: "La imagen se está exportando."
description: "La imagen se ha exportado exitosamente."
schema:
type: object
properties:
@ -1456,7 +1456,7 @@ paths:
example: true
output:
type: string
example: "Exporting image..."
example: "Image exported successfully"
"400 (Image not found)":
description: "No se ha encontrado la imagen."
schema:
@ -1501,17 +1501,6 @@ paths:
exception:
type: string
example: "Image already exists on remote server"
"500":
description: "Error al exportar la imagen."
schema:
type: object
properties:
success:
type: boolean
example: false
error:
type: string
example: "Export image failed"
"500 (Error)":
description: "Error al exportar la imagen."
schema:

View File

@ -27,7 +27,7 @@ import unittest
from unittest.mock import patch, mock_open, MagicMock, AsyncMock
from flask import json
import os
from repo_api import app, get_image_params, search_process, check_remote_connection, check_remote_image, check_lock_remote, check_lock_local, check_aux_files, check_file_exists
from repo_api import app, get_image_params, search_process, check_remote_connection, check_remote_image, check_lock_local, check_aux_files, check_file_exists
# --------------------------------------------------------------------------------------------
@ -471,35 +471,31 @@ class RepoApiTestCase(unittest.TestCase):
@patch('repo_api.check_remote_connection')
@patch('repo_api.check_lock_remote')
@patch('repo_api.get_image_params')
@patch('repo_api.subprocess.Popen')
def test_export_image(self, mock_popen, mock_get_image_params, mock_check_remote_connection, mock_check_lock_remote):
@patch('repo_api.subprocess.run')
def test_export_image(self, mock_subprocess, mock_get_image_params, mock_check_remote_connection):
""" Método de prueba del endpoint "Exportar una Imagen".
"""
print("Testing endpoint 'Exportar una Imagen'...")
mock_check_remote_connection.return_value = True
mock_get_image_params.return_value = {'name': 'test4unittest', 'extension': 'img'}
mock_popen.return_value = MagicMock(returncode=None)
mock_check_lock_remote.return_value = AsyncMock(returncode=None)
mock_subprocess.return_value = MagicMock(returncode=0)
response = self.app.put('/ogrepository/v1/repo/images', data=json.dumps({"ID_img": "test_image_id", "repo_ip": "127.0.0.1", "user": "test_user"}), content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertIn('Exporting image...', json.loads(response.data)['output'])
self.assertIn('Image exported successfully', json.loads(response.data)['output'])
@patch('repo_api.check_remote_connection')
@patch('repo_api.check_lock_remote')
@patch('repo_api.get_image_params')
@patch('repo_api.subprocess.Popen')
def test_export_image_error500(self, mock_popen, mock_get_image_params, mock_check_remote_connection, mock_check_lock_remote):
@patch('repo_api.subprocess.run')
def test_export_image_error500(self, mock_subprocess, mock_get_image_params, mock_check_remote_connection):
""" Método de prueba del endpoint "Exportar una Imagen"
(en caso de error "500").
"""
print("Testing endpoint 'Exportar una Imagen' (error 500)...")
mock_check_remote_connection.return_value = True
mock_get_image_params.return_value = {'name': 'test4unittest', 'extension': 'img'}
mock_popen.side_effect = Exception("Error al exportar la imagen")
mock_check_lock_remote.return_value = AsyncMock(returncode=None)
mock_subprocess.side_effect = Exception("Error al exportar la imagen")
response = self.app.put('/ogrepository/v1/repo/images', data=json.dumps({"ID_img": "test_image_id", "repo_ip": "127.0.0.1", "user": "test_user"}), content_type='application/json')
self.assertEqual(response.status_code, 500)
self.assertIn('Error al exportar la imagen', response.data.decode())
@ -547,15 +543,15 @@ class RepoApiTestCase(unittest.TestCase):
@patch('repo_api.check_remote_connection')
@patch('repo_api.get_image_params')
@patch('repo_api.subprocess.Popen')
def test_export_image_error400_remote_image_exists(self, mock_popen, mock_get_image_params, mock_check_remote_connection):
@patch('repo_api.subprocess.run')
def test_export_image_error400_remote_image_exists(self, mock_subprocess, mock_get_image_params, mock_check_remote_connection):
""" Método de prueba del endpoint "Exportar una Imagen"
(en caso de error "400", por imagen remota ya existente).
"""
print("Testing endpoint 'Exportar una Imagen' (error 400, remote image exists)...")
mock_check_remote_connection.return_value = True
mock_get_image_params.return_value = {'name': 'test4unittest', 'extension': 'img'}
mock_popen.side_effect = Exception("exit status 5")
mock_subprocess.side_effect = Exception("exit status 5")
response = self.app.put('/ogrepository/v1/repo/images', data=json.dumps({"ID_img": "test_image_id", "repo_ip": "127.0.0.1", "user": "test_user"}), content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertIn("Image already exists on remote server", json.loads(response.data)['exception'])

View File

@ -4,8 +4,10 @@
"""
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.
Realiza la acción contraria que el script "importImage.py", pero es preferible usar "importImage.py" (porque chequea la integridad de la imagen, genera el archivo ".torrent" y actualiza el repositorio).
Cuando se utilice "exportImage.py" (este script) se debe generar el archivo ".torrent" (llamando al script "createTorrentSum.py" o a su endpoint asociado), que también actualizará la info del repoisitorio,
y es conveniente chequear la integridad de la imagen exportada (llamando al script "checkImage.py" o a su endpoint asociado). Ambas acciones deben realizarse en el equipo destino.
Librerías Python requeridas: "paramiko" (se puede instalar con "sudo apt install python3-paramiko")

View File

@ -4,8 +4,11 @@
"""
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.
Es similar al script bash original (cuyo nombre es "importimage", a secas), pero con ciertas diferencias.
Comprueba si la importación se ha realizado correctamente, comparando el contenido de los archivos "sum" y "size" con los valores reales (que vuelve a calcular).
Si la importación ha sido correcta llama al script "createTorrentSum.py", para crear el archivo ".torrent" (que debe crearse desde el repositorio en el que esté), y actualizar la info del repositorio.
Si la importación no ha sido correcta (porque la imagen no ha pasado el check de integridad), borra los archivos importados.
Librerías Python requeridas: "paramiko" (se puede instalar con "sudo apt install python3-paramiko")
@ -41,6 +44,7 @@ import warnings
warnings.filterwarnings("ignore")
import os
import sys
import hashlib
import subprocess
import paramiko
import warnings
@ -53,7 +57,7 @@ from systemd import journal
script_name = os.path.basename(__file__)
repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final
update_repo_script = '/opt/opengnsys/ogrepository/bin/updateRepoInfo.py'
create_torrent_script = '/opt/opengnsys/ogrepository/bin/createTorrentSum.py'
# --------------------------------------------------------------------------------------------
@ -110,7 +114,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 = ['', '.size', '.sum', '.full.sum', '.torrent', '.info.checked']
extensions = ['', '.size', '.sum', '.full.sum', '.info.checked'] # Quitamos la extensión ".torrent", porque hay que generarlo en el repo que almacena la imagen
# Iniciamos un cliente SSH:
ssh_client = paramiko.SSHClient()
@ -155,20 +159,75 @@ def import_image(file_path, remote_host, remote_user):
def update_repo_info():
""" Actualiza la información del repositorio, ejecutando el script "updateRepoInfo.py".
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()
def check_image(file_path):
""" Comprueba si los valores actuales de "sum" y "size"
coinciden con los almacenados en los archivos ".sum" y ".size".
Si coinciden retorna "True", y si no retorna "False".
"""
# Almacenamos los valores actuales de "sum" y "size":
actual_datasum = get_md5_sum(file_path)
actual_size = os.path.getsize(file_path)
# Almacenamos el valor de "sum" almacenado en el archivo:
with open(f"{file_path}.sum", 'r') as file:
file_datasum = file.read().strip('\n')
# Almacenamos el valor de "size" almacenado en el archivo:
with open(f"{file_path}.size", 'r') as file:
file_datasize = int(file.read().strip('\n'))
# Si los valores actuales coinciden con los almacenados en los archivos retornamos "True", y si no "False":
if actual_datasum == file_datasum and actual_size == file_datasize:
return True
else:
return False
def create_torrent(file_path):
""" Crea el archivo ".torrent" asociado a la imagen importada (los demás archivos auxiliares ya están creados), y actualiza la información del repositorio
(llamando al script "createTorrentSum.py", que a su vez llama al script "updateRepoInfo.py").
Se le llama cuando se ha comprobado que la imagen se ha importado correctamente (pasando el check de integridad).
"""
try:
journal.send("importImage.py: Running script 'updateRepoInfo.py'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
result = subprocess.run(['python3', update_repo_script], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
journal.send("importImage.py: Running script 'createTorrentSum.py'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
result = subprocess.run(['python3', create_torrent_script, file_path], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except subprocess.CalledProcessError as error:
journal.send(f"importImage.py: 'updateRepoInfo.py' error: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send(f"importImage.py: 'createTorrentSum.py' error: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
print(f"Error Output: {error.stderr.decode()}")
sys.exit(2)
sys.exit(5)
except Exception as error:
journal.send(f"importImage.py: 'updateRepoInfo.py' exception: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send(f"importImage.py: 'createTorrentSum.py' exception: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
print(f"Se ha producido un error inesperado: {error}")
sys.exit(3)
sys.exit(6)
def erase_image(file_path):
""" Borra la imagen importada y cada uno de los archivos asociados.
Se le llama cuando se ha comprobado que la imagen NO se ha importado correctamente (porque NO ha pasado el check de integridad).
"""
journal.send("importImage.py: Removing imported image files...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# 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', '.info']
# Borramos cada uno de los archivos (incluyendo la imagen):
for ext in extensions:
if os.path.exists(f"{file_path}{ext}"):
os.remove(f"{file_path}{ext}")
@ -193,18 +252,28 @@ def main():
# Importamos la imagen del repositorio remoto:
import_image(file_path, remote_host, remote_user)
# Comprobamos si la imagen se ha importado correctamente (comparando los archivos ".size" y ".sum" con los valores actuales):
image_OK = check_image(file_path)
# Renombramos el archivo ".info.checked" a ".info", para que lo pille el script "updateRepoInfo.py":
journal.send("importImage.py: Renaming '.info' file to '.info.checked'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("importImage.py: Renaming '.info.checked' file to '.info'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
os.rename(f"{file_path}.info.checked", f"{file_path}.info")
# Eliminamos el archivo de bloqueo:
journal.send("importImage.py: Removing '.lock' file...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
os.remove(f"{file_path}.lock")
# Actualizamos la información del repositorio, ejecutando el script "updateRepoInfo.py":
journal.send("importImage.py: Updating repository info...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
print("Updating Repository Info...")
update_repo_info()
# Si la imagen ha pasado el check de integridad, creamos el archivo ".torrent" y actualizamos el repositorio,
# y si no lo ha pasado borramos la imagen y los archivos asociados:
if image_OK == True:
create_torrent(file_path)
journal.send("importImage.py: Image imported successfully (Integrity Check OK)", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
print("Image imported successfully")
else:
erase_image(file_path)
journal.send("importImage.py: Imported image didn't pass the Integrity Check", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
print("Imported image didn't pass the Integrity Check")
sys.exit(7)