diff --git a/README.md b/README.md index 8a88506..de7da92 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/api/README.md b/api/README.md index 5ab435c..15d9b90 100644 --- a/api/README.md +++ b/api/README.md @@ -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 diff --git a/api/repo_api.py b/api/repo_api.py index 0ff0545..1b56bf6 100644 --- a/api/repo_api.py +++ b/api/repo_api.py @@ -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 + + # -------------------------------------------------------------------------------------------- diff --git a/api/swagger.yaml b/api/swagger.yaml index e5b69d4..91a06a0 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -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)" + # ----------------------------------------------------------------------------------------------------------- diff --git a/api/test_repo_api.py b/api/test_repo_api.py index 36b7a66..64395b7 100644 --- a/api/test_repo_api.py +++ b/api/test_repo_api.py @@ -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" diff --git a/bin/convertVMtoIMG.py b/bin/convertVMtoIMG.py new file mode 100644 index 0000000..89a04da --- /dev/null +++ b/bin/convertVMtoIMG.py @@ -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() + +# --------------------------------------------------------------------------------------------