diff --git a/README.md b/README.md index a206438..21ca53a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,691 @@ -OpenGnsys Repository Manager README -======================================= +ogRepository - OpenGnsys Repository Manager +=========================================== -Este repositorio GIT contiene la estructura de datos del repositorio de datos de OpenGnsys. +Este repositorio GIT contiene la estructura de datos del repositorio de imágenes de OpenGnsys. -- bin: binarios y scripts de gestión del repositorio. -- etc: ficheros o plantillas de configuración del repositorio. \ No newline at end of file +- **admin** --- Archivos de configuración de ogRepository. +- **api** ------ API de ogRepository. +- **bin** ------ Scripts en Python 3 y binarios de gestión de ogRepository. +- **etc** ------ Ficheros y plantillas de configuración de ogRepository. +- **packets** - Paquetes cuya instalación es requerida. + +--- + +## Requerimientos: + +Paquetes APT requeridos: + - **uftp** (se puede instalar con "sudo DEBIAN_FRONTEND=noninteractive apt install uftp -y", para que no pida la ruta predeterminada) + - **udpcast** (se puede instalar con "sudo apt install ./udpcast_20230924_amd64.deb", apuntando al paquete) + - **ctorrent** (se puede instalar con "sudo apt install ctorrent") + - **bittorrent** (se puede instalar con "sudo apt install bittorrent", pero previamente hay que añadir un repositorio de Debian) + - **bittornado** (se puede instalar con "sudo apt install bittornado", pero previamente hay que añadir un repositorio de Debian) + +Librerías Python requeridas: + - **flask** (se puede instalar con "sudo apt install python3-flask") + - **paramiko** (se puede instalar con "sudo apt install python3-paramiko") + - **psutil** (se puede instalar con "sudo apt install python3-psutil") + - **flasgger** (se puede instalar con "sudo apt install python3-flasgger") + +Para que todos los endpoints y scripts funcionen con la configuración actual deben existir los siguientes directorios: + - **/opt/opengnsys/ogrepository/images/** + - **/opt/opengnsys/ogrepository/images_trash/** (debe estar en la misma partición que el anterior, o tardarán mucho las eliminaciones y restauraciones) + - **/opt/opengnsys/ogrepository/bin/** (aquí deben estar todos los scripts de Python, y el binario "udp-sender") + - **/opt/opengnsys/ogrepository/etc/** (aquí se guardan los archivos "repoinfo.json" y "trashinfo.json") + - **/opt/opengnsys/ogrepository/log/** (aquí se guardan los logs) + +Y también debe existir el siguiente archivo: + - **/opt/opengnsys/ogrepository/etc/ogAdmRepo.cfg** (de aquí pilla la IP de ogRepository) + +--- + +## API de ogRepository + +La API de ogRepository proporciona una interfaz para facilitar la administración de las imágenes almacenadas en los repositorios de imágenes, permitiendo eliminarlas, enviarlas a clientes ogLive (con diferentes protocolos de transmisión), importarlas desde otros repositorios, etc. + +El presente documento detalla los endpoints de la API, con sus respectivos parámetros de entrada, así como las acciones que llevan a cabo. + +--- +### Tabla de Contenido: + +1. [Obtener Información de Estado de ogRepository](#obtener-información-de-estado-de-ogrepository) - `GET /ogrepository/v1/status` +2. [Obtener Información de todas las Imágenes](#obtener-información-de-todas-las-imágenes) - `GET /ogrepository/v1/images` +3. [Obtener Información de una Imagen concreta](#obtener-información-de-una-imagen-concreta) - `GET /ogrepository/v1/images/{ID_img}` +4. [Actualizar Información del Repositorio](#actualizar-información-del-repositorio) - `PUT /ogrepository/v1/images` +5. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/status/images/{ID_img}` +6. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/{ID_img}?method={method}` +7. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/trash/images` +8. [Eliminar una Imagen de la Papelera](#eliminar-una-imagen-de-la-papelera) - `DELETE /ogrepository/v1/trash/images/{ID_img}` +9. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/repo/images` +10. [Exportar una Imagen](#exportar-una-imagen) - `PUT /ogrepository/v1/repo/images` +11. [Crear archivos auxiliares](#crear-archivos-auxiliares) - `POST /ogrepository/v1/images/torrentsum` +12. [Enviar paquete Wake On Lan](#enviar-paquete-wake-on-lan) - `POST /ogrepository/v1/wol` +13. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/udpcast` +14. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/uftp` +15. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/p2p` +16. [Ver Estado de Transmisiones UDPcast](#ver-estado-de-transmisiones-udpcast) - `GET /ogrepository/v1/udpcast` +17. [Ver Estado de Transmisiones UFTP](#ver-estado-de-transmisiones-uftp) - `GET /ogrepository/v1/uftp` +18. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}` +19. [Cancelar Transmisión UFTP](#cancelar-transmisión-uftp) - `DELETE /ogrepository/v1/uftp/images/{ID_img}` +20. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p` + +--- +### Obtener Información de Estado de ogRepository + +Se devolverá informacion de CPU, memoria RAM, disco duro y el estado de ciertos servicios y procesos de ogRepository, en formato JSON. +Se puede utilizar el script "**getRepoStatus.py**, que debe ser llamado por el endpoint. +**NOTA**: En los apartados "services" y "processes" he especificado los servicios y procesos que me han parecido interesantes, pero se puede añadir o eliminar los que se desee. + +**URL:** `/ogrepository/v1/status` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/status +``` + +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al consultar y/o devolver la información de estado. +- **Código 200 OK:** La información de estado se obtuvo exitosamente. + - **Contenido:** Información de estado en formato JSON. + ```json + { + "cpu": { + "used_percentage": "35%" + }, + "ram": { + "total": "7.8GB", + "used": "0.3GB", + "available": "7.2GB", + "used_percentage": "7%" + }, + "disk": { + "total": "11.7GB", + "used": "7.7GB", + "available": "3.4GB", + "used_percentage": "69%" + }, + "services": { + "ssh": "active", + "smbd": "active", + "rsync": "active" + }, + "processes": { + "udp-sender": "stopped", + "uftp": "stopped", + "bttrack": "stopped", + "btlaunchmany": "stopped" + } + } + ``` +--- +### Obtener Información de todas las Imágenes + +Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/ogrepository/etc/repoinfo.json**" (que corresponde a todas las imágenes almacenadas en el repositorio), y en el archivo "**/opt/opengnsys/ogrepository/etc/trashinfo.json**" (que corresponde a las imágenes que fueron eliminadas, que estarán en la papelera). +Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint, que a su vez llama al script "**updateRepoInfo.py**", para actualizar previamente la información del repositorio. +**NOTA**: El script requiere que se le pase "all" como primer parámetro (que correspondería al nombre de la imagen) y "none" como segundo parámetro (que corresponderia al nombre del subdirectorio correspondiente a la OU). Esta transformación de parámetros se realiza en la API. + +**URL:** `/ogrepository/v1/images` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images +``` + +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al consultar y/o devolver la información de las imágenes. +- **Código 200 OK:** La información de las imágenes se obtuvo exitosamente. + - **Contenido:** Información de imágenes en formato JSON. + ```json + { + "REPOSITORY": { + "directory": "/opt/opengnsys/ogrepository/images", + "images": [ + { + "name": "Ubuntu24", + "type": "img", + "clientname": "Ubuntu_24", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "EXTFS", + "datasize": 9859634200000, + "size": 4505673214, + "sum": "065a933c780ab1aaa044435ad5d4bf87", + "fullsum": "33575b9070e4a8043371b8c6ae52b80e" + }, + { + "name": "Windows10", + "type": "img", + "clientname": "Windows_10", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "NTFS", + "datasize": 24222105600000, + "size": 13198910185, + "sum": "8874d5ab84314f44841c36c69bb5aa82", + "fullsum": "9e7cd32c606ebe5bd39ba212ce7aeb02" + } + ], + "ous": [ + { + "subdir": "OU_subdir", + "images": [ + { + "name": "Ubuntu20", + "type": "img", + "clientname": "Ubuntu_20", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "EXTFS", + "datasize": 8912896000000, + "size": 3803794535, + "sum": "081a933c780ab1aaa044435ad5d4bf56", + "fullsum": "22735b9070e4a8043371b8c6ae52b90d" + } + ] + } + ] + } + }, + "TRASH": { + "directory": "/opt/opengnsys/ogrepository/images_trash", + "images": [], + "ous": [ + { + "subdir": "CentroVirtual", + "images": [ + { + "name": "Ubuntu20OLD", + "type": "img", + "clientname": "Ubuntu_20", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "EXTFS", + "datasize": 8912896000000, + "size": 3803794535, + "sum": "081a933c780ab1aaa044435ad5d4bf56", + "fullsum": "22735b9070e4a8043371b8c6ae52b90d" + } + ] + } + ] + } + ``` + - **name**: Nombre de la imagen, sin extensión. + - **type**: Extensión de la imagen. + - **clientname**: Nombre asignado al modelo del que se ha obtenido la imagen. + - **clonator**: Programa utilizado para la clonación. + - **compressor**: Programa utilizado para la compresión. + - **filesystem**: Sistema de archivos utilizado en la partición clonada. + - **datasize**: Tamaño de la imagen una vez restaurada, en bytes (tamaño de los datos). + - **size**: Tamaño del archivo de imagen, en bytes. + - **sum**: Hash MD5 del último MB del archivo de imagen. + - **fullsum**: Hash MD5 de todo el archivo de imagen. + +--- +### Obtener Información de una Imagen concreta + +Se devolverá la informacion de la imagen especificada, que puede estar en el archivo "**/opt/opengnsys/ogrepository/etc/repoinfo.json**" o en el archivo "**/opt/opengnsys/ogrepository/etc/trashinfo.json**" (en este último caso, si la imagen está en la papelera). +Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint, que a su vez llama al script "**updateRepoInfo.py**", para actualizar previamente la información del repositorio. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión) como primer parámetro, y el subdirectorio correspondiente a la OU (o "none" si no es el caso) como segundo parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. + +**URL:** `/ogrepository/v1/images/{ID_img}` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/22735b9070e4a8043371b8c6ae52b90d +``` + +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al consultar y/o devolver la información de la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La información de la imagen se obtuvo exitosamente. + - **Contenido:** Información de la imagen en formato JSON. + ```json + { + "directory": "/opt/opengnsys/ogrepository/images", + "images": [ + { + "name": "Windows10", + "type": "img", + "clientname": "Windows_10", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "NTFS", + "datasize": 9859634200000, + "size": 4505673214, + "sum": "065a933c780ab1aaa044435ad5d4bf87", + "fullsum": "33575b9070e4a8043371b8c6ae52b80e" + } + ] + } + ``` + - **name**: Nombre de la imagen, sin extensión. + - **type**: Extensión de la imagen. + - **clientname**: Nombre asignado al modelo del que se ha obtenido la imagen. + - **clonator**: Programa utilizado para la clonación. + - **compressor**: Programa utilizado para la compresión. + - **filesystem**: Sistema de archivos utilizado en la partición clonada. + - **datasize**: Tamaño de la imagen una vez restaurada, en bytes (tamaño de los datos). + - **size**: Tamaño del archivo de imagen, en bytes. + - **sum**: Hash MD5 del último MB del archivo de imagen. + - **fullsum**: Hash MD5 de todo el archivo de imagen. + +--- +### Actualizar Información del Repositorio + +Se actualizará la información de las imágenes almacenadas en el repositorio, reflejándola en el archivo "**/opt/opengnsys/ogrepository/etc/repoinfo.json**". +Se puede hacer con el script "**updateRepoInfo.py**", que debe ser llamado por el endpoint (y que es similar al script bash original "**checkrepo**"). +Este endpoint es llamado por el script "**deleteImage.py**" (para actualizar la información cada vez que se elimine una imagen), y creemos que también debe ser llamado por ogCore u ogLive cada vez que se haya creado una imagen. + +**URL:** `/ogrepository/v1/images` +**Método HTTP:** PUT + +**Ejemplo de Solicitud:** + +```bash +curl -X PUT -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al actualizar la información de las imágenes. +- **Código 200 OK:** La actualización se realizó exitosamente. + +--- +### Chequear Integridad de Imagen + +Se comprobará la integridad del fichero de imagen especificado como parámetro. +Se puede hacer con el script "**checkImage.py**", que compara el tamaño actual del archivo con el almacenado en el archivo "**.size**", y el hash MD5 del último MB del archivo con el almacenado en el archivo "**.sum**". +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. + +**URL:** `/ogrepository/v1/status/images/{ID_img}` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/status/images/22735b9070e4a8043371b8c6ae52b90d +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al chequear la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se ha chequeado exitosamente. +- **Código 200 KO:** La imagen se ha chequeado correctamente, pero no ha pasado el test. + +--- +### Eliminar una Imagen + +Se eliminará la imagen especificada como parámetro, pudiendo eliminarla permanentemente o enviarla a la papelera. +Se puede hacer con el script "**deleteimage.py**", que debe ser llamado por el endpoint (y que incluye la funcionalidad "papelera"), y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y el parámetro opcional "-p" (para que la eliminación sea permanente). Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros, pero también hay que especificar el método de eliminación en la URL, como parámetro adicional. + +**URL:** `/ogrepository/v1/images/{ID_img}?method={method}` +**Método HTTP:** DELETE + +**Parámetro adicional (en URL):** +- **method**: Método de eliminación (puede ser "trash" o "permanent"). + +**Ejemplo de Solicitud:** + +```bash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/22735b9070e4a8043371b8c6ae52b90d?method=trash +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al eliminar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se eliminó exitosamente. + +--- +### Recuperar una Imagen + +Se recuperará la imagen especificada como parámetro, desde la papelera. +Se puede hacer con el script "**recoverImage.py**", que debe ser llamado por el endpoint, y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. + +**URL:** `/ogrepository/v1/trash/images` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d"}' http://example.com/ogrepository/v1/trash/images +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al recuperar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se recuperó exitosamente. + +--- +### Eliminar una Imagen de la Papelera + +Se eliminará permanentemente la imagen especificada como parámetro, desde la papelera. +Se puede hacer con el script "**deleteTrashImage.py**", que debe ser llamado por el endpoint, y que a su vez llama al script "**updateTrashInfo.py**", para actualizar la información de la papelera. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. + +**URL:** `/ogrepository/v1/trash/images/{ID_img}` +**Método HTTP:** DELETE + +**Ejemplo de Solicitud:** + +```bash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/trash/images/22735b9070e4a8043371b8c6ae52b90d +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al eliminar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se eliminó exitosamente. + +--- +### Importar una Imagen + +Se importará una imagen de un repositorio remoto al repositorio local. +Se puede hacer con el script "**importImage.py**", que debe ser llamado por el endpoint. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. Estos parámetros deben enviarse desde ogCore (en el JSON), porque el repositorio local no puede extraer la información de la imagen de un ID almacenado en un repositorio remoto. +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está importando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + +**URL:** `/ogrepository/v1/repo/images` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). +- **repo_ip**: Dirección IP del repositorio remoto (desde el que se importará la imagen). +- **user**: Usuario con el que acceder al repositorio remoto. + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none", "repo_ip":"192.168.56.100", "user":"opengnsys"}' http://example.com/ogrepository/v1/repo/images +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al importar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen y/o el equipo remoto especificados. +- **Código 200 OK:** La imagen se está importando. + +--- +### Exportar una Imagen + +Se exportará una imagen del repositorio local a un repositorio remoto. +Se puede hacer con el script "**exportImage.py**", que debe ser llamado por el endpoint. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero la IP del repositorio remoto y el usuario remoto deben enviarse desde ogCore (en el JSON). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está exportando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + +**URL:** `/ogrepository/v1/repo/images` +**Método HTTP:** PUT + +**Cuerpo de la Solicitud (JSON):** +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). +- **repo_ip**: Dirección IP del repositorio remoto (al que se exportrará la imagen). +- **user**: Usuario con el que acceder al repositorio remoto. + +**Ejemplo de Solicitud:** + +```bash +curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d", "repo_ip":"192.168.56.100", "user":"opengnsys"}' http://example.com/ogrepository/v1/repo/images +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al exportar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen y/o el equipo remoto especificados. +- **Código 200 OK:** La imagen se está exportando. + +--- +### Crear archivos auxiliares + +Se crearán los archivos ".sum", ".full.sum", ".size" y ".torrent", para la imagen especificada como parámetro. +Se puede hacer con el script "**createTorrentSum.py**", que debe ser llamado por el endpoint. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como unico parámetro. Este parámetro no puede obtenerse en la API, a partir del ID de imagen (como en otros casos), porque el ID corresponde al contenido del archivo "full.sum" asociado (que no estará creado hasta que no se ejecute este script). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar cierto tiempo, por lo que solo informa de que los archivos auxiliares se están creando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + +**URL:** `/ogrepository/v1/images/torrentsum` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/torrentsum +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al crear los archivos auxiliares. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** Los archivos auxiliares se están creando. + +--- +### Enviar paquete Wake On Lan + +Se enviará un paquete Wake On Lan a la dirección MAC especificada, a través de la IP de broadcast especificada. +Se puede hacer con el script "**sendWakeOnLan.py**", que debe ser llamado por el endpoint. +**NOTA**: La versión actual de este script requiere que se le pase la dirección IP de broadcast como primer parámetro, y la dirección MAC destino como segundo parámetro. Estos datos deben enviarse desde ogCore (en el JSON). + +**URL:** `/ogrepository/v1/wol` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **broadcast_ip**: IP de broadcast a la que enviar el paquete (puede ser "255.255.255.255", o la IP de broadcast de una subred). +- **mac**: Dirección MAC del equipo que se desea encender via Wake On Lan. + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"broadcast_ip":"255.255.255.255", "mac":"00:19:99:5c:bb:bb"}' http://example.com/ogrepository/v1/wol +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al enviar el paquete Wake On Lan. +- **Código 200 OK:** El paquete Wake On Lan se ha enviado exitosamente. + +--- +### Enviar una Imagen mediante UDPcast + +Se enviará la imagen especificada por Multicast, mediante la aplicación UDPcast. +Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binario "**udp-sender**", que es quien realmente realiza el envío. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo (pero no avisa a ogCore de su finalización, porque no puede comprobar cuando acaba la tarea de restauración de la imagen). + +**URL:** `/ogrepository/v1/udpcast` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). +- **port**: Puerto Multicast. +- **method**: Modalidad half-duplex o full-duplex ("half" o "full"). +- **ip**: IP Multicast. +- **bitrate**: Velocidad de transmisión (en Mbps). +- **nclients**: Número mínimo de clientes. +- **maxtime**: Tiempo máximo de espera. + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d", "port":"9000", "method":"full", "ip":"239.194.17.2", "bitrate":"70M", "nclients":"20", "maxtime":"120"}' http://example.com/ogrepository/v1/udpcast +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se está enviando mediante UDPcast. + +--- +### Enviar una Imagen mediante UFTP + +Se enviará la imagen especificada por Unicast o Multicast, mediante el protocolo "UFTP". +Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente los clientes ogLive destino se pongan en escucha con un daemon "UFTPD" (ejecutando el script "**listenUFTPD.py**"). Esto funciona al revés que "UDPcast", ya que primero se debe ejecutar un comando en los clientes, y luego en el servidor. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo (pero no avisa a ogCore de su finalización, porque no puede comprobar cuando acaba la tarea de restauración de la imagen). + +**URL:** `/ogrepository/v1/uftp` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). +- **port**: Puerto Multicast. +- **ip**: IP Unicast/Multicast. +- **bitrate**: Velocidad de transmisión (con "K" para Kbps, "M" para Mbps o "G" para Gbps). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d", "port":"9000", "ip":"239.194.17.2", "bitrate":"1G"}' http://example.com/ogrepository/v1/uftp +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se está enviando mediante UFTP. + +--- +### Enviar una Imagen mediante P2P + +Se enviará la imagen especificada mediante "P2P", iniciando el tracker y el seeder (que harán tracking y seed de los torrents contenidos en la raiz del directorio especificado). +Se puede hacer con los scripts "**runTorrentTracker.py**" y "**runTorrentSeeder.py**", que deben ser llamados por el endpoint. +**NOTA**: Estos scripts requieren que se les pase el directorio en el que está situada la imagen a enviar como único parámetro. Este dato se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo (pero no avisa a ogCore de su finalización, porque no puede comprobar cuando acaba la tarea de restauración de la imagen). + + +**URL:** `/ogrepository/v1/p2p` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d"}' http://example.com/ogrepository/v1/p2p +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al intentar enviar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se está enviando mediante P2P. + +--- +### Ver Estado de Transmisiones UDPcast + +Se devolverá el pid de los procesos de transferencias UDPcast activas, y sus imágenes asociadas (con nombre e ID), en formato JSON, o un mensaje informativo si no hay procesos activos, o si se produce un error. +Se puede hacer con el script "**getUDPcastInfo.py**", que debe ser llamado por el endpoint. + +**URL:** `/ogrepository/v1/udpcast` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/udpcast +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al comprobar las transmisiones UDPcast. +- **Código 400 Bad Request:** No se han encontrado transmisiones UDPcast activas. +- **Código 200 OK:** La información de las transmisiones UDPcast activas se obtuvo exitosamente. + - **Contenido:** Información de las transmisiones UDPcast activas en formato JSON. + ```json + { + "6720": { + "image_id": "22735b9070e4a8043371b8c6ae52b90d", + "image_name": "Ubuntu20.img" + }, + "6721": { + "image_id": "9e7cd32c606ebe5bd39ba212ce7aeb02", + "image_name": "Windows10.img" + } + } + ``` +--- +### Ver Estado de Transmisiones UFTP + +Se devolverá el pid de los procesos de transferencias UFTP activas, y sus imágenes asociadas (con nombre e ID), en formato JSON, o un mensaje informativo si no hay procesos activos, o si se produce un error. +Se puede hacer con el script "**getUFTPInfo.py**", que debe ser llamado por el endpoint. + +**URL:** `/ogrepository/v1/uftp` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/uftp +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al comprobar las transmisiones UFTP. +- **Código 400 Bad Request:** No se han encontrado transmisiones UFTP activas. +- **Código 200 OK:** La información de las transmisiones UFTP activas se obtuvo exitosamente. + - **Contenido:** Información de las transmisiones UFTP activas en formato JSON. + ```json + { + "3427": { + "image_id": "22735b9070e4a8043371b8c6ae52b90d", + "image_name": "Ubuntu20.img" + }, + "4966": { + "image_id": "9e7cd32c606ebe5bd39ba212ce7aeb02", + "image_name": "Windows10.img" + } + } + ``` +--- +### Cancelar Transmisión UDPcast + +Se cancelará la transmisión por UDPcast activa de la imagen especificada como parámetro, deteniendo el proceso "udp-sender" asociado a dicha imagen. +Se puede hacer con el script "**stopUDPcast.py**", que debe ser llamado por el endpoint. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Este dato se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"). + +**URL:** `/ogrepository/v1/udpcast/images/{ID_img}` +**Método HTTP:** DELETE + +**Ejemplo de Solicitud:** + +```bash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/udpcast/images/22735b9070e4a8043371b8c6ae52b90d +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al cancelar la transmisión UDPcast. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 400 Bad Request:** No hay transmisiones UDPcast activas para la imagen especificada. +- **Código 200 OK:** La transmisión UDPcast se ha cancelado exitosamente. + +--- +### Cancelar Transmisión UFTP + +Se cancelará la transmisión por UFTP activa de la imagen especificada como parámetro, deteniendo el proceso "uftp" asociado a dicha imagen. +Se puede hacer con el script "**stopUFTP.py**", que debe ser llamado por el endpoint. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Este dato se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"). + +**URL:** `/ogrepository/v1/uftp/images/{ID_img}` +**Método HTTP:** DELETE + +**Ejemplo de Solicitud:** + +```bash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/uftp/images/22735b9070e4a8043371b8c6ae52b90d +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al cancelar la transmisión UFTP. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 400 Bad Request:** No hay transmisiones UFTP activas para la imagen especificada. +- **Código 200 OK:** La transmisión UFTP se ha cancelado exitosamente. + +--- +### Cancelar Transmisiones P2P + +Se cancelarán las transmisiones P2P activas en el ogRepository al que se envíe la orden, deteniendo los procesos "bttrack" y "btlaunchmany.bittornado". +Se puede hacer con el script "**stopP2P.py**", que debe ser llamado por el endpoint. +**NOTA**: No he encontrado la forma de detener la transmisión de una imagen concreta, ya que "bttrack" y "btlaunchmany.bittornado" hacen tracking y seed (respectivamente) de todos los torrents existentes en la raíz del directorio especificado. + +**URL:** `/ogrepository/v1/p2p` +**Método HTTP:** DELETE + +**Ejemplo de Solicitud:** + +```bash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/p2p +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al cancelar las transmisiones P2P. +- **Código 200 OK:** Las transmisiones P2P se han cancelado exitosamente. + +--- \ No newline at end of file diff --git a/admin/Sources/Services/ogAdmRepoAux b/admin/Sources/Services/ogAdmRepoAux new file mode 100644 index 0000000..ede2bea --- /dev/null +++ b/admin/Sources/Services/ogAdmRepoAux @@ -0,0 +1,285 @@ +#!/bin/bash +PARM=`cat` +#PARM=$@ + + +#TODO: ticket 379 +#buscar parametro de identificador de operacion. +#usar parametro de identificacion para anexarlo al nombre de log +#Comprobar si la variable está seteas. +#Si no lo está setearla. +#Si esta seteada (en progreso) salir. + + +TIME=$SECONDS + +BASEDIR=/opt/opengnsys +PATH=$PATH:$BASEDIR/bin +REPONAME=ogAdmRepo +REPODIR="$BASEDIR/images/" + +# Para las sincronizadas +# BACKUP: Define si se realiza copia de seguridad al crear una imagen (true|false). +# IMGFS: Sistema de ficheros al crear las sincronizadas tipo archivo (EXT4|BTRFS). +[ -z $OGENGINECONFIGURATE ] && source $BASEDIR/client/etc/engine.cfg +# FS segun la configuracion y la version del kernel. ext4 para < 3.7, para >= BTRFS +KERNEL=$(file -bkr /opt/opengnsys/tftpboot/ogclient/ogvmlinuz |awk '/Linux/ {for(i=1;i<=NF;i++) if($i~/version/) {v=$(i+1);printf("%d",v);sub(/[0-9]*\./,"",v);printf(".%02d",v)}}') +[ $KERNEL \< 3.07 ] && IMGFS="EXT4" || IMGFS=${IMGFS:-"BTRFS"} + +# Añade registro de incidencias. +function echolog () { + logger --tag $0 --priority local0.info "$*" + echo "$*" +} + +function mountImage () { + #@param 1 image_file + #@param 2 mount_dir + #@param 3 openciones mount + [ "$3" != "" ] && OPTMOUNT=" -o $3 " + # Si está montado nada que hacer + df |grep "$2$" 2>&1 >/dev/null && return 0 + # FS de la imagen segun el contenido del archivo .img + if file "$1" |grep -i -e " ext4 filesystem " 2>&1 > /dev/null ; then + echolog "mount $OPTMOUNT -t ext4 $1 $2" + mount $OPTMOUNT -t ext4 $1 $2 + else + echolog "mount $OPTMOUNT -o compress=lzo $1 $2" + mount $OPTMOUNT -o compress=lzo "$1" "$2" + fi + # Si esta montado da error 32, lo damos como bueno + RETVAL=$? + [ $RETVAL -eq 32 ] && RETVAL=0 + return $RETVAL +} + + +PARM1=$(echo $PARM | cut -f1 -d" ") +PARM2=$(echo $PARM | cut -f2 -d" ") +PARM3=$(echo $PARM | cut -f3 -d" ") +PARM4=$(echo $PARM | cut -f4 -d" ") + +# Determinamos el tipo de sistema de fichero de las imagenes segun el kernel que tenga + + +case "$PARM1" in + START_MULTICAST) + #1 START_MULTICAST + #2 fichero a enviar + #3 opciones de multicast + FILE="$PARM2" + MCASTOPT="$PARM3" + echolog "Ejecutar $(which sendFileMcast) $FILE $MCASTOPT" + sendFileMcast $FILE $MCASTOPT |logger --tag $0 --priority local0.info + case $? in + 1) echolog "Parametros insuficientes" + exit 1 ;; + 2) echolog "Fichero no accesible" + exit 2 ;; + 3) echolog "Sesion multicast no valida" + exit 3 ;; + esac + ;; + CREATE_IMAGE) + # Creamos/Redimensionamos el fichero de imagen y lo montamos para que se pueda escribir sobre el + #1 CREATE_IMAGE + #2 nombre imagen + #3 tipo de imagen [ img | diff ] + #4 tamaño imagen + LOOPDEVICE=$(losetup -f) + DIRMOUNT="$REPODIR/mount/$PARM2" + if [ "$PARM3" == "img" ] ; then + IMGEXT="img" + else + IMGEXT="img.diff" + DIRMOUNT="$DIRMOUNT.diff" + fi + IMGFILE="$REPODIR/$PARM2.$IMGEXT" + IMGDIR="$(dirname $IMGFILE)" + [ -d $IMGDIR ] || mkdir -p $IMGDIR + mkdir -p "$DIRMOUNT" + + LOCKFILE="$IMGFILE.lock" + + SIZEREQUIRED="$PARM4" + + # Si existe la imagen hacemos copia de seguridad y la redimesionamos + if [ -f "$IMGFILE" ]; then + echolog "La imagen $IMGFILE ya existe." + # TODO modificar ogGetImageSize + IMGSIZE=$(ls -l --block-size=1024 $IMGFILE| cut -f5 -d" ") + + if [ "$BACKUP" == "true" -o "$BACKUP" == "TRUE" -o $IMGSIZE -lt $SIZEREQUIRED ]; then + # Si la imagen esta montada la desmonto + if [ -r "$DIRMOUNT/ogimg.info" ]; then + echolog "umount $DIRMOUNT" + umount "$DIRMOUNT" + [ $? -ne 0 ] && echolog "Error: No podemos desmontar la imagen para hacer copia de seguridad o redimensionar" && exit 1 + fi + fi + + # Copia de seguridad de la imagen + if [ "$BACKUP" == "true" -o "$BACKUP" == "TRUE" ]; then + echolog "Copia de seguridad de la imagen anterior" + echolog "cp $IMGFILE $IMGFILE.ant" + cp "$IMGFILE" "$IMGFILE.ant" + echolog mv -f "$IMGFILE.torrent" "$IMGFILE.torrent.ant" 2>/dev/null + mv -f "$IMGFILE.torrent" "$IMGFILE.torrent.ant" 2>/dev/null + fi + + # Redimensionamos la imagen al tamaño necesario + if [ $IMGSIZE -lt $SIZEREQUIRED ];then + echolog "Redimensionamos la imagen $IMGFILE al tamaño necesario: $SIZEREQUIRED" + echolog "truncate --size=\">$SIZEREQUIRED\"k $IMGFILE" + truncate --size=">$SIZEREQUIRED"k $IMGFILE 2>&1 |logger --tag $0 --priority local0.info + # FS de la imagen segun el contenido del archivo .img + if file "$IMGFILE" |grep -i -e " ext4 filesystem " 2>&1 > /dev/null ; then + losetup $LOOPDEVICE "$IMGFILE" + echolog "resize2fs -f $LOOPDEVICE" + resize2fs -f $LOOPDEVICE |logger --tag $0 --priority local0.info + else + mount -o compress=lzo "$IMGFILE" "$DIRMOUNT" + echolog "btrfs filesystem resize max $DIRMOUNT" + btrfs filesystem resize max "$DIRMOUNT" 2>&1 |logger --tag $0 --priority local0.info + fi + fi + + + # Si no existe la imagen creamos el fichero. + else + echolog "Creamos la imagen $IMGFILE al tamaño necesario: $SIZEREQUIRED" + touch "$IMGFILE" + echolog "truncate --size=\">$SIZEREQUIRED\"k $IMGFILE" + truncate --size=">$SIZEREQUIRED"k $IMGFILE 2>&1 |logger --tag $0 --priority local0.info + #Formateamos imagen + echo losetup $LOOPDEVICE "$IMGFILE" + losetup $LOOPDEVICE "$IMGFILE" + if [ $IMGFS == "EXT4" ] ; then + echolog " mkfs.ext4 -i 4096 -b 4096 -L ${PARM2##*\/} $LOOPDEVICE" + mkfs.ext4 -i 4096 -b 4096 -L ${PARM2##*\/} $LOOPDEVICE + else + echolog mkfs.btrfs -L ${PARM2##*\/} $LOOPDEVICE + mkfs.btrfs -L ${PARM2##*\/} $LOOPDEVICE #&> $OGLOGCOMMAND + fi + fi + # Montamos la imagen. + mountImage "$IMGFILE" "$DIRMOUNT" + if [ $? -ne 0 ]; then + rmdir "$DIRMOUNT" + echolog "Error al crear/redimensionar la imagen" + exit 1 + fi + + #touch "$DIRMOUNT/ogimg.info" + echo "mounted"> "$LOCKFILE" + TIME2=$[SECONDS-TIME] + echolog "Fin creación/redimension de la imagen: $[TIME2/60]m $[TIME2%60]s" + # Si existe dispositivo loop lo borramos. + [ $LOOPDEVICE ] && losetup -a| grep $LOOPDEVICE &> /dev/null && losetup -d $LOOPDEVICE + # TODO: comprobar que no se quede el losetup bloqueado. + + ;; + MOUNT_IMAGE) + # Montamos el fichero imagen para que se pueda + #1 MOUNT_IMAGE + #2 nombre imagen + #3 tipo de imagen [ img | diff ] + DIRMOUNT="$REPODIR""mount/$PARM2" + if [ "$PARM3" == "img" ] ; then + IMGEXT="img" + else + IMGEXT="img.diff" + DIRMOUNT="$DIRMOUNT.diff" + fi + IMGFILE="$REPODIR/$PARM2.$IMGEXT" + echolog "Montamos la imagen $IMGFILE " + mkdir -p "$DIRMOUNT" + mountImage "$IMGFILE" "$DIRMOUNT" ro || (echolog "Error al montar la imagen"; exit 1) + ;; + UMOUNT_IMAGE) + # Desmontamos el fichero imagen. + # Si el directorio esta ocupado no se desmontará + #1 UMOUNT_IMAGE + #2 nombre imagen + #3 tipo de imagen [ img | diff ] + IMGTYPE="$PARM3" + DIRMOUNT="$REPODIR/mount/$PARM2" + if [ "$IMGTYPE" == "img" ]; then + IMGEXT="img" + else + DIRMOUNT="$DIRMOUNT.$IMGTYPE" + IMGEXT="img.diff" + fi + LOCKFILE="$REPODIR/$PARM2.$IMGEXT.lock" + echolog "Desmontamos la imagen $PARM2 $PARM3 " + umount $DIRMOUNT + rmdir $DIRMOUNT + [ -f $LOCKFILE ] && sed -i s/mounted//g $LOCKFILE + + ;; + REDUCE_IMAGE) + # Reduce el archivo de la imagen a tamaño datos + 500M + #1 REDUCE_IMAGE + #2 Nombre Imagen + #3 Tipo de imagen [ img |diff ] + DIRMOUNT="${REPODIR}mount/${PARM2}" + if [ "$PARM3" == "img" ] ; then + IMGEXT="img" + else + IMGEXT="img.diff" + DIRMOUNT="$DIRMOUNT.diff" + fi + IMGFILE="$REPODIR$PARM2.$IMGEXT" + LOCKFILE="$IMGFILE.lock" + [ ! -f $IMGFILE ] && echolog "Imagen $IMGFILE no existe" && exit 1 + + # Para imagenes EXT4 reduzco, para BTRFS solo desmonto. + if file $IMGFILE |grep -i -e " ext4 filesystem " 2>&1 > /dev/null ; then + + [ -d $DIRMOUNT ] || mkdir $DIRMOUNT + mountImage "$IMGFILE" "$DIRMOUNT" || (echolog "Error al montar la imagen $IMGFILE"; exit 1) + + + # Si el espacio libre menor que 200Mb desmontamos la imagen y nos salimos + AVAILABLE=$(df -k|grep $DIRMOUNT|awk '{print $4}') + if [ $AVAILABLE -lt 200000 ]; then + echolog "reducir imagen REPO $PARM2 $IMGEXT. tamaño minimo, nada que hacer" + umount $DIRMOUNT || (echolog "Error al desmontar la imagen $IMGFILE"; exit 1) + else + + # Calculamos la diferencia entre el tamaño interno y externo + EXTSIZE=$(ls -l --block-size=1024 $IMGFILE | cut -f5 -d" ") + INTSIZE=$(df -k|grep "$DIRMOUNT"|awk '{print $2}') + let EDGESIZE=$EXTSIZE-$INTSIZE + + echolog "reducir imagen REPO $PARM2 $IMGEXT, tamaño final: $ENDSIZE" + umount $DIRMOUNT + LOOPDEVICE=$(losetup -f) + losetup $LOOPDEVICE "$IMGFILE" + + # Redimensiono sistema de ficheros + echolog "resize2fs -fpM $LOOPDEVICE " + resize2fs -fpM $LOOPDEVICE # 2>&1 |logger --tag $0 --priority local0.info + mountImage "$IMGFILE" "$DIRMOUNT" + # Calculamos el tamaño final del archivo + INTSIZE=$(df -k|grep "$DIRMOUNT"|awk '{print $2}') + let EXTSIZE=$INTSIZE+$EDGESIZE + umount $DIRMOUNT || (echolog "Error al desmontar la imagen $IMGFILE"; exit 1) + # Si existe dispositivo loop lo borramos. + [ $LOOPDEVICE ] && losetup -a| grep $LOOPDEVICE &> /dev/null && losetup -d $LOOPDEVICE + # Corto el archivo al tamaño del sistema de ficheros + echo "truncate --size=\"$EXTSIZE\"k $IMGFILE" + echolog "truncate --size=\"$EXTSIZE\"k $IMGFILE" + truncate --size="$EXTSIZE"k $IMGFILE + fi + else + umount $DIRMOUNT || (echolog "Error al desmontar la imagen $IMGFILE"; exit 1) + fi + rmdir $DIRMOUNT + echo "reduced" >$LOCKFILE + + ;; + default) + echolog "Solicitud con parametros \"$PARM\" no realizada, no registrada o con errores" + ;; +esac diff --git a/admin/Sources/Services/opengnsys.default b/admin/Sources/Services/opengnsys.default new file mode 100644 index 0000000..d7376cf --- /dev/null +++ b/admin/Sources/Services/opengnsys.default @@ -0,0 +1,15 @@ +# RUN_OGADMSERVER defined as OpenGnsys Admin Server +# RUN_OGADMREPO defined as OpenGnsys Repository Manager +# RUN_OGADMAGENT run task scheduler service, only if Admin Server is enabled +# RUN_BTTRACKER run Bittorrent Tracker, only if Repository is enabled +# RUN_BTSEEDER start seeding of selected torrent files, only if Repository is enabled +# BTSEEDER_PRIORITY nice priority to seed torrent files; recommended values: +# 8 for Admin Server or Repo without Torrent +# 0 for Admin Server and Repo with Torrent +# -8 for Repo with Torrent +RUN_OGADMSERVER="yes" +RUN_OGADMREPO="yes" +RUN_OGADMAGENT="yes" +RUN_BTTRACKER="yes" +RUN_BTSEEDER="yes" +BTSEEDER_PRIORITY=0 diff --git a/admin/Sources/Services/opengnsys.init b/admin/Sources/Services/opengnsys.init new file mode 100644 index 0000000..ae27593 --- /dev/null +++ b/admin/Sources/Services/opengnsys.init @@ -0,0 +1,224 @@ +#!/bin/bash + +### BEGIN INIT INFO +# Provides: opengnsys +# Required-Start: +# Required-Stop: +# Default-Start: 2 3 4 5 +# Default-Stop: 1 +# Short-Description: Servicios del sistema OpenGnsys +# Description: Servicios del sistema OpenGnsys +### END INIT INFO + +# +# Definiciones globales +# +BASEDIR=/opt/opengnsys +OPENGNSYSUSER="opengnsys" +IMAGEDIR=$BASEDIR/images +CLIENTLOGDIR=$BASEDIR/log/clients + +# +# Servidor de OpenGnsys +# +SERVERNAME=ogAdmServer +SERVERDAEMON=$BASEDIR/sbin/$SERVERNAME +SERVERCFG=$BASEDIR/etc/$SERVERNAME.cfg +SERVERLOG=$BASEDIR/log/$SERVERNAME.log +SERVERDAEMON_OPTIONS="-f $SERVERCFG -l $SERVERLOG" + +# +# Servidor de Repositorio +# +############## ADV +REPOAUXNAME=ogAdmRepoAux +REPOAUXDAEMON=$BASEDIR/sbin/$REPOAUXNAME +REPOAUXPORT=$(awk -F= '/PUERTO/ {print $2+1}' $SERVERCFG 2>/dev/null) +############## ADV +############# IRINA # para setBootMode desde el cliente +SERVERAUXNAME=ogAdmServerAux +SERVERAUXDAEMON=$BASEDIR/sbin/$SERVERAUXNAME +SERVERAUXPORT=2011 +############# IRINA + +# +# Servidor de tareas programadas +# +AGENTNAME=ogAdmAgent +AGENTDAEMON=$BASEDIR/sbin/$AGENTNAME +AGENTCFG=$BASEDIR/etc/$AGENTNAME.cfg +AGENTLOG=$BASEDIR/log/$AGENTNAME.log +AGENTDAEMON_OPTIONS="-f $AGENTCFG -l $AGENTLOG" + +# +# Opciones Bittorrent +# + +BTTRACK=/usr/bin/bttrack.bittorrent +BTSEEDER=/usr/bin/btlaunchmany.bittornado +BTTRACKPORT=6969 +BTTRACKDFILE=/tmp/dstate +BTTRACKLOG=$BASEDIR/log/bttrack.log +BTINTERVAL=30 +BTTORRENTSDIR=$BASEDIR/images +BTTRACK_OPTIONS=" --port $BTTRACKPORT --dfile $BTTRACKDFILE --reannounce_interval $BTINTERVAL --logfile $BTTRACKLOG --allowed_dir $BTTORRENTSDIR --allow_get 1" +BTTRACKPID="/var/run/bttrack.pid" +BTSEEDERPID="/var/run/btseeder.pid" + + +export PATH="${PATH:+$PATH:}/usr/sbin:/sbin:/usr/bin" + +# Read config file if it is present. +if [ -r /etc/default/opengnsys ]; then + source /etc/default/opengnsys +fi + +# Configuración de arranque según la distribución Linux usada. +config() { + if [ -f /etc/os-release ]; then + source /etc/os-release + OSDISTRIB="$ID" + else + OSDISTRIB=$(lsb_release -is 2>/dev/null) + fi + OSDISTRIB="${OSDISTRIB,,}" + case "$OSDISTRIB" in + ubuntu|debian|linuxmint) + INITFUNCTIONS=/lib/lsb/init-functions + DAEMONSTART="start-stop-daemon --start --quiet --background --exec" + EXTRAOPTS="--" + DAEMONSTOP="start-stop-daemon --stop --quiet --oknodo --name" + ACTIONMSG="log_daemon_msg" + SUCCESSMSG="log_end_msg 0" + FAILMSG="log_end_msg 1" + TRACKERSTART="start-stop-daemon --make-pidfile --pidfile $BTTRACKPID --start --quiet --background --exec" + BTTRACK_OPTIONS="$BTTRACK_OPTIONS --parse_allowed_interval 1" + TRACKERSTOP="start-stop-daemon --stop --quiet --oknodo --retry 30 --pidfile $BTTRACKPID" + SEEDERSTART="start-stop-daemon --make-pidfile --pidfile $BTSEEDERPID --start --quiet --background --exec" + SEEDERSTOP="start-stop-daemon --stop --quiet --oknodo --pidfile $BTSEEDERPID" + ;; + centos|fedora) + INITFUNCTIONS=/etc/init.d/functions + DAEMONSTART="daemon" + ENDOPTS="&" + DAEMONSTOP="killproc" + ACTIONMSG="echo -n" + SUCCESSMSG="eval ( success; echo )" + FAILMSG="eval ( failure; echo )" + BTTRACK=/usr/bin/bttrack.py + BTSEEDER=/usr/bin/btlaunchmany.py + TRACKERSTART="daemon --pidfile $BTTRACKPID" + TRACKERSTOP="killproc -p $BTTRACKPID $BTTRACK" + SEEDERSTART="daemon --pidfile $BTSEEDERPID" + SEEDERSTOP="killproc -p $BTSEEDERPID $BTSEEDER" + ;; + *) echo "Distribución Linux desconcocida o no soportada." + exit ;; + esac + if [ -r $INITFUNCTIONS ]; then + source $INITFUNCTIONS + fi +} + +arranca_demonios() { + # Comprobar que está instalado OpenGnsys. + if [ ! -d $BASEDIR ]; then + $ACTIONMSG "ERROR: No existe el directorio $BASEDIR" + $FAILMSG + exit $? + fi + # Deshabilitar modo reforzado de SELinux. + [ -f /selinux/enforce ] && echo 0 > /selinux/enforce + # Verificar permisos básicos. + if [ "$(stat --printf="%A%G" $IMAGEDIR 2>/dev/null)" != "drwxrwxr-x$OPENGNSYSUSER" ]; then + mkdir $IMAGEDIR 2>/dev/null + chmod 775 $IMAGEDIR + chgrp $OPENGNSYSUSER $IMAGEDIR + fi + if [ "$(stat --printf="%A%G" $CLIENTLOGDIR 2>/dev/null)" != "drwxrwxr-x$OPENGNSYSUSER" ]; then + mkdir -p $CLIENTLOGDIR 2>/dev/null + chmod 775 $CLIENTLOGDIR + chgrp $OPENGNSYSUSER $CLIENTLOGDIR + fi + # Arrancar los servicios indicados. + if [ $RUN_OGADMSERVER = "yes" ]; then + $ACTIONMSG "Iniciando demonio: $SERVERNAME" + $DAEMONSTART $SERVERDAEMON $EXTRAOPTS $SERVERDAEMON_OPTIONS $ENDOPTS + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG + # Para SetBootmode desde el cliente + $ACTIONMSG "Iniciando demonio: $SERVERAUXNAME" # + faucet $SERVERAUXPORT --daemon --in bash -c "$SERVERAUXDAEMON" # NUEVO + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG + fi + if [ $RUN_OGADMREPO = "yes" ]; then + $ACTIONMSG "Iniciando demonio: $REPOAUXNAME" + faucet $REPOAUXPORT --daemon --in bash -c "$REPOAUXDAEMON" + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG + fi + if [ $RUN_OGADMSERVER = "yes" ] && [ $RUN_OGADMAGENT = "yes" ]; then + sleep 5 # Damos tiempo a que ogAdmServer este funcionando + fi + if [ $RUN_OGADMAGENT = "yes" ]; then + $ACTIONMSG "Iniciando demonio: $AGENTNAME" + $DAEMONSTART $AGENTDAEMON $EXTRAOPTS $AGENTDAEMON_OPTIONS $ENDOPTS + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG + fi + if [ $RUN_BTTRACKER = "yes" ]; then + $ACTIONMSG "Iniciando demonio: $BTTRACK" + $TRACKERSTART $BTTRACK $EXTRAOPTS $BTTRACK_OPTIONS $ENDOPTS + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG + fi + if [ $RUN_BTSEEDER = "yes" ]; then + $ACTIONMSG "Iniciando demonio: $BTSEEDER" + $SEEDERSTART $BTSEEDER $EXTRAOPTS $BTTORRENTSDIR &>/dev/null $ENDOPTS + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG + fi + +} + +para_demonios() { + if [ -e $BTSEEDERPID ]; then + $ACTIONMSG "Parando demonio: $BTSEEDER" + $SEEDERSTOP + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG + rm -f $BTSEEDERPID + fi + if [ -e $BTTRACKPID ]; then + $ACTIONMSG "Parando demonio: $BTTRACK" + $TRACKERSTOP + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG + rm -f $BTTRACKPID + fi + $ACTIONMSG "Parando demonio: $AGENTNAME" + $DAEMONSTOP $AGENTNAME + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG + $ACTIONMSG "Parando demonio: $REPOAUXNAME" + pkill faucet + [ $? -le 1 ] && $SUCCESSMSG || $FAILMSG + $ACTIONMSG "Parando demonio: $SERVERNAME" + $DAEMONSTOP $SERVERNAME + [ $? = 0 ] && $SUCCESSMSG || $FAILMSG +} + +config + +case "$1" in + start) + arranca_demonios + ;; + stop) + para_demonios + ;; + restart) + para_demonios + arranca_demonios + ;; + + *) + echo "Uso: $0 {start|stop|restart}" + exit 1 + ;; +esac + +exit 0 + diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..ee5d44e --- /dev/null +++ b/api/README.md @@ -0,0 +1,651 @@ + +## API de ogRepository + +La API de ogRepository proporciona una interfaz para facilitar la administración de las imágenes almacenadas en los repositorios de imágenes, permitiendo eliminarlas, enviarlas a clientes ogLive (con diferentes protocolos de transmisión), importarlas desde otros repositorios, etc. + +El presente documento detalla los endpoints de la API, con sus respectivos parámetros de entrada, así como las acciones que llevan a cabo. + +--- +### Tabla de Contenido: + +1. [Obtener Información de Estado de ogRepository](#obtener-información-de-estado-de-ogrepository) - `GET /ogrepository/v1/status` +2. [Obtener Información de todas las Imágenes](#obtener-información-de-todas-las-imágenes) - `GET /ogrepository/v1/images` +3. [Obtener Información de una Imagen concreta](#obtener-información-de-una-imagen-concreta) - `GET /ogrepository/v1/images/{ID_img}` +4. [Actualizar Información del Repositorio](#actualizar-información-del-repositorio) - `PUT /ogrepository/v1/images` +5. [Chequear integridad de Imagen](#chequear-integridad-de-imagen) - `GET /ogrepository/v1/status/images/{ID_img}` +6. [Eliminar una Imagen](#eliminar-una-imagen) - `DELETE /ogrepository/v1/images/{ID_img}?method={method}` +7. [Recuperar una Imagen](#recuperar-una-imagen) - `POST /ogrepository/v1/trash/images` +8. [Eliminar una Imagen de la Papelera](#eliminar-una-imagen-de-la-papelera) - `DELETE /ogrepository/v1/trash/images/{ID_img}` +9. [Importar una Imagen](#importar-una-imagen) - `POST /ogrepository/v1/repo/images` +10. [Exportar una Imagen](#exportar-una-imagen) - `PUT /ogrepository/v1/repo/images` +11. [Crear archivos auxiliares](#crear-archivos-auxiliares) - `POST /ogrepository/v1/images/torrentsum` +12. [Enviar paquete Wake On Lan](#enviar-paquete-wake-on-lan) - `POST /ogrepository/v1/wol` +13. [Enviar una Imagen mediante UDPcast](#enviar-una-imagen-mediante-udpcast) - `POST /ogrepository/v1/udpcast` +14. [Enviar una Imagen mediante UFTP](#enviar-una-imagen-mediante-uftp) - `POST /ogrepository/v1/uftp` +15. [Enviar una Imagen mediante P2P](#enviar-una-imagen-mediante-p2p) - `POST /ogrepository/v1/p2p` +16. [Ver Estado de Transmisiones UDPcast](#ver-estado-de-transmisiones-udpcast) - `GET /ogrepository/v1/udpcast` +17. [Ver Estado de Transmisiones UFTP](#ver-estado-de-transmisiones-uftp) - `GET /ogrepository/v1/uftp` +18. [Cancelar Transmisión UDPcast](#cancelar-transmisión-udpcast) - `DELETE /ogrepository/v1/udpcast/images/{ID_img}` +19. [Cancelar Transmisión UFTP](#cancelar-transmisión-uftp) - `DELETE /ogrepository/v1/uftp/images/{ID_img}` +20. [Cancelar Transmisiones P2P](#cancelar-transmisiones-p2p) - `DELETE /ogrepository/v1/p2p` + +--- +### Obtener Información de Estado de ogRepository + +Se devolverá informacion de CPU, memoria RAM, disco duro y el estado de ciertos servicios y procesos de ogRepository, en formato JSON. +Se puede utilizar el script "**getRepoStatus.py**, que debe ser llamado por el endpoint. +**NOTA**: En los apartados "services" y "processes" he especificado los servicios y procesos que me han parecido interesantes, pero se puede añadir o eliminar los que se desee. + +**URL:** `/ogrepository/v1/status` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/status +``` + +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al consultar y/o devolver la información de estado. +- **Código 200 OK:** La información de estado se obtuvo exitosamente. + - **Contenido:** Información de estado en formato JSON. + ```json + { + "cpu": { + "used_percentage": "35%" + }, + "ram": { + "total": "7.8GB", + "used": "0.3GB", + "available": "7.2GB", + "used_percentage": "7%" + }, + "disk": { + "total": "11.7GB", + "used": "7.7GB", + "available": "3.4GB", + "used_percentage": "69%" + }, + "services": { + "ssh": "active", + "smbd": "active", + "rsync": "active" + }, + "processes": { + "udp-sender": "stopped", + "uftp": "stopped", + "bttrack": "stopped", + "btlaunchmany": "stopped" + } + } + ``` +--- +### Obtener Información de todas las Imágenes + +Se devolverá la informacion contenida en el archivo "**/opt/opengnsys/ogrepository/etc/repoinfo.json**" (que corresponde a todas las imágenes almacenadas en el repositorio), y en el archivo "**/opt/opengnsys/ogrepository/etc/trashinfo.json**" (que corresponde a las imágenes que fueron eliminadas, que estarán en la papelera). +Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint, que a su vez llama al script "**updateRepoInfo.py**", para actualizar previamente la información del repositorio. +**NOTA**: El script requiere que se le pase "all" como primer parámetro (que correspondería al nombre de la imagen) y "none" como segundo parámetro (que corresponderia al nombre del subdirectorio correspondiente a la OU). Esta transformación de parámetros se realiza en la API. + +**URL:** `/ogrepository/v1/images` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images +``` + +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al consultar y/o devolver la información de las imágenes. +- **Código 200 OK:** La información de las imágenes se obtuvo exitosamente. + - **Contenido:** Información de imágenes en formato JSON. + ```json + { + "REPOSITORY": { + "directory": "/opt/opengnsys/ogrepository/images", + "images": [ + { + "name": "Ubuntu24", + "type": "img", + "clientname": "Ubuntu_24", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "EXTFS", + "datasize": 9859634200000, + "size": 4505673214, + "sum": "065a933c780ab1aaa044435ad5d4bf87", + "fullsum": "33575b9070e4a8043371b8c6ae52b80e" + }, + { + "name": "Windows10", + "type": "img", + "clientname": "Windows_10", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "NTFS", + "datasize": 24222105600000, + "size": 13198910185, + "sum": "8874d5ab84314f44841c36c69bb5aa82", + "fullsum": "9e7cd32c606ebe5bd39ba212ce7aeb02" + } + ], + "ous": [ + { + "subdir": "OU_subdir", + "images": [ + { + "name": "Ubuntu20", + "type": "img", + "clientname": "Ubuntu_20", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "EXTFS", + "datasize": 8912896000000, + "size": 3803794535, + "sum": "081a933c780ab1aaa044435ad5d4bf56", + "fullsum": "22735b9070e4a8043371b8c6ae52b90d" + } + ] + } + ] + } + }, + "TRASH": { + "directory": "/opt/opengnsys/ogrepository/images_trash", + "images": [], + "ous": [ + { + "subdir": "CentroVirtual", + "images": [ + { + "name": "Ubuntu20OLD", + "type": "img", + "clientname": "Ubuntu_20", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "EXTFS", + "datasize": 8912896000000, + "size": 3803794535, + "sum": "081a933c780ab1aaa044435ad5d4bf56", + "fullsum": "22735b9070e4a8043371b8c6ae52b90d" + } + ] + } + ] + } + ``` + - **name**: Nombre de la imagen, sin extensión. + - **type**: Extensión de la imagen. + - **clientname**: Nombre asignado al modelo del que se ha obtenido la imagen. + - **clonator**: Programa utilizado para la clonación. + - **compressor**: Programa utilizado para la compresión. + - **filesystem**: Sistema de archivos utilizado en la partición clonada. + - **datasize**: Tamaño de la imagen una vez restaurada, en bytes (tamaño de los datos). + - **size**: Tamaño del archivo de imagen, en bytes. + - **sum**: Hash MD5 del último MB del archivo de imagen. + - **fullsum**: Hash MD5 de todo el archivo de imagen. + +--- +### Obtener Información de una Imagen concreta + +Se devolverá la informacion de la imagen especificada, que puede estar en el archivo "**/opt/opengnsys/ogrepository/etc/repoinfo.json**" o en el archivo "**/opt/opengnsys/ogrepository/etc/trashinfo.json**" (en este último caso, si la imagen está en la papelera). +Se puede utilizar el script "**getRepoInfo.py**, que debe ser llamado por el endpoint, que a su vez llama al script "**updateRepoInfo.py**", para actualizar previamente la información del repositorio. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión) como primer parámetro, y el subdirectorio correspondiente a la OU (o "none" si no es el caso) como segundo parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. + +**URL:** `/ogrepository/v1/images/{ID_img}` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/22735b9070e4a8043371b8c6ae52b90d +``` + +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al consultar y/o devolver la información de la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La información de la imagen se obtuvo exitosamente. + - **Contenido:** Información de la imagen en formato JSON. + ```json + { + "directory": "/opt/opengnsys/ogrepository/images", + "images": [ + { + "name": "Windows10", + "type": "img", + "clientname": "Windows_10", + "clonator": "partclone", + "compressor": "lzop", + "filesystem": "NTFS", + "datasize": 9859634200000, + "size": 4505673214, + "sum": "065a933c780ab1aaa044435ad5d4bf87", + "fullsum": "33575b9070e4a8043371b8c6ae52b80e" + } + ] + } + ``` + - **name**: Nombre de la imagen, sin extensión. + - **type**: Extensión de la imagen. + - **clientname**: Nombre asignado al modelo del que se ha obtenido la imagen. + - **clonator**: Programa utilizado para la clonación. + - **compressor**: Programa utilizado para la compresión. + - **filesystem**: Sistema de archivos utilizado en la partición clonada. + - **datasize**: Tamaño de la imagen una vez restaurada, en bytes (tamaño de los datos). + - **size**: Tamaño del archivo de imagen, en bytes. + - **sum**: Hash MD5 del último MB del archivo de imagen. + - **fullsum**: Hash MD5 de todo el archivo de imagen. + +--- +### Actualizar Información del Repositorio + +Se actualizará la información de las imágenes almacenadas en el repositorio, reflejándola en el archivo "**/opt/opengnsys/ogrepository/etc/repoinfo.json**". +Se puede hacer con el script "**updateRepoInfo.py**", que debe ser llamado por el endpoint (y que es similar al script bash original "**checkrepo**"). +Este endpoint es llamado por el script "**deleteImage.py**" (para actualizar la información cada vez que se elimine una imagen), y creemos que también debe ser llamado por ogCore u ogLive cada vez que se haya creado una imagen. + +**URL:** `/ogrepository/v1/images` +**Método HTTP:** PUT + +**Ejemplo de Solicitud:** + +```bash +curl -X PUT -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al actualizar la información de las imágenes. +- **Código 200 OK:** La actualización se realizó exitosamente. + +--- +### Chequear Integridad de Imagen + +Se comprobará la integridad del fichero de imagen especificado como parámetro. +Se puede hacer con el script "**checkImage.py**", que compara el tamaño actual del archivo con el almacenado en el archivo "**.size**", y el hash MD5 del último MB del archivo con el almacenado en el archivo "**.sum**". +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. + +**URL:** `/ogrepository/v1/status/images/{ID_img}` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/status/images/22735b9070e4a8043371b8c6ae52b90d +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al chequear la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se ha chequeado exitosamente. +- **Código 200 KO:** La imagen se ha chequeado correctamente, pero no ha pasado el test. + +--- +### Eliminar una Imagen + +Se eliminará la imagen especificada como parámetro, pudiendo eliminarla permanentemente o enviarla a la papelera. +Se puede hacer con el script "**deleteimage.py**", que debe ser llamado por el endpoint (y que incluye la funcionalidad "papelera"), y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y el parámetro opcional "-p" (para que la eliminación sea permanente). Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros, pero también hay que especificar el método de eliminación en la URL, como parámetro adicional. + +**URL:** `/ogrepository/v1/images/{ID_img}?method={method}` +**Método HTTP:** DELETE + +**Parámetro adicional (en URL):** +- **method**: Método de eliminación (puede ser "trash" o "permanent"). + +**Ejemplo de Solicitud:** + +```bash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/images/22735b9070e4a8043371b8c6ae52b90d?method=trash +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al eliminar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se eliminó exitosamente. + +--- +### Recuperar una Imagen + +Se recuperará la imagen especificada como parámetro, desde la papelera. +Se puede hacer con el script "**recoverImage.py**", que debe ser llamado por el endpoint, y que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. + +**URL:** `/ogrepository/v1/trash/images` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d"}' http://example.com/ogrepository/v1/trash/images +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al recuperar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se recuperó exitosamente. + +--- +### Eliminar una Imagen de la Papelera + +Se eliminará permanentemente la imagen especificada como parámetro, desde la papelera. +Se puede hacer con el script "**deleteTrashImage.py**", que debe ser llamado por el endpoint, y que a su vez llama al script "**updateTrashInfo.py**", para actualizar la información de la papelera. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Estos datos se obtienen en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), y alli se realiza la transformación de parámetros. + +**URL:** `/ogrepository/v1/trash/images/{ID_img}` +**Método HTTP:** DELETE + +**Ejemplo de Solicitud:** + +```bash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/trash/images/22735b9070e4a8043371b8c6ae52b90d +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al eliminar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se eliminó exitosamente. + +--- +### Importar una Imagen + +Se importará una imagen de un repositorio remoto al repositorio local. +Se puede hacer con el script "**importImage.py**", que debe ser llamado por el endpoint. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. Estos parámetros deben enviarse desde ogCore (en el JSON), porque el repositorio local no puede extraer la información de la imagen de un ID almacenado en un repositorio remoto. +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está importando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + +**URL:** `/ogrepository/v1/repo/images` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). +- **repo_ip**: Dirección IP del repositorio remoto (desde el que se importará la imagen). +- **user**: Usuario con el que acceder al repositorio remoto. + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none", "repo_ip":"192.168.56.100", "user":"opengnsys"}' http://example.com/ogrepository/v1/repo/images +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al importar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen y/o el equipo remoto especificados. +- **Código 200 OK:** La imagen se está importando. + +--- +### Exportar una Imagen + +Se exportará una imagen del repositorio local a un repositorio remoto. +Se puede hacer con el script "**exportImage.py**", que debe ser llamado por el endpoint. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, la IP o hostname del repositorio remoto como segundo parámetro, y el usuario remoto como tercer parámetro. El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero la IP del repositorio remoto y el usuario remoto deben enviarse desde ogCore (en el JSON). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está exportando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + +**URL:** `/ogrepository/v1/repo/images` +**Método HTTP:** PUT + +**Cuerpo de la Solicitud (JSON):** +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). +- **repo_ip**: Dirección IP del repositorio remoto (al que se exportrará la imagen). +- **user**: Usuario con el que acceder al repositorio remoto. + +**Ejemplo de Solicitud:** + +```bash +curl -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d", "repo_ip":"192.168.56.100", "user":"opengnsys"}' http://example.com/ogrepository/v1/repo/images +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al exportar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen y/o el equipo remoto especificados. +- **Código 200 OK:** La imagen se está exportando. + +--- +### Crear archivos auxiliares + +Se crearán los archivos ".sum", ".full.sum", ".size" y ".torrent", para la imagen especificada como parámetro. +Se puede hacer con el script "**createTorrentSum.py**", que debe ser llamado por el endpoint. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como unico parámetro. Este parámetro no puede obtenerse en la API, a partir del ID de imagen (como en otros casos), porque el ID corresponde al contenido del archivo "full.sum" asociado (que no estará creado hasta que no se ejecute este script). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar cierto tiempo, por lo que solo informa de que los archivos auxiliares se están creando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + +**URL:** `/ogrepository/v1/images/torrentsum` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **image**: Nombre de la imagen (con extensión). +- **ou_subdir**: Subdirectorio correspondiente a la OU (o "none" si no es el caso). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"image":"Windows10.img", "ou_subdir":"none"}' http://example.com/ogrepository/v1/images/torrentsum +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al crear los archivos auxiliares. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** Los archivos auxiliares se están creando. + +--- +### Enviar paquete Wake On Lan + +Se enviará un paquete Wake On Lan a la dirección MAC especificada, a través de la IP de broadcast especificada. +Se puede hacer con el script "**sendWakeOnLan.py**", que debe ser llamado por el endpoint. +**NOTA**: La versión actual de este script requiere que se le pase la dirección IP de broadcast como primer parámetro, y la dirección MAC destino como segundo parámetro. Estos datos deben enviarse desde ogCore (en el JSON). + +**URL:** `/ogrepository/v1/wol` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **broadcast_ip**: IP de broadcast a la que enviar el paquete (puede ser "255.255.255.255", o la IP de broadcast de una subred). +- **mac**: Dirección MAC del equipo que se desea encender via Wake On Lan. + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"broadcast_ip":"255.255.255.255", "mac":"00:19:99:5c:bb:bb"}' http://example.com/ogrepository/v1/wol +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al enviar el paquete Wake On Lan. +- **Código 200 OK:** El paquete Wake On Lan se ha enviado exitosamente. + +--- +### Enviar una Imagen mediante UDPcast + +Se enviará la imagen especificada por Multicast, mediante la aplicación UDPcast. +Se puede hacer con el script "**sendFileMcast.py**", que a su vez llama al binario "**udp-sender**", que es quien realmente realiza el envío. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo (pero no avisa a ogCore de su finalización, porque no puede comprobar cuando acaba la tarea de restauración de la imagen). + +**URL:** `/ogrepository/v1/udpcast` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). +- **port**: Puerto Multicast. +- **method**: Modalidad half-duplex o full-duplex ("half" o "full"). +- **ip**: IP Multicast. +- **bitrate**: Velocidad de transmisión (en Mbps). +- **nclients**: Número mínimo de clientes. +- **maxtime**: Tiempo máximo de espera. + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d", "port":"9000", "method":"full", "ip":"239.194.17.2", "bitrate":"70M", "nclients":"20", "maxtime":"120"}' http://example.com/ogrepository/v1/udpcast +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se está enviando mediante UDPcast. + +--- +### Enviar una Imagen mediante UFTP + +Se enviará la imagen especificada por Unicast o Multicast, mediante el protocolo "UFTP". +Se puede hacer con el script "**sendFileUFTP.py**", que requiere que previamente los clientes ogLive destino se pongan en escucha con un daemon "UFTPD" (ejecutando el script "**listenUFTPD.py**"). Esto funciona al revés que "UDPcast", ya que primero se debe ejecutar un comando en los clientes, y luego en el servidor. +**NOTA**: El script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como primer parámetro, y los datos de transferencia como segundo parámetro (en una cadena, con los datos separados por dos puntos). El primer parámetro se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"), pero los datos de transferencia deben enviarse desde ogCore (y luego son tratados en la API, para construir la cadena correspondiente al parámetro). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo (pero no avisa a ogCore de su finalización, porque no puede comprobar cuando acaba la tarea de restauración de la imagen). + +**URL:** `/ogrepository/v1/uftp` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). +- **port**: Puerto Multicast. +- **ip**: IP Unicast/Multicast. +- **bitrate**: Velocidad de transmisión (con "K" para Kbps, "M" para Mbps o "G" para Gbps). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d", "port":"9000", "ip":"239.194.17.2", "bitrate":"1G"}' http://example.com/ogrepository/v1/uftp +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al enviar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se está enviando mediante UFTP. + +--- +### Enviar una Imagen mediante P2P + +Se enviará la imagen especificada mediante "P2P", iniciando el tracker y el seeder (que harán tracking y seed de los torrents contenidos en la raiz del directorio especificado). +Se puede hacer con los scripts "**runTorrentTracker.py**" y "**runTorrentSeeder.py**", que deben ser llamados por el endpoint. +**NOTA**: Estos scripts requieren que se les pase el directorio en el que está situada la imagen a enviar como único parámetro. Este dato se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"). +**NOTA2**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo (pero no avisa a ogCore de su finalización, porque no puede comprobar cuando acaba la tarea de restauración de la imagen). + + +**URL:** `/ogrepository/v1/p2p` +**Método HTTP:** POST + +**Cuerpo de la Solicitud (JSON):** +- **ID_img**: Identificador de la imagen (correspondiente al contenido del archivo "full.sum" asociado). + +**Ejemplo de Solicitud:** + +```bash +curl -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" -d '{"ID_img":"22735b9070e4a8043371b8c6ae52b90d"}' http://example.com/ogrepository/v1/p2p +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al intentar enviar la imagen. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 200 OK:** La imagen se está enviando mediante P2P. + +--- +### Ver Estado de Transmisiones UDPcast + +Se devolverá el pid de los procesos de transferencias UDPcast activas, y sus imágenes asociadas (con nombre e ID), en formato JSON, o un mensaje informativo si no hay procesos activos, o si se produce un error. +Se puede hacer con el script "**getUDPcastInfo.py**", que debe ser llamado por el endpoint. + +**URL:** `/ogrepository/v1/udpcast` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/udpcast +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al comprobar las transmisiones UDPcast. +- **Código 400 Bad Request:** No se han encontrado transmisiones UDPcast activas. +- **Código 200 OK:** La información de las transmisiones UDPcast activas se obtuvo exitosamente. + - **Contenido:** Información de las transmisiones UDPcast activas en formato JSON. + ```json + { + "6720": { + "image_id": "22735b9070e4a8043371b8c6ae52b90d", + "image_name": "Ubuntu20.img" + }, + "6721": { + "image_id": "9e7cd32c606ebe5bd39ba212ce7aeb02", + "image_name": "Windows10.img" + } + } + ``` +--- +### Ver Estado de Transmisiones UFTP + +Se devolverá el pid de los procesos de transferencias UFTP activas, y sus imágenes asociadas (con nombre e ID), en formato JSON, o un mensaje informativo si no hay procesos activos, o si se produce un error. +Se puede hacer con el script "**getUFTPInfo.py**", que debe ser llamado por el endpoint. + +**URL:** `/ogrepository/v1/uftp` +**Método HTTP:** GET + +**Ejemplo de Solicitud:** + +```bash +curl -X GET -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/uftp +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al comprobar las transmisiones UFTP. +- **Código 400 Bad Request:** No se han encontrado transmisiones UFTP activas. +- **Código 200 OK:** La información de las transmisiones UFTP activas se obtuvo exitosamente. + - **Contenido:** Información de las transmisiones UFTP activas en formato JSON. + ```json + { + "3427": { + "image_id": "22735b9070e4a8043371b8c6ae52b90d", + "image_name": "Ubuntu20.img" + }, + "4966": { + "image_id": "9e7cd32c606ebe5bd39ba212ce7aeb02", + "image_name": "Windows10.img" + } + } + ``` +--- +### Cancelar Transmisión UDPcast + +Se cancelará la transmisión por UDPcast activa de la imagen especificada como parámetro, deteniendo el proceso "udp-sender" asociado a dicha imagen. +Se puede hacer con el script "**stopUDPcast.py**", que debe ser llamado por el endpoint. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Este dato se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"). + +**URL:** `/ogrepository/v1/udpcast/images/{ID_img}` +**Método HTTP:** DELETE + +**Ejemplo de Solicitud:** + +```bash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/udpcast/images/22735b9070e4a8043371b8c6ae52b90d +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al cancelar la transmisión UDPcast. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 400 Bad Request:** No hay transmisiones UDPcast activas para la imagen especificada. +- **Código 200 OK:** La transmisión UDPcast se ha cancelado exitosamente. + +--- +### Cancelar Transmisión UFTP + +Se cancelará la transmisión por UFTP activa de la imagen especificada como parámetro, deteniendo el proceso "uftp" asociado a dicha imagen. +Se puede hacer con el script "**stopUFTP.py**", que debe ser llamado por el endpoint. +**NOTA**: La versión actual de este script requiere que se le pase el nombre de la imagen (con extensión, e incluyendo el nombre del directorio correspondiente a la OU, si fuera el caso) como único parámetro. Este dato se obtiene en la API, a partir del ID de la imagen (que corresponde al contenido del archivo "full.sum"). + +**URL:** `/ogrepository/v1/uftp/images/{ID_img}` +**Método HTTP:** DELETE + +**Ejemplo de Solicitud:** + +```bash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/uftp/images/22735b9070e4a8043371b8c6ae52b90d +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al cancelar la transmisión UFTP. +- **Código 400 Bad Request:** No se ha encontrado la imagen especificada. +- **Código 400 Bad Request:** No hay transmisiones UFTP activas para la imagen especificada. +- **Código 200 OK:** La transmisión UFTP se ha cancelado exitosamente. + +--- +### Cancelar Transmisiones P2P + +Se cancelarán las transmisiones P2P activas en el ogRepository al que se envíe la orden, deteniendo los procesos "bttrack" y "btlaunchmany.bittornado". +Se puede hacer con el script "**stopP2P.py**", que debe ser llamado por el endpoint. +**NOTA**: No he encontrado la forma de detener la transmisión de una imagen concreta, ya que "bttrack" y "btlaunchmany.bittornado" hacen tracking y seed (respectivamente) de todos los torrents existentes en la raíz del directorio especificado. + +**URL:** `/ogrepository/v1/p2p` +**Método HTTP:** DELETE + +**Ejemplo de Solicitud:** + +```bash +curl -X DELETE -H "Authorization: $API_KEY" http://example.com/ogrepository/v1/p2p +``` +**Respuestas:** +- **Código 500 Internal Server Error:** Ocurrió un error al cancelar las transmisiones P2P. +- **Código 200 OK:** Las transmisiones P2P se han cancelado exitosamente. + +--- \ No newline at end of file diff --git a/api/repo_api.py b/api/repo_api.py new file mode 100644 index 0000000..6544a53 --- /dev/null +++ b/api/repo_api.py @@ -0,0 +1,1343 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + API de ogRepository, programada en Flask. + +Responde a peticiones HTTP (en principio, enviadas desde ogCore) mediante endpoints, que a su vez ejecutan los scripts Python almacenados en ogRepository. +En ciertos casos, transforma los parámetros recibidos desde el portal, para adaptarlos a los que es necesario enviar a los scripts + (por ejemplo, a partir del ID de una imagen obtiene su nombre, su extensión y el subdirectorio de OU). + +Librerías Python requeridas: - flask (se puede instalar con "sudo apt install python3-flask") + - paramiko (se puede instalar con "sudo apt install python3-paramiko") + - requests (se puede instalar con "sudo apt install python3-requests") - No tengo claro que para este paquete sea necesario + - flasgger (se puede instalar con "sudo apt install python3-flasgger") +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +from flask import Flask, jsonify, request +import os +import subprocess +import json +from time import sleep +import paramiko +import logging +import threading +import requests +import random +# Imports para Swagger: +from flasgger import Swagger +import yaml + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final +script_path = '/opt/opengnsys/ogrepository/bin' +repo_file = '/opt/opengnsys/ogrepository/etc/repoinfo.json' +trash_file = '/opt/opengnsys/ogrepository/etc/trashinfo.json' + +ogcore_ip = '172.17.8.26' # En la versión final, se tendrá que pillar de una variable de entorno + +""" +repo_path = '/home/user/images/' +script_path = '/home/user' +repo_file = '/home/user/jsons/repoinfo.json' +trash_file = '/home/user/jsons/trashinfo.json' +""" + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +# Creamos una instancia de la aplicación Flask: +app = Flask(__name__) + + +# Configuración Swagger: +with open("swagger.yaml", "r") as file: + swagger_template = yaml.safe_load(file) + +swagger = Swagger(app, template=swagger_template) + + +# --------------------------------------------------------- + + +def get_image_params(image_id, search='all'): + """ A partir de un ID de imagen (que corresponde al "fullsum"), busca la imagen en el repositorio y/o en la papelera (dependiendo del parámetro "search"). + Si encuentra la imagen devuelve su nombre, su extensión y el subdirectorio de OU (si fuera el caso) en un diccionario, + y si no encuentra la imagen especificada retorna "None". + El parámtro "search" tiene el valor predeterminado "all" (que hará que busque tanto en el repo como en la papelera), + pero se le puede pasar el valor "repo" (para que busque solo en el repo) o "trash" (para que busque solo en la papelera). + """ + # Creamos un diccionario vacío, para almacenar los resultados: + result = {} + + # Abrimos y almacenamos el archivo "repoinfo.json" (solo si se ha de buscar en el repo, y si el archivo tiene contenido): + if (search == 'all' or search == 'repo') and os.path.getsize(repo_file) > 0: + with open(repo_file, 'r') as file: + repo_data = json.load(file) + # Iteramos la clave "images" y buscamos la imagen (y si la encontramos almacenamos el nombre y la extension): + for image in repo_data.get('images', []): + if image.get('fullsum') == image_id: + result['name'] = image.get('name') + result['extension'] = image.get('type') + return result + # Iteramos la clave "ous" y su sublclave "images" y buscamos la imagen (y si la encontramos almacenamos el nombre y la extension): + for ou in repo_data.get('ous', []): + for image in ou.get('images', []): + if image.get('fullsum') == image_id: + result['name'] = image.get('name') + result['extension'] = image.get('type') + result['subdir'] = ou.get('subdir') + return result + + # Abrimos y almacenamos el archivo "trashinfo.json" (solo si se ha de buscar en la papelera, y si el archivo tiene contenido): + if (search == 'all' or search == 'trash') and os.path.getsize(trash_file) > 0: + with open(trash_file, 'r') as file: + trash_data = json.load(file) + # Iteramos la clave "images" y buscamos la imagen (y si la encontramos almacenamos el nombre y la extension): + for image in trash_data.get('images', []): + if image.get('fullsum') == image_id: + result['name'] = image.get('name') + result['extension'] = image.get('type') + return result + # Iteramos la clave "ous" y su sublclave "images" y buscamos la imagen (y si la encontramos almacenamos el nombre y la extension): + for ou in trash_data.get('ous', []): + for image in ou.get('images', []): + if image.get('fullsum') == image_id: + result['name'] = image.get('name') + result['extension'] = image.get('type') + result['subdir'] = ou.get('subdir') + return result + + # Si no encontramos la imagen, retornamos "None": + return None + + +# --------------------------------------------------------- + + +def search_process(process, string_to_search): + """ Busca procesos que contengan el valor del parámetro "process" y el valor del parámetro "string_to_search" (la ruta de la imagen, normalmente). + Si encuentra alguno retorna "True", y si no encuentra ninguno retorna "False". + """ + try: + # Obtenemos todos los procesos que están corriendo, y almacenamos la salida y los errores: + result = subprocess.Popen(['ps', '-aux'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + out, error = result.communicate() + + # Almacenamos en una lista los procesos que contengan el proceso del parámetro y la cadena a buscar: + process_list = [line for line in out.split('\n') if process in line and string_to_search in line] + + # Si hemos encontrado algún proceso que cumpla las condiciones, retornamos "True", y si no retornamos "False": + if process_list != []: + return True + else: + return False + # Si se ha producido una excepción, imprimimos el error: + except Exception as error_description: + print(f"Unexpected error: {error_description}") + + +# --------------------------------------------------------- + + +def check_lock_local(image_file_path, job_id): + """ Cada minuto comprueba si existe un archivo ".lock" asociado a la imagen que recibe como parámetro + (lo que significará que hay una tarea en curso), en el repositorio local. + Cuando no encuentre el archivo ".lock" lo comunicará a ogCore, llamando a un endpoint, + y dejará de realizar la comprobación (saliendo del bucle). + """ + # Esperamos 30 segundos, para dar tiempo a que se cree el archivo ".lock": + sleep(30) + + # Creamos un bucle infinito: + while True: + # Si ya no existe el archivo ".lock", imprimimos un mensaje en la API, respondemos a ogCore y salimos del bucle: + if not os.path.exists(f"{image_file_path}.lock"): + app.logger.info("Task finalized (no .lock file)") + app.logger.info(f"Job_ID: {job_id}") + + # Almacenamos en un diccionario los datos a enviar a ogCore: + data = { + 'job_id': job_id + } + # Llamamos al endpoint de ogCore, enviando los datos (de momento comento la llamada, porque la función llama a un endpoint inexistente): + #recall_ogcore(data) + break + # Si aun existe el archivo ".lock", imprimimos un mensaje en la API: + else: + app.logger.info("Task in process (.lock file exists)") + # Esperamos 1 minuto para volver a realizar la comprobación: + sleep(60) + + +# --------------------------------------------------------- + + +def check_lock_remote(image_file_path, remote_host, remote_user, job_id): + """ Cada minuto comprueba si existe un archivo ".lock" asociado a la imagen que recibe como parámetro + (lo que significará que hay una tarea en curso), en un repositorio remoto (al que conecta por SSH/SFTP). + Cuando no encuentre el archivo ".lock" lo comunicará a ogCore, llamando a un endpoint, + y dejará de realizar la comprobación (saliendo del bucle). + """ + # Iniciamos un cliente SSH: + ssh_client = paramiko.SSHClient() + # Establecemos la política por defecto para localizar la llave del host localmente: + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + # Conectamos con el equipo remoto por SSH: + ssh_client.connect(remote_host, 22, remote_user) # Así se hace con claves + #ssh_client.connect(remote_host, 22, remote_user, 'opengnsys') # Así se haría con password + + # Iniciamos un cliente SFTP: + sftp_client = ssh_client.open_sftp() + # Esperamos 30 segundos, para dar tiempo a que se cree el archivo ".lock": + sleep(30) + + # Creamos un bucle infinito: + while True: + try: + # Si aun existe el archivo ".lock", imprimimos un mensaje en la API: + sftp_client.stat(f"{image_file_path}.lock") + app.logger.info("Task in process (.lock file exists)") + except IOError: + # Si ya no existe el archivo ".lock", imprimimos un mensaje en la API, respondemos a ogCore y salimos del bucle: + app.logger.info("Task finalized (no .lock file)") + app.logger.info(f"Job_ID: {job_id}") + + # Almacenamos en un diccionario los datos a enviar a ogCore: + data = { + 'job_id': job_id + } + # Llamamos al endpoint de ogCore, enviando los datos (de momento comento la llamada, porque la función llama a un endpoint inexistente): + #recall_ogcore(data) + break + # Esperamos 1 minuto para volver a realizar la comprobación: + sleep(60) + + # Ya fuera del bucle, cerramos el cliente SSH y el cliente SFTP: + ssh_client.close() + sftp_client.close() + + +# --------------------------------------------------------- + + +def check_aux_files(image_file_path, job_id): + """ Cada 10 segundos comprueba si se han creado todos los archivos auxiliares de la imagen que recibe como parámetro, + en cuyo caso lo comunicará a ogCore, llamando a un endpoint, y dejará de realizar la comprobación. + También obtiene el valor del archivo ".full.sum" (que corresonde al ID), y se lo comunica a ogCore. + """ + # Creamos un bucle infinito: + while True: + # Si faltan archivos auxiliares por crear, imprimimos un mensaje en la API: + if not os.path.exists(f"{image_file_path}.size") or not os.path.exists(f"{image_file_path}.sum") or not os.path.exists(f"{image_file_path}.full.sum") or not os.path.exists(f"{image_file_path}.torrent") or not os.path.exists(f"{image_file_path}.info.checked"): + app.logger.info("Task in process (auxiliar files remaining)") + # Si ya se han creado todos los archivos auxiliares, imprimimos un mensaje en la API, respondemos a ogCore y salimos del bucle: + else: + app.logger.info("Task finalized (all auxilar files created)") + # Obtenemos el valor del archivo "full.sum", que corresponde al ID, y lo imprimimos: + with open(f"{image_file_path}.full.sum", 'r') as file: + image_id = file.read().strip('\n') + app.logger.info(f"Job_ID: {job_id}") + app.logger.info(f"Image_ID: {image_id}") + + # Almacenamos en un diccionario los datos a enviar a ogCore: + data = { + 'job_id': job_id, + 'image_id': image_id + } + # Llamamos al endpoint de ogCore, enviando los datos (de momento comento la llamada, porque la función llama a un endpoint inexistente): + #recall_ogcore(data) + break + # Esperamos 10 segundos para volver a realizar la comprobación: + sleep(10) + + +# --------------------------------------------------------- + + +def recall_ogcore(data): + """ Hace una petición HTTP de tipo POST a un endpoint de ogCore, enviando datos que dependen del caso. + Se utiliza para informar a ogCore del resultado de una tarea asíncrona, + que estaba corriendo en un proceso independiente (no controlado por los endpoints). + """ + # Almacenamos la URL del endpoint de ogCore (prueba): + endpoint_url = f"http://{ogcore_ip}:8006/ogcore/v1/test" + + # Almacenamos los headers, y convertiomos "data" a JSON: + headers = {'content-type': 'application/json'} + data = json.dumps(data) + + # Hacemos una petición POST al endpoint, enviando lo almacenado en "data": + response = requests.post(endpoint_url, data=data, headers=headers) + + # Imprimimos el código de estado de la petición y la respuesta de ogCore: + app.logger.info(f"HTTP Status Code: {response.status_code}") + app.logger.info(f"HTTP Response: {response.text}") + + + +# -------------------------------------------------------------------------------------------- +# ENDPOINTS +# -------------------------------------------------------------------------------------------- + + +# 1 - Endpoint "Obtener Información de Estado de ogRepository" (SINCRONO): +@app.route("/ogrepository/v1/status", methods=['GET']) +def get_repo_status(): + """ Este endpoint devuelve información de CPU, memoria RAM, disco duro y el estado de ciertos servicios y procesos de ogRepository, en formato json. + Para ello, ejecuta el script "getRepoStatus.py", que no recibe parámetros. + """ + try: + # Ejecutamos el script "getRepoStatus.py", y almacenamos el resultado: + result = subprocess.run(['sudo', 'python3', f"{script_path}/getRepoStatus.py"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos la respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": json.loads(result.stdout) + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 2 - Endpoint "Obtener Información de todas las Imágenes" (SINCRONO): +@app.route("/ogrepository/v1/images", methods=['GET']) +def get_repo_info(): + """ Este endpoint devuelve información de todas las imágenes contenidas en el repositorio (incluída la papelera), en formato json. + Para ello, ejecuta el script "getRepoInfo.py", con los parámetros "all" y "none". + """ + try: + # Ejecutamos el script "getRepoInfo.py" (con los parámetros "all" y "none"), y almacenamos el resultado: + result = subprocess.run(['sudo', 'python3', f"{script_path}/getRepoInfo.py", 'all', 'none'], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos la respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": json.loads(result.stdout) + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 3 - Endpoint "Obtener Información de una Imagen concreta" (SINCRONO): +@app.route("/ogrepository/v1/images/", methods=['GET']) +def get_repo_image_info(imageId): + """ Este endpoint devuelve información de la imagen especificada como parámetro, en formato json. + Para ello, ejecuta el script "getRepoInfo.py", con el nombre de la imagen como primer parámetro, + y el subdirectorio correspondiente a la OU (o "none") como segundo parámetro. + """ + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(imageId) + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd = ['sudo', 'python3', f"{script_path}/getRepoInfo.py", f"{param_dict['name']}.{param_dict['extension']}", param_dict['subdir']] + else: + cmd = ['sudo', 'python3', f"{script_path}/getRepoInfo.py", f"{param_dict['name']}.{param_dict['extension']}", 'none'] + else: + return jsonify({ + "success": False, + "error": "Image not found" + }), 400 + + try: + # Ejecutamos el script "getRepoInfo.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos la respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": json.loads(result.stdout) + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 4 - Endpoint "Actualizar Información del Repositorio" (SINCRONO): +@app.route("/ogrepository/v1/images", methods=['PUT']) +def update_repo_info(): + """ Este endpoint actualiza la información del repositorio y de la papelera, reflejándola en los archivos "repoinfo.json" y "trashinfo.josn". + Para ello, ejecuta el script "updateRepoInfo.py", que a su vez ejecuta el script "updateTrashInfo.py". + """ + try: + # Ejecutamos el script "updateRepoInfo.py" (sin parámetros), y almacenamos el resultado: + result = subprocess.run(['sudo', 'python3', f"{script_path}/updateRepoInfo.py"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "Repository info updated successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 5 - Endpoint "Chequear Integridad de Imagen" (SINCRONO): +@app.route("/ogrepository/v1/status/images/", methods=['GET']) +def check_image(imageId): + """ Este endpoint comprueba la integridad de la imagen especificada como parámetro, + comparando el tamaño y el hash MD5 del último MB con los valores almacenados en los archivos "size" y "sum". + Para ello, ejecuta el script "checkImage.py", con el nombre de la imagen como único parámetro, + incluyendo el subdirectorio correspondiente a la OU (si fuera el caso). + """ + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(imageId, "repo") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd = ['sudo', 'python3', f"{script_path}/checkImage.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}"] + else: + cmd = ['sudo', 'python3', f"{script_path}/checkImage.py", f"{param_dict['name']}.{param_dict['extension']}"] + else: + return jsonify({ + "success": False, + "error": "Image not found (inexistent or deleted)" + }), 400 + + try: + # Ejecutamos el script "checkImage.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos la respuesta: + if result.returncode == 0: + if "Error" in result.stdout: + return jsonify({ + "success": True, + "output": "Image file didn't pass the Integrity Check" + }), 200 + else: + return jsonify({ + "success": True, + "output": "Image file passed the Integrity Check correctly" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 6 - Endpoint "Eliminar una Imagen" (SINCRONO): +@app.route("/ogrepository/v1/images/", methods=['DELETE']) +def delete_image(imageId): + """ Este endpoint elimina la imagen especificada como parámetro (y todos sus archivos asociados), + moviéndolos a la papelera o eliminándolos permanentemente (dependiendo del parámetro "method"). + Para ello, ejecuta el script "deleteImage.py", con el nombre de la imagen como primer parámetro, + incluyendo el subdirectorio correspondiente a la OU (si fuera el caso), y el parámetro opcional "-p". + """ + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(imageId, "repo") + # Almacenams el método de eliminación ("trash" o "permanent") + method = request.values.get('method') + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + if method == "permanent": + cmd = ['sudo', 'python3', f"{script_path}/deleteImage.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}", '-p'] + elif method == "trash": + cmd = ['sudo', 'python3', f"{script_path}/deleteImage.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}"] + else: + return jsonify({ + "success": False, + "error": "Incorrect method (must be 'permanent' or 'trash')" + }), 400 + else: + if method == "permanent": + cmd = ['sudo', 'python3', f"{script_path}/deleteImage.py", f"{param_dict['name']}.{param_dict['extension']}", '-p'] + elif method == "trash": + cmd = ['sudo', 'python3', f"{script_path}/deleteImage.py", f"{param_dict['name']}.{param_dict['extension']}"] + else: + return jsonify({ + "success": False, + "error": "Incorrect method (must be 'permanent' or 'trash')" + }), 400 + else: + return jsonify({ + "success": False, + "error": "Image not found (inexistent or deleted)" + }), 400 + + try: + # Ejecutamos el script "deleteImage.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "Image deleted successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 7 - Endpoint "Recuperar una Imagen" (SINCRONO): +@app.route("/ogrepository/v1/trash/images", methods=['POST']) +def recover_image(): + """ Este endpoint recupera la imagen especificada como parámetro (y todos sus archivos asociados), + moviéndolos nuevamente al repositorio de imágenes, desde la papelera. + Para ello, ejecuta el script "recoverImage.py", con el nombre de la imagen como único parámetro, + incluyendo el subdirectorio correspondiente a la OU (si fuera el caso). + """ + # Almacenamos el parámetro "ID_img" (enviado por JSON): + json_data = json.loads(request.data) + image_id = json_data.get("ID_img") + + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(image_id, "trash") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd = ['sudo', 'python3', f"{script_path}/recoverImage.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}"] + else: + cmd = ['sudo', 'python3', f"{script_path}/recoverImage.py", f"{param_dict['name']}.{param_dict['extension']}"] + else: + return jsonify({ + "success": False, + "error": "Image not found (inexistent or recovered previously)" + }), 400 + + try: + # Ejecutamos el script "recoverImage.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "Image recovered successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 8 - Endpoint "Eliminar una Imagen de la Papelera" (SINCRONO): +@app.route("/ogrepository/v1/trash/images/", methods=['DELETE']) +def delete_trash_image(imageId): + """ Este endpoint elimina permanentemente la imagen especificada como parámetro (y todos sus archivos asociados), desde la papelera. + Para ello, ejecuta el script "deleteTrashImage.py", con el nombre de la imagen como único parámetro, + incluyendo el subdirectorio correspondiente a la OU (si fuera el caso). + """ + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(imageId, "trash") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd = ['sudo', 'python3', f"{script_path}/deleteTrashImage.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}"] + else: + cmd = ['sudo', 'python3', f"{script_path}/deleteTrashImage.py", f"{param_dict['name']}.{param_dict['extension']}"] + else: + return jsonify({ + "success": False, + "error": "Image not found at trash" + }), 400 + + try: + # Ejecutamos el script "deleteTrashImage.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "Image deleted successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 9 - Endpoint "Importar una Imagen" (ASINCRONO): +@app.route("/ogrepository/v1/repo/images", methods=['POST']) +def import_image(): + """ Este endpoint importa la imagen especificada como primer parámetro (y todos sus archivos asociados), + desde un servidor remoto al servidor local. + Para ello, ejecuta el script "importImage.py", con el nombre de la imagen como primer parámetro (con subdirectorio de OU, si es el caso), + la IP o hostname del servidor remoto como segundo parámetro, y el usuario con el que conectar al servidor como tercer parámetro. + """ + # Almacenamos los parámetros enviados en el JSON: + json_data = json.loads(request.data) + image_name = json_data.get("image") + ou_subdir = json_data.get("ou_subdir") + remote_ip = json_data.get("repo_ip") + remote_user = json_data.get("user") + + # Evaluamos los parámetros obtenidos, para construir la ruta de la imagen: + if ou_subdir == "none": + image_file_path = f"{repo_path}{image_name}" + else: + image_file_path = f"{repo_path}{ou_subdir}/{image_name}" + + # Construimos la llamada al script: + cmd = ['sudo', 'python3', f"{script_path}/importImage.py", image_file_path, remote_ip, remote_user] + + try: + # Ejecutamos el script "importImage.py" (con los parámetros almacenados), y almacenamos el resultado (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"ImportImage_{''.join(random.choice('0123456789abcdef') for char in range(8))}" + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode is None: + # Si el resultado es correcto, llamamos a la función "check_lock_local" en un hilo paralelo + # (para que compruebe si la imagen se ha acabado de importar exitosamente): + threading.Thread(target=check_lock_local, args=(image_file_path, job_id,)).start() + + # Informamos que la imagen se está importando, y salimos del endpoint: + return jsonify({ + "success": True, + "output": "Importing image...", + "job_id": job_id + }), 200 + else: + return jsonify({ + "success": False, + "error": "Image import failed" + }), 500 + except subprocess.CalledProcessError as error: + return jsonify({ + "success": False, + "process exception": str(error) + }), 500 + except Exception as error_description: + if "exit status 2" in str(error_description): + return jsonify({ + "success": False, + "exception": "Can't connect to remote server" + }), 400 + elif "exit status 3" in str(error_description): + return jsonify({ + "success": False, + "exception": "Remote image not found" + }), 400 + elif "exit status 4" in str(error_description): + return jsonify({ + "success": False, + "exception": "Remote image is locked" + }), 400 + else: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 10 - Endpoint "Exportar una Imagen" (ASINCRONO): +@app.route("/ogrepository/v1/repo/images", methods=['PUT']) +def export_image(): + """ Este endpoint exporta la imagen especificada como primer parámetro (y todos sus archivos asociados), + desde el servidor local a un servidor remoto. + Para ello, ejecuta el script "exportImage.py", con el nombre de la imagen como primer parámetro (con subdirectorio de OU, si es el caso), + la IP o hostname del servidor remoto como segundo parámetro, y el usuario con el que conectar al servidor como tercer parámetro. + """ + # Almacenamos los parámetros enviados en el JSON: + json_data = json.loads(request.data) + image_id = json_data.get("ID_img") + remote_ip = json_data.get("repo_ip") + remote_user = json_data.get("user") + + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(image_id, "repo") + + # Evaluamos los parámetros obtenidos, para construir la ruta de la imagen, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + image_file_path = f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}" + else: + image_file_path = f"{param_dict['name']}.{param_dict['extension']}" + else: + return jsonify({ + "success": False, + "error": "Image not found" + }), 400 + + # Construimos la llamada al script: + cmd = ['sudo', 'python3', f"{script_path}/exportImage.py", image_file_path, remote_ip, remote_user] + + try: + # Ejecutamos el script "exportImage.py" (con los parámetros almacenados), y almacenamos el resultado (pero sin esperar a que termine el proceso): + result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Generamos el ID para identificar el trabajo asíncrono: + job_id = f"ExportImage_{''.join(random.choice('0123456789abcdef') for char in range(8))}" + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode is None: + # Si el resultado es correcto, llamamos a la función "check_lock_remote" en un hilo paralelo + # (para que compruebe si la imagen se ha acabado de exportar exitosamente): + threading.Thread(target=check_lock_remote, args=(f"{repo_path}{image_file_path}", remote_ip, remote_user, job_id,)).start() + + # Informamos que la imagen se está exportando, y salimos del endpoint: + return jsonify({ + "success": True, + "output": "Exporting image...", + "job_id": job_id + }), 200 + else: + return jsonify({ + "success": False, + "error": "Export image failed" + }), 500 + except subprocess.CalledProcessError as error: + return jsonify({ + "success": False, + "process exception": str(error) + }), 500 + except Exception as error_description: + if "exit status 3" in str(error_description): + return jsonify({ + "success": False, + "exception": "Image is locked" + }), 400 + elif "exit status 4" in str(error_description): + return jsonify({ + "success": False, + "exception": "Can't connect to remote server" + }), 400 + elif "exit status 5" in str(error_description): + return jsonify({ + "success": False, + "exception": "Image already exists on remote server" + }), 400 + else: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 11 - Endpoint "Crear archivos auxiliares" (ASINCRONO): +@app.route("/ogrepository/v1/images/torrentsum", methods=['POST']) +def create_torrent_sum(): + """ Este endpoint crea los archivos ".size", ".sum", ".full.sum" y ".torrent" para la imagen que recibe como parámetro. + Para ello, ejecuta el script "createTorrentSum.py", con el nombre de la imagen como único parámetro (con subdirectorio de OU, si es el caso). + """ + # Almacenamos los parámetros enviados en el JSON: + json_data = json.loads(request.data) + image_name = json_data.get("image") + ou_subdir = json_data.get("ou_subdir") + + # Evaluamos los parámetros obtenidos, para construir la ruta de la imagen (relativa a "repo_path"): + if ou_subdir == "none": + image_file_path = image_name + else: + image_file_path = f"{ou_subdir}/{image_name}" + + # Construimos la llamada al script: + cmd = ['sudo', 'python3', f"{script_path}/createTorrentSum.py", image_file_path] + + try: + # Ejecutamos el script "createTorrentSum.py" (con los parámetros almacenados), y almacenamos el resultado (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"CreateAuxiliarFiles_{''.join(random.choice('0123456789abcdef') for char in range(8))}" + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode is None: + # Si el resultado es correcto, llamamos a la función "check_aux_files" en un hilo paralelo + # (para que compruebe si se han creado todos los archivos auxiliares exitosamente): + threading.Thread(target=check_aux_files, args=(f"{repo_path}{image_file_path}", job_id,)).start() + + # Informamos que los archivos auxiliares se están creando, y salimos del endpoint: + return jsonify({ + "success": True, + "output": "Creating auxiliar files...", + "job_id": job_id + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + if "exit status 2" in str(error_description): + return jsonify({ + "success": False, + "exception": "Image not found" + }), 400 + elif "exit status 3" in str(error_description): + return jsonify({ + "success": False, + "exception": "Image is locked" + }), 400 + else: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 12 - Endpoint "Enviar paquete Wake On Lan" (SINCRONO): +@app.route("/ogrepository/v1/wol", methods=['POST']) +def send_wakeonlan(): + """ Este endpoint envía un paquete mágico Wake On Lan a la dirección MAC especificada, a través de la IP de broadcast especificadac. + Para ello, ejecuta el script "sendWakeOnLan.py", con la IP de broadcast como primer parámetro, y la MAC como segundo parámetro. + """ + # Almacenamos los parámetros enviados en el JSON: + json_data = json.loads(request.data) + broadcast_ip = json_data.get("broadcast_ip") + mac = json_data.get("mac") + + try: + # Ejecutamos el script "sendWakeOnLan.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(['sudo', 'python3', f"{script_path}/sendWakeOnLan.py", broadcast_ip, mac], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "Wake On Lan packet sended successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 13 - Endpoint "Enviar una Imagen mediante UDPcast" (ASINCRONO): +@app.route("/ogrepository/v1/udpcast", methods=['POST']) +def send_udpcast(): + """ Este endpoint envía mediante UDPcast la imagen que recibe como primer parámetro, con los datos de transferencia que recibe en los demás parámetros. + Para ello, ejecuta el script "sendFileMcast.py", con la imagen como primer parámetro, y los demás en una cadena (como segundo parámetro). + """ + # Almacenamos los parámetros enviados en el JSON: + json_data = json.loads(request.data) + image_id = json_data.get("ID_img") + port = json_data.get("port") + method = json_data.get("method") + ip = json_data.get("ip") + bitrate = json_data.get("bitrate") + nclients = json_data.get("nclients") + maxtime = json_data.get("maxtime") + + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(image_id, "repo") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd = ['sudo', 'python3', f"{script_path}/sendFileMcast.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}", f"{port}:{method}:{ip}:{bitrate}:{nclients}:{maxtime}"] + else: + cmd = ['sudo', 'python3', f"{script_path}/sendFileMcast.py", f"{param_dict['name']}.{param_dict['extension']}", f"{port}:{method}:{ip}:{bitrate}:{nclients}:{maxtime}"] + else: + return jsonify({ + "success": False, + "error": "Image not found" + }), 400 + + try: + # Ejecutamos el script "sendFileMcast.py" (con los parámetros almacenados), y almacenamos el resultado (pero sin esperar a que termine el proceso): + result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Comprobamos si está corriendo el proceso correspondiente de "udp-sender" (esperando 5 segundos para darle tiempo a iniciarse): + sleep(5) + process_running = search_process('udp-sender', f"{param_dict['name']}.{param_dict['extension']}") + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode is None and process_running == True: + return jsonify({ + "success": True, + "output": "Sending image..." + }), 200 + else: + return jsonify({ + "success": False, + "error": "Image send failed" + }), 500 + except subprocess.CalledProcessError as error: + return jsonify({ + "success": False, + "process exeption": str(error) + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 14 - Endpoint "Enviar una Imagen mediante UFTP" (ASINCRONO): +@app.route("/ogrepository/v1/uftp", methods=['POST']) +def send_uftp(): + """ Este endpoint envía mediante UFTP la imagen que recibe como primer parámetro, con los datos de transferencia que recibe en los demás parámetros. + Para ello, ejecuta el script "sendFileUFTP.py", con la imagen como primer parámetro, y los demás en una cadena (como segundo parámetro). + NOTA: Es necesario que los clientes se hayan puesto en escucha previamente (ejecutando el script "listenUFTPD.py"). + """ + # Almacenamos los parámetros enviados en el JSON: + json_data = json.loads(request.data) + image_id = json_data.get("ID_img") + port = json_data.get("port") + ip = json_data.get("ip") + bitrate = json_data.get("bitrate") + + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(image_id, "repo") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd = ['sudo', 'python3', f"{script_path}/sendFileUFTP.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}", f"{port}:{ip}:{bitrate}"] + else: + cmd = ['sudo', 'python3', f"{script_path}/sendFileUFTP.py", f"{param_dict['name']}.{param_dict['extension']}", f"{port}:{ip}:{bitrate}"] + else: + return jsonify({ + "success": False, + "error": "Image not found" + }), 400 + + try: + # Ejecutamos el script "sendFileUFTP.py" (con los parámetros almacenados), y almacenamos el resultado (pero sin esperar a que termine el proceso): + result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Comprobamos si está corriendo el proceso correspondiente de "uftp" (esperando 5 segundos para darle tiempo a iniciarse): + sleep(5) + process_running = search_process('uftp', f"{param_dict['name']}.{param_dict['extension']}") + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode is None and process_running == True: + return jsonify({ + "success": True, + "output": "Sending image..." + }), 200 + else: + return jsonify({ + "success": False, + "error": "Image send failed" + }), 500 + except subprocess.CalledProcessError as error: + return jsonify({ + "success": False, + "process exeption": str(error) + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 15 - Endpoint "Enviar una Imagen mediante P2P" (ASINCRONO): +@app.route("/ogrepository/v1/p2p", methods=['POST']) +def send_p2p(): + """ Este endpoint inicia el tracker "bttrack" y el seeder "bittornado", en el directorio en el que esté situada la imagen que recibe como parámetro. + Para ello, ejecuta los scripts "runTorrentTracker.py" y "runTorrentSeeder.py", pasándoles el subdirectorio de OU de la imagen (o "none" si no es el caso). + """ + # Almacenamos los parámetros enviados en el JSON: + json_data = json.loads(request.data) + image_id = json_data.get("ID_img") + + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(image_id, "repo") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd_tracker = ['sudo', 'python3', f"{script_path}/runTorrentTracker.py", param_dict['subdir']] + cmd_seeder = ['sudo', 'python3', f"{script_path}/runTorrentSeeder.py", param_dict['subdir']] + base_path = f"{repo_path}{param_dict['subdir']}" + else: + cmd_tracker = ['sudo', 'python3', f"{script_path}/runTorrentTracker.py", 'none'] + cmd_seeder = ['sudo', 'python3', f"{script_path}/runTorrentSeeder.py", 'none'] + base_path = repo_path.rstrip('/') # Le quito la última barra para poder buscar correctamente en los procesos + else: + return jsonify({ + "success": False, + "error": "Image not found" + }), 400 + + # Ejecutamos los scripts "runTorrentSeeder.py" y "runTorrentSeeder.py" (con los parámetros almacenados). + # NOTA: No almacenamos la salida ni comprobamos los errores, porque los procesos quedarán corriendo hasta que se finalicen manualmente, + # por lo que no podemos comprobar el returncode (luego comprobaremos si los procesos se han iniciado correctamente). + subprocess.Popen(cmd_tracker) + subprocess.Popen(cmd_seeder) + + # Comprobamos si el tracker y el seeder están corriendo, y si apuntan al directorio que le hemos pasado + # (esperamos 10 segundos antes de hacerlo, porque los procesos no se inician inmediatamente): + sleep(10) + tracker_running = search_process('bttrack', base_path) + seeder_running = search_process('btlaunchmany', base_path) + + # Evaluamos las comprobaciones anteriores, para devolver la respuesta que corresponda: + if tracker_running and seeder_running: + return jsonify({ + "success": True, + "output": "Tracker and Seeder serving image correctly" + }), 200 + else: + return jsonify({ + "success": False, + "error": "Tracker or Seeder (or both) not running" + }), 500 + + +# --------------------------------------------------------- + + +# 16 - Endpoint "Ver Estado de Transmisiones UDPcast" (SINCRONO): +@app.route("/ogrepository/v1/udpcast", methods=['GET']) +def get_udpcast_info(): + """ Este endpoint devuelve información sobre los procesos de "udp-sender" activos, en formato json, + lo que en la práctica permite comprobar las transferencias UDPcast activas. + Para ello, ejecuta el script "getUDPcastInfo.py", que no recibe parámetros. + """ + try: + # Ejecutamos el script "getUDPcastInfo.py", y almacenamos el resultado: + result = subprocess.run(['sudo', 'python3', f"{script_path}/getUDPcastInfo.py"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos la respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": json.loads(result.stdout) + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + if "exit status 1" in str(error_description): + return jsonify({ + "success": False, + "exception": "No UDPCast active transmissions" + }), 400 + else: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 17 - Endpoint "Ver Estado de Transmisiones UFTP" (SINCRONO): +@app.route("/ogrepository/v1/uftp", methods=['GET']) +def get_uftp_info(): + """ Este endpoint devuelve información sobre los procesos de "uftp" activos, en formato json, + lo que en la práctica permite comprobar las transferencias UFTP activas. + Para ello, ejecuta el script "getUFTPInfo.py", que no recibe parámetros. + """ + try: + # Ejecutamos el script "getUFTPInfo.py", y almacenamos el resultado: + result = subprocess.run(['sudo', 'python3', f"{script_path}/getUFTPInfo.py"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos la respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": json.loads(result.stdout) + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + if "exit status 1" in str(error_description): + return jsonify({ + "success": False, + "exception": "No UFTP active transmissions" + }), 400 + else: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 18 - Endpoint "Cancelar Transmisión UDPcast" (SINCRONO): +@app.route("/ogrepository/v1/udpcast/images/", methods=['DELETE']) +def stop_udpcast(imageId): + """ Este endpoint cancela la transmisión UDPcast de la imagen que recibe como parámetro, finalizando el proceso "udp-sender" asociado. + Para ello, ejecuta el script "stopUDPcast.py", pasándole el nombre de la imagen (con subdirectorio de OU, si procede). + """ + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(imageId, "repo") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd = ['sudo', 'python3', f"{script_path}/stopUDPcast.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}"] + else: + cmd = ['sudo', 'python3', f"{script_path}/stopUDPcast.py", f"{param_dict['name']}.{param_dict['extension']}"] + else: + return jsonify({ + "success": False, + "error": "Image not found" + }), 400 + + try: + # Ejecutamos el script "stopUDPcast.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "Image transmission canceled successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + if "exit status 3" in str(error_description): + return jsonify({ + "success": False, + "exception": "No UDPCast active transmissions for specified image" + }), 400 + elif "exit status 4" in str(error_description): + return jsonify({ + "success": False, + "exception": "Unexpected error checking UDPcast transmissions" + }), 500 + elif "exit status 5" in str(error_description): + return jsonify({ + "success": False, + "exception": "Unexpected error finalizing UDPcast transmission" + }), 500 + else: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 19 - Endpoint "Cancelar Transmisión UFTP" (SINCRONO): +@app.route("/ogrepository/v1/uftp/images/", methods=['DELETE']) +def stop_uftp(imageId): + """ Este endpoint cancela la transmisión UFTP de la imagen que recibe como parámetro, finalizando el proceso "uftp" asociado. + Para ello, ejecuta el script "stopUFTP.py", pasándole el nombre de la imagen (con subdirectorio de OU, si procede). + """ + # Obtenemos el nombre y la extensión de la imagen (y el subdirectorio de OU, si fuera el caso): + param_dict = get_image_params(imageId, "repo") + + # Evaluamos los parámetros obtenidos, para construir la llamada al script, o para devover un error si no se ha encontrado la imagen: + if param_dict: + if 'subdir' in param_dict: + cmd = ['sudo', 'python3', f"{script_path}/stopUFTP.py", f"{param_dict['subdir']}/{param_dict['name']}.{param_dict['extension']}"] + else: + cmd = ['sudo', 'python3', f"{script_path}/stopUFTP.py", f"{param_dict['name']}.{param_dict['extension']}"] + else: + return jsonify({ + "success": False, + "error": "Image not found" + }), 400 + + try: + # Ejecutamos el script "stopUFTP.py" (con los parámetros almacenados), y almacenamos el resultado: + result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos una respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "Image transmission canceled successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + if "exit status 3" in str(error_description): + return jsonify({ + "success": False, + "exception": "No UFTP active transmissions for specified image" + }), 400 + elif "exit status 4" in str(error_description): + return jsonify({ + "success": False, + "exception": "Unexpected error checking UFTP transmissions" + }), 500 + elif "exit status 5" in str(error_description): + return jsonify({ + "success": False, + "exception": "Unexpected error finalizing UFTP transmission" + }), 500 + else: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + +# --------------------------------------------------------- + + +# 20 - Endpoint "Cancelar Transmisiones P2P" (SINCRONO): +@app.route("/ogrepository/v1/p2p", methods=['DELETE']) +def stop_p2p(): + """ Este endpoint cancela las transmisiones P2P activas, finalizando los procesos "btlaunchmany.bittornado" (seeder) y "bttrack" (tracker). + Para ello, ejecuta el script "stopP2P.py", que no recibe parámetros. + """ + try: + # Ejecutamos el script "stopP2P.py", y almacenamos el resultado: + result = subprocess.run(['sudo', 'python3', f"{script_path}/stopP2P.py"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + + # Evaluamos el resultado de la ejecución, y devolvemos la respuesta: + if result.returncode == 0: + return jsonify({ + "success": True, + "output": "P2P transmissions canceled successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": result.stderr + }), 500 + except Exception as error_description: + return jsonify({ + "success": False, + "exception": str(error_description) + }), 500 + + + +# -------------------------------------------------------------------------------------------- + + +# Ejecutamos la aplicación, en el puerto "8006": +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=8006) + + +# -------------------------------------------------------------------------------------------- diff --git a/api/swagger.yaml b/api/swagger.yaml new file mode 100644 index 0000000..33232d4 --- /dev/null +++ b/api/swagger.yaml @@ -0,0 +1,1780 @@ +swagger: "2.0" +info: + title: "OgRepository API" + version: "1.0" + description: | + --- + + **API de ogRepository, programada en Flask**. + + Responde a peticiones HTTP mediante endpoints, que a su vez ejecutan los scripts de Python 3 almacenados en ogRepository. En el entorno real, estas peticiones HTTP se enviarán desde ogCore. + En la mayoría de los casos, transforma los parámetros recibidos para adaptarlos a los que es necesario enviar a los scripts (por ejemplo, a partir del ID de una imagen obtiene su nombre, su extensión y el subdirectorio de OU). + + --- + + Paquetes APT requeridos: + - **uftp** (se puede instalar con "sudo DEBIAN_FRONTEND=noninteractive apt install uftp -y", para que no pida la ruta predeterminada) + - **udpcast** (se puede instalar con "sudo apt install ./udpcast_20230924_amd64.deb", apuntando al paquete) + - **ctorrent** (se puede instalar con "sudo apt install ctorrent") + - **bittorrent** (se puede instalar con "sudo apt install bittorrent", pero previamente hay que añadir un repositorio de Debian) + - **bittornado** (se puede instalar con "sudo apt install bittornado", pero previamente hay que añadir un repositorio de Debian) + + Librerías Python requeridas: + - **flask** (se puede instalar con "sudo apt install python3-flask") + - **paramiko** (se puede instalar con "sudo apt install python3-paramiko") + - **psutil** (se puede instalar con "sudo apt install python3-psutil") + - **flasgger** (se puede instalar con "sudo apt install python3-flasgger") + + Para que todos los endpoints de la API 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/bin/** (aquí deben estar todos los scripts de Python, y el binario "udp-sender") + - **/opt/opengnsys/ogrepository/etc/** (aquí se guardan los archivos "repoinfo.json" y "trashinfo.json") + - **/opt/opengnsys/ogrepository/log/** (aquí se guardan los logs) + + Y también debe existir el siguiente archivo: + - **/opt/opengnsys/ogrepository/etc/ogAdmRepo.cfg** (de aquí pilla la IP de ogRepository) + + --- + +# ----------------------------------------------------------------------------------------------------------- + +# Esto hace que el Swagger se ordene por los tags (apartados), de la forma especificada: +tags: + - name: "Estado de ogRepository" + - name: "Información de Imágenes" + - name: "Eliminar y Recuperar Imágenes" + - name: "Transferencia de Imágenes (UDPcast)" + - name: "Transferencia de Imágenes (UFTP)" + - name: "Transferencia de Imágenes (P2P)" + - name: "Importar y Exportar Imágenes" + - name: "Varios" + + +# ----------------------------------------------------------------------------------------------------------- +# Apartado "Estado de ogRepository" +# ----------------------------------------------------------------------------------------------------------- + +paths: + /ogrepository/v1/status: + get: + summary: "Obtener Información de Estado de ogRepository" + description: > + Este endpoint ejecuta el script "**getRepoStatus.py**" y devuelve su salida en formato JSON, + incluyendo información sobre la CPU, memoria RAM, disco duro, servicios, y procesos específicos de ogRepository. + tags: + - "Estado de ogRepository" + responses: + "200": + description: "La información de estado se obtuvo exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: object + properties: + cpu: + type: object + properties: + used_percentage: + type: string + example: "35%" + ram: + type: object + properties: + total: + type: string + example: "7.8GB" + used: + type: string + example: "0.3GB" + available: + type: string + example: "7.2GB" + used_percentage: + type: string + example: "7%" + disk: + type: object + properties: + total: + type: string + example: "11.7GB" + used: + type: string + example: "7.7GB" + available: + type: string + example: "3.4GB" + used_percentage: + type: string + example: "69%" + services: + type: object + properties: + ssh: + type: string + example: "active" + smbd: + type: string + example: "active" + rsync: + type: string + example: "active" + processes: + type: object + properties: + udp-sender: + type: string + example: "stopped" + uftp: + type: string + example: "stopped" + bttrack: + type: string + example: "stopped" + btlaunchmany: + type: string + example: "stopped" + "500 (Error)": + description: "Error al consultar y/o devolver la información de estado." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al consultar y/o devolver la información de estado." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- +# Apartado "Información de Imágenes" +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/images: + put: + summary: "Actualizar Información del Repositorio" + description: | + Este endpoint actualiza la información de las imágenes almacenadas en el repositorio, reflejándola en los archivos "**repoinfo.json**" y "**trashinfo.json**". + Utiliza el script "**updateRepoInfo.py**", que a su vez llama al script "**updateTrashInfo.py**", para actualizar también la información de la papelera. + + No hace falta que se le llame al crear o exportar una imagen, ya que lo llama el endpoint "**Crear archivos auxiliares**" (que sí debe ser llamado en esos casos). + tags: + - "Información de Imágenes" + responses: + "200": + description: "La actualización de la información de las imágenes se realizó exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Repository info updated successfully" + "500 (Error)": + description: "Error al actualizar la información de las imágenes." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al actualizar la información de las imágenes." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + #/ogrepository/v1/images: + get: + summary: "Obtener Información de todas las Imágenes" + description: | + Este endpoint ejecuta el script "**getRepoInfo.py**" con los parámetros "**all**" y "**none**" para devolver información de todas las imágenes almacenadas en el repositorio y en la papelera, que a su vez llama al script "**updateRepoInfo.py**", para actualizar previamente la información del repositorio. + Devuelve detalles como el nombre de la imagen, tipo, nombre del cliente, clonador, compresor, sistema de archivos, tamaño de los datos, tamaño de la imagen, y hashes MD5. + tags: + - "Información de Imágenes" + responses: + "200": + description: "La información de las imágenes se obtuvo exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: object + properties: + REPOSITORY: + type: object + properties: + directory: + type: string + example: "/opt/opengnsys/ogrepository/images" + images: + type: array + items: + type: object + properties: + name: + type: string + example: "Ubuntu24" + type: + type: string + example: "img" + clientname: + type: string + example: "Ubuntu_24" + clonator: + type: string + example: "partclone" + compressor: + type: string + example: "lzop" + filesystem: + type: string + example: "EXTFS" + datasize: + type: integer + example: 9859634200000 + size: + type: integer + example: 4505673214 + sum: + type: string + example: "065a933c780ab1aaa044435ad5d4bf87" + fullsum: + type: string + example: "33575b9070e4a8043371b8c6ae52b80e" + ous: + type: array + items: + type: object + properties: + subdir: + type: string + example: "OU_subdir" + images: + type: array + items: + type: object + properties: + name: + type: string + example: "Ubuntu20" + type: + type: string + example: "img" + clientname: + type: string + example: "Ubuntu_20" + clonator: + type: string + example: "partclone" + compressor: + type: string + example: "lzop" + filesystem: + type: string + example: "EXTFS" + datasize: + type: integer + example: 8912896000000 + size: + type: integer + example: 3803794535 + sum: + type: string + example: "081a933c780ab1aaa044435ad5d4bf56" + fullsum: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + TRASH: + type: object + properties: + directory: + type: string + example: "/opt/opengnsys/ogrepository/images_trash" + images: + type: array + items: + type: object + ous: + type: array + items: + type: object + properties: + subdir: + type: string + example: "CentroVirtual" + images: + type: array + items: + type: object + properties: + name: + type: string + example: "Ubuntu20OLD" + type: + type: string + example: "img" + clientname: + type: string + example: "Ubuntu_20" + clonator: + type: string + example: "partclone" + compressor: + type: string + example: "lzop" + filesystem: + type: string + example: "EXTFS" + datasize: + type: integer + example: 8912896000000 + size: + type: integer + example: 3803794535 + sum: + type: string + example: "081a933c780ab1aaa044435ad5d4bf56" + fullsum: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + "500 (Error)": + description: "Error al consultar y/o devolver la información de las imágenes." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al consultar y/o devolver la información de las imágenes." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/images/{imageId}: + get: + summary: "Obtener Información de una Imagen concreta" + description: | + Este endpoint devuelve información de la imagen especificada mediante su ID, en formato JSON. + Utiliza el script "**getRepoInfo.py**" que recibe como parámetros el nombre de la imagen con extensión y el subdirectorio correspondiente a la OU (o "none" si no es el caso), que a su vez llama al script "**updateRepoInfo.py**", para actualizar previamente la información del repositorio. + La imagen puede estar en el archivo "**repoinfo.json**" (si está almacenada en el repositorio) o en "**trashinfo.json**" (si está en la papelera). + tags: + - "Información de Imágenes" + parameters: + - name: imageId + in: path + required: true + type: string + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum')" + responses: + "200": + description: "La información de la imagen se obtuvo exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: object + properties: + directory: + type: string + example: "/opt/opengnsys/ogrepository/images" + images: + type: array + items: + type: object + properties: + name: + type: string + example: "Windows10" + type: + type: string + example: "img" + clientname: + type: string + example: "Windows_10" + clonator: + type: string + example: "partclone" + compressor: + type: string + example: "lzop" + filesystem: + type: string + example: "NTFS" + datasize: + type: integer + example: 9859634200000 + size: + type: integer + example: 4505673214 + sum: + type: string + example: "065a933c780ab1aaa044435ad5d4bf87" + fullsum: + type: string + example: "33575b9070e4a8043371b8c6ae52b80e" + "400": + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found" + "500 (Error)": + description: "Error al consultar y/o devolver la información de la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al consultar y/o devolver la información de la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/status/images/{imageId}: + get: + summary: "Chequear Integridad de Imagen" + description: | + Este endpoint comprueba la integridad de la imagen especificada como parámetro, comparando el tamaño y el hash MD5 del último MB con los valores almacenados en los archivos ".size" y ".sum". + Utiliza el script "**checkImage.py**", que recibe el nombre de la imagen (con extensión) y el subdirectorio correspondiente a la OU, si aplica. + tags: + - "Información de Imágenes" + parameters: + - name: imageId + in: path + required: true + type: string + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum')" + responses: + "200 (Check OK)": + description: "La imagen se ha chequeado exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image file passed the Integrity Check correctly" + "200 (Check KO)": + description: "La imagen se ha chequeado exitosamente, pero no ha pasado el test de integridad." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image file didn't pass the Integrity Check" + "400": + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found (inexistent or deleted)" + "500 (Error)": + description: "Error al chequear la integridad de la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al chequear la integridad de la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- +# Apartado "Eliminar y Recuperar Imágenes" +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/images/{imageId}?method={method}: + delete: + summary: "Eliminar una Imagen" + description: | + Este endpoint elimina la imagen especificada como parámetro (y todos sus archivos asociados), moviéndolos a la papelera o eliminándolos permanentemente (dependiendo del parámetro "method"). + Utiliza el script "**deleteImage.py**" que recibe el nombre de la imagen (con extensión y subdirectorio correspondiente a la OU, si aplica) como primer parámetro, y opcionalmente el parámetro "-p" (para eliminación permanente), que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. + tags: + - "Eliminar y Recuperar Imágenes" + parameters: + - name: imageId + in: path + required: true + type: string + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum')" + - name: method + in: query + required: true + type: string + description: "Método de eliminación (puede ser 'trash', para enviar la imagen a la papelera, o 'permanent', para eliminarla definitivamente)" + responses: + "200": + description: "La imagen se eliminó exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image deleted successfully" + "400": + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found (inexistent or deleted)" + "500 (Error)": + description: "Error al eliminar la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al eliminar la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/trash/images: + post: + summary: "Recuperar una Imagen" + description: | + Este endpoint recupera la imagen especificada, moviéndola desde la papelera al repositorio de imágenes. + Utiliza el script "**recoverImage.py**", que recibe el nombre de la imagen (con extensión y subdirectorio correspondiente a la OU, si aplica), que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. + tags: + - "Eliminar y Recuperar Imágenes" + parameters: + - name: JSON + in: body + required: true + description: | + * **ID_img** - Identificador de la imagen, correspondiente al contenido del archivo 'full.sum' + schema: + type: object + properties: + ID_img: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + responses: + "200": + description: "La imagen se recuperó exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image recovered successfully" + "400": + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found (inexistent or recovered previously)" + "500 (Error)": + description: "Error al recuperar la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al recuperar la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/trash/images/{imageId}: + delete: + summary: "Eliminar una Imagen de la Papelera" + description: | + Este endpoint elimina permanentemente la imagen especificada, desde la papelera. + Utiliza el script "**deleteTrashImage.py**", que recibe el nombre de la imagen (con extensión y subdirectorio correspondiente a la OU, si aplica), que a su vez llama al script "**updateTrashInfo.py**", para actualizar la información de la papelera. + tags: + - "Eliminar y Recuperar Imágenes" + parameters: + - name: imageId + in: path + required: true + type: string + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum')" + responses: + "200": + description: "La imagen se eliminó exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image deleted successfully" + "400": + description: "No se ha encontrado la imagen especificada en la papelera." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found at trash" + "500 (Error)": + description: "Error al eliminar la imagen de la papelera." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al eliminar la imagen de la papelera." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- +# Apartado "Transferencia de Imágenes (UDPcast)" +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/udpcast: + post: + summary: "Enviar una Imagen mediante UDPcast" + description: | + Este endpoint envía la imagen especificada a través de UDPcast, utilizando el script "**sendFileMcast.py**". + Recibe la imagen y los parámetros de configuración de transferencia, que son usados para construir la cadena de parámetros que se envía al script. + + **NOTA**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo (pero no avisa a ogCore de su finalización, porque no puede comprobar cuando acaba la tarea de restauración de la imagen). + tags: + - "Transferencia de Imágenes (UDPcast)" + parameters: + - name: JSON + in: body + required: true + description: | + * **ID_img** - Identificador de la imagen, correspondiente al contenido del archivo 'full.sum' + * **port** - Puerto Multicast + * **method** - Modalidad half-duplex o full-duplex ("half" o "full") + * **ip** - IP Multicast + * **bitrate** - Velocidad de transmisión, en Mbps + * **nclients** - Número minimo de clientes + * **maxtime** - Tiempo máximo de espera + schema: + type: object + properties: + ID_img: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + port: + type: string + description: "Puerto Multicast a utilizar para la transmisión" + example: "9000" + method: + type: string + description: "Modalidad de transmisión (puede ser 'half' o 'full' para half-duplex o full-duplex)" + example: "full" + ip: + type: string + description: "IP Multicast a la que se enviará la imagen" + example: "239.194.17.2" + bitrate: + type: string + description: "Velocidad de transmisión en Mbps" + example: "70M" + nclients: + type: integer + description: "Número mínimo de clientes" + example: 20 + maxtime: + type: integer + description: "Tiempo máximo de espera en segundos" + example: 120 + responses: + "200": + description: "La imagen se está enviando mediante UDPcast." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Sending image.." + "400": + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found" + "500 (Error)": + description: "Error al enviar la imagen mediante UDPcast." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image send failed" + "500 (Exception)": + description: "Excepción inesperada al enviar la imagen mediante UDPcast." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + #/ogrepository/v1/udpcast: + get: + summary: "Ver Estado de Transmisiones UDPcast" + description: | + Este endpoint devuelve información sobre los procesos activos de "**udp-sender**" en formato JSON, permitiendo comprobar las transferencias UDPcast activas. + Utiliza el script "**getUDPcastInfo.py**" para obtener esta información. + tags: + - "Transferencia de Imágenes (UDPcast)" + responses: + "200": + description: "La información de las transmisiones UDPcast activas se obtuvo exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: object + additionalProperties: + type: object + properties: + image_id: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + image_name: + type: string + example: "Ubuntu20.img" + "400": + description: "No se han encontrado transmisiones UDPcast activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "No UDPCast active transmissions" + "500 (Error)": + description: "Error al comprobar las transmisiones UDPcast activas." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al comprobar las transmisiones UDPcast activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/udpcast/images/{imageId}: + delete: + summary: "Cancelar Transmisión UDPcast" + description: | + Este endpoint cancela la transmisión UDPcast activa de la imagen especificada, deteniendo el proceso "**udp-sender**" asociado. + Utiliza el script "**stopUDPcast.py**" para finalizar la transmisión. + tags: + - "Transferencia de Imágenes (UDPcast)" + parameters: + - name: imageId + in: path + required: true + type: string + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum')" + responses: + "200": + description: "La transmisión UDPcast se ha cancelado exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image transmission canceled successfully" + "400 (Image not found)": + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Image not found" + "400 (No transmissions for image)": + description: "No hay transmisiones UDPcast activas para la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "No UDPCast active transmissions for specified image" + "500 (Error)": + description: "Error al cancelar la transmisión UDPcast." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Check Exception)": + description: "Error al verificar las transmisiones UDPcast activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Unexpected error checking UDPcast transmissions" + "500 (Finalize Exception)": + description: "Error inesperado al finalizar la transmisión UDPcast." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Unexpected error finalizing UDPcast transmission" + "500 (General Exception)": + description: "Excepción inesperada al cancelar la transmisión UDPcast." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- +# Apartado "Transferencia de Imágenes (UFTP)" +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/uftp: + post: + summary: "Enviar una Imagen mediante UFTP" + description: | + Este endpoint envía una imagen especificada a través de UFTP, utilizando el script "**sendFileUFTP.py**". + Requiere que los clientes ogLive estén previamente en escucha con un daemon "UFTPD", ejecutando el script "**listenUFTPD.py**". + Recibe la imagen y los parámetros de configuración de transferencia, que son usados para construir la cadena de parámetros que se envía al script. + + **NOTA**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo (pero no avisa a ogCore de su finalización, porque no puede comprobar cuando acaba la tarea de restauración de la imagen). + tags: + - "Transferencia de Imágenes (UFTP)" + parameters: + - name: JSON + in: body + required: true + description: | + * **ID_img** - Identificador de la imagen, correspondiente al contenido del archivo 'full.sum' + * **port** - Puerto Multicast + * **ip** - IP Unicast/Multicast), + * **bitrate** - Velocidad de transmisión, con 'K' para Kbps, 'M' para Mbps o 'G' para Gbps + schema: + type: object + properties: + ID_img: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + port: + type: string + description: "Puerto para la transmisión UFTP" + example: "9000" + ip: + type: string + description: "IP Unicast o Multicast para la transmisión" + example: "239.194.17.2" + bitrate: + type: string + description: "Velocidad de transmisión (con 'K' para Kbps, 'M' para Mbps, o 'G' para Gbps)" + example: "1G" + responses: + "200": + description: "La imagen se está enviando mediante UFTP." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Sending image..." + "400": + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found" + "500 (Error)": + description: "Error al enviar la imagen mediante UFTP." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image send failed" + "500 (Exception)": + description: "Excepción inesperada al enviar la imagen mediante UFTP." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + #/ogrepository/v1/uftp: + get: + summary: "Ver Estado de Transmisiones UFTP" + description: | + Este endpoint devuelve información sobre los procesos activos de "**uftp**" en formato JSON, permitiendo comprobar las transferencias UFTP activas. + Utiliza el script "**getUFTPInfo.py**" para obtener esta información. + tags: + - "Transferencia de Imágenes (UFTP)" + responses: + "200": + description: "La información de las transmisiones UFTP activas se obtuvo exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: object + additionalProperties: + type: object + properties: + image_id: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + image_name: + type: string + example: "Ubuntu20.img" + "400": + description: "No se han encontrado transmisiones UFTP activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "No UFTP active transmissions" + "500 (Error)": + description: "Error al comprobar las transmisiones UFTP activas." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al comprobar las transmisiones UFTP activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/uftp/images/{imageId}: + delete: + summary: "Cancelar Transmisión UFTP" + description: | + Este endpoint cancela la transmisión UFTP activa de una imagen especificada, deteniendo el proceso "**uftp**" asociado. + Utiliza el script "**stopUFTP.py**" para finalizar la transmisión. + tags: + - "Transferencia de Imágenes (UFTP)" + parameters: + - name: imageId + in: path + required: true + type: string + description: "Identificador de la imagen (correspondiente al contenido del archivo 'full.sum')" + responses: + "200": + description: "La transmisión UFTP se ha cancelado exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Image transmission canceled successfully" + "400 (Image not found)": + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Image not found" + "400 (No transmissions for image)": + description: "No hay transmisiones UFTP activas para la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "No UFTP active transmissions for specified image" + "500 (Error)": + description: "Error al cancelar la transmisión UFTP." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Check Exception)": + description: "Error al verificar las transmisiones UFTP activas." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Unexpected error checking UFTP transmissions" + "500 (Finalize Exception)": + description: "Error inesperado al finalizar la transmisión UFTP." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Unexpected error finalizing UFTP transmission" + "500 (General Exception)": + description: "Excepción inesperada al cancelar la transmisión UFTP." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- +# Apartado "Transferencia de Imágenes (P2P)" +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/p2p: + post: + summary: "Enviar una Imagen mediante P2P" + description: | + Este endpoint inicia el tracker y el seeder de torrents para enviar una imagen especificada mediante P2P. + Utiliza los scripts "**runTorrentTracker.py**" y "**runTorrentSeeder.py**" para iniciar el tracker y el seeder en el directorio de la imagen. + + **NOTA**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está enviando, y abre un proceso paralelo (pero no avisa a ogCore de su finalización, porque no puede comprobar cuando acaba la tarea de restauración de la imagen). + tags: + - "Transferencia de Imágenes (P2P)" + parameters: + - name: JSON + in: body + required: true + description: | + * **ID_img** - Identificador de la imagen, correspondiente al contenido del archivo 'full.sum' + schema: + type: object + properties: + ID_img: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + responses: + "200": + description: "La imagen se está enviando mediante P2P." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Tracker and Seeder serving image correctly" + "400": + description: "No se ha encontrado la imagen especificada." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image not found" + "500": + description: "Error al enviar la imagen mediante P2P." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Tracker or Seeder (or both) not running" + +# ----------------------------------------------------------------------------------------------------------- + + #/ogrepository/v1/p2p: + delete: + summary: "Cancelar Transmisiones P2P" + description: | + Este endpoint cancela todas las transmisiones P2P activas, finalizando los procesos "**bttrack**" (tracker) y "**btlaunchmany.bittornado**" (seeder). + Utiliza el script "**stopP2P.py**" para detener las transmisiones P2P activas en el servidor. + tags: + - "Transferencia de Imágenes (P2P)" + responses: + "200": + description: "Las transmisiones P2P se han cancelado exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "P2P transmissions canceled successfully" + "500 (Error)": + description: "Error al cancelar las transmisiones P2P." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al cancelar las transmisiones P2P." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- +# Apartado "Importar y Exportar Imágenes" +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/repo/images: + post: + summary: "Importar una Imagen" + description: | + Este endpoint importa la imagen especificada desde un servidor remoto al servidor local. + Utiliza el script "**importImage.py**", que recibe como parámetros el nombre de la imagen, la IP o hostname del servidor remoto, el usuario para la conexión, y el subdirectorio correspondiente a la OU (si aplica), + que a su vez llama al script "**updateRepoInfo.py**", para actualizar la información del repositorio. + + **NOTA**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está importando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + tags: + - "Importar y Exportar Imágenes" + parameters: + - name: JSON + in: body + required: true + description: | + * **image** - Nombre de la imagen, con extensión + * **ou_subdir** - Subdirectorio correspondiente a la OU, o 'none' si no procede + * **repo_ip** - Dirección IP del servidor remoto + * **user** - Usuario con el que conectar al servidor remoto + schema: + type: object + properties: + image: + type: string + example: "Windows10.img" + ou_subdir: + type: string + description: "Subdirectorio correspondiente a la OU (o 'none' si no es el caso)" + example: "none" + repo_ip: + type: string + description: "Dirección IP del repositorio remoto" + example: "192.168.56.100" + user: + type: string + description: "Usuario para acceder al repositorio remoto" + example: "user_name" + responses: + "200": + description: "La imagen se está importando." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Importing image..." + "400 (Connection fail)": + description: "Error de conexión con el servidor remoto." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Can't connect to remote server" + "400 (Image not found)": + description: "No se ha encontrado la imagen remota." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Remote image not found" + "400 (Image locked)": + description: "La imagen remota está bloqueada." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Remote image is locked" + "500": + description: "Error al importar la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Image import failed" + "500 (Error)": + description: "Error al importar la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al importar la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + #/ogrepository/v1/repo/images: + put: + summary: "Exportar una Imagen" + description: | + Este endpoint exporta la imagen especificada desde el servidor local a un servidor remoto. + Utiliza el script "**exportImage.py**", que recibe como parámetros el nombre de la imagen, la IP o hostname del servidor remoto, el usuario para la conexión, y el subdirectorio correspondiente a la OU (si aplica). + + Una vez que acabe, debe llamarse al endpoint "**Actualizar Información del Repositorio**" en el ogRepository destino de la exportación, para actualizar la información del repositorio. + + **NOTA**: Este endpoint es asíncrono, ya que puede tardar mucho tiempo, por lo que solo informa de que la imagen se está exportando, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + tags: + - "Importar y Exportar Imágenes" + parameters: + - name: JSON + in: body + required: true + description: | + * **ID_img** - Identificador de la imagen, correspondiente al contenido del archivo 'full.sum' + * **repo_ip** - Dirección IP del servidor remoto + * **user** - Usuario con el que conectar al servidor remoto + schema: + type: object + properties: + ID_img: + type: string + example: "22735b9070e4a8043371b8c6ae52b90d" + repo_ip: + type: string + description: "Dirección IP del repositorio remoto" + example: "192.168.56.100" + user: + type: string + description: "Usuario para acceder al repositorio remoto" + example: "user_name" + responses: + "200": + description: "La imagen se está exportando." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Exporting image..." + "400 (Image not found)": + description: "No se ha encontrado la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + 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" + "400 (Connection fail)": + description: "Error de conexión con el servidor remoto." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Can't connect to remote server" + "400 (Image present)": + description: "La imagen ya existe en el servidor remoto." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "Image already exists on remote server" + "500": + description: "Error al exportar la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Export image failed" + "500 (Error)": + description: "Error al exportar la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al exportar la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- +# Apartado "Varios" +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/images/torrentsum: + post: + summary: "Crear archivos auxiliares" + description: | + Este endpoint crea los archivos "**size**", "**sum**", "**full.sum**" y "**torrent**" para la imagen especificada. + Utiliza el script "**createTorrentSum.py**", que recibe como parámetro el nombre de la imagen (con subdirectorio de OU, si aplica), que a su vez llama al script "**updateRepoInfo.py**, para actualizar la información del repositorio". + + Debe ser llamado cada vez que se cree una imagen desde un ogLive, y cada vez que se llame al endpoint "**Exportar una Imagen**" (en este último caso, debe ejecutarse en el ogRepository destino de la exportación). + + **NOTA**: Este endpoint es asíncrono, ya que puede tardar cierto tiempo, por lo que solo informa de que se están creando los archivos auxiliares, y abre un proceso paralelo, que avisará a ogCore cuando finalice la tarea (llamando a un endpoint de ogCore). + tags: + - "Varios" + parameters: + - name: JSON + in: body + required: true + description: | + * **image** - Nombre de la imagen, con extensión + * **ou_subdir** - Subdirectorio correspondiente a la OU, o 'none' si no procede + schema: + type: object + properties: + image: + type: string + example: "Windows10.img" + ou_subdir: + type: string + example: "none" + responses: + "200": + description: "Los archivos auxiliares se están creando." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Creating auxiliar files..." + "400 (Image not found)": + description: "No se ha encontrado la imagen." + schema: + type: object + properties: + success: + type: boolean + example: false + 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: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al crear los archivos auxiliares." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- + + /ogrepository/v1/wol: + post: + summary: "Enviar paquete Wake On Lan" + description: | + Este endpoint envía un paquete mágico Wake On Lan (**WOL**) a la dirección MAC especificada, a través de la IP de broadcast especificada. + Utiliza el script "**sendWakeOnLan.py**", que recibe como parámetros la IP de broadcast y la dirección MAC del equipo a encender. + tags: + - "Varios" + parameters: + - name: JSON + in: body + required: true + description: | + * **broadcast_ip** - IP de broadcast a la que se enviará el paquete WOL, que puede ser '255.255.255.255' o la IP de broadcast de una subred + * **mac** - Dirección MAC del equipo que se desea encender via Wake On Lan + schema: + type: object + properties: + broadcast_ip: + type: string + example: "255.255.255.255" + mac: + type: string + example: "00:19:99:5c:bb:bb" + responses: + "200": + description: "El paquete Wake On Lan se ha enviado exitosamente." + schema: + type: object + properties: + success: + type: boolean + example: true + output: + type: string + example: "Wake On Lan packet sent successfully" + "500 (Error)": + description: "Error al enviar el paquete Wake On Lan." + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "(Error description)" + "500 (Exception)": + description: "Excepción inesperada al enviar el paquete Wake On Lan." + schema: + type: object + properties: + success: + type: boolean + example: false + exception: + type: string + example: "(Exception description)" + +# ----------------------------------------------------------------------------------------------------------- diff --git a/bin/checkrepo b/bin/OLD/checkrepo similarity index 100% rename from bin/checkrepo rename to bin/OLD/checkrepo diff --git a/bin/createfileimage b/bin/OLD/createfileimage similarity index 100% rename from bin/createfileimage rename to bin/OLD/createfileimage diff --git a/bin/deleteimage b/bin/OLD/deleteimage similarity index 100% rename from bin/deleteimage rename to bin/OLD/deleteimage diff --git a/bin/deletepreimage b/bin/OLD/deletepreimage similarity index 100% rename from bin/deletepreimage rename to bin/OLD/deletepreimage diff --git a/bin/getRepoIface b/bin/OLD/getRepoIface similarity index 100% rename from bin/getRepoIface rename to bin/OLD/getRepoIface diff --git a/bin/importimage b/bin/OLD/importimage similarity index 100% rename from bin/importimage rename to bin/OLD/importimage diff --git a/bin/mountimage b/bin/OLD/mountimage similarity index 100% rename from bin/mountimage rename to bin/OLD/mountimage diff --git a/bin/partclone2sync b/bin/OLD/partclone2sync similarity index 100% rename from bin/partclone2sync rename to bin/OLD/partclone2sync diff --git a/bin/reduceimage b/bin/OLD/reduceimage similarity index 100% rename from bin/reduceimage rename to bin/OLD/reduceimage diff --git a/bin/sendFileMcast b/bin/OLD/sendFileMcast similarity index 100% rename from bin/sendFileMcast rename to bin/OLD/sendFileMcast diff --git a/bin/torrent-creator b/bin/OLD/torrent-creator similarity index 100% rename from bin/torrent-creator rename to bin/OLD/torrent-creator diff --git a/bin/OLD/torrent-tracker b/bin/OLD/torrent-tracker new file mode 100644 index 0000000..36f8b19 --- /dev/null +++ b/bin/OLD/torrent-tracker @@ -0,0 +1,21 @@ +#!/bin/bash +BTTRACK=/usr/bin/bttrack.bittorrent +BTSEEDER=/usr/bin/btlaunchmany.bittornado +BTTRACKPORT=6969 +BTTRACKDFILE=/tmp/dstate +BTTRACKLOG=/opt/opengnsys/log/bttrack.log +BTINTERVAL=10 +BTTORRENTSDIR=/opt/opengnsys/images +# Desactivar descarga de torrents desde clientes no autorizados. +BTALLOW_GET=0 +# parametros basados en EAC 05-04-2009 antonio doblas viso. +BTTRACK_OPTIONS=" --save_dfile_interval $BTINTERVAL --timeout_downloaders_interval $BTINTERVAL --port $BTTRACKPORT --dfile $BTTRACKDFILE --reannounce_interval $BTINTERVAL --logfile $BTTRACKLOG --allowed_dir $BTTORRENTSDIR --allow_get $BTALLOW_GET " +BTTRACKPID="/var/run/bttrack.pid" +BTSEEDERPID="/var/run/btseeder.pid" + +################### ####################################### + +pkill bttrack +rm -f $BTTRACKDFILE +sleep 2 +bttrack $BTTRACK_OPTIONS &>> $BTTRACKLOG & diff --git a/bin/torrent-tracker b/bin/OLD/torrent-tracker_OLD similarity index 100% rename from bin/torrent-tracker rename to bin/OLD/torrent-tracker_OLD diff --git a/bin/OLD/udp-sender_20120424 b/bin/OLD/udp-sender_20120424 new file mode 100644 index 0000000..87789a0 Binary files /dev/null and b/bin/OLD/udp-sender_20120424 differ diff --git a/bin/unmountimage b/bin/OLD/unmountimage similarity index 100% rename from bin/unmountimage rename to bin/OLD/unmountimage diff --git a/bin/checkImage.py b/bin/checkImage.py new file mode 100644 index 0000000..e97867d --- /dev/null +++ b/bin/checkImage.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script comprueba la integridad de la imagen que recibe como parámetro, volviendo a calcular el tamaño del archivo y el hash MD5 del último MB, + y comparándolos con los valores almacenados en el archivo ".size" y en el archivo ".sum", respectivamente. + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a chequear (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images/ou_subdir/image1.img + + Sintaxis +---------- +./checkImage.py [ou_subdir/]image_name|/image_path/image_name + + Ejemplos + --------- +./checkImage.py image1.img +./checkImage.py /opt/opengnsys/ogrepository/images/image1.img +./checkImage.py ou_subdir/image1.img +./checkImage.py /ou_subdir/image1.img +./checkImage.py /opt/opengnsys/ogrepository/images/ou_subdir/image1.img +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import hashlib + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name + Ejemplo1: {script_name} image1.img + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images/image1.img + Ejemplo3: {script_name} ou_subdir/image1.img + Ejemplo4: {script_name} /ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images/ou_subdir/image1.img + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 1 parámetro, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 2: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 parámetro") + show_help() + sys.exit(1) + + +def build_file_path(): + """ Construye la ruta completa del archivo a chequear + (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "repo_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(repo_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(repo_path): + file_path = os.path.join(repo_path, param_path) + else: + file_path = param_path + return file_path + + +def get_md5_sum(file_path, megabytes=1): + """ Calcula y retorna el hash MD5 del último MB del archivo de imagen que recibe como parámetro. + Se utiliza para comprobar el valor del archivo ".sum". + """ + hash_md5 = hashlib.md5() + with open(file_path, "rb") as f: + f.seek(-megabytes * 1024 * 1024, os.SEEK_END) + data = f.read(megabytes * 1024 * 1024) + hash_md5.update(data) + return hash_md5.hexdigest() + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo a chequear: + file_path = build_file_path() + + # Si no existe el archivo de imagen, imprimimos un mensaje de error y salimos del script: + if not os.path.exists(file_path): + print("Image file doesn't exist") + sys.exit(2) + + # Comprobamos si existe el archivo ".sum", y si su contenido coincide con el valor real: + if os.path.exists(f"{file_path}.sum"): + sumOK = False + actual_datasum = get_md5_sum(file_path) + with open(f"{file_path}.sum", 'r') as file: + file_datasum = file.read().strip('\n') + if actual_datasum == file_datasum: + sumOK = True + else: + print("Sum file doesn't exist") + sys.exit(3) + + # Comprobamos si existe el archivo ".size", y si su contenido coincide con el valor real: + if os.path.exists(f"{file_path}.size"): + sizeOK = False + actual_size = os.path.getsize(file_path) + with open(f"{file_path}.size", 'r') as file: + file_datasize = int(file.read().strip('\n')) + if actual_size == file_datasize: + sizeOK = True + else: + print("Size file doesn't exist") + sys.exit(4) + + # Evaluamos los resultados, e imprimimos un mensaje "OK" o "KO": + if sumOK == True and sizeOK == True: + print("Image file passed the Integrity Check correctly") + else: + print("Error: Image file didn't pass the Integrity Check") + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/clients/listenUFTPD.py b/bin/clients/listenUFTPD.py new file mode 100644 index 0000000..f20d477 --- /dev/null +++ b/bin/clients/listenUFTPD.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script hace que el cliente se ponga a escuchar en el puerto e IP Multicast especificados en el único parámetro (cuya sintaxis es "Port:IP"), con un proceso "uftpd". +Posteriormente, el servidor puede hacer una transferencia UFTP a la IP Multicast, o directamente a la IP de un cliente ogLive. +NOTA: La imagen se enviará a la ruta de la caché de los clientes (que actualmente es "/opt/opengnsys/cache"). + +Paquetes APT requeridos: "uftp" (se puede instalar con "sudo apt install uftp"). + + Parámetros +------------ +sys.argv[1] - Parámetros Multicast (en formato "Port:IP") + - Ejemplo: 9000:239.194.17.2 + + Sintaxis +---------- +./listenUFTPD.py Port:IP + + Ejemplo +--------- +./listenUFTPD.py 9000:239.194.17.2 +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +cache_path = '/opt/opengnsys/cache' +log_file = '/tmp/uftpd.log' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} Port:IP + Ejemplo: {script_name} 9000:239.194.17.2 + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta la función con más o menos de 1 parámetro, se muestra un mensaje de error, y se sale del script: + elif len(sys.argv) != 2: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 parámetro (port:ip)") + sys.exit(1) + # Si en el parámetro no hay 2 elementos (separados por ":"), se muestra un mensaje de error, y se sale del script: + param_list = sys.argv[1].split(':') + if len(param_list) != 2: + print(f"{script_name} Error: Datos Multicast incorrectos: \"{sys.argv[1]}\" (se debe especificar \"puerto:ip\")") + sys.exit(2) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Almacenamos los elementos del parámetro en variables (su formato es "puerto:ip"): + param_list = sys.argv[1].split(':') + port, ip = param_list + + # Creamos una lista con el comando a enviar (esto es requerido por la función "subprocess.run"), e impimimos el comando con espacios: + splitted_cmd = f"uftpd -M {ip} -p {port} -L {log_file} -D {cache_path} -E -K rsa:1024".split() + + print(f"Sending command: {' '.join(splitted_cmd)}") + + # Ejecutamos el comando en el sistema, e imprimimos el resultado: + try: + result = subprocess.run(splitted_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"ReturnCode: {result.returncode}") + except subprocess.CalledProcessError as error: + print(f"ReturnCode: {error.returncode}") + print(f"Error Output: {error.stderr.decode()}") + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/createTorrentSum.py b/bin/createTorrentSum.py new file mode 100644 index 0000000..83d2a0e --- /dev/null +++ b/bin/createTorrentSum.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script crea los archivos ".size", ".sum", ".full.sum" y ".torrent" para la imagen que recibe como parámetro. +Hace casi lo mismo que el script bash original (cuyo nombre es "torrent-creator"), pero añade el archivo ".size". +Al acabar, llama al script "updateRepoInfo.py", para actualizar la información del repositorio. + +Debería ser llamado por ogCore u ogLive cada vez que se cree una imagen. + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images/ou_subdir/image1.img + + Sintaxis +---------- +./createTorrentSum.py [ou_subdir/]image_name|/image_path/image_name + + Ejemplos + --------- +./createTorrentSum.py image1.img +./createTorrentSum.py /opt/opengnsys/ogrepository/images/image1.img +./createTorrentSum.py ou_subdir/image1.img +./createTorrentSum.py /ou_subdir/image1.img +./createTorrentSum.py /opt/opengnsys/ogrepository/images/ou_subdir/image1.img +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess +import hashlib + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final +config_file = '/opt/opengnsys/ogrepository/etc/ogAdmRepo.cfg' +update_repo_script = '/opt/opengnsys/ogrepository/bin/updateRepoInfo.py' + + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name + Ejemplo1: {script_name} image1.img + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images/image1.img + Ejemplo3: {script_name} ou_subdir/image1.img + Ejemplo4: {script_name} /ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images/ou_subdir/image1.img + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 1 parámetro, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 2: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 parámetro") + show_help() + sys.exit(1) + + +def build_file_path(): + """ Construye la ruta completa al archivo de imagen + (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "repo_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(repo_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(repo_path): + file_path = os.path.join(repo_path, param_path) + else: + file_path = param_path + return file_path + + +def get_md5_sum(file_path, megabytes=1): + """ Calcula y retorna el hash MD5 del último MB del archivo de imagen que recibe como parámetro. + Se utiliza para crear el archivo ".sum" (para transferencias Unicast y Multicast). + """ + hash_md5 = hashlib.md5() + with open(file_path, "rb") as f: + f.seek(-megabytes * 1024 * 1024, os.SEEK_END) + data = f.read(megabytes * 1024 * 1024) + hash_md5.update(data) + return hash_md5.hexdigest() + + +def get_md5_fullsum(file_path): + """ Calcula y retorna el hash MD5 del archivo de imagen que recibe como parámetro. + Se utiliza para crear el archivo ".full.sum" (para transferencias P2P). + """ + hash_md5 = hashlib.md5() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() + + +def get_IPlocal(): + """ Retorna la IP asociada a la variable "IPlocal", del archivo '/opt/opengnsys/etc/ogAdmRepo.cfg' + (que corresponde a la IP del repositorio). + """ + with open(config_file, 'r') as file: + for line in file: + if line.startswith('IPlocal'): + IPlocal = line.split('=')[1].strip() + return IPlocal + + +def create_torrent(file_path, torrent_file, datafullsum): + """ Crea un archivo ".torrent" para la imagen que recibe como primer parámetro. + Obtiene la IP del repositorio llamando a la función "get_IPlocal", + que a su vez la obtiene del archivo '/opt/opengnsys/etc/ogAdmRepo.cfg'. + """ + # Almacenamos la IP del repositorio, y construimos la URL del tracker: + repo_ip = get_IPlocal() + tracker_url = f"http://{repo_ip}:6969/announce" + + # Creamos una lista con el comando para crear el torrrent, y lo imprimimos con espacios: + splitted_cmd = f"nice -n 0 ctorrent -t {file_path} -u {tracker_url} -s {torrent_file} -c {datafullsum} -l 4194304".split() + print(f"Sending command: {' '.join(splitted_cmd)}") + + # Ejecutamos el comando en el sistema, e imprimimos el resultado: + try: + result = subprocess.run(splitted_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"ReturnCode: {result.returncode}") + except subprocess.CalledProcessError as error: + print(f"ReturnCode: {error.returncode}") + print(f"Error Output: {error.stderr.decode()}") + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + + +def update_repo_info(): + """ Actualiza la información del repositorio, ejecutando el script "updateRepoInfo.py". + Como se ve, es necesario que el script se ejecute como sudo, o dará error. + """ + try: + result = subprocess.run(['sudo', 'python3', update_repo_script], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except subprocess.CalledProcessError as error: + print(f"Error Output: {error.stderr.decode()}") + sys.exit(3) + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + sys.exit(4) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa de la imagen: + file_path = build_file_path() + + # Si no existe el archivo de imagen, imprimimos un mensaje de error y salimos del script: + if not os.path.exists(file_path): + print("Image file doesn't exist") + sys.exit(2) + + # Si la imagen está bloqueada, imprimimos un mensaje de error y salimos del script: + if os.path.exists(f"{file_path}.lock"): + print("Image is locked") + sys.exit(3) + + # Creamos un archivo de bloqueo: + open(f"{file_path}.lock", "w").close() + + # Construimos las rutas completas de los archivos ".size", ".sum", ".full.sum" y ".torrent": + size_file = f"{file_path}.size" + sum_file = f"{file_path}.sum" + fullsum_file = f"{file_path}.full.sum" + torrent_file = f"{file_path}.torrent" + + # Creamos el archivo ".size" (pque almacenará el tamaño del archivo), siempre que no exista: + if not os.path.exists(size_file): + print("Creating '.size' file...") + with open(size_file, 'w') as file: + datasize = os.path.getsize(file_path) + file.write(str(datasize)) + else: + print("Size file exists") + + # Creamos el archivo ".sum" (para transferencias Unicast y Multicast), siempre que no exista: + if not os.path.exists(sum_file): + print("Creating '.sum' file...") + with open(sum_file, 'w') as file: + datasum = get_md5_sum(file_path) + file.write(datasum) + else: + print("Sum file exists") + + # Creamos el archivo ".full.sum" (para transferencias P2P), siempre que no exista: + if not os.path.exists(fullsum_file): + print("Creating '.ful.sum' file...") + with open(fullsum_file, 'w') as file: + datafullsum = get_md5_fullsum(file_path) + file.write(datafullsum) + else: + print("Fullsum file exists") + + # Creamos el archivo ".torrent" (siempre que no exista): + if not os.path.exists(torrent_file): + create_torrent(file_path, torrent_file, datafullsum) + else: + print("Torrent file exists") + + # Eliminamos el archivo de bloqueo: + os.remove(f"{file_path}.lock") + + # Actualizamos la información del repositorio, ejecutando el script "updateRepoInfo.py": + print("Updating Repository Info...") + update_repo_info() + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/deleteImage.py b/bin/deleteImage.py new file mode 100644 index 0000000..15ca10c --- /dev/null +++ b/bin/deleteImage.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script elimina la imagen que recibe como parámetro (y todos sus archivos asociados), moviendo los archivos a la papelera + (respetando el subdirectorio correspondiente a la OU, si fuera el caso), o eliminándolos permanentemente si se especifica el parámetro "-p". +Es similar al script bash original (cuyo nombre es "deleteimage", a secas), pero este no incluía la funcionalidad papelera. +Llama al script "updateRepoInfo.py", para actualizar la información del repositorio. + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a eliminar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images/ou_subdir/image1.img + +sys.argv[2] - Parámetro opcional para especificar que la eliminación sea permanente (sin papelera). + - Ejemplo: -p + + Sintaxis +---------- +./deleteImage.py [ou_subdir/]image_name|/image_path/image_name [-p] + + Ejemplos + --------- +./deleteImage.py image1.img -p +./deleteImage.py /opt/opengnsys/ogrepository/images/image1.img -p +./deleteImage.py ou_subdir/image1.img -p +./deleteImage.py /ou_subdir/image1.img +./deleteImage.py /opt/opengnsys/ogrepository/images/ou_subdir/image1.img +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import shutil +import pwd +import grp +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final +trash_path = '/opt/opengnsys/ogrepository/images_trash/' +update_repo_script = '/opt/opengnsys/ogrepository/bin/updateRepoInfo.py' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name [-p] + Ejemplo1: {script_name} image1.img -p + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images/image1.img -p + Ejemplo3: {script_name} ou_subdir/image1.img -p + Ejemplo4: {script_name} /ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images/ou_subdir/image1.img + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script sin parámetros, se muestra un error y la ayuda, y se sale del script: + if len(sys.argv) == 1: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar al menos 1 parámetro") + show_help() + sys.exit(0) + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + elif len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(1) + # Si se ejecuta el script con el parámetro "help" y más parámetros, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) > 2 and sys.argv[1] == "help": + print(f"{script_name} Error: Formato incorrecto: Para invocar la ayuda, se debe especificar 'help' como único parámetro") + show_help() + sys.exit(2) + # Si se ejecuta el script con 2 parámetros y el segundo es diferente de "-p", se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) == 3 and sys.argv[2] != "-p": + print(f"{script_name} Error: Formato incorrecto: El segundo parámetro solo puede ser '-p'") + show_help() + sys.exit(3) + # Si se ejecuta el script con más de 2 parámetros, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) > 3: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 o 2 parámetros") + show_help() + sys.exit(4) + + +def build_file_path(): + """ Construye la ruta completa al archivo a eliminar + (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "repo_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(repo_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(repo_path): + file_path = os.path.join(repo_path, param_path) + else: + file_path = param_path + return file_path + + +def create_trash_folder(): + """ Crea el directorio correspondiente a la papelera, y le asigna propietarios y permisos. + Evidentemente, esta función solo es llamada cuando no existe el directorio. + """ + # Obtenemos el UID del usuario "root" y el GID del grupo "opengnsys": + uid = pwd.getpwnam('root').pw_uid + gid = grp.getgrnam('opengnsys').gr_gid + # Creamos el directorio correspondiente a la papelera: + os.mkdir(trash_path) + # Asignamos el usuario y el grupo propietarios del directorio: + os.chown(trash_path, uid, gid) + # Asignamos permisos "775" al directorio : + os.chmod(trash_path, 0o775) + + +def delete_normal_image(file_path, method, extensions): + """ Elimina la imagen "normal" que recibe en el parámetro "file_path", y todos sus archivos asociados, + moviéndolos a la papelera o eliminándolos permanentemente (en función del parámetro "method"). + """ + # Iteramos las extensiones de los archivos, y construimos la ruta completa de cada uno de ellos: + for ext in extensions: + file_to_remove = f"{file_path}{ext}" + # Si el archivo actual existe, lo eliminamos o lo movemos a la papelera + # (dependiendo del valor del parámetro "method"): + if os.path.exists(file_to_remove): + if method == 'trash': + shutil.move(file_to_remove, trash_path) + elif method == 'permanent': + os.remove(file_to_remove) + + +def delete_ou_image(file_path, method, extensions): + """ Elimina la imagen basada en OU que recibe en el parámetro "file_path", y todos sus archivos asociados, + moviéndolos a la papelera o eliminándolos permanentemente (en función del parámetro "method"). + """ + # Iteramos las extensiones de los archivos, y construimos la ruta completa de cada uno de ellos: + for ext in extensions: + file_to_remove = f"{file_path}{ext}" + # Si el archivo actual existe, lo eliminamos o lo movemos a la papelera (dependiendo del valor del parámetro "method"), + # y en el último caso lo situamos en un subdirectorio correspondiente a la OU: + if os.path.exists(file_to_remove): + if method == 'trash': + ou_subdir = file_to_remove.split('/')[4] + # Comprobamos si en la papelera existe un subdirectorio correspondiente a la OU (y si no existe lo creamos): + if not os.path.exists(f"{trash_path}{ou_subdir}"): + os.mkdir(f"{trash_path}{ou_subdir}") + shutil.move(file_to_remove, f"{trash_path}{ou_subdir}") + elif method == 'permanent': + os.remove(file_to_remove) + + +def update_repo_info(): + """ Actualiza la información del repositorio, ejecutando el script "updateRepoInfo.py". + Como se ve, es necesario que el script se ejecute como sudo, o dará error. + """ + try: + result = subprocess.run(['sudo', 'python3', update_repo_script], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except subprocess.CalledProcessError as error: + print(f"Error Output: {error.stderr.decode()}") + sys.exit(3) + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + sys.exit(4) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo a eliminar: + file_path = build_file_path() + + # Si no existe el archivo de imagen, imprimimos un mensaje de error y salimos del script: + if not os.path.exists(file_path): + print("Image file doesn't exist") + sys.exit(5) + + # Comprobamos si existe el directorio correspondiente a la papelera, y en caso contrario lo creamos: + if not os.path.exists(trash_path): + print("Creating trash folder...") + create_trash_folder() + + # Especificamos el método de eliminación (permanente o utilizando la papelera): + if len(sys.argv) == 3 and sys.argv[2] == "-p": + method = 'permanent' + else: + method = 'trash' + + # Creamos una lista con las extensiones de los archivos asociados a la imagen + # (incluyendo ninguna extensión, que corresponde a la propia imagen): + extensions = ['', '.size', '.sum', '.full.sum', '.torrent', '.info', '.info.checked'] + + # Evaluamos la cantidad de barras que hay en la ruta de la imagen, para diferenciar entre imágenes "normales" y basadas en OU + # (y llamamos a la función correspondiente para eliminarla): + if file_path.count('/') == 5: + print("Deleting normal image...") + delete_normal_image(file_path, method, extensions) + elif file_path.count('/') == 6: + print("Deleting OU based image...") + delete_ou_image(file_path, method, extensions) + + # Actualizamos la información del repositorio, ejecutando el script "updateRepoInfo.py": + print("Updating Repository Info...") + update_repo_info() + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/deleteTrashImage.py b/bin/deleteTrashImage.py new file mode 100644 index 0000000..8f9a4bb --- /dev/null +++ b/bin/deleteTrashImage.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script elimina permanentemente la imagen que recibe como parámetro (y todos sus archivos asociados), desde la papelera. +Llama al script "updateTrashInfo.py", para actualizar la información de las imágenes de la papelera. + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a eliminar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images_trash/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images_trash/ou_subdir/image1.img + + Sintaxis +---------- +./deleteTrashImage.py [ou_subdir/]image_name|/image_path/image_name + + Ejemplos + --------- +./deleteTrashImage.py image1.img +./deleteTrashImage.py /opt/opengnsys/ogrepository/images_trash/image1.img +./deleteTrashImage.py ou_subdir/image1.img +./deleteTrashImage.py /ou_subdir/image1.img +./deleteTrashImage.py /opt/opengnsys/ogrepository/images_trash/ou_subdir/image1.img +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +trash_path = '/opt/opengnsys/ogrepository/images_trash/' # No borrar la barra final +update_trash_script = '/opt/opengnsys/ogrepository/bin/updateTrashInfo.py' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name + Ejemplo1: {script_name} image1.img + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images_trash/image1.img + Ejemplo3: {script_name} ou_subdir/image1.img + Ejemplo4: {script_name} /ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images_trash/ou_subdir/image1.img + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 1 parámetro, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 2: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 parámetro") + show_help() + sys.exit(1) + + +def build_file_path(): + """ Construye la ruta completa al archivo a eliminar + (agregando "/opt/opengnsys/images_trash/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "trash_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(trash_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(trash_path): + file_path = os.path.join(trash_path, param_path) + else: + file_path = param_path + return file_path + + +def delete_image(file_path, extensions): + """ Elimina permanentemente la imagen que recibe en el parámetro "file_path", y todos sus archivos asociados. + """ + # Iteramos las extensiones de los archivos, y construimos la ruta completa de cada uno de ellos: + for ext in extensions: + file_to_remove = f"{file_path}{ext}" + # Si el archivo actual existe, lo eliminamos permanentemente: + if os.path.exists(file_to_remove): + os.remove(file_to_remove) + + +def update_trash_info(): + """ Actualiza la información de la papelera, ejecutando el script "updateTrashInfo.py". + Como se ve, es necesario que el script se ejecute como sudo, o dará error. + """ + try: + result = subprocess.run(['sudo', 'python3', update_trash_script], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except subprocess.CalledProcessError as error: + print(f"Error Output: {error.stderr.decode()}") + sys.exit(3) + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + sys.exit(4) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo a eliminar: + file_path = build_file_path() + + # Si no existe el archivo de imagen, imprimimos un mensaje de error y salimos del script: + if not os.path.exists(file_path): + print("Image file doesn't exist") + sys.exit(2) + + # Creamos una lista con las extensiones de los archivos asociados a la imagen + # (incluyendo ninguna extensión, que corresponde a la propia imagen): + extensions = ['', '.size', '.sum', '.full.sum', '.torrent', '.info', '.info.checked'] + + # Eliminamos la imagen y sus archivos asociados: + delete_image(file_path, extensions) + + # Actualizamos la información de la papelera, ejecutando el script "updateTrashInfo.py": + print("Updating Trash Info...") + update_trash_info() + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/exportImage.py b/bin/exportImage.py new file mode 100644 index 0000000..ddbbb36 --- /dev/null +++ b/bin/exportImage.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script exporta la imagen especificada como primer parámetro (y sus archivos asociados), al repositorio remoto especificado como segundo parámetro, + con las credenciales del usuario especificado como tercer parámetro (en principio, mediante claves). +Realiza la acción contraria que el script "importImage.py", pero es preferible usar "exportImage.py" (porque permite buscar la imagen por ID). +Al acabar, ogCore debe llamar al script "updateRepoInfo.py" en el repositorio remoto, para actualizar la información de dicho repositorio. + +Librerías Python requeridas: "paramiko" (se puede instalar con "sudo apt install python3-paramiko") + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a exportar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images/ou_subdir/image1.img + +sys.argv[2] - IP o hostname del repositorio remoto. + - Ejemplo1: 192.168.56.100 + - Ejemplo2: remote_repo + +sys.argv[3] - Usuario con el que conectar al repositorio remoto. + - Ejemplo1: remote_user + - Ejemplo2: root + + Sintaxis +---------- +./exportImage.py [ou_subdir/]image_name|/image_path/image_name remote_host remote_user + + Ejemplos + --------- +./exportImage.py image1.img 192.168.56.100 user +./exportImage.py /opt/opengnsys/ogrepository/images/image1.img 192.168.56.100 user +./exportImage.py ou_subdir/image1.img remote_hostname user +./exportImage.py /ou_subdir/image1.img remote_hostname root +./exportImage.py /opt/opengnsys/ogrepository/images/ou_subdir/image1.img remote_hostname root +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import warnings +warnings.filterwarnings("ignore") +import os +import sys +import subprocess +import paramiko +import warnings + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name remote_host remote_user + Ejemplo1: {script_name} image1.img 192.168.56.100 user + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images/image1.img 192.168.56.100 user + Ejemplo3: {script_name} ou_subdir/image1.img remote_hostname user + Ejemplo4: {script_name} /ou_subdir/image1.img remote_hostname root + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images/ou_subdir/image1.img remote_hostname root + """ + print(help_text) + + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 3 parámetros, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 4: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 3 parámetros") + show_help() + sys.exit(1) + + + +def build_file_path(): + """ Construye la ruta completa al archivo a exportar + (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "repo_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(repo_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(repo_path): + file_path = os.path.join(repo_path, param_path) + else: + file_path = param_path + return file_path + + + +def export_image(file_path, remote_host, remote_user): + """ Conecta al repositorio remoto por SSH e inicia un cliente SFTP. + Luego exporta la imagen al repositorio remoto (junto con sus archivos asociados). + """ + # Creamos una lista con las extensiones de los archivos asociados a la imagen + # (incluyendo ninguna extensión, que corresponde a la propia imagen): + extensions = ['', '.size', '.sum', '.full.sum', '.torrent', '.info.checked'] + + # Iniciamos un cliente SSH: + ssh_client = paramiko.SSHClient() + # Establecemos la política por defecto para localizar la llave del host localmente: + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + # Intentamos conectar con el equipo remoto por SSH, e iniciar un cliente SFTP, + try: # y en caso de fallar devolvemos un error y salimos del script: + ssh_client.connect(remote_host, 22, remote_user) # Así se hace con claves + #ssh_client.connect(remote_host, 22, remote_user, 'opengnsys') # Así se haría con password + sftp_client = ssh_client.open_sftp() + except Exception as error_description: + print(f"Connection has returned an exception: {error_description}") + sys.exit(4) + + # Comprobamos si la imagen ya existe en el equipo remoto, en cuyo caso devolvemos un error y salimos del script: + try: + sftp_client.stat(file_path) + print("Image already exists on remote repository.") + sys.exit(5) + except IOError: + print("As expected, image doesn't exist on remote repository.") + + # Evaluamos si la ruta de la imagen tiene 6 barras, en cuyo caso corresponderá a una imagen basada en OU, + # y almacenamos el nombre del directorio correspondiente a la OU: + if file_path.count('/') == 6: + ou_subdir = file_path.split('/')[5] + # Comprobamos si el directorio de OU existe en el equipo remoto, y en caso contrario lo creamos: + try: + sftp_client.stat(f"{repo_path}{ou_subdir}") + except IOError: + sftp_client.mkdir(f"{repo_path}{ou_subdir}", mode=755) + + # Creamos un archivo de bloqueo en el servidor remoto: + sftp_client.open(f"{file_path}.lock", 'w') + + # Exportamos la imagen al servidor remoto, junto con sus archivos asociados: + for ext in extensions: + sftp_client.put(f"{file_path}{ext}", f"{file_path}{ext}") + + # Renombramos el archivo remoto ".info.checked" a ".info", para que lo pille el script "updateRepoInfo.py": + sftp_client.rename(f"{file_path}.info.checked", f"{file_path}.info") + + # Eliminamos el archivo de bloqueo del servidor remoto: + sftp_client.remove(f"{file_path}.lock") + + # Cerramos el cliente SSH y el cliente SFTP: + ssh_client.close() + sftp_client.close() + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo a exportar: + file_path = build_file_path() + + # Si no existe el archivo de imagen, imprimimos un mensaje de error y salimos del script: + if not os.path.exists(file_path): + print("Image file doesn't exist") + sys.exit(2) + + # Si la imagen está bloqueada, imprimimos un mensaje de error y salimos del script: + if os.path.exists(f"{file_path}.lock"): + print("Image is locked.") + sys.exit(3) + + # Almacenamos la IP/hostname del repositorio remoto, y el usuario remoto (desde los parámetros): + remote_host = sys.argv[2] + remote_user = sys.argv[3] + + # Exportamos la imagen al repositorio remoto: + export_image(file_path, remote_host, remote_user) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/getRepoIface.py b/bin/getRepoIface.py new file mode 100644 index 0000000..84284e2 --- /dev/null +++ b/bin/getRepoIface.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script obtiene y devuelve la interfaz de red asociada a la IP especificada en el archivo "/opt/opengnsys/ogrepository/etc/ogAdmRepo.cfg" (en la clave "IPlocal"). +En principio, debería hacer lo mismo que el script bash original (cuyo nombre es "getRepoIface", a secas). + +No recibe ningún parámetro, y siempre es llamado por otros scripts, que necesitan dicha interfaz (por ejemplo, "sendFileMcast"). +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import socket +import fcntl +import struct + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +config_file = '/opt/opengnsys/ogrepository/etc/ogAdmRepo.cfg' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def get_IPlocal(): + """ Obtiene el valor asociado a la variable "IPlocal", desde el archivo '/opt/opengnsys/etc/ogAdmRepo.cfg'. + Retorna la IP encontrada, o un error (si no la encuentra). + """ + IPlocal = None + with open(config_file, 'r') as file: + for line in file: + if line.startswith('IPlocal'): + IPlocal = line.split('=')[1].strip() + return IPlocal + if IPlocal is None: + return "IP no encontrada en el archivo de configuración" + + +def get_ip_address(ifname): + """ Obtiene y retorna la IP asociada a la interfaz especificada como parámetro. + """ + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + return socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, struct.pack('256s', ifname[:15].encode('utf-8')))[20:24]) + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Obtenemos la IP especificada en el archivo '/opt/opengnsys/etc/ogAdmRepo.cfg' (como valor de "IPlocal"): + IPlocal = get_IPlocal() + + # Si no se ha encontrado, imprimimos un error y salimos del script: + if "IP no encontrada" in IPlocal: + print("IP no encontrada en el archivo de configuración") + sys.exit(0) + + # Obtenemos una lista de tuplas, que contiene los índices y los nombres de las interfaces + # (en formato "[(1, 'lo'), (2, 'enp0s3'), (3, 'enp0s8')]"): + interfaces = socket.if_nameindex() + + # Iteramos los índices y nombres contenidos en "interfaces", obtenemos la IP asociada a cada interfaz, + # y las comparamos a la variable "IPlocal", para obtener el nombre de la interfaz asociada a dicha IP: + for ifindex, ifname in interfaces: + try: + ip_address = get_ip_address(ifname) + if ip_address == IPlocal: + interface_name = ifname + break + except IOError: + continue + + # Si hemos obtenido la interfaz la imprimimos, y si no imprimimos un error y salimos del script: + if interface_name: + print(interface_name) + else: + print("No se encontró la interfaz asociada a IPlocal") + sys.exit(1) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/getRepoInfo.py b/bin/getRepoInfo.py new file mode 100644 index 0000000..011a165 --- /dev/null +++ b/bin/getRepoInfo.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script devuelve información (en formato json) de todas las imágenes contenidas en el repositorio (incluída la papelera), + o de la imagen que se especifique como primer parámetro (debiendo especificar también el subdirectorio de OU como segundo parámetro, si procede). +Previamente, llama al script "updateRepoInfo.py", para actualizar la información del repositorio (para evitar que dé error si no hay ninguna, por ejemplo). + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a consultar (con extensión y sin ruta), u "all" (para obtener información de todas las imágenes). + - Ejemplo1: all + - Ejemplo2: image1.img + +sys.argv[2] - Subdirectorio correspondiente a la OU, o "none" si no procede.. + - Ejemplo1: none + - Ejemplo2: OU_subdirectory + + Sintaxis +---------- +./getRepoInfo.py image_name|all ou_subdir|none + + Ejemplos + --------- +./getRepoInfo.py all none +./getRepoInfo.py image1.img none +./getRepoInfo.py image1.img OU_subdirectory +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess +import json + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_file = '/opt/opengnsys/ogrepository/etc/repoinfo.json' +trash_file = '/opt/opengnsys/ogrepository/etc/trashinfo.json' +update_repo_script = '/opt/opengnsys/ogrepository/bin/updateRepoInfo.py' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} image_name|all ou_subdir|none + Ejemplo1: {script_name} all none + Ejemplo2: {script_name} image1.img none + Ejemplo3: {script_name} image1.img OU_subdirectory + """ + print(help_text) + + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 2 parámetros, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 3: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 2 parámetros") + show_help() + sys.exit(1) + + + +def update_repo_info(): + """ Actualiza la información del repositorio, ejecutando el script "updateRepoInfo.py". + Como se ve, es necesario que el script se ejecute como sudo, o dará error. + """ + try: + result = subprocess.run(['sudo', 'python3', update_repo_script], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except subprocess.CalledProcessError as error: + print(f"Error Output: {error.stderr.decode()}") + sys.exit(3) + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + sys.exit(4) + + + +def get_all_info(repo_data, trash_data): + """ Imprime un json con la información de todo el repositorio, con todas las imágenes que contiene, + incluyendo las imágenes que fueron eliminadas (que estarán en la papelera). + """ + # Creamos un diccionario, combinando la información del repositorio y de la papelera: + dictionary = {"REPOSITORY": repo_data, + "TRASH": trash_data} + # Convertimos el diccionario a json, y lo imprimimos: + final_json = json.dumps(dictionary, indent=2) + print(final_json) + + + +def get_image_info(repo_data, trash_data, image_name, image_ext): + """ Busca la imagen especificada en el repositorio y en la papelera, devolviendo la información asociada si la encuentra, + (o devolviendo un mensaje de error y saliendo del script si no la encuentra). + """ + dictionary = "" + # Buscamos la imagen en el repositorio, y si la encontramos creamos un diccionario con los datos: + if repo_data != "": + for image in repo_data['images']: + if image['name'] == image_name and image['type'] == image_ext: + dictionary = {"directory": repo_data['directory'], + "images": [image]} + # Buscamos la imagen en la papelera, y si la encontramos creamos un diccionario con los datos: + if trash_data != "": + for image in trash_data['images']: + if image['name'] == image_name: + dictionary = {"directory": trash_data['directory'], + "images": [image]} + # Si hemos obtenido datos de la imagen, los pasamos a json y los imprmimos, + # y si no, imprimimos un mensaje de error y salimos del script: + if dictionary != "": + final_json = json.dumps(dictionary, indent=2) + print(final_json) + else: + print("No se ha encontrado la imagen especificada en el repositorio") + sys.exit(2) + + + +def get_ou_image_info(repo_data, trash_data, image_name, image_ext, ou_subdir): + """ Busca la imagen basada en OU en el repositorio y en la papelera, devolviendo la información asociada si la encuentra, + (o devolviendo un mensaje de error y saliendo del script si no la encuentra). + """ + dictionary = "" + # Buscamos la OU y la imagen en el repositorio, y si los encontramos creamos un diccionario con los datos: + if repo_data != "": + for ou in repo_data['ous']: + if ou['subdir'] == ou_subdir: + for image in ou['images']: + if image['name'] == image_name and image['type'] == image_ext: + dictionary = {"directory": repo_data['directory'], + "ous": [{"subdir": ou_subdir, "images": [image]}]} + # Buscamos la OU y la imagen en la papelera, y si los encontramos creamos un diccionario con los datos: + if trash_data != "": + for ou in trash_data['ous']: + if ou['subdir'] == ou_subdir: + for image in ou['images']: + if image['name'] == image_name: + dictionary = {"directory": trash_data['directory'], + "ous": [{"subdir": ou_subdir, "images": [image]}]} + # Si hemos obtenido datos de la imagen, los pasamos a json y los imprmimos, + # y si no, imprimimos un mensaje de error y salimos del script: + if dictionary != "": + final_json = json.dumps(dictionary, indent=2) + print(final_json) + else: + print("No se ha encontrado la imagen especificada en el repositorio") + sys.exit(3) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Actualizamos la información del repositorio, ejecutando el script "updateRepoInfo.py": + update_repo_info() + + # Almacenamos la información de las imágenes del repositorio, en la variable "repo_data" + # (solo si el archivo tiene contenido, o dará error): + if os.path.getsize(repo_file) > 0: + with open(repo_file, 'r') as file: + repo_data = json.load(file) + else: + repo_data = "" + + # Almacenamos la información de las imágenes de la papelera, en la variable "trash_data" + # (solo si el archivo tiene contenido, o dará error): + if os.path.getsize(trash_file) > 0: + with open(trash_file, 'r') as file: + trash_data = json.load(file) + else: + trash_data = "" + + # Dependiendo del valor de los parámetros, llamamos a la función correspondiente, para imprimir la información + # (extrayendo el nombre, la extensión de la imagen, y/o la OU cuando se necesite): + if sys.argv[1] == 'all': + get_all_info(repo_data, trash_data) + elif sys.argv[2] == 'none': + image_name = sys.argv[1].split('.')[0] + image_ext = sys.argv[1].split('.')[1] + get_image_info(repo_data, trash_data, image_name, image_ext) + else: + image_name = sys.argv[1].split('.')[0] + image_ext = sys.argv[1].split('.')[1] + ou_subdir = sys.argv[2] + get_ou_image_info(repo_data, trash_data, image_name, image_ext, ou_subdir) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/getRepoStatus.py b/bin/getRepoStatus.py new file mode 100644 index 0000000..ad46ea2 --- /dev/null +++ b/bin/getRepoStatus.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script devuelve información de CPU, memoria RAM, disco duro y el estado de ciertos servicios y procesos de ogRepository, en formato json. +No recibe ningún parámetro. + +Librerías Python requeridas: "psutil" (se puede instalar con "sudo apt install python3-psutil". o "pip install psutil") +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import psutil +import os +import json +import subprocess + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def get_cpu_info(): + """ Obtiene y retorna información de la CPU. + """ + cpu_percent = psutil.cpu_percent(interval=1) + return cpu_percent + + +def get_ram_info(): + """ Obtiene y retorna información de la memoria RAM. + """ + ram = psutil.virtual_memory() + return ram.total, ram.used, ram.available, ram.percent + + +def get_disk_info(): + """ Obtiene y retorna información del disco duro. + """ + disk = psutil.disk_usage('/') + return disk.total, disk.used, disk.free, disk.percent + + +def get_service_status(service): + """ Obtiene y retorna el estado del servicio que recibe como parámetro. + En caso de error, retorna un mensaje estándar. + """ + try: + result = subprocess.run(['systemctl', 'is-active', service], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + service_status = result.stdout.strip() + return service_status + except Exception: + return "status not accesible" + + +def get_process_status(process): + """ Obtiene y retorna el estado del proceso que recibe como parámetro. + """ + #for proc in psutil.process_iter(['pid', 'name', 'status']): + for proc in psutil.process_iter(['name']): + if proc.info['name'] == process: + return 'running' + return 'stopped' + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + +def main(): + """ + """ + # Obtenemos información de la CPU: + cpu_percent = get_cpu_info() + + # Obtenemos información de la memoria RAM: + total_ram, used_ram, available_ram, percent_ram = get_ram_info() + + # Obtenemos información del disco duro: + total_disk, used_disk, free_disk, percent_disk = get_disk_info() + + # Obtenemos el estado de los servicios listados, que almacenamos en un diccionario: + service_list = ['ssh', 'smbd', 'rsync'] + services_status = {service: get_service_status(service) for service in service_list} + + # Obtenemos el estado de los procesos listados, que almacenamos en un diccionario: + process_list = ['udp-sender', 'uftp', 'bttrack', 'btlaunchmany'] + process_status = {process: get_process_status(process) for process in process_list} + + # Creamos un diccionario con toda la información obtenida: + data_dict = { + 'cpu': { + 'used_percentage': f"{int(cpu_percent)}%" + }, + 'ram': { + 'total': f"{round(total_ram / (1024 ** 3), 1)}GB", + 'used': f"{round(used_ram / (1024 ** 3), 1)}GB", + 'available': f"{round(available_ram / (1024 ** 3), 1)}GB", + 'used_percentage': f"{int(percent_ram)}%" + }, + 'disk': { + 'total': f"{round(total_disk / (1024 ** 3), 1)}GB", + 'used': f"{round(used_disk / (1024 ** 3), 1)}GB", + 'available': f"{round(free_disk / (1024 ** 3), 1)}GB", + 'used_percentage': f"{int(percent_disk)}%" + }, + 'services': services_status, + 'processes': process_status + } + + # Convertimos el diccionario a JSON, y lo imprimimos: + json_data = json.dumps(data_dict, indent=4) + print(json_data) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/getUDPcastInfo.py b/bin/getUDPcastInfo.py new file mode 100644 index 0000000..f525a96 --- /dev/null +++ b/bin/getUDPcastInfo.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +Este script busca procesos activos de "udp-sender", y si encuentra alguno devuelve el pid y la imagen asociada de cada uno de ellos, en una estructura JSON. +Si no encuentra ninguno, o si se produce un error, imprime un mensaje informativo. +No recibe ningún parámetro. + +En la práctica, permite comprobar las transminisones UDPcast activas, porque cuando finalizan también finaliza el proceso asociado. +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import subprocess +import json +import sys + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def get_udpsender_processes(): + """ Busca procesos de "udp-sender", y si los encuentra retorna el pid, el ID, y la imagen asociada de cada uno de ellos, en un diccionario. + Si no encuentra ningun proceso, o si se produce un error, retorna un mensaje. + """ + try: + # Obtenemos todos los procesos, y almacenamos la salida y los errores: + result = subprocess.Popen(['ps', '-aux'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + out, error = result.communicate() + + # Almacenamos en una lista los procesos que contengan "udp-sender": + process_list = [line for line in out.split('\n') if 'udp-sender' in line] + # Si hemos encontrado procesos de udp-sender creamos un diccionario para almacenarlos: + if process_list != []: + result_dict = {} + # Iteramos los procesos y extraemos el pid, el nombre de la imagen (con subdirectorio de OU, si es el caso), y la ruta de la imagen de cada uno: + for process in process_list: + pid = process.split()[1] + image_name = process.split(repo_path)[1] + image_path = process.split('--file ')[1] + # Obtenemos el ID de la imagen actual: + with open(f"{image_path}.full.sum", 'r') as file: + image_id = file.read().strip('\n') + # Creamos una clave en el diccionario de resultados, correspondiente a la imagen actual: + result_dict[pid] = {'image_id':image_id, 'image_name':image_name} + # Retornamos el diccionario de resultados: + return result_dict + # Si no hemos encontrado procesos de udp-sender retrornamos un mensaje: + else: + return "udp-sender process not found" + # Si se ha producido una excepción, retornamos un mensaje: + except Exception: + return "Unexpected error" + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Obtenemos información sobre los procesos de udp-sender: + results = get_udpsender_processes() + + # Si no hay procesos activos, o si se ha producido un error, imprimimos un mensaje explicativo, y salimos del script: + if results == "udp-sender process not found": + print("No UDPcast active transmissions") + sys.exit(1) + elif results == "Unexpected error": + print("Unexpected error checking UDPcast transmissions") + sys.exit(2) + # Si hay procesos activos, convertimos el diccionario de resultados a JSON, e imprimimos este: + else: + json_data = json.dumps(results, indent=4) + print(json_data) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/getUFTPInfo.py b/bin/getUFTPInfo.py new file mode 100644 index 0000000..6d0579e --- /dev/null +++ b/bin/getUFTPInfo.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +Este script busca procesos activos de "uftp", y si encuentra alguno devuelve el pid y la imagen asociada de cada uno de ellos, en una estructura JSON. +Si no encuentra ninguno, o si se produce un error, imprime un mensaje informativo. +No recibe ningún parámetro. + +En la práctica, permite comprobar las transminisones UFTP activas, porque cuando finalizan también finaliza el proceso asociado. +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import subprocess +import json +import sys + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def get_uftp_processes(): + """ Busca procesos de "uftp", y si los encuentra retorna el pid, el ID, y la imagen asociada de cada uno de ellos, en un diccionario. + Si no encuentra ningun proceso, o si se produce un error, retorna un mensaje. + """ + try: + # Obtenemos todos los procesos, y almacenamos la salida y los errores: + result = subprocess.Popen(['ps', '-aux'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + out, error = result.communicate() + + # Almacenamos en una lista los procesos que contengan "uftp": + process_list = [line for line in out.split('\n') if 'uftp' in line] + # Si hemos encontrado procesos de udp-sender creamos un diccionario para almacenarlos: + if process_list != []: + result_dict = {} + # Iteramos los procesos y extraemos el pid, el nombre de la imagen (con subdirectorio de OU, si es el caso), y la ruta de la imagen de cada uno: + for process in process_list: + pid = process.split()[1] + image_name = process.split(repo_path)[1] + image_path = process.split()[-1] + # Obtenemos el ID de la imagen actual: + with open(f"{image_path}.full.sum", 'r') as file: + image_id = file.read().strip('\n') + # Creamos una clave en el diccionario de resultados, correspondiente a la imagen actual: + result_dict[pid] = {'image_id':image_id, 'image_name':image_name} + # Retornamos el diccionario de resultados: + return result_dict + # Si no hemos encontrado procesos de uftp retornamos un mensaje: + else: + return "uftp process not found" + # Si se ha producido una excepción, retornamos un mensaje: + except Exception: + return "Unexpected error" + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Obtenemos información sobre los procesos "uftp": + results = get_uftp_processes() + + # Si no hay procesos activos, o si se ha producido un error, imprimimos un mensaje explicativo, y salimos del script: + if results == "uftp process not found": + print("No UFTP active transmissions") + sys.exit(1) + elif results == "Unexpected error": + print("Unexpected error checking UFTP transmissions") + sys.exit(2) + # Si hay procesos activos, convertimos el diccionario de resultados a JSON, e imprimimos este: + else: + json_data = json.dumps(results, indent=4) + print(json_data) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/importImage.py b/bin/importImage.py new file mode 100644 index 0000000..e6798b7 --- /dev/null +++ b/bin/importImage.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script importa la imagen especificada como primer parámetro (y sus archivos asociados), desde el repositorio remoto especificado como segundo parámetro, + con las credenciales del usuario especificado como tercer parámetro (en principio, mediante claves). +Es muy similar al script bash original (cuyo nombre es "importimage", a secas), pero con ciertas diferencias. +Al acabar, llama al script "updateRepoInfo.py", para actualizar la información del repositorio. + +Librerías Python requeridas: "paramiko" (se puede instalar con "sudo apt install python3-paramiko") + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a importar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images/ou_subdir/image1.img + +sys.argv[2] - IP o hostname del repositorio remoto. + - Ejemplo1: 192.168.56.100 + - Ejemplo2: remote_repo + +sys.argv[3] - Usuario con el que conectar al repositorio remoto. + - Ejemplo1: remote_user + - Ejemplo2: root + + Sintaxis +---------- +./importImage.py [ou_subdir/]image_name|/image_path/image_name remote_host remote_user + + Ejemplos + --------- +./importImage.py image1.img 192.168.56.100 user +./importImage.py /opt/opengnsys/ogrepository/images/image1.img 192.168.56.100 user +./importImage.py ou_subdir/image1.img remote_hostname user +./importImage.py /ou_subdir/image1.img remote_hostname root +./importImage.py /opt/opengnsys/ogrepository/images/ou_subdir/image1.img remote_hostname root +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import warnings +warnings.filterwarnings("ignore") +import os +import sys +import subprocess +import paramiko +import warnings + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final +update_repo_script = '/opt/opengnsys/ogrepository/bin/updateRepoInfo.py' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name remote_host remote_user + Ejemplo1: {script_name} image1.img 192.168.56.100 user + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images/image1.img 192.168.56.100 user + Ejemplo3: {script_name} ou_subdir/image1.img remote_hostname user + Ejemplo4: {script_name} /ou_subdir/image1.img remote_hostname root + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images/ou_subdir/image1.img remote_hostname root + """ + print(help_text) + + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 3 parámetros, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 4: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 3 parámetros") + show_help() + sys.exit(1) + + + +def build_file_path(): + """ Construye la ruta completa al archivo a importar + (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "repo_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(repo_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(repo_path): + file_path = os.path.join(repo_path, param_path) + else: + file_path = param_path + return file_path + + + +def import_image(file_path, remote_host, remote_user): + """ Conecta al repositorio remoto por SSH e inicia un cliente SFTP. + Comprueba que la imagen no esté bloqueada, en cuyo caso la descarga (junto con sus archivos asociados). + """ + # Creamos una lista con las extensiones de los archivos asociados a la imagen + # (incluyendo ninguna extensión, que corresponde a la propia imagen): + extensions = ['', '.size', '.sum', '.full.sum', '.torrent', '.info.checked'] + + # Iniciamos un cliente SSH: + ssh_client = paramiko.SSHClient() + # Establecemos la política por defecto para localizar la llave del host localmente: + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + # Intentamos conectar con el equipo remoto por SSH, e iniciar un cliente SFTP, + try: # y en caso de fallar devolvemos un error y salimos del script: + ssh_client.connect(remote_host, 22, remote_user) # Así se hace con claves + #ssh_client.connect(remote_host, 22, remote_user, 'opengnsys') # Así se haría con password + sftp_client = ssh_client.open_sftp() + except Exception as error_description: + print(f"Connection has returned an exception: {error_description}") + sys.exit(2) + + # Comprobamos si la imagen existe en el equipo remoto, y en caso contrario devolvemos un error y salimos del script: + try: + sftp_client.stat(file_path) + except IOError: + print("Remote image doesn't exist") + sys.exit(3) + + # Comprobamos si la imagen remota está bloqueada, en cuyo caso devolvemos un error y salimos del script, + try: # y en caso contrario la importamos (junto con todos sus archivos asociados): + sftp_client.stat(f"{file_path}.lock") + print("Remote image is locked.") + sys.exit(4) + except IOError: + print("Importing remote image...") + open(f"{file_path}.lock", "w").close() # Creamos un archivo de bloqueo + for ext in extensions: + sftp_client.get(f"{file_path}{ext}", f"{file_path}{ext}") + + # Cerramos el cliente SSH y el cliente SFTP: + ssh_client.close() + sftp_client.close() + + + +def update_repo_info(): + """ Actualiza la información del repositorio, ejecutando el script "updateRepoInfo.py". + Como se ve, es necesario que el script se ejecute como sudo, o dará error. + """ + try: + result = subprocess.run(['sudo', 'python3', update_repo_script], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except subprocess.CalledProcessError as error: + print(f"Error Output: {error.stderr.decode()}") + sys.exit(2) + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + sys.exit(3) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo a importar: + file_path = build_file_path() + + # Almacenamos la IP/hostname del repositorio remoto, y el usuario remoto (desde los parámetros): + remote_host = sys.argv[2] + remote_user = sys.argv[3] + + # Evaluamos si la ruta de la imagen tiene 6 barras, en cuyo caso corresponderá a una imagen basada en OU, + # y almacenamos el nombre del directorio correspondiente a la OU: + if file_path.count('/') == 6: + ou_subdir = file_path.split('/')[5] + # Si no existe un directorio correspondiente a la OU en el repo local, lo creamos: + if not os.path.exists(f"{repo_path}{ou_subdir}"): + os.mkdir(f"{repo_path}{ou_subdir}", 0o755) + + # Importamos la imagen del repositorio remoto: + import_image(file_path, remote_host, remote_user) + + # Renombramos el archivo ".info.checked" a ".info", para que lo pille el script "updateRepoInfo.py": + os.rename(f"{file_path}.info.checked", f"{file_path}.info") + + # Eliminamos el archivo de bloqueo: + os.remove(f"{file_path}.lock") + + # Actualizamos la información del repositorio, ejecutando el script "updateRepoInfo.py": + print("Updating Repository Info...") + update_repo_info() + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/recoverImage.py b/bin/recoverImage.py new file mode 100644 index 0000000..e83f306 --- /dev/null +++ b/bin/recoverImage.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script recupera la imagen que recibe como parámetro (y todos sus archivos asociados), moviendo los archivos a "/opt/opengnsys/ogrepository/images", desde la papelera + (respetando el subdirectorio correspondiente a la OU, si fuera el caso). +Llama al script "updateRepoInfo.py", para actualizar la información del repositorio. + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a recuperar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images_trash/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images_trash/ou_subdir/image1.img + + Sintaxis +---------- +./recoverImage.py [ou_subdir/]image_name|/image_path/image_name + + Ejemplos + --------- +./recoverImage.py image1.img +./recoverImage.py /opt/opengnsys/ogrepository/images_trash/image1.img +./recoverImage.py ou_subdir/image1.img +./recoverImage.py /ou_subdir/image1.img +./recoverImage.py /opt/opengnsys/ogrepository/images_trash/ou_subdir/image1.img +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import shutil +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final +trash_path = '/opt/opengnsys/ogrepository/images_trash/' # No borrar la barra final +update_repo_script = '/opt/opengnsys/ogrepository/bin/updateRepoInfo.py' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name + Ejemplo1: {script_name} image1.img + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images_trash/image1.img + Ejemplo3: {script_name} ou_subdir/image1.img + Ejemplo4: {script_name} /ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images_trash/ou_subdir/image1.img + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script sin parámetros, se muestra un error y la ayuda, y se sale del script: + if len(sys.argv) == 1: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar al menos 1 parámetro") + show_help() + sys.exit(0) + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + elif len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(1) + # Si se ejecuta el script con más de 1 parámetro, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) > 2: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 parámetro") + show_help() + sys.exit(2) + + +def build_file_path(): + """ Construye la ruta completa al archivo a recuperar + (agregando "/opt/opengnsys/images_trash/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "trash_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(trash_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(trash_path): + file_path = os.path.join(trash_path, param_path) + else: + file_path = param_path + return file_path + + +def recover_normal_image(file_path, extensions): + """ Recupera la imagen "normal" que recibe en el parámetro "file_path", y todos sus archivos asociados, + moviéndolos a "/opt/opengnsys/images" (desde la papelera). + """ + # Iteramos las extensiones de los archivos, y construimos la ruta completa de cada uno de ellos: + for ext in extensions: + file_to_recover = f"{file_path}{ext}" + # Si el archivo actual existe, lo movemos a "/opt/opengnsys/images" (recuperándolo desde la papelera): + if os.path.exists(file_to_recover): + # Si la extensión del archivo actual es ".info.checked" la renombramos a ".info" (para que lo pille "updateRepoInfo"): + if ext == '.info.checked': + os.rename(file_to_recover, file_to_recover.strip('.checked')) + file_to_recover = file_to_recover.strip('.checked') + shutil.move(file_to_recover, repo_path) + + +def recover_ou_image(file_path, extensions): + """ Recupera la imagen basada en OU que recibe en el parámetro "file_path", y todos sus archivos asociados, + moviéndolos a "/opt/opengnsys/images" (desde la papelera), respetando el subdirectorio correspondiente a la OU. + """ + # Iteramos las extensiones de los archivos, y construimos la ruta completa de cada uno de ellos: + for ext in extensions: + file_to_recover = f"{file_path}{ext}" + # Si el archivo actual existe, lo movemos a "/opt/opengnsys/images/ou_subdir": + if os.path.exists(file_to_recover): + # Si la extensión del archivo actual es ".info.checked" la renombramos a ".info" (para que lo pille "updateRepoInfo"): + if ext == '.info.checked': + os.rename(file_to_recover, file_to_recover.strip('.checked')) + file_to_recover = file_to_recover.strip('.checked') + ou_subdir = file_to_recover.split('/')[4] + # Comprobamos si en "repo_path" existe un subdirectorio correspondiente a la OU (y si no existe lo creamos): + if not os.path.exists(f"{repo_path}{ou_subdir}"): + os.mkdir(f"{repo_path}{ou_subdir}") + shutil.move(file_to_recover, f"{repo_path}{ou_subdir}") + + +def update_repo_info(): + """ Actualiza la información del repositorio, ejecutando el script "updateRepoInfo.py". + Como se ve, es necesario que el script se ejecute como sudo, o dará error. + """ + try: + result = subprocess.run(['sudo', 'python3', update_repo_script], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except subprocess.CalledProcessError as error: + print(f"Error Output: {error.stderr.decode()}") + sys.exit(3) + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + sys.exit(4) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo a eliminar: + file_path = build_file_path() + + # Creamos una lista con las extensiones de los archivos asociados a la imagen + # (incluyendo ninguna extensión, que corresponde a la propia imagen): + extensions = ['', '.size', '.sum', '.full.sum', '.torrent', '.info', '.info.checked'] + + # Evaluamos la cantidad de barras que hay en la ruta de la imagen, para diferenciar entre imágenes "normales" y basadas en OU + # (y llamamos a la función correspondiente para recuperarla): + if file_path.count('/') == 5: + print("Recovering normal image...") + recover_normal_image(file_path, extensions) + elif file_path.count('/') == 6: + print("Recovering OU based image...") + recover_ou_image(file_path, extensions) + + # Actualizamos la información del repositorio, ejecutando el script "updateRepoInfo.py": + print("Updating Repository Info...") + update_repo_info() + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/runTorrentSeeder.py b/bin/runTorrentSeeder.py new file mode 100644 index 0000000..11b8735 --- /dev/null +++ b/bin/runTorrentSeeder.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script inicia el seeder "bittornado" (o lo reinicia, si ya estuviera iniciado), finalizando previamente cualquier proceso activo. +En principio, debería hacer lo mismo que la sección correspondiente del script "/etc/init.d/opengnsys", que se ejecuta al inicio (pero que debería dejar de hacerlo). +Creemos que debe ser llamado únicamente cuando se quiera hacer una descarga mediante P2P (junto al script "runTorrentTracker.py"). +NOTA: El paquete no hace una búsqueda recursiva, por lo que se debe especificar el subdirectorio correspondiente a la OU, si es el caso. + + Parámetros +------------ +sys.argv[1] - Subdirectorio correspondiente a la OU (o "none" si no es el caso). + - Ejemplo1: none + - Ejemplo2: ou_subdir + + Sintaxis +---------- +./runTorrentSeeder.py none|ou_subdir + + Ejemplos + --------- +./runTorrentSeeder.py none +./runTorrentSeeder.py ou_subdir +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/ogrepository/images' # En este caso, no lleva barra final + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} none|ou_subdir + Ejemplo1: {script_name} none + Ejemplo2: {script_name} ou_subdir + """ + print(help_text) + + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 1 parámetro, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 2: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 parámetro") + show_help() + sys.exit(1) + + + +def run_bittornado(torrent_path): + """ Ejecuta el comando "btlaunchmany.bittornado", con sus parámetros correspondientes. + Además, captura el resultado y los posibles errores, y los imprime. + """ + # Creamos una lista con el comando "btlaunchmany.bittornado" y sus parámetros, y lo imprimimos con espacios: + splitted_cmd = f"btlaunchmany.bittornado {torrent_path}".split() + print(f"Sending command: {' '.join(splitted_cmd)}") + + # Ejecutamos el comando "btlaunchmany.bittornado" en el sistema, e imprimimos el resultado: + try: + result = subprocess.run(splitted_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"Bittornado ReturnCode: {result.returncode}") + except subprocess.CalledProcessError as error: + print(f"Bittornado ReturnCode: {error.returncode}") + print(f"Bittornado Error Output: {error.stderr.decode()}") + except Exception as error: + print(f"Unexpected bittornado error: {error}") + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Finalizamos el proceso "btlaunchmany.bittornado" (en caso de que estuviera corriendo): + try: + subprocess.run(f"pkill btlaunchmany".split(), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except Exception as error_description: + print(f"No btlaunchmany.bittornado process running? Returned error: {error_description}") + + # Construimos la ruta en la que buscar los torrents, en base al parámetro especificado: + if sys.argv[1] == 'none': + torrent_path = repo_path + else: + torrent_path = f"{repo_path}/{sys.argv[1]}" + + # Ejecutamos el comando "btlaunchmany.bittornado" (para hacer seed de los torrents): + run_bittornado(torrent_path) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/runTorrentTracker.py b/bin/runTorrentTracker.py new file mode 100644 index 0000000..48f89cc --- /dev/null +++ b/bin/runTorrentTracker.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script inicia el tracker "bttrack" (o lo reinicia, si ya estuviera iniciado), finalizando previamente cualquier proceso activo, y borrando el archivo "/tmp/dstate". +En principio, debería hacer lo mismo que el script bash original (cuyo nombre es "torrent-tracker"), que se ejecutaba por cron cada hora. +Creemos que debe ser llamado únicamente cuando se quiera hacer una descarga mediante P2P (junto al script "runTorrentSeeder.py"). +NOTA: El paquete no hace una búsqueda recursiva, por lo que se debe especificar el subdirectorio correspondiente a la OU, si es el caso. + + Parámetros +------------ +sys.argv[1] - Subdirectorio correspondiente a la OU (o "none" si no es el caso). + - Ejemplo1: none + - Ejemplo2: ou_subdir + + Sintaxis +---------- +./runTorrentTracker.py none|ou_subdir + + Ejemplos + --------- +./runTorrentTracker.py none +./runTorrentTracker.py ou_subdir +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess +import time + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/ogrepository/images' # En este caso, no lleva barra final + +bttrack_port = 6969 +bttrack_dfile = '/tmp/dstate' +bttrack_log = '/opt/opengnsys/ogrepository/log/bttrack.log' +bttrack_interval = 10 +bttrack_allow_get = 0 # Este valor impide la descarga desde clientes no autorizados + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} none|ou_subdir + Ejemplo1: {script_name} none + Ejemplo2: {script_name} ou_subdir + """ + print(help_text) + + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 1 parámetro, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 2: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 parámetro") + show_help() + sys.exit(1) + + + +def run_bttrack(torrent_path): + """ Ejecuta el comando "bttrack", con sus parámetros correspondientes. + Además, captura el resultado y los posibles errores, y los imprime. + """ + # Creamos una lista con el comando "bttrack" y sus parámetros, y lo imprimimos con espacios: + splitted_cmd = f"bttrack --port {bttrack_port} --dfile {bttrack_dfile} --save_dfile_interval {bttrack_interval} --reannounce_interval {bttrack_interval} --logfile {bttrack_log} --allowed_dir {torrent_path} --allow_get {bttrack_allow_get}".split() + print(f"Sending command: {' '.join(splitted_cmd)}") + + # Ejecutamos el comando "bttrack" en el sistema, e imprimimos el resultado: + try: + result = subprocess.run(splitted_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"Bttrack ReturnCode: {result.returncode}") + except subprocess.CalledProcessError as error: + print(f"Bttrack ReturnCode: {error.returncode}") + print(f"Bttrack Error Output: {error.stderr.decode()}") + except Exception as error: + print(f"Unexpected bttrack error: {error}") + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Finalizamos el proceso "bttrack" (en caso de que estuviera corriendo): + try: + subprocess.run(f"pkill bttrack".split(), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except Exception as error_description: + print(f"No bttrack process running? Returned error: {error_description}") + + # Si existe el archivo "/tmp/dstate", lo eliminamos: + if os.path.exists(bttrack_dfile): + os.remove(bttrack_dfile) + + # Esperamos 2 segundos: + time.sleep(2) + + # Construimos la ruta en la que buscar los torrents, en base al parámetro especificado: + if sys.argv[1] == 'none': + torrent_path = repo_path + else: + torrent_path = f"{repo_path}/{sys.argv[1]}" + + # Ejecutamos el comando "bttrack" (para hacer tracking de los torrents): + run_bttrack(torrent_path) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/sendFileMcast.py b/bin/sendFileMcast.py new file mode 100644 index 0000000..452769a --- /dev/null +++ b/bin/sendFileMcast.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script envía mediante UDPCast la imagen recibida como primer parámetro, con los datos Multicast especificados en el segundo parámetro. +En principio, debería hacer lo mismo que el script bash original (cuyo nombre es "sendFileMcast", a secas). + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a enviar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images/ou_subdir/image1.img + +sys.argv[2] - Parámetros Multicast (en formato "Port:Duplex:IP:Mpbs:Nclients:Timeout") + - Ejemplo: 9000:full:239.194.17.2:70M:20:120 + + Sintaxis +---------- +./sendFileMcast.py [ou_subdir/]image_name|/image_path/image_name Port:Duplex:IP:Mpbs:Nclients:Timeout + + Ejemplos + --------- +./sendFileMcast.py image1.img 9000:full:239.194.17.2:70M:20:120 +./sendFileMcast.py /opt/opengnsys/ogrepository/images/image1.img 9000:full:239.194.17.2:70M:20:120 +./sendFileMcast.py ou_subdir/image1.img 9000:full:239.194.17.2:70M:20:120 +./sendFileMcast.py /ou_subdir/image1.img 9000:full:239.194.17.2:70M:20:120 +./sendFileMcast.py /opt/opengnsys/ogrepository/images/ou_subdir/image1.img 9000:full:239.194.17.2:70M:20:120 +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final +bin_path = '/opt/opengnsys/ogrepository/bin/' # No borrar la barra final +repo_iface_script = '/opt/opengnsys/ogrepository/bin/getRepoIface.py' +log_file = '/opt/opengnsys/ogrepository/log/udpcast.log' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name Port:Duplex:IP:Mpbs:Nclients:Timeout + Ejemplo1: {script_name} image1.img 9000:full-duplex:239.194.17.2:70M:20:120 + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images/image1.img 9000:full-duplex:239.194.17.2:70M:20:120 + Ejemplo3: {script_name} ou_subdir/image1.img 9000:full-duplex:239.194.17.2:70M:20:120 + Ejemplo4: {script_name} /ou_subdir/image1.img 9000:full-duplex:239.194.17.2:70M:20:120 + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images/ou_subdir/image1.img 9000:full-duplex:239.194.17.2:70M:20:120 + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta la función con más o menos de 2 parámetros, se muestra un mensaje de error, y se sale del script: + elif len(sys.argv) != 3: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 2 parámetros (image_name|/image_path/image_name Port:Duplex:IP:Mpbs:Nclients:Timeout)") + sys.exit(1) + # Si en el segundo parámetro no hay 6 elementos (separados por ":"), se muestra un mensaje de error, y se sale del script: + param_list = sys.argv[2].split(':') + if len(param_list) != 6: + print(f"{script_name} Error: Datos Multicast incorrectos: \"{sys.argv[2]}\" (se debe especificar \"Port:Duplex:IP:Mpbs:Nclients:Timeout\")") + sys.exit(3) + + +def build_file_path(): + """ Construye la ruta completa al archivo a enviar + (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "repo_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(repo_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(repo_path): + file_path = os.path.join(repo_path, param_path) + else: + file_path = param_path + return file_path + + +def get_repo_iface(): + """ Obtiene y retorna la interfaz del repositorio, ejecutando el script "getRepoIface.py". + Como se ve, es necesario que el script se ejecute como sudo, o dará error. + """ + try: + result = subprocess.run(['sudo', 'python3', repo_iface_script], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + repo_iface = result.stdout.decode().strip() # Es necesario poner "strip", o dará error. + return repo_iface + except subprocess.CalledProcessError as error: + print(f"Error Output: {error.stderr.decode()}") + sys.exit(3) + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + sys.exit(4) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo a enviar: + file_path = build_file_path() + + # Si el fichero no es accesible, devolvermos un error, y salimos del script: + if not os.path.isfile(file_path): + print(f"{script_name} Error: Fichero \"{file_path}\" no accesible") + sys.exit(2) + + # Almacenamos los elementos del segundo parámetro en variables (su formato es "Port:Duplex:IP:Mpbs:Nclients:Timeout"): + param_list = sys.argv[2].split(':') + port, method, ip, bitrate, nclients, maxtime = param_list + + # Retocamos las variables "method" y "bitrate": + method = '--half-duplex' if 'half' in method.lower() else '--full-duplex' + bitrate = bitrate.lower() + + # Creamos la variable "cerror" (no sé que hace, pero estaba en el script original): + cerror = "8x8/128" + + # Obtenemos y almacenamos la interfaz del repositorio, mediante el script "getRepoIface.py": + repo_iface = get_repo_iface() + + # Creamos una lista con el comando a enviar (esto es requerido por la función "subprocess.run"). + splitted_cmd = [ + os.path.join(bin_path, 'udp-sender'), + '--nokbd', + '--retries-until-drop', '65', + '--portbase', port, + method, + '--interface', repo_iface, + '--mcast-data-address', ip, + '--fec', cerror, + '--max-bitrate', bitrate, + '--ttl', '16', + '--min-clients', nclients, + '--max-wait', maxtime, # Esto hace que espere 120 segundos desde la conexión del primer cliente para empezar la transmisión. + #'--autostart', '20', # Esto hace que empiece la transmisión automáticamente después de enviar 20 paquetes "hello". + '--log', log_file, + '--file', file_path + ] + + # Imprimimos el comando con espacios (como realmente se enviará): + print(f"Sending command: {' '.join(splitted_cmd)}") + + # Ejecutamos el comando en el sistema, e imprimimos el resultado: + try: + result = subprocess.run(splitted_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"ReturnCode: {result.returncode}") + except subprocess.CalledProcessError as error: + print(f"ReturnCode: {error.returncode}") + print(f"Error Output: {error.stderr.decode()}") + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/sendFileUFTP.py b/bin/sendFileUFTP.py new file mode 100644 index 0000000..85c9851 --- /dev/null +++ b/bin/sendFileUFTP.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script envía mediante UFTP la imagen recibida como primer parámetro, al puerto e IP (Multicast o Unicast) especificados en el segundo parámetro, + a la velocidad de transferencia tambíén especificada en el segundo parámetro (la sintaxis de este parámetro es "Port:IP:Bitrate"). +Previamente, los clientes deben haberse puesto a escuchar con un proceso "uftpd" en la IP Multicast correspondiente (tanto para envíos Multicast como para envíos Unicast). +NOTA: La imagen se envía a la ruta de la caché de los clientes (que actualmente es "/opt/opengnsys/cache"). + +Paquetes APT requeridos: "uftp" (se puede instalar con "sudo apt install uftp"). + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a enviar (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images/ou_subdir/image1.img + +sys.argv[2] - Parámetros Multicast/Unicast (en formato "Port:IP:Bitrate") + - Ejemplo1: 9000:239.194.17.2:100M + - Ejemplo2: 9000:192.168.56.101:1G + + Sintaxis +---------- +./sendFileUFTP.py [ou_subdir/]image_name|/image_path/image_name Port:IP:Bitrate + + Ejemplos + --------- +./sendFileUFTP.py image1.img 9000:239.194.17.2:100M +./sendFileUFTP.py /opt/opengnsys/ogrepository/images/image1.img 9000:239.194.17.2:100M +./sendFileUFTP.py ou_subdir/image1.img 9000:192.168.56.101:1G +./sendFileUFTP.py /ou_subdir/image1.img 9000:192.168.56.101:1G +./sendFileUFTP.py /opt/opengnsys/ogrepository/images/ou_subdir/image1.img 9000:192.168.56.101:1G +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final +cache_path = '/opt/opengnsys/cache' +log_file = '/opt/opengnsys/ogrepository/log/uftp.log' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name Port:IP:Bitrate + Ejemplo1: {script_name} image1.img 9000:239.194.17.2:100M + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images/image1.img 9000:239.194.17.2:100M + Ejemplo3: {script_name} ou_subdir/image1.img 9000:192.168.56.101:1G + Ejemplo4: {script_name} /ou_subdir/image1.img 9000:192.168.56.101:1G + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images/ou_subdir/image1.img 9000:192.168.56.101:1G + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta la función con más o menos de 2 parámetros, se muestra un mensaje de error, y se sale del script: + elif len(sys.argv) != 3: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 2 parámetros (image_name|/image_path/image_name port:ip:bitrate)") + sys.exit(1) + # Si en el segundo parámetro no hay 3 elementos (separados por ":"), se muestra un mensaje de error, y se sale del script: + param_list = sys.argv[2].split(':') + if len(param_list) != 3: + print(f"{script_name} Error: Datos Multicast incorrectos: \"{sys.argv[2]}\" (se debe especificar \"puerto:ip:bitrate\")") + sys.exit(3) + + +def build_file_path(): + """ Construye la ruta completa al archivo a enviar + (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "repo_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(repo_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(repo_path): + file_path = os.path.join(repo_path, param_path) + else: + file_path = param_path + return file_path + + +def calculate_bitrate(bitrate): + """ Calcula el valor de la variable "bitrate", en base a la letra especificada ("K", "M" o "G"). + Luego elimina la letra y retorna el valor con tipo de datos "int". + """ + bitrate = bitrate.lower() + if "m" in bitrate: + bitrate = int(bitrate.strip("m")) * 1024 + elif "g" in bitrate: + bitrate = int(bitrate.strip("g")) * 1024 * 1024 + else: + bitrate = int(bitrate.strip("k")) + return bitrate + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo a enviar: + file_path = build_file_path() + + # Si no existe el archivo de imagen, imprimimos un mensaje de error y salimos del script: + if not os.path.exists(file_path): + print("Image file doesn't exist") + sys.exit(2) + + # Si no existe el archivo de log, lo creamos: + if not os.path.exists(log_file): + print("Creating log file...") + open(log_file, "w").close() + + # Almacenamos los elementos del segundo parámetro en variables (su formato es "puerto:ip:bitrate"): + param_list = sys.argv[2].split(':') + port, ip, bitrate = param_list + + # Calculamos el valor de la variable "bitrate", en base a la letra especificada (que luego eliminamos de la variable): + bitrate = calculate_bitrate(bitrate) + + # Creamos una lista con el comando a enviar (esto es requerido por la función "subprocess.run"), e imprimimos el comando con espacios: + splitted_cmd = f"uftp -M {ip} -p {port} -L {log_file} -o -D {cache_path} -Y aes256-cbc -h sha256 -e rsa -c -K 1024 -R {bitrate} {file_path}".split() + + print(f"Sending command: {' '.join(splitted_cmd)}") + + # Ejecutamos el comando en el sistema, e imprimimos el resultado: + try: + result = subprocess.run(splitted_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"ReturnCode: {result.returncode}") + except subprocess.CalledProcessError as error: + print(f"ReturnCode: {error.returncode}") + print(f"Error Output: {error.stderr.decode()}") + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/sendWakeOnLan.py b/bin/sendWakeOnLan.py new file mode 100644 index 0000000..7fc0923 --- /dev/null +++ b/bin/sendWakeOnLan.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script envía un paquete mágico WOL a la dirección MAC especificada como segundo parámetro, a través de la IP de broadcast especificada como primer parámetro. +La IP de broadcast puede corresponder a toda la red ("255.255.255.255"), que es el valor por defecto, o a una subred concreta (por ejemplo, "10.2.7.255"). + + Parámetros +------------ +sys.argv[1] - Dirección IP de broadcast de toda la red o de una subred concreta. + - Ejemplo1: 255.255.255.255 + - Ejemplo2: 10.2.7.255 + +sys.argv[2] - Dirección MAC del equipo que se quiere enceder via WOL. + - Ejemplo: 00:19:99:5c:bb:bb + + Sintaxis +---------- +./sendWakeOnLan.py broadcast_IP MAC + + Ejemplos + --------- +./sendWakeOnLan.py 255.255.255.255 00:19:99:5c:bb:bb +./sendWakeOnLan.py 10.2.7.255 00:19:99:5c:bb:bb +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} broadcast_IP MAC + Ejemplo1: {script_name} 255.255.255.255 00:19:99:5c:bb:bb + Ejemplo2: {script_name} 10.2.7.255 00:19:99:5c:bb:bb + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 2 parámetros, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 3: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 2 parámetros") + show_help() + sys.exit(1) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Almacenamos los parámetros en variables: + broadcast_IP = sys.argv[1] + MAC = sys.argv[2] + + # Creamos una lista con el comando a enviar, y lo imprimimos con espacios: + splitted_cmd = f"wakeonlan -i {broadcast_IP} {MAC}".split() + + print(f"Sending command: {' '.join(splitted_cmd)}") + + # Ejecutamos el comando en el sistema, e imprimimos el resultado: + try: + result = subprocess.run(splitted_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"ReturnCode: {result.returncode}") + except subprocess.CalledProcessError as error: + print(f"ReturnCode: {error.returncode}") + print(f"Error Output: {error.stderr.decode()}") + except Exception as error_description: + print(f"Unexpeted error: {error_description}") + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/stopP2P.py b/bin/stopP2P.py new file mode 100644 index 0000000..9905742 --- /dev/null +++ b/bin/stopP2P.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script finaliza el proceso "btlaunchmany.bittornado" (correspondiente al seeder P2P) y el proceso "bttrack" (correspondiente al tracker P2P), + lo que en la práctica hará que se cancelen las transmisiones P2P activas en el momento de ejecutarlo. + +No he encontrado la forma de cancelar una transferencia P2P concreta, ya que "bttrack" hace tracking de todos los torrents de ogRepo, + y es el proceso que es necesario finalizar para cancelar las transferencias P2P (con el seeder no bastaría, porque los clientes también hacen seed). + +No recibe ningún parámetro. +""" +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import subprocess + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def kill_seeder(): + """ Finaliza cualquier proceso activo de "btlaunchmany.bittornado", + que corresponde al seeder P2P. + """ + try: + subprocess.run(f"pkill btlaunchmany".split(), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print("Process btlaunchmany.bittornado finalized") + except Exception as error_description: + print(f"No btlaunchmany.bittornado process running? Returned error: {error_description}") + + + +def kill_tracker(): + """ Finaliza cualquier proceso activo de "bttrack", + que corresponde al tracker P2P. + """ + try: + subprocess.run(f"pkill bttrack".split(), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print("Process bttrack finalized") + except Exception as error_description: + print(f"No bttrack process running? Returned error: {error_description}") + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Finalizamos cualquier proceso activo de "btlaunchmany.bittornado" (seeder): + kill_seeder() + + # Finalizamos cualquier proceso activo de "bttrack" (tracker): + kill_tracker() + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/stopUDPcast.py b/bin/stopUDPcast.py new file mode 100644 index 0000000..83dbb91 --- /dev/null +++ b/bin/stopUDPcast.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +Este script finaliza el proceso "udp-sender" asociado a la imagen que recibe como parámetro, + lo que en la práctica hará que se cancele la transmisión existente de dicha imagen mediante UDPcast. + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a cancelar su transmisión (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images/ou_subdir/image1.img + + Sintaxis +---------- +./stopUDPcast.py [ou_subdir/]image_name|/image_path/image_name + + Ejemplos + --------- +./stopUDPcast.py image1.img +./stopUDPcast.py /opt/opengnsys/ogrepository/images/image1.img +./stopUDPcast.py ou_subdir/image1.img +./stopUDPcast.py /ou_subdir/image1.img +./stopUDPcast.py /opt/opengnsys/ogrepository/images/ou_subdir/image1.img +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name + Ejemplo1: {script_name} image1.img + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images/image1.img + Ejemplo3: {script_name} ou_subdir/image1.img + Ejemplo4: {script_name} /ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images/ou_subdir/image1.img + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 1 parámetro, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 2: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 parámetro") + show_help() + sys.exit(1) + + +def build_file_path(): + """ Construye la ruta completa del archivo a chequear + (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "repo_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(repo_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(repo_path): + file_path = os.path.join(repo_path, param_path) + else: + file_path = param_path + return file_path + + +def get_process_pid(file_path): + """ Busca un proceso que contenga "udp-sender" y la ruta de la imagen que recibe como parámetro, + y si lo encuentra almacena y retorna su pid asociado. + Si no encuentra ningún proceso que cumpla las condiciones (o si se produce una excepción) sale del script. + """ + try: + # Obtenemos todos los procesos, y almacenamos la salida y los errores: + result = subprocess.Popen(['ps', '-aux'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + out, error = result.communicate() + + # Almacenamos en una lista los procesos que contengan "udp-sender" y la imagen especificada como parámetro: + filtered_lines = [line for line in out.split('\n') if 'udp-sender' in line and file_path in line] + # Si hemos encontrado un proceso retornamos su pid, y si no imprimimos un mensaje de error y salimos del script: + if filtered_lines != []: + pid = filtered_lines[0].split()[1] + return pid + else: + print("udp-sender process not found") + sys.exit(3) + # Si se ha producido una excepción, imprimimos el error y salimos del script: + except Exception as error_description: + print(f"Unexpected error: {error_description}") + sys.exit(4) + + +def kill_udp_sender(pid): + """ Finaliza el proceso asociado al pid que recibe como parámetro, e imprime el return code. + Si se produce una excepción, imprime el error y sale del script. + """ + try: + # Finalizamos el proceso asociado al pid especificado como parámetro, e imprimimos el return code: + result = subprocess.run(f"kill {pid}".split(), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"ReturnCode: {result.returncode}") + # Si se ha producido una excepción, imprimimos el error y salimos del script: + except Exception as error_description: + print(f"Unexpected error: {error_description}") + sys.exit(5) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo de imagen: + file_path = build_file_path() + + # Si no existe el archivo de imagen, imprimimos un mensaje de error y salimos del script: + if not os.path.exists(file_path): + print("Image file doesn't exist") + sys.exit(2) + + # Obtenemos el pid del proceso "udp-sender" asociado a la imagen especificada: + pid = get_process_pid(file_path) + + # Finalizamos el proceso "udp-sender" encontrado: + kill_udp_sender(pid) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/stopUFTP.py b/bin/stopUFTP.py new file mode 100644 index 0000000..18fa7b9 --- /dev/null +++ b/bin/stopUFTP.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +Este script finaliza el proceso "uftp" asociado a la imagen que recibe como parámetro, + lo que en la práctica hará que se cancele la transmisión existente de dicha imagen mediante UFTP. + + Parámetros +------------ +sys.argv[1] - Nombre completo de la imagen a cancelar su transmisión (con o sin ruta), pero incluyendo el subdirectorio correspondiente a la OU, si es el caso. + - Ejemplo1: image1.img + - Ejemplo2: /opt/opengnsys/ogrepository/images/image1.img + - Ejemplo3: ou_subdir/image1.img + - Ejemplo4: /ou_subdir/image1.img + - Ejemplo5: /opt/opengnsys/ogrepository/images/ou_subdir/image1.img + + Sintaxis +---------- +./stopUDPcast.py [ou_subdir/]image_name|/image_path/image_name + + Ejemplos + --------- +./stopUDPcast.py image1.img +./stopUDPcast.py /opt/opengnsys/ogrepository/images/image1.img +./stopUDPcast.py ou_subdir/image1.img +./stopUDPcast.py /ou_subdir/image1.img +./stopUDPcast.py /opt/opengnsys/ogrepository/images/ou_subdir/image1.img +""" + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import subprocess + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +script_name = os.path.basename(__file__) +repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def show_help(): + """ Imprime la ayuda, cuando se ejecuta el script con el parámetro "help". + """ + help_text = f""" + Sintaxis: {script_name} [ou_subdir/]image_name|/image_path/image_name + Ejemplo1: {script_name} image1.img + Ejemplo2: {script_name} /opt/opengnsys/ogrepository/images/image1.img + Ejemplo3: {script_name} ou_subdir/image1.img + Ejemplo4: {script_name} /ou_subdir/image1.img + Ejemplo5: {script_name} /opt/opengnsys/ogrepository/images/ou_subdir/image1.img + """ + print(help_text) + + +def check_params(): + """ Comprueba que se haya enviado la cantidad correcta de parámetros, y en el formato correcto. + Si no es así, muestra un mensaje de error, y sale del script. + LLama a la función "show_help" cuando se ejecuta el script con el parámetro "help". + """ + # Si se ejecuta el script con el parámetro "help", se muestra la ayuda, y se sale del script: + if len(sys.argv) == 2 and sys.argv[1] == "help": + show_help() + sys.exit(0) + # Si se ejecuta el script con más o menos de 1 parámetro, se muestra un error y la ayuda, y se sale del script: + elif len(sys.argv) != 2: + print(f"{script_name} Error: Formato incorrecto: Se debe especificar 1 parámetro") + show_help() + sys.exit(1) + + +def build_file_path(): + """ Construye la ruta completa del archivo a chequear + (agregando "/opt/opengnsys/images/" si no se ha especificado en el parámetro). + """ + param_path = sys.argv[1] + # Si la ruta comienza con una barra, pero que no corresponde a "repo_path" + # (porque corresponderá al subdirectorio de una OU), eliminamos la barra: + if param_path.startswith('/') and not param_path.startswith(repo_path): + param_path = param_path.lstrip('/') + # Construimos la ruta completa: + if not param_path.startswith(repo_path): + file_path = os.path.join(repo_path, param_path) + else: + file_path = param_path + return file_path + + +def get_process_pid(file_path): + """ Busca un proceso que contenga "uftp" y la ruta de la imagen que recibe como parámetro, + y si lo encuentra almacena y retorna su pid asociado. + Si no encuentra ningún proceso que cumpla las condiciones (o si se produce una excepción) sale del script. + """ + try: + # Obtenemos todos los procesos, y almacenamos la salida y los errores: + result = subprocess.Popen(['ps', '-aux'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='UTF8') + out, error = result.communicate() + + # Almacenamos en una lista los procesos que contengan "uftp" y la imagen especificada como parámetro: + filtered_lines = [line for line in out.split('\n') if 'uftp' in line and file_path in line] + # Si hemos encontrado un proceso retornamos su pid, y si no imprimimos un mensaje de error y salimos del script: + if filtered_lines != []: + pid = filtered_lines[0].split()[1] + return pid + else: + print("uftp process not found") + sys.exit(3) + # Si se ha producido una excepción, imprimimos el error y salimos del script: + except Exception as error_description: + print(f"Unexpected error: {error_description}") + sys.exit(4) + + +def kill_uftp(pid): + """ Finaliza el proceso asociado al pid que recibe como parámetro, e imprime el return code. + Si se produce una excepción, imprime el error y sale del script. + """ + try: + # Finalizamos el proceso asociado al pid especificado como parámetro, e imprimimos el return code: + result = subprocess.run(f"kill {pid}".split(), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"ReturnCode: {result.returncode}") + # Si se ha producido una excepción, imprimimos el error y salimos del script: + except Exception as error_description: + print(f"Unexpected error: {error_description}") + sys.exit(5) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Evaluamos si se ha enviado la cantidad correcta de parámetros, y en el formato correcto: + check_params() + + # Obtenemos la ruta completa al archivo de imagen: + file_path = build_file_path() + + # Si no existe el archivo de imagen, imprimimos un mensaje de error y salimos del script: + if not os.path.exists(file_path): + print("Image file doesn't exist") + sys.exit(2) + + # Obtenemos el pid del proceso "uftp" asociado a la imagen especificada: + pid = get_process_pid(file_path) + + # Finalizamos el proceso "uftp" encontrado: + kill_uftp(pid) + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/udp-sender b/bin/udp-sender index 87789a0..4c5bbd5 100644 Binary files a/bin/udp-sender and b/bin/udp-sender differ diff --git a/bin/updateRepoInfo.py b/bin/updateRepoInfo.py new file mode 100644 index 0000000..ca498d7 --- /dev/null +++ b/bin/updateRepoInfo.py @@ -0,0 +1,323 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script actualiza la información de las imágenes del repositorio, reflejándola en el archivo "/opt/opengnsys/ogrepository/etc/repoinfo.json", + añadiendo información de las imágenes nuevas, y borrando la información de las imágenes que fueron eliminadas. +La información es obtenida desde archivos ".info", que originalmente eran eliminados, + pero que en esta versión son renombrados a ".info.checked", obteniendo la misma funcionalidad. +Al acabar, llama al script "updateTrashInfo.py", para actualizar también la información de la papelera del repositorio. + +No recibe ningún parámetro, y es llamado por el propio ogRepoitory cada vez que se elimina una imagen (desde el script "deleteImage.py"). +""" + + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import sys +import json +import subprocess +import shutil + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +repo_path = '/opt/opengnsys/ogrepository/images' # En este caso, no lleva barra final +info_file = '/opt/opengnsys/ogrepository/etc/repoinfo.json' +update_trash_script = '/opt/opengnsys/ogrepository/bin/updateTrashInfo.py' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + +def create_empty_json(): + """ Esta función crea el archivo "repoinfo.json", con la estructura básica que debe contener. + Evidentemente, solo es llamada cuando no existe el archivo. + """ + # Creamos un diccionario con la estructura básica que debe tener el archivo json: + json_data = {"directory": repo_path, "images": [], "ous": []} + + # Abrimos el archivo json en modo escritura (creándolo si no existe, como es el caso), + # y le añadimos la estructura almacenada en el diccionario "json_data": + with open(info_file, 'w') as file: + json.dump(json_data, file, indent=2) + + + +def check_files(): + """ Esta función recorre el directorio de imágenes, para buscar archivos con extensiones ".img" o ".dsk". + Llama a la función "add_to_json" para que esta añada información de las imágenes no bloqueadas (sin archivo ".lock"), + que además tengan un archivo ".info" asociado (siempre que este no se haya modificado antes que la propia imagen). + Originalmente eliminaba los archivos ".info" después de extraer la información, pero ahora los renombra (agrega ".checked"). + """ + # Iteramos recursivamente todos los archivos y directorios de "/opt/opengnsys/images", + # y luego iteramos todos los archivos encontrados (en una iteración anidada): + for root, dirs, files in os.walk(repo_path): + for file in files: + # Si la imagen actual tiene extensión ".img" o ".dsk", construimos la ruta completa ("img_path"): + if file.endswith((".img", ".dsk")): + img_path = os.path.join(root, file) + # Si existe un archivo ".lock" asociado a la imagen actual, pasamos a la siguiente: + if os.path.exists(f"{img_path}.lock"): + continue + # Comprobamos si existe un archivo ".info" asociado a la imagen actual: + info_file = f"{img_path}.info" + if os.path.exists(info_file): + # Si la fecha de modificación del archivo ".info" es anterior a la de la imagen, lo eliminamos (porque estará desactualizado): + if os.path.getmtime(info_file) < os.path.getmtime(img_path): + os.remove(info_file) + print(f"Warning: Deleted outdated file {info_file}") + # En caso contrario, almacenamos el contenido del archivo ".info" (del tipo "PARTCLONE:LZOP:EXTFS:8500000:Ubuntu_20") en la variable "data": + else: + with open(info_file, 'r') as file: + info_data = file.read() + # Almacenamos el contenido de los archivos ".size", ".sum" y ".full.sum": + with open(f"{img_path}.size", 'r') as file: + size = file.read().strip('\n') + with open(f"{img_path}.sum", 'r') as file: + _sum = file.read().strip('\n') + with open(f"{img_path}.full.sum", 'r') as file: + fullsum = file.read().strip('\n') + # Llamamos a la función "add_to_json", para que inserte la información de la imagen en el archivo json + # (pasándole el nombre de la imagen, la extensión, y los datos extraídos del archivo ".info"): + img_name = os.path.relpath(img_path, repo_path) + add_to_json(os.path.splitext(img_name)[0], os.path.splitext(img_name)[1][1:], info_data, size, _sum, fullsum) + + # Renombramos el archivo ".info" a ".info.checked", para que ya no se añada la información que contiene + # (originalmente se eliminaba el archivo, pero creo que es mejor mantenerlo): + os.rename(info_file, f"{info_file}.checked") + + + +def check_dirs(): + """ Esta función recorre el directorio de imágenes, para buscar archivos con nombre "ogimg.info". + Llama a la función "add_to_json" para que esta añada información de las imágenes no bloqueadas (sin archivo ".lock"), + que además tengan un archivo "ogimg.info" asociado (pero no sé que tipo de imágenes son esas). + """ + # Iteramos recursivamente todos los archivos y directorios de "/opt/opengnsys/images", + # y luego iteramos todos los archivos encontrados (en una iteración anidada): + for root, dirs, files in os.walk(repo_path): + for file in files: + # Si el nombre del archivo actual es "ogimg.info", construimos su ruta completa ("info_path"), y la ruta del directorio ("img_path"): + if file == "ogimg.info": + info_path = os.path.join(root, file) + img_path = os.path.dirname(info_path) + # Si existe un archivo ".lock" asociado a la imagen actual, o si la ruta coincide con "repo_path", pasamos a la siguiente: + if img_path == repo_path or os.path.exists(f"{img_path}.lock"): + continue + # Almacenamos el contenido del archivo "ogimg.info", en la variable "lines": + with open(info_path, 'r') as file: + lines = file.readlines() + # Iteramos las lineas obtenidas, para extraer el tipo de sistema de archivos ("fstype") y el tamaño de los datos ("sizedata"), + # y con ello construimos una cadena "rsync::{fstype}:{sizedata}:" (que almacenamos en "data"): + for line in lines: + if line.startswith("# fstype"): + fstype = line.split("=")[1].strip() + elif line.startswith("# sizedata"): + sizedata = line.split("=")[1].strip() + info_data = f"rsync::{fstype}:{sizedata}:" + # Almacenamos el contenido de los archivos ".size", ".sum" y ".full.sum": + with open(f"{img_path}.size", 'r') as file: + size = file.read().strip('\n') + with open(f"{img_path}.sum", 'r') as file: + _sum = file.read().strip('\n') + with open(f"{img_path}.full.sum", 'r') as file: + fullsum = file.read().strip('\n') + # Llamamos a la función "add_to_json", para que inserte la información de la imagen en el archivo json + # (pasándole el nombre de la imagen, el tipo "dir"", y los datos extraídos del archivo "ogimg.info"): + img_name = os.path.relpath(img_path, repo_path) + add_to_json(img_name, "dir", info_data, size, _sum, fullsum) + + + +def add_to_json(image_name, image_type, data, size, _sum, fullsum): + """ Esta función añade al archivo "repoinfo.json" la información de las imágenes que aun no ha sido introducida en él (imágenes nuevas, básicamente). + El procedimiento es diferente para las imágenes "normales" y para las imágenes basadas en OU. + """ + # Almacenamos el contenido de la variable "data" (del tipo "PARTCLONE:LZOP:EXTFS:8500000:Ubuntu_20") en variables separadas: + clonator, compressor, fstype, datasize, client = data.split(":") + + # Si la imagen está dentro de una OU (directorio), extraemos el nombre de la OU y de la imagen: + ou_name = None + if '/' in image_name: + ou_name = os.path.dirname(image_name) + image_name = os.path.basename(image_name) + + # Creamos un diccionario con los datos de la imagen, que luego insertaremos en el json: + json_data = { + "name": image_name, + "type": image_type.lower(), + "clientname": client.strip('\n'), # Eliminamos el salto de linea + "clonator": clonator.lower(), + "compressor": compressor.lower(), + "filesystem": fstype.upper(), + "datasize": int(datasize) * 1024, # Convertimos el valor a bytes (desde KB) + "size": int(size), + "sum": _sum, + "fullsum": fullsum + } + # Almacenamos el contenido del archivo "repoinfo.json" en la variable "info_data" + # (y si no tiene contenido creamos la estructura básica): + if os.path.getsize(info_file) > 0: + with open(info_file, 'r') as file: + info_data = json.load(file) + else: + info_data = {"directory": repo_path, "images": [], "ous": []} + + # Comprobamos si las claves "info_data" (o sea, del archivo json) son las correctas: + if set(info_data.keys()) == {"directory", "images", "ous"}: + + # Actualizamos la información de las imágenes "normales" (no basadas en OU): + if ou_name is None: + img_index = next((i for i, img in enumerate(info_data["images"]) if img["name"] == image_name), None) + if img_index is not None: + # Update if image data changes + if info_data["images"][img_index] != json_data: + info_data["images"][img_index] = json_data + else: + # Append a new entry + info_data["images"].append(json_data) + + # Actualizamos la información de las imágenes basadas en OU: + else: + ou_index = next((i for i, ou in enumerate(info_data["ous"]) if ou["subdir"] == ou_name), None) + if ou_index is None: + # Append a new OU entry + info_data["ous"].append({"subdir": ou_name, "images": [json_data]}) + else: + img_index = next((i for i, img in enumerate(info_data["ous"][ou_index]["images"]) if img["name"] == image_name), None) + if img_index is not None: + # Update if image data changes + if info_data["ous"][ou_index]["images"][img_index] != json_data: + info_data["ous"][ou_index]["images"][img_index] = json_data + else: + # Append a new entry + info_data["ous"][ou_index]["images"].append(json_data) + + # Si las claves de "info_data" no son las correctas, creamos toda la estructura: + else: + if ou_name is None: + info_data = {"directory": repo_path, "images": [json_data], "ous": []} + else: + info_data = {"directory": repo_path, "images": [], "ous": [{"subdir": ou_name, "images": [json_data]}]} + + # Sobreescribimos el archivo "repoinfo.json", con el contenido de "info_data" (que estará actualizado): + with open(info_file, 'w') as file: + json.dump(info_data, file, indent=2) + + + +def remove_from_json(): + """ Esta función carga el contenido del archivo "repoinfo.json", y comprueba la existencia de las imágenes especificadas allí. + Elimina las claves correspondientes a imágenes inexistentes, y posteriormente sobreescribe el archivo "repoinfo.json". + """ + # Almacenamos el contenido del archivo "repoinfo.json", en la variable "json_data": + with open(info_file, 'r') as file: + json_data = json.load(file) + + # Iteramos las imágenes de la clave "images" de "json_data", construimos sus rutas y comprobamos su existencia + # (si no existen, las eliminamos de "json_data"): + for i, image in enumerate(json_data.get("images", [])): + img_name = image["name"] + img_type = image["type"] + if img_type != "dir": + img_name = f"{img_name}.{img_type}" + img_path = os.path.join(repo_path, img_name) + if not os.path.exists(img_path) and not os.path.exists(f"{img_path}.lock"): + json_data["images"].pop(i) + + # Iteramos las imágenes de la clave "ous" de "json_data", construimos sus rutas y comprobamos su existencia + # (si no existen, las eliminamos de "json_data"): + for j, ou in enumerate(json_data.get("ous", [])): + ou_name = ou["subdir"] + ou_path = os.path.join(repo_path, ou_name) + if not os.path.exists(ou_path): + json_data["ous"].pop(j) + else: + for i, image in enumerate(ou.get("images", [])): + img_name = image["name"] + img_type = image["type"] + if img_type != "dir": + img_name = f"{img_name}.{img_type}" + img_path = os.path.join(ou_path, img_name) + if not os.path.exists(img_path) and not os.path.exists(f"{img_path}.lock"): + ou["images"].pop(i) + + # Sobreescribimos el archivo "repoinfo.json", con el contenido de "json_data" + # (una vez eliminadas las imágenes inexistentes): + with open(info_file, 'w') as file: + json.dump(json_data, file, indent=2) + + + +def update_trash_info(): + """ Actualiza la información de la papelera del repositorio, ejecutando el script "updateTrashInfo.py". + Como se ve, es necesario que el script se ejecute como sudo, o dará error. + """ + try: + result = subprocess.run(['sudo', 'python3', update_trash_script], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except subprocess.CalledProcessError as error: + print(f"Error Output: {error.stderr.decode()}") + sys.exit(3) + except Exception as error: + print(f"Se ha producido un error inesperado: {error}") + sys.exit(4) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Comprobamos si tenemos permisos de escritura sobre el directorio que contiene el archivo "repoinfo.json" + # ("/opt/opengnsys/etc"), y en caso contrario lanzamos una excepción: + if not os.access(os.path.dirname(info_file), os.W_OK): + raise PermissionError(f"Cannot access {info_file}") + + # Comprobamos si existe el archivo "repoinfo.json", y en caso contrario lo creamos: + if not os.path.exists(info_file): + print("Creating empty json file...") + create_empty_json() + + # Comprobamos si tenemos permisos de escritura sobre el archivo, y en caso contrario lanzamos un error: + if not os.access(info_file, os.W_OK): + raise PermissionError(f"Cannot access {info_file}") + + # Llamamos a la función "check_files", para añadir al archivo json las imágenes ".img" o ".dsk" aun no añadidas + # (que son las que tienen asociado un archivo ".info", aun no renombrado o eliminado): + print("Checking file images...") + check_files() + + # Llamamos a la función "check_dirs", para añadir al archivo json las imágenes aun no añadidas que tienen asociado un archivo "ogimg.info" + # (no sé qué imágenes son estas, pero de alguna forma están basadas en directorios): + print("Checking dir images...") + check_dirs() + + # Llamamos a la función "remove_from_json", para eliminar del archivo json las imágenes que fueron eliminadas del repositorio: + if os.path.getsize(info_file) > 0: + print("Removing deleted images...") + remove_from_json() + + # Actualizamos la información de la papelera, ejecutando el script "updateTrashInfo.py" + # (solo si el archivo tiene contenido, o dará error): + print("Updating Trash Info...") + update_trash_info() + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/bin/updateTrashInfo.py b/bin/updateTrashInfo.py new file mode 100644 index 0000000..bc0d3ea --- /dev/null +++ b/bin/updateTrashInfo.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Este script actualiza la información de las imágenes de la papelera del repositorio, reflejándola en el archivo "/opt/opengnsys/ogrepository/etc/trashinfo.json", + añadiendo información de las nuevas imágenes eliminadas, y borrando la información de las imágenes que fueron restauradas (o que ya no están en la papelera). +La información es obtenida desde archivos ".info.checked", cuyo nombre original era ".info" + (pero que fueron renombrados al ser insertada su información en el archivo ""/opt/opengnsys/ogrepository/etc/repoinfo.json", antes de ser eliminadas). + +No recibe ningún parámetro, y no necesita ser llamado explícitamente (porque lo llama el script "updateRepoInfo.py")". +""" + + +# -------------------------------------------------------------------------------------------- +# IMPORTS +# -------------------------------------------------------------------------------------------- + +import os +import pwd +import grp +import json +import subprocess +import shutil + + +# -------------------------------------------------------------------------------------------- +# VARIABLES +# -------------------------------------------------------------------------------------------- + +trash_path = '/opt/opengnsys/ogrepository/images_trash' # En este caso, no lleva barra final +info_file = '/opt/opengnsys/ogrepository/etc/trashinfo.json' + + +# -------------------------------------------------------------------------------------------- +# FUNCTIONS +# -------------------------------------------------------------------------------------------- + + + +def create_trash_folder(): + """ Crea el directorio correspondiente a la papelera, y le asigna propietarios y permisos. + Evidentemente, esta función solo es llamada cuando no existe el directorio. + """ + # Obtenemos el UID del usuario "root" y el GID del grupo "opengnsys": + uid = pwd.getpwnam('root').pw_uid + gid = grp.getgrnam('opengnsys').gr_gid + # Creamos el directorio correspondiente a la papelera: + os.mkdir(trash_path) + # Asignamos el usuario y el grupo propietarios del directorio: + os.chown(trash_path, uid, gid) + # Asignamos permisos "775" al directorio : + os.chmod(trash_path, 0o775) + + + +def create_empty_json(): + """ Esta función crea el archivo "trashinfo.json", con la estructura básica que debe contener. + Evidentemente, solo es llamada cuando no existe el archivo. + """ + # Creamos un diccionario con la estructura básica que debe tener el archivo json: + json_data = {"directory": trash_path, "images": [], "ous": []} + + # Abrimos el archivo json en modo escritura (creándolo si no existe, como es el caso), + # y le añadimos la estructura almacenada en el diccionario "json_data": + with open(info_file, 'w') as file: + json.dump(json_data, file, indent=2) + + + +def check_files(): + """ Esta función recorre el directorio de imágenes, para buscar archivos con extensiones ".img" o ".dsk". + Llama a la función "add_to_json" para que esta añada información de las imágenes no bloqueadas (sin archivo ".lock"), + que además tengan un archivo ".info.checked" asociado (siempre que este no se haya modificado antes que la propia imagen). + """ + # Iteramos recursivamente todos los archivos y directorios de "/opt/opengnsys/images_trash", + # y luego iteramos todos los archivos encontrados (en una iteración anidada): + for root, dirs, files in os.walk(trash_path): + for file in files: + # Si la imagen actual tiene extensión ".img" o ".dsk", construimos la ruta completa ("img_path"): + if file.endswith((".img", ".dsk")): + img_path = os.path.join(root, file) + # Si existe un archivo ".lock" asociado a la imagen actual, pasamos a la siguiente: + if os.path.exists(f"{img_path}.lock"): + continue + # Comprobamos si existe un archivo ".info.checked" asociado a la imagen actual: + info_file = f"{img_path}.info.checked" + if os.path.exists(info_file): + # Si la fecha de modificación del archivo ".info.checked" es anterior a la de la imagen, lo eliminamos (porque estará desactualizado): + if os.path.getmtime(info_file) < os.path.getmtime(img_path): + os.remove(info_file) + print(f"Warning: Deleted outdated file {info_file}") + # En caso contrario, almacenamos el contenido del archivo ".info.checked" (del tipo "PARTCLONE:LZOP:EXTFS:8500000:Ubuntu_20") en la variable "data": + else: + with open(info_file, 'r') as file: + info_data = file.read() + # Almacenamos el contenido de los archivos ".size", ".sum" y ".full.sum": + with open(f"{img_path}.size", 'r') as file: + size = file.read().strip('\n') + with open(f"{img_path}.sum", 'r') as file: + _sum = file.read().strip('\n') + with open(f"{img_path}.full.sum", 'r') as file: + fullsum = file.read().strip('\n') + # Llamamos a la función "add_to_json", para que inserte la información de la imagen en el archivo json + # (pasándole el nombre de la imagen, la extensión, y los datos extraídos del archivo ".info.checked"): + img_name = os.path.relpath(img_path, trash_path) + add_to_json(os.path.splitext(img_name)[0], os.path.splitext(img_name)[1][1:], info_data, size, _sum, fullsum) + + + +def check_dirs(): + """ Esta función recorre el directorio de imágenes, para buscar archivos con nombre "ogimg.info". + Llama a la función "add_to_json" para que esta añada información de las imágenes no bloqueadas (sin archivo ".lock"), + que además tengan un archivo "ogimg.info" asociado (pero no sé que tipo de imágenes son esas). + """ + # Iteramos recursivamente todos los archivos y directorios de "/opt/opengnsys/images_trash", + # y luego iteramos todos los archivos encontrados (en una iteración anidada): + for root, dirs, files in os.walk(trash_path): + for file in files: + # Si el nombre del archivo actual es "ogimg.info", construimos su ruta completa ("info_path"), y la ruta del directorio ("img_path"): + if file == "ogimg.info": + info_path = os.path.join(root, file) + img_path = os.path.dirname(info_path) + # Si existe un archivo ".lock" asociado a la imagen actual, o si la ruta coincide con "trash_path", pasamos a la siguiente: + if img_path == trash_path or os.path.exists(f"{img_path}.lock"): + continue + # Almacenamos el contenido del archivo "ogimg.info", en la variable "lines": + with open(info_path, 'r') as file: + lines = file.readlines() + # Iteramos las lineas obtenidas, para extraer el tipo de sistema de archivos ("fstype") y el tamaño de los datos ("sizedata"), + # y con ello construimos una cadena "rsync::{fstype}:{sizedata}:" (que almacenamos en "data"): + for line in lines: + if line.startswith("# fstype"): + fstype = line.split("=")[1].strip() + elif line.startswith("# sizedata"): + sizedata = line.split("=")[1].strip() + info_data = f"rsync::{fstype}:{sizedata}:" + # Almacenamos el contenido de los archivos ".size", ".sum" y ".full.sum": + with open(f"{img_path}.size", 'r') as file: + size = file.read().strip('\n') + with open(f"{img_path}.sum", 'r') as file: + _sum = file.read().strip('\n') + with open(f"{img_path}.full.sum", 'r') as file: + fullsum = file.read().strip('\n') + # Llamamos a la función "add_to_json", para que inserte la información de la imagen en el archivo json + # (pasándole el nombre de la imagen, el tipo "dir"", y los datos extraídos del archivo "ogimg.info"): + img_name = os.path.relpath(img_path, trash_path) + add_to_json(img_name, "dir", info_data, size, _sum, fullsum) + + + +def add_to_json(image_name, image_type, data, size, _sum, fullsum): + """ Esta función añade al archivo "trashinfo.json" la información de las imágenes que aun no ha sido introducida en él (imágenes eliminadas recientemente, básicamente). + El procedimiento es diferente para las imágenes "normales" y para las imágenes basadas en OU. + """ + # Almacenamos el contenido de la variable "data" (del tipo "PARTCLONE:LZOP:EXTFS:8500000:Ubuntu_20") en variables separadas: + clonator, compressor, fstype, datasize, client = data.split(":") + + # Si la imagen está dentro de una OU (directorio), extraemos el nombre de la OU y de la imagen: + ou_name = None + if '/' in image_name: + ou_name = os.path.dirname(image_name) + image_name = os.path.basename(image_name) + + # Creamos un diccionario con los datos de la imagen, que luego insertaremos en el json: + json_data = { + "name": image_name, + "type": image_type.lower(), + "clientname": client.strip('\n'), # Eliminamos el salto de linea + "clonator": clonator.lower(), + "compressor": compressor.lower(), + "filesystem": fstype.upper(), + "datasize": int(datasize) * 1024, # Convertimos el valor a bytes (desde KB) + "size": int(size), + "sum": _sum, + "fullsum": fullsum + } + # Almacenamos el contenido del archivo "trashinfo.json" en la variable "info_data" + # (y si no tiene contenido creamos la estructura básica): + if os.path.getsize(info_file) > 0: + with open(info_file, 'r') as file: + info_data = json.load(file) + else: + info_data = {"directory": trash_path, "images": [], "ous": []} + + # Comprobamos si las claves "info_data" (o sea, del archivo json) son las correctas: + if set(info_data.keys()) == {"directory", "images", "ous"}: + + # Actualizamos la información de las imágenes "normales" (no basadas en OU): + if ou_name is None: + img_index = next((i for i, img in enumerate(info_data["images"]) if img["name"] == image_name), None) + if img_index is not None: + # Update if image data changes + if info_data["images"][img_index] != json_data: + info_data["images"][img_index] = json_data + else: + # Append a new entry + info_data["images"].append(json_data) + + # Actualizamos la información de las imágenes basadas en OU: + else: + ou_index = next((i for i, ou in enumerate(info_data["ous"]) if ou["subdir"] == ou_name), None) + if ou_index is None: + # Append a new OU entry + info_data["ous"].append({"subdir": ou_name, "images": [json_data]}) + else: + img_index = next((i for i, img in enumerate(info_data["ous"][ou_index]["images"]) if img["name"] == image_name), None) + if img_index is not None: + # Update if image data changes + if info_data["ous"][ou_index]["images"][img_index] != json_data: + info_data["ous"][ou_index]["images"][img_index] = json_data + else: + # Append a new entry + info_data["ous"][ou_index]["images"].append(json_data) + + # Si las claves de "info_data" no son las correctas, creamos toda la estructura: + else: + if ou_name is None: + info_data = {"directory": trash_path, "images": [json_data], "ous": []} + else: + info_data = {"directory": trash_path, "images": [], "ous": [{"subdir": ou_name, "images": [json_data]}]} + + # Sobreescribimos el archivo "trashinfo.json", con el contenido de "info_data" (que estará actualizado): + with open(info_file, 'w') as file: + json.dump(info_data, file, indent=2) + + + +def remove_from_json(): + """ Esta función carga el contenido del archivo "trashinfo.json", y comprueba la existencia de las imágenes especificadas allí. + Elimina las claves correspondientes a imágenes inexistentes en la papelera, y posteriormente sobreescribe el archivo "trashinfo.json". + """ + # Almacenamos el contenido del archivo "trashinfo.json", en la variable "json_data": + with open(info_file, 'r') as file: + json_data = json.load(file) + + # Iteramos las imágenes de la clave "images" de "json_data", construimos sus rutas y comprobamos su existencia + # (si no existen, las eliminamos de "json_data"): + for i, image in enumerate(json_data.get("images", [])): + img_name = image["name"] + img_type = image["type"] + if img_type != "dir": + img_name = f"{img_name}.{img_type}" + img_path = os.path.join(trash_path, img_name) + if not os.path.exists(img_path) and not os.path.exists(f"{img_path}.lock"): + json_data["images"].pop(i) + + # Iteramos las imágenes de la clave "ous" de "json_data", construimos sus rutas y comprobamos su existencia + # (si no existen, las eliminamos de "json_data"): + for j, ou in enumerate(json_data.get("ous", [])): + ou_name = ou["subdir"] + ou_path = os.path.join(trash_path, ou_name) + if not os.path.exists(ou_path): + json_data["ous"].pop(j) + else: + for i, image in enumerate(ou.get("images", [])): + img_name = image["name"] + img_type = image["type"] + if img_type != "dir": + img_name = f"{img_name}.{img_type}" + img_path = os.path.join(ou_path, img_name) + if not os.path.exists(img_path) and not os.path.exists(f"{img_path}.lock"): + ou["images"].pop(i) + + # Sobreescribimos el archivo "trashinfo.json", con el contenido de "json_data" + # (una vez eliminadas las imágenes inexistentes): + with open(info_file, 'w') as file: + json.dump(json_data, file, indent=2) + + + +# -------------------------------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------------------------------- + + +def main(): + """ + """ + # Comprobamos si existe el directorio correspondiente a la papelera, y en caso contrario lo creamos: + if not os.path.exists(trash_path): + print("Creating trash folder...") + create_trash_folder() + + # Comprobamos si tenemos permisos de escritura sobre el directorio que contiene el archivo "trashinfo.json" + # ("/opt/opengnsys/etc"), y en caso contrario lanzamos una excepción: + if not os.access(os.path.dirname(info_file), os.W_OK): + raise PermissionError(f"Cannot access {info_file}") + + # Comprobamos si existe el archivo "trashinfo.json", y en caso contrario lo creamos: + if not os.path.exists(info_file): + print("Creating empty json file...") + create_empty_json() + + # Comprobamos si tenemos permisos de escritura sobre el archivo, y en caso contrario lanzamos un error: + if not os.access(info_file, os.W_OK): + raise PermissionError(f"Cannot access {info_file}") + + # Llamamos a la función "check_files", para añadir al archivo json las imágenes ".img" o ".dsk" aun no añadidas + # (que son las que tienen asociado un archivo ".info.checked"): + print("Checking file images...") + check_files() + + # Llamamos a la función "check_dirs", para añadir al archivo json las imágenes aun no añadidas que tienen asociado un archivo "ogimg.info" + # (no sé qué imágenes son estas, pero de alguna forma están basadas en directorios): + print("Checking dir images...") + check_dirs() + + # Llamamos a la función "remove_from_json", para eliminar del archivo json las imágenes que ya no están en la papelera + # (solo si el archivo tiene contenido, o dará error): + if os.path.getsize(info_file) > 0: + print("Removing inexistent images...") + remove_from_json() + + + +# -------------------------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + +# -------------------------------------------------------------------------------------------- diff --git a/installer/files/ctorrent.sources b/installer/files/ctorrent.sources new file mode 100644 index 0000000..dc5dea0 --- /dev/null +++ b/installer/files/ctorrent.sources @@ -0,0 +1,6 @@ +Types: deb +URIs: http://ftp.de.debian.org/debian +Suites: buster +Components: main +Signed-By: /usr/share/keyrings/debian-archive-buster-stable.gpg + diff --git a/installer/files/ogrepo-api.service b/installer/files/ogrepo-api.service new file mode 100644 index 0000000..136de86 --- /dev/null +++ b/installer/files/ogrepo-api.service @@ -0,0 +1,12 @@ +[Unit] +Description=Gunicorn instance to serve repo_api +After=network.target + +[Service] +User=ogrepository +Group=ogrepository +WorkingDirectory=/opt/opengnsys/ogrepository/api +ExecStart=/usr/bin/gunicorn -w 4 -b 0.0.0.0:8006 repo_api:app + +[Install] +WantedBy=multi-user.target diff --git a/installer/files/ogrepo-smb.conf b/installer/files/ogrepo-smb.conf new file mode 100644 index 0000000..3b76b2b --- /dev/null +++ b/installer/files/ogrepo-smb.conf @@ -0,0 +1,8 @@ +[ogimages] +comment = OpenGnsys Repository +browseable = no +writeable = yes +locking = no +path = /opt/opengnsys/ogrepository/images +guest ok = no +valid users = %%OGREPOSITORY_USER%% diff --git a/installer/installer.sh b/installer/installer.sh new file mode 100644 index 0000000..0015672 --- /dev/null +++ b/installer/installer.sh @@ -0,0 +1,109 @@ +#!/bin/bash + +set -e + +GIT_BRANCH=$1 +GIT_REPO=https://ognproject.evlt.uma.es/gitea/opengnsys/ogrepository.git +GIT_SSL_NO_VERIFY=true +REPO_IP=${REPO_IP:-"127.0.0.1"} +SMBUSER=${SMBUSER:-"ogrepository"} +SMBPASS=${SMBPASS:-"ogrepository"} +INSTALL_DIR=/opt/opengnsys/ogrepository +DOWNLOAD_DIR=/tmp/ogrepository +DEBIAN_FRONTEND=noninteractive +export DEBIAN_FRONTEND +export GIT_SSL_NO_VERIFY + +clone_repository() { + local BRANCH=$1 + rm -rf $DOWNLOAD_DIR + git clone -b "$BRANCH" $GIT_REPO $DOWNLOAD_DIR + chown -R ogrepository:ogrepository $DOWNLOAD_DIR +} + +check_root() { + if [ "$(id -u)" != "0" ]; then + echo "This script must be run as root" 1>&2 + exit 1 + fi +} + +install_uftp() { + apt install uftp -y +} + +install_updcast () { + apt install $DOWNLOAD_DIR/packets/udpcast_20230924_amd64.deb +} + +add_user_ogrepository() { + if ! id "ogrepository" &>/dev/null; then + echo "User ogrepository does not exist, creating it" + useradd -r -s /bin/bash ogrepository + fi + if [ ! -f /etc/sudoers.d/ogrepository ]; then + echo "User ogrepository does not have sudo permissions, adding it" + echo 'ogrepository ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/ogrepository + fi + +} + +create_directories() { + mkdir -p $INSTALL_DIR + mkdir -p $INSTALL_DIR/images $INSTALL_DIR/images_trash/ $INSTALL_DIR/bin/ $INSTALL_DIR/etc/ $INSTALL_DIR/log/ $INSTALL_DIR/api/ + chown -R ogrepository:ogrepository $INSTALL_DIR +} + +install_dependencies() { + apt update -y + apt install -y git python3 python3-pip python3-flask python3-paramiko python3-psutil python3-flasgger debian-archive-keyring samba gunicorn +} + +install_ext_repo() { + cp $DOWNLOAD_DIR/installer/files/ctorrent.sources /etc/apt/sources.list.d/ctorrent.sources + apt update -y +} + +install_external_packages() { + apt install -y bittorrent bittornado ctorrent +} + +install_ogrepo-api_service() { + cp -r $DOWNLOAD_DIR/installer/files/ogrepo-api.service /etc/systemd/system/ogrepo-api.service + systemctl enable --now ogrepo-api +} + +install_files() { + cp -pr $DOWNLOAD_DIR/bin/* $INSTALL_DIR/bin/ + cp -pr $DOWNLOAD_DIR/etc/* $INSTALL_DIR/etc/ + cp -pr $DOWNLOAD_DIR/api/* $INSTALL_DIR/api/ + chown -R ogrepository:ogrepository $INSTALL_DIR + echo IPlocal="$REPO_IP" > $INSTALL_DIR/etc/ogAdmRepo.cfg + sudo chown ogrepository:ogrepository $INSTALL_DIR/etc/ogAdmRepo.cfg +} + +configure_samba() { + echo "include = /etc/samba/smb.conf.ogrepository" >> /etc/samba/smb.conf + cp $DOWNLOAD_DIR/installer/files/ogrepo-smb.conf /etc/samba/smb.conf.ogrepository + sed -i "s/%%OGREPOSITORY_USER%%/$SMBUSER/g" /etc/samba/smb.conf.ogrepository + systemctl restart smbd + # Create default user ogrepository + (echo $SMBPASS; echo $SMBPASS) | smbpasswd -s -a $SMBUSER + +} + +## Main program +check_root +install_dependencies +add_user_ogrepository +clone_repository "$GIT_BRANCH" +install_ext_repo +install_external_packages +install_uftp +install_updcast +create_directories +install_files +install_ogrepo-api_service +configure_samba + + diff --git a/packets/udpcast_20230924_amd64.deb b/packets/udpcast_20230924_amd64.deb new file mode 100644 index 0000000..a6e16b8 Binary files /dev/null and b/packets/udpcast_20230924_amd64.deb differ