@eu4ia/node-red-contrib-awx
Version:
Node to launch playbooks in awx
291 lines (242 loc) • 11.2 kB
JavaScript
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)
}