n8n-nodes-awx
Version:
n8n node to interact with Ansible AWX/Tower with improved type safety
544 lines • 23.9 kB
JavaScript
;
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