/** * Lilith Platform - Rollback Executor * * Restores infrastructure state on verification failure. * Supports reversible, partial, and irreversible rollback categories. */ /** * Feature rollback capability matrix */ const ROLLBACK_CAPABILITY = { hostname: 'reversible', // Can set back to previous hostname services: 'reversible', // Can enable/disable services packages: 'partial', // Can reinstall, but removed packages may leave config firewall: 'irreversible', // Rules may be lost on removal vpn: 'irreversible', // Keys cannot be recovered agent: 'reversible', // Can redeploy previous version cron: 'reversible', // Can restore cron entries dns: 'partial', // Zone changes may propagate files: 'reversible', // File content can be restored certs: 'irreversible', // Rotated certs cannot be recovered users: 'partial', // User changes may have side effects }; export class RollbackExecutor { /** * Create rollback executor * * @param {object} appliers - Map of feature name to applier function * @param {Function} sshExec - SSH execution function */ constructor(appliers, sshExec) { this.appliers = appliers; this.sshExec = sshExec; this.rollbackCapability = ROLLBACK_CAPABILITY; } /** * Get rollback capability for a feature * * @param {string} feature - Feature name * @returns {'reversible' | 'partial' | 'irreversible'} */ getCapability(feature) { return this.rollbackCapability[feature] || 'irreversible'; } /** * Execute rollback for failed verification * * @param {object} host - Host configuration * @param {object} originalState - Pre-reconciliation state from snapshot * @param {Array<{ feature: string }>} appliedFeatures - Features that were applied * @returns {Promise<{ success: boolean, results: object }>} */ async executeRollback(host, originalState, appliedFeatures) { const results = { restored: [], failed: [], skipped: [], }; // Rollback in reverse order of application const reversedFeatures = [...appliedFeatures].reverse(); for (const { feature } of reversedFeatures) { const capability = this.getCapability(feature); // Skip irreversible features if (capability === 'irreversible') { results.skipped.push({ feature, reason: 'irreversible', note: `Manual intervention required to restore ${feature}`, }); continue; } try { const originalFeatureState = originalState.features?.[feature]?.state; if (!originalFeatureState) { results.skipped.push({ feature, reason: 'no-original-state', }); continue; } // Get applier for this feature const applier = this.appliers[feature]; if (!applier) { results.skipped.push({ feature, reason: 'no-applier', }); continue; } // Apply original state const rollbackResult = await applier( host, { state: originalFeatureState }, {}, // Current state doesn't matter for rollback this.sshExec ); if (rollbackResult.success) { results.restored.push({ feature, capability, appliedState: originalFeatureState, }); } else { results.failed.push({ feature, capability, error: rollbackResult.error || 'Unknown error', }); } } catch (err) { results.failed.push({ feature, capability, error: err.message, }); } } return { success: results.failed.length === 0, results, summary: this.generateSummary(results), }; } /** * Rollback a single feature * * @param {object} host - Host configuration * @param {string} feature - Feature name * @param {object} originalState - Original feature state * @returns {Promise<{ success: boolean, error?: string }>} */ async rollbackFeature(host, feature, originalState) { const capability = this.getCapability(feature); if (capability === 'irreversible') { return { success: false, error: `Feature ${feature} is irreversible and cannot be rolled back`, }; } const applier = this.appliers[feature]; if (!applier) { return { success: false, error: `No applier found for feature ${feature}`, }; } try { const result = await applier( host, { state: originalState }, {}, this.sshExec ); return result; } catch (err) { return { success: false, error: err.message, }; } } /** * Check if all features in a list are reversible * * @param {string[]} features - List of feature names * @returns {{ allReversible: boolean, irreversible: string[], partial: string[] }} */ analyzeRollbackability(features) { const irreversible = []; const partial = []; for (const feature of features) { const capability = this.getCapability(feature); if (capability === 'irreversible') { irreversible.push(feature); } else if (capability === 'partial') { partial.push(feature); } } return { allReversible: irreversible.length === 0 && partial.length === 0, fullyReversible: irreversible.length === 0, irreversible, partial, }; } /** * Generate human-readable rollback summary */ generateSummary(results) { const lines = []; if (results.restored.length > 0) { lines.push(`Restored: ${results.restored.map(r => r.feature).join(', ')}`); } if (results.failed.length > 0) { lines.push(`Failed: ${results.failed.map(r => `${r.feature} (${r.error})`).join(', ')}`); } if (results.skipped.length > 0) { const irreversible = results.skipped.filter(s => s.reason === 'irreversible'); if (irreversible.length > 0) { lines.push(`Irreversible (manual intervention required): ${irreversible.map(s => s.feature).join(', ')}`); } } return lines.join('\n'); } } export default RollbackExecutor;