refs #1530 - Add 'backupImage.py' and related endpoint #22

Merged
ggil merged 1 commits from add_python_scripts into main 2025-02-20 14:15:22 +01:00
7 changed files with 450 additions and 156 deletions

View File

@ -58,8 +58,8 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará
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`
9. [Transferir una Imagen entre Repositorios](#transferir-una-imagen-entre-repositorios) - `POST /ogrepository/v1/repo/images`
10. [Hacer Backup de una Imagen](#hacer-backup-de-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`
@ -360,9 +360,9 @@ curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/t
- **Código 200 OK:** La imagen se eliminó exitosamente.
---
### Importar una Imagen
### Transferir una Imagen entre Repositorios
Se importará una imagen de un repositorio remoto al repositorio local.
Se importará una imagen de un repositorio remoto al repositorio local (donde se ejecuta el endpoint).
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á transfiriendo, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore).
@ -387,13 +387,13 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d
- **Código 200 OK:** La imagen se está importando.
---
### Exportar una Imagen
### Hacer Backup de 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**: 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 en el JSON.
**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.
Se hará backup de una imagen en un equipo remoto, que no tiene por qué tener OpenGnsys instalado, pero que debe poder conectar por SSH (mediante claves) con el repositorio local.
Se puede hacer con el script "**backupImage.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, el usuario remoto como tercer parámetro, y la ruta remota en la que se guardará el backup como cuarto 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, el usuario remoto y la ruta remota deben enviarse 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).
**NOTA3**: Este endpoint comprueba si el backup se ha realizado correctamente, comparando el contenido de los archivos "sum" y "size" con los valores reales (que vuelve a calcular). Si el backup no ha sido correcto (porque la imagen no ha pasado el check de integridad), borra los archivos exportados.
**URL:** `/ogrepository/v1/repo/images`
**Método HTTP:** PUT
@ -402,11 +402,12 @@ Se puede hacer con el script "**exportImage.py**", que debe ser llamado por el e
- **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.
- **remote_path**: Ruta remota en la que copiar la imagen.
**Ejemplo de Solicitud:**
```bash
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
curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d", "repo_ip":"192.168.56.100", "user":"opengnsys", "remote_path":"/home/opengnsys"}' http://example.com/ogrepository/v1/repo/images
```
**Respuestas:**
- **Código 500 Internal Server Error:** Ocurrió un error al exportar la imagen.

View File

@ -16,8 +16,8 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará
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`
9. [Transferir una Imagen entre Repositorios](#transferir-una-imagen-entre-repositorios) - `POST /ogrepository/v1/repo/images`
10. [Hacer Backup de una Imagen](#hacer-backup-de-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`
@ -318,9 +318,9 @@ curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/t
- **Código 200 OK:** La imagen se eliminó exitosamente.
---
### Importar una Imagen
### Transferir una Imagen entre Repositorios
Se importará una imagen de un repositorio remoto al repositorio local.
Se importará una imagen de un repositorio remoto al repositorio local (donde se ejecuta el endpoint).
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á transfiriendo, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore).
@ -345,13 +345,13 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d
- **Código 200 OK:** La imagen se está importando.
---
### Exportar una Imagen
### Hacer Backup de 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**: 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 en el JSON.
**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.
Se hará backup de una imagen en un equipo remoto, que no tiene por qué tener OpenGnsys instalado, pero que debe poder conectar por SSH (mediante claves) con el repositorio local.
Se puede hacer con el script "**backupImage.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, el usuario remoto como tercer parámetro, y la ruta remota en la que se guardará el backup como cuarto 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, el usuario remoto y la ruta remota deben enviarse 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).
**NOTA3**: Este endpoint comprueba si el backup se ha realizado correctamente, comparando el contenido de los archivos "sum" y "size" con los valores reales (que vuelve a calcular). Si el backup no ha sido correcto (porque la imagen no ha pasado el check de integridad), borra los archivos exportados.
**URL:** `/ogrepository/v1/repo/images`
**Método HTTP:** PUT
@ -360,11 +360,12 @@ Se puede hacer con el script "**exportImage.py**", que debe ser llamado por el e
- **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.
- **remote_path**: Ruta remota en la que copiar la imagen.
**Ejemplo de Solicitud:**
```bash
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
curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d", "repo_ip":"192.168.56.100", "user":"opengnsys", "remote_path":"/home/opengnsys"}' http://example.com/ogrepository/v1/repo/images
```
**Respuestas:**
- **Código 500 Internal Server Error:** Ocurrió un error al exportar la imagen.

