Skip to main content
Glama

OPNSense MCP Server

engine.ts11.4 kB
/** * Deployment Execution Engine * Executes deployment plans with rollback support */ import { DeploymentPlan, ExecutionWave, ResourceChange } from '../deployment/planner'; import { IaCResource, ResourceStatus } from '../resources/base'; import { OPNSenseAPIClient } from '../api/client'; export interface ExecutionResult { success: boolean; planId: string; executedChanges: ExecutedChange[]; failedChanges?: FailedChange[]; rollbackPerformed: boolean; duration: number; logs: ExecutionLog[]; } export interface ExecutedChange { changeId: string; resourceId: string; type: string; status: 'success' | 'failed' | 'skipped'; outputs?: Record<string, any>; duration: number; } export interface FailedChange { changeId: string; resourceId: string; error: string; canRetry: boolean; } export interface ExecutionLog { timestamp: string; level: 'info' | 'warn' | 'error' | 'debug'; message: string; resourceId?: string; metadata?: Record<string, any>; } export interface ExecutionOptions { dryRun?: boolean; parallel?: boolean; maxConcurrency?: number; continueOnError?: boolean; force?: boolean; progressCallback?: (progress: ExecutionProgress) => void; } export interface ExecutionProgress { totalChanges: number; completedChanges: number; currentWave: number; totalWaves: number; currentResource?: string; estimatedTimeRemaining: number; } export class ExecutionEngine { private client: OPNSenseAPIClient; private logs: ExecutionLog[] = []; private checkpoints: Map<string, any> = new Map(); constructor(client: OPNSenseAPIClient) { this.client = client; } /** * Execute a deployment plan */ async execute( plan: DeploymentPlan, options: ExecutionOptions = {} ): Promise<ExecutionResult> { const startTime = Date.now(); const executedChanges: ExecutedChange[] = []; const failedChanges: FailedChange[] = []; let rollbackPerformed = false; this.log('info', `Starting execution of plan: ${plan.id}`); try { // Execute each wave for (let i = 0; i < plan.executionWaves.length; i++) { const wave = plan.executionWaves[i]; this.log('info', `Executing wave ${wave.wave} with ${wave.changes.length} changes`); if (options.progressCallback) { options.progressCallback({ totalChanges: this.getTotalChanges(plan), completedChanges: executedChanges.length, currentWave: wave.wave, totalWaves: plan.executionWaves.length, estimatedTimeRemaining: this.calculateRemainingTime(plan, i) }); } const waveResults = await this.executeWave(wave, options); executedChanges.push(...waveResults.executed); if (waveResults.failed.length > 0) { failedChanges.push(...waveResults.failed); if (!options.continueOnError) { this.log('error', 'Wave execution failed, initiating rollback'); rollbackPerformed = await this.rollback(executedChanges); break; } } // Create checkpoint after successful wave this.createCheckpoint(wave.wave, executedChanges); } } catch (error: any) { this.log('error', `Execution failed: ${error.message}`); rollbackPerformed = await this.rollback(executedChanges); } const duration = Date.now() - startTime; return { success: failedChanges.length === 0 && !rollbackPerformed, planId: plan.id, executedChanges, failedChanges: failedChanges.length > 0 ? failedChanges : undefined, rollbackPerformed, duration, logs: this.logs }; } /** * Execute a single wave of changes */ private async executeWave( wave: ExecutionWave, options: ExecutionOptions ): Promise<{ executed: ExecutedChange[]; failed: FailedChange[] }> { const executed: ExecutedChange[] = []; const failed: FailedChange[] = []; if (options.parallel && options.maxConcurrency && options.maxConcurrency > 1) { // Parallel execution const chunks = this.chunkArray(wave.changes, options.maxConcurrency); for (const chunk of chunks) { const promises = chunk.map(change => this.executeChange(change, options.dryRun || false) ); const results = await Promise.allSettled(promises); results.forEach((result, index) => { if (result.status === 'fulfilled') { executed.push(result.value); } else { failed.push({ changeId: this.getChangeId(chunk[index]), resourceId: chunk[index].resource.id, error: result.reason.message, canRetry: true }); } }); } } else { // Sequential execution for (const change of wave.changes) { try { const result = await this.executeChange(change, options.dryRun || false); executed.push(result); } catch (error: any) { failed.push({ changeId: this.getChangeId(change), resourceId: change.resource.id, error: error.message, canRetry: true }); if (!options.continueOnError) { break; } } } } return { executed, failed }; } /** * Execute a single resource change */ private async executeChange( change: ResourceChange, dryRun: boolean ): Promise<ExecutedChange> { const startTime = Date.now(); const changeId = this.getChangeId(change); this.log('info', `Executing ${change.type} for ${change.resource.id}`, { resourceId: change.resource.id, changeType: change.type }); try { if (dryRun) { // Simulate execution await this.simulateDelay(1000); return { changeId, resourceId: change.resource.id, type: change.type, status: 'success', duration: Date.now() - startTime }; } let outputs: Record<string, any> | undefined; switch (change.type) { case 'create': outputs = await this.createResource(change.resource); break; case 'update': outputs = await this.updateResource(change.resource, change.diff || []); break; case 'delete': await this.deleteResource(change.resource); break; case 'replace': await this.deleteResource(change.before!); outputs = await this.createResource(change.resource); break; } return { changeId, resourceId: change.resource.id, type: change.type, status: 'success', outputs, duration: Date.now() - startTime }; } catch (error: any) { this.log('error', `Failed to execute ${change.type} for ${change.resource.id}: ${error.message}`, { resourceId: change.resource.id, error: error.message }); throw error; } } /** * Create a resource */ private async createResource(resource: IaCResource): Promise<Record<string, any>> { // Resource-specific creation logic would go here // This is a placeholder that would call the appropriate API const payload = resource.toAPIPayload(); // Example for VLAN if (resource.type === 'opnsense:network:vlan') { const response = await this.client.post('/api/interfaces/vlan_settings/addItem', payload); resource.fromAPIResponse(response); return resource.getOutputs(); } throw new Error(`Create not implemented for resource type: ${resource.type}`); } /** * Update a resource */ private async updateResource( resource: IaCResource, diff: any[] ): Promise<Record<string, any>> { // Resource-specific update logic would go here const payload = resource.toAPIPayload(); // Example for VLAN if (resource.type === 'opnsense:network:vlan') { const response = await this.client.post( `/api/interfaces/vlan_settings/setItem/${resource.id}`, payload ); resource.fromAPIResponse(response); return resource.getOutputs(); } throw new Error(`Update not implemented for resource type: ${resource.type}`); } /** * Delete a resource */ private async deleteResource(resource: IaCResource): Promise<void> { // Resource-specific deletion logic would go here // Example for VLAN if (resource.type === 'opnsense:network:vlan') { await this.client.post(`/api/interfaces/vlan_settings/delItem/${resource.id}`, {}); return; } throw new Error(`Delete not implemented for resource type: ${resource.type}`); } /** * Rollback executed changes */ private async rollback(executedChanges: ExecutedChange[]): Promise<boolean> { this.log('warn', 'Starting rollback procedure'); // Rollback in reverse order const reversedChanges = [...executedChanges].reverse(); for (const change of reversedChanges) { try { this.log('info', `Rolling back ${change.type} for ${change.resourceId}`); // Rollback logic based on change type switch (change.type) { case 'create': // Delete the created resource // await this.deleteResource(...); break; case 'update': // Restore previous state // await this.updateResource(...); break; case 'delete': // Recreate the deleted resource // await this.createResource(...); break; } } catch (error: any) { this.log('error', `Failed to rollback ${change.resourceId}: ${error.message}`); return false; } } return true; } /** * Create execution checkpoint */ private createCheckpoint(waveNumber: number, executedChanges: ExecutedChange[]): void { const checkpointId = `wave-${waveNumber}`; this.checkpoints.set(checkpointId, { timestamp: new Date().toISOString(), executedChanges: [...executedChanges] }); } /** * Utility functions */ private getTotalChanges(plan: DeploymentPlan): number { return plan.executionWaves.reduce((total, wave) => total + wave.changes.length, 0); } private calculateRemainingTime(plan: DeploymentPlan, currentWaveIndex: number): number { return plan.executionWaves .slice(currentWaveIndex + 1) .reduce((total, wave) => total + wave.estimatedTime, 0); } private getChangeId(change: ResourceChange): string { return `${change.type}-${change.resource.id}-${Date.now()}`; } private chunkArray<T>(array: T[], size: number): T[][] { const chunks: T[][] = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } private async simulateDelay(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } private log(level: ExecutionLog['level'], message: string, metadata?: Record<string, any>): void { this.logs.push({ timestamp: new Date().toISOString(), level, message, ...metadata }); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/vespo92/OPNSenseMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server