372 lines
7.8 KiB
Lua
372 lines
7.8 KiB
Lua
|
|
local Ansible = {}
|
|
|
|
local io = require("io")
|
|
local json = require("dkjson")
|
|
|
|
Ansible.__index = Ansible
|
|
|
|
function Ansible.new(spec)
|
|
local self = setmetatable({}, Ansible)
|
|
self.spec = spec
|
|
for k,v in pairs(spec) do
|
|
v['name'] = k
|
|
end
|
|
self.params = nil
|
|
return self
|
|
end
|
|
|
|
local function split(str, delimiter)
|
|
local toks = {}
|
|
|
|
for tok in string.gmatch(str, "[^".. delimiter .. "]+") do
|
|
toks[#toks + 1] = tok
|
|
end
|
|
|
|
return toks
|
|
end
|
|
|
|
local function append(t1, t2)
|
|
for k,v in ipairs(t2) do
|
|
t1[#t1 + k] = v
|
|
end
|
|
return t1
|
|
end
|
|
|
|
function Ansible.contains(needle, haystack)
|
|
for _,v in pairs(haystack) do
|
|
if needle == v then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
local function findspec(name, spec)
|
|
if spec[name] then
|
|
return spec[name]
|
|
end
|
|
|
|
-- check whether an alias exists
|
|
for k,v in pairs(spec) do
|
|
if type(v) == "table" and v['aliases'] then
|
|
if Ansible.contains(name, v['aliases']) then
|
|
return v
|
|
end
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function canonicalize(params, spec)
|
|
local copy = {}
|
|
for k,v in pairs(params) do
|
|
local desc = findspec(k, spec)
|
|
if not desc then
|
|
-- ignore _ansible parameters
|
|
if 1 ~= string.find(k, "_ansible") then
|
|
return nil, "no such parameter " .. k
|
|
end
|
|
else
|
|
if copy[desc['name']] then
|
|
return nil, "duplicate parameter " .. desc['name']
|
|
end
|
|
copy[desc['name']] = v
|
|
end
|
|
end
|
|
|
|
params = copy
|
|
|
|
return copy
|
|
end
|
|
|
|
function Ansible:slurp(path)
|
|
local f, err = io.open(path, "r")
|
|
if f == nil then
|
|
Ansible.fail_json({msg="failed to open file " .. path .. ": " .. err})
|
|
end
|
|
local content = f:read("*a")
|
|
if content == nil then
|
|
self:fail_json({msg="read from file " .. path .. "failed"})
|
|
end
|
|
f:close()
|
|
return content
|
|
end
|
|
|
|
local function parse_dict_from_string(str)
|
|
if 1 == string.find(str, "{") then
|
|
-- assume json, try to decode it
|
|
local dict, pos, err = json.decode(str)
|
|
if not err then
|
|
return dict
|
|
end
|
|
elseif string.find(str, "=") then
|
|
fields = {}
|
|
field_buffer = ""
|
|
in_quote = nil
|
|
in_escape = false
|
|
for c in str:gmatch(".") do
|
|
if in_escape then
|
|
field_buffer = field_buffer .. c
|
|
in_escape = false
|
|
elseif c == '\\' then
|
|
in_escape = true
|
|
elseif not in_quote and ('\'' == c or '"' == c) then
|
|
in_quote = c
|
|
elseif in_quote and in_quote == c then
|
|
in_quote = nil
|
|
elseif not in_quote and (',' == c or ' ' == c) then
|
|
if string.len(field_buffer) > 0 then
|
|
fields[#fields + 1] = field_buffer
|
|
end
|
|
field_buffer=""
|
|
else
|
|
field_buffer = field_buffer .. c
|
|
end
|
|
end
|
|
-- append the final field
|
|
fields[#fields + 1] = field_buffer
|
|
|
|
local dict = {}
|
|
|
|
for _,v in ipairs(fields) do
|
|
local key, val = string.match(v, "^([^=]+)=(.*)")
|
|
|
|
if key and val then
|
|
dict[key] = val
|
|
end
|
|
end
|
|
|
|
return dict
|
|
end
|
|
|
|
return nil, str .. " dictionary requested, could not parse JSON or key=value"
|
|
end
|
|
|
|
local function check_transform_type(variable, ansibletype)
|
|
-- Types: str list dict bool int float path raw jsonarg
|
|
if "str" == ansibletype then
|
|
if type(variable) == "string" then
|
|
return variable
|
|
end
|
|
elseif "list" == ansibletype then
|
|
if type(variable) == "table" then
|
|
return variable
|
|
end
|
|
|
|
if type(variable) == "string" then
|
|
return split(variable, ",")
|
|
elseif type(variable) == "number" then
|
|
return {variable}
|
|
end
|
|
elseif "dict" == ansibletype then
|
|
if type(variable) == "table" then
|
|
return variable
|
|
elseif type(variable) == "string" then
|
|
return parse_dict_from_string(variable)
|
|
end
|
|
elseif "bool" == ansibletype then
|
|
if "boolean" == type(variable) then
|
|
return variable
|
|
elseif "number" == type(variable) then
|
|
return not (0 == variable)
|
|
elseif "string" == type(variable) then
|
|
local BOOLEANS_TRUE = {'yes', 'on', '1', 'true', 'True'}
|
|
local BOOLEANS_FALSE = {'no', 'off', '0', 'false', 'False'}
|
|
|
|
if Ansible.contains(variable, BOOLEANS_TRUE) then
|
|
return true
|
|
elseif Ansible.contains(variable, BOOLEANS_FALSE) then
|
|
return false
|
|
end
|
|
end
|
|
elseif "int" == ansibletype or "float" == ansibletype then
|
|
if type(variable) == "string" then
|
|
local var = tonumber(variable)
|
|
if var then
|
|
return var
|
|
end
|
|
elseif type(variable) == "number" then
|
|
return variable
|
|
end
|
|
elseif "path" == ansibletype then
|
|
-- A bit basic, i know
|
|
if type(variable) == "string" then
|
|
return variable
|
|
end
|
|
elseif "raw" == ansibletype then
|
|
return variable
|
|
elseif "jsonarg" == ansibletype then
|
|
if "table" == type(variable) then
|
|
return variable
|
|
elseif "string" == type(variable) then
|
|
local dict, pos, err = json.decode(variable)
|
|
if not err then
|
|
return dict
|
|
end
|
|
end
|
|
else
|
|
return nil, ansibletype .. " is not a known type"
|
|
end
|
|
|
|
return nil, tostring(variable) .. " does not conform to type " .. ansibletype
|
|
end
|
|
|
|
function Ansible:parse(inputfile)
|
|
local params, pos, err = json.decode(self:slurp(inputfile))
|
|
|
|
if err then
|
|
self:fail_json({msg="INTERNAL: Illegal json input received"})
|
|
end
|
|
|
|
-- resolve aliases
|
|
params, err = canonicalize(params, self.spec)
|
|
|
|
if not params then
|
|
self:fail_json({msg="Err: " .. tostring(err)})
|
|
end
|
|
|
|
for k,v in pairs(self.spec) do
|
|
-- setup defaults
|
|
if v['default'] then
|
|
if nil == params[k] then
|
|
params[k] = v['default']
|
|
end
|
|
end
|
|
|
|
-- assert requires
|
|
if v['required'] then
|
|
if not params[k] then
|
|
self:fail_json({msg="Required parameter " .. k .. " not provided"})
|
|
end
|
|
end
|
|
end
|
|
|
|
-- check types/choices
|
|
for k,v in pairs(params) do
|
|
local typedesc = self.spec[k]['type']
|
|
if typedesc then
|
|
local val, err = check_transform_type(v, typedesc)
|
|
if nil ~= val then
|
|
params[k] = val
|
|
else
|
|
self:fail_json({msg="Err: " .. tostring(err)})
|
|
end
|
|
end
|
|
|
|
local choices = self.spec[k]['choices']
|
|
if choices then
|
|
if not Ansible.contains(v, choices) then
|
|
self:fail_json({msg=v .. " not a valid choice for " .. k})
|
|
end
|
|
end
|
|
end
|
|
|
|
self.params = params
|
|
|
|
return params
|
|
end
|
|
|
|
local function file_exists(path)
|
|
local f=io.open(path,"r")
|
|
if f~=nil then
|
|
io.close(f)
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
function Ansible:get_bin_path(name, required, candidates)
|
|
if not candidates then
|
|
candidates = {}
|
|
end
|
|
|
|
local path = os.getenv("PATH")
|
|
if path then
|
|
candidates = append(candidates, split(path, ":"))
|
|
end
|
|
|
|
for _,dir in pairs(candidates) do
|
|
local fpath = dir .. "/" .. name
|
|
if file_exists(fpath) then
|
|
return fpath
|
|
end
|
|
end
|
|
|
|
if required then
|
|
self:fail_json({msg="No executable " .. name .. " found in PATH or candidates"})
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
function Ansible:remove_file(path)
|
|
local rc, err = os.remove(path)
|
|
if nil == rc then
|
|
self.fail_json({msg="Internal, execute: failed to remove file " .. path})
|
|
end
|
|
return rc
|
|
end
|
|
|
|
local function get_version()
|
|
local version = assert(string.match(_VERSION, "Lua (%d+.%d+)"))
|
|
return tonumber(version) -- Aaaah, it hurts to use floating point like this...
|
|
end
|
|
|
|
function Ansible:run_command(command)
|
|
local stdout = os.tmpname()
|
|
local stderr = os.tmpname()
|
|
|
|
local cmd = string.format("%s >%q 2>%q", command, stdout, stderr)
|
|
|
|
local rc = nil
|
|
if 5.1 < get_version() then
|
|
_, _, rc = os.execute(cmd)
|
|
else
|
|
rc = os.execute(cmd)
|
|
end
|
|
|
|
local out = self:slurp(stdout)
|
|
local err = self:slurp(stderr)
|
|
|
|
self:remove_file(stdout)
|
|
self:remove_file(stderr)
|
|
|
|
return rc, out, err
|
|
end
|
|
|
|
function Ansible:fail_json(kwargs)
|
|
assert(kwargs['msg'])
|
|
kwargs['failed'] = true
|
|
if nil == kwargs['changed'] then
|
|
kwargs['changed'] = false
|
|
end
|
|
if nil == kwargs['invocation'] then
|
|
kwargs['invocations'] = {module_args=self.params}
|
|
end
|
|
|
|
io.write(json.encode(kwargs))
|
|
os.exit(1)
|
|
end
|
|
|
|
function Ansible:exit_json(kwargs)
|
|
assert(kwargs['msg'])
|
|
if nil == kwargs['changed'] then
|
|
kwargs['changed'] = false
|
|
end
|
|
if nil == kwargs['invocation'] then
|
|
kwargs['invocations'] = {module_args=self:get_params()}
|
|
end
|
|
|
|
io.write(json.encode(kwargs))
|
|
os.exit(0)
|
|
end
|
|
|
|
function Ansible:get_params()
|
|
return self.params
|
|
end
|
|
|
|
return Ansible
|