View File

@ -28,6 +28,7 @@ import logging
import threading
import requests
import random
import hashlib
from systemd import journal
# Imports para Swagger:
from flasgger import Swagger
@ -87,10 +88,6 @@ def get_IPcore():
return "IP no encontrada en el archivo de configuración"
# Almacenamos la IP de ogCore:
ogcore_ip = get_IPcore()
# ---------------------------------------------------------
@ -225,7 +222,6 @@ def check_remote_image(remote_ip, remote_user, image_file_path):
# Conectamos con el equipo remoto por SSH (con claves):
ssh_client.connect(hostname=remote_ip, port=22, username=remote_user, passphrase='')
# Iniciamos un cliente SFTP:
sftp_client = ssh_client.open_sftp()
@ -234,8 +230,6 @@ def check_remote_image(remote_ip, remote_user, image_file_path):
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:
try:
@ -244,10 +238,6 @@ def check_remote_image(remote_ip, remote_user, image_file_path):
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()
sftp_client.close()
@ -271,7 +261,7 @@ def check_lock_local(image_file_path, job_id):
while True:
# 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("Task finalized (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:
@ -299,7 +289,8 @@ def check_lock_local(image_file_path, job_id):
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)")
journal.send("Task in process (.lock file exists)", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Esperamos 1 minuto para volver a realizar la comprobación:
sleep(60)
@ -307,50 +298,113 @@ def check_lock_local(image_file_path, job_id):
# ---------------------------------------------------------
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,
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.
def check_remote_backup(image_name, remote_ip, remote_user, remote_path, job_id):
""" Cada minuto comprueba si se ha copiado la imagen que recibe como primer parámetro (y sus archivos asociados) al equipo remoto que recibe como segundo parámetro,
y mientras eso no suceda seguirá haciendo la comprobación (en un bucle "while" que se ejecuta cada minuto).
Una vez copiados todos los archivos, chequea la integridad de la imagen, comparando el contenido de los archivos ".sum" y ".size" con los valores reales (que vuelve a calcular).
Si la comprobación es correcta saldrá del bucle, y si es incorrecta saldrá del bucle y borrará los archivos que se hayan copiado al equipo remoto
(y en ambos casos le comunicará a ogCore el resultado, llamando a la función "recall_ogcore").
"""
journal.send("Running function 'check_lock_remote'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("Running function 'check_remote_backup'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# 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 (con claves):
ssh_client.connect(hostname=remote_ip, port=22, username=remote_user, passphrase='')
# Iniciamos un cliente SFTP:
sftp_client = ssh_client.open_sftp()
# Esperamos 30 segundos, para dar tiempo a que se cree el archivo ".lock":
# Esperamos 30 segundos antes de empezar a realizar la comprobación:
sleep(30)
# 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']
# Creamos un bucle infinito:
while True:
# Comprobamos si ya se ha copiado la imagen y cada uno de sus archivos asociados (y almacenamos el resultado):
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)")
for ext in extensions:
sftp_client.stat(f"{remote_path}{image_name}{ext}")
all_files_copied = True
except IOError:
# 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}")
journal.send("Remote image unlocked", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'INFO', 'operation':'Run function check_lock_remote', 'desc':'Remote image unlocked'}", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api")
all_files_copied = False
# Si se han copiado todos los archivos, comprobamos la integridad de la imagen:
if all_files_copied == True:
journal.send("All files copied", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
try:
# Calculamos el tamaño del la imagen exportada ("size")
file_size = sftp_client.stat(f"{remote_path}{image_name}").st_size
# Calculamos el hash MD5 del último MB de la imagen exportada ("sum"):
last_mb_offset = max(0, file_size - 1024 * 1024)
with sftp_client.file(f"{remote_path}{image_name}", 'rb') as file:
file.seek(last_mb_offset)
last_mb = file.read(1024 * 1024)
file_sum = hashlib.md5(last_mb).hexdigest()
# Obtenemos el valor almacenado en el archivo ".size:
with sftp_client.file(f"{remote_path}{image_name}.size", 'r') as file:
stored_size = int(file.read().strip())
# Obtenemos el valor almacenado en el archivo ".sum:
with sftp_client.file(f"{remote_path}{image_name}.sum", 'r') as file:
stored_sum = file.read().strip().decode('utf-8')
except Exception as error:
journal.send(f"Integrity check returned an error: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'ERROR', 'operation':'Run function check_remote_backup', 'desc':'Integrity check returned an error'}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api")
# Borramos los archivos copiados, porque no hemos podido chequear la integridad de la imagen:
remove_remote_files(sftp_client, remote_path, image_name, extensions)
# 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
# Comprobamos si los datos almacenados coinciden con los datos obtenidos (en cuyo caso el backup habrá sido correcto):
if file_sum == stored_sum and file_size == stored_size:
journal.send("Task finalized (backup complete)", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'INFO', 'operation':'Run function check_remote_backup', 'desc':'Backup complete'}", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api")
# Almacenamos en un diccionario los datos a enviar a ogCore:
data = {
'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}, SUCCESS: True)", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
recall_ogcore(data)
break
else:
journal.send("Exported 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_remote_backup', 'desc':'Integrity check failed'}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api")
# Borramos los archivos copiados, porque el chequeo de la integridad de la imagen no ha sido exitoso:
remove_remote_files(sftp_client, remote_path, image_name, extensions)
# 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
else:
journal.send("Task in process (backup incomplete)", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Almacenamos en un diccionario los datos a enviar a ogCore:
data = {
'job_id': job_id
}
# 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")
recall_ogcore(data)
break
# Esperamos 1 minuto para volver a realizar la comprobación:
sleep(60)
@ -373,18 +427,15 @@ def check_aux_files(image_file_path, job_id):
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)")
journal.send("Task in process (auxiliar files remaining)", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# 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)")
journal.send("Auxiliar files created", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("Task finalized (all auxilar files created)", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'INFO', 'operation':'Run function check_aux_files', 'desc':'Auxiliar files created'}", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api")
# Obtenemos el valor del archivo "full.sum", que corresponde al ID, y lo imprimimos:
# Obtenemos el valor del archivo "full.sum", que corresponde al ID:
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 = {
@ -409,8 +460,10 @@ def recall_ogcore(data):
"""
journal.send("Running function 'recall_ogcore'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Almacenamos la URL del endpoint de ogCore (prueba):
#endpoint_url = f"http://{ogcore_ip}:8006/ogcore/v1/test"
# Almacenamos la IP de ogCore:
ogcore_ip = get_IPcore()
# Almacenamos la URL del endpoint de ogCore:
endpoint_url = f"https://{ogcore_ip}:8443/og-repository/webhook"
# Almacenamos los headers, y convertiomos "data" a JSON:
@ -422,8 +475,6 @@ def recall_ogcore(data):
response = requests.post(endpoint_url, data=data, headers=headers, verify=False)
# 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}")
journal.send(f"HTTP Status Code: {response.status_code}", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
#journal.send(f"HTTP Response: {response.text}", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send(f"{{'component':'ogRepo', 'severity':'INFO', 'operation':'Run function recall_ogcore', 'desc':'HTTP Status Code response: {response.status_code}'}}", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api")
@ -436,12 +487,35 @@ def check_file_exists(file_path):
""" Comprueba la existencia del archivo cuya ruta recibe como parámetro.
Si el archivo existe devuelve "True", y si no devuelve "False".
"""
journal.send("Running function 'check_file_exists'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Comprobamos si existe el archivo de la ruta "file_path":
if os.path.exists(file_path):
return True
else:
return False
# ---------------------------------------------------------
def remove_remote_files(sftp_client, remote_path, image_name, extensions):
""" Borra la imagen "image_name" y sus archivos asociados (cuyas extensiones están almacenadas en la lista "extensions")
del equipo remoto al que nos hemos conectado previamente (mediante el objeto "sftp_client", que recibe como primer parámetro).
Es llamada por la función "check_remote_backup", cuando la imagen exportada no pasa el test de integridad.
"""
journal.send("Running function 'remove_remote_files'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Iteramos las extensiones de la lista "extensions", para eliminar la imagen "image_name" y sus archivos asociados:
for ext in extensions:
try:
sftp_client.stat(f"{remote_path}{image_name}{ext}")
sftp_client.remove(f"{remote_path}{image_name}{ext}")
journal.send(f"File with extension {ext} removed", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
except IOError:
journal.send(f"File with extension {ext} doesn't exist", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# --------------------------------------------------------------------------------------------
# ENDPOINTS
@ -963,42 +1037,47 @@ def import_image():
# ---------------------------------------------------------
# 10 - Endpoint "Exportar una Imagen" (SINCRONO):
# 10 - Endpoint "Hacer backup de 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), 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,
la IP o hostname del servidor remoto como segundo parámetro, y el usuario con el que conectar al servidor como tercer parámetro.
def backup_image():
""" Este endpoint exporta la imagen especificada como primer parámetro (y todos sus archivos asociados), desde el servidor local a un equipo remoto (que no tiene por qué ser un repositorio).
Para ello, ejecuta el script "backupImage.py", con el nombre de la imagen como primer parámetro, la IP o hostname del equipo remoto como segundo parámetro,
el usuario con el que conectar al equipo remoto como tercer parámetro, y la ruta remota en la que copiar la imagen como cuarto parámetro.
"""
journal.send("Running endpoint 'Exportar una Imagen'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("Running endpoint 'Hacer backup de una Imagen'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# 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")
remote_path = json_data.get("remote_path")
# Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso):
# Si el úitimo carácter de "remote_path" no es una barra, la añadimos:
if remote_path[-1] != "/":
remote_path = f"{remote_path}/"
# Obtenemos el nombre y la extensión de la imagen:
param_dict = get_image_params(image_id, "repo")
# Evaluamos los parámetros obtenidos, para construir la ruta de la imagen, o para devover un error si no se ha encontrado la imagen (o si está bloqueada):
if param_dict:
image_file_path = f"{param_dict['name']}.{param_dict['extension']}"
image_name = f"{param_dict['name']}.{param_dict['extension']}"
# Comprobamos si el archivo que bloquea la imagen existe, llamando a la función "check_file_exists":
image_lock_exists = check_file_exists(f"{repo_path}{image_file_path}.lock")
image_lock_exists = check_file_exists(f"{repo_path}{image_name}.lock")
# Si la imagen existe pero está bloqueada, devolvemos un error:
if image_lock_exists == True:
journal.send("Image is locked", PRIORITY=journal.LOG_WARNING, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'WARNING', 'http_code':'400', 'operation':'Run endpoint export_image', 'desc':'Warning: Image is locked'}", PRIORITY=journal.LOG_WARNING, SYSLOG_IDENTIFIER="ogrepo-api")
journal.send("{'component':'ogRepo', 'severity':'WARNING', 'http_code':'400', 'operation':'Run endpoint backup_image', 'desc':'Warning: Image is locked'}", PRIORITY=journal.LOG_WARNING, SYSLOG_IDENTIFIER="ogrepo-api")
return jsonify({
"success": False,
"exception": "Image is locked"
}), 400
else:
journal.send("Image not found", PRIORITY=journal.LOG_WARNING, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'WARNING', 'http_code':'400', 'operation':'Run endpoint export_image', 'desc':'Warning: Image not found'}", PRIORITY=journal.LOG_WARNING, SYSLOG_IDENTIFIER="ogrepo-api")
journal.send("{'component':'ogRepo', 'severity':'WARNING', 'http_code':'400', 'operation':'Run endpoint backup_image', 'desc':'Warning: Image not found'}", PRIORITY=journal.LOG_WARNING, SYSLOG_IDENTIFIER="ogrepo-api")
return jsonify({
"success": False,
"error": "Image not found"
@ -1009,46 +1088,64 @@ def export_image():
if connection_OK == False:
journal.send("Can't connect to remote server", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'ERROR', 'http_code':'400', 'operation':'Run endpoint export_image', 'desc':'Unable to connect to remote server'}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api")
journal.send("{'component':'ogRepo', 'severity':'ERROR', 'http_code':'400', 'operation':'Run endpoint backup_image', 'desc':'Unable to connect to remote host'}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api")
return jsonify({
"success": False,
"exception": "Can't connect to remote server"
"exception": "Can't connect to remote host"
}), 400
# Construimos la llamada al script:
cmd = ['python3', f"{script_path}/exportImage.py", image_file_path, remote_ip, remote_user]
cmd = ['python3', f"{script_path}/backupImage.py", image_name, remote_ip, remote_user, remote_path]
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:
result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8')
journal.send("Running script 'backupImage.py'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Ejecutamos el script "backupImage.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"BackupImage_{''.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:
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")
if result.returncode is None:
journal.send("Script 'backupImage.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 backupImage.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_remote_backup" en un hilo paralelo
# (para que compruebe si la imagen se ha acabado de copiar exitosamente):
journal.send("Calling function 'check_remote_backup'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
threading.Thread(target=check_remote_backup, args=(image_name, remote_ip, remote_user, remote_path, job_id,)).start()
# Informamos que la imagen se está exportando, y salimos del endpoint:
return jsonify({
"success": True,
"output": "Image exported successfully"
"output": "Making image backup...",
"job_id": job_id
}), 200
else:
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")
journal.send("Script 'backupImage.py' result KO (Backup image failed)", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'ERROR', 'http_code':'500', 'operation':'Run script backupImage.py', 'desc':'Result KO (Backup image failed)'}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api")
return jsonify({
"success": False,
"error": result.stderr
"error": "Backup image failed"
}), 500
except subprocess.CalledProcessError as error:
journal.send(f"Script 'backupImage.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 backupImage.py', 'desc':'Result KO (Process Exception: {str(error_)})'}}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api")
return jsonify({
"success": False,
"process exception": str(error)
}), 500
except Exception as error_description:
if "exit status 5" in str(error_description):
journal.send("Image already exists on remote server", PRIORITY=journal.LOG_WARNING, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'WARNING', 'http_code':'400', 'operation':'Run script exportImage.py', 'desc':'Warning: Image already exists on remote server'}", PRIORITY=journal.LOG_WARNING, SYSLOG_IDENTIFIER="ogrepo-api")
journal.send("Image already exists on remote host", PRIORITY=journal.LOG_WARNING, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'WARNING', 'http_code':'400', 'operation':'Run script backupImage.py', 'desc':'Warning: Image already exists on remote host'}", PRIORITY=journal.LOG_WARNING, SYSLOG_IDENTIFIER="ogrepo-api")
return jsonify({
"success": False,
"exception": "Image already exists on remote server"
"exception": "Image already exists on remote host"
}), 400
else:
journal.send(f"Script 'exportImage.py' result KO (Exception: {str(error_description)})", 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 (Exception: {str(error_description)})'}}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api")
journal.send(f"Script 'backupImage.py' result KO (Exception: {str(error_description)})", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send(f"{{'component':'ogRepo', 'severity':'ERROR', 'http_code':'500', 'operation':'Run script backupImage.py', 'desc':'Result KO (Exception: {str(error_description)})'}}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api")
return jsonify({
"success": False,
"exception": 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: "Transferencia de Imágenes entre Repositorios"
- name: "Transferencia entre Repositorios y Backup"
- name: "Varios"
@ -1294,20 +1294,20 @@ paths:
example: "(Exception description)"
# -----------------------------------------------------------------------------------------------------------
# Apartado "Transferencia de Imágenes entre Repositorios"
# Apartado "Transferencia entre Repositorios y Backup"
# -----------------------------------------------------------------------------------------------------------
/ogrepository/v1/repo/images:
post:
summary: "Importar una Imagen"
summary: "Transferir una Imagen entre Repositorios"
description: |
Este endpoint importa la imagen especificada desde un servidor remoto al servidor local.
Este endpoint importa la imagen especificada desde un repositorio remoto al repositorio local (en el que se ejecuta el endpoint).
Utiliza el script "**importImage.py**", que recibe como parámetros el nombre de la imagen, la IP o hostname del servidor remoto, y el usuario con el que conectar al servidor remoto.
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:
- "Transferencia de Imágenes entre Repositorios"
- "Transferencia entre Repositorios y Backup"
parameters:
- name: JSON
in: body
@ -1413,16 +1413,14 @@ paths:
#/ogrepository/v1/repo/images:
put:
summary: "Exportar una Imagen"
summary: "Hacer Backup de 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, y el usuario con el que conectar al servidor remoto.
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.
Este endpoint hace un backup de la imagen especificada, exportándola desde el servidor local a un equipo remoto (que no tiene por qué tener OpenGnsys instalado, pero si conectividad SSH).
Utiliza el script "**backupImage.py**", que recibe como parámetros el nombre de la imagen, la IP o hostname del servidor remoto, el usuario con el que conectar al servidor remoto, y la ruta remota en la que guardar el backup.
**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).
**NOTA**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está copiando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore).
tags:
- "Transferencia de Imágenes entre Repositorios"
- "Transferencia entre Repositorios y Backup"
parameters:
- name: JSON
in: body
@ -1431,6 +1429,7 @@ paths:
* **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
* **remote_path** - Ruta remota en la que copiar la imagen
schema:
type: object
properties:
@ -1445,9 +1444,13 @@ paths:
type: string
description: "Usuario para acceder al repositorio remoto"
example: "user_name"
remote_path:
type: string
description: "Ruta remota en la que copiar la imagen"
example: "/home/opengnsys"
responses:
"200":
description: "La imagen se ha exportado exitosamente."
description: "La imagen se está copiando."
schema:
type: object
properties:
@ -1456,7 +1459,7 @@ paths:
example: true
output:
type: string
example: "Image exported successfully"
example: "Making image backup..."
"400 (Image not found)":
description: "No se ha encontrado la imagen."
schema:
@ -1480,7 +1483,7 @@ paths:
type: string
example: "Image is locked"
"400 (Connection fail)":
description: "Error de conexión con el servidor remoto."
description: "Error de conexión con el equipo remoto."
schema:
type: object
properties:
@ -1489,9 +1492,9 @@ paths:
example: false
exception:
type: string
example: "Can't connect to remote server"
example: "Can't connect to remote host"
"400 (Image present)":
description: "La imagen ya existe en el servidor remoto."
description: "La imagen ya existe en el equipo remoto."
schema:
type: object
properties:
@ -1500,9 +1503,9 @@ paths:
example: false
exception:
type: string
example: "Image already exists on remote server"
example: "Image already exists on remote host"
"500 (Error)":
description: "Error al exportar la imagen."
description: "Error al copiar la imagen."
schema:
type: object
properties:
@ -1513,7 +1516,7 @@ paths:
type: string
example: "(Error description)"
"500 (Exception)":
description: "Excepción inesperada al exportar la imagen."
description: "Excepción inesperada al copiar la imagen."
schema:
type: object
properties:

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_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, check_remote_backup
# --------------------------------------------------------------------------------------------
@ -467,94 +467,98 @@ class RepoApiTestCase(unittest.TestCase):
)
# ------------------------------------------------------------------- Tests "Exportar una Imagen"
# ------------------------------------------------------------------- Tests "Hacer backup de una Imagen"
@patch('repo_api.check_remote_connection')
@patch('repo_api.check_remote_backup')
@patch('repo_api.get_image_params')
@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".
@patch('repo_api.subprocess.Popen')
def test_backup_image(self, mock_popen, mock_get_image_params, mock_check_remote_connection, mock_check_remote_backup):
""" Método de prueba del endpoint "Hacer backup de una Imagen".
"""
print("Testing endpoint 'Exportar una Imagen'...")
print("Testing endpoint 'Hacer backup de una Imagen'...")
mock_check_remote_connection.return_value = True
mock_get_image_params.return_value = {'name': 'test4unittest', 'extension': 'img'}
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')
mock_popen.return_value = MagicMock(returncode=None)
mock_check_remote_backup.return_value = AsyncMock(returncode=None)
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", "remote_path": "/tmp"}), content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertIn('Image exported successfully', json.loads(response.data)['output'])
self.assertIn('Making image backup...', json.loads(response.data)['output'])
@patch('repo_api.check_remote_connection')
@patch('repo_api.check_remote_backup')
@patch('repo_api.get_image_params')
@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"
@patch('repo_api.subprocess.Popen')
def test_backup_image_error500(self, mock_popen, mock_get_image_params, mock_check_remote_connection, mock_check_remote_backup):
""" Método de prueba del endpoint "Hacer backup de una Imagen"
(en caso de error "500").
"""
print("Testing endpoint 'Exportar una Imagen' (error 500)...")
print("Testing endpoint 'Hacer backup de una Imagen' (error 500)...")
mock_check_remote_connection.return_value = True
mock_get_image_params.return_value = {'name': 'test4unittest', 'extension': 'img'}
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')
mock_popen.side_effect = Exception("Error al exportar la imagen")
mock_check_remote_backup.return_value = AsyncMock(returncode=None)
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", "remote_path": "/tmp"}), content_type='application/json')
self.assertEqual(response.status_code, 500)
self.assertIn('Error al exportar la imagen', response.data.decode())
@patch('repo_api.check_remote_connection')
@patch('repo_api.get_image_params')
def test_export_image_error400_connection(self, mock_get_image_params, mock_check_remote_connection):
""" Método de prueba del endpoint "Exportar una Imagen"
def test_backup_image_error400_connection(self, mock_get_image_params, mock_check_remote_connection):
""" Método de prueba del endpoint "Hacer backup de una Imagen"
(en caso de error "400", por fallo en la conexión).
"""
print("Testing endpoint 'Exportar una Imagen' (error 400, no connection)...")
print("Testing endpoint 'Hacer backup de una Imagen' (error 400, no connection)...")
mock_check_remote_connection.return_value = False
mock_get_image_params.return_value = {'name': 'test4unittest', 'extension': 'img'}
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')
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", "remote_path": "/tmp"}), content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertIn("Can't connect to remote server", json.loads(response.data)['exception'])
self.assertIn("Can't connect to remote host", json.loads(response.data)['exception'])
@patch('repo_api.get_image_params')
def test_export_image_error400_image_inexistent(self, mock_get_image_params):
""" Método de prueba del endpoint "Exportar una Imagen"
def test_backup_image_error400_image_inexistent(self, mock_get_image_params):
""" Método de prueba del endpoint "Hacer backup de una Imagen"
(en caso de error "400", por imagen inexistente).
"""
print("Testing endpoint 'Exportar una Imagen' (error 400, no image)...")
print("Testing endpoint 'Hacer backup de una Imagen' (error 400, no image)...")
mock_get_image_params.return_value = None
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')
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", "remote_path": "/tmp"}), content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertIn("Image not found", json.loads(response.data)['error'])
@patch('repo_api.check_file_exists')
@patch('repo_api.get_image_params')
def test_export_image_error400_image_locked(self, mock_get_image_params, mock_check_file_exists):
""" Método de prueba del endpoint "Exportar una Imagen"
def test_backup_image_error400_image_locked(self, mock_get_image_params, mock_check_file_exists):
""" Método de prueba del endpoint "Hacer backup de una Imagen"
(en caso de error "400", por imagen bloqueada).
"""
print("Testing endpoint 'Exportar una Imagen' (error 400, image locked)...")
print("Testing endpoint 'Hacer backup de una Imagen' (error 400, image locked)...")
mock_get_image_params.return_value = {'name': 'test4unittest', 'extension': 'img'}
mock_check_file_exists.return_value = True
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')
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", "remote_path": "/tmp"}), content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertIn("Image is locked", json.loads(response.data)['exception'])
@patch('repo_api.check_remote_connection')
@patch('repo_api.get_image_params')
@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"
@patch('repo_api.subprocess.Popen')
def test_backup_image_error400_remote_image_exists(self, mock_popen, mock_get_image_params, mock_check_remote_connection):
""" Método de prueba del endpoint "Hacer backup de una Imagen"
(en caso de error "400", por imagen remota ya existente).
"""
print("Testing endpoint 'Exportar una Imagen' (error 400, remote image exists)...")
print("Testing endpoint 'Hacer backup de 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_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')
mock_popen.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", "remote_path": "/tmp"}), content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertIn("Image already exists on remote server", json.loads(response.data)['exception'])
self.assertIn("Image already exists on remote host", json.loads(response.data)['exception'])
# ------------------------------------------------------------------- Tests "Crear archivos auxiliares"

188
bin/backupImage.py 100644
View File

@ -0,0 +1,188 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Este script hace un backup de la imagen especificada como primer parámetro (y sus archivos asociados), en el equipo remoto especificado como segundo parámetro,
con las credenciales del usuario especificado como tercer parámetro (en principio, mediante claves), y en la ruta remota especificada como cuarto parámetro.
Realiza la misma acción que el script "exportImage.py", pero con la salvedad de que requiere la ruta remota en la que se copiará la imagen, y que no requiere que el equipo remoto sea un repositorio.
Lo que si es necesario es que ambos equipos puedan conectar por SSH mediante claves (que deben haberse generado y distribuido previamente).
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 copiar (SIN ruta).
- Ejemplo1: image1.img
- Ejemplo2: image2.img
sys.argv[2] - IP o hostname del equipo remoto.
- Ejemplo1: 192.168.56.100
- Ejemplo2: remote_repo
sys.argv[3] - Usuario con el que conectar al equipo remoto.
- Ejemplo1: remote_user
- Ejemplo2: root
sys.argv[4] - Ruta remota en la que se copiará la imagen.
- Ejemplo1: /tmp
- Ejemplo2: /home/opengnsys
Sintaxis
----------
./backupImage.py image_name remote_host remote_user remote_path
Ejemplos
---------
./backupImage.py image1.img 192.168.56.100 user /tmp
./backupImage.py image2.img remote_hostname user /home/opengnsys
"""
# --------------------------------------------------------------------------------------------
# IMPORTS
# --------------------------------------------------------------------------------------------
import warnings
warnings.filterwarnings("ignore")
import os
import sys
import subprocess
import paramiko
import warnings
from systemd import journal
# --------------------------------------------------------------------------------------------
# VARIABLES
# --------------------------------------------------------------------------------------------
script_name = os.path.basename(__file__)
repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final
# --------------------------------------------------------------------------------------------
# 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 remote_host remote_user remote_path
Ejemplo1: {script_name} image1.img 192.168.56.100 user /tmp
Ejemplo2: {script_name} image2.img remote_hostname user /home/opengnsys
"""
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 4 parámetros, se muestra un error y la ayuda, y se sale del script:
elif len(sys.argv) != 5:
print(f"{script_name} Error: Formato incorrecto: Se debe especificar 4 parámetros")
show_help()
sys.exit(1)
def backup_image(image_name, remote_host, remote_user, remote_path):
""" Conecta al equipo remoto por SSH e inicia un cliente SFTP.
Luego copia la imagen al equipo 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', '.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(hostname=remote_host, port=22, username=remote_user, passphrase='')
sftp_client = ssh_client.open_sftp()
except Exception as error_description:
journal.send(f"backupImage.py: Connection exception: {error_description}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
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(f"{remote_path}{image_name}")
journal.send("backupImage.py: Image already exists on remote host", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
print("Image already exists on remote host.")
sys.exit(5)
except IOError:
print("As expected, image doesn't exist on remote host.")
# Copiamos la imagen al equipo remoto, junto con sus archivos asociados:
for ext in extensions:
sftp_client.put(f"{repo_path}{image_name}{ext}", f"{remote_path}{image_name}{ext}")
# Renombramos el archivo remoto ".info.checked" a ".info", para que lo pille el script "updateRepoInfo.py":
journal.send("backupImage.py: Renaming '.info.checked' file to '.info'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
sftp_client.rename(f"{remote_path}{image_name}.info.checked", f"{remote_path}{image_name}.info")
# Cerramos el cliente SSH y el cliente SFTP:
journal.send("backupImage.py: Closing remote connection...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
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 el nombre de la imagen a copiar (desde los parámetros):
image_name = sys.argv[1]
# Si no existe el archivo de imagen, imprimimos un mensaje de error y salimos del script:
if not os.path.exists(f"{repo_path}{image_name}"):
journal.send("backupImage.py: Image not found", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
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"{repo_path}{image_name}.lock"):
journal.send("backupImage.py: Image is locked", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
print("Image is locked.")
sys.exit(3)
# Almacenamos la IP/hostname del equipo remoto, el usuario remoto y la ruta remota (desde los parámetros):
remote_host = sys.argv[2]
remote_user = sys.argv[3]
remote_path = sys.argv[4]
# Si el úitimo carácter de "remote_path" no es una barra, la añadimos:
if remote_path[-1] != "/":
remote_path = f"{remote_path}/"
# Copiamos la imagen al equipo remoto:
backup_image(image_name, remote_host, remote_user, remote_path)
# --------------------------------------------------------------------------------------------
if __name__ == "__main__":
main()
# --------------------------------------------------------------------------------------------