UNPKG

n8n-nodes-awx

Version:

n8n node to interact with Ansible AWX/Tower with improved type safety

544 lines 23.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.awxApiRequest = awxApiRequest; exports.awxApiRequestAllItems = awxApiRequestAllItems; exports.formatAwxFilters = formatAwxFilters; exports.waitForJobCompletion = waitForJobCompletion; exports.getJobTemplate = getJobTemplate; exports.getGroup = getGroup; exports.getHost = getHost; exports.getProject = getProject; exports.getWorkflowJobTemplate = getWorkflowJobTemplate; exports.createProject = createProject; exports.updateProject = updateProject; exports.deleteProject = deleteProject; exports.createJobTemplate = createJobTemplate; exports.updateJobTemplate = updateJobTemplate; exports.deleteJobTemplate = deleteJobTemplate; exports.createWorkflowJobTemplate = createWorkflowJobTemplate; exports.updateWorkflowJobTemplate = updateWorkflowJobTemplate; exports.deleteWorkflowJobTemplate = deleteWorkflowJobTemplate; exports.resolveResourceId = resolveResourceId; exports.postAwxResource = postAwxResource; exports.parseAwxVariables = parseAwxVariables; exports.getResourceById = getResourceById; exports.getInventoryById = getInventoryById; exports.getGroupById = getGroupById; exports.getHostById = getHostById; exports.getProjectById = getProjectById; exports.getJobTemplateById = getJobTemplateById; exports.getWorkflowTemplateById = getWorkflowTemplateById; const n8n_workflow_1 = require("n8n-workflow"); async function awxApiRequest(method, endpoint, body = {}, query = {}, options = { timeout: null, waitForCompletion: null, pollInterval: null, maxRetries: null, forceRefresh: null, }) { var _a, _b, _c; const credentials = await this.getCredentials('awxApi'); const baseUrl = String(credentials.baseUrl || ''); const sanitizedBaseUrl = baseUrl.replace(/\/+$/, ''); const hasApiPrefix = endpoint.includes('/api/v2'); const sanitizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; const fullUrl = hasApiPrefix ? `${sanitizedBaseUrl}${sanitizedEndpoint}` : `${sanitizedBaseUrl}/api/v2${sanitizedEndpoint}`; console.log('==================== AWX DEBUG ===================='); console.log(`Original endpoint: "${endpoint}"`); console.log(`Sanitized endpoint: "${sanitizedEndpoint}"`); console.log(`Base URL: "${baseUrl}"`); console.log(`Cleaned Base URL: "${sanitizedBaseUrl}"`); console.log(`Has API prefix: ${hasApiPrefix}`); console.log(`Full URL: "${fullUrl}"`); console.log('==================================================='); const opts = { method, body, qs: query, url: fullUrl, json: true, headers: { 'Authorization': `Bearer ${credentials.token}`, 'Content-Type': 'application/json' }, timeout: options.timeout || 10000, }; try { if (credentials.allowUnauthorizedCerts === true) { console.debug('Making AWX API request with SSL verification disabled'); } const response = await this.helpers.request(opts); console.log(`[AWX] Successful API call to: ${method} ${fullUrl}`); return response; } catch (error) { console.error(`[AWX] API Error: ${method} ${fullUrl}`, { status: (_a = error.response) === null || _a === void 0 ? void 0 : _a.status, message: ((_c = (_b = error.response) === null || _b === void 0 ? void 0 : _b.body) === null || _c === void 0 ? void 0 : _c.detail) || error.message }); if (error.response && error.response.body) { const errorBody = error.response.body; const message = errorBody.detail || errorBody.error || error.message || 'Unknown AWX API error'; throw new n8n_workflow_1.NodeApiError(this.getNode(), error, { message, description: `AWX API Error (${fullUrl}): ${message}`, }); } throw new n8n_workflow_1.NodeApiError(this.getNode(), error, { message: `AWX API Error: ${error.message}`, description: `Failed request to: ${fullUrl}` }); } } const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); async function awxApiRequestAllItems(method, endpoint, body = {}, query = {}, options = { timeout: null, waitForCompletion: null, pollInterval: null, maxRetries: null, forceRefresh: null, }) { let allItems = []; let nextUrl = endpoint; let currentPage = 1; const paginationDelayMs = 500; let currentQuery = { ...query }; while (nextUrl) { console.log(`[AWX] Fetching page ${currentPage} from: ${nextUrl}`); try { const response = await awxApiRequest.call(this, method, nextUrl, body, currentQuery, options); if (response.results && Array.isArray(response.results)) { allItems = allItems.concat(response.results); } else if (Array.isArray(response)) { allItems = allItems.concat(response); console.warn(`[AWX] Paginated response for ${nextUrl} was an array directly. Assuming end of pagination.`); nextUrl = null; break; } if (response.next) { const url = new URL(response.next, await this.getCredentials('awxApi').then(c => c.baseUrl)); nextUrl = `${url.pathname}${url.search}`; currentQuery = {}; console.log(`[AWX] Next page URL: ${nextUrl}`); } else { nextUrl = null; console.log(`[AWX] No more pages for ${endpoint}. Total items fetched: ${allItems.length}`); } currentPage++; if (nextUrl) { console.log(`[AWX] Waiting ${paginationDelayMs}ms before fetching next page...`); await delay(paginationDelayMs); } } catch (error) { console.error(`[AWX] Error fetching page ${currentPage} from ${nextUrl}:`, error); throw error; } if (method !== 'GET' && method !== 'HEAD') { body = {}; } } return allItems; } function formatAwxFilters(filters, resource) { const query = {}; if (filters.name) { query[`${resource}__name__icontains`] = filters.name; } if (filters.id) { query[`${resource}__id`] = filters.id; } return query; } async function waitForJobCompletion(jobId, options = { timeout: null, waitForCompletion: null, pollInterval: null, maxRetries: null, forceRefresh: null, }) { if (!options.waitForCompletion) { return awxApiRequest.call(this, 'GET', `/jobs/${jobId}/`); } const pollInterval = options.pollInterval || 5000; const maxRetries = options.maxRetries || 60; let retries = 0; let job; do { job = await awxApiRequest.call(this, 'GET', `/jobs/${jobId}/`); if (['successful', 'failed', 'error', 'canceled'].includes(job.status)) { return job; } await new Promise(resolve => setTimeout(resolve, pollInterval)); retries++; } while (retries < maxRetries); throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Timeout waiting for job ${jobId} to complete after ${(pollInterval * maxRetries) / 1000} seconds`); } async function getJobTemplate(nameOrId, throwOnNotFound = false) { try { try { const response = await awxApiRequest.call(this, 'GET', `/job_templates/${nameOrId}/`); console.log(`Found job template directly with ID: ${nameOrId}`); return response; } catch (directError) { console.log(`Direct ID lookup failed for job template: ${nameOrId}, trying name lookup...`); } const queryUrl = `/job_templates/?name=${encodeURIComponent(nameOrId)}`; console.log(`Looking up job template by name using: ${queryUrl}`); const searchResults = await awxApiRequest.call(this, 'GET', queryUrl); if ((searchResults === null || searchResults === void 0 ? void 0 : searchResults.results) && searchResults.results.length > 0) { const foundTemplate = searchResults.results[0]; console.log(`Found job template by name: ${nameOrId}, ID: ${foundTemplate.id}`); return foundTemplate; } if (throwOnNotFound) { throw new Error(`No job template found with name: ${nameOrId}`); } console.log(`No job template found with name: ${nameOrId}`); return null; } catch (error) { console.log(`Error in getJobTemplate: ${error.message}`); if (throwOnNotFound) { throw error; } return null; } } async function getGroup(nameOrId, inventoryId, throwOnNotFound = true) { try { if (/^\d+$/.test(nameOrId)) { console.log(`AWX Node: Getting group with ID ${nameOrId}`); const endpoint = inventoryId ? `/inventories/${inventoryId}/groups/${nameOrId}/` : `/groups/${nameOrId}/`; return await awxApiRequest.call(this, 'GET', endpoint); } console.log(`AWX Node: Searching for group with name ${nameOrId}`); const query = { name: nameOrId }; const endpoint = inventoryId ? `/inventories/${inventoryId}/groups/` : `/groups/`; const response = await awxApiRequest.call(this, 'GET', endpoint, {}, query); if ((response === null || response === void 0 ? void 0 : response.results) && response.results.length > 0) { console.log(`AWX Node: Found group with name ${nameOrId}`); return response.results[0]; } if (throwOnNotFound) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Group "${nameOrId}" not found`); } console.log(`AWX Node: Group "${nameOrId}" not found, but not throwing error`); return null; } catch (error) { console.log(`AWX Node: Error getting group ${nameOrId}: ${error.message}`); if (throwOnNotFound) { throw error; } return null; } } async function getHost(nameOrId, inventoryId, groupId, throwOnNotFound = true) { try { if (/^\d+$/.test(nameOrId)) { console.log(`AWX Node: Getting host with ID ${nameOrId}`); let endpoint = ''; if (inventoryId && groupId) { endpoint = `/inventories/${inventoryId}/groups/${groupId}/hosts/${nameOrId}/`; } else if (inventoryId) { endpoint = `/inventories/${inventoryId}/hosts/${nameOrId}/`; } else { endpoint = `/hosts/${nameOrId}/`; } return await awxApiRequest.call(this, 'GET', endpoint); } console.log(`AWX Node: Searching for host with name ${nameOrId}`); const query = { name: nameOrId }; let endpoint = ''; if (inventoryId && groupId) { endpoint = `/inventories/${inventoryId}/groups/${groupId}/hosts/`; } else if (inventoryId) { endpoint = `/inventories/${inventoryId}/hosts/`; } else { endpoint = `/hosts/`; } const response = await awxApiRequest.call(this, 'GET', endpoint, {}, query); if ((response === null || response === void 0 ? void 0 : response.results) && response.results.length > 0) { console.log(`AWX Node: Found host with name ${nameOrId}`); return response.results[0]; } if (throwOnNotFound) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Host "${nameOrId}" not found`); } console.log(`AWX Node: Host "${nameOrId}" not found, but not throwing error`); return null; } catch (error) { console.log(`AWX Node: Error getting host ${nameOrId}: ${error.message}`); if (throwOnNotFound) { throw error; } return null; } } async function getProject(nameOrId, throwOnNotFound = true) { try { if (/^\d+$/.test(nameOrId)) { const response = await awxApiRequest.call(this, 'GET', `/projects/${nameOrId}/`); if (!response || typeof response !== 'object') { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid project response from AWX API'); } return response; } const projects = await awxApiRequestAllItems.call(this, 'GET', '/projects/', { name: nameOrId }); if (!Array.isArray(projects)) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid projects response from AWX API'); } if (projects.length === 0) { if (throwOnNotFound) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Project '${nameOrId}' not found`); } return null; } const project = projects[0]; if (!project.id || typeof project.name !== 'string') { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid project data received from AWX API'); } return project; } catch (error) { if (error.statusCode === 404 && !throwOnNotFound) { return null; } throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to get project: ${error.message}`); } } async function getWorkflowJobTemplate(nameOrId, throwOnNotFound = true) { if (/^\d+$/.test(nameOrId)) { try { const response = await awxApiRequest.call(this, 'GET', `/workflow_job_templates/${nameOrId}/`); return response; } catch (error) { if (error.statusCode === 404 && !throwOnNotFound) return null; throw error; } } const templates = await awxApiRequestAllItems.call(this, 'GET', '/workflow_job_templates/', {}, { name: nameOrId }); if (templates.length === 0) { if (throwOnNotFound) throw new Error(`Workflow Job Template "${nameOrId}" not found`); return null; } return templates[0]; } async function createProject(data) { const body = { ...data }; return await awxApiRequest.call(this, 'POST', '/projects/', body); } async function updateProject(id, data) { const body = { ...data }; return await awxApiRequest.call(this, 'PATCH', `/projects/${id}/`, body); } async function deleteProject(id) { await awxApiRequest.call(this, 'DELETE', `/projects/${id}/`); } async function createJobTemplate(data) { const body = { ...data }; return await awxApiRequest.call(this, 'POST', '/job_templates/', body); } async function updateJobTemplate(id, data) { const body = { ...data }; return await awxApiRequest.call(this, 'PATCH', `/job_templates/${id}/`, body); } async function deleteJobTemplate(id) { await awxApiRequest.call(this, 'DELETE', `/job_templates/${id}/`); } async function createWorkflowJobTemplate(data) { const body = { ...data }; return await awxApiRequest.call(this, 'POST', '/workflow_job_templates/', body); } async function updateWorkflowJobTemplate(id, data) { const body = { ...data }; return await awxApiRequest.call(this, 'PATCH', `/workflow_job_templates/${id}/`, body); } async function deleteWorkflowJobTemplate(id) { await awxApiRequest.call(this, 'DELETE', `/workflow_job_templates/${id}/`); } async function resolveResourceId(resourceType, id, name, cache) { console.log('==================== RESOLVE RESOURCE ID ===================='); console.log(`Resolving resource type: ${resourceType}`); console.log(`ID: ${id}, Name: ${name}`); if (id) { console.log(`Using provided ID: ${id}`); console.log('==========================================================='); return id; } if (!name || name.trim() === '') { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Empty ${resourceType} name provided`, { itemIndex: 0 }); } const commonPlaceholders = ['id', 'groupId', 'hostId', 'inventoryId', 'name', 'groupName', 'hostName', 'inventoryName']; if (commonPlaceholders.includes(name)) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid ${resourceType} name: "${name}". This appears to be a placeholder or parameter name, not an actual resource name.`, { itemIndex: 0 }); } const cacheKey = `${resourceType}:${name}`; if (cache && cache.has(cacheKey)) { console.log(`Found in cache: ${cacheKey} -> ${cache.get(cacheKey)}`); console.log('==========================================================='); return cache.get(cacheKey); } const endpointMap = { job_template: '/api/v2/job_templates/', job_templates: '/api/v2/job_templates/', workflow_job_template: '/api/v2/workflow_job_templates/', workflow_job_templates: '/api/v2/workflow_job_templates/', project: '/api/v2/projects/', projects: '/api/v2/projects/', inventory: '/api/v2/inventories/', inventories: '/api/v2/inventories/', host: '/api/v2/hosts/', hosts: '/api/v2/hosts/', group: '/api/v2/groups/', groups: '/api/v2/groups/', }; console.log(`Endpoint map: ${JSON.stringify(endpointMap)}`); const endpoint = endpointMap[resourceType]; if (!endpoint) throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown resource type: ${resourceType}`, { itemIndex: 0 }); const queryUrl = `${endpoint}?name=${encodeURIComponent(name)}`; console.log(`Constructed query URL: ${queryUrl}`); try { const results = await awxApiRequest.call(this, 'GET', queryUrl); console.log(`API response: ${JSON.stringify(results)}`); if (!(results === null || results === void 0 ? void 0 : results.results) || results.results.length === 0) { console.log(`No ${resourceType} found with name '${name}'`); console.log('==========================================================='); throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No ${resourceType} found with name '${name}'`, { itemIndex: 0 }); } console.log(`Found ${results.results.length} potential matches for ${resourceType} '${name}'`); results.results.forEach((result, index) => { console.log(`Match ${index + 1}: id=${result.id}, name='${result.name}', exact match: ${result.name === name}`); if (typeof result.name === 'string') { console.log(` Lowercase comparison: '${result.name.toLowerCase()}' vs '${name.toLowerCase()}', match: ${result.name.toLowerCase() === name.toLowerCase()}`); console.log(` Trimmed comparison: '${result.name.trim()}' vs '${name.trim()}', match: ${result.name.trim() === name.trim()}`); console.log(` Trimmed lowercase: '${result.name.trim().toLowerCase()}' vs '${name.trim().toLowerCase()}', match: ${result.name.trim().toLowerCase() === name.trim().toLowerCase()}`); } }); const exactMatch = results.results.find(item => item.name === name); if (exactMatch) { console.log(`Found exact match for '${name}' with ID ${exactMatch.id}`); const foundId = exactMatch.id; if (cache) cache.set(cacheKey, foundId); console.log(`Found resource ID: ${foundId}`); console.log('==========================================================='); return foundId; } const caseInsensitiveMatch = results.results.find(item => typeof item.name === 'string' && item.name.toLowerCase() === name.toLowerCase()); if (caseInsensitiveMatch) { console.log(`Found case-insensitive match for '${name}' with ID ${caseInsensitiveMatch.id}`); const foundId = caseInsensitiveMatch.id; if (cache) cache.set(cacheKey, foundId); console.log(`Found resource ID: ${foundId}`); console.log('==========================================================='); return foundId; } const trimmedCaseInsensitiveMatch = results.results.find(item => typeof item.name === 'string' && item.name.trim().toLowerCase() === name.trim().toLowerCase()); if (trimmedCaseInsensitiveMatch) { console.log(`Found trimmed case-insensitive match for '${name}' with ID ${trimmedCaseInsensitiveMatch.id}`); const foundId = trimmedCaseInsensitiveMatch.id; if (cache) cache.set(cacheKey, foundId); console.log(`Found resource ID: ${foundId}`); console.log('==========================================================='); return foundId; } const availableNames = results.results .map(res => `"${res.name}"${res.id ? ` (ID: ${res.id})` : ''}`) .join(', '); throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No exact match found for ${resourceType} with name "${name}". Available options: ${availableNames}`, { itemIndex: 0 }); } catch (error) { console.log(`Error resolving resource: ${error.message}`); console.log('==========================================================='); if (error instanceof n8n_workflow_1.NodeOperationError) { throw error; } else { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error resolving ${resourceType} ID: ${error.message}`, { itemIndex: 0 }); } } } async function postAwxResource(resourcePath, body = {}, options = { timeout: null, waitForCompletion: null, pollInterval: null, maxRetries: null, forceRefresh: null, }) { const response = await awxApiRequest.call(this, 'POST', resourcePath, body, {}, options); return response; } function parseAwxVariables(variablesString) { if (!variablesString) { return {}; } try { if (typeof variablesString === 'string') { return JSON.parse(variablesString); } else if (typeof variablesString === 'object') { return variablesString; } return {}; } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid variables format. Please provide valid JSON.'); } } async function getResourceById(resourceType, id, name, itemIndex = 0) { try { return await resolveResourceId.call(this, resourceType, id, name); } catch (error) { if (error instanceof n8n_workflow_1.NodeOperationError) { throw error; } else { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error resolving ${resourceType} ID: ${error.message}`, { itemIndex }); } } } async function getInventoryById(id, name, itemIndex = 0) { return getResourceById.call(this, 'inventory', id, name, itemIndex); } async function getGroupById(id, name, itemIndex = 0) { return getResourceById.call(this, 'group', id, name, itemIndex); } async function getHostById(id, name, itemIndex = 0) { return getResourceById.call(this, 'host', id, name, itemIndex); } async function getProjectById(id, name, itemIndex = 0) { return getResourceById.call(this, 'project', id, name, itemIndex); } async function getJobTemplateById(id, name, itemIndex = 0) { return getResourceById.call(this, 'job_template', id, name, itemIndex); } async function getWorkflowTemplateById(id, name, itemIndex = 0) { return getResourceById.call(this, 'workflow_job_template', id, name, itemIndex); } //# sourceMappingURL=GenericFunctions.js.map