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
text/typescript
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];
}
}