UNPKG

n8n-nodes-proxmox

Version:

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

591 lines (473 loc) 19.2 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 executeContainerOperation( this: IExecuteFunctions, operation: string, i: number, ): Promise<IDataObject | IDataObject[]> { try { logger.operation('container', operation, 'start', { operation }); let response: IDataObject | IDataObject[]; switch (operation) { case 'create': response = await createContainer.call(this, i); break; case 'delete': response = await deleteContainer.call(this, i); break; case 'get': response = await getContainer.call(this, i); break; case 'getAll': response = await getAllContainers.call(this, i); break; case 'start': response = await startContainer.call(this, i); break; case 'stop': response = await stopContainer.call(this, i); break; case 'shutdown': response = await shutdownContainer.call(this, i); break; case 'reboot': response = await rebootContainer.call(this, i); break; case 'suspend': response = await suspendContainer.call(this, i); break; case 'resume': response = await resumeContainer.call(this, i); break; case 'clone': response = await cloneContainer.call(this, i); break; case 'migrate': response = await migrateContainer.call(this, i); break; case 'snapshot': response = await snapshotContainer.call(this, i); break; case 'getStatus': response = await getContainerStatus.call(this, i); break; default: throw new Error(`Unknown container operation: ${operation}`); } logger.operation('container', operation, 'success', { operation }); return response; } catch (error) { logger.operation('container', operation, 'error', { operation, error: error.message }); return handleProxMoxError.call(this, error, operation, 'container'); } } 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 2 seconds before checking again await new Promise((resolve) => setTimeout(resolve, 2000)); } 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 createContainer(this: IExecuteFunctions, i: number): Promise<IDataObject> { const nodeName = this.getNodeParameter('nodeName', i) as string; const newVmId = this.getNodeParameter('newVmId', i) as number; const ostemplate = this.getNodeParameter('ostemplate', i) as string; const waitForTask = this.getNodeParameter('waitForTask', i, true) as boolean; const taskTimeout = this.getNodeParameter('taskTimeout', i, 300) as number; const additionalOptions = this.getNodeParameter('additionalOptions', i, {}) as IDataObject; validateParameters.call( this, { nodeName, newVmId, ostemplate }, ['nodeName', 'newVmId', 'ostemplate'], 'create container', ); const body: IDataObject = { vmid: newVmId, ostemplate, ...additionalOptions, }; // Set defaults for container creation if (!body.memory) body.memory = 512; if (!body.swap) body.swap = 512; if (!body.cores) body.cores = 1; if (!body.storage) body.storage = 'local-lvm'; if (!body.rootfs) body.rootfs = `${body.storage}:8`; if (!body.net0) body.net0 = 'name=eth0,bridge=vmbr0,firewall=1,ip=dhcp'; if (body.unprivileged === undefined) body.unprivileged = 1; logger.debug('container', 'Creating container', { nodeName, vmId: newVmId, ostemplate }); const response = await apiRequest.call(this, 'POST', `/nodes/${nodeName}/lxc`, body); if (waitForTask && response.data) { const taskResult = await waitForTask.call(this, nodeName, response.data, taskTimeout); // Start container if requested if (additionalOptions.startAfterCreate) { logger.debug('container', 'Starting container after creation'); await apiRequest.call(this, 'POST', `/nodes/${nodeName}/lxc/${newVmId}/status/start`); } return { ...response, taskResult }; } return response; } async function deleteContainer(this: IExecuteFunctions, i: number): Promise<IDataObject> { const nodeName = this.getNodeParameter('nodeName', i) as string; const vmId = this.getNodeParameter('vmId', i) as number; const waitForTask = this.getNodeParameter('waitForTask', i, true) as boolean; const taskTimeout = this.getNodeParameter('taskTimeout', i, 300) as number; validateParameters.call(this, { nodeName, vmId }, ['nodeName', 'vmId'], 'delete container'); logger.debug('container', 'Deleting container', { nodeName, vmId }); const response = await apiRequest.call(this, 'DELETE', `/nodes/${nodeName}/lxc/${vmId}`); if (waitForTask && response.data) { const taskResult = await waitForTask.call(this, nodeName, response.data, taskTimeout); return { ...response, taskResult }; } return response; } async function getContainer(this: IExecuteFunctions, i: number): Promise<IDataObject> { const nodeName = this.getNodeParameter('nodeName', i) as string; const vmId = this.getNodeParameter('vmId', i) as number; validateParameters.call(this, { nodeName, vmId }, ['nodeName', 'vmId'], 'get container'); logger.debug('container', 'Getting container details', { nodeName, vmId }); return await apiRequest.call(this, 'GET', `/nodes/${nodeName}/lxc/${vmId}/config`); } async function getAllContainers(this: IExecuteFunctions, i: number): Promise<IDataObject[]> { const returnAll = this.getNodeParameter('returnAll', i) as boolean; logger.debug('container', 'Getting all containers'); console.log('=== getAllContainers Debug ==='); console.log('returnAll parameter:', returnAll); if (returnAll) { console.log('Making API request for all VMs (includes containers)...'); // Get all VMs and filter for LXC containers (type: 'lxc' in response data) const response = await apiRequestAllItems.call( this, 'GET', '/cluster/resources', {}, { type: 'vm' }, ); console.log( 'API response received, response type:', typeof response, 'isArray:', Array.isArray(response), ); console.log( 'Sample raw response data:', JSON.stringify(Array.isArray(response) ? response.slice(0, 3) : response, null, 2), ); // The ProxMox API should return an array directly for apiRequestAllItems let allResources: any[]; if (Array.isArray(response)) { allResources = response; console.log('Response is direct array with', allResources.length, 'resources'); } else if (response && response.data && Array.isArray(response.data)) { allResources = response.data; console.log('Found data array with', allResources.length, 'resources'); } else { allResources = [response]; console.log('Response is single object, converting to array'); } // Filter for LXC containers const containers = allResources.filter((resource: any) => { console.log( 'Checking resource:', JSON.stringify({ id: resource.id, type: resource.type, vmid: resource.vmid }, null, 2), ); return resource.type === 'lxc'; }); console.log('Filtered containers count:', containers.length); console.log('Filtered containers:', JSON.stringify(containers.slice(0, 2), null, 2)); return containers; } else { const limit = this.getNodeParameter('limit', i) as number; console.log('Making limited API request for VMs (includes containers), limit:', limit); // Get all VMs and filter for LXC containers const response = await apiRequest.call(this, 'GET', '/cluster/resources', {}, { type: 'vm' }); console.log('Limited API response type:', typeof response, 'isArray:', Array.isArray(response)); console.log( 'Limited API response received:', JSON.stringify(Array.isArray(response) ? response.slice(0, 3) : response, null, 2), ); // The ProxMox API wraps the data in a 'data' property let allResources: any[]; if (response && response.data && Array.isArray(response.data)) { allResources = response.data; console.log('Found data array with', allResources.length, 'resources'); } else if (Array.isArray(response)) { allResources = response; console.log('Response is direct array with', allResources.length, 'resources'); } else { allResources = [response]; console.log('Response is single object, converting to array'); } // Filter for LXC containers and then apply limit const containers = allResources.filter((resource: any) => { console.log( 'Checking resource:', JSON.stringify({ id: resource.id, type: resource.type, vmid: resource.vmid }, null, 2), ); return resource.type === 'lxc'; }); console.log('Filtered containers count before limit:', containers.length); return containers.slice(0, limit); } } async function startContainer(this: IExecuteFunctions, i: number): Promise<IDataObject> { const nodeName = this.getNodeParameter('nodeName', i) as string; const vmId = this.getNodeParameter('vmId', i) as number; const waitForTask = this.getNodeParameter('waitForTask', i, true) as boolean; const taskTimeout = this.getNodeParameter('taskTimeout', i, 300) as number; validateParameters.call(this, { nodeName, vmId }, ['nodeName', 'vmId'], 'start container'); logger.debug('container', 'Starting container', { nodeName, vmId }); const response = await apiRequest.call( this, 'POST', `/nodes/${nodeName}/lxc/${vmId}/status/start`, ); if (waitForTask && response.data) { const taskResult = await waitForTask.call(this, nodeName, response.data, taskTimeout); return { ...response, taskResult }; } return response; } async function stopContainer(this: IExecuteFunctions, i: number): Promise<IDataObject> { const nodeName = this.getNodeParameter('nodeName', i) as string; const vmId = this.getNodeParameter('vmId', i) as number; const waitForTask = this.getNodeParameter('waitForTask', i, true) as boolean; const taskTimeout = this.getNodeParameter('taskTimeout', i, 300) as number; const additionalOptions = this.getNodeParameter('additionalOptions', i, {}) as IDataObject; validateParameters.call(this, { nodeName, vmId }, ['nodeName', 'vmId'], 'stop container'); const body: IDataObject = {}; if (additionalOptions.force) body.force = 1; if (additionalOptions.skipLock) body.skiplock = 1; logger.debug('container', 'Stopping container', { nodeName, vmId, options: additionalOptions }); const response = await apiRequest.call( this, 'POST', `/nodes/${nodeName}/lxc/${vmId}/status/stop`, body, ); if (waitForTask && response.data) { const taskResult = await waitForTask.call(this, nodeName, response.data, taskTimeout); return { ...response, taskResult }; } return response; } async function shutdownContainer(this: IExecuteFunctions, i: number): Promise<IDataObject> { const nodeName = this.getNodeParameter('nodeName', i) as string; const vmId = this.getNodeParameter('vmId', i) as number; const waitForTask = this.getNodeParameter('waitForTask', i, true) as boolean; const taskTimeout = this.getNodeParameter('taskTimeout', i, 300) as number; const additionalOptions = this.getNodeParameter('additionalOptions', i, {}) as IDataObject; validateParameters.call(this, { nodeName, vmId }, ['nodeName', 'vmId'], 'shutdown container'); const body: IDataObject = {}; if (additionalOptions.force) body.forceStop = 1; if (additionalOptions.skipLock) body.skiplock = 1; logger.debug('container', 'Shutting down container', { nodeName, vmId, options: additionalOptions, }); const response = await apiRequest.call( this, 'POST', `/nodes/${nodeName}/lxc/${vmId}/status/shutdown`, body, ); if (waitForTask && response.data) { const taskResult = await waitForTask.call(this, nodeName, response.data, taskTimeout); return { ...response, taskResult }; } return response; } async function rebootContainer(this: IExecuteFunctions, i: number): Promise<IDataObject> { const nodeName = this.getNodeParameter('nodeName', i) as string; const vmId = this.getNodeParameter('vmId', i) as number; const waitForTask = this.getNodeParameter('waitForTask', i, true) as boolean; const taskTimeout = this.getNodeParameter('taskTimeout', i, 300) as number; validateParameters.call(this, { nodeName, vmId }, ['nodeName', 'vmId'], 'reboot container'); logger.debug('container', 'Rebooting container', { nodeName, vmId }); const response = await apiRequest.call( this, 'POST', `/nodes/${nodeName}/lxc/${vmId}/status/reboot`, ); if (waitForTask && response.data) { const taskResult = await waitForTask.call(this, nodeName, response.data, taskTimeout); return { ...response, taskResult }; } return response; } async function suspendContainer(this: IExecuteFunctions, i: number): Promise<IDataObject> { const nodeName = this.getNodeParameter('nodeName', i) as string; const vmId = this.getNodeParameter('vmId', i) as number; const waitForTask = this.getNodeParameter('waitForTask', i, true) as boolean; const taskTimeout = this.getNodeParameter('taskTimeout', i, 300) as number; validateParameters.call(this, { nodeName, vmId }, ['nodeName', 'vmId'], 'suspend container'); logger.debug('container', 'Suspending container', { nodeName, vmId }); const response = await apiRequest.call( this, 'POST', `/nodes/${nodeName}/lxc/${vmId}/status/suspend`, ); if (waitForTask && response.data) { const taskResult = await waitForTask.call(this, nodeName, response.data, taskTimeout); return { ...response, taskResult }; } return response; } async function resumeContainer(this: IExecuteFunctions, i: number): Promise<IDataObject> { const nodeName = this.getNodeParameter('nodeName', i) as string; const vmId = this.getNodeParameter('vmId', i) as number; const waitForTask = this.getNodeParameter('waitForTask', i, true) as boolean; const taskTimeout = this.getNodeParameter('taskTimeout', i, 300) as number; validateParameters.call(this, { nodeName, vmId }, ['nodeName', 'vmId'], 'resume container'); logger.debug('container', 'Resuming container', { nodeName, vmId }); const response = await apiRequest.call( this, 'POST', `/nodes/${nodeName}/lxc/${vmId}/status/resume`, ); if (waitForTask && response.data) { const taskResult = await waitForTask.call(this, nodeName, response.data, taskTimeout); return { ...response, taskResult }; } return response; } async function cloneContainer(this: IExecuteFunctions, i: number): Promise<IDataObject> { const nodeName = this.getNodeParameter('nodeName', i) as string; const vmId = this.getNodeParameter('vmId', i) as number; const newVmId = this.getNodeParameter('newVmId', i) as number; const cloneName = this.getNodeParameter('cloneName', i, '') as string; const waitForTask = this.getNodeParameter('waitForTask', i, true) as boolean; const taskTimeout = this.getNodeParameter('taskTimeout', i, 300) as number; const additionalOptions = this.getNodeParameter('additionalOptions', i, {}) as IDataObject; validateParameters.call( this, { nodeName, vmId, newVmId }, ['nodeName', 'vmId', 'newVmId'], 'clone container', ); const body: IDataObject = { newid: newVmId, ...additionalOptions, }; if (cloneName) { body.hostname = cloneName; } logger.debug('container', 'Cloning container', { nodeName, vmId, newVmId, cloneName }); const response = await apiRequest.call( this, 'POST', `/nodes/${nodeName}/lxc/${vmId}/clone`, body, ); if (waitForTask && response.data) { const taskResult = await waitForTask.call(this, nodeName, response.data, taskTimeout); return { ...response, taskResult }; } return response; } async function migrateContainer(this: IExecuteFunctions, i: number): Promise<IDataObject> { const nodeName = this.getNodeParameter('nodeName', i) as string; const vmId = this.getNodeParameter('vmId', i) as number; const targetNode = this.getNodeParameter('targetNode', i) as string; const waitForTask = this.getNodeParameter('waitForTask', i, true) as boolean; const taskTimeout = this.getNodeParameter('taskTimeout', i, 300) as number; const additionalOptions = this.getNodeParameter('additionalOptions', i, {}) as IDataObject; validateParameters.call( this, { nodeName, vmId, targetNode }, ['nodeName', 'vmId', 'targetNode'], 'migrate container', ); const body: IDataObject = { target: targetNode, ...additionalOptions, }; // Set default migration options if (additionalOptions.online !== false) { body.online = 1; } if (additionalOptions.restart) { body.restart = 1; } logger.debug('container', 'Migrating container', { nodeName, vmId, targetNode, options: additionalOptions, }); const response = await apiRequest.call( this, 'POST', `/nodes/${nodeName}/lxc/${vmId}/migrate`, body, ); if (waitForTask && response.data) { const taskResult = await waitForTask.call(this, nodeName, response.data, taskTimeout); return { ...response, taskResult }; } return response; } async function snapshotContainer(this: IExecuteFunctions, i: number): Promise<IDataObject> { const nodeName = this.getNodeParameter('nodeName', i) as string; const vmId = this.getNodeParameter('vmId', i) as number; const snapshotName = this.getNodeParameter('snapshotName', i) as string; const waitForTask = this.getNodeParameter('waitForTask', i, true) as boolean; const taskTimeout = this.getNodeParameter('taskTimeout', i, 300) as number; validateParameters.call( this, { nodeName, vmId, snapshotName }, ['nodeName', 'vmId', 'snapshotName'], 'snapshot container', ); const body: IDataObject = { snapname: snapshotName, }; logger.debug('container', 'Creating container snapshot', { nodeName, vmId, snapshotName }); const response = await apiRequest.call( this, 'POST', `/nodes/${nodeName}/lxc/${vmId}/snapshot`, body, ); if (waitForTask && response.data) { const taskResult = await waitForTask.call(this, nodeName, response.data, taskTimeout); return { ...response, taskResult }; } return response; } async function getContainerStatus(this: IExecuteFunctions, i: number): Promise<IDataObject> { const nodeName = this.getNodeParameter('nodeName', i) as string; const vmId = this.getNodeParameter('vmId', i) as number; validateParameters.call(this, { nodeName, vmId }, ['nodeName', 'vmId'], 'get container status'); logger.debug('container', 'Getting container status', { nodeName, vmId }); return await apiRequest.call(this, 'GET', `/nodes/${nodeName}/lxc/${vmId}/status/current`); }