UNPKG

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
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]; } }