feature/integracion-panel-restore-targets
This commit is contained in:
@@ -34,7 +34,8 @@ class SQLServerManager:
|
||||
username: Usuario SQL (si no usa Windows Auth)
|
||||
password: Contraseña SQL (si no usa Windows Auth)
|
||||
"""
|
||||
self.server = server
|
||||
# ODBC usa coma para el puerto (ip,puerto), no dos puntos
|
||||
self.server = server.replace(":", ",") if ":" in server else server
|
||||
self.use_windows_auth = use_windows_auth
|
||||
self.username = username
|
||||
self.password = password
|
||||
@@ -168,85 +169,112 @@ class SQLServerManager:
|
||||
Returns:
|
||||
Tupla (éxito, stdout, error)
|
||||
"""
|
||||
conn = None
|
||||
try:
|
||||
# Construir las cláusulas MOVE
|
||||
move_clauses = []
|
||||
for lf in logical_files:
|
||||
if lf.type == 'D': # Data file
|
||||
new_path = f"{data_folder}\\{db_name}.mdf"
|
||||
elif lf.type == 'L': # Log file
|
||||
new_path = f"{data_folder}\\{db_name}_log.ldf"
|
||||
else:
|
||||
# Archivos adicionales (filestream, etc.)
|
||||
continue
|
||||
|
||||
move_clauses.append(f"MOVE N'{lf.logical_name}' TO N'{new_path}'")
|
||||
|
||||
# Construir las cláusulas MOVE con un destino único por archivo lógico.
|
||||
# Renombrar todos los data files a {db}.mdf colisiona si el backup tiene
|
||||
# varios archivos; aquí cada archivo recibe un nombre distinto (G4).
|
||||
move_clauses = self._build_move_clauses(db_name, data_folder, logical_files)
|
||||
if not move_clauses:
|
||||
return False, None, "No se pudieron determinar los archivos de datos y log"
|
||||
|
||||
# Construir el comando RESTORE
|
||||
restore_cmd = f"""
|
||||
-- Poner la base de datos en modo single user
|
||||
ALTER DATABASE [{db_name}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
|
||||
return False, None, "No se pudieron determinar los archivos del backup"
|
||||
|
||||
-- Restaurar
|
||||
RESTORE DATABASE [{db_name}]
|
||||
FROM DISK = N'{backup_path}'
|
||||
WITH {', '.join(move_clauses)}, REPLACE;
|
||||
restore_query = (
|
||||
f"RESTORE DATABASE [{db_name}] FROM DISK = N'{backup_path}' "
|
||||
f"WITH {', '.join(move_clauses)}, REPLACE"
|
||||
)
|
||||
|
||||
app_logger.info(f"Comando RESTORE generado:\n{restore_query}")
|
||||
|
||||
-- Volver a modo multi user
|
||||
ALTER DATABASE [{db_name}] SET MULTI_USER;
|
||||
"""
|
||||
|
||||
app_logger.info(f"Comando RESTORE generado:\n{restore_cmd}")
|
||||
|
||||
if dry_run:
|
||||
app_logger.info("Modo DRY RUN: No se ejecutará el RESTORE")
|
||||
return True, restore_cmd, None
|
||||
|
||||
return True, restore_query, None
|
||||
|
||||
# Ejecutar RESTORE
|
||||
conn_str = self.get_connection_string()
|
||||
conn = pyodbc.connect(conn_str, timeout=timeout_minutes * 60)
|
||||
conn.autocommit = True # Necesario para ALTER DATABASE
|
||||
cursor = conn.cursor()
|
||||
|
||||
|
||||
start_time = time.time()
|
||||
app_logger.info(f"Ejecutando RESTORE DATABASE [{db_name}]...")
|
||||
|
||||
# Ejecutar en múltiples pasos
|
||||
|
||||
output_lines = []
|
||||
|
||||
# 1. Single user
|
||||
try:
|
||||
cursor.execute(f"ALTER DATABASE [{db_name}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE")
|
||||
output_lines.append("Base de datos configurada en modo SINGLE_USER")
|
||||
except Exception as e:
|
||||
app_logger.warning(f"Error configurando SINGLE_USER (puede no existir la DB): {e}")
|
||||
|
||||
# 2. RESTORE
|
||||
restore_query = f"RESTORE DATABASE [{db_name}] FROM DISK = N'{backup_path}' WITH {', '.join(move_clauses)}, REPLACE"
|
||||
cursor.execute(restore_query)
|
||||
output_lines.append("RESTORE DATABASE completado")
|
||||
|
||||
# 3. Multi user
|
||||
cursor.execute(f"ALTER DATABASE [{db_name}] SET MULTI_USER")
|
||||
output_lines.append("Base de datos configurada en modo MULTI_USER")
|
||||
|
||||
# 1. Single user (puede no existir la DB en una primera restauración)
|
||||
try:
|
||||
cursor.execute(
|
||||
f"ALTER DATABASE [{db_name}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE"
|
||||
)
|
||||
output_lines.append("Base de datos configurada en modo SINGLE_USER")
|
||||
except Exception as e:
|
||||
app_logger.warning(
|
||||
f"No se pudo configurar SINGLE_USER (puede no existir la DB): {e}"
|
||||
)
|
||||
|
||||
# 2. RESTORE
|
||||
cursor.execute(restore_query)
|
||||
output_lines.append("RESTORE DATABASE completado")
|
||||
finally:
|
||||
# 3. Volver SIEMPRE a MULTI_USER, incluso si el RESTORE falló, para
|
||||
# no dejar la BD inaccesible en un servidor remoto compartido (G7).
|
||||
try:
|
||||
cursor.execute(f"ALTER DATABASE [{db_name}] SET MULTI_USER")
|
||||
output_lines.append("Base de datos configurada en modo MULTI_USER")
|
||||
except Exception as e:
|
||||
app_logger.error(
|
||||
f"No se pudo volver a MULTI_USER la BD [{db_name}] "
|
||||
f"(¿quedó en estado RESTORING tras un fallo?): {e}. "
|
||||
"Requiere intervención manual del DBA."
|
||||
)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
output_lines.append(f"Restauración completada en {elapsed:.2f}s")
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
output = "\n".join(output_lines)
|
||||
app_logger.info(output)
|
||||
|
||||
|
||||
return True, output, None
|
||||
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error restaurando base de datos: {str(e)}"
|
||||
app_logger.error(error_msg)
|
||||
return False, None, error_msg
|
||||
finally:
|
||||
if conn is not None:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
app_logger.warning("No se pudo cerrar la conexión SQL tras el RESTORE")
|
||||
|
||||
@staticmethod
|
||||
def _build_move_clauses(db_name, data_folder, logical_files) -> list:
|
||||
"""
|
||||
Genera una cláusula MOVE por archivo lógico con destino único, evitando
|
||||
colisiones cuando el backup tiene múltiples data files o logs (G4):
|
||||
- 1er data → {db}.mdf, siguientes → {db}_N.ndf
|
||||
- 1er log → {db}_log.ldf, siguientes → {db}_log_N.ldf
|
||||
- otros tipos (FILESTREAM/full-text) → {db}_{nombre_lógico_saneado}
|
||||
"""
|
||||
clauses = []
|
||||
data_idx = 0
|
||||
log_idx = 0
|
||||
for lf in logical_files:
|
||||
if lf.type == 'D':
|
||||
suffix = "" if data_idx == 0 else f"_{data_idx}"
|
||||
ext = "mdf" if data_idx == 0 else "ndf"
|
||||
new_path = f"{data_folder}\\{db_name}{suffix}.{ext}"
|
||||
data_idx += 1
|
||||
elif lf.type == 'L':
|
||||
suffix = "" if log_idx == 0 else f"_{log_idx}"
|
||||
new_path = f"{data_folder}\\{db_name}_log{suffix}.ldf"
|
||||
log_idx += 1
|
||||
else:
|
||||
# No descartar otros tipos: moverlos preservando el nombre lógico.
|
||||
safe = "".join(c if c.isalnum() else "_" for c in lf.logical_name)
|
||||
new_path = f"{data_folder}\\{db_name}_{safe}"
|
||||
|
||||
clauses.append(f"MOVE N'{lf.logical_name}' TO N'{new_path}'")
|
||||
return clauses
|
||||
|
||||
def database_exists(self, db_name: str) -> bool:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user