Añadida opción en Administración para importar una base de datos OG 1.1 en la nueva version. Permite subir fichero .sql o .zip o usar parametros de conexión a la base de datos de OG 1.1 #92

Merged
jmbardallo merged 1 commits from import-legacy into main 2026-05-08 15:39:38 +02:00
10 changed files with 441 additions and 6 deletions

View File

@ -1,5 +1,9 @@
# Changelog
## [1.6.0] - 2026-05-08
### Added
- Añadida opción en Administración para importar una base de datos OG 1.1 en la nueva version. Permite subir fichero .sql o .zip o usar parametros de conexión a la base de datos de OG 1.1
## [1.5.9] - 2026-05-07
### Fixed
- Asistente de particionado: en discos GPT se elimina el límite de 4 particiones primarias, se oculta la opción de añadir partición lógica/extendida y se suprime el estilo visual de partición lógica (borde amarillo e icono └)

View File

@ -34,6 +34,7 @@ import {
} from "./components/repositories/main-repository-view/main-repository-view.component";
import { EnvVarsComponent } from "./components/admin/env-vars/env-vars.component";
import { MenusComponent } from "./components/menus/menus.component";
import { ImportLegacyComponent } from './components/admin/import-legacy/import-legacy.component';
import { OgDhcpSubnetsComponent } from "./components/ogdhcp/og-dhcp-subnets.component";
import { DhcpServersComponent } from './components/ogdhcp/dhcp-servers/dhcp-servers.component';
import { BootServersComponent } from './components/ogboot/boot-servers/boot-servers.component';
@ -55,6 +56,7 @@ const routes: Routes = [
{ path: 'users', component: UsersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
{ path: 'env-vars', component: EnvVarsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
{ path: 'roles', component: RolesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
{ path: 'import-legacy', component: ImportLegacyComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
{ path: 'groups', component: GroupsComponent },
{ path: 'pxe-images', component: PXEimagesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'pxe', component: PxeComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },

View File

@ -205,6 +205,7 @@ export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './locale/', '.json');
}
import { ImportLegacyComponent } from './components/admin/import-legacy/import-legacy.component';
import { PageNotFoundComponent } from './shared/page-not-found/page-not-found.component';
export function initializeApp(configService: ConfigService) {
@ -343,6 +344,7 @@ registerLocaleData(localeEs, 'es-ES');
GlobalOgbootStatusComponent,
OgrepositoryStatusComponent,
OgdhcpStatusComponent,
ImportLegacyComponent,
PageNotFoundComponent
],
bootstrap: [AppComponent],

View File

@ -0,0 +1,146 @@
.main-content {
padding: 20px;
width: 100%;
box-sizing: border-box;
}
.radio-group {
display: flex;
flex-direction: column;
margin-bottom: 20px;
gap: 10px;
}
.form-container {
display: flex;
flex-direction: column;
gap: 15px;
max-width: 400px;
margin-top: 15px;
}
.help-text {
font-size: 14px;
color: #666;
margin-bottom: -5px;
}
.file-input-container {
display: flex;
align-items: center;
gap: 15px;
}
.file-name {
font-style: italic;
color: #666;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px;
text-align: center;
width: 100%;
box-sizing: border-box;
}
.warning-text {
color: #f44336;
font-weight: bold;
margin-top: 10px;
}
.success-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
text-align: center;
gap: 15px;
}
.success-icon {
font-size: 64px;
height: 64px;
width: 64px;
color: #4caf50;
}
.error-banner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
text-align: center;
background-color: #ffebee;
border-radius: 8px;
margin-bottom: 20px;
border: 1px solid #ef9a9a;
}
.error-banner h3 {
color: #c62828;
margin-top: 10px;
margin-bottom: 5px;
}
.error-banner p {
color: #c62828;
margin: 0;
}
.error-icon {
font-size: 48px;
height: 48px;
width: 48px;
}
.loader-box {
display: block;
text-align: center;
margin: 30px 0;
}
.loader-box mat-spinner {
margin: 0 auto;
}
.unified-log-box {
display: block;
width: 100%;
box-sizing: border-box;
text-align: left;
margin-top: 30px;
background-color: #1e1e1e;
color: #e0e0e0;
padding: 20px;
border-radius: 6px;
border: 1px solid #333;
}
.unified-log-box h4 {
margin-top: 0;
margin-bottom: 10px;
color: #4caf50;
border-bottom: 1px solid #333;
padding-bottom: 5px;
}
.unified-log-box pre {
white-space: pre-wrap;
word-wrap: break-word;
max-height: 400px;
overflow-y: auto;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
margin: 0;
}
.full-width-card {
width: 100% !important;
box-sizing: border-box;
}

View File

@ -0,0 +1,95 @@
<div class="main-content">
<div class="header-container">
<div class="header-container-title">
<h2>{{ 'importLegacyDbTitle' | translate }}</h2>
</div>
</div>
<mat-card class="full-width-card">
<mat-card-header>
<mat-card-title>{{ 'importLegacyDbDesc' | translate }}</mat-card-title>
</mat-card-header>
<mat-card-content>
@if (isLoading) {
<div class="loader-box">
<mat-spinner diameter="50"></mat-spinner>
<h3>{{ 'migrationInProgress' | translate }}</h3>
<p class="warning-text">{{ 'migrationWaitWarning' | translate }}</p>
</div>
} @else if (migrationStatus === 'success') {
<div class="success-container">
<mat-icon color="primary" class="success-icon">check_circle</mat-icon>
<h3>{{ 'migrationSuccess' | translate }}</h3>
<p>Los datos han sido migrados correctamente a la nueva versión.</p>
<button mat-raised-button color="primary" (click)="resetForm()">Realizar otra importación</button>
</div>
} @else {
@if (migrationStatus === 'error') {
<div class="error-banner">
<mat-icon color="warn" class="error-icon">error</mat-icon>
<h3>{{ 'migrationError' | translate }}</h3>
<p>Revisa el log en la parte inferior para más detalles o corrige los parámetros e inténtalo de nuevo.</p>
</div>
}
<mat-radio-group [(ngModel)]="importMode" class="radio-group">
<mat-radio-button value="db">{{ 'fromRemoteDb' | translate }}</mat-radio-button>
<mat-radio-button value="file">{{ 'fromSqlFile' | translate }}</mat-radio-button>
</mat-radio-group>
@if (importMode === 'db') {
<form [formGroup]="dbForm" (ngSubmit)="onSubmit()" class="form-container">
<mat-form-field appearance="outline">
<mat-label>{{ 'hostLabel' | translate }}</mat-label>
<input matInput formControlName="legacyHost" placeholder="192.168.1.100">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'userLabel' | translate }}</mat-label>
<input matInput formControlName="legacyUser" placeholder="root">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'passLabel' | translate }}</mat-label>
<input matInput formControlName="legacyPassword" type="password">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'dbLabel' | translate }}</mat-label>
<input matInput formControlName="legacyDb" placeholder="opengnsys">
</mat-form-field>
<button mat-raised-button color="primary" type="submit" [disabled]="dbForm.invalid">
<mat-icon>cloud_download</mat-icon> {{ 'importButton' | translate }}
</button>
</form>
}
@if (importMode === 'file') {
<div class="form-container">
<p class="help-text">{{ 'uploadSqlOrZip' | translate }}</p>
<div class="file-input-container">
<button mat-stroked-button type="button" (click)="fileInput.click()">
<mat-icon>attach_file</mat-icon> {{ 'sqlFileLabel' | translate }}
</button>
<input hidden (change)="onFileSelected($event)" #fileInput type="file" accept=".sql,.zip">
@if (selectedFile) {
<span class="file-name">{{ selectedFile.name }}</span>
}
</div>
<button mat-raised-button color="primary" (click)="onSubmit()" [disabled]="!selectedFile" style="margin-top: 15px;">
<mat-icon>upload_file</mat-icon> {{ 'importButton' | translate }}
</button>
</div>
}
}
@if (outputLog) {
<div class="unified-log-box">
<h4>Consola en vivo:</h4>
<pre>{{ outputLog }}</pre>
</div>
}
</mat-card-content>
</mat-card>
</div>

