Merge pull request 'refs #1525 - Add Convert VM to IMG' (#23) from add_python_scripts into main

Reviewed-on: #23
deb-jenkins-pkg 0.6.0
Gerardo GIl Elizeire 2025-03-03 16:32:38 +01:00
commit 48dfc3d190
6 changed files with 878 additions and 115 deletions

View File

@ -20,6 +20,9 @@ Paquetes APT requeridos:
- **bittorrent** (se puede instalar con "sudo apt install bittorrent", pero previamente hay que añadir un repositorio de Debian)
- **bittornado** (se puede instalar con "sudo apt install bittornado", pero previamente hay que añadir un repositorio de Debian)
- **wakeonlan** (se puede instalar con "sudo apt install wakeonlan")
- **qemu** (se puede instalar con "sudo apt install qemu-utils")
- **partclone** (se puede instalar con "sudo apt install partclone")
- **lzop** (se puede instalar con "sudo apt install lzop")
Librerías Python requeridas:
- **flask** (se puede instalar con "sudo apt install python3-flask")
@ -31,6 +34,7 @@ Librerías Python requeridas:
Para que todos los endpoints y scripts funcionen con la configuración actual deben existir los siguientes directorios:
- **/opt/opengnsys/ogrepository/images/**
- **/opt/opengnsys/ogrepository/images_trash/** (debe estar en la misma partición que el anterior, o tardarán mucho las eliminaciones y restauraciones)
- **/opt/opengnsys/ogrepository/images_virtual/** (aquí deben copiarse las imágenes virtuales que se quiera convertir a "img")
- **/opt/opengnsys/ogrepository/bin/** (aquí deben estar todos los scripts de Python, y el binario "udp-sender")
- **/opt/opengnsys/ogrepository/api/** (aquí debe estar la API y el Swagger)
- **/opt/opengnsys/ogrepository/etc/** (aquí se guardan los archivos "repoinfo.json" y "trashinfo.json")
@ -61,20 +65,21 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará
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`
14. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/uftp`
15. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/p2p`
16. [Ver Estado de Transmisiones UDPcast](#ver-estado-de-transmisiones-udpcast) - `GET /ogrepository/v1/udpcast`
17. [Ver Estado de Transmisiones UFTP](#ver-estado-de-transmisiones-uftp) - `GET /ogrepository/v1/uftp`
18. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}`
19. [Cancelar Transmisión UFTP](#cancelar-transmisión-uftp) - `DELETE /ogrepository/v1/uftp/images/{ID_img}`
20. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p`
12. [Convertir Imagen Virtual](#convertir-imagen-virtual) - `POST /ogrepository/v1/images/virtual`
13. [Enviar paquete Wake On Lan](#enviar-paquete-wake-on-lan) - `POST /ogrepository/v1/wol`
14. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/udpcast`
15. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/uftp`
16. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/p2p`
17. [Ver Estado de Transmisiones UDPcast](#ver-estado-de-transmisiones-udpcast) - `GET /ogrepository/v1/udpcast`
18. [Ver Estado de Transmisiones UFTP](#ver-estado-de-transmisiones-uftp) - `GET /ogrepository/v1/uftp`
19. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}`
20. [Cancelar Transmisión UFTP](#cancelar-transmisión-uftp) - `DELETE /ogrepository/v1/uftp/images/{ID_img}`
21. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p`
---
### Obtener Información de Estado de ogRepository
Se devolverá informacion de CPU, memoria RAM, disco duro y el estado de ciertos servicios y procesos de ogRepository, en formato JSON.
Se devolverá informacion de CPU, memoria RAM, disco duro y el estado de ciertos servicios y procesos de ogRepository, en formato JSON.
Se puede utilizar el script "**getRepoStatus.py**, que debe ser llamado por el endpoint.
**NOTA**: En los apartados "services" y "processes" he especificado los servicios y procesos que me han parecido interesantes, pero se puede añadir o eliminar los que se desee.
@ -382,9 +387,9 @@ Se puede hacer con el script "**importImage.py**", que debe ser llamado por el e
curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "repo_ip":"192.168.56.100", "user":"opengnsys"}' http://example.com/ogrepository/v1/repo/images
```
**Respuestas:**
- **Código 500 Internal Server Error:** Ocurrió un error al importar la imagen.
- **Código 500 Internal Server Error:** Ocurrió un error al transferir 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á importando.
- **Código 200 OK:** La imagen se está transfiriendo.
---
### Hacer Backup de una Imagen
@ -410,9 +415,9 @@ Se puede hacer con el script "**backupImage.py**", que debe ser llamado por el e
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.
- **Código 500 Internal Server Error:** Ocurrió un error al hacer backup de la imagen.
- **Código 400 Bad Request:** No se ha encontrado la imagen y/o el equipo remoto especificados.
- **Código 200 OK:** La imagen se ha exportando exitosamente.
- **Código 200 OK:** Se está haciendo backup de la imagen.
---
### Crear archivos auxiliares
@ -438,6 +443,31 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d
- **Código 400 Bad Request:** No se ha encontrado la imagen especificada.
- **Código 200 OK:** Los archivos auxiliares se están creando.
---
### Convertir Imagen Virtual
Se convertirá la imagen virtual especificada (que debe haberse copiado previamente en la ruta "opt/opengnsys/ogrepository/images_virtual") en una imagen "img" como las que se generan desde OpenGnsys (con "partclone" y "lzop").
Se puede hacer con el script "**convertVMtoIMG.py**", que debe ser llamado por el endpoint.
**NOTA**: El script requiere que se le pase el nombre de la imagen virtual (con extensión) como primer parámetro, y el sistema de archivos de la partición a clonar como segundo parámetro (en formato "blkid"). Estos parámetros 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 virtual se está convirtiendo, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore).
**URL:** `/ogrepository/v1/images/virtual`
**Método HTTP:** POST
**Cuerpo de la Solicitud (JSON):**
- **virtual_image**: Nombre de la imagen virtual (con extensión).
- **filesystem**: Sistema de archivos de la partición a clonar, en formato "blkid".
**Ejemplo de Solicitud:**
```bash
curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"virtual_image":"UbuntuVM.vdi", "filesystem":"ext4"}' http://example.com/ogrepository/v1/images/virtual
```
**Respuestas:**
- **Código 500 Internal Server Error:** Ocurrió un error al convertir la imagen virtual.
- **Código 400 Bad Request:** No se ha encontrado la imagen virtual especificada.
- **Código 200 OK:** La imagen virtual se está convirtiendo.
---
### Enviar paquete Wake On Lan

View File

@ -19,20 +19,21 @@ El presente documento detalla los endpoints de la API, con sus respectivos pará
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`
14. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/uftp`
15. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/p2p`
16. [Ver Estado de Transmisiones UDPcast](#ver-estado-de-transmisiones-udpcast) - `GET /ogrepository/v1/udpcast`
17. [Ver Estado de Transmisiones UFTP](#ver-estado-de-transmisiones-uftp) - `GET /ogrepository/v1/uftp`
18. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}`
19. [Cancelar Transmisión UFTP](#cancelar-transmisión-uftp) - `DELETE /ogrepository/v1/uftp/images/{ID_img}`
20. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p`
12. [Convertir Imagen Virtual](#convertir-imagen-virtual) - `POST /ogrepository/v1/images/virtual`
13. [Enviar paquete Wake On Lan](#enviar-paquete-wake-on-lan) - `POST /ogrepository/v1/wol`
14. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/udpcast`
15. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/uftp`
16. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/p2p`
17. [Ver Estado de Transmisiones UDPcast](#ver-estado-de-transmisiones-udpcast) - `GET /ogrepository/v1/udpcast`
18. [Ver Estado de Transmisiones UFTP](#ver-estado-de-transmisiones-uftp) - `GET /ogrepository/v1/uftp`
19. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}`
20. [Cancelar Transmisión UFTP](#cancelar-transmisión-uftp) - `DELETE /ogrepository/v1/uftp/images/{ID_img}`
21. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p`
---
### Obtener Información de Estado de ogRepository
Se devolverá informacion de CPU, memoria RAM, disco duro y el estado de ciertos servicios y procesos de ogRepository, en formato JSON.
Se devolverá informacion de CPU, memoria RAM, disco duro y el estado de ciertos servicios y procesos de ogRepository, en formato JSON.
Se puede utilizar el script "**getRepoStatus.py**, que debe ser llamado por el endpoint.
**NOTA**: En los apartados "services" y "processes" he especificado los servicios y procesos que me han parecido interesantes, pero se puede añadir o eliminar los que se desee.
@ -340,9 +341,9 @@ Se puede hacer con el script "**importImage.py**", que debe ser llamado por el e
curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "repo_ip":"192.168.56.100", "user":"opengnsys"}' http://example.com/ogrepository/v1/repo/images
```
**Respuestas:**
- **Código 500 Internal Server Error:** Ocurrió un error al importar la imagen.
- **Código 500 Internal Server Error:** Ocurrió un error al transferir 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á importando.
- **Código 200 OK:** La imagen se está transfiriendo.
---
### Hacer Backup de una Imagen
@ -368,9 +369,9 @@ Se puede hacer con el script "**backupImage.py**", que debe ser llamado por el e
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.
- **Código 500 Internal Server Error:** Ocurrió un error al hacer backup de la imagen.
- **Código 400 Bad Request:** No se ha encontrado la imagen y/o el equipo remoto especificados.
- **Código 200 OK:** La imagen se ha exportando exitosamente.
- **Código 200 OK:** Se está haciendo backup de la imagen.
---
### Crear archivos auxiliares
@ -396,6 +397,31 @@ curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d
- **Código 400 Bad Request:** No se ha encontrado la imagen especificada.
- **Código 200 OK:** Los archivos auxiliares se están creando.
---
### Convertir Imagen Virtual
Se convertirá la imagen virtual especificada (que debe haberse copiado previamente en la ruta "opt/opengnsys/ogrepository/images_virtual") en una imagen "img" como las que se generan desde OpenGnsys (con "partclone" y "lzop").
Se puede hacer con el script "**convertVMtoIMG.py**", que debe ser llamado por el endpoint.
**NOTA**: El script requiere que se le pase el nombre de la imagen virtual (con extensión) como primer parámetro, y el sistema de archivos de la partición a clonar como segundo parámetro (en formato "blkid"). Estos parámetros 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 virtual se está convirtiendo, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore).
**URL:** `/ogrepository/v1/images/virtual`
**Método HTTP:** POST
**Cuerpo de la Solicitud (JSON):**
- **virtual_image**: Nombre de la imagen virtual (con extensión).
- **filesystem**: Sistema de archivos de la partición a clonar, en formato "blkid".
**Ejemplo de Solicitud:**
```bash
curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"virtual_image":"UbuntuVM.vdi", "filesystem":"ext4"}' http://example.com/ogrepository/v1/images/virtual
```
**Respuestas:**
- **Código 500 Internal Server Error:** Ocurrió un error al convertir la imagen virtual.
- **Código 400 Bad Request:** No se ha encontrado la imagen virtual especificada.
- **Código 200 OK:** La imagen virtual se está convirtiendo.
---
### Enviar paquete Wake On Lan

View File

@ -29,6 +29,7 @@ import threading
import requests
import random
import hashlib
import psutil
from systemd import journal
# Imports para Swagger:
from flasgger import Swagger
@ -40,6 +41,7 @@ import yaml
# --------------------------------------------------------------------------------------------
repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final
vm_path = '/opt/opengnsys/ogrepository/images_virtual/' # No borrar la barra final
script_path = '/opt/opengnsys/ogrepository/bin'
repo_file = '/opt/opengnsys/ogrepository/etc/repoinfo.json'
trash_file = '/opt/opengnsys/ogrepository/etc/trashinfo.json'
@ -453,6 +455,62 @@ def check_aux_files(image_file_path, job_id):
# ---------------------------------------------------------
def check_virtual_image_conversion(image_name, job_id):
""" Cada minuto comprueba si existe la imagen convertida (y sus archivos asociados), o si no existe ninguno de los archivos que se crean en el proceso.
Cuando ya no exista el archivo ".lock" (pero si los demás archivos), le comunicará a ogCore que la conversión ha sido exitosa, y saldrá de bucle.
Cuando no exista ninguno de los archivos que se crean en el proceso (incluyendo la imagen convertida), le comunicará a ogCore que la conversión ha fallado, y saldrá de bucle.
Mientras no se cumpla ninguna de las condiciones anteriores, seguirá haciendo la comprobación (repitiendo el bucle cada minuto).
"""
journal.send("Running function 'check_virtual_image_conversion'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Esperamos 30 segundos, para dar tiempo a que se cree algún archivo:
sleep(30)
# Construimos la ruta de la imagen (una vez convertida):
image_file_path = f"{repo_path}{image_name}.img"
# Creamos un bucle infinito:
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("Task finalized (Virtual image converted)", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'INFO', 'operation':'Run function check_virtual_image_conversion', 'desc':'Virtual image converted'}", 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
# Si no existe ninguno de los archivos que se crean en el proceso (incluyendo la imagen convertida), respondemos a ogCore (con "success: False") y salimos del bucle:
elif not os.path.exists(f"{vm_path}{image_name}.raw") and not os.path.exists(f"{vm_path}{image_name}.img") and not os.path.exists(f"{vm_path}{image_name}.img.lzo") and not os.path.exists(f"{repo_path}{image_name}.img"):
journal.send("Virtual image conversion failed", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'ERROR', 'operation':'Run function check_virtual_image_conversion', 'desc':'Virtual image conversion 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 no se han cumplido las condiciones anteriores), imprimimos un mensaje en la API:
else:
journal.send("Task in process (Conversion not finalized)", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Esperamos 1 minuto para volver a realizar la comprobación:
sleep(60)
# ---------------------------------------------------------
def recall_ogcore(data):
""" Hace una petición HTTP de tipo POST a un endpoint de ogCore, enviando datos que dependen del caso.
Se utiliza para informar a ogCore del resultado de una tarea asíncrona,
@ -516,6 +574,27 @@ def remove_remote_files(sftp_client, remote_path, image_name, extensions):
journal.send(f"File with extension {ext} doesn't exist", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# ---------------------------------------------------------
def check_free_space(vm_image_name_full):
""" Comprueba si hay suficiente espacio en disco para convertir la imagen virtual que recibe como parámetro
(3 veces el tamaño del archivo), devolviendo "True" si lo hay, y "False" si no lo hay.
Lo utiliza el endpoint "Convertir imagen virtual"
"""
# Obtenemos el tamaño de la imagen virtual:
vm_size = int(os.path.getsize(f"{vm_path}{vm_image_name_full}"))
# Obtenemos la cantidad de espacio libre en disco:
disk = psutil.disk_usage('/')
free_space = int(disk.free)
# Si no hay suficiente espacio libre en disco (3 veces el tamaño de la imagen virtual), devolvemos "False", y en caso contrario "True":
if free_space < (vm_size * 3):
return False
else:
return True
# --------------------------------------------------------------------------------------------
# ENDPOINTS
@ -1136,20 +1215,12 @@ def backup_image():
"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 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 host"
}), 400
else:
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)
}), 500
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)
}), 500
# ---------------------------------------------------------
@ -1217,27 +1288,12 @@ def create_torrent_sum():
"error": result.stderr
}), 500
except Exception as error_description:
if "exit status 2" in str(error_description):
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 script createTorrentSum.py', 'desc':'Warning: Image not found'}", PRIORITY=journal.LOG_WARNING, SYSLOG_IDENTIFIER="ogrepo-api")
return jsonify({
"success": False,
"exception": "Image not found"
}), 400
elif "exit status 3" in str(error_description):
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 script createTorrentSum.py', '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(f"Script 'createTorrentSum.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 createTorrentSum.py', 'desc':'Result KO (Exception: {str(error_description)})'}}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api")
return jsonify({
"success": False,
"exception": str(error_description)
}), 500
journal.send(f"Script 'createTorrentSum.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 createTorrentSum.py', 'desc':'Result KO (Exception: {str(error_description)})'}}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api")
return jsonify({
"success": False,
"exception": str(error_description)
}), 500
# ---------------------------------------------------------
@ -1786,6 +1842,113 @@ def stop_p2p():
}), 500
# ---------------------------------------------------------
# 21 - Endpoint "Convertir imagen virtual" (ASINCRONO):
@app.route("/ogrepository/v1/images/virtual", methods=['POST'])
def convert_virtual_image():
""" Este endpoint convierte la imagen virtual especificada como primer parámetro en una imagen "img" como las que se generan desde OpenGnsys
(con "partclone" y "lzop"), por lo que luego puede ser restaurada como cualquier otra imagen del repositorio.
Para ello, ejecuta el script "convertVMtoIMG.py", con el nombre de la imagen virtual como primer parámetro,
y el sistema de archivos de la partición a clonar (en formato "blkid") como segundo parámetro.
"""
journal.send("Running endpoint 'Convertir imagen virtual'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Almacenamos los parámetros enviados en el JSON, y extraemos el nombre y la extensión:
json_data = json.loads(request.data)
vm_image_name_full = json_data.get("virtual_image")
vm_image_name = vm_image_name_full.split('.')[0]
vm_extension = vm_image_name_full.split('.')[1]
filesystem = json_data.get("filesystem").lower()
# Comprobamos si existe la imagen virtual, llamando a la función "check_file_exists":
vm_image_exists = check_file_exists(f"{vm_path}{vm_image_name_full}")
# Si la imagen virtual no existe, devolvemos un error:
if vm_image_exists == False:
journal.send("Virtual image not found", PRIORITY=journal.LOG_WARNING, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'WARNING', 'http_code':'400', 'operation':'Run endpoint convert_virtual_image', 'desc':'Warning: Virtual image not found'}", PRIORITY=journal.LOG_WARNING, SYSLOG_IDENTIFIER="ogrepo-api")
return jsonify({
"success": False,
"exception": "Virtual image not found"
}), 400
# Comprobamos si ya existe una imagen "img" con el mismo nombre que la imagen virtual, llamando a la función "check_file_exists":
img_image_exists = check_file_exists(f"{repo_path}{vm_image_name}.img")
# Si existe una imagen con el mismo nombre que la imagen virtual (salvo por la extensión), devolvemos un error:
if img_image_exists == True:
journal.send("There is an image with the same name as the virtual image", PRIORITY=journal.LOG_WARNING, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'WARNING', 'http_code':'400', 'operation':'Run endpoint convert_virtual_image', 'desc':'Warning: There is an image with the same name as the virtual image'}", PRIORITY=journal.LOG_WARNING, SYSLOG_IDENTIFIER="ogrepo-api")
return jsonify({
"success": False,
"exception": "There is an image with the same name as the virtual image"
}), 400
# Comprobamos si hay espacio suficiente en disco para convertir la imagen virtual (3 veces su tamaño):
enough_free_space = check_free_space(vm_image_name_full)
# Si no hay suficiente espacio libre en disco, devolvemos un error:
if enough_free_space == False:
journal.send("There is not enough free disk space", PRIORITY=journal.LOG_WARNING, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'WARNING', 'http_code':'400', 'operation':'Run endpoint convert_virtual_image', 'desc':'Warning: There is not enough free disk space'}", PRIORITY=journal.LOG_WARNING, SYSLOG_IDENTIFIER="ogrepo-api")
return jsonify({
"success": False,
"exception": "There is not enough free disk space"
}), 400
# Construimos la llamada al script:
cmd = ['python3', f"{script_path}/convertVMtoIMG.py", vm_image_name_full, filesystem]
try:
journal.send("Running script 'convertVMtoIMG.py'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Ejecutamos el script "convertVMtoIMG.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"ConvertImage_{''.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 is None:
journal.send("Script 'convertVMtoIMG.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 convertVMtoIMG.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_virtual_image_conversion" en un hilo paralelo
# (para que compruebe si la imagen se ha acabado de convertir exitosamente):
journal.send("Calling function 'check_virtual_image_conversion'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
threading.Thread(target=check_virtual_image_conversion, args=(vm_image_name, job_id,)).start()
# Informamos que la imagen se está convirtiendo, y salimos del endpoint:
return jsonify({
"success": True,
"output": "Converting virtual image...",
"job_id": job_id
}), 200
else:
journal.send("Script 'convertVMtoIMG.py' result KO (Virtual image conversion failed)", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
journal.send("{'component':'ogRepo', 'severity':'ERROR', 'http_code':'500', 'operation':'Run script convertVMtoIMG.py', 'desc':'Result KO (Error: Virtual image conversion failed)'}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api")
return jsonify({
"success": False,
"error": "Virtual image conversion failed"
}), 500
except subprocess.CalledProcessError as error:
journal.send(f"Script 'convertVMtoIMG.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 convertVMtoIMG.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:
journal.send(f"Script 'convertVMtoIMG.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 convertVMtoIMG.py', 'desc':'Result KO (Exception: {str(error_description)})'}}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api")
return jsonify({
"success": False,
"exception": str(error_description)
}), 500
# --------------------------------------------------------------------------------------------

View File

@ -1332,7 +1332,7 @@ paths:
example: "user_name"
responses:
"200":
description: "La imagen se está importando."
description: "La imagen se está transfiriendo."
schema:
type: object
properties:
@ -1450,7 +1450,7 @@ paths:
example: "/home/opengnsys"
responses:
"200":
description: "La imagen se está copiando."
description: "Se está haciendo backup de la imagen."
schema:
type: object
properties:
@ -1493,17 +1493,6 @@ paths:
exception:
type: string
example: "Can't connect to remote host"
"400 (Image present)":
description: "La imagen ya existe en el equipo remoto."
schema:
type: object
properties:
success:
type: boolean
example: false
exception:
type: string
example: "Image already exists on remote host"
"500 (Error)":
description: "Error al copiar la imagen."
schema:
@ -1578,17 +1567,6 @@ paths:
exception:
type: string
example: "Image not found"
"400 (Image locked)":
description: "La imagen está bloqueada."
schema:
type: object
properties:
success:
type: boolean
example: false
exception:
type: string
example: "Image is locked"
"500 (Error)":
description: "Error al crear los archivos auxiliares."
schema:
@ -1673,4 +1651,112 @@ paths:
type: string
example: "(Exception description)"
# -----------------------------------------------------------------------------------------------------------
/ogrepository/v1/images/virtual:
post:
summary: "Convertir imagen virtual"
description: |
Este endpoint convierte la imagen virtual especificada como primer parámetro en una imagen "img" como las que se generan desde OpenGnsys, debiendo haberse copiado previamente en la ruta "opt/opengnsys/ogrepository/images_virtual".
Utiliza el script "**convertVMtoIMG.py**", que recibe como parámetros el nombre de la imagen virtual, y el sistema de archivos de la partición a clonar (en formato "blkid").
Se puede comprobar todos los sistemas de archivos aceptados por "blkid" ejecutando el comando "blkid -k".
**NOTA**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen virtual se está convirtiendo, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore).
tags:
- "Varios"
parameters:
- name: JSON
in: body
required: true
description: |
* **virtual_image** - Nombre de la imagen virtual, con extensión
* **filesystem** - Sistema de archivos de la partición a clonar, en formato "blkid"
schema:
type: object
properties:
virtual_image:
type: string
example: "UbuntuVM.vdi"
filesystem:
type: string
example: "ext4"
responses:
"200":
description: "La imagen virtual se está convirtiendo."
schema:
type: object
properties:
success:
type: boolean
example: true
output:
type: string
example: "Converting virtual image..."
"400 (Virtual image not found)":
description: "No se ha encontrado la imagen virtual."
schema:
type: object
properties:
success:
type: boolean
example: false
exception:
type: string
example: "Virtual image not found"
"400 (Name incorrect)":
description: "Ya existe una imagen con el mismo nombre que la imagen virtual."
schema:
type: object
properties:
success:
type: boolean
example: false
exception:
type: string
example: "There is an image with the same name as the virtual image"
"400 (No disk space)":
description: "No hay espacio suficiente en disco."
schema:
type: object
properties:
success:
type: boolean
example: false
exception:
type: string
example: "There is not enough free disk space"
"500":
description: "Error al convertir la imagen virtual."
schema:
type: object
properties:
success:
type: boolean
example: false
error:
type: string
example: "Virtual image conversion failed"
"500 (Error)":
description: "Error al convertir la imagen virtual."
schema:
type: object
properties:
success:
type: boolean
example: false
error:
type: string
example: "(Error description)"
"500 (Exception)":
description: "Excepción inesperada al convertir la imagen virtual."
schema:
type: object
properties:
success:
type: boolean
example: false
exception:
type: string
example: "(Exception description)"
# -----------------------------------------------------------------------------------------------------------

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, check_remote_backup
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, check_virtual_image_conversion, check_free_space
# --------------------------------------------------------------------------------------------
@ -35,7 +35,8 @@ from repo_api import app, get_image_params, search_process, check_remote_connect
# --------------------------------------------------------------------------------------------
repo_path = '/opt/opengnsys/ogrepository/images'
trash_path = '/opt/opengnsys/ogrepository/images_trash/'
trash_path = '/opt/opengnsys/ogrepository/images_trash'
vm_path = '/opt/opengnsys/ogrepository/images_virtual'
# --------------------------------------------------------------------------------------------
@ -56,23 +57,28 @@ class RepoApiTestCase(unittest.TestCase):
def setUp(self):
""" Configura el cliente de prueba de Flask y habilita el modo de prueba (antes de cada test).
Esto permite simular peticiones HTTP a la API sin necesidad de un servidor en ejecución.
Además, crea el archivo "test4unittest.img", para realizar las pruebas.
Además, crea los archivos "test4unittest.img" y "test4unittest.vdi", para realizar las pruebas.
"""
self.app = app.test_client()
self.app.testing = True
# Hay que crear la imagen de prueba, y eliminarla al finalizar las pruebas.
# Creamos las imágenes de prueba ("img" y "vdi"):
with open(f"{repo_path}/test4unittest.img", 'w') as test_image:
test_image.write(' ')
with open(f"{vm_path}/test4unittestvm.vdi", 'w') as vtest_image:
vtest_image.write(' ')
def tearDown(self):
""" Limpia el entorno de pruebas (después de cada test).
En este caso, elimina el archivo "test4unittest.img" (de "repo_path" y de "trash_path").
En este caso, elimina el archivo "test4unittest.img" (de "repo_path" y de "trash_path"),
y el archivo "test4unittestvm.vdi" (de "vm_path").
"""
if os.path.exists(f"{repo_path}/test4unittest.img"):
os.remove(f"{repo_path}/test4unittest.img")
if os.path.exists(f"{trash_path}/test4unittest.img"):
os.remove(f"{trash_path}/test4unittest.img")
if os.path.exists(f"{vm_path}/test4unittestvm.vdi"):
os.remove(f"{vm_path}/test4unittestvm.vdi")
def mock_search_process(process, string_to_search):
@ -545,22 +551,6 @@ class RepoApiTestCase(unittest.TestCase):
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.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 '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_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 host", json.loads(response.data)['exception'])
# ------------------------------------------------------------------- Tests "Crear archivos auxiliares"
@ -601,16 +591,72 @@ class RepoApiTestCase(unittest.TestCase):
self.assertIn('Image not found', json.loads(response.data)['exception'])
# ------------------------------------------------------------------- Tests "Convertir imagen virtual"
@patch('repo_api.subprocess.Popen')
def test_create_torrent_sum_error400_image_locked(self, mock_popen):
""" Método de prueba del endpoint "Crear archivos auxiliares"
(en caso de error "400", por imagen bloqueada).
@patch('repo_api.check_virtual_image_conversion')
def test_convert_virtual_image(self, mock_popen, mock_check_virtual_image_conversion):
""" Método de prueba del endpoint "Convertir imagen virtual".
"""
print("Testing endpoint 'Crear archivos auxiliares' (error 400, image locked)...")
mock_popen.side_effect = Exception("exit status 3")
response = self.app.post('/ogrepository/v1/images/torrentsum', data=json.dumps({"image": "test4unittest.img"}), content_type='application/json')
print("Testing endpoint 'Convertir imagen virtual'...")
mock_popen.return_value = MagicMock(returncode=None)
mock_check_virtual_image_conversion.return_value = AsyncMock(returncode=None)
response = self.app.post('/ogrepository/v1/images/virtual', data=json.dumps({"virtual_image":"test4unittestvm.vdi", "filesystem":"ext4"}), content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertIn('Converting virtual image...', json.loads(response.data)['output'])
@patch('repo_api.check_virtual_image_conversion')
@patch('repo_api.check_file_exists')
@patch('repo_api.subprocess.Popen')
def test_convert_virtual_image_error500(self, mock_popen, mock_check_virtual_image_conversion, mock_check_file_exists):
""" Método de prueba del endpoint "Convertir imagen virtual"
(en caso de error "500").
"""
print("Testing endpoint 'Convertir imagen virtual' (error 500)...")
mock_check_file_exists.return_value = True
mock_popen.side_effect = Exception("Error al convertir la imagen virtual")
mock_check_virtual_image_conversion.return_value = AsyncMock(returncode=None)
response = self.app.post('/ogrepository/v1/images/virtual', data=json.dumps({"virtual_image":"test4unittestvm.vdi", "filesystem":"ext4"}), content_type='application/json')
self.assertEqual(response.status_code, 500)
self.assertIn('Error al convertir la imagen virtual', response.data.decode())
@patch('repo_api.check_file_exists')
def test_convert_virtual_image_error400_no_virtual_image(self, mock_check_file_exists):
""" Método de prueba del endpoint "Convertir imagen virtual"
(en caso de error "400", por imagen virtual inexistente).
"""
print("Testing endpoint 'Convertir imagen virtual' (error 400, no virtual image)...")
mock_check_file_exists.return_value = False
response = self.app.post('/ogrepository/v1/images/virtual', data=json.dumps({"virtual_image":"test4unittestvm.vdi", "filesystem":"ext4"}), content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertIn('Image is locked', json.loads(response.data)['exception'])
self.assertIn('Virtual image not found', json.loads(response.data)['exception'])
@patch('repo_api.check_file_exists')
def test_convert_virtual_image_error400_name_incorrect(self, mock_check_file_exists):
""" Método de prueba del endpoint "Convertir imagen virtual"
(en caso de error "400", por nombre incorrecto).
"""
print("Testing endpoint 'Convertir imagen virtual' (error 400, name incorrect)...")
mock_check_file_exists.return_value = True
response = self.app.post('/ogrepository/v1/images/virtual', data=json.dumps({"virtual_image":"test4unittestvm.vdi", "filesystem":"ext4"}), content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertIn('There is an image with the same name as the virtual image', json.loads(response.data)['exception'])
@patch('repo_api.check_free_space')
def test_convert_virtual_image_error400_not_enough_space(self, mock_check_free_space):
""" Método de prueba del endpoint "Convertir imagen virtual"
(en caso de error "400", por falta de espacio en disco).
"""
print("Testing endpoint 'Convertir imagen virtual' (error 400, not enough space)...")
mock_check_free_space.return_value = False
response = self.app.post('/ogrepository/v1/images/virtual', data=json.dumps({"virtual_image":"test4unittestvm.vdi", "filesystem":"ext4"}), content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertIn('There is not enough free disk space', json.loads(response.data)['exception'])
# ------------------------------------------------------------------- Tests "Enviar paquete Wake On Lan"

View File

@ -0,0 +1,412 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Este script convierte la imagen virtual especificada como primer parámetro (que debe haberse copiado previamente en la ruta "opt/opengnsys/ogrepository/images_virtual")
en una imagen "img" como las que se generan desde OpenGnsys (con "partclone" y "lzop"), por lo que luego puede ser restaurada como cualquier otra imagen del repositorio.
Como segundo parámetro debe especificarse el sistema de archivos de la partición a clonar, en formato "blkid" ("ext4", "ntfs", etc).
NOTA: Se puede comprobar todos los sistemas de archivos aceptados por "blkid" ejecutando el comando "blkid -k".
Una vez realizada la conversión llama al script "createTorrentSum.py", para crear los archivos auxiliares y actualizar la info del repositorio.
Paquetes APT requeridos: "qemu" (se puede instalar con "sudo apt install qemu-utils").
"partclone" (se puede instalar con "sudo apt install partclone").
"lzop" (se puede instalar con "sudo apt install lzop").
Parámetros
------------
sys.argv[1] - Nombre completo de la imagen virtual a convertir (sin ruta).
- Ejemplo1: UbuntuVM.vdi
- Ejemplo2: WindowsVM.vmdk
sys.argv[2] - Sistema de archivos de la partición a convertir (en formato "blkid").
- Ejemplo1: ext4
- Ejemplo2: ntfs
Sintaxis
----------
./convertVMtoIMG.py vm_image_name partition_filesystem
Ejemplos
---------
./convertVMtoIMG.py UbuntuVM.vdi ext4
./convertVMtoIMG.py WindowsVM.vmdk ntfs
"""
# --------------------------------------------------------------------------------------------
# IMPORTS
# --------------------------------------------------------------------------------------------
import os
import sys
import shutil
import subprocess
from systemd import journal
# --------------------------------------------------------------------------------------------
# VARIABLES
# --------------------------------------------------------------------------------------------
script_name = os.path.basename(__file__)
repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final
vm_path = '/opt/opengnsys/ogrepository/images_virtual/' # No borrar la barra final
partclone_logfile = '/opt/opengnsys/ogrepository/log/partclone.log'
create_torrent_script = '/opt/opengnsys/ogrepository/bin/createTorrentSum.py'
# --------------------------------------------------------------------------------------------
# FUNCTIONS
# --------------------------------------------------------------------------------------------
def show_help():
""" Imprime la ayuda, cuando se ejecuta el script con el parámetro "help".
"""
help_text = f"""
Sintaxis: {script_name} vm_image_name partition_filesystem
Ejemplo1: {script_name} UbuntuVM.vdi ext4
Ejemplo2: {script_name} WindowsVM.vmdk ntfs
"""
print(help_text)
def check_params():
""" Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto.
Si no es así, muestra un mensaje de error, y sale del script.
LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help".
"""
# Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script:
if len(sys.argv) == 2 and sys.argv[1] == "help":
show_help()
sys.exit(0)
# Si se ejecuta el script con más o menos de 2 parámetroa, se muestra un error y la ayuda, y se sale del script:
elif len(sys.argv) != 3:
print(f"{script_name} Error: Formato incorrecto: Se debe especificar 2 parámetros")
show_help()
sys.exit(1)
def convert_to_raw(vm_image_name, vm_extension):
""" Convierte la imagen virtual a formato "RAW", mediante "qemu-img".
Si se ejecuta correctamente retorna "True", y si da error retorna "False".
"""
try:
journal.send("convertVMtoIMG.py: Running command 'qemu-img convert'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
result = subprocess.run(['qemu-img', 'convert', '-O', 'raw', f"{vm_path}{vm_image_name}.{vm_extension}", f"{vm_path}{vm_image_name}.raw"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# Evaluamos el resultado de la ejecución, retornando "True" si es correcta y "False" si no lo es:
if result.returncode == 0:
return True
else:
return False
# Si se produce un error o una excepción lo imprimimos en el log, y retornamos "False":
except subprocess.CalledProcessError as error:
journal.send(f"convertVMtoIMG.py: 'qemu-img' error: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
return False
except Exception as error:
journal.send(f"convertVMtoIMG.py: 'qemu-img' exception: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
print(f"Unexpected error: {error}")
return False
def map_vm_partitions(vm_image_name):
""" Mapea las particiones de la imagen RAW en "dev/mapper", para que "partclone" pueda convertir la imagen.
Si se ejecuta correctamente retorna "True", y si da error retorna "False".
NOTA: Debe ejecutarse con "sudo", o dará error.
"""
try:
journal.send("convertVMtoIMG.py: Running command 'kpartx -a'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
result = subprocess.run(['sudo', 'kpartx', '-a', f"{vm_path}{vm_image_name}.raw"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# Evaluamos el resultado de la ejecución, retornando "True" si es correcta y "False" si no lo es:
if result.returncode == 0:
return True
else:
return False
# Si se produce un error o una excepción lo imprimimos en el log, y retornamos "False":
except subprocess.CalledProcessError as error:
journal.send(f"convertVMtoIMG.py: 'kpartx -a' error: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
return False
except Exception as error:
journal.send(f"convertVMtoIMG.py: 'kpartx -a' exception: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
return False
def get_target_device(filesystem):
""" Busca entre los mapeos generados por "kpartx" el dispositivo correspondiente a la partición a restaurar (en base al filesystem especificado como parámetro),
ejecutando el comando "blkid" sobre cada dispositivo mapeado (por lo que el filesystem debe respetar la nomenclatura de "blkid").
Si se ejecuta correctamente retorna el dispositivo de destino, y si da error retorna un mensaje que incluye "Filesystem".
No estoy seguro de que sea necesario, pero por las dudas lo ejecuto con "sudo" (como no crea ningún archivo, no dará problemas de propietario).
"""
try:
journal.send("convertVMtoIMG.py: Getting target device...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Almacenamos en una lista los mapeos generados por "kpartx", y eliminamos el elemento "control" (que no lo ha generado "kpartx"):
map_list = os.listdir('/dev/mapper')
map_list.remove('control')
# Sobre cada mapeo ejecutamos el comando "blkid", buscamos el filesystem en la respuesta, y si lo encontramos extraemos el nombre del dispositivo (para pasárselo a "partclone"):
for device in map_list:
# Ejecutamos el comando "blkid" sobre el mapeo actual:
result = subprocess.run(['sudo', 'blkid', f"/dev/mapper/{device}"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8')
# Si encontramos el filesystem, extraemos el dispositivo, y lo retornamos:
if f'TYPE="{filesystem}"' in result.stdout:
target_device = result.stdout.split('/')[3].split(':')[0]
journal.send(f"convertVMtoIMG.py: Target device obtained: {target_device}", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
return target_device
# Si no encontramos el filesystem en ninguno de los mapeos, guardamos un log y retornamos un mensaje informativo:
journal.send("convertVMtoIMG.py: Filesystem not found", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
return "Filesystem not found"
# Si se produce una excepción lo imprimimos en el log, y retornamos un mensaje informativo:
except Exception as error:
journal.send(f"convertVMtoIMG.py: 'get_target_device' exception: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
return "Error getting Filesystem"
def convert_to_partclone(vm_image_name, target_device):
""" Convierte la imagen "vm_image_name" con "partclone", para que pueda ser restaurada desde ogLive.
Como origen no utiliza la imagen "RAW", sino una partición mapeada en "/dev/mapper" (almacenada en "target_device").
Si se ejecuta correctamente retorna "True", y si da error retorna "False".
"""
try:
journal.send("convertVMtoIMG.py: Running command 'partclone.extfs'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
result = subprocess.run(['partclone.extfs', '-c', '-s', f"/dev/mapper/{target_device}", '-o', f"{vm_path}{vm_image_name}.img", '-L', partclone_logfile], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# Evaluamos el resultado de la ejecución, retornando "True" si es correcta y "False" si no lo es:
if result.returncode == 0:
return True
else:
return False
# Si se produce un error o una excepción lo imprimimos en el log, y retornamos "False":
except subprocess.CalledProcessError as error:
journal.send(f"convertVMtoIMG.py: 'partclone.extfs' error: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
return False
except Exception as error:
journal.send(f"convertVMtoIMG.py: 'partclone.extfs' exception: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
return False
def umap_vm_partitions(vm_image_name):
""" Desmapea las particiones de la imagen RAW, desde "dev/mapper".
No retorna "True" o "False", porque este paso no afecta a la conversión de la imagen.
NOTA: Debe ejecutarse con "sudo", o dará error.
"""
try:
journal.send("convertVMtoIMG.py: Running command 'kpartx -d'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
result = subprocess.run(['sudo', 'kpartx', '-d', f"{vm_path}{vm_image_name}.raw"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# Evaluamos el resultado de la ejecución, imprimiendo en el log el mensaje correspondiente:
if result.returncode == 0:
journal.send("convertVMtoIMG.py: Partitions ummap OK (ReturnCode: 0)", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
else:
journal.send("convertVMtoIMG.py: Partitions umap failed", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Si se produce un error o una excepción lo imprimimos en el log:
except subprocess.CalledProcessError as error:
journal.send(f"convertVMtoIMG.py: 'kpartx -d' error: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
except Exception as error:
journal.send(f"convertVMtoIMG.py: 'kpartx -d' exception: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
def compress_image(vm_image_name):
""" Comprime la imagen generada con "partclone", con "lzop".
Si se ejecuta correctamente retorna "True", y si da error retorna "False".
"""
try:
journal.send("convertVMtoIMG.py: Running command 'lzop'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
result = subprocess.run(['lzop', f"{vm_path}{vm_image_name}.img"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# Evaluamos el resultado de la ejecución, retornando "True" si es correcta y "False" si no lo es:
if result.returncode == 0:
return True
else:
return False
# Si se produce un error o una excepción lo imprimimos en el log, y retornamos "False":
except subprocess.CalledProcessError as error:
journal.send(f"convertVMtoIMG.py: 'lzop' error: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
return False
except Exception as error:
journal.send(f"convertVMtoIMG.py: 'lzop' exception: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
return False
def prepare_image(vm_image_name):
""" Mueve la imagen comprimida al repositorio de imágenes, sustituyendo la extensión ".img.lzo" por ".img".
Calcula el "datasize" aproximado, y crea el archivo "info", para dejar la imagen preparada para añadir al repositorio.
Si se ejecuta correctamente retorna "True", y si da error retorna "False".
"""
try:
journal.send("convertVMtoIMG.py: Preparing image...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Movemos la imagen comprimida al repositorio de imágenes, y sustituimos la extensión ".img.lzo" por ".img":
shutil.move(f"{vm_path}{vm_image_name}.img.lzo", f"{repo_path}{vm_image_name}.img")
# Calculamos aproximadamente lo que puede ocupar la imagen una vez restaurada (multiplicando el tamaño de la imagen por "2.5"):
datasize = int(os.path.getsize(f"{repo_path}{vm_image_name}.img") * 2.5)
# Creamos el archivo "info":
line_to_write = f"PARTCLONE:LZOP:EXTFS:{datasize}:unknown"
with open(f"{repo_path}{vm_image_name}.img.info", 'w') as file:
file.write(line_to_write)
# Como todo ha ido bien hasta aquí, retornamos "True":
return True
# Si se produce una excepción lo imprimimos en el log, y retornamos "False":
except Exception as error:
journal.send(f"convertVMtoIMG.py: Prepare image exception: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
return False
def create_torrentsum(vm_image_name):
""" Crea los archivos auxiliares asociados a la imagen convertida, y actualiza la información del repositorio
(llamando al script "createTorrentSum.py", que a su vez llama al script "updateRepoInfo.py").
Si se ejecuta correctamente retorna "True", y si da error retorna "False".
"""
try:
journal.send("convertVMtoIMG.py: Running script 'createTorrentSum.py'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
result = subprocess.run(['python3', create_torrent_script, f"{repo_path}{vm_image_name}.img"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# Evaluamos el resultado de la ejecución, retornando "True" si es correcta y "False" si no lo es:
if result.returncode == 0:
return True
else:
return False
# Si se produce un error o una excepción lo imprimimos en el log, y retornamos "False":
except subprocess.CalledProcessError as error:
journal.send(f"convertVMtoIMG.py: 'createTorrentSum.py' error: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
return False
except Exception as error:
journal.send(f"convertVMtoIMG.py: 'createTorrentSum.py' exception: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
return False
def erase_image_file(vm_image_name, ext):
""" Borra el archivo "vm_image_name" con extensión "ext",
desde el directorio de imágenes virtuales.
No retorna "True" o "False", porque este paso no afecta a la conversión de la imagen.
"""
journal.send(f"convertVMtoIMG.py: Erasing file with extension {ext}...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Si existe el archivo "vm_image_name.ext", lo borramos:
if os.path.exists(f"{vm_path}{vm_image_name}{ext}"):
os.remove(f"{vm_path}{vm_image_name}{ext}")
# --------------------------------------------------------------------------------------------
# MAIN
# --------------------------------------------------------------------------------------------
def main():
"""
"""
# Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto:
check_params()
# Almacenamos el nombre completo de la imagen y el sistema de archivos (desde los parámetros), y extraemos el nombre y la extensión:
vm_image_name_full = sys.argv[1]
vm_image_name = vm_image_name_full.split('.')[0]
vm_extension = vm_image_name_full.split('.')[1]
filesystem = sys.argv[2].lower()
# Convertimos la imagen virtual a RAW (con "qemu-img"):
raw_conversion = convert_to_raw(vm_image_name, vm_extension)
if raw_conversion == False:
journal.send("convertVMtoIMG.py: Conversion to RAW failed", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
erase_image_file(vm_image_name, '.raw') # Como ha fallado, borramos la imagen "RAW"
sys.exit(2)
else:
journal.send("convertVMtoIMG.py: Conversion to RAW OK (ReturnCode: 0)", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Mapeamos las particiones de la imagen RAW (con "kpartx -a"):
partitions_map = map_vm_partitions(vm_image_name)
if partitions_map == False:
journal.send("convertVMtoIMG.py: Partitions map failed", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
erase_image_file(vm_image_name, '.raw') # Como ha fallado, borramos la imagen "RAW"
sys.exit(3)
else:
journal.send("convertVMtoIMG.py: Partitions map OK (ReturnCode: 0)", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Obtenemos la partición mapeada de destino (con "blkid"):
target_device = get_target_device(filesystem)
if "Filesystem" in target_device:
journal.send("convertVMtoIMG.py: Get target device failed", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
umap_vm_partitions(vm_image_name) # Como ha fallado, desmapeamos las particiones de la imagen "RAW"
erase_image_file(vm_image_name, '.raw') # Como ha fallado, borramos la imagen "RAW"
sys.exit(4)
else:
journal.send("convertVMtoIMG.py: Get target device OK", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Convertimos la imagen con "partclone", desde la partición mapeada:
partclone_conversion = convert_to_partclone(vm_image_name, target_device)
if partclone_conversion == False:
journal.send("convertVMtoIMG.py: Conversion to Partclone failed", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
erase_image_file(vm_image_name, '.raw') # Como ha fallado, borramos la imagen "RAW"
erase_image_file(vm_image_name, '.img') # Como ha fallado, borramos la imagen generada con "partclone" (sin comprimir)
sys.exit(5)
else:
journal.send("convertVMtoIMG.py: Conversion to Partclone OK (ReturnCode: 0)", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Desmapeamos las particiones de la imagen RAW (con "kpartx -d"):
umap_vm_partitions(vm_image_name)
# Borramos la imagen "RAW", generada con "qemu-img":
erase_image_file(vm_image_name, '.raw')
# Comprimimos la imagen con "lzop":
image_compression = compress_image(vm_image_name)
if image_compression == False:
journal.send("convertVMtoIMG.py: Image compression failed", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
erase_image_file(vm_image_name, '.img') # Como ha fallado, borramos la imagen generada con "partclone" (sin comprimir)
sys.exit(6)
else:
journal.send("convertVMtoIMG.py: Image compression OK (ReturnCode: 0)", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Borramos la imagen generada con "partclone" (sin comprimir):
erase_image_file(vm_image_name, '.img')
# Movemos la imagen comprimida al repositorio de imágenes, y creamos el archivo "info":
image_prepared = prepare_image(vm_image_name)
if image_prepared == False:
journal.send("convertVMtoIMG.py: Image preparation failed", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
sys.exit(7)
else:
journal.send("convertVMtoIMG.py: Image preparation OK", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Creamos los archivos auxiliares, y actualizamos la información del repositorio:
image_ready = create_torrentsum(vm_image_name)
if image_ready == False:
journal.send("convertVMtoIMG.py: Auxiliar files creation failed", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
sys.exit(8)
else:
journal.send("convertVMtoIMG.py: Auxiliar files creation OK", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# --------------------------------------------------------------------------------------------
if __name__ == "__main__":
main()
# --------------------------------------------------------------------------------------------