ogrepository/bin/importImage.py

285 lines
13 KiB
Python

#!/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 similar al script bash original (cuyo nombre es "importimage", a secas), pero con ciertas diferencias.
Comprueba si la importación se ha realizado correctamente, comparando el contenido de los archivos "sum" y "size" con los valores reales (que vuelve a calcular).
Si la importación ha sido correcta llama al script "createTorrentSum.py", para crear el archivo ".torrent" (que debe crearse desde el repositorio en el que esté), y actualizar la info del repositorio.
Si la importación no ha sido correcta (porque la imagen no ha pasado el check de integridad), borra los archivos importados.
Librerías Python requeridas: "paramiko" (se puede instalar con "sudo apt install python3-paramiko")
Parámetros
------------
sys.argv[1] - Nombre completo de la imagen a importar (con o sin ruta).
- Ejemplo1: image1.img
- Ejemplo2: /opt/opengnsys/ogrepository/images/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 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 remote_hostname user
"""
# --------------------------------------------------------------------------------------------
# IMPORTS
# --------------------------------------------------------------------------------------------
import warnings
warnings.filterwarnings("ignore")
import os
import sys
import hashlib
import subprocess
import paramiko
import warnings
from systemd import journal
# --------------------------------------------------------------------------------------------
# VARIABLES
# --------------------------------------------------------------------------------------------
script_name = os.path.basename(__file__)
repo_path = '/opt/opengnsys/ogrepository/images/' # No borrar la barra final
create_torrent_script = '/opt/opengnsys/ogrepository/bin/createTorrentSum.py'
# --------------------------------------------------------------------------------------------
# FUNCTIONS
# --------------------------------------------------------------------------------------------
def show_help():
""" Imprime la ayuda, cuando se ejecuta el script con el parámetro "help".
"""
help_text = f"""
Sintaxis: {script_name} 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 remote_hostname user
"""
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/ogrepository/images/" si no se ha especificado en el parámetro).
"""
param_path = sys.argv[1]
# 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', '.info.checked'] # Quitamos la extensión ".torrent", porque hay que generarlo en el repo que almacena la imagen
# Iniciamos un cliente SSH:
ssh_client = paramiko.SSHClient()
# Establecemos la política por defecto para localizar la llave del host localmente:
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# Intentamos conectar con el equipo remoto por SSH, e iniciar un cliente SFTP,
try: # y en caso de fallar devolvemos un error y salimos del script:
ssh_client.connect(hostname=remote_host, port=22, username=remote_user, passphrase='')
sftp_client = ssh_client.open_sftp()
except Exception as error_description:
journal.send(f"importImage.py: Connection exception: {error_description}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
print(f"Connection has returned an exception: {error_description}")
sys.exit(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:
journal.send("importImage.py: Remote image not found", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
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")
journal.send("importImage.py: Remote image is locked", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
print("Remote image is locked.")
sys.exit(4)
except IOError:
print("Importing remote image...")
journal.send("importImage.py: Creating '.lock' file...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
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:
journal.send("importImage.py: Closing remote connection...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
ssh_client.close()
sftp_client.close()
def get_md5_sum(file_path, megabytes=1):
""" Calcula y retorna el hash MD5 del último MB del archivo de imagen que recibe como parámetro.
Se utiliza para comprobar el valor del archivo ".sum".
"""
hash_md5 = hashlib.md5()
with open(file_path, "rb") as f:
f.seek(-megabytes * 1024 * 1024, os.SEEK_END)
data = f.read(megabytes * 1024 * 1024)
hash_md5.update(data)
return hash_md5.hexdigest()
def check_image(file_path):
""" Comprueba si los valores actuales de "sum" y "size"
coinciden con los almacenados en los archivos ".sum" y ".size".
Si coinciden retorna "True", y si no retorna "False".
"""
# Almacenamos los valores actuales de "sum" y "size":
actual_datasum = get_md5_sum(file_path)
actual_size = os.path.getsize(file_path)
# Almacenamos el valor de "sum" almacenado en el archivo:
with open(f"{file_path}.sum", 'r') as file:
file_datasum = file.read().strip('\n')
# Almacenamos el valor de "size" almacenado en el archivo:
with open(f"{file_path}.size", 'r') as file:
file_datasize = int(file.read().strip('\n'))
# Si los valores actuales coinciden con los almacenados en los archivos retornamos "True", y si no "False":
if actual_datasum == file_datasum and actual_size == file_datasize:
return True
else:
return False
def create_torrent(file_path):
""" Crea el archivo ".torrent" asociado a la imagen importada (los demás archivos auxiliares ya están creados), y actualiza la información del repositorio
(llamando al script "createTorrentSum.py", que a su vez llama al script "updateRepoInfo.py").
Se le llama cuando se ha comprobado que la imagen se ha importado correctamente (pasando el check de integridad).
"""
try:
journal.send("importImage.py: Running script 'createTorrentSum.py'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
result = subprocess.run(['python3', create_torrent_script, file_path], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except subprocess.CalledProcessError as error:
journal.send(f"importImage.py: 'createTorrentSum.py' error: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
print(f"Error Output: {error.stderr.decode()}")
sys.exit(5)
except Exception as error:
journal.send(f"importImage.py: 'createTorrentSum.py' exception: {error}", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
print(f"Se ha producido un error inesperado: {error}")
sys.exit(6)
def erase_image(file_path):
""" Borra la imagen importada y cada uno de los archivos asociados.
Se le llama cuando se ha comprobado que la imagen NO se ha importado correctamente (porque NO ha pasado el check de integridad).
"""
journal.send("importImage.py: Removing imported image files...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
# Creamos una lista con las extensiones de los archivos asociados a la imagen
# (incluyendo ninguna extensión, que corresponde a la propia imagen):
extensions = ['', '.size', '.sum', '.full.sum', '.info']
# Borramos cada uno de los archivos (incluyendo la imagen):
for ext in extensions:
if os.path.exists(f"{file_path}{ext}"):
os.remove(f"{file_path}{ext}")
# --------------------------------------------------------------------------------------------
# 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]
# Importamos la imagen del repositorio remoto:
import_image(file_path, remote_host, remote_user)
# Comprobamos si la imagen se ha importado correctamente (comparando los archivos ".size" y ".sum" con los valores actuales):
image_OK = check_image(file_path)
# Renombramos el archivo ".info.checked" a ".info", para que lo pille el script "updateRepoInfo.py":
journal.send("importImage.py: Renaming '.info.checked' file to '.info'...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
os.rename(f"{file_path}.info.checked", f"{file_path}.info")
# Eliminamos el archivo de bloqueo:
journal.send("importImage.py: Removing '.lock' file...", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
os.remove(f"{file_path}.lock")
# Si la imagen ha pasado el check de integridad, creamos el archivo ".torrent" y actualizamos el repositorio,
# y si no lo ha pasado borramos la imagen y los archivos asociados:
if image_OK == True:
create_torrent(file_path)
journal.send("importImage.py: Image imported successfully (Integrity Check OK)", PRIORITY=journal.LOG_INFO, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
print("Image imported successfully")
else:
erase_image(file_path)
journal.send("importImage.py: Imported image didn't pass the Integrity Check", PRIORITY=journal.LOG_ERR, SYSLOG_IDENTIFIER="ogrepo-api_DEBUG")
print("Imported image didn't pass the Integrity Check")
sys.exit(7)
# --------------------------------------------------------------------------------------------
if __name__ == "__main__":
main()
# --------------------------------------------------------------------------------------------