View File

@ -0,0 +1,138 @@
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslateService } from '@ngx-translate/core';
import { ConfigService } from '../../../services/config.service';
@Component({
selector: 'app-import-legacy',
templateUrl: './import-legacy.component.html',
styleUrls: ['./import-legacy.component.css']
})
export class ImportLegacyComponent {
importMode: 'file' | 'db' = 'db';
dbForm: FormGroup;
selectedFile: File | null = null;
isLoading = false;
outputLog = '';
migrationStatus: 'idle' | 'success' | 'error' = 'idle';
constructor(
private fb: FormBuilder,
private http: HttpClient,
private snackBar: MatSnackBar,
private translate: TranslateService,
private configService: ConfigService
) {
this.dbForm = this.fb.group({
legacyHost: ['', Validators.required],
legacyUser: ['', Validators.required],
legacyPassword: [''],
legacyDb: ['', Validators.required]
});
}
ngOnInit() {
this.checkMigrationStatus();
}
checkMigrationStatus() {
this.http.get<any>(`${this.configService.apiUrl}/migration/import/status`).subscribe({
next: (response) => {
if (response.isRunning) {
this.isLoading = true;
this.outputLog = response.log || 'Recuperando estado de migración...';
this.connectToMercure();
}
},
error: (error) => {
console.error('Error checking migration status:', error);
}
});
}
private connectToMercure() {
const url = new URL(this.configService.mercureUrl);
url.searchParams.append('topic', '/migration');
const eventSource = new EventSource(url.toString());
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'migration_progress') {
this.outputLog = data.output;
}
else if (data.type === 'migration_result') {
this.isLoading = false;
this.migrationStatus = data.status;
this.outputLog = data.output;
if (data.status === 'success') {
this.snackBar.open(this.translate.instant('migrationSuccess'), 'OK', { duration: 5000 });
} else {
this.snackBar.open(this.translate.instant('migrationError'), 'OK', { duration: 5000 });
}
eventSource.close();
}
};
eventSource.onerror = (err) => {
console.error("Mercure connection error", err);
};
}
onFileSelected(event: any) {
const file = event.target.files[0];
if (file) {
this.selectedFile = file;
}
}
resetForm() {
this.migrationStatus = 'idle';
this.outputLog = '';
this.selectedFile = null;
this.dbForm.reset();
}
onSubmit() {
if (this.importMode === 'file' && !this.selectedFile) {
this.snackBar.open('Please select a file', 'Close', { duration: 3000 });
} else if ((this.importMode === 'db' && this.dbForm.valid) || (this.importMode === 'file' && this.selectedFile)) {
this.isLoading = true;
this.outputLog = '';
this.migrationStatus = 'idle';
let request;
if (this.importMode === 'file') {
const formData = new FormData();
formData.append('sql_file', this.selectedFile!);
request = this.http.post<any>(`${this.configService.apiUrl}/migration/import`, formData);
} else {
request = this.http.post<any>(`${this.configService.apiUrl}/migration/import`, this.dbForm.value);
}
request.subscribe({
next: (response) => {
if (response.status === 'started') {
this.outputLog = 'Proceso de migración en curso. Esperando respuesta del servidor en segundo plano...';
this.connectToMercure();
} else {
this.isLoading = false;
this.migrationStatus = 'success';
this.outputLog = response.output || response.message;
this.snackBar.open(this.translate.instant('migrationSuccess'), 'OK', { duration: 5000 });
}
},
error: (error) => {
this.isLoading = false;
this.migrationStatus = 'error';
const details = error.error?.error || error.error?.details || error.message || 'Unknown error';
this.outputLog = details;
this.snackBar.open(this.translate.instant('migrationError'), 'OK', { duration: 5000 });
}
});
}
}
}

