oggui/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts

662 lines
22 KiB
TypeScript

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<string, string[]> = {
'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<any>(`${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<string, any>): 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<string, any> = {}): 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<any>(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);
}
});
}
}