UNPKG

@eu4ia/node-red-contrib-awx

Version:

Node to launch playbooks in awx

291 lines (242 loc) 11.2 kB
module.exports = function (RED) { const axios = require("axios") const httpsProxyAgent = require('https-proxy-agent') const https = require("https") let axiosInstance = axios function getProxyConfig (n, url) { var prox, noprox; if (process.env.http_proxy) { prox = process.env.http_proxy; } if (process.env.HTTP_PROXY) { prox = process.env.HTTP_PROXY; } if (process.env.no_proxy) { noprox = process.env.no_proxy.split(","); } if (process.env.NO_PROXY) { noprox = process.env.NO_PROXY.split(","); } var proxyNode; if (n.proxy) { proxyNode = RED.nodes.getNode(n.proxy); prox = proxyNode.url; noprox = proxyNode.noproxy; } var matchWithNoProxy; if (noprox) { for (var i = 0; i < noprox.length; i += 1) { if (url.indexOf(noprox[i]) !== -1) { matchWithNoProxy = true; } } } var proxyConf; if (prox && !matchWithNoProxy) { let re = /^(http:\/\/)?(.+)?:([0-9]+)?/i var match = prox.match(re); if (match) { proxyConf = { host: match[2], port: match[3] || 80 } } else { node.warn("Bad proxy url: " + prox); } } if (proxyConf && proxyNode && proxyNode.credentials) { var proxyUsername = proxyNode.credentials.username || ''; var proxyPassword = proxyNode.credentials.password || ''; if (proxyUsername && proxyPassword) { proxyConf.auth = { username: proxyUsername, password: proxyPassword } } } return proxyConf; } function delay(t, v) { return new Promise(function(resolve) { setTimeout(resolve.bind(null, v), t) }); } function startPolling(jobId, requestConf, node, timing) { let initialWait = timing.initialWait * 1000 let interval = timing.interval * 1000 let timeout = timing.timeout * 60 * 1000 let endTime = Number(new Date()) + timeout let poller = function(resolve,reject) { delay(initialWait) .then(() => { return doPolling(jobId, requestConf, node, interval, endTime); }) .then(data => { resolve(data) }) .catch(err => { reject(err) }) } return new Promise(poller); } function doPolling (jobId, requestConf, node, interval, endTime) { let checkCondition = function (resolve, reject) { node.status({ fill: "blue", shape: "ring", text: "polling" }); let pollFn = pollJob(requestConf); pollFn.then(function (response) { if (response.data.finished) { resolve(response.data) } else if (Number(new Date()) < endTime) { node.status({ fill: "blue", shape: "dot", text: `job ${jobId} ${response.data.status}` }); setTimeout(checkCondition, interval, resolve, reject); } else { node.status({ fill: "red", shape: "dot", text: `job ${jobId} timed out` }); reject(new Error(`job ${jobId} timed out`)) } }); } return new Promise(checkCondition); } function pollJob (requestConf) { return axiosInstance.request(requestConf); } function AwxLaunchNode (n) { RED.nodes.createNode(this, n) var node = this var advanced = n.advanced var timings = { //in seconds initialWait: 10, interval: 5, timeout: 5 } if (advanced) { timings.initialWait = n.initialWait timings.interval = n.interval timings.timeout = n.timeout } var awxService = RED.nodes.getNode(n.awxService) var protocol = awxService.useTLS ? "https" : "http" var baseURL = protocol + `${awxService.host}/api/v2` node.on('input', function (msg, nodeSend, nodeDone) { node.status({ fill: "blue", shape: "dot", text: "launching job" }); // node-red 0.x compatibility nodeSend = nodeSend || function () { node.send.apply(node, arguments) } var jobTemplate = RED.util.evaluateNodeProperty(n.jobTemplate, n.jobTemplateType, node, msg) const baseConf = { baseURL: `${protocol}://${awxService.host}/api/v2`, headers: { "Authorization": `Bearer ${awxService.token}`, "Content-Type": "application/json" }, proxy: false } let proxyConf = getProxyConfig(n, baseConf.baseURL); if (proxyConf) { proxyConf.rejectUnauthorized = awxService.rejectUnauthorized const httpsAgent = new httpsProxyAgent(proxyConf) axiosInstance = axios.create({httpsAgent}) } else { baseConf.httpsAgent = new https.Agent({ "rejectUnauthorized": awxService.rejectUnauthorized }) } let infoConf = { method: "get", url: `/job_templates/${jobTemplate}/launch/` } infoConf = { ...baseConf, ...infoConf } let payloadIsObject = typeof msg.payload === 'object' && msg.payload !== null let launchConf = { method: "post", url: `/job_templates/${jobTemplate}/launch/`, } launchConf = { ...baseConf, ...launchConf } if (payloadIsObject) { launchConf.data = msg.payload } else { launchConf.data = {} } axiosInstance.request(infoConf) .then(function (response) { asks = { "limit": "ask_limit_on_launch", "job_tags": "ask_tags_on_launch", "skip_tags": "ask_skip_tags_on_launch", "extra_vars": "ask_variables_on_launch", "inventory": "ask_inventory_on_launch", // "credential": "ask_credential_on_launch", "scm_branch": "ask_scm_branch_on_launch" } jobOptions = response.data willBeIgnored = [] if (payloadIsObject) { for (const key in asks) { if (key in msg.payload) { ask = asks[key] if (jobOptions[ask] == false) { node.warn(`${key} field is set, but will be ignored by this job template. To avoid unexpected results, this job will not be launched`) willBeIgnored.push(key) } } } if (willBeIgnored.length > 0) { node.status({ fill: "red", shape: "dot", text: "error" }); errMsg = `Job was not launched because the following fields would have been ignored: ${willBeIgnored.join(',')}` throw new Error(errMsg); } } console.log("checked the requirements, doing the rest now...") //call actual launch request return axiosInstance.request(launchConf) }) .then(function (response) { if (response.status == 201) { jobId = response.data.job node.status({ fill: "blue", shape: "dot", text: `job ${jobId} launched...` }); let ignored = Object.keys(response.data.ignored_fields) if (ignored.length > 0) { node.warn(`these fields were ignored when launching the job: ${ignored.join(', ')}`) } let pollConf = { method: "get", url: `/jobs/${jobId}` } pollConf = { ...baseConf, ...pollConf } let summaryConf = { method: "get", url: `/jobs/${jobId}/job_host_summaries/` } summaryConf = { ...baseConf, ...summaryConf } startPolling(jobId, pollConf, node, timings) .then(data => { msg.payload = data return axiosInstance.request(summaryConf) }) .then(response => { if (response.status == 200) { msg.payload.related.job_host_summaries = response.data } if (msg.payload.failed) { node.status({ fill: "yellow", shape: "dot", text: `job ${jobId} ${msg.payload.status}` }); nodeSend([null, msg]); } else { node.status({ fill: "green", shape: "dot", text: `job ${jobId} ${msg.payload.status}` }); nodeSend([msg, null]); } }) .catch( error => { node.status({ fill: "red", shape: "dot", text: error.message }); nodeDone(`job ${jobId} timed out`) }) } else { node.status({ fill: "red", shape: "dot", text: "error" }); msg.payload = response nodeSend([null, msg]); nodeDone(); } }) .catch(function (error) { node.status({ fill: "red", shape: "dot", text: "error" }); if (error.response) { console.log(error.response) nodeDone(error.message) } else if (error.request) { console.log(error.request) nodeDone(error.message) } else { nodeDone(error.message) } }) }) } RED.nodes.registerType("awx-launch", AwxLaunchNode) }