import { inject } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { Component, Input, OnInit } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Router } from "@angular/router"; import { ToastrService } from "ngx-toastr"; import { ConfigService } from '@services/config.service'; import { BootSoPartitionComponent } from "./boot-so-partition/boot-so-partition.component"; import { MatDialog } from "@angular/material/dialog"; import { RemoveCacheImageComponent } from "./remove-cache-image/remove-cache-image.component"; import { AuthService } from '@services/auth.service'; import {SoftwareProfilePartitionComponent} from "./software-profile-partition/software-profile-partition.component"; import { QueueConfirmationModalComponent } from '../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component'; import { PluginCommandModalComponent } from '../../../../shared/plugin-command-modal/plugin-command-modal.component'; import { ExecuteTaskNowComponent } from './execute-task-now/execute-task-now.component'; import { PluginService, PluginCommand } from '@services/plugin.service'; @Component({ selector: 'app-execute-command', templateUrl: './execute-command.component.html', styleUrls: ['./execute-command.component.css'] }) export class ExecuteCommandComponent implements OnInit { public translate = inject(TranslateService); @Input() runScriptContext: any = null; @Input() organizationalUnit: any = null; @Input() showOnlyDirectClients: boolean = false; @Input() clientState: string = 'off'; @Input() clientData: any[] = []; @Input() buttonType: 'icon' | 'text' | 'menu-item' = 'icon'; @Input() buttonText: string = 'Ejecutar Comandos'; @Input() icon: string = 'terminal'; @Input() disabled: boolean = false; baseUrl: string; loading: boolean = true; isFetchingOuClients: boolean = false; arrayCommands: any[] = [ { translationKey: 'executeCommands.powerOn', slug: 'power-on', disabled: false }, { translationKey: 'executeCommands.powerOff', slug: 'power-off', disabled: false }, { translationKey: 'executeCommands.reboot', slug: 'reboot', disabled: false }, { translationKey: 'executeCommands.login', slug: 'login', disabled: false }, { translationKey: 'executeCommands.createImage', slug: 'create-image', disabled: false }, { translationKey: 'executeCommands.deployImage', slug: 'deploy-image', disabled: false }, { translationKey: 'executeCommands.deleteImageCache', slug: 'remove-cache-image', disabled: false }, { translationKey: 'executeCommands.partition', slug: 'partition', disabled: false }, { translationKey: 'executeCommands.softwareInventory', slug: 'software-inventory', disabled: false }, { translationKey: 'executeCommands.hardwareInventory', slug: 'hardware-inventory', disabled: false }, { translationKey: 'executeCommands.runScript', slug: 'run-script', disabled: false }, { translationKey: 'executeCommands.refresh', slug: 'refresh', disabled: false }, { translationKey: 'executeCommands.deleteLease', slug: 'delete-lease', disabled: false }, { translationKey: 'executeCommands.executeTask', slug: 'execute-task', disabled: false }, ]; client: any = {}; constructor( private http: HttpClient, private router: Router, private configService: ConfigService, private toastService: ToastrService, public auth: AuthService, private dialog: MatDialog, public pluginService: PluginService, ) { this.baseUrl = this.configService.apiUrl; } pluginGroups: any[] = []; ngOnInit(): void { this.clientData = this.clientData || []; const allowed = this.getAllowedCommandsByRole(); this.arrayCommands = this.arrayCommands.filter(c => allowed.includes(c.slug)); // Punto de anclaje reactivo para grupos de comandos aportados por plugins this.pluginGroups = this.pluginService.getPluginGroups(); this.pluginService.plugins$.subscribe(() => { this.pluginGroups = this.pluginService.getPluginGroups(); this.updateCommandStates(); }); this.pluginService.statusUpdates$.subscribe(() => { this.updateCommandStates(); }); this.updateCommandStates(); } ngOnChanges(): void { this.updateCommandStates(); } private getAllowedCommandsByRole(): string[] { const role = this.auth.userCategory; const permissions: Record = { 'super-admin': ['*'], 'ou-admin': ['*'], 'ou-operator': [ 'power-on', 'power-off', 'reboot', 'login', 'create-image', 'deploy-image', 'software-inventory', 'hardware-inventory', 'remove-cache-image', 'partition', 'refresh', 'delete-lease', 'execute-task' ], 'ou-minimal': [ 'power-on', 'power-off' ] }; const allowed = permissions[role] || []; return allowed.includes('*') ? this.arrayCommands.map(c => c.slug) : allowed; } private updateCommandStates(): void { let states: string[] = []; if (this.clientData.length > 0) { states = this.clientData.map(client => client.status); } else if (this.clientState) { states = [this.clientState]; } const hasClients = this.clientData.length > 0 || !!this.clientState; const multipleClients = this.clientData.length > 1; const hasOgLive = states.some(state => state === 'og-live'); // Acciones que admiten encolado o son independientes del estado del cliente const queueableOrStateAgnostic = [ 'power-on', 'power-off', 'reboot', 'login', 'deploy-image', 'partition', 'run-script', 'execute-task', 'delete-lease' ]; this.arrayCommands = this.arrayCommands.map(command => { if (!hasClients) { command.disabled = true; return command; } // Restricciones lógicas estrictas if (command.slug === 'create-image') { command.disabled = multipleClients || !hasOgLive; } else if (command.slug === 'hardware-inventory' || command.slug === 'software-inventory') { command.disabled = multipleClients || !hasOgLive; } else if (command.slug === 'remove-cache-image') { command.disabled = !hasOgLive; } else if (command.slug === 'refresh') { command.disabled = !hasOgLive; } else if (queueableOrStateAgnostic.includes(command.slug)) { command.disabled = false; } else { command.disabled = false; } return command; }); if (this.pluginGroups && this.pluginGroups.length > 0) { const clientsToCheck = this.clientData.length > 0 ? this.clientData : (this.clientState ? [{ status: this.clientState }] : []); for (const group of this.pluginGroups) { for (const cmd of group.commands) { cmd.disabled = !hasClients || !this.pluginService.isCommandSupportedForClients(cmd, clientsToCheck, group.plugin.name); // Adaptar dinámicamente la etiqueta de activar/desactivar en ogaquarium si hay un solo cliente seleccionado if (cmd.slug === 'ogaquarium-toggle-status' && this.clientData.length === 1) { const client = this.clientData[0]; const idKey = client.id ? String(client.id) : null; const uuidKey = client.uuid || client.name; const currentStatus = (idKey ? this.pluginService.getPluginStatus(group.plugin.name, idKey) : null) || (uuidKey ? this.pluginService.getPluginStatus(group.plugin.name, uuidKey) : null) || 'DISABLED'; if (currentStatus === 'DISABLED') { cmd.label = this.translate.instant('ogaquarium.commands.activate') || 'Activar puesto'; } else { cmd.label = this.translate.instant('ogaquarium.commands.deactivate') || 'Desactivar puesto'; } cmd.labelKey = undefined; } } } } } onCommandSelect(action: any): void { if (this.organizationalUnit && this.organizationalUnit.id) { this.fetchClientsForOuAndExecute(action); return; } this.executeAction(action); } private fetchClientsForOuAndExecute(action: any): void { if (this.isFetchingOuClients) { return; } this.isFetchingOuClients = true; const unitId = this.organizationalUnit.id; this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${unitId}&page=1&itemsPerPage=10000`).subscribe({ next: (response) => { this.isFetchingOuClients = false; let allClients: any[] = response['hydra:member'] || []; if (this.showOnlyDirectClients && this.organizationalUnit['@id']) { allClients = allClients.filter((client: any) => client.organizationalUnit?.['@id'] === this.organizationalUnit['@id'] ); } if (allClients.length === 0) { this.toastService.warning(this.translate.instant('thisOrganizationalUnitDoesNotHave') || 'Esta unidad organizativa no tiene clientes.'); return; } this.clientData = allClients; this.executeAction(action); }, error: (error) => { this.isFetchingOuClients = false; this.toastService.error(this.translate.instant('errorLoadingClients1') || 'Error al cargar los clientes'); } }); } private executeAction(action: any): void { // Punto de anclaje para ejecución de comandos aportados por plugins const pluginCmd = this.pluginService.findCommand(action); if (pluginCmd) { this.executePluginCommand(pluginCmd); return; } if (action === 'partition') { this.openPartitionAssistant(); } if (action === 'create-image') { this.openCreateImageAssistant(); } if (action === 'deploy-image') { this.openDeployImageAssistant(); } if (action === 'run-script') { this.openRunScriptAssistant(); } if (action === 'login') { this.loginClient(); } if (action === 'reboot') { this.rebootClient(); } if (action === 'power-off') { this.powerOffClient(); } if (action === 'power-on') { this.powerOnClient(); } if (action === 'remove-cache-image') { this.removeImageCache(); } if (action === 'hardware-inventory') { this.hardwareInventory(); } if (action === 'software-inventory') { this.softwareInventory(); } if (action === 'refresh') { this.refreshClient(); } if (action === 'delete-lease') { this.deleteClientLease(); } if (action === 'execute-task') { this.openExecuteTaskModal(); } } openExecuteTaskModal(): void { this.dialog.open(ExecuteTaskNowComponent, { width: '500px', height: 'auto', data: { clients: this.clientData } }); } rebootClient(): void { const dialogRef = this.dialog.open(QueueConfirmationModalComponent, { width: '400px', data: { clients: this.clientData } }); dialogRef.afterClosed().subscribe((queue: boolean | undefined) => { if (queue === undefined) return; this.http.post(`${this.baseUrl}/clients/server/reboot`, { clients: this.clientData.map((client: any) => client['@id']), queue }).subscribe( () => { this.toastService.success(this.translate.instant('clientUpdatedSuccessfully')); }, (error: any) => { this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente'); } ); }); } refreshClient(): void { this.http.post(`${this.baseUrl}/clients/server/refresh`, { clients: this.clientData.map((client: any) => client['@id']) }).subscribe( response => { this.toastService.success(this.translate.instant('refreshmentRequestSentCorrectly')); }, error => { this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente'); } ); } loginClient(): void { const clientDataToSend = this.clientData.map(client => ({ name: client.name, mac: client.mac, uuid: '/clients/' + client.uuid, status: client.status, partitions: client.partitions, firmwareType: client.firmwareType, ip: client.ip })); const dialogRef = this.dialog.open(BootSoPartitionComponent, { width: '70vw', height: 'auto', data: { clients: clientDataToSend } }); dialogRef.afterClosed().subscribe(result => { if (result) { this.toastService.success(this.translate.instant('oSBootRequest')); } }); } removeImageCache(): void { const clientDataToSend = this.clientData.map(client => ({ name: client.name, mac: client.mac, uuid: '/clients/' + client.uuid, status: client.status, partitions: client.partitions, firmwareType: client.firmwareType, ip: client.ip })); const dialogRef = this.dialog.open(RemoveCacheImageComponent, { width: '70vw', height: 'auto', data: { clients: clientDataToSend } }); dialogRef.afterClosed().subscribe(result => { if (result) { this.toastService.success(this.translate.instant('cacheClearRequest')); } }); } hardwareInventory(): void { if (this.clientData.length === 0) { this.toastService.error(this.translate.instant('noClientsSelected')); return; } const clientId = this.clientData[0].uuid; this.http.post(`${this.baseUrl}/clients/server/${clientId}/hardware-inventory`, { clients: this.clientData.map((client: any) => client['@id']) }).subscribe( response => { this.toastService.success(this.translate.instant('hardwareInventorySuccessfullyUpdated')); }, error => { this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente'); } ); } softwareInventory(): void { if (this.clientData.length === 0) { this.toastService.error(this.translate.instant('noClientsSelected')); return; } const clientDataToSend = { clientId: this.clientData[0].uuid, name: this.clientData[0].name, mac: this.clientData[0].mac, status: this.clientData[0].status, partitions: this.clientData[0].partitions, firmwareType: this.clientData[0].firmwareType, ip: this.clientData[0].ip } const clientId = this.clientData[0].uuid; const dialogRef = this.dialog.open(SoftwareProfilePartitionComponent, { width: '70vw', height: 'auto', data: { client: clientDataToSend } }); dialogRef.afterClosed().subscribe(result => { }); } powerOnClient(): void { this.http.post(`${this.baseUrl}/image-repositories/wol`, { clients: this.clientData.map((client: any) => client['@id']) }).subscribe( response => { this.toastService.success(this.translate.instant('ignitionRequestSentCorrectly')); }, error => { this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente'); } ); } powerOffClient(): void { const dialogRef = this.dialog.open(QueueConfirmationModalComponent, { width: '400px', data: { clients: this.clientData } }); dialogRef.afterClosed().subscribe((queue: boolean | undefined) => { if (queue === undefined) return; this.http.post(`${this.baseUrl}/clients/server/power-off`, { clients: this.clientData.map((client: any) => client['@id']), queue }).subscribe( () => { this.toastService.success(this.translate.instant('shutdownRequestSentCorrectly')); }, (error: any) => { this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente'); } ); }); } openPartitionAssistant(): void { const clientDataToSend = this.clientData.map(client => ({ name: client.name, mac: client.mac, uuid: '/clients/' + client.uuid, status: client.status, partitions: client.partitions, firmwareType: client.firmwareType, ip: client.ip })); this.router.navigate(['/clients/partition-assistant'], { queryParams: { clientData: JSON.stringify(clientDataToSend), runScriptContext: JSON.stringify(this.runScriptContext) } }); } openCreateImageAssistant(): void { this.router.navigate([`/clients/${this.clientData[0].uuid}/create-image`]).then(r => { console.log('navigated', r); }); } openDeployImageAssistant(): void { const clientDataToSend = this.clientData.map(client => ({ name: client.name, mac: client.mac, uuid: '/clients/' + client.uuid, status: client.status, partitions: client.partitions, ip: client.ip })); this.router.navigate(['/clients/deploy-image'], { queryParams: { clientData: JSON.stringify(clientDataToSend), runScriptContext: JSON.stringify(this.runScriptContext) } }); } openRunScriptAssistant(): void { const clientDataToSend = this.clientData.map(client => ({ name: client.name, mac: client.mac, uuid: '/clients/' + client.uuid, status: client.status, partitions: client.partitions, ip: client.ip })); this.router.navigate(['/clients/run-script'], { queryParams: { clientData: JSON.stringify(clientDataToSend), runScriptContext: JSON.stringify(this.runScriptContext) } }) } deleteClientLease(): void { if (this.clientData.length === 0) { this.toastService.error(this.translate.instant('noClientsSelected')); return; } const requests = this.clientData.map((client: any) => this.http.delete(`${this.baseUrl}/clients/${client.uuid}/delete-lease`).toPromise() ); Promise.allSettled(requests).then(results => { const errors = results.filter(r => r.status === 'rejected'); if (errors.length === 0) { this.toastService.success(this.translate.instant('dhcpLeasesRemovedSuccessfully')); } else if (errors.length < results.length) { this.toastService.warning(`${results.length - errors.length} lease(s) eliminada(s), ${errors.length} con error`); } else { this.toastService.error(this.translate.instant('errorWhenDeletingLeases')); } }); } private executePluginCommand(pluginCmd: PluginCommand): void { if (!this.clientData || this.clientData.length === 0) { this.toastService.warning('No hay ningún equipo seleccionado para ejecutar esta acción.', pluginCmd.label); return; } const hasParams = !!(pluginCmd.params && pluginCmd.params.length > 0); const hasConfirm = !!(pluginCmd.confirm || pluginCmd.confirmKey); if (hasParams || hasConfirm) { const dialogRef = this.dialog.open(PluginCommandModalComponent, { width: '450px', data: { command: pluginCmd, clients: this.clientData } }); dialogRef.afterClosed().subscribe((result: any) => { if (!result || !result.confirmed) { return; // Cancelado por el usuario } const customParams = result.params || {}; this.promptQueueAndSendPluginCommand(pluginCmd, customParams); }); return; } this.promptQueueAndSendPluginCommand(pluginCmd, {}); } private promptQueueAndSendPluginCommand(pluginCmd: PluginCommand, extraParams: Record): void { const requiredOs = (pluginCmd.os || []).map(os => os.toLowerCase()); // 1. Equipos en un sistema operativo no coincidente (ej. Linux u og-live cuando se requiere Windows) const incompatibleRunningClients = this.clientData.filter(c => { const status = (c.status || '').toLowerCase(); const isOnline = status !== 'off' && status !== 'disconnected' && status !== ''; if (!isOnline || requiredOs.length === 0) return false; return !requiredOs.some(req => status.includes(req)); }); // 2. Equipos apagados o desconectados const offlineClients = this.clientData.filter(c => { const status = (c.status || '').toLowerCase(); return !status || status === 'off' || status === 'disconnected'; }); // Si hay equipos no listos inmediatamente (apagados o en otro SO), ofrecer encolado if (offlineClients.length > 0 || incompatibleRunningClients.length > 0) { const dialogRef = this.dialog.open(QueueConfirmationModalComponent, { width: '400px', data: { clients: this.clientData } }); dialogRef.afterClosed().subscribe((queue: boolean | undefined) => { if (queue === undefined) { // El usuario canceló o cerró el modal return; } this.sendPluginCommandRequest(pluginCmd, queue, extraParams); }); return; } // 3. Todos los equipos están en línea en el sistema operativo correcto this.sendPluginCommandRequest(pluginCmd, false, extraParams); } private sendPluginCommandRequest(pluginCmd: PluginCommand, queue: boolean, extraParams: Record = {}): void { if (!pluginCmd.endpoint) { this.toastService.error('El comando no especifica un endpoint de ejecución', pluginCmd.label); return; } const cleanEndpoint = pluginCmd.endpoint.replace(/^\/api/, ''); const url = `${this.baseUrl}${cleanEndpoint}`; const clientIds = this.clientData.map(c => c.id || c['@id']); this.http.post(url, { clients: clientIds, queue: queue, target_os: pluginCmd.os, ...extraParams }).subscribe({ next: (response) => { const msg = response?.message || 'Comando procesado correctamente'; this.toastService.success(msg, pluginCmd.label); this.pluginService.loadActivePluginsStatus().subscribe(); }, error: (error) => { const errorMsg = error?.error?.message || error?.error?.['hydra:description'] || 'Error al comunicar con el servidor'; this.toastService.error(errorMsg, pluginCmd.label); } }); } }