View File

@ -110,5 +110,8 @@
<button mat-menu-item routerLink="/env-vars" [matTooltip]="'manageEnvVars' | translate" matTooltipShowDelay="1000">
{{ 'labelEnvVars' | translate }}
</button>
<button mat-menu-item routerLink="/import-legacy" [matTooltip]="'importLegacyDbDesc' | translate" matTooltipShowDelay="1000">
{{ 'importLegacyDbTitle' | translate }}
</button>
</mat-menu>
</mat-toolbar>

View File

@ -1524,5 +1524,20 @@
"youMustSelectAtLeastOne1": "Has de seleccionar almenys un dia de la setmana",
"youMustSelectAtLeastOne2": "Has de seleccionar almenys un mes",
"youMustSpecifyStartDate": "Has d'especificar data d'inici i data de fi",
"youWantToPropagateTheParametersOf": "Vols propagar els paràmetres de xarxa (ogLive, repositori, menú, plantilla PXE, perfil de hardware) a tots els clients d'aquesta unitat organitzativa?"
}
"youWantToPropagateTheParametersOf": "Vols propagar els paràmetres de xarxa (ogLive, repositori, menú, plantilla PXE, perfil de hardware) a tots els clients d'aquesta unitat organitzativa?",
"importLegacyDbTitle": "Importar des d'OG 1.1",
"importLegacyDbDesc": "Migra les dades d'una instal·lació OpenGnsys 1.1 a la versió 3.0",
"fromSqlFile": "Des de fitxer SQL",
"fromRemoteDb": "Des de Base de Dades remota",
"importButton": "Importar",
"sqlFileLabel": "Fitxer (.sql/.zip)",
"hostLabel": "Host de la Base de Dades",
"userLabel": "Usuari",
"passLabel": "Contrasenya (Opcional)",
"dbLabel": "Nom de la BD",
"migrationInProgress": "Migració en curs...",
"migrationWaitWarning": "Aquest procés pot trigar uns minuts. Pots navegar per altres pestanyes o recarregar la pàgina; la migració continuarà en segon pla.",
"migrationSuccess": "Migració completada amb èxit",
"migrationError": "Error durant la migració",
"uploadSqlOrZip": "Puja el fitxer .sql o comprimit en zip (.zip)"
}

View File

@ -1526,5 +1526,20 @@
"youMustSelectAtLeastOne1": "You must select at least one day of the week",
"youMustSelectAtLeastOne2": "You must select at least one month",
"youMustSpecifyStartDate": "You must specify start date and end date",
"youWantToPropagateTheParametersOf": "Do you want to propagate the network parameters (ogLive, repository, menu, PXE template, hardware profile) to all clients in this organizational unit?"
}
"youWantToPropagateTheParametersOf": "Do you want to propagate the network parameters (ogLive, repository, menu, PXE template, hardware profile) to all clients in this organizational unit?",
"importLegacyDbTitle": "Import from OG 1.1",
"importLegacyDbDesc": "Migrates data from an OpenGnsys 1.1 installation to version 3.0",
"fromSqlFile": "From SQL File",
"fromRemoteDb": "From Remote Database",
"importButton": "Import",
"sqlFileLabel": "File (.sql/.zip)",
"hostLabel": "Database Host",
"userLabel": "Username",
"passLabel": "Password (Optional)",
"dbLabel": "Database Name",
"migrationInProgress": "Migration in progress...",
"migrationWaitWarning": "This process can take several minutes. You can navigate to other tabs or reload the page; the migration will continue in the background.",
"migrationSuccess": "Migration completed successfully",
"migrationError": "Error during migration",
"uploadSqlOrZip": "Upload the .sql file or compressed in zip (.zip)"
}

View File

@ -1526,5 +1526,20 @@
"youMustSelectAtLeastOne1": "Debe seleccionar al menos un día de la semana",
"youMustSelectAtLeastOne2": "Debe seleccionar al menos un mes",
"youMustSpecifyStartDate": "Debe especificar fecha de inicio y fecha de fin",
"youWantToPropagateTheParametersOf": "¿Desea propagar los parámetros de red (ogLive, repositorio, menú, plantilla PXE, perfil de hardware) a todos los clientes de esta unidad organizativa?"
}
"youWantToPropagateTheParametersOf": "¿Desea propagar los parámetros de red (ogLive, repositorio, menú, plantilla PXE, perfil de hardware) a todos los clientes de esta unidad organizativa?",
"importLegacyDbTitle": "Importar desde OG 1.1",
"importLegacyDbDesc": "Migra los datos de una instalación OpenGnsys 1.1 a la versión 3.0",
"fromSqlFile": "Desde fichero SQL",
"fromRemoteDb": "Desde Base de Datos remota",
"importButton": "Importar",
"sqlFileLabel": "Fichero (.sql/.zip)",
"hostLabel": "Host de la Base de Datos",
"userLabel": "Usuario",
"passLabel": "Contraseña (Opcional)",
"dbLabel": "Nombre de la BD",
"migrationInProgress": "Migración en curso...",
"migrationWaitWarning": "Este proceso puede tardar varios minutos. Puedes navegar por otras pestañas o recargar la página; la migración continuará en segundo plano.",
"migrationSuccess": "Migración completada con éxito",
"migrationError": "Error durante la migración",
"uploadSqlOrZip": "Sube el fichero .sql o comprimido en zip (.zip)"
}