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