n8n-nodes-proxmox
Version:
n8n community node for Proxmox Virtual Environment (VE) API integration with VM, container, storage, and cluster management capabilities
211 lines (184 loc) • 6.05 kB
text/typescript
import {
IExecuteFunctions,
IHttpRequestOptions,
NodeApiError,
NodeOperationError,
IDataObject,
IHttpRequestMethods,
ICredentialDataDecryptedObject,
} from 'n8n-workflow';
interface TicketCacheEntry {
ticket: string;
csrfToken: string;
expiresAt: number;
}
const ticketCache: Record<string, TicketCacheEntry> = {};
/**
* Check if a ticket is expired
*/
function isTicketExpired(expiresAt: number): boolean {
return Date.now() > expiresAt - 60000; // Consider expired 60 seconds before actual expiration
}
/**
* Get authentication ticket for username/password auth
*/
async function getAuthTicket(
this: IExecuteFunctions,
credentials: ICredentialDataDecryptedObject,
): Promise<{ ticket: string; csrfToken: string }> {
const cacheKey = `${credentials.host}_${credentials.port}_${credentials.username}`;
// Check cache first
if (ticketCache[cacheKey] && !isTicketExpired(ticketCache[cacheKey].expiresAt)) {
return {
ticket: ticketCache[cacheKey].ticket,
csrfToken: ticketCache[cacheKey].csrfToken,
};
}
// Get new ticket
const protocol = credentials.skipSslVerify ? 'http' : 'https';
const baseUrl = `${protocol}://${credentials.host}:${credentials.port}`;
try {
const response = await this.helpers.request({
method: 'POST',
uri: `${baseUrl}/api2/json/access/ticket`,
body: {
username: credentials.username,
password: credentials.password,
},
json: true,
rejectUnauthorized: !credentials.skipSslVerify,
timeout: credentials.timeout || 30000,
});
if (!response?.data?.ticket || !response?.data?.CSRFPreventionToken) {
throw new Error('Invalid authentication response from ProxMox');
}
// Cache ticket (ProxMox tickets typically expire after 2 hours)
const expiresAt = Date.now() + 2 * 60 * 60 * 1000; // 2 hours
ticketCache[cacheKey] = {
ticket: response.data.ticket,
csrfToken: response.data.CSRFPreventionToken,
expiresAt,
};
return {
ticket: response.data.ticket,
csrfToken: response.data.CSRFPreventionToken,
};
} catch (error) {
throw new NodeOperationError(this.getNode(), `ProxMox authentication failed: ${error.message}`);
}
}
/**
* Make an API request to ProxMox VE
*/
export async function apiRequest(
this: IExecuteFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject = {},
qs: IDataObject = {},
): Promise<any> {
const credentials = await this.getCredentials('proxMoxApi');
if (!credentials) {
throw new NodeOperationError(this.getNode(), 'No credentials provided');
}
const protocol = credentials.skipSslVerify ? 'http' : 'https';
const baseUrl = `${protocol}://${credentials.host}:${credentials.port}`;
const fullUrl = `${baseUrl}/api2/json${endpoint}`;
let headers: IDataObject = {
'Content-Type': 'application/json',
};
// Handle authentication
if (credentials.authMethod === 'apiToken') {
// API Token authentication - ProxMox expects 'PVEAPIToken=TOKEN' format
const tokenValue = credentials.tokenId as string;
// Check if token already has PVEAPIToken prefix
if (tokenValue.startsWith('PVEAPIToken=')) {
headers.Authorization = tokenValue;
} else {
headers.Authorization = `PVEAPIToken=${tokenValue}`;
}
} else {
// Username/Password with ticket authentication
const auth = await getAuthTicket.call(this, credentials);
headers.Cookie = `PVEAuthCookie=${auth.ticket}`;
// Add CSRF token for write operations
if (method !== 'GET') {
headers.CSRFPreventionToken = auth.csrfToken;
}
}
const requestOptions: IHttpRequestOptions = {
method,
url: fullUrl,
headers,
body: Object.keys(body).length > 0 ? body : undefined,
qs: Object.keys(qs).length > 0 ? qs : undefined,
json: true,
rejectUnauthorized: !credentials.skipSslVerify,
timeout: (credentials.timeout as number) || 30000,
resolveWithFullResponse: true,
};
try {
const response = await this.helpers.httpRequest(requestOptions);
// ProxMox API returns data in response.data
if (response.body?.data !== undefined) {
return response.body.data;
}
return response.body || response;
} catch (error) {
if (error.response) {
let message = 'ProxMox API error';
const errorBody = error.error || error.response?.body;
if (errorBody) {
if (typeof errorBody === 'object') {
if (errorBody.errors) {
// ProxMox validation errors
const errors = Array.isArray(errorBody.errors)
? errorBody.errors.join(', ')
: JSON.stringify(errorBody.errors);
message = `Validation error: ${errors}`;
} else if (errorBody.message) {
message = errorBody.message;
} else if (errorBody.reason) {
message = errorBody.reason;
}
} else if (typeof errorBody === 'string') {
message = errorBody;
}
}
// Handle specific HTTP status codes
if (error.statusCode === 401) {
// Clear cached ticket if authentication fails
const cacheKey = `${credentials.host}_${credentials.port}_${credentials.username}`;
delete ticketCache[cacheKey];
message = 'Authentication failed. Please check your credentials.';
} else if (error.statusCode === 403) {
// Permission denied - include the status text for more detail
const statusText = error.response?.statusText || 'Permission denied';
message = `Permission denied: ${statusText}`;
}
throw new NodeApiError(this.getNode(), error, {
message: `${message} (Status: ${error.statusCode})`,
});
}
throw new NodeOperationError(this.getNode(), `ProxMox API request failed: ${error.message}`);
}
}
/**
* Make paginated requests to ProxMox API
*/
export async function apiRequestAllItems(
this: IExecuteFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject = {},
qs: IDataObject = {},
): Promise<any[]> {
// ProxMox API doesn't support standard pagination, just make a single request
const response = await apiRequest.call(this, method, endpoint, body, qs);
if (Array.isArray(response)) {
return response;
} else {
// Single item response
return [response];
}
}