UNPKG

n8n-nodes-proxmox

Version:

n8n community node for Proxmox Virtual Environment (VE) API integration with VM, container, storage, and cluster management capabilities

278 lines (227 loc) 8.55 kB
import { IExecuteFunctions, IDataObject } from 'n8n-workflow'; import { apiRequest, apiRequestAllItems } from '../helpers/apiRequest'; import { handleProxMoxError, validateParameters } from '../helpers/errorHandler'; import { logger } from '../helpers/logger'; export async function executeBackupOperation( this: IExecuteFunctions, operation: string, i: number, ): Promise<IDataObject | IDataObject[]> { try { logger.operation('backup', operation, 'start', { operation }); let response: IDataObject | IDataObject[]; switch (operation) { case 'create': response = await createBackup.call(this, i); break; case 'restore': response = await restoreBackup.call(this, i); break; case 'getAll': response = await getAllBackups.call(this, i); break; case 'delete': response = await deleteBackup.call(this, i); break; case 'getJobs': response = await getBackupJobs.call(this, i); break; default: throw new Error(`Unknown backup operation: ${operation}`); } logger.operation('backup', operation, 'success', { operation }); return response; } catch (error) { logger.operation('backup', operation, 'error', { operation, error: error.message }); return handleProxMoxError.call(this, error, operation, 'backup'); } } async function waitForTask( this: IExecuteFunctions, nodeName: string, taskId: string, timeout: number = 300, ): Promise<IDataObject> { const startTime = Date.now(); const timeoutMs = timeout * 1000; logger.debug('task', `Waiting for task ${taskId} on node ${nodeName}`, { taskId, timeout }); while (Date.now() - startTime < timeoutMs) { try { const taskStatus = await apiRequest.call( this, 'GET', `/nodes/${nodeName}/tasks/${taskId}/status`, ); if (taskStatus.status === 'stopped') { if (taskStatus.exitstatus === 'OK') { logger.debug('task', `Task ${taskId} completed successfully`); return taskStatus; } else { throw new Error(`Task failed: ${taskStatus.exitstatus}`); } } // Wait 5 seconds before checking again (backups can take a while) await new Promise((resolve) => setTimeout(resolve, 5000)); } catch (error) { logger.error('task', `Error checking task status: ${error.message}`); throw error; } } throw new Error(`Task ${taskId} timed out after ${timeout} seconds`); } async function createBackup(this: IExecuteFunctions, i: number): Promise<IDataObject> { const nodeName = this.getNodeParameter('nodeName', i) as string; const vmId = this.getNodeParameter('vmId', i) as number; const storage = this.getNodeParameter('storage', i) as string; const waitForTask = this.getNodeParameter('waitForTask', i, true) as boolean; const taskTimeout = this.getNodeParameter('taskTimeout', i, 300) as number; const backupOptions = this.getNodeParameter('backupOptions', i, {}) as IDataObject; validateParameters.call( this, { nodeName, vmId, storage }, ['nodeName', 'vmId', 'storage'], 'create backup', ); const body: IDataObject = { vmid: vmId, storage, ...backupOptions, }; // Set default backup options if (!body.compress) body.compress = 'zstd'; if (!body.mode) body.mode = 'snapshot'; if (body.includeRam) body.vmstate = 1; if (body.remove === undefined) body.remove = 1; logger.debug('backup', 'Creating backup', { nodeName, vmId, storage, options: backupOptions }); const response = await apiRequest.call(this, 'POST', `/nodes/${nodeName}/vzdump`, body); if (waitForTask && response.data) { const taskResult = await waitForTask.call(this, nodeName, response.data, taskTimeout); return { ...response, taskResult }; } return response; } async function restoreBackup(this: IExecuteFunctions, i: number): Promise<IDataObject> { const nodeName = this.getNodeParameter('nodeName', i) as string; const archive = this.getNodeParameter('archive', i) as string; const newVmId = this.getNodeParameter('newVmId', i) as number; const waitForTask = this.getNodeParameter('waitForTask', i, true) as boolean; const taskTimeout = this.getNodeParameter('taskTimeout', i, 300) as number; const restoreOptions = this.getNodeParameter('restoreOptions', i, {}) as IDataObject; validateParameters.call( this, { nodeName, archive, newVmId }, ['nodeName', 'archive', 'newVmId'], 'restore backup', ); const body: IDataObject = { archive, vmid: newVmId, ...restoreOptions, }; // Set default restore options if (!body.storage) body.storage = 'local-lvm'; if (body.start) body.start = 1; if (body.unique) body.unique = 1; if (body.force) body.force = 1; logger.debug('backup', 'Restoring backup', { nodeName, archive, newVmId, options: restoreOptions, }); // Determine if it's a VM or Container backup based on the archive name const isContainer = archive.includes('vzdump-lxc-') || archive.includes('-ct-'); const endpoint = isContainer ? `/nodes/${nodeName}/lxc` : `/nodes/${nodeName}/qemu`; const response = await apiRequest.call(this, 'POST', endpoint, body); if (waitForTask && response.data) { const taskResult = await waitForTask.call(this, nodeName, response.data, taskTimeout); return { ...response, taskResult }; } return response; } async function getAllBackups(this: IExecuteFunctions, i: number): Promise<IDataObject[]> { const nodeName = this.getNodeParameter('nodeName', i) as string; const returnAll = this.getNodeParameter('returnAll', i) as boolean; const filterOptions = this.getNodeParameter('filterOptions', i, {}) as IDataObject; validateParameters.call(this, { nodeName }, ['nodeName'], 'get all backups'); let qs: IDataObject = { content: filterOptions.content || 'backup', }; // Add additional filters if (filterOptions.vmid) { qs.vmid = filterOptions.vmid; } logger.debug('backup', 'Getting all backups', { nodeName, filterOptions }); // Get backups from all storages on the node const storagesResponse = await apiRequest.call(this, 'GET', `/nodes/${nodeName}/storage`); // Handle different response formats let storages: IDataObject[]; if (Array.isArray(storagesResponse)) { storages = storagesResponse; } else if (storagesResponse && typeof storagesResponse === 'object' && storagesResponse.data) { storages = Array.isArray(storagesResponse.data) ? storagesResponse.data : [storagesResponse.data]; } else { logger.error('backup', 'Unexpected storages response format', { storagesResponse }); throw new Error('Failed to get storage list - unexpected response format'); } const allBackups: IDataObject[] = []; for (const storage of storages) { if (storage.content && (storage.content as string).includes('backup')) { try { const backups = await apiRequest.call( this, 'GET', `/nodes/${nodeName}/storage/${storage.storage}/content`, {}, qs, ); if (Array.isArray(backups)) { allBackups.push(...backups.map((backup) => ({ ...backup, storage: storage.storage }))); } } catch (error) { logger.warn( 'backup', `Failed to get backups from storage ${storage.storage}: ${error.message}`, ); } } } if (returnAll) { return allBackups; } else { const limit = this.getNodeParameter('limit', i) as number; return allBackups.slice(0, limit); } } async function deleteBackup(this: IExecuteFunctions, i: number): Promise<IDataObject> { const nodeName = this.getNodeParameter('nodeName', i) as string; const storage = this.getNodeParameter('storage', i) as string; const backupFile = this.getNodeParameter('backupFile', i) as string; validateParameters.call( this, { nodeName, storage, backupFile }, ['nodeName', 'storage', 'backupFile'], 'delete backup', ); // The volume ID format is typically storage:path const volumeId = `${storage}:${backupFile}`; logger.debug('backup', 'Deleting backup', { nodeName, storage, backupFile, volumeId }); return await apiRequest.call( this, 'DELETE', `/nodes/${nodeName}/storage/${storage}/content/${encodeURIComponent(volumeId)}`, ); } async function getBackupJobs(this: IExecuteFunctions, i: number): Promise<IDataObject[]> { const returnAll = this.getNodeParameter('returnAll', i) as boolean; logger.debug('backup', 'Getting backup jobs'); if (returnAll) { return await apiRequestAllItems.call(this, 'GET', '/cluster/backup', {}); } else { const limit = this.getNodeParameter('limit', i) as number; const response = await apiRequest.call(this, 'GET', '/cluster/backup', {}, { limit }); return Array.isArray(response) ? response.slice(0, limit) : [response]